Захотелось поделиться чудесным процессом разработки, с которым недавно познакомился. Я раньше не видел такого подхода, и люди, как только с ним знакомятся, долго не могут понять и принять такой способ построения игр. И, если честно, я сам понял все не в первую неделю. Но после некоторого освоения я уже забыл как делать игры иначе. В планах написать цикл статей, но начнем с малого и постепенно будем наращивать понимание что и зачем и с чем это есть.

Как кое-кто уже мог догадаться, я сегодня расскажу про паттерн “Command” и как его использовать для разработки игр с использованием движка Unity 3D. Это один из ключевых паттернов в этом подходе. Код будет упрощенным, но рабочим и должен дать понимание процесса.

Пролог


Вы наверное когда-нибудь уже видели статьи в которых разработчики рассказывают как использовать Actor-ы в Unity? Если нет, то я сейчас быстро объясню суть на примере: в вашей игре есть десяток игровых персонажей, которые должны, например, по-разному прыгать. Конечно задачу можно решить через всеми любимый полиморфизм: сделать базового юнита и просто перегружать виртуальный метод Jump для каждого юнита.

Как-то так
public class UnitController : MonoBehaviour 
{
    public Rigidbody AttachedRigidbody;
    //...
    public virtual void Jump()
    {
	rigidbody.velocity = new Vector3 (0, 10, 0);
    }
    //...
}

public class RabitUnitController : UnitController 
{
    //...
    public override void Jump ()
    {
        //very high jump
    }
    //...
}


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

С помощью Actor-oв эта задача решается иначе. Применяя этот подход^ нужно было бы также написать класс юнита, только теперь вместо виртуального метода Jump написать ряд отдельных компонент UnitJumper и просто цеплять подходящую компоненту на правильного юнита. И в момент прыжка вызывать метод Jump на прикрепленном компоненте:

Код Actor-ов
public class UnitJumper : MonoBehaviour 
{
    public virtual void Jump(Rigidbody rigidbody)
    {}
}

public class RegularJumper : UnitJumper 
{
    public override void Jump (Rigidbody rigidbody)
    {
        base.Jump (rigidbody);
        rigidbody.velocity = new Vector3 (0, 10, 0);
    }
}

public class MajesticAFJumper : UnitJumper 
{
    public override void Jump (Rigidbody rigidbody)
    {
        base.Jump (rigidbody);
        rigidbody.velocity = new Vector3 (0, 15, 10);
        /*
         * some magic here
         */
    }
}


А таким стал контроллер
public class UnitController : MonoBehaviour 
{
    [SerializeField]
    private UnitJumper _unitJumper;
    public Rigidbody AttachedRigidbody;
    
    //...
    public virtual void Jump()
    {
        if (_unitJumper != null)
            _unitJumper.Jump (AttachedRigidbody);
        else
            Debug.Log("UnitJumper Component is missing");
    }
    //...
}


Теперь стало всё просто и красиво. Меньше проблем с иерархией, код прыжка вынесен в отдельный маленький класс, из-за чего его легко изменять. Каждый способ прыжка может иметь сколько-угодно параметров и вы будете точно уверены, что изменяя их вы не сломаете, к примеру, бег. Также модификация способа прыжка для юнита теперь тоже очень проста. Кроме того, сама среда наталкивает нас на следование такой архитектуре, а с помощью атрибута [RequireComponent()] с редактором можно возиться еще меньше. Сейчас вы должны спросить зачем я это все рассказываю и в чем связь. А значит пришло время для логического перехода к паттерну Command.

Логический переход


Мы уже отошли от того, чтобы писать весь код прыжка в нашем примере в один класс, но как быть если нужно чтобы юниты не только сами по себе по-разному прыгали, но и чтобы, например, могли изменять способ прыжка в зависимости от обстоятельств (сделать сальто, пробежать по стене)? Вот здесь-то нам и нужна будет команда.

Суть остается всё та же — вынести все элементарные действия в отдельные классы. Только теперь мы будем добавлять необходимый компонент на юнита непосредственно перед применением и это позволит нам когда-угодно изменять поведение юнита и не будет такой сильной связи как в случае с Actor-ами. Напишем небольшой базовый класс для команды, который пока будет только служить для вызова команды на заданном объекте.

Базовая команда
public class Command : MonoBehaviour 
{
    public static T ExecuteOn<T>(GameObject target)
        where T : Command
    {
        return target.AddComponent <T>();
    }

    private void Start()
    {
        OnStart ();
    }

    protected virtual void OnStart()
    {}
}


Выше изложенный код служит только для удобного добавления компоненты на объект, а метод OnStart() пока (но только пока) исключительно для intellisense.

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

Примерный класс прыжка
public class RegularJumpCommand : Command
{
    protected override void OnStart ()
    {
        base.OnStart ();
        gameObject.GetComponent <Rigidbody>().velocity = new Vector3(0, 10, 0);
    }
}


И теперь чтобы заставить юнита прыгнуть нам нужно будет только выполнить на нем команду:

Вызов команды
public class SomeController : MonoBehaviour 
{
    //don't forget to set this in editor
    public UnitController _targetUnit;

    private void Start()
    {
        if (_targetUnit != null)
        {
            Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject);
        }
    }
}


Первое, что бросается в глаза, — это то, что значения velocity у нас константы. Поэтому просто сделать прыжок чуть выше не получится. Раньше мы бы это решали передавая аргументы в метод прыжка, давайте и здесь так сделаем. Перепишем-ка нашу красивую команду:

Команда с аргументами
public class Command : MonoBehaviour 
{
    private object[] _args;

    public static T ExecuteOn<T>(GameObject target, object[] args = null)
        where T : Command
    {
        T result = target.AddComponent <T>();
        result._args = args;
        return result;
    }

    private void Start()
    {
            OnStart (_args);
    }

    protected virtual void OnStart(object[] args)
    {}
}


Теперь высоту и направление нашего прыжка можно менять передавая в команду аргументы (не забывайте приводить). Поскольку Start() вызывается немного позже создания объекта, то аргументы передадутся корректно в наш метод OnStart(object[] args).

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

Использование аргументов в команде
public class RegularJumpCommand : Command
{
    protected override void OnStart (object[] args)
    {
        base.OnStart (args);
        gameObject.GetComponent <Rigidbody> ().velocity = (Vector3)args [0];
    }
}


Немного сильнее изменится вызов команды:

Вызов команды с аргументами
public class SomeController : MonoBehaviour 
{
    // don't forget to set this in editor
    public UnitController _targetUnit;

    private void Start()
    {
        if (_targetUnit != null)
        {
            Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject,
                                                   new object[]{new Vector3(0, 10, 0)});
        }
    }
}


После проделанных манипуляций команды стали гибкими и отдельный класс теперь нужно будет только для сальто. Но для инициализации параметров нужно будет пользоваться только методом OnStart(object[] args).

Вторая проблема которая у нас еще осталась — это то что каждый раз при прыжке у нас будет вызываться дорогой метод GetComponent(). Чтобы это решить давайте вспомним, что еще с Actor-ов у нас остался контроллер, который держит ссылки на все важные компоненты и в команде будем у него просить все что нам нужно. Контроллер мы тоже можем передавать в аргументы и я предлагаю сделать это немного более формализовано. Напишем для команды дочерний класс с контроллером:

Команда с контроллером
public class CommandWithType<T> : Command
    where T : MonoBehaviour
{
    protected T Controller
    {
        get;
        private set;
    }

    protected override void OnStart (object[] args)
    {
        base.OnStart (args);
        Controller = args [0] as T;
    }
}


В самой команде после этого изменился только номер аргумента, который мы используем, но об этом тоже не забывайте. Зато появился удобный способ получить контроллер не прибегая к GetComponent(). И обязательно нужно вызывать base.OnStart (args), иначе мы не сможем пользоваться контроллером:

Использование контроллера
public class RegularJumpCommand : CommandWithType<UnitController>
{
    protected override void OnStart (object[] args)
    {
        base.OnStart (args);
        Controller.AttachedRigidbody.velocity = (Vector3)args [1];
    }
}


Вызов команды тоже стал немножко другим:
public class SomeController : MonoBehaviour 
{
    //don't forget to set this in editor
    public UnitController _targetUnit;

    private void Start()
    {
        if (_targetUnit != null)
        {
            Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject,
                                                   new object[]{_targetUnit ,new Vector3(0, 10, 0)});
        }
    }
}


Теперь все стало совсем хорошо: у нас есть команды, которые могут обойтись без контроллера (показать рекламу, запостить что-то где-то) и команды которым нужен контроллер (идти, бежать, лететь). Команды с контроллером заточены под работу с семьей классов и не будут доступны для других семей, что вносит дополнительную упорядоченность. А так же у нас еще остались плюсы Actor-в. А еще вы не могли не заметить насколько они маленькие и аккуратные. Контроллеры от этого тоже только выиграли: стали такими-же лаконичными вместилищами ссылок на нужные нам компоненты (позже мы, конечно же, дадим им больше веса). Но это все еще только начало и с таким функционалом мы не очень далеко уедем.

Чтобы не уходить пока далеко от прыжка, давайте посмотрим что мы еще пропустили. Доведем до логического завершения этот неуклюжий, но полезный пример.

Первое, что бросается в глаза: после десяти прыжков на объекте будет висеть десять абсолютно бесполезных команд. Второе, что нужно заметить, — это то, что velocity изменяется в старте и это не гарантирует корректную работу физического движка.

Сосредоточимся пока на первом, поди второе и само решится, так ведь бывает. Самое логичное чтобы команда за собой сама убирала, как только напакостила. Добавим функционал завершения команды и два флага, чтобы знать завершила ли команда свое выполнение (об этом немного позже). И после небольших преобразований команда эволюционирует (но это еще даже не последняя ее форма).

Уборка команды за собой
public class Command : MonoBehaviour 
{
    private object[] _args;

    private bool _started = false;
    private bool _isReleased = false;

    public bool IsRunning
    {
        get{ return _started && !_isReleased;}
    }

    public static T ExecuteOn<T>(GameObject target, object[] args = null)
        where T : Command
    {
        T result = target.AddComponent <T>();
        result._args = args;
        return result;
    }

    private void Start()
    {
        _started = true;
        OnStart (_args);
    }

    protected virtual void OnStart(object[] args)
    {}

    private void OnDestroy()
    {
        if (!_isReleased)
            OnReleaseResources ();
    }

    protected virtual void OnReleaseResources()
    {
        _isReleased = true;
    }

    protected void FinishCommand()
    {
        OnReleaseResources ();
        Destroy (this, 1f);
    }

    protected virtual void OnFinishCommand(){}
}


Теперь, в нужный момент, команда, как порядочный гражданин и член социума самоуничтожится, стоит только вызвать метод FinishCommand() после всех необходимых манипуляций. Destroy() у нас слегка с задержкой, чтобы все, кому надо, могли воспользоваться командой перед исчезновением (взять из нее данные, но об этом позже), а флаг IsRunning нужен самой команде, чтобы она не начала работать раньше времени и не продолжала после завершения. Все отписки от событий и освобождение ресурсов можно легко сделать в OnReleaseResources() или в OnFinishCommand(). И не надо бояться что случайно напишешь OnDestory() и будешь долго мучатся (как я когда-то).

Теперь с этим всем мы сможем решить и вторую проблему:

Изменение velocity в FixedUpdate
public class RegularJumpCommand : CommandWithType<UnitController>
{
    private Vector3 _velocity;
    protected override void OnStart (object[] args)
    {
        base.OnStart (args);
        _velocity = (Vector3)args [1];
    }

    private void FixedUpdate()
    {
        if (!IsRunning)
            return;
        
        Controller.AttachedRigidbody.velocity = _velocity;
        FinishCommand ();
    }
}


Теперь значение velocity изменится в момент первой после Start-a итерации физического движка. Команда на данном этапе своего развития чудесно справится с задачами вроде наложения заклинания, бега, разных прыжков и визуальных эффектов.

Но как же?!


Этим еще не выйдет воспользоваться всюду, где только хочется. Если надо скачать конфиг или провести валидацию действий пользователя (не дать детям покупать плюшки за родительские деньги) или просто использовать команды как составные части бега зигзагами. Сплошь и рядом ситуации когда надо знать завершилась ли команда, успешно ли она выполнилась и если да, то взять из нее необходимые данные (для чего и делалась задержка уничтожения). Короче: здесь не разобраться без callback-ов. Лично мне новая система Unity UI нравится из-за них, но только когда их добавление или удаление происходит из кода (делать это в редакторе — грешно, не делайте так).

Можно сделать всё на событиях, даже думать не надо, но держать ссылки на все запущенные команды, отписываться в правильный момент не очень-то и хочется. А боль чуть ниже спины, в случае когда забыл отписаться, мало кому может принести удовольствие. Давайте сначала оговорим что нам надо сделать для того, чтобы начать пользоваться командами в полную силу и чтобы потом не было правок. Основная задача — сделать callback-и на успешное и не успешное завершение выполнение команды. Сделать их удобными для подписывания и без необходимости следить за отпиской. Также удобно будет при завершении команды передавать в аргумент callback-а саму команду, чтобы не держать ее отдельным полем в классе. А еще у нас не реализован способ остановки команды извне.

Первое: сделаем небольшую обертку для тех самых callback-ов. Сказано — сделано, мы программисты — народ простой. Вышло у нас примерно так:

Callback
    public class Callback<T> 
        where T : Command
    {
        public readonly Action<T> Succeed;
        public readonly Action<T> Fault;

        public Callback (Action<T> succeed)
        {
            this.Succeed = succeed;
        }

        public Callback (Action<T> succeed, Action<T> fault)
        {
            this.Succeed = succeed;
            this.Fault = fault;
        }
    }


Просто и удобно. Заметьте что по-умолчанию если callback один, то мы автоматически считаем что нас интересует только успешное завершение команды и вызовется он только в этом случае. Следующим логическим шагом будет сделать контейнер для этих самых callback-ов, ведь одного всегда будет мало. И у нас получилось вот что:

CallbackToken
    public class CallbackToken <T>
        where T : Command
    {
        private List<Callback<T>> _callbacks;

        private T _command;

        public CallbackToken (T _command)
        {
            this._command = _command;
            _callbacks = new List<Callback<T>>();
        }

        public void AddCallback(Callback<T> callback)
        {
            _callbacks.Add (callback);
        }

        public void RemoveCallback(Callback<T> callback)
        {
            _callbacks.Remove (callback);
        }

        public void FireSucceed()
        {
            foreach (Callback<T> calback in _callbacks)
            {
                calback.Succeed(_command);
            }
        }

        public void FireFault()
        {
            foreach (Callback<T> callback in _callbacks)
            {
                if (callback.Fault != null)
                {
                    callback.Fault (_command);
                }
            }
        }
    }


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

Команда с Callback-ом
public class Command : MonoBehaviour 
{
    private object[] _args;

    private bool _started = false;
    private bool _isReleased = false;

    public CallbackToken<Command> CallbackToken
    {
        get;
        private set;
    }

    public Command ()
    {
        CallbackToken = new CallbackToken<Command> (this);
    }

    public bool IsRunning
    {
        get{ return _started && !_isReleased;}
    }

    public static T ExecuteOn<T>(GameObject target, object[] args = null)
        where T : Command
    {
        T result = target.AddComponent <T>();
        result._args = args;
        return result;
    }

    private void Start()
    {
        _started = true;
        OnStart (_args);
    }

    protected virtual void OnStart(object[] args)
    {}

    private void OnDestroy()
    {
        if (!_isReleased)
            OnReleaseResources ();
    }

    protected virtual void OnReleaseResources()
    {
        _isReleased = true;
    }

    protected void FinishCommand(bool result = true)
    {
        OnReleaseResources ();
        OnFinishCommand ();

        if (result)
            CallbackToken.FireSucceed ();
        else
            CallbackToken.FireFault ();
        
        Destroy (this, 1f);
    }

    protected virtual void OnFinishCommand(){}

    public void Terminate(bool result = false)
    {
        FinishCommand (result);
    }


Теперь метод FinishCommand() будет принимать аргументом успешность выполнения, а для прерывания работы команды извне будет использоваться метод Terminate().

Посмотрим теперь как выглядит подписка:

Подписка на Callback
public class SomeController : MonoBehaviour 
{
    //don't forget to set this in editor
    public UnitController _targetUnit;

    private void Start()
    {
        if (_targetUnit != null)
        {
            Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject,
                                                   new object[]{_targetUnit ,new Vector3(0, 10, 0)})
                .CallbackToken.AddCallback (new Callback<Command>(OnJumpFinish));
        }
    }

    private void OnJumpFinish (Command command)
    {
        Debug.Log(string.Format("{0}", "Successfully jumped"));
    }
}


Теперь у нас легко получится решить и вторую поставленную задачу: забрать из команды данные, (ведь мы получаем её в callback метод), просто сделав публичный get-er для необходимой информации и — вуаля.

Конец!


В завершении хочется сказать, что подход очень удачный для разработки игр на Unity. Все просто и красиво, легко поддается модификациям, а утилитарные команды (скачать, запостить) легко переносятся в любой проект. Если эта статья вам понравится, мне будет не только приятно, но и появится стимул рассказать о конечных автоматах, MVC, стратегиях и о том как это живет и сосуществует в одном проекте.

P.S.: не забывайте, что код здесь исключительно для ознакомительных целей и тестировался только в редакторе.

Комментарии (28)


  1. KumoKairo
    28.03.2016 14:30
    +3

    Статья написана добротно, читать интересно, примеры понятные. Вы правы, метод очень гибкий и выглядит всё красиво, но есть пара замечаний. Меня сразу смутило огромное количество выделений памяти. Если пойти по порядку, то:

    AddComponent не только медленней, чем GetComponent, но и выделяет память на новый объект в куче. Плюс инициализация со стороны Unity (всё это собственно и замедляет выполнение метода). То есть вы решили одну проблему, но забыли о другой, которая в данном контексте более весома.

    Передача аргументов в массиве — ещё один объект в куче. Для примера с Vector3 это такой "продвинутый боксинг", когда мы структуру целенаправленно и сознательно превращаем в управляемый объект не получая ничего полезного взамен.

    Подписка на колбэк — там аж два новых объекта (на колбэк и на создание нового делегата при передаче метода в качестве параметра)

    Если предположить, что подобных команд будет достаточно много (как для примера с прыжком), то оверхед от GC Alloc и его последующие "икания", потраченные на чистку кучи от мусора сведут на нет все преимущества подобного подхода. Если честно, я читал статью с надеждой что под конец будет освещён и этот вопрос, что будут убраны AddComponent и много этих new, но увы.

    Что ещё немного вызвало сомнения — передача массива параметров без статической типизации с привязкой к индексам параметров. Мне кажется для этих случаев подошёл бы дженерик метод и пара вспомогательных tuple структур (то есть второй дженерик параметр у метода с описанием входной структуры).

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

    Как я уже отметил выше, статья написана живо и интересно, и по крайней мере даёт пищу для размышлений. Так что подобные статьи на описанные вами темы придутся вполне кстати (особенно интересует ваш MVC).


    1. martynko
      28.03.2016 15:24

      Спасибо за комментарий, приятно слышать.
      По поводу AddComponent я действительно забыл, и я пока не думал как решить это. Пока это не вызывало проблем с производительностью, хотя проекты, с которыми я сейчас работаю, целиком построены на использовании команд.
      Типизированные аргументы — хороший вариант для рассмотрения, хотя если мне надо передавать больше 2-х параметров, то я просто пишу кастомную структуру привязанную к команде и мне не приходиться запоминать порядок.
      И по-поводу callback-ов, я тоже могу только сказать, что это значительно удобнее чем event-ы а памяти на них выделяется не так уж и много. Чаще из-за текстур и моделей по памяти не помещаются и в таких случаях оптимизация callbeck-ов не поможет.


      1. KumoKairo
        28.03.2016 15:57

        А что за проекты позволите поинтересоваться? Интересен контекст, в котором используется подобное решение.

        хотя если мне надо передавать больше 2-х параметров, то я просто пишу кастомную структуру привязанную к команде и мне не приходиться запоминать порядок

        Это именно то, о чём я писал про пару вспомогательных tuple структур

        памяти на них выделяется не так уж и много

        зачастую напрягает не столько объем выделяемой памяти, сколько сам факт её выделения (GC Pressure)


        1. martynko
          28.03.2016 19:07
          +1

          А что за проекты позволите поинтересоваться? Интересен контекст, в котором используется подобное решение.

          Сейчас я занимаюсь клоном Crossy Road, перед этим были тоже ранэр и разукраска.
          Это именно то, о чём я писал про пару вспомогательных tuple структур

          Да, но я не делаю для этого дженерики.
          зачастую напрягает не столько объем выделяемой памяти, сколько сам факт её выделения (GC Pressure)

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


  1. OnionFan
    28.03.2016 14:40

    Почему бы не использовать ключевое слово params для передачи аргументов? Намного удобней выглядит и код не пострадает


    1. martynko
      28.03.2016 15:25

      Это просто дело привычки, ничего больше


    1. Leopotam
      07.04.2016 17:04

      Передача параметров через params — это просто сахар, внутри каждый раз создается массив объектов, куда пихаются параметры и обратная распаковка внутри метода. Как результат — гарантированный GC allocation.


  1. AllexIn
    28.03.2016 15:25
    -2

    Странное ощущение… Как будто вернулся во времена, когда не было ООП и методы работали над данными без жесткой связи...


    1. martynko
      28.03.2016 19:08

      Вы правы, здесь чувствуется функциональный стиль.


      1. TheShock
        28.03.2016 22:49

        Скорее процедурный)


  1. Aler
    29.03.2016 03:38
    +2

    А зачем команду наследовать от MonoBehaviour? Если команду сделать обычным классом и использовать конструктор, то все проблемы с аргументами отпадут, да и производительность вырастет (монобех очень тяжелый). Вроде этот подход называется композиция и самая приятная на мой взгляд реализация у Entitas — https://github.com/sschmid/Entitas-CSharp 


    1. martynko
      29.03.2016 11:26

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


      1. KumoKairo
        30.03.2016 08:09

        Кстати по поводу отладки, Wooga (в Entitas) сделали достаточно хорошую работу — их non-monobeh компоненты можно смотреть в редакторе почти как в обычном инспекторе (какие компоненты висят на каки объектах), плюс там свой дебаговый график для проверки производительности и пиков нагрузки. Что ещё более интересно — в их подходе решена проблема выделений памяти и скорости работы AddComponent — их компоненты (non-monobeh) берутся из пула. То есть два одинаковых прыжка на одном объекте в разное время будут использовать один компонент без необходимости каждый раз создавать новый объект. В принципе там как раз решается как раз описанная в статье проблема.


  1. MonkAlex
    29.03.2016 10:15

    Но в таком случае, если нужно будет чтобы несколько готовых юнитов, которые по разному прыгали, стали прыгать одинаково, то вам придется или чуть подправить иерархию классов, или просто скопипастить подходящий кусок кода во все нужные классы (что есть ужасающе).
    А что за странный кейс вообще? Просто по статье остается ощущение, что вы придумали проблему и решение, но проблема — выдуманная.


    1. martynko
      29.03.2016 10:31

      Суть статьи — показать подход, который позволяет динамически изменять поведение при этом остается легким для понимания и изменения. Особенно хорошо ощущается легкость изменения при работе над проектом в команде — для внесения нового функционала достаточно будет написать команду и не перелопачивать класс вникая в зависимости оставленные там кем-либо. Это касается чего-угодно в Вашей игре, будь то юнит (каст заклинания), окружение (изменение погоды) либо сам игровой процесс (показать обучение).


      1. MonkAlex
        29.03.2016 12:06

        Это очень спорное решение. Увеличение точек входа в действие конечно упрощает его настройку на каждый чих, зато анализ и исправление — усложняет.


        1. martynko
          29.03.2016 12:41

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


          1. MonkAlex
            29.03.2016 17:48

            Был простой прыжок в юните. Вы добавляете Actor -> Теперь есть прыжок и в юните, и в Actor-е, можно писать код и там и там.


            1. martynko
              29.03.2016 19:39

              Извените, но вы не поняли принцып. Таким же аргументом было бы писать код прыжка в менеджере сцены. Actor-ы и Command-ы — это способ инкапсулировать от юнита код прыжка. Тоесть в самом юните кода прыжка быть не должно. В случае с Actor-ами менеджер вызывает метод на юните, а юнит вызывает метод в Actor-е и он уже проделывает всю работу. В случае с Command менеджер цеплчет команду на юнита и она все делает. В любом случае сам юнит не содержит в себе кода прыжка и не знает как он прыгнет. И это дает легкий способ заставить гнома порхать как бабочка не меняя код в самом юните а просто прицепив на него подшодящую компоненту или запустив правильную команду.


  1. Neuyazvimy1
    29.03.2016 10:31

    А почему вы не используйте SendMessage? docs.unity3d.com/ScriptReference/Component.SendMessage.html


    1. martynko
      29.03.2016 10:37

      При использовании SendMessage возникает сильная привязка к названию метода и при этом пропадает легкий способ отследить что и откуда вызывается. К примеру Вы случайно опечатались при названии метода, эта ошибка перешла в Ваши SendMessage, и если кто-нибудь исправит ошибку в названии метода, то сломает логику программы и долго никто не будет понимать почему.


      1. Leopotam
        07.04.2016 16:57
        -1

        сломает логику программы и долго никто не будет понимать почему.

        Это не так, гарантированной доставкой можно управлять путем указания требования на наличие получателя последним параметром SendMessage.


    1. Igor_Sib
      29.03.2016 19:18

      Помимо вышесказанного SendMessage медленный.


      1. Leopotam
        07.04.2016 16:58

        Зависит от способа использования. Если нужно вызывать больше пары раз за фрейм — да, можно подумать о чем-то другом, иначе не сильно критично.


  1. SchrodingersCatExperiment
    29.03.2016 18:43

    Вуаля! Шикарная статья! Спасибо автор :)


  1. Igor_Sib
    29.03.2016 19:26

    Пока читал статью, возникло 3 замечания:
    1) Уже сказали выше, что AddComponent тормозной. Будет у вас сотня юнитов прыгать, лазить, плавать и прочие команды выполнять — будут тормоза. На медленных девайсах точно. Хотя для каких-нибудь паззлов/хогов/квестов будет нормально.
    2) Про массив параметров — тоже уже сказали, тоже сомнительный велосипед.
    3) Start выполняется с некоторой задержкой (Awake мгновенно). Поэтому может быть некоторая инертность при выполнении.
    А вообще поход интересный, хоть и не понятно чем он лучше оверрайда методов.


    1. KumoKairo
      30.03.2016 08:10

      Композиция в любом виде предпочтительней оверрайда методов, так как не привязывает функционал к определённой иерархии классов.


    1. Leopotam
      07.04.2016 17:00

      Awake мгновенно

      Это не так для компонентов, висящих на задизейбленных GameObject-ах, например, если они содержаться в инстанцирующемся префабе и GO были помечены как неактивные.