Для тех кто пропустил первую часть — Часть 1
Следующая часть — Часть 3

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

Итак, начинаем собирать всё в кучу




Ракета:

Класс базовой ракеты
using DG.Tweening;
using GlobalEventAggregator;
using UnityEngine;

namespace PlayerRocket
{
    public class Rocket : PlayerRocketBase
    {
        [SerializeField] private float pathСorrectionTime = 10;
        private Vector3 movingUp = new Vector3(0, 1, 0);

        protected override void StartEventReact(ButtonStartPressed buttonStartPressed)
        {
            transform.SetParent(null);
            rocketState = RocketState.MOVE;
            transform.DORotate(Vector3.zero, pathСorrectionTime);
        }

        protected override void Start()
        {
            base.Start();

            EventAggregator.Invoke(new RegisterUser { playerHelper = this });
            if (rocketState == RocketState.WAITFORSTART)
                return;
            RocketBehaviour();
        }


        private void FixedUpdate()
        {
            RocketBehaviour();
        }

        private void RocketBehaviour()
        {
            switch (rocketState)
            {
                case RocketState.WAITFORSTART:
                    if (inputController.OnTouch && !inputController.OnDrag)
                        rocketHolder.RotateHolder(inputController.worldMousePos);
                    break;
                case RocketState.MOVE:
                    rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime));
                    forceModel.AddModificator();
                    break;
                case RocketState.STOP:
                    Debug.Log("мы стопаемся");
                    rigidbody.velocity = Vector3.zero;
                    rigidbody.drag = 50;
                    rocketState = RocketState.COMPLETESTOP;
                    break;
                case RocketState.COMPLETESTOP:
                    break;
                default:
                    rocketState = RocketState.COMPLETESTOP;
                    break;
            }
        }
    }
}


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

  1. Ждать старта
  2. Лететь
  3. Подвергаться влиянию модификаторов
  4. Останавливаться

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

Для сложных поведений объектов — лучше использовать поведенческие паттерны, например паттерн состояние. Для простых — начинающие программисты часто используют много много if else. Я же рекомендую использовать switch и enum. Во первых это более четкое разделение логики на конкретные этапы, благодаря этому мы точно будем знать в каком состоянии мы сейчас находимся, и что происходит, меньше возможностей превратить код в лапшу из десятков исключений.

Как это работает:

Сначала заводим enum с нужными нам состояниями:

 public enum RocketState
    {
        WAITFORSTART = 0,
        MOVE = 1,
        STOP = 2,
        COMPLETESTOP = 3,
    }

В родительском классе у нас есть поле —
protected RocketState rocketState;

По дефолту ему назначается первое значение. Enum по умолчанию сам выставляет значения, но для данных которые могут изменяться сверху или настраиваться геймдизайнерами — я прописываю значения вручную, для чего? Для того чтобы можно было добавить еще одно значение в инам в любое место и не нарушить хранимые данные. Также советую изучить flag enum.

Далее:

Само поведение мы определяем в апдейте, в зависимости от значения поля rocketState

 private void FixedUpdate()
        {
            RocketBehaviour();
        }

        private void RocketBehaviour()
        {
            switch (rocketState)
            {
                case RocketState.WAITFORSTART:
                    if (inputController.OnTouch && !inputController.OnDrag)
                        rocketHolder.RotateHolder(inputController.worldMousePos);
                    break;
                case RocketState.MOVE:
                    rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime));
                    forceModel.AddModificator();
                    break;
                case RocketState.STOP:
                    Debug.Log("мы стопаемся");
                    rigidbody.velocity = Vector3.zero;
                    rigidbody.drag = 50;
                    rocketState = RocketState.COMPLETESTOP;
                    break;
                case RocketState.COMPLETESTOP:
                    break;
                default:
                    rocketState = RocketState.COMPLETESTOP;
                    break;
            }
        }

Расшифрую что происходит:

  1. Когда ждем — просто вращаем ракету по направлению к курсору мыши, таким образом задаём начальную траекторию
  2. Второе состояние — мы летим, разгоняем ракету в нужном направлении, и обновляем модель модификаторов на предмет появления объектов влияющих на траекторию
  3. Третье состояние это когда нам прилетает команда остановиться, тут отрабатываем всё чтобы ракета остановилась и переводим в состояние — мы полностью остановились.
  4. Последнее состояние — стоим ничего не делаем.

Удобство текущего паттерна — это всё очень легко расширяется и регулируется, но есть одно но, слабое звено — это когда у нас может быть состояние которое комбинирует ряд других состояний. Тут или флаговый инам, с усложнением обработки, или уже переходить на более «тяжелые» паттерны.

С ракетой разобрались. На очереди простой, но забавный объект — кнопка старта.

Кнопка старта


От неё требуется следующий функционал — нажали, она оповестила что на неё нажали.

Класс кнопки старт
using UnityEngine;
using UnityEngine.EventSystems;

public class StartButton : MonoBehaviour, IPointerDownHandler
{
    private bool isTriggered;

    private void ButtonStartPressed()
    {
        if (isTriggered)
            return;
        isTriggered = true;
        GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed());
        Debug.Log("поехали");
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        ButtonStartPressed();
    }
}

public struct ButtonStartPressed { }


По геймдизайну это 3д объект на сцене, кнопку предполагается интегрировать в дизайн стартовой планеты. Ну ок, есть нюанс — как отслеживать нажатие на объект в сцене?

Если гуглить то мы найдем кучу методов OnMouse, среди которых будет и нажатие. Казалось бы легкий выбор, но он как раз является очень плохим, начиная с того что он часто криво работает(есть много нюансов по отслеживанию нажатия), «дорогой», заканчивая тем что он не дает той тонны плюшек которая есть в UnityEngine.EventSystems.

В итоге я рекомендую пользоваться UnityEngine.EventSystems и интерфейсами — IPointerDownHandler, IPointerClickHandler. В их методах мы и реализуем реакцию на нажатие, но тут есть несколько нюансов.

  1. В сцене должна присутствовать EventSystem, это объект/класс/компонент юнити, обычно создается когда мы создаем канвас для интерфейса, но его также можно создать самому.
  2. На камере должен присутствовать Physics RayCaster (это для 3д, для 2д графики там отдельный рейкастер)
  3. На объекте должен быть коллайдер

В проекте это выглядит так:



Теперь объект отслеживает нажатие и вызывается этот метод:


public void OnPointerDown(PointerEventData eventData)
    {
        ButtonStartPressed();
    }

    private void ButtonStartPressed()
    {
        if (isTriggered)
            return;
        isTriggered = true;
        GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed());
        Debug.Log("поехали");
    }

Что тут происходит:

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

Далее мы вызываем ивент — кнопка нажата, на который подписан класс ракета, и переводим ракету в состояние движения.

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

Почему событийную парадигму не любят многие программисты? Потому-что тонна событий и подписок на эти события легко превращают код в лапшу, в которой вообще не понятно откуда начать и закончится ли это где то, не говоря о том что также надо следить за отпиской/подпиской и чтобы все объекты были живыми.

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

Вращение ракеты для определения стартовой траектории



По геймдизайну ракета должна уметь вращаться вокруг планеты, для определения начальной траектории, но не больше какого то угла. Вращение осуществляется тачем — ракета просто следит за пальцем и направлена всегда на то место куда мы ткнули в экран. Кстати как раз прототип позволил определить что это слабое место и возникает много пограничных с этим функционалом эпизодов связанных с управлением.

Но по порядку:

  1. Нам нужно чтобы ракета поворачивалась относительно планеты в сторону тача
  2. Нам нужно клампить угол поворота

Насчет поворота относительно планеты — можно хитро вращать вокруг оси и вычислять ось вращения, а можно просто создать объект пустышку с центром внутри планеты, переместить туда ракету, и спокойно вращать пустышку вокруг оси Z, пустышка будет иметь класс который будет определять поведение объекта. Ракета будет вращаться с ней. Объект я назвал RocketHolder. С этим разобрались.

Теперь насчёт ограничения поворота и поворота в сторону тача:

сlass RocketHolder
using UnityEngine;

public class RocketHolder : MonoBehaviour
{
    [SerializeField] private float clampAngle = 45;

    private void Awake()
    {
        GlobalEventAggregator.EventAggregator.AddListener(this, (InjectEvent<RocketHolder> obj) => obj.inject(this));
    }
       
    private float ClampAngle(float angle, float from, float to)
    {
        if (angle < 0f) angle = 360 + angle;
        if (angle > 180f) return Mathf.Max(angle, 360 + from);
        return Mathf.Min(angle, to);
    }

    private Vector3 ClampRotationVectorZ (Vector3 rotation )
    {
        return new Vector3(rotation.x, rotation.y, ClampAngle(rotation.z, -clampAngle, clampAngle));
    }

    public void RotateHolder(Vector3 targetPosition)
    {
        var diff = targetPosition - transform.position;
        diff.Normalize();
        float rot_z = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg;
        transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90);
        transform.eulerAngles = ClampRotationVectorZ(transform.rotation.eulerAngles);
    }
}


Не смотря на то что игра по идее 3д, но вся логика и игровой процесс на самом деле 2д. И нам просто надо довернуть ракету вокруг оси Z по направлению к месту нажатия. В конце метода мы клампим градус поворота по значению заданному в инспекторе. В методе Awake можно посмотреть самую правильную реализацию инъекции класса через агрегатор.

InputController


Один из самых важных классов, именно он собирает и обрабатывает поведение пользователя. Нажатия хоткеев, кнопок геймпада, клавиатуры и тд. У меня в прототипе довольно простой инпут, по факту надо знать только 3и вещи:

  1. Есть ли нажатие и его координаты
  2. Есть ли вертикальный свайп и насколько свайпаться
  3. Оперирую ли я с интерфейсом/модификаторами

class InputController
using System;
using UnityEngine;
using UnityEngine.EventSystems;

public class InputController : MonoBehaviour
{
    public const float DirectionRange = 10;
    private Vector3 clickedPosition;

    [Header("расстояние после которого мы считаем свайп")]
    [SerializeField] private float afterThisDistanceWeGonnaDoSwipe = 0.5f;

    [Header("скорость вертикального скролла")]
    [SerializeField] private float speedOfVerticalScroll = 2;
    public ReactiveValue<float> ReactiveVerticalScroll { get; private set; }

    public Vector3 worldMousePos => Camera.main.ScreenToWorldPoint(Input.mousePosition);

    public bool OnTouch { get; private set; }
    public bool OnDrag { get; private set; }

    // Start is called before the first frame update
    private void Awake()
    {
        ReactiveVerticalScroll = new ReactiveValue<float>();
        GlobalEventAggregator.EventAggregator.AddListener(this, (ImOnDragEvent obj) => OnDrag = obj.IsDragging);
        GlobalEventAggregator.EventAggregator.AddListener<InjectEvent<InputController>>(this, InjectReact);
    }

    private void InjectReact(InjectEvent<InputController> obj)
    {
        obj.inject(this);
    }

    private void OnEnable()
    {
        GlobalEventAggregator.EventAggregator.Invoke(this);
    }

    void Start()
    {
        GlobalEventAggregator.EventAggregator.Invoke(this);
    }

    private void MouseInput()
    {
        if (EventSystem.current.IsPointerOverGameObject() && EventSystem.current.gameObject.layer == 5)
            return;

        if (Input.GetKeyDown(KeyCode.Mouse0))
            clickedPosition = Input.mousePosition;

        if (Input.GetKey(KeyCode.Mouse0))
        {
            if (OnDrag)
                return;

            VerticalMove();
            OnTouch = true;
            return;
        }

        OnTouch = false;
        ReactiveVerticalScroll.CurrentValue = 0;
    }

    private void VerticalMove()
    {
        if ( Math.Abs(Input.mousePosition.y-clickedPosition.y) < afterThisDistanceWeGonnaDoSwipe)
            return;
        var distance = clickedPosition.y + Input.mousePosition.y * speedOfVerticalScroll;

        if (Input.mousePosition.y > clickedPosition.y)
            ReactiveVerticalScroll.CurrentValue = distance;
        else if (Input.mousePosition.y < clickedPosition.y)
            ReactiveVerticalScroll.CurrentValue = -distance;
        else
            ReactiveVerticalScroll.CurrentValue = 0;
    }

    // Update is called once per frame
    void Update()
    {
        MouseInput();
    }
}
}


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

Выглядит это так:

class ReactiveValue
public class ReactiveValue<T> where T: struct
{
    private T currentState;
    public Action<T> OnChange;

    public T CurrentValue
    {
        get => currentState;
        set
        {
            if (value.Equals(currentState))
                return;
            else
            {
                currentState = value;
                OnChange?.Invoke(currentState);
            }
        }
    }
}


Подписываемся на OnChange, и дергаемся если только значение изменилось.

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

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


  1. sith
    24.05.2019 20:39

    Спасибо за статью. Небольшой Cod Review, если Вы не против.

    public class Rocket : PlayerRocketBase

    Зачем понадобилось усложнять и делать родительский класс, если почти наверняка будет всего один тип ракет в игре (SOLID, KISS, YAGNI)?

    protected override void StartEventReact(ButtonStartPressed buttonStartPressed)

    Класс Rocket не должен знать про кнопку старта (он должен быть как можно тупее — помните KISS?). Вообще, Rocket перегружен и нарушает множество принципов, перечисленных в первой статье. Код ракеты должен только помогать отображать эту ракету и помогать изменять её положение на экране. При этом это должны быть несколько несвязных между собой компонентов на gameObject ракеты (и/или его частях). Ваш класс Rocket больше похож на RocketMovement. При этом может быть ещё RocketFire, RocketHealth, RocketFuel и т.д.

                switch (rocketState)
                {
                    case RocketState.WAITFORSTART:
                        if (inputController.OnTouch && !inputController.OnDrag)
                            rocketHolder.RotateHolder(inputController.worldMousePos);
                        break;


    Код под каждым case должен быть вынесен в отдельный метод. Но, вообще, класс Rocket ничего не должен знать про inputController. У Rocket должен быть отдельный компонент Driver или Engine, который должен иметь что-то типа SetTrust(float trust);

    GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed());

    Зачем нужен агрегатор событий ещё и видимый всеми? Почему не использовать обычный static Action someAction в нужных классах и подписки на него? Я видел Вашу статью про этот агрегатор но так и не увидел достоинств усложнения и замедления кода.

    public Action<T> OnChange;

    Неверное именование. События называют просто Change/Click/Update а методы подписчиков уже OnChange, OnClick, OnUpdate.

    Вообще, начинающие разработчики Unity (я и сам когда-то был в их числе) долгое время не понимают идеологии Unity. Идеология такая — если это что-то визуальное (звуковое и т.д.) на сцене, то это gameObject (или entity) на котором нужные компоненты, которые помогают его «визуализировать» и ничего больше. Всё, что не относится к визуализации, лучше выносить отдельно. Т.е. это чем-то похоже на MVC (gameObject это View) но совсем не MVC.
    Если посмотреть на MonoBehaviour то видно, что внутри всё помогает отображать, анимировать, двигать объект и так далее. Логика должна быть снаружи.
    Т.е. класс Rocket можно упразднить и разбить его на несколько компонентов и управлять этими компонентами уже снаружи не позволяя им знать детали реализации. Т.е. очень просто говоря — ракета, это очередной визуальный компонент, такой как button или text и, конечно, она ничего не должна знать про другие кнопки (startButton) и прочие inputController, события, глобальные состояния игры и прочее. Также, Rocket gameObject должен запросто допускать добавления новых компонентов на себя (SOLID) и, по хорошему, ещё и удаление любых компонентов. Когда проектируете Rocket задумайтесь — можно ли будет изменить её поведение просто добавив или убрав компоненты из gameObject. В случае Вашего прототипа это невозможно — события и прочее глубоко зашиты в класс.


    1. Brightori Автор
      25.05.2019 02:23

      Спасибо за ответ и код ревью ) Особенно за начинающего программиста. Долго отвечал — был в кино, советую кстати Джонни Уика.

      Итак:
      1)

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

      2)
      Класс Rocket не должен знать про кнопку старта
      Ну он и не знает, ракета только знает что ей прилетает ивент на старт

      3)
      Код под каждым case должен быть вынесен в отдельный метод.
      Согласен, я просто придерживаюсь правила — если кода не очень много, можно оставить в кейсе. Но в отдельный метод будет правильнее.

      4)
      Но, вообще, класс Rocket ничего не должен знать про inputController. У Rocket должен быть отдельный компонент Driver или Engine, который должен иметь что-то типа SetTrust(float trust);
      В любом деле главное без фанатизма, инпут контроллер в данном ракурсе не верхняя сущность, а всего лишь источник данных для принятия решений, да, в нём есть лишние данные для ракеты, более расово правильно это было всё связать через интерфейс с конкретными штуками, но снаружи мы не можем менять данные, поэтому если торчат пара лишних для ракеты полей — ничего страшного. Всё равно эти данные будут необходимы, если не ракете, так более верхней сущности/объекту.

      5)
      Зачем нужен агрегатор событий ещё и видимый всеми?
      Я на самом деле понимаю что у меня есть проблема с позиционированием агрегатора, ибо под капотом там ивенты, механика работы у него — по сути потоки данных, а основная фича — именно контейнеры данных. Видимость всеми — ну в этом и фича, подписка на нужный тебе тип данных. В третьей части будут модификаторы которые просто присылают данные о модификаторе, может будет более очевидно, не говоря уже о инъекциях.

      6)
      Неверное именование. События называют просто Change/Click/Update а методы подписчиков уже OnChange, OnClick, OnUpdate.
      склонен согласиться.

      7)
      Вообще, начинающие разработчики Unity (я и сам когда-то был в их числе) долгое время не понимают идеологии Unity. Идеология такая — если это что-то визуальное (звуковое и т.д.) на сцене, то это gameObject (или entity) на котором нужные компоненты, которые помогают его «визуализировать» и ничего больше. Всё, что не относится к визуализации, лучше выносить отдельно. Т.е. это чем-то похоже на MVC (gameObject это View) но совсем не MVC.
      Если посмотреть на MonoBehaviour то видно, что внутри всё помогает отображать, анимировать, двигать объект и так далее. Логика должна быть снаружи.


      COOP люблю, хорошая штука, но надо взглянуть на вещи шире — компонент Unity, это по факту куча барахла который тащит с собой монобех. Выделять функционал в отдельный юнити компонент, лучше когда он действительно отдельный и прям либо ваще никак не связан с конкретным классами. Например компонент для дефолтной озвучки UI(клик, наведение и тд), он вообще может ни про кого не знать и действовать автономно. В случае с ракетой — у неё только поведение связанное с движением. Допустим у нас всё становится сложнее — появляется стрельба, защитные поля, хелс поинты и тд, и допустим появятся враги, у которых будет такой же функционал (чисто фантазии). То этот функционал лучше разнести по классам моделям. И будет модель движения, модель стрельбы и тд. Опять же если смотреть шире — класс модель, и есть компонент в каком то смысле, ничто не мешает внедрить этот класс любому заинтересованному лицу. Пример в этом коде — ForceModel forceModel, этот класс можно применить например к астериодам, и они тоже станут попадать под влияние модификаторов. Плюс не забываем про любимый ООП, и у наследников мы можем переопределить или расширить функционал. Я против изначального дробления на мифические компоненты, вот это как раз и есть нарушения KISS в чистом виде. Вместо простого класса с простым поведением, мы пытаемся заложиться в COOP, плодим классы, плодим монобехи, каждый крутит свой апдейт, складирует в памяти кучу гавна которое не используется и тд. Те же Unity поняли что COOP в Unity тупиковый для оптимизации, и поэтому активно пилят ECS для слабых мест, который как раз выигрывает в том числе за счёт того что не тащит с собой целый багаж ненужных штук, которые валяются в куче.


      1. Brightori Автор
        25.05.2019 03:32

        Вдогонку к пункту 7 и 4, я вообще за разумную композицию и агрегацию


  1. sith
    25.05.2019 19:28

    Особенно за начинающего программиста

    Я писал не про начинающего программиста, а про начинающего разработчика на Unity. Я до сих пор время от времени сам себя считаю таковым.

    Мы рассматривали возможность нескольких вариантов ракет

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

    Ну он и не знает, ракета только знает что ей прилетает ивент на старт

    И про этот event не должна знать, как и какая-нибудь кнопка не должна знать про event StartGame, например. Ракета должна управляться извне (я писал про пример двигателя) или отдельным компонентом (набором компонентов), который можно заменить на другой. И вообще не должна знать что такое event.

    что у меня есть проблема с позиционированием агрегатора

    На мой взгляд проблема не с позиционированием, а с тем, что это слишком общее решение.

    это по факту куча барахла который тащит с собой монобех

    Верно.

    лучше когда он действительно отдельный и прям либо ваще никак не связан с конкретным классами

    Верно! Поэтому нужно писать именно такие компоненты. Чем меньше связность, тем лучше.

    Я против изначального дробления на мифические компоненты

    Придётся. От DOTS уже никуда не убежишь. Лучше начать сейчас, чем опоздать.

    вот это как раз и есть нарушения KISS в чистом виде

    Дробление не приводит к усложнению.

    и поэтому активно пилят ECS для слабых мест

    Это уже по сути случилось. Я понимаю про какую именно оптимизацию Вы пишете. Но всё же это самое «дробление», тем более в Вашем случае того точно стоит.


    1. Brightori Автор
      25.05.2019 20:21

      Только что был по лекции про DOTS, не везде он зайдет, и там очень много писанины, далее:

      1) Компоненты у Unity слабый элемент, поэтому я в большинстве случаев за наследование и композицию. Композиция если брать абстрактно и есть парадигма COOP. Просто либо мы пользуемся обертками Unity со всеми вытекающими, либо сами пишем и дробим функционал как нам хочется. И скажем так — текущие компоненты это для удобства инди разработчика, понавесил какой то функционал, запихал в публичные поля объекты, и оно взлетело.

      2)

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

      3) Насчёт DOTS/ECS — ну это попытка пропихнуть DDD в более качественной обертке с волшебными штуками, но по факту у неё есть те же слабые места что и у DDD. Другой вопрос что при определенных задачах профит от DOTS очень крут. А вот то что писанины становится в разы больше и она не всегда оправдана — это факт. Порой для описания простой сущности нужны десятки объектов. Поэтому для простого прототипа казуалки я бы избегал нативного ECS.


  1. sith
    25.05.2019 22:47

    Только что был по лекции про DOTS, не везде он зайдет

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

    и там очень много писанины

    Да, и при этом, как это ни странно, cоблюдаются все эти SOLID, KISS etc, в отличии от.

    либо мы пользуемся обертками Unity со всеми вытекающими

    Unity обязывает ими пользоваться (в DOTS в том числе, но под другим соусом) если нужно представление чего-либо на сцене. Тут важно понимать (и я писал об этом) где нужно использовать MonoBehaviour, а где нет.

    Как еще ракете узнать что пора?

    Этот вопрос созвучен с вопросом «как узнать кнопке (т.е. по сути такому-же визуальному объекту, что и ракета), что её нужно показать/скрыть?». Решения могут быть разными, но не изменение кода кнопки.

    Откуда ракета в принципе узнает что пора?

    Она и не должна. Вообще, для ракеты не должно быть понятия «пора». У неё (точнее двигателя) должны быть функции PrepareToStart(), SetTrust(float trust) и т.д. Ракета не должна быть настолько умной, чтобы «знать, что пора».

    В данный момент есть условный поток данных — старт ракеты

    И это проблема — сложно тестировать поведение (например, двигателей) — нужно писать отдельный функционал для тестов. Другая проблема — inputController который жёстко зашит внутри ракеты. Что если мы хотим управлять ракетой через тесты? Что если через сеть? Что если через обучающий сценарий, что если хотим записать её полёт и повторить потом? Если бы у ракеты было просто SetTrust(float trust) то всех этих проблем не было бы — делай с ней, что хочешь.


  1. Brightori Автор
    26.05.2019 00:14

    И это проблема — сложно тестировать поведение (например, двигателей) — нужно писать отдельный функционал для тестов. Другая проблема — inputController который жёстко зашит внутри ракеты. Что если мы хотим управлять ракетой через тесты? Что если через сеть? Что если через обучающий сценарий, что если хотим записать её полёт и повторить потом? Если бы у ракеты было просто SetTrust(float trust) то всех этих проблем не было бы — делай с ней, что хочешь.


    В данном контексте тестирование супер изи, ибо есть конфиг в котором зашит параметр скорость — запихиваем разный конфиг, можно через инъекцию как раз агрегатором. Далее даём отмашку старт, далее присылаем ивенты добавления модификаторов. Тест занял бы от 2х с плюсом строчек строчек. Это легко делается тестовыми методами/классами, и можно проверить самые сложные сценарии типа применения одновременно нескольких модификаторов с разной силой влияния.

    На самом деле я думаю уже можно закругляться, я понял вашу точку зрения. Я придерживаюсь другой и признаю большинство парадигм, и на мой взгляд всему своё место, COOP, OOP, AOP, Reactive, Event based, DDD.

    И вопрос с подвохом — DDD придумали не сегодня, почему он не занял доминирующую позицию в программировании?


    1. sith
      26.05.2019 00:55

      тестирование супер изи, ибо

      И далее идёт описание большой цепочка необходимых действий, некоторые из которых потенциально опасны о могут поломать объект — т.е. есть целый контекст для тестирования, который нужно при необходимости передать/объяснить другому члену команды. «супер изи» тестирование это SetTrust(-1) для отдельного компонента и всё — сейчас так не получится — нужна цепочка.

      И вопрос с подвохом

      Не люблю вопросы с подвохом — обычно, в них есть какой-нибудь подвох :)

      DDD придумали не сегодня, почему он не занял доминирующую позицию в программировании?

      И TDD не занял. Пожимаю плечами. Я в комментариях к статье на рассуждаю о доминирующих позициях. Просто написал свой небольшой и далеко не полный review. Например, если продолжать, то этот код правильный:
      private float ClampAngle(float angle, float from, float to)

      А вот этот уже нет:
      private Vector3 ClampRotationVectorZ (Vector3 rotation)

      потому, что он должен был быть таким:
      private Vector3 ClampRotationVectorZ (Vector3 rotation, float clampAngle)

      Класс ReactiveValue делает CurrentValue публичным, что неправильно.
      public class ReactiveValue<T> where T: struct


      И так далее.

      В целом мне нравятся Ваши статьи и я просто хочу помочь советом. При этом, конечно, ни на чём не настаиваю — это просто советы и, несомненно, я могу ошибаться.


      1. Brightori Автор
        26.05.2019 01:16

        я бы поставил лайк, но кармы не хватает )