Для тех кто пропустил первую часть — Часть 1
Следующая часть — Часть 3
Если кому интересно почитать про используемый ивент агрегатор, то вам сюда, но это не обязательно.
Ракета:
Что нам нужно чтобы ракета взлетела? В игровом пространстве нам нужна условная планета с которой стартуем, кнопка старт и ракета. Что должна уметь ракета?
То есть у нас появляется разное поведение/состояние ракеты, в зависимости от текущего состояния, ракета должна обеспечивать разное поведение. В программировании мы постоянно сталкиваемся с ситуацией, когда у объекта может быть много кардинально разных поведений.
Для сложных поведений объектов — лучше использовать поведенческие паттерны, например паттерн состояние. Для простых — начинающие программисты часто используют много много if else. Я же рекомендую использовать switch и enum. Во первых это более четкое разделение логики на конкретные этапы, благодаря этому мы точно будем знать в каком состоянии мы сейчас находимся, и что происходит, меньше возможностей превратить код в лапшу из десятков исключений.
Как это работает:
Сначала заводим enum с нужными нам состояниями:
В родительском классе у нас есть поле —
По дефолту ему назначается первое значение. Enum по умолчанию сам выставляет значения, но для данных которые могут изменяться сверху или настраиваться геймдизайнерами — я прописываю значения вручную, для чего? Для того чтобы можно было добавить еще одно значение в инам в любое место и не нарушить хранимые данные. Также советую изучить flag enum.
Далее:
Само поведение мы определяем в апдейте, в зависимости от значения поля rocketState
Расшифрую что происходит:
Удобство текущего паттерна — это всё очень легко расширяется и регулируется, но есть одно но, слабое звено — это когда у нас может быть состояние которое комбинирует ряд других состояний. Тут или флаговый инам, с усложнением обработки, или уже переходить на более «тяжелые» паттерны.
С ракетой разобрались. На очереди простой, но забавный объект — кнопка старта.
От неё требуется следующий функционал — нажали, она оповестила что на неё нажали.
По геймдизайну это 3д объект на сцене, кнопку предполагается интегрировать в дизайн стартовой планеты. Ну ок, есть нюанс — как отслеживать нажатие на объект в сцене?
Если гуглить то мы найдем кучу методов OnMouse, среди которых будет и нажатие. Казалось бы легкий выбор, но он как раз является очень плохим, начиная с того что он часто криво работает(есть много нюансов по отслеживанию нажатия), «дорогой», заканчивая тем что он не дает той тонны плюшек которая есть в UnityEngine.EventSystems.
В итоге я рекомендую пользоваться UnityEngine.EventSystems и интерфейсами — IPointerDownHandler, IPointerClickHandler. В их методах мы и реализуем реакцию на нажатие, но тут есть несколько нюансов.
В проекте это выглядит так:
Теперь объект отслеживает нажатие и вызывается этот метод:
Что тут происходит:
У нас есть булево поле в котором мы отслеживаем нажали кнопку или нет (это защита от многократного нажатия, чтобы у нас не запускался каждый раз сценарий старта).
Далее мы вызываем ивент — кнопка нажата, на который подписан класс ракета, и переводим ракету в состояние движения.
Немного забегая вперед — почему тут сплошь и рядом ивенты? Это событийно-ориентированное программирование, Во первых событийная модель дешевле постоянной обработки данных, с целью выяснения их изменений. Во вторых это та самая слабая связанность, нам не нужно на ракете знать что существует кнопка, что кто то её нажал и так далее, мы просто знаем что есть событие для старта, мы его получили и действуем. Далее — это событие интересно не только ракете, например на это же событие подписана панель с модификаторами, она скрывается при старте ракеты. Также это событие может быть интересно инпут контроллеру — и пользовательский ввод может не обрабатываться или обрабатываться по другому после старта ракеты.
Почему событийную парадигму не любят многие программисты? Потому-что тонна событий и подписок на эти события легко превращают код в лапшу, в которой вообще не понятно откуда начать и закончится ли это где то, не говоря о том что также надо следить за отпиской/подпиской и чтобы все объекты были живыми.
И именно поэтому для реализации ивентов я использую свой агрегатор ивентов, который по сути передаёт не ивенты, а контейнеры данных посредством ивентов, и классы подписываются на те данные которые им интересны. Также агрегатор сам следит за живыми объектами и выкидывает из подписчиков дохлые объекты. Благодаря перебросу контейнера, также возможна реализация инъекции, можно передать ссылку на интересующий нас класс. По контейнеру можно легко отследить кто эти данные обрабатывает и посылает. Для прототипирования — отличная вещь.
По геймдизайну ракета должна уметь вращаться вокруг планеты, для определения начальной траектории, но не больше какого то угла. Вращение осуществляется тачем — ракета просто следит за пальцем и направлена всегда на то место куда мы ткнули в экран. Кстати как раз прототип позволил определить что это слабое место и возникает много пограничных с этим функционалом эпизодов связанных с управлением.
Но по порядку:
Насчет поворота относительно планеты — можно хитро вращать вокруг оси и вычислять ось вращения, а можно просто создать объект пустышку с центром внутри планеты, переместить туда ракету, и спокойно вращать пустышку вокруг оси Z, пустышка будет иметь класс который будет определять поведение объекта. Ракета будет вращаться с ней. Объект я назвал RocketHolder. С этим разобрались.
Теперь насчёт ограничения поворота и поворота в сторону тача:
Не смотря на то что игра по идее 3д, но вся логика и игровой процесс на самом деле 2д. И нам просто надо довернуть ракету вокруг оси Z по направлению к месту нажатия. В конце метода мы клампим градус поворота по значению заданному в инспекторе. В методе Awake можно посмотреть самую правильную реализацию инъекции класса через агрегатор.
Один из самых важных классов, именно он собирает и обрабатывает поведение пользователя. Нажатия хоткеев, кнопок геймпада, клавиатуры и тд. У меня в прототипе довольно простой инпут, по факту надо знать только 3и вещи:
Тут всё в лоб и без заморочек, из интересного может быть примитивная реализация реактивной проперти — когда я только начинал программировать, всегда было интересно как же узнать о том что данные изменились, без постоянной вентиляции данных. Ну так вот, это оно.
Выглядит это так:
Подписываемся на OnChange, и дергаемся если только значение изменилось.
Касательно прототипирования и архитектуры — советы всё те же самые, публичные только проперти и методы, все данные должны изменяться только локально. Любые обработки и вычисления — складывайте по отдельным методам. В итоге вы всегда сможете поменять реализацию/вычисления, и это не будет задевать внешних пользователей класса. На этом пока всё, в третьей заключительной части — про модификаторы и интерфейс (драг дроп). И планирую выложить проект на гит, чтобы можно было посмотреть/пощупать. Если есть вопросы по прототипированию — задавайте, попробую внятно ответить.
Следующая часть — Часть 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;
}
}
}
}
Что нам нужно чтобы ракета взлетела? В игровом пространстве нам нужна условная планета с которой стартуем, кнопка старт и ракета. Что должна уметь ракета?
- Ждать старта
- Лететь
- Подвергаться влиянию модификаторов
- Останавливаться
То есть у нас появляется разное поведение/состояние ракеты, в зависимости от текущего состояния, ракета должна обеспечивать разное поведение. В программировании мы постоянно сталкиваемся с ситуацией, когда у объекта может быть много кардинально разных поведений.
Для сложных поведений объектов — лучше использовать поведенческие паттерны, например паттерн состояние. Для простых — начинающие программисты часто используют много много 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;
}
}
Расшифрую что происходит:
- Когда ждем — просто вращаем ракету по направлению к курсору мыши, таким образом задаём начальную траекторию
- Второе состояние — мы летим, разгоняем ракету в нужном направлении, и обновляем модель модификаторов на предмет появления объектов влияющих на траекторию
- Третье состояние это когда нам прилетает команда остановиться, тут отрабатываем всё чтобы ракета остановилась и переводим в состояние — мы полностью остановились.
- Последнее состояние — стоим ничего не делаем.
Удобство текущего паттерна — это всё очень легко расширяется и регулируется, но есть одно но, слабое звено — это когда у нас может быть состояние которое комбинирует ряд других состояний. Тут или флаговый инам, с усложнением обработки, или уже переходить на более «тяжелые» паттерны.
С ракетой разобрались. На очереди простой, но забавный объект — кнопка старта.
Кнопка старта
От неё требуется следующий функционал — нажали, она оповестила что на неё нажали.
Класс кнопки старт
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. В их методах мы и реализуем реакцию на нажатие, но тут есть несколько нюансов.
- В сцене должна присутствовать EventSystem, это объект/класс/компонент юнити, обычно создается когда мы создаем канвас для интерфейса, но его также можно создать самому.
- На камере должен присутствовать Physics RayCaster (это для 3д, для 2д графики там отдельный рейкастер)
- На объекте должен быть коллайдер
В проекте это выглядит так:
Теперь объект отслеживает нажатие и вызывается этот метод:
public void OnPointerDown(PointerEventData eventData)
{
ButtonStartPressed();
}
private void ButtonStartPressed()
{
if (isTriggered)
return;
isTriggered = true;
GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed());
Debug.Log("поехали");
}
Что тут происходит:
У нас есть булево поле в котором мы отслеживаем нажали кнопку или нет (это защита от многократного нажатия, чтобы у нас не запускался каждый раз сценарий старта).
Далее мы вызываем ивент — кнопка нажата, на который подписан класс ракета, и переводим ракету в состояние движения.
Немного забегая вперед — почему тут сплошь и рядом ивенты? Это событийно-ориентированное программирование, Во первых событийная модель дешевле постоянной обработки данных, с целью выяснения их изменений. Во вторых это та самая слабая связанность, нам не нужно на ракете знать что существует кнопка, что кто то её нажал и так далее, мы просто знаем что есть событие для старта, мы его получили и действуем. Далее — это событие интересно не только ракете, например на это же событие подписана панель с модификаторами, она скрывается при старте ракеты. Также это событие может быть интересно инпут контроллеру — и пользовательский ввод может не обрабатываться или обрабатываться по другому после старта ракеты.
Почему событийную парадигму не любят многие программисты? Потому-что тонна событий и подписок на эти события легко превращают код в лапшу, в которой вообще не понятно откуда начать и закончится ли это где то, не говоря о том что также надо следить за отпиской/подпиской и чтобы все объекты были живыми.
И именно поэтому для реализации ивентов я использую свой агрегатор ивентов, который по сути передаёт не ивенты, а контейнеры данных посредством ивентов, и классы подписываются на те данные которые им интересны. Также агрегатор сам следит за живыми объектами и выкидывает из подписчиков дохлые объекты. Благодаря перебросу контейнера, также возможна реализация инъекции, можно передать ссылку на интересующий нас класс. По контейнеру можно легко отследить кто эти данные обрабатывает и посылает. Для прототипирования — отличная вещь.
Вращение ракеты для определения стартовой траектории
По геймдизайну ракета должна уметь вращаться вокруг планеты, для определения начальной траектории, но не больше какого то угла. Вращение осуществляется тачем — ракета просто следит за пальцем и направлена всегда на то место куда мы ткнули в экран. Кстати как раз прототип позволил определить что это слабое место и возникает много пограничных с этим функционалом эпизодов связанных с управлением.
Но по порядку:
- Нам нужно чтобы ракета поворачивалась относительно планеты в сторону тача
- Нам нужно клампить угол поворота
Насчет поворота относительно планеты — можно хитро вращать вокруг оси и вычислять ось вращения, а можно просто создать объект пустышку с центром внутри планеты, переместить туда ракету, и спокойно вращать пустышку вокруг оси 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и вещи:
- Есть ли нажатие и его координаты
- Есть ли вертикальный свайп и насколько свайпаться
- Оперирую ли я с интерфейсом/модификаторами
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)
sith
25.05.2019 19:28Особенно за начинающего программиста
Я писал не про начинающего программиста, а про начинающего разработчика на Unity. Я до сих пор время от времени сам себя считаю таковым.
Мы рассматривали возможность нескольких вариантов ракет
Тут, как раз, и пригодится это дробление на компоненты, а не базовый общий класс.
Ну он и не знает, ракета только знает что ей прилетает ивент на старт
И про этот event не должна знать, как и какая-нибудь кнопка не должна знать про event StartGame, например. Ракета должна управляться извне (я писал про пример двигателя) или отдельным компонентом (набором компонентов), который можно заменить на другой. И вообще не должна знать что такое event.
что у меня есть проблема с позиционированием агрегатора
На мой взгляд проблема не с позиционированием, а с тем, что это слишком общее решение.
это по факту куча барахла который тащит с собой монобех
Верно.
лучше когда он действительно отдельный и прям либо ваще никак не связан с конкретным классами
Верно! Поэтому нужно писать именно такие компоненты. Чем меньше связность, тем лучше.
Я против изначального дробления на мифические компоненты
Придётся. От DOTS уже никуда не убежишь. Лучше начать сейчас, чем опоздать.
вот это как раз и есть нарушения KISS в чистом виде
Дробление не приводит к усложнению.
и поэтому активно пилят ECS для слабых мест
Это уже по сути случилось. Я понимаю про какую именно оптимизацию Вы пишете. Но всё же это самое «дробление», тем более в Вашем случае того точно стоит.Brightori Автор
25.05.2019 20:21Только что был по лекции про DOTS, не везде он зайдет, и там очень много писанины, далее:
1) Компоненты у Unity слабый элемент, поэтому я в большинстве случаев за наследование и композицию. Композиция если брать абстрактно и есть парадигма COOP. Просто либо мы пользуемся обертками Unity со всеми вытекающими, либо сами пишем и дробим функционал как нам хочется. И скажем так — текущие компоненты это для удобства инди разработчика, понавесил какой то функционал, запихал в публичные поля объекты, и оно взлетело.
2)И про этот event не должна знать
Не согласен, вполне себе в рамках событийной модели решение. Как еще ракете узнать что пора? Вешать более верхнюю сущность и хендлить там? Что еще будет делать эта сущность? Зачем она? Откуда ракета в принципе узнает что пора? В данный момент есть условный поток данных — старт ракеты, где может быть весь необходимый контекст. Всем кому интересны эти данные — реагируют.
3) Насчёт DOTS/ECS — ну это попытка пропихнуть DDD в более качественной обертке с волшебными штуками, но по факту у неё есть те же слабые места что и у DDD. Другой вопрос что при определенных задачах профит от DOTS очень крут. А вот то что писанины становится в разы больше и она не всегда оправдана — это факт. Порой для описания простой сущности нужны десятки объектов. Поэтому для простого прототипа казуалки я бы избегал нативного ECS.
sith
25.05.2019 22:47Только что был по лекции про DOTS, не везде он зайдет
На самом деле, даже не с точки зрения производительности, а именно с точки зрения архитектуры он зайдёт много где.
и там очень много писанины
Да, и при этом, как это ни странно, cоблюдаются все эти SOLID, KISS etc, в отличии от.
либо мы пользуемся обертками Unity со всеми вытекающими
Unity обязывает ими пользоваться (в DOTS в том числе, но под другим соусом) если нужно представление чего-либо на сцене. Тут важно понимать (и я писал об этом) где нужно использовать MonoBehaviour, а где нет.
Как еще ракете узнать что пора?
Этот вопрос созвучен с вопросом «как узнать кнопке (т.е. по сути такому-же визуальному объекту, что и ракета), что её нужно показать/скрыть?». Решения могут быть разными, но не изменение кода кнопки.
Откуда ракета в принципе узнает что пора?
Она и не должна. Вообще, для ракеты не должно быть понятия «пора». У неё (точнее двигателя) должны быть функции PrepareToStart(), SetTrust(float trust) и т.д. Ракета не должна быть настолько умной, чтобы «знать, что пора».
В данный момент есть условный поток данных — старт ракеты
И это проблема — сложно тестировать поведение (например, двигателей) — нужно писать отдельный функционал для тестов. Другая проблема — inputController который жёстко зашит внутри ракеты. Что если мы хотим управлять ракетой через тесты? Что если через сеть? Что если через обучающий сценарий, что если хотим записать её полёт и повторить потом? Если бы у ракеты было просто SetTrust(float trust) то всех этих проблем не было бы — делай с ней, что хочешь.
Brightori Автор
26.05.2019 00:14И это проблема — сложно тестировать поведение (например, двигателей) — нужно писать отдельный функционал для тестов. Другая проблема — inputController который жёстко зашит внутри ракеты. Что если мы хотим управлять ракетой через тесты? Что если через сеть? Что если через обучающий сценарий, что если хотим записать её полёт и повторить потом? Если бы у ракеты было просто SetTrust(float trust) то всех этих проблем не было бы — делай с ней, что хочешь.
В данном контексте тестирование супер изи, ибо есть конфиг в котором зашит параметр скорость — запихиваем разный конфиг, можно через инъекцию как раз агрегатором. Далее даём отмашку старт, далее присылаем ивенты добавления модификаторов. Тест занял бы от 2х с плюсом строчек строчек. Это легко делается тестовыми методами/классами, и можно проверить самые сложные сценарии типа применения одновременно нескольких модификаторов с разной силой влияния.
На самом деле я думаю уже можно закругляться, я понял вашу точку зрения. Я придерживаюсь другой и признаю большинство парадигм, и на мой взгляд всему своё место, COOP, OOP, AOP, Reactive, Event based, DDD.
И вопрос с подвохом — DDD придумали не сегодня, почему он не занял доминирующую позицию в программировании?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
И так далее.
В целом мне нравятся Ваши статьи и я просто хочу помочь советом. При этом, конечно, ни на чём не настаиваю — это просто советы и, несомненно, я могу ошибаться.
sith
Спасибо за статью. Небольшой Cod Review, если Вы не против.
Зачем понадобилось усложнять и делать родительский класс, если почти наверняка будет всего один тип ракет в игре (SOLID, KISS, YAGNI)?
Класс Rocket не должен знать про кнопку старта (он должен быть как можно тупее — помните KISS?). Вообще, Rocket перегружен и нарушает множество принципов, перечисленных в первой статье. Код ракеты должен только помогать отображать эту ракету и помогать изменять её положение на экране. При этом это должны быть несколько несвязных между собой компонентов на gameObject ракеты (и/или его частях). Ваш класс Rocket больше похож на RocketMovement. При этом может быть ещё RocketFire, RocketHealth, RocketFuel и т.д.
Код под каждым case должен быть вынесен в отдельный метод. Но, вообще, класс Rocket ничего не должен знать про inputController. У Rocket должен быть отдельный компонент Driver или Engine, который должен иметь что-то типа SetTrust(float trust);
Зачем нужен агрегатор событий ещё и видимый всеми? Почему не использовать обычный static Action someAction в нужных классах и подписки на него? Я видел Вашу статью про этот агрегатор но так и не увидел достоинств усложнения и замедления кода.
Неверное именование. События называют просто 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. В случае Вашего прототипа это невозможно — события и прочее глубоко зашиты в класс.
Brightori Автор
Спасибо за ответ и код ревью ) Особенно за начинающего программиста. Долго отвечал — был в кино, советую кстати Джонни Уика.
Мы рассматривали возможность нескольких вариантов ракет, одна из которых может смещаться в полёте.Итак:
1)
2) Ну он и не знает, ракета только знает что ей прилетает ивент на старт
3) Согласен, я просто придерживаюсь правила — если кода не очень много, можно оставить в кейсе. Но в отдельный метод будет правильнее.
4) В любом деле главное без фанатизма, инпут контроллер в данном ракурсе не верхняя сущность, а всего лишь источник данных для принятия решений, да, в нём есть лишние данные для ракеты, более расово правильно это было всё связать через интерфейс с конкретными штуками, но снаружи мы не можем менять данные, поэтому если торчат пара лишних для ракеты полей — ничего страшного. Всё равно эти данные будут необходимы, если не ракете, так более верхней сущности/объекту.
5) Я на самом деле понимаю что у меня есть проблема с позиционированием агрегатора, ибо под капотом там ивенты, механика работы у него — по сути потоки данных, а основная фича — именно контейнеры данных. Видимость всеми — ну в этом и фича, подписка на нужный тебе тип данных. В третьей части будут модификаторы которые просто присылают данные о модификаторе, может будет более очевидно, не говоря уже о инъекциях.
6) склонен согласиться.
7)
COOP люблю, хорошая штука, но надо взглянуть на вещи шире — компонент Unity, это по факту куча барахла который тащит с собой монобех. Выделять функционал в отдельный юнити компонент, лучше когда он действительно отдельный и прям либо ваще никак не связан с конкретным классами. Например компонент для дефолтной озвучки UI(клик, наведение и тд), он вообще может ни про кого не знать и действовать автономно. В случае с ракетой — у неё только поведение связанное с движением. Допустим у нас всё становится сложнее — появляется стрельба, защитные поля, хелс поинты и тд, и допустим появятся враги, у которых будет такой же функционал (чисто фантазии). То этот функционал лучше разнести по классам моделям. И будет модель движения, модель стрельбы и тд. Опять же если смотреть шире — класс модель, и есть компонент в каком то смысле, ничто не мешает внедрить этот класс любому заинтересованному лицу. Пример в этом коде — ForceModel forceModel, этот класс можно применить например к астериодам, и они тоже станут попадать под влияние модификаторов. Плюс не забываем про любимый ООП, и у наследников мы можем переопределить или расширить функционал. Я против изначального дробления на мифические компоненты, вот это как раз и есть нарушения KISS в чистом виде. Вместо простого класса с простым поведением, мы пытаемся заложиться в COOP, плодим классы, плодим монобехи, каждый крутит свой апдейт, складирует в памяти кучу гавна которое не используется и тд. Те же Unity поняли что COOP в Unity тупиковый для оптимизации, и поэтому активно пилят ECS для слабых мест, который как раз выигрывает в том числе за счёт того что не тащит с собой целый багаж ненужных штук, которые валяются в куче.
Brightori Автор
Вдогонку к пункту 7 и 4, я вообще за разумную композицию и агрегацию