Как в Unity запустить обход в глубину параллельно?

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

Далее в коде GameState, Move, PlayerColor не наследуют MonoBehavior и вообще изолированы от логики Unity.

Класс Player имеет наследников Human, Bot. Метод Player.GetTopMoves(GameState gameState) возвращает список некоторых выбранных ходов. Стратегия выбора ходов у каждого наследника своя:

Human - это, по сути, пользователь, и выбор хода эквивалентен тому, что пользователь сам выбирает ход нажатием мыши по некоторым объектам.

Bot - выбирает ходы обходом в глубину дерева возможных позиций

В следующем коде, очевидно, метод StartGame абсолютно непригоден для использования в Unity, так как

  1. сам метод работает пока не закончится вся настольная игра (скорее всего его нужно будет переписать как корутину?)
  2. метод выбора хода тоже занимает большое время (человек долго думает над ходом и бот долго считает)
public class Game : MonoBehaviour
{
    [SerializeField] private BoardViewer _boardViewer;

    private GameState _gameState;
    private Dictionary<PlayerColor, Player> _players;
    private Player _playerToMove;


    public void StartGame(Player whitePlayer, Player blackPlayer)
    {
        Dictionary<PlayerColor, Player> players = new Dictionary<PlayerColor, Player>
        {
            { PlayerColor.White, whitePlayer },
            { PlayerColor.Black, blackPlayer },
        };
        _gameState = GameState.NewGame;
        

        while (_gameState.IsOver() == false)
        {
            _playerToMove = _players[_gameState.PlayerColorToMove];
            
            // логика выбора хода зависит от типа Игрока
            Move bestMove = _playerToMove.GetTopMoves(_gameState)[0];

            _boardViewer.SpawnStone(...);
            _gameState = _gameState.ApplyMove(bestMove);
        }
    }
}

Таким образом, я бы хотел адоптировать код выше для использования в проекте Unity, но абсолютно не имею понятия как это можно сделать. Не хотел бы запускать выбор хода корутиной (так как где-то вычитал, что тяжелые вычисления для этого менее пригодны), но тогда и Thread не подходит, так как Player (точнее его наследник Human) наследует MonoBehavior (насколько я знаю, это потоко небезопасно).

Возможно стоит забить на общий класс Player и вместо Game сделать две версии PvP и PvE? Спасибо


Ответы (1 шт):

Автор решения: aepot

Есть же методы Update и FixedUpdate, здесь по сути ничего изобретать не надо. Это вполне себе заменяет цикл while. Если нужна теория для разобраться - почитайте про шаблон проектирования "конечный автомат" или еще его называют "машина состояний".

К примеру, возмем цикл, умножающий число на 2 десять раз, для наглядности буду использовать тот же while

int i = 0;
int number = 1;
while (i < 10)
{
    number *= 2;
    i++;
}
Console.WriteLine(number);

Выведем саму операцию в отдельный метод.

public void Multiply(int number, int count)
{
    int i = 0;
    while (i < count)
    {
        number *= 2;
        i++;
    }
    return number;
}

Тогда это вызвать можно будет так

int number = Multiply(1, 10);
Console.WriteLine(number);

Вот по сути получился метод, содержащий всю логику внутри.

Этот цикл не завершится, пока не закончит считать. Когда надо еще что-то делать или делать несколько дел одновременно, вызывая итерации цикла по одной, используют конечный автомат.

Вынесем логику в отдельный класс. Переименую i в state

class CounterMachine
{
    private int _state;
    private int _number;
    private readonly int _count;

    public bool Finished { get; private set; }

    public CounterMachine(int number, int count)
    {
        _number = number;
        _count = count;
    }

    public void Update()
    {
        if (_state < _count)
        {
            _number *= 2;
            _state++;
        }
        else if (_state == _count)
        {
            Console.WriteLine(_number);
            _state++;
            Finished = true;
        }
    }
}

Вот вся логика упакована в некий черный ящик.

Вызов этого монстра будет выглядеть так, что надо его создать и вызывать обновление, пока он не закончит работать.

var counterMachine = new CounterMachine(1, 10);
while (!counterMachine.Finished)
    counterMachine.Update();

А теперь представьте, что вот это создание класса и вызовы Update будет делать Unity за вас, останется только написать саму машину. А сама машина - это и есть скрипт Unity. Единственное отличие в том, что эта машина бесконечная, будет выполняться, пока существует объект, к которому прицеплен скрипт.

public class Counter : MonoBehavior
{
    private int _state;
    private int _number = 1;
    private int _count = 10;
    private bool _finished;

    void Update()
    {
        if (_finished)
            return;

        if (_state < _count)
        {
            _number *= 2;
            _state++;
        }
        else if (_state == _count)
        {
            _finished = true;
            OnFinished();
        }
    }

    private void OnFinished()
    {
        // вывести _number
    }
}

Эта логика как бы ложится на игру. Внутри метода вы делаете одну итерацию обсчета, меняете состояние игры и поехали дальше по кругу.

→ Ссылка