Это руководство является ставит перед собой цель дать как можно большие знания об кодбазе СС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** (Шаред) - код, дублирующийся и на сервер, и на клиент.
Огромная час