Это руководство является ставит перед собой цель дать как можно большие знания об кодбазе СС14 как таковой, так и об аспектах ECS и в целом нетворкинга. Тут не содержится никакая информация об с-шарпе как таковом, так как документация майков вполне неплохо справляется с целью обучения этому языку, его принципам, ООП. Можно считать, что это руководство способно дополнить его и дать знания, которые помогут помимо банального знания синтаксиса и парадигмы ООП, будут содержать в себе информацию реально полезную для разработки сски. # Введение Стоит начать с самых основ; СС14 разработана на самописном движке робаст тулбокс (далее РТ), использует си-шарп и парадигму ECS. ECS это достаточно простая концепция, разбивающая весь проект на три составные части: 1. E - entities, сущности 2. C - components, компоненты 3. S - systems, системы Позже мы разберём их отдельно подробнее, а пока хочу обратить внимание на то, как именно разделён РТ и сама игра (в дальнейшем вместо "самой игры" я буду использовать слово контент, так как помимо удобства в написании, этот вариант ещё и единственно верное определение этой самой "игры" как таковой). Движок разработан учитывая необходимость в нетворкинге (разберём его отдельно позже), являющимся основной причиной разделения движка, а следовательно, и самой игры на две составные части - клиент и сервер. Помимо этого существует также шаред, это часть кода, которая может проигрываться как на клиенте, так и на сервере. РТ использует сериализацию при помощи yaml, да-да, те самые прототипы это лишь набор сериализованных сущностей и компонентов, не правда-ли чудесно? # ECS Давайте разберём ранее упомянутую ECS подробнее; **Сущности** - сами по себе они не хранят почти никакой информации, но при этом являются основным костяком для работы систем и сериализации, благодаря тому, что им можно присваивать компоненты содержащие в себе информацию. **Компоненты** - основной контейнер для хранения информации о сущности. **Системы** - совокупность методов, работающих с компонентами и сущностями. Приведу пример; существует сущность. Она не содержит в себе никаких компонентов, кроме пары технических. Она не видна глазу, она не способна двигаться, она не способна разрушаться. Но стоит нам добавить `TransformComponent` ответственный за местоположение сущности в мире и у неё теперь есть местоположение! Всё это благодоря тому, что `TransformSystem` просчитывает логику перемещения объекта в мире, а сам `TransformComponent` хранит в себе всю необходимую систему для работы системы, локальные координаты, координаты карты, грида, самой сущности. Карту с этой сущностью можно сериализовать (сохранить), сохранив информацию об компонентах и самой сущности. --- ## Сущности Сущности в РТ представлены цифровым значением (uid). --- ## Компоненты Компоненты являются классами, и имею более сложную структуру, чем сущности. Во первых, компонент должен наследоваться от класса компонента: ``` c# public sealed partial class ExampleComponent : Component ``` Во вторых, для того, чтобы он действительно считался компонентом ему требуется соответствующий атрибьют - `RegisterComponent`: ``` c# [RegisterComponent] public sealed partial class ExampleComponent : Component ``` ### Тело компонента Стоит дать несколько уточнений по атрибьютам и самим полям. Для начала, добавим вышепреведённому компоненту поле, например, с float. ``` c# [RegisterComponent] public sealed partial class ExampleComponent : Component { public float ExampleField = 5; } ``` Мы - молодцы; компонент получил поле, которое может меняться системами. Добавим к нему атрибьют ViewVariables. Он позволит просматривать значения в панели этих самых ViewVariables прямо в игре. ``` c# [RegisterComponent] public sealed partial class ExampleComponent : Component { [ViewVariables] public float ExampleField = 5; } ``` Но это только начало; важнейший эллемент в компонентах - поля с атрибьютом **`DataField`**. Что они делают? Они позволяют сериализовывать поля. Как и было сказано ранее - сериализация происходит в yaml. Это значит, что поля, которые не содержат `DataField` сохраняться в yaml не могут, следовательно в прототипах (которые фактически являются уже сохранёнными сущностями-заготовками) мы изменять значения таких полей не сможем, ровно также как и не сможем сохранять значения этих полей вместе с картой. Т.е. если у вас, например, есть прототип холодильника, и у него есть компонент, отвечающий за его температуру, а поля в этом компоненте не сериализуются, то при попытке сохранения этого холодильника вместе с картой, значения в этом поле компонента будут сбрасываться к обычным. Тоже самое и в самих прототипах - сколько бы вы не пытались менять значения в самом yaml - в игре изменений не будет. Важно также уточнить неверное использование аргументов, предоставляемых атрибьютуту, например: ``` c# [RegisterComponent] public sealed partial class ExampleComponent : Component { [DataField(exampleField)] public float ExampleField = 5; } ``` Это ошибка. У датафилдов есть названия, и эти названия генерируются автоматически - они являются названием самого поля, при этом декапитализируя первую букву. Так, например, тоже самый код, но без названия поля будет работать также, но при этом выглядеть чище: ``` c# [RegisterComponent] public sealed partial class ExampleComponent : Component { [DataField] public float ExampleField = 5; } ``` ``` yaml - type: Example exampleField: 10 ``` Стоит также уточнить важную детать - датафилд содержит в себе атрибьют `ViewVariables`, поэтому его указание, например: ``` c# [RegisterComponent] public sealed partial class ExampleComponent : Component { [DataField, ViewVariables] public float ExampleField = 5; } ``` Является ошибкой. --- Я так-же хочу поговорить об самих полях. Несмотря на то, что я говорил о том, что в этом руководстве не будет советов по си-шарп, я просто обязан указать несколько деталей, без которых этот блок будет неполным, так как некоторые путаются в этом или не могут сформулировать запрос, чтобы найти ответ. Речь пойдёт об значениях полей по умолчанию; в отличии от переменных внутри методов поле класса, может быть объявлено без присваивания, и тогда оно примет значение по-умолчанию. Так например в ``` c# [RegisterComponent] public sealed partial class ExampleComponent : Component { [DataField] public float ExampleField; } ``` поле `ExampleField` будет не null, а значением float по-умолчанию, т.е. будет равно 0. А вот в ``` c# [RegisterComponent] public sealed partial class ExampleComponent : Component { [DataField] public float? ExampleField; } ``` Оно уже будет null. --- ## Системы Если компоненты и сущности это просто контейнеры для хранения информации, то системы это вещь хранящая логику для их работы. Первое, и самое важное - все системы должны наслодоваться от базового класса - `EntitySystem` ``` c# public sealed class ExampleSystem : EntitySystem ``` Он содержит полезные методы для перегрузки, например `Update()` и методы `EntityManager`, вот несколько из них: ``` c# // Создаёт сущность с определённым прототипом в нуллспейсе Spawn("прототип сущности") // Удаляет сущность с определённой uid. Del("uid сущности") // Проверяет, есть-ли у сущности компонент. HasComp<"тип компонента">("uid сущности") // Проверяет, есть-ли у сущности компонент, и возвращет его с out. TryComp<"тип компонента">("uid сущности", out "компонент (если он есть)") // Проверяет, есть-ли у сущности компонент // Использование этого метода предпочтительнее, если у нас уже есть переменная с типом необходимого нам компонента. Resolve("uid сущности", ref "переменная компонента, существование которого должно быть доказано") ``` Это далеко не все прокси методы для работы в `EntitySystem`. В качестве примера приведу написание простейшего метода: ``` c# private void ExampleMethod(EntityUid uid, ExampleComponent? comp) { if (!Resolve(uid, ref comp)) return; if (HasComp<ItemComponent>(uid) && TryComp<ArmorComponent>(uid, out var armor)) { Spawn("прототип для спавна", Transform(uid).Coordinates); RemComp(uid, armor); } } ``` Это метод, принимающий в качестве аргумента `uid` сущности и компонент (опционально). Он заресолвит компонент, который является нулл на нашей сущности и присвоит ему значение компонента с неё, затем проверит, есть-ли у сущности компонент предмета, и брони (при этом компонент брони он так-же ещё и вернёт, что может быть полезно), а затем создаст новую сущность с нужным нам прототипом на месте нашей. Стоит уточнить одну деталь: аргументы метода - использование `EntityUid` и `Component` по-отдельности является плохой практикой, так как есть более современный способ, способный сократить это в одну переменную - тип `Entity<Component>`. В нашем случае, с его использованием метод будет выглядеть как: ``` c# private void ExampleMethod(Entity<ExampleComponent?> ent) { if (!Resolve(ent, ref ent.Comp)) return; if (HasComp<ItemComponent>(ent) && TryComp<ArmorComponent>(ent, out var armor)) { Spawn("прототип для спавна", Transform(ent).Coordinates); RemComp(ent, armor); } } ``` Как понятно из метода - разделяется он обратно на `EntityUid` и `Component` при помощи расширений `ent.Owner` и `ent.Comp` где `Owner` как-бы указывает на владельца компонентов. Но в большинстве случаев получение конкретно `uid` - не требуется, так как сам `Entity<T>` прекрасно справляется с этой ролью, только если не будет требоваться предоставление uid с другими аргументами, как например в этом случае: ``` c# private void ExampleMethod(Entity<ExampleComponent?> ent) { if (!Resolve(ent, ref ent.Comp)) return; if (HasComp<ItemComponent>(ent) && TryComp<ArmorComponent>(ent, out var armor)) { Spawn("прототип для спавна", Transform(ent).Coordinates); RemComp(ent, armor); ExampleVoid(ent.Owner); // Мы предостовляем ent.Owner вместо ent, потому-что они используют разные компоненты. Такая-же ситуация с null и не null компонентами. } } private void ExampleVoid(Entity<ItemComponent?> ent) { if (!Resolve(ent, ref ent.Comp)) return; } ``` Достаточно частой ошибкой является использование методов из `EntityManager` напрямую, вместо использования их прокси-версий, любезно предоставленных в `EntitySystem` по-умолчанию. Например `EntityManager.HasComponent` вместо `HasComp`. ## Не-ECS Несмотря на то, что игра использует парадигму ECS, далеко не всё является сущностями, компонентами или системами для работы с сущностями. Достаточно просто зайти в папку с прототипами и увидеть то, насколько-же много разнообразных прототипов. Прототип это **уже** сериализиванные (сохранённые) в yml данные, например, в случае с сущностями их прототипы хранят их айди, компоненты, их имя, родительские сущности и т.д. В случае с технологиями, то, какие предметы можно получить, открыв их, айди технологии, иконку, локализируемое название. Но не смотря на всё это, работа с любыми сохранёнными в прототипе данными в `EntitySystem` более чем возможна. Также со структурой ECS обладают крайне малой связью интерфейсы. Их необходимо разбирать отдельно, так как это сам по-себе один из самых затруднительных аспектов в кодбазе. # Предугадывание на стороне клиента Поговорим о достаточно непопулярной в ру-сегменте теме - предугадывании на стороне клиента. Что-же это такое? Стоит вернуться к самому началу и вспомнить, то, что игра и движок разделяются на три части: **Server** (Серверная) - основной костяк игры, именно на сервере проигрывается основной пласт контента **Client** (Клиентская) - отвечает в основном за визуал и логику интерфейсов. Именно дублирование кода на сторону клиента позволяет предугадыванию существовать. **Shared** (Шаред) - код, дублирующийся и на сервер, и на клиент. Огромная час