Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Refactor log:
- 1)Юнити используется только как представление, т.е. скрипты не содержат логики и данных, они выполняют только функции прослойки между игроком и внутренней логикой. Это не слишком важно сейчас, но по мере увеличения количества кода зависимость от редактора/сцены/тегов и прочих юнитековских понятий будет порождать глюки и неочевидности в коде.
- 2)Объекты в космосе не должны делиться на типы на базовом уровне. Т.е. для игры должно быть совершенно не важно, чем является тело в космосе: планетой, пиратом Васей или ошметками этого пирата.
- На уровне представления:
- За идентификацию будут отвечать компоненты (компонент "Planet", например), через методы которых идет обращение к внутренней логике, см ниже.
- На уровне логики:
- а)Глобальный список капитанов кораблей. Там содержится (будет содержаться) вся информация: деньги, положение, собственность (корабли, товары на складах и т.д.). Улучшенная замена текущему PlayerState.
- б)В свою очередь, корабли являются космическими объектами (т.е. наследниками SpaceObject), находящимися в списке объектов в классе звездной системы (да, вселенная более не живет отдельно от менее присносущих объектов). Ссылки на них содержатся в полях собственности капитанов или в компонентах на их геймобъектах, если данный корабль находится на сцене.
- 3)Сериализация. Описанная выше структура позволяет удобно проводить сериализацию. Сохраняемые данные не разбросаны по всему коду, а гарантированно содержатся только в классах объектов. Достаточно будет объявить атрибут в стиле [SerializableInformation] и с помощью простой рефлекции рекурсивно проходить "по всей вселенной" и делать автоматический слепок всех данных о состоянии мира в файл. Что-то похожее, кстати, действует в KSP, где каждое сохранение содержит данные вообще обо всем. Единственное но: связь капитан-собственность. Сам-то класс капитана сохранится точно таким же способом, но вот ссылки - с ними проблема.
- Таким образом, на данный момент файл сохранения будет содержать две секции: капитаны и вселенная, содержащая все остальное.
- Десериализация - тоже самое, но наоборот.
- 4)Устройство вселенной. В текущей реализации проблемы с идентификацией систем и их поиском. Структура на замену:
- 4.1) UniverseMap: словарь звезд. В ключ - id.
- 4.2) StarSystem:
- Список всех SpaceObject'ов, находящихся в данной системе. Планеты, звезды, корабли, астероиды, бутылки с ромом - все здесь. Можно создать вспомогательные списки планет/кораблей/бочек с ромом, или даже свойство, аналогичное FindAllEquipment<EquipmentType> в структуре корабля. Когда корабль прыгает, он удаляется из текущей системы и добавляется в следующую, другие объекты аналогично.
- 5)Про LevelBuilder. Предыдущая схема позволяет уменьшить количество высокоуровнего кода. Простой проход по звездной системе и вызов некоего "конструктора" вместо текущего монотонного спауна сначала планет, потом корабля и т.д.
- Итак, с общей структурой хранения определился - SpaceObjects в специальном словаре в ЗС, доступ по ID. А теперь вот начал думать про то, как сделать в нормальном виде движение в т.ч. при удаленном просчете. Понятное дело, работать только с SpaceObject.Position, а если объект находится в одной ЗС с игроком, то дергать скриптом в Update этот Position и двигать GameObject на эту же позицию. А вот как это делать внутри - много вариантов.
- 1)Push модель. Событие начала хода, события обновления кадра, событие конца хода. В соответствии со своим поведением объект сам себя двигает при вызове указанных событий. Самый простой вариант, но крайне медленной из-за одновременной полной обработки всех объектов во всех системах. Можно оптимизировать, убрав рассчет каждый кадр для объектов вне системы игрока, но все равно слишком много.
- 2)Ленивая pull модель. Местоположение вычисляется только при обращению к полю Position. Работает для планет, да в общем для всего - у кораблей всегда есть траектория (или они не двигаются/летают по орбите как и планеты). Минус - если за ход не было обращений, то объект и не сдвинется никуда, несмотря на то, что у него может быть и траектория заготовлена. В общем, в чистом виде неприменимо.
- 3)Смешанная. Чистый pull только для небесных тел, потому что их позиция есть функция от даты. Остальные - если не в системе с игроком, то удаленный просчет будет происходить мгновенно при событии TurnStart или TurnEnd. Если с игроком, то нужно плавное движение - Position будет высчитываться в момент обращения и возвращать позицию на интерполированной траектории, как и сейчас + по событию TurnEnd выставить в конечную точку.
- Цель всего этого - вообще минимизировать разницу между обсчетом системы игрока и всех остальных без особого ущерба производительности. Пока получается так:
- 1)Планете вообще индифферентно. Ее Position возвращает позицию из простой функции от массы звезды, радиуса орбиты и даты (кстати дата теперь в DateTime, чтобы с переводом счетчика не париться). AsteroidBelt аналогично.
- 2)Корабли. Если в системе с игроком - полный просчет траектории и интерполированное движение. Position комбинированный - он как сохраняет значение позиции корабля между ходами, так и может считать интерполированные координаты, если ход запущен.
- Таким образом, координаты небесных тел (кстати, в т.ч. и звезд, новая схема с SpaceObject позволяет запросто запилить бинарные системы без ущерба базовой логике, в отличие от старой) рассчитываются при обращении (если корабль в удаленной системе ищет ближайшую планету, то он в любом случае обратится к ним ко всем, т.е. получит актуальные данные) - никакого оверхеда. Если корабль в удаленной системе, то вызовутся упрощенные алгоритмы по событию WorldCtl.TurnStop. Если рядом с игроком, то его будет каждый кадр дергать юнька и заставлять обновлять свою позицию, как обычно.
- Касательно отслеживания объектов в системе. Если с удалением проблем не возникает, то с появлением их две:
- 1)Как узнать, что объект прилетел в систему игрока?
- Самое простое, что приходит в голову - обычное событие, вызываемое в StarSystem.Add. Скрипт инициализатора, подписанный на это дело, получает из параметров события SpaceObject и вызывает его инициализатор. Сделано.
- 2)А вот как вызывать инициализатор?
- Перебор в стиле if (spaceObject is Planet)... else if... не есть хороший вариант. Добавлять ссылку на инициализатор GameObject в сам SpaceObject нельзя, т.к. это ломает логику модель - представление (а таких "добавлений" потом может всплыть большое количество).
- В итоге было сделано по скрипту инициализатоу на каждый тип SpaceObject с методом, подписанным на событие StarSystem.SpaceObjectAdded и с одной проверкой типа внутри. Принцип подстановки Лисков идет лесом, едем дальше.
- Про ввод. Старый InputController есть ужас на крыльях кода из-за лишней самостоятельности. Не факт, что это хорошая идея, но возможно стоит сделать его децентрализованным - скрипты в стиле CameraMovingContoller, отвечающие за обработку ввода для своих объектов. Также прямое считывание кнопок есть плохо, для этих целей в юнити есть Input Manager с возможностью настройки осей юзверем. Вообще, там еще есть своя система событий, но она пока не показалась мне удобнее в использовании, чем обычные делегаты и события. Посмотрим, как пойдет с простой схемой, пока пара скриптов работает нормально.
- А за вводом следует система управления гуем. Главная ее задача - разграничить логические "экраны". Т.е. она должна выполнять функции менеджера GameObject'ов.
- Кроме того, важно разграничить обработку ввода - клик по galaxyMap не должен вызывать событие запроса пересчета траектории. Частично эта проблема может решиться размещением контроллеров ввода на переключаемых объектах, но только частично. Кроме того, в некоторых случаях может понадобиться ограничить включение/выключение каких-либо экранов. Надо подумать.
- Попробуем такую схему:
- 1)Интерфейс IGUIScreen, содержащий событие переключения и метод его вызова. На событие можно подписать SetActive нужных GameObject'ов, переключение какого-нибудь контроллера ввода и т.д.
- 2)Его наследники, в которых можно реализовать проверку определенных условий перед вызовом, например, запретить включение, если ход запущен.
- 3)GUIScreensManager, осуществляющий управление экземплярами IGUIScreen. В простейшем случае - просто доступ к экземплярам GUIScreen, при необходимости можно реализовать более сложные правила переключения.
- Ну а теперь на очереди самое веселое - имплементация базовой логики корабля и его владельца.
- Шаг 1: Ввести уже список капитанов. Для простоты пусть пока в классе капитана будет единственная ссылка на его корабль (: SpaceObject).
- Шаг 2: Восстановить работоспособность ShipStructure (пока без гипердвигателя, т.к. у звездных систем еще не определены координаты)
- Шаг 3: Научить Ship : SpaceObject летать.
- Шаг 4: Научить его GameObject-эго летать. Тут практически без изменений, просто получаем Ship.Position и Ship.Rotation.
- Про шаг 3 подробнее, как-никак основа всей корабельной логики.
- Для начала, кораблю нужен интерфейс, позволяющий указать цель полета. В прошлой версии был enum hell с кучей состояний полета, т.ч. этот путь, по крайней мере в том исполнении, плохой.
- Как можно заметить, у корабля непрямое управление - игрок может задать только цель маршрута, траекторию корабль строит сам. Значит, должен быть некий класс "задачи", в соответствии с которой корабль выстраивает свою траекторию и действия.
- Например:
- 1)Лететь в точку
- 2)Следовать за объектом
- 3)Лететь к планете и выполнить посадку (в простом случае - подтип первого, в более сложном - предварительный рассчет позиции планеты и полет сразу к ней)
- 4)Лететь к точке гиперпрыжка, после чего совершить его.
- Моя ошибка была еще в том, что я пытался каждую из этих задач реализовать монолитно, в то время как для большинство из них это последовательность действий (например полет в точку + действие).
- Кажется, неплохой идеей будет создать в классе корабля обычную Queue<ITask>, который тот будет по мере сил выполнять. Вопрос в том, что будет в себе этот ITask содержать и каков протокол взаимодействия с кораблем.
- Т.к. механика игры требует, чтобы будущую траектори корабля можно было визуализировать, ITask должен уметь отдавать ее в виде списка Vector2. Или в виде чего-нибудь более сложного, не суть.
- Кстати, про механику. Это может показаться странным, но все действующие лица на момент начала хода должны знать свою траекторию (и уметь сообщать окружающим). Это вытекает из походовой механики игры. В циве она решена поочередными ходами, но в нашем случае больше подходит КР/StarControl механика, в которой ходы происходят одновременно, но по заранее просчитанному сценарию. Будущим улучшениям инерциальности траектории от этого тоже будет проще.
- Возникает вопрос: кто будет заниматься выводом позиции - реализация ITask или Ship.Position будут сами интерполировать свою позицию из траектории, полученной от CurrentTask. С одной стороны, траектория вроде как полностью определяет движение корабля за этот ход (пока речь идет про активную систему, в остальных, естественно, будет нужна только конечная позиция) и он может и сам разобраться, как по ней лететь. С другой, если корабль на статичной орбите, логично будет траекторию использовать только для декоративных целей, а реальное движение выполнять по формулам, аналогичным планетарным, т.е. это уже определяется ITask. Но сути для внешнего наблюдателя это особо не меняет, т.ч. пока пусть будет в Task, а там если проблемы будут, можно и перенести. Т.е. Ship.Position и Ship.Rotation будут ссылаться на аналогичные свойства в CurrentTask.
- Фактически, выше описано поведение FlyToPointTask. Для сложных последовательностей задач ситуация такая:
- 1)Если нам нужен гиперпрыжок, мы ставим первой задачей полет в точку, после ее выполнения управление переходит непосредственно задаче гиперпрыжа, которая, скажем, резко ускоряет корабль так, чтобы он вылетел за границы экрана.
- 2)А вот с посадкой на планету не так все стройно. Дело в том, что планета двигается. Мы знаем, где она будет в любой момент времени, но не можем такого сказать о себе - во время полета двигатель вполне может и поменять свои характеристики. Т.е. если будет задача FlyToPointTask и в качестве цели будет стоять положение планеты через несколько суток, а в полете внезапно сбросят энергию с двигателя, по прилету в точку планеты там уже не будет и следующая задача весело выкинет исключение. Вылезает проблема вот такой вот очереди задач - несмотря на то, что корабль вроде как на планету летит, по CurrentTask ничего сказать нельзя, т.к. она до последнего будет обычным FlyToPointTask. А получается, что следующая за ней задача посадки на планету должна ее контроллировать, корректируя траекторию при необходимости, хотя, по логике очереди, она должна быть неактивна.
- Окей. Видоизменим логику. Очередь задач из Ship выкидываем, оставляем только CurrentTask. Интерфейс ITask не меняется. Да и FlyToPointTask тоже. А вот поведение PlanetLandTask изменится. Она будет внутри себя создавать задачу полета в точку, при необходимости пересчитывать целевую позицию. Когда нужная локация будет достигнута, включит уже свое поведение.
- Аналогично трансформируется гиперпрыжок - он становится оберткой над FlyToPointTask.
- Посмотрим, как это получится в виде кода.
- 1)Клик мыши
- 2)Инициализируется и назначается кораблю таск FlyToPointTask.
- 3)TrajectoryDrawer рисует траекторию из Ship.CurrentTask. Ход неактивен, свойства Ship.Position/Rotation возвращают Ship.position/rotation.
- 4)Начинается ход. Вместо Ship.position/rotation соответствующие свойства отдают ITask.Position и ITask.Rotation.
- 5)Конец хода, вызывается событие TurnStop. У ITask запрашиваются конечные значения Ship.rotation и position.
- Параллельно с реализацией корабля встал в полный рост вопрос с сериализацией. Думаю, скоро буду и ее делать, т.ч. стоит заранее ее продумать.
- JSONObject библиотека, конечно, хорошая, но слишком уж муторно вручную заполнять поля. Полностью автоматической сериализации с помощью JsonUtility или JSON.NET сделать не получится из-за сложности сериализуемых классов, наличия у них конструкторов и инкапсулированности сериализуемых членов. Поэтому нужна схема с простыми классами-прокси, содержащими только публичные поля и массивы других проксей, наподобие этой:
- 1)Прокси к UniverseMap содержит массив проксей ЗС.
- 2)Прокси ЗС содержит в себе массив проксей SO.
- 3)Прокси SO это уже иерархия классов - базовый класс, умеющий в клонирование простейших членов SO (ID, Name) и его наследники, соответствующие наследникам SO, копирующие в себя все специфичные характеристики конкретных объектов. Для планеты/звезды это тупо набор характеристик (но для планеты это также ID своей звезды) + Position, если надо, для корабля - координаты, вращение, содержащийся внутри Spacecraft (со своим прокси, а как же), CurrentTask пока не трогаем.
- Прокси должен уметь копировать в себя данные нужного объекта (после чего какой-нибудь JsonUtility его автоматом сериализует в строку) и загружать их обратно. Последнее идет примерно таким каскадом:
- 0)Иерархия проксей автоматически восстанавливается из файла сохранения.
- 1)Прокси к UniverseMap просто идет по проксям ЗС и получает у них экземпляры StarSystem, добавляя их в словарь.
- 2)Прокси к ЗС создает экземпляр StarSystem с нужным ID и заполняет ее десериализованными данными, в т.ч. получая SO из следующего пункта и добавляя их в свой словарь.
- 3)...
- Возможны вариации на тему хранения всех SO в одном массиве и с последующим добавлением их в словари систем с нужным LocationID, но особого смысла вроде как и нет.
- Данные о игроках будут сериализоваться аналогично. Но есть непродуманный момент с кораблями. А именно, разграничения или отсутствия разграничения между конфигом шасси (или корпуса, не важно), который сейчас представлен IPDK.json и конкретным экземпляром этого корабля, с оборудованием, текущим распределением энергии и т.д. Скорее всего, в разделении смысла нет, и в качестве конфига можно хранить тупо пустой корабль, а уж дальше, кому надо, его заполнят оборудованием и сохранят в Ship.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement