Асинхронность (async/await) в C#: Полное руководство для Unity

Асинхронность

Асинхронность — это способ выполнять долгие операции (загрузка из интернета, чтение файла, запрос к базе данных) без заморозки игры.

Когда игрок нажимает «Загрузить профиль», игра не должна зависать на 2 секунды пока идет запрос к серверу.

Почему это важно? (Проблема синхронного кода)

❌ Синхронный код (ПЛОХО)

C#
public void LoadPlayerData()
{
    // ИГРА ЗАВИСАЕТ НА 2 СЕКУНДЫ!
    var data = DownloadDataFromServer(); 
    
    // Игрок не может двигать камерой, нажимать кнопки
    UpdateUI(data);
}

Результат: Игра «фризит», игрок думает что она сломалась, может закрыть приложение.

✅ Асинхронный код (ХОРОШО)

C#
public async void LoadPlayerData()
{
    // Игра ПРОДОЛЖАЕТ работать (60 FPS)
    var data = await DownloadDataFromServer(); 
    
    // Когда данные придут — обновляем UI
    UpdateUI(data);
}

Результат: Игра работает плавно, можно крутить камерой пока данные грузятся.

Базовый синтаксис

Ключевые слова

Ключевое слово
Описание
async
Ставится перед методом, разрешает использовать await внутри
await
«Жди результат, но не блокируй поток»
Task
Возвращаемое значение асинхронного метода (без результата)
Task<T>
Возвращаемое значение с результатом типа T

Пример

C#
// Синхронный метод (блокирует)
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

C#
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: Используем в игре

C#
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.

C#
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;
}

Параллельное выполнение (Когда нужно загрузить много данных)

Иногда нужно загрузить несколько вещей одновременно (профиль + инвентарь + друзья).

❌ Последовательно (медленно)

C#
var profile = await LoadProfile();    // 2 сек
var inventory = await LoadInventory(); // 2 сек
var friends = await LoadFriends();     // 2 сек
// Итого: 6 секунд

✅ Параллельно (быстро)

C#
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)

Если игрок закрыл окно загрузки, нет смысла ждать ответа сервера.

C#
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 из другого потока

C#
public async void LoadData()
{
    var data = await DownloadData();
    
    // Может вызвать ошибку!
    textComponent.text = data.name; 
}

✅ Правильно: Возврат на главный поток

C#
public async void LoadData()
{
    var data = await DownloadData();
    
    // После await код автоматически продолжается на главном потоке Unity
    textComponent.text = data.name; // Безопасно
}

Почему это работает: await в Unity по умолчанию возвращает выполнение в тот же SynchronizationContext (главный поток), откуда был вызван.

Практический пример

Вот полный класс для работы с PlayFab/API с асинхронностью:

C#
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 для вашей игры

  1. Показывайте индикатор загрузки — игрок должен видеть, что что-то происходит.
  2. Делайте таймауты — не ждите бесконечно (максимум 10-15 секунд).
  3. Кэшируйте данные — не загружайте одно и то же каждый раз.
  4. Авто-сохранение — сохраняйте прогресс каждые 1-2 минуты + при выходе.
  5. Очередь запросов — если игрок быстро кликает, не отправляйте 100 запросов, объединяйте их.
  6. Оффлайн-режим — если нет интернета, сохраняйте данные локально и синхронизируйте позже.
C#
// Пример кэширования
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 имеет свои асинхронные методы. Вот как их использовать:

C#
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 + ретраи

Каждое взаимодействие с сервером (покупка, постройка, сохранение) должно быть асинхронным, чтобы игра оставалась отзывчивой даже при плохом интернете.

Оцените статью
Unity Learn
Добавить комментарий