Advertisement
ArXen42

Refactoring log

Mar 25th, 2016
374
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 27.28 KB | None | 0 0
  1. Refactor log:
  2. 1)Юнити используется только как представление, т.е. скрипты не содержат логики и данных, они выполняют только функции прослойки между игроком и внутренней логикой. Это не слишком важно сейчас, но по мере увеличения количества кода зависимость от редактора/сцены/тегов и прочих юнитековских понятий будет порождать глюки и неочевидности в коде.
  3.  
  4. 2)Объекты в космосе не должны делиться на типы на базовом уровне. Т.е. для игры должно быть совершенно не важно, чем является тело в космосе: планетой, пиратом Васей или ошметками этого пирата.
  5. На уровне представления:
  6. За идентификацию будут отвечать компоненты (компонент "Planet", например), через методы которых идет обращение к внутренней логике, см ниже.
  7. На уровне логики:
  8. а)Глобальный список капитанов кораблей. Там содержится (будет содержаться) вся информация: деньги, положение, собственность (корабли, товары на складах и т.д.). Улучшенная замена текущему PlayerState.
  9. б)В свою очередь, корабли являются космическими объектами (т.е. наследниками SpaceObject), находящимися в списке объектов в классе звездной системы (да, вселенная более не живет отдельно от менее присносущих объектов). Ссылки на них содержатся в полях собственности капитанов или в компонентах на их геймобъектах, если данный корабль находится на сцене.
  10.  
  11. 3)Сериализация. Описанная выше структура позволяет удобно проводить сериализацию. Сохраняемые данные не разбросаны по всему коду, а гарантированно содержатся только в классах объектов. Достаточно будет объявить атрибут в стиле [SerializableInformation] и с помощью простой рефлекции рекурсивно проходить "по всей вселенной" и делать автоматический слепок всех данных о состоянии мира в файл. Что-то похожее, кстати, действует в KSP, где каждое сохранение содержит данные вообще обо всем. Единственное но: связь капитан-собственность. Сам-то класс капитана сохранится точно таким же способом, но вот ссылки - с ними проблема.
  12. Таким образом, на данный момент файл сохранения будет содержать две секции: капитаны и вселенная, содержащая все остальное.
  13. Десериализация - тоже самое, но наоборот.
  14.  
  15. 4)Устройство вселенной. В текущей реализации проблемы с идентификацией систем и их поиском. Структура на замену:
  16. 4.1) UniverseMap: словарь звезд. В ключ - id.
  17. 4.2) StarSystem:
  18. Список всех SpaceObject'ов, находящихся в данной системе. Планеты, звезды, корабли, астероиды, бутылки с ромом - все здесь. Можно создать вспомогательные списки планет/кораблей/бочек с ромом, или даже свойство, аналогичное FindAllEquipment<EquipmentType> в структуре корабля. Когда корабль прыгает, он удаляется из текущей системы и добавляется в следующую, другие объекты аналогично.
  19.  
  20. 5)Про LevelBuilder. Предыдущая схема позволяет уменьшить количество высокоуровнего кода. Простой проход по звездной системе и вызов некоего "конструктора" вместо текущего монотонного спауна сначала планет, потом корабля и т.д.
  21.  
  22. Итак, с общей структурой хранения определился - SpaceObjects в специальном словаре в ЗС, доступ по ID. А теперь вот начал думать про то, как сделать в нормальном виде движение в т.ч. при удаленном просчете. Понятное дело, работать только с SpaceObject.Position, а если объект находится в одной ЗС с игроком, то дергать скриптом в Update этот Position и двигать GameObject на эту же позицию. А вот как это делать внутри - много вариантов.
  23. 1)Push модель. Событие начала хода, события обновления кадра, событие конца хода. В соответствии со своим поведением объект сам себя двигает при вызове указанных событий. Самый простой вариант, но крайне медленной из-за одновременной полной обработки всех объектов во всех системах. Можно оптимизировать, убрав рассчет каждый кадр для объектов вне системы игрока, но все равно слишком много.
  24. 2)Ленивая pull модель. Местоположение вычисляется только при обращению к полю Position. Работает для планет, да в общем для всего - у кораблей всегда есть траектория (или они не двигаются/летают по орбите как и планеты). Минус - если за ход не было обращений, то объект и не сдвинется никуда, несмотря на то, что у него может быть и траектория заготовлена. В общем, в чистом виде неприменимо.
  25. 3)Смешанная. Чистый pull только для небесных тел, потому что их позиция есть функция от даты. Остальные - если не в системе с игроком, то удаленный просчет будет происходить мгновенно при событии TurnStart или TurnEnd. Если с игроком, то нужно плавное движение - Position будет высчитываться в момент обращения и возвращать позицию на интерполированной траектории, как и сейчас + по событию TurnEnd выставить в конечную точку.
  26.  
  27.  
  28. Цель всего этого - вообще минимизировать разницу между обсчетом системы игрока и всех остальных без особого ущерба производительности. Пока получается так:
  29. 1)Планете вообще индифферентно. Ее Position возвращает позицию из простой функции от массы звезды, радиуса орбиты и даты (кстати дата теперь в DateTime, чтобы с переводом счетчика не париться). AsteroidBelt аналогично.
  30. 2)Корабли. Если в системе с игроком - полный просчет траектории и интерполированное движение. Position комбинированный - он как сохраняет значение позиции корабля между ходами, так и может считать интерполированные координаты, если ход запущен.
  31.  
  32. Таким образом, координаты небесных тел (кстати, в т.ч. и звезд, новая схема с SpaceObject позволяет запросто запилить бинарные системы без ущерба базовой логике, в отличие от старой) рассчитываются при обращении (если корабль в удаленной системе ищет ближайшую планету, то он в любом случае обратится к ним ко всем, т.е. получит актуальные данные) - никакого оверхеда. Если корабль в удаленной системе, то вызовутся упрощенные алгоритмы по событию WorldCtl.TurnStop. Если рядом с игроком, то его будет каждый кадр дергать юнька и заставлять обновлять свою позицию, как обычно.
  33.  
  34.  
  35. Касательно отслеживания объектов в системе. Если с удалением проблем не возникает, то с появлением их две:
  36. 1)Как узнать, что объект прилетел в систему игрока?
  37. Самое простое, что приходит в голову - обычное событие, вызываемое в StarSystem.Add. Скрипт инициализатора, подписанный на это дело, получает из параметров события SpaceObject и вызывает его инициализатор. Сделано.
  38. 2)А вот как вызывать инициализатор?
  39. Перебор в стиле if (spaceObject is Planet)... else if... не есть хороший вариант. Добавлять ссылку на инициализатор GameObject в сам SpaceObject нельзя, т.к. это ломает логику модель - представление (а таких "добавлений" потом может всплыть большое количество).
  40. В итоге было сделано по скрипту инициализатоу на каждый тип SpaceObject с методом, подписанным на событие StarSystem.SpaceObjectAdded и с одной проверкой типа внутри. Принцип подстановки Лисков идет лесом, едем дальше.
  41.  
  42. Про ввод. Старый InputController есть ужас на крыльях кода из-за лишней самостоятельности. Не факт, что это хорошая идея, но возможно стоит сделать его децентрализованным - скрипты в стиле CameraMovingContoller, отвечающие за обработку ввода для своих объектов. Также прямое считывание кнопок есть плохо, для этих целей в юнити есть Input Manager с возможностью настройки осей юзверем. Вообще, там еще есть своя система событий, но она пока не показалась мне удобнее в использовании, чем обычные делегаты и события. Посмотрим, как пойдет с простой схемой, пока пара скриптов работает нормально.
  43.  
  44. А за вводом следует система управления гуем. Главная ее задача - разграничить логические "экраны". Т.е. она должна выполнять функции менеджера GameObject'ов.
  45. Кроме того, важно разграничить обработку ввода - клик по galaxyMap не должен вызывать событие запроса пересчета траектории. Частично эта проблема может решиться размещением контроллеров ввода на переключаемых объектах, но только частично. Кроме того, в некоторых случаях может понадобиться ограничить включение/выключение каких-либо экранов. Надо подумать.
  46. Попробуем такую схему:
  47. 1)Интерфейс IGUIScreen, содержащий событие переключения и метод его вызова. На событие можно подписать SetActive нужных GameObject'ов, переключение какого-нибудь контроллера ввода и т.д.
  48. 2)Его наследники, в которых можно реализовать проверку определенных условий перед вызовом, например, запретить включение, если ход запущен.
  49. 3)GUIScreensManager, осуществляющий управление экземплярами IGUIScreen. В простейшем случае - просто доступ к экземплярам GUIScreen, при необходимости можно реализовать более сложные правила переключения.
  50.  
  51. Ну а теперь на очереди самое веселое - имплементация базовой логики корабля и его владельца.
  52. Шаг 1: Ввести уже список капитанов. Для простоты пусть пока в классе капитана будет единственная ссылка на его корабль (: SpaceObject).
  53. Шаг 2: Восстановить работоспособность ShipStructure (пока без гипердвигателя, т.к. у звездных систем еще не определены координаты)
  54. Шаг 3: Научить Ship : SpaceObject летать.
  55. Шаг 4: Научить его GameObject-эго летать. Тут практически без изменений, просто получаем Ship.Position и Ship.Rotation.
  56.  
  57. Про шаг 3 подробнее, как-никак основа всей корабельной логики.
  58. Для начала, кораблю нужен интерфейс, позволяющий указать цель полета. В прошлой версии был enum hell с кучей состояний полета, т.ч. этот путь, по крайней мере в том исполнении, плохой.
  59. Как можно заметить, у корабля непрямое управление - игрок может задать только цель маршрута, траекторию корабль строит сам. Значит, должен быть некий класс "задачи", в соответствии с которой корабль выстраивает свою траекторию и действия.
  60. Например:
  61. 1)Лететь в точку
  62. 2)Следовать за объектом
  63. 3)Лететь к планете и выполнить посадку (в простом случае - подтип первого, в более сложном - предварительный рассчет позиции планеты и полет сразу к ней)
  64. 4)Лететь к точке гиперпрыжка, после чего совершить его.
  65.  
  66. Моя ошибка была еще в том, что я пытался каждую из этих задач реализовать монолитно, в то время как для большинство из них это последовательность действий (например полет в точку + действие).
  67. Кажется, неплохой идеей будет создать в классе корабля обычную Queue<ITask>, который тот будет по мере сил выполнять. Вопрос в том, что будет в себе этот ITask содержать и каков протокол взаимодействия с кораблем.
  68. Т.к. механика игры требует, чтобы будущую траектори корабля можно было визуализировать, ITask должен уметь отдавать ее в виде списка Vector2. Или в виде чего-нибудь более сложного, не суть.
  69. Кстати, про механику. Это может показаться странным, но все действующие лица на момент начала хода должны знать свою траекторию (и уметь сообщать окружающим). Это вытекает из походовой механики игры. В циве она решена поочередными ходами, но в нашем случае больше подходит КР/StarControl механика, в которой ходы происходят одновременно, но по заранее просчитанному сценарию. Будущим улучшениям инерциальности траектории от этого тоже будет проще.
  70. Возникает вопрос: кто будет заниматься выводом позиции - реализация ITask или Ship.Position будут сами интерполировать свою позицию из траектории, полученной от CurrentTask. С одной стороны, траектория вроде как полностью определяет движение корабля за этот ход (пока речь идет про активную систему, в остальных, естественно, будет нужна только конечная позиция) и он может и сам разобраться, как по ней лететь. С другой, если корабль на статичной орбите, логично будет траекторию использовать только для декоративных целей, а реальное движение выполнять по формулам, аналогичным планетарным, т.е. это уже определяется ITask. Но сути для внешнего наблюдателя это особо не меняет, т.ч. пока пусть будет в Task, а там если проблемы будут, можно и перенести. Т.е. Ship.Position и Ship.Rotation будут ссылаться на аналогичные свойства в CurrentTask.
  71.  
  72. Фактически, выше описано поведение FlyToPointTask. Для сложных последовательностей задач ситуация такая:
  73. 1)Если нам нужен гиперпрыжок, мы ставим первой задачей полет в точку, после ее выполнения управление переходит непосредственно задаче гиперпрыжа, которая, скажем, резко ускоряет корабль так, чтобы он вылетел за границы экрана.
  74. 2)А вот с посадкой на планету не так все стройно. Дело в том, что планета двигается. Мы знаем, где она будет в любой момент времени, но не можем такого сказать о себе - во время полета двигатель вполне может и поменять свои характеристики. Т.е. если будет задача FlyToPointTask и в качестве цели будет стоять положение планеты через несколько суток, а в полете внезапно сбросят энергию с двигателя, по прилету в точку планеты там уже не будет и следующая задача весело выкинет исключение. Вылезает проблема вот такой вот очереди задач - несмотря на то, что корабль вроде как на планету летит, по CurrentTask ничего сказать нельзя, т.к. она до последнего будет обычным FlyToPointTask. А получается, что следующая за ней задача посадки на планету должна ее контроллировать, корректируя траекторию при необходимости, хотя, по логике очереди, она должна быть неактивна.
  75.  
  76. Окей. Видоизменим логику. Очередь задач из Ship выкидываем, оставляем только CurrentTask. Интерфейс ITask не меняется. Да и FlyToPointTask тоже. А вот поведение PlanetLandTask изменится. Она будет внутри себя создавать задачу полета в точку, при необходимости пересчитывать целевую позицию. Когда нужная локация будет достигнута, включит уже свое поведение.
  77. Аналогично трансформируется гиперпрыжок - он становится оберткой над FlyToPointTask.
  78. Посмотрим, как это получится в виде кода.
  79.  
  80. 1)Клик мыши
  81. 2)Инициализируется и назначается кораблю таск FlyToPointTask.
  82. 3)TrajectoryDrawer рисует траекторию из Ship.CurrentTask. Ход неактивен, свойства Ship.Position/Rotation возвращают Ship.position/rotation.
  83. 4)Начинается ход. Вместо Ship.position/rotation соответствующие свойства отдают ITask.Position и ITask.Rotation.
  84. 5)Конец хода, вызывается событие TurnStop. У ITask запрашиваются конечные значения Ship.rotation и position.
  85.  
  86. Параллельно с реализацией корабля встал в полный рост вопрос с сериализацией. Думаю, скоро буду и ее делать, т.ч. стоит заранее ее продумать.
  87. JSONObject библиотека, конечно, хорошая, но слишком уж муторно вручную заполнять поля. Полностью автоматической сериализации с помощью JsonUtility или JSON.NET сделать не получится из-за сложности сериализуемых классов, наличия у них конструкторов и инкапсулированности сериализуемых членов. Поэтому нужна схема с простыми классами-прокси, содержащими только публичные поля и массивы других проксей, наподобие этой:
  88. 1)Прокси к UniverseMap содержит массив проксей ЗС.
  89. 2)Прокси ЗС содержит в себе массив проксей SO.
  90. 3)Прокси SO это уже иерархия классов - базовый класс, умеющий в клонирование простейших членов SO (ID, Name) и его наследники, соответствующие наследникам SO, копирующие в себя все специфичные характеристики конкретных объектов. Для планеты/звезды это тупо набор характеристик (но для планеты это также ID своей звезды) + Position, если надо, для корабля - координаты, вращение, содержащийся внутри Spacecraft (со своим прокси, а как же), CurrentTask пока не трогаем.
  91. Прокси должен уметь копировать в себя данные нужного объекта (после чего какой-нибудь JsonUtility его автоматом сериализует в строку) и загружать их обратно. Последнее идет примерно таким каскадом:
  92. 0)Иерархия проксей автоматически восстанавливается из файла сохранения.
  93. 1)Прокси к UniverseMap просто идет по проксям ЗС и получает у них экземпляры StarSystem, добавляя их в словарь.
  94. 2)Прокси к ЗС создает экземпляр StarSystem с нужным ID и заполняет ее десериализованными данными, в т.ч. получая SO из следующего пункта и добавляя их в свой словарь.
  95. 3)...
  96. Возможны вариации на тему хранения всех SO в одном массиве и с последующим добавлением их в словари систем с нужным LocationID, но особого смысла вроде как и нет.
  97.  
  98. Данные о игроках будут сериализоваться аналогично. Но есть непродуманный момент с кораблями. А именно, разграничения или отсутствия разграничения между конфигом шасси (или корпуса, не важно), который сейчас представлен IPDK.json и конкретным экземпляром этого корабля, с оборудованием, текущим распределением энергии и т.д. Скорее всего, в разделении смысла нет, и в качестве конфига можно хранить тупо пустой корабль, а уж дальше, кому надо, его заполнят оборудованием и сохранят в Ship.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement