Асинхронность — это способ выполнять долгие операции (загрузка из интернета, чтение файла, запрос к базе данных) без заморозки игры.
Когда игрок нажимает «Загрузить профиль», игра не должна зависать на 2 секунды пока идет запрос к серверу.
- Почему это важно? (Проблема синхронного кода)
- ❌ Синхронный код (ПЛОХО)
- ✅ Асинхронный код (ХОРОШО)
- Базовый синтаксис
- Работа с UnityWebRequest (Загрузка из интернета)
- Шаг 1: Создаем Extension Method
- Шаг 2: Используем в игре
- Обработка ошибок (Try/Catch)
- Параллельное выполнение (Когда нужно загрузить много данных)
- ❌ Последовательно (медленно)
- ✅ Параллельно (быстро)
- Отмена операций (CancellationToken)
- Unity-специфика: Главный поток
- ❌ Ошибка: Изменение UI из другого потока
- ✅ Правильно: Возврат на главный поток
- Практический пример
- Частые ошибки и как их избежать
- Best Practices для вашей игры
- Асинхронность с PlayFab SDK
- Итог
Почему это важно? (Проблема синхронного кода)
❌ Синхронный код (ПЛОХО)
public void LoadPlayerData()
{
// ИГРА ЗАВИСАЕТ НА 2 СЕКУНДЫ!
var data = DownloadDataFromServer();
// Игрок не может двигать камерой, нажимать кнопки
UpdateUI(data);
}Результат: Игра «фризит», игрок думает что она сломалась, может закрыть приложение.
✅ Асинхронный код (ХОРОШО)
public async void LoadPlayerData()
{
// Игра ПРОДОЛЖАЕТ работать (60 FPS)
var data = await DownloadDataFromServer();
// Когда данные придут — обновляем UI
UpdateUI(data);
}Результат: Игра работает плавно, можно крутить камерой пока данные грузятся.
Базовый синтаксис
Ключевые слова
|
Ключевое слово
|
Описание
|
|---|---|
async |
Ставится перед методом, разрешает использовать
await внутри |
await |
«Жди результат, но не блокируй поток»
|
Task |
Возвращаемое значение асинхронного метода (без результата)
|
Task<T> |
Возвращаемое значение с результатом типа
T |
Пример
// Синхронный метод (блокирует)
public int GetData()
{
return 100;
}
// Асинхронный метод (не блокирует)
public async Task<int> GetDataAsync()
{
await Task.Delay(1000); // Ждем 1 секунду
return 100;
}
// Вызов асинхронного метода
public async void Start()
{
int value = await GetDataAsync();
Debug.Log(value);
}Важно: async void используйте только для методов событий (кнопки, Start, Update). В остальных случаях всегда возвращайте Task.
Работа с UnityWebRequest (Загрузка из интернета)
Unity не имеет встроенных async методов для UnityWebRequest, но их можно обернуть.
Шаг 1: Создаем Extension Method
using UnityEngine.Networking;
using System.Threading.Tasks;
public static class UnityWebRequestExtensions
{
public static async Task<UnityWebRequest> SendWebRequestAsync(this UnityWebRequest request)
{
request.SendWebRequest();
// Ждем завершения загрузки
while (!request.isDone)
{
await Task.Yield(); // Возвращаем управление Unity на 1 кадр
}
return request;
}
}Шаг 2: Используем в игре
public class PlayerDataManager : MonoBehaviour
{
private const string API_URL = "https://api.mygame.com/player/data";
public async void LoadPlayerData()
{
ShowLoadingSpinner(); // Показываем индикатор загрузки
try
{
using (UnityWebRequest request = UnityWebRequest.Get(API_URL))
{
await request.SendWebRequestAsync();
if (request.result == UnityWebRequest.Result.Success)
{
string json = request.downloadHandler.text;
PlayerData data = JsonUtility.FromJson<PlayerData>(json);
UpdateUI(data);
}
else
{
Debug.LogError($"Ошибка загрузки: {request.error}");
ShowError("Не удалось загрузить данные");
}
}
}
catch (System.Exception ex)
{
Debug.LogException(ex);
ShowError("Произошла непредвиденная ошибка");
}
finally
{
HideLoadingSpinner(); // Скрываем индикатор в любом случае
}
}
public async void SavePlayerData(PlayerData data)
{
string json = JsonUtility.ToJson(data);
using (UnityWebRequest request = UnityWebRequest.Put(API_URL, json))
{
request.method = "POST";
await request.SendWebRequestAsync();
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log("Данные сохранены!");
}
}
}
}Обработка ошибок (Try/Catch)
Асинхронные операции часто failятся (нет интернета, таймаут, ошибка сервера). Всегда оборачивайте в try/catch.
public async Task<PlayerData> LoadDataWithRetry(int maxRetries = 3)
{
for (int i = 0; i < maxRetries; i++)
{
try
{
return await FetchDataFromServer();
}
catch (Exception ex)
{
Debug.LogWarning($"Попытка {i + 1} не удалась: {ex.Message}");
if (i == maxRetries - 1) throw; // Последняя попытка
await Task.Delay(1000 * (i + 1)); // Ждем дольше с каждой попыткой
}
}
return null;
}Параллельное выполнение (Когда нужно загрузить много данных)
Иногда нужно загрузить несколько вещей одновременно (профиль + инвентарь + друзья).
❌ Последовательно (медленно)
var profile = await LoadProfile(); // 2 сек
var inventory = await LoadInventory(); // 2 сек
var friends = await LoadFriends(); // 2 сек
// Итого: 6 секунд✅ Параллельно (быстро)
var profileTask = LoadProfile();
var inventoryTask = LoadInventory();
var friendsTask = LoadFriends();
// Ждем завершения всех одновременно
await Task.WhenAll(profileTask, inventoryTask, friendsTask);
// Получаем результаты
var profile = await profileTask;
var inventory = await inventoryTask;
var friends = await friendsTask;
// Итого: ~2 секундыОтмена операций (CancellationToken)
Если игрок закрыл окно загрузки, нет смысла ждать ответа сервера.
private CancellationTokenSource _cts;
public async void StartLoading()
{
_cts = new CancellationTokenSource();
try
{
await LoadDataAsync(_cts.Token);
}
catch (OperationCanceledException)
{
Debug.Log("Загрузка отменена");
}
}
public void StopLoading()
{
_cts?.Cancel(); // Отменяем все ожидающие операции
}
public async Task LoadDataAsync(CancellationToken token)
{
using (UnityWebRequest request = UnityWebRequest.Get(API_URL))
{
request.SendWebRequest();
while (!request.isDone)
{
// Проверяем, не отменили ли загрузку
token.ThrowIfCancellationRequested();
await Task.Yield();
}
}
}Unity-специфика: Главный поток
Важное правило: В Unity почти все API (GameObject, Transform, UI) работают только на главном потоке.
❌ Ошибка: Изменение UI из другого потока
public async void LoadData()
{
var data = await DownloadData();
// Может вызвать ошибку!
textComponent.text = data.name;
}✅ Правильно: Возврат на главный поток
public async void LoadData()
{
var data = await DownloadData();
// После await код автоматически продолжается на главном потоке Unity
textComponent.text = data.name; // Безопасно
}Почему это работает: await в Unity по умолчанию возвращает выполнение в тот же SynchronizationContext (главный поток), откуда был вызван.
Практический пример
Вот полный класс для работы с PlayFab/API с асинхронностью:
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
[System.Serializable]
public class PlayerData
{
public string playerId;
public int gold;
public int level;
public BuildingData[] buildings;
}
public class DataService : MonoBehaviour
{
private const string API_URL = "https://api.mygame.com";
private CancellationTokenSource _cts;
private void OnDestroy()
{
_cts?.Cancel(); // Отменяем все операции при уничтожении
}
// === ЗАГРУЗКА ПРОФИЛЯ ===
public async Task<PlayerData> LoadPlayerProfile(string playerId)
{
_cts = new CancellationTokenSource();
try
{
string url = $"{API_URL}/player/{playerId}";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
await request.SendWebRequestAsync(_cts.Token);
if (request.result == UnityWebRequest.Result.Success)
{
return JsonUtility.FromJson<PlayerData>(request.downloadHandler.text);
}
else
{
throw new Exception($"HTTP Error: {request.responseCode}");
}
}
}
catch (OperationCanceledException)
{
Debug.Log("Загрузка профиля отменена");
return null;
}
catch (Exception ex)
{
Debug.LogError($"Ошибка загрузки: {ex.Message}");
throw;
}
}
// === СОХРАНЕНИЕ ПРОГРЕССА ===
public async Task<bool> SavePlayerProgress(PlayerData data)
{
try
{
string json = JsonUtility.ToJson(data);
string url = $"{API_URL}/player/save";
using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
{
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
await request.SendWebRequestAsync(_cts.Token);
return request.result == UnityWebRequest.Result.Success;
}
}
catch (Exception ex)
{
Debug.LogError($"Ошибка сохранения: {ex.Message}");
return false;
}
}
// === АВТО-СОХРАНЕНИЕ С ИНТЕРВАЛОМ ===
public async void StartAutoSave(PlayerData data, int intervalSeconds = 60)
{
while (true)
{
await Task.Delay(intervalSeconds * 1000, _cts.Token);
await SavePlayerProgress(data);
Debug.Log("Авто-сохранение выполнено");
}
}
}
// === Extension для поддержки CancellationToken ===
public static class UnityWebRequestExtensions
{
public static async Task<UnityWebRequest> SendWebRequestAsync(
this UnityWebRequest request,
CancellationToken token = default)
{
request.SendWebRequest();
while (!request.isDone)
{
token.ThrowIfCancellationRequested();
await Task.Yield();
}
return request;
}
}Частые ошибки и как их избежать
|
Ошибка
|
Проблема
|
Решение
|
|---|---|---|
async void везде |
Нельзя отловить ошибки, нельзя ждать завершения
|
Используйте
async Task |
|
Нет
try/catch |
Игра крашится при ошибке сети
|
Всегда оборачивайте в
try/catch |
|
Нет отмены
|
Загрузка продолжается после выхода из сцены
|
Используйте
CancellationToken |
|
Блокировка потока
|
Task.Result или Task.Wait() |
Всегда используйте
await |
|
Утечка памяти
|
UnityWebRequest не dispose |
Используйте
using блок |
|
UI обновляется не там
|
Изменение UI до завершения загрузки
|
Обновляйте UI после
await |
Best Practices для вашей игры
- Показывайте индикатор загрузки — игрок должен видеть, что что-то происходит.
- Делайте таймауты — не ждите бесконечно (максимум 10-15 секунд).
- Кэшируйте данные — не загружайте одно и то же каждый раз.
- Авто-сохранение — сохраняйте прогресс каждые 1-2 минуты + при выходе.
- Очередь запросов — если игрок быстро кликает, не отправляйте 100 запросов, объединяйте их.
- Оффлайн-режим — если нет интернета, сохраняйте данные локально и синхронизируйте позже.
// Пример кэширования
private PlayerData _cachedData;
private float _cacheTime;
private const float CACHE_DURATION = 300f; // 5 минут
public async Task<PlayerData> GetPlayerData(string id)
{
// Проверяем кэш
if (_cachedData != null && Time.time - _cacheTime < CACHE_DURATION)
{
return _cachedData;
}
// Загружаем с сервера
_cachedData = await LoadPlayerProfile(id);
_cacheTime = Time.time;
return _cachedData;
}Асинхронность с PlayFab SDK
PlayFab имеет свои асинхронные методы. Вот как их использовать:
using PlayFab;
using PlayFab.ClientModels;
using System.Threading.Tasks;
public class PlayFabService : MonoBehaviour
{
public async Task<PlayFabResult<GetPlayerDataResult>> GetDataAsync()
{
var tcs = new TaskCompletionSource<PlayFabResult<GetPlayerDataResult>>();
PlayFabClientAPI.GetPlayerData(new GetPlayerDataRequest(),
result => tcs.SetResult(new PlayFabResult<GetPlayerDataResult> { Result = result, IsError = false }),
error => tcs.SetResult(new PlayFabResult<GetPlayerDataResult> { Error = error, IsError = true })
);
return await tcs.Task;
}
}Совет: Для PlayFab проще использовать их встроенные колбэки, но обертка в Task делает код чище и единым с остальной асинхронностью.
Итог
|
Что
|
Зачем
|
Как
|
|---|---|---|
|
Загрузка данных
|
Не блокировать игру
|
await UnityWebRequest |
|
Сохранение прогресса
|
Надежность
|
async Task<bool> + try/catch |
|
Таймауты
|
Не ждать вечно
|
CancellationToken + Task.Delay |
|
Параллельность
|
Ускорить загрузку
|
Task.WhenAll |
|
Ошибки
|
Не крашить игру
|
try/catch + ретраи |
Каждое взаимодействие с сервером (покупка, постройка, сохранение) должно быть асинхронным, чтобы игра оставалась отзывчивой даже при плохом интернете.





