Имейте в виду, что некоторые старые участки кодовой базы могут не следовать этим соглашениям. В будущем они должны быть отрефакторены. Весь новый код должен стараться следовать этим соглашениям как можно точнее.
Общие соглашения по программированию
Эти соглашения не специфичны для Space Station 14, и вы должны следовать им независимо от того, над каким проектом работаете. Любой опытный программист должен знать их наизусть.Не копипастите код
Если вы смотрите на другой код и думаете «я хочу сделать то же самое»: НЕ КОПИРУЙТЕ его. Создайте новую функцию или абстракцию, которая позволит переиспользовать как можно больше кода. Копипаст кода — это огромная проблема для поддержки, так как в будущем, если кому-то понадобится обновить скопированный код, ему придётся делать это в двух местах (и знать, что эти два места вообще существуют). Конечно, есть места, где вы можете думать, что «копипаст» неизбежен. Например, базовая структура созданияEntitySystem, который что-то делает, всегда включает определение класса, несколько зависимостей, override void Initialize() и так далее. Такого рода «boilerplate» нормально копировать, так как этого действительно не избежать.
Не используйте магические строки/числа
Это своего рода подмножество «не копипастите код». «Магическое» значение — это любое значение в коде (строка или число), которое должно быть именно этим конкретным значением, потому что оно должно совпадать с каким-то другим значением в другом месте. Суть в том, чтобы «сделать практически невозможным несовпадение двух значений, которые должны совпадать», будь то через ошибку компилятора, провал юнит-теста, гарантированный краш при запуске и т.д. В простейшем случае такие магические значения следует хранить вconst или static readonly, которые используются из нескольких мест, чтобы компилятор C# гарантировал их одинаковость. Если вам нужно сослаться на ID prototype из C#, вы должны определить ID prototype в static readonly ProtoId<T>, поскольку наш инструмент валидации гарантирует, что ID в этих полях всегда действительны.
Комментарии
- Комментируйте код на высоком уровне, чтобы объяснить что код делает, и что более важно, почему код делает то, что он делает.
-
При документировании классов, структур, методов, свойств/полей и членов класса используйте XML docs. DataFields и публичные методы должны быть задокументированы всегда.
- Пример:
Почему, а не Что
Некоторые люди слепо следуют «комментируй почему, а не что» и считают, что «код должен быть самодокументируемым, а комментарии — крайняя мера». Ниже мы приводим несколько примеров, которые, надеемся, изменят ваше мнение.Пример 1
Пример 2
Строки и идентификаторы
Человекочитаемый текст никогда не должен использоваться в качестве идентификатора и наоборот. В одном направлении это означает, что нельзя помещать человекочитаемый текст (результат функций локализации) в ключи словаря, сравнивать с== и т.д. В другом направлении это означает «никогда не показывать Enum.ToString() пользователю напрямую».
Это позволяет избежать путаницы, когда их неизбежно приходится разделять по разным причинам, и избегает неэффективности и багов при сравнении человекочитаемых строк.
Пример:
Инвариантные сравнения человекочитаемых строк
Если вы делаете диалог фильтрации/поиска, используйтеCurrentCulture для сравнения человекочитаемых строк. Не используйте инвариантные культуры.
Свойства
В сеттере свойства значение свойства всегда должно буквально становиться переданнымvalue. Никак не так:
Правильный порядок членов в типе
При расположении содержимого типа вы всегда должны помещать поля выше всех остальных членов экземпляра. При чтении кода лучший способ ознакомиться с ним — посмотреть на данные, с которыми он работает. Если поля и другие члены смешаны случайным образом, понять код может быть гораздо сложнее. Для этого правила авто-свойства (например,string FooBar { get; set; }) считаются полями, так как у них есть внутреннее поле. Не-авто-свойства (например, string FooBar => _field.Trim();) не считаются, поэтому не должны смешиваться.
Плохо:
Проектные соглашения
Эти соглашения специфичны для Space Station 14. Они могут касаться кода или систем, нерелевантных для других проектов, или у других проектов может быть иное мнение о стиле кода.Расположение файлов
- Начинайте с using directives в верхней части файла.
-
Все классы должны быть явно указаны в namespace. Используйте file-scoped namespaces, например один
namespace Content.Server.Atmos.EntitySystems;перед определениями классов вместоnamespace Content.Server.Atmos.EntitySystems { /* class here */ }. - Всегда размещайте все поля и авто-свойства перед любыми методами в определении класса.
Методы
Переносы строк в списках параметров/аргументов
Если вы определяете функцию, и объявления параметров настолько длинные, что не помещаются на одной строке, разбейте их так, чтобы было один параметр на строку. Допускается некоторая гибкость для тесно связанных пар параметров, таких как координаты X/Y и указатель/длина в C API. Плохо:Константы и CVars
Если у вас есть определённое значение, например целое число, вы обычно должны делать его либо:- константой (const), если оно никогда не должно изменяться
- CVar, если оно должно быть настраиваемым
Prototypes
Поля данных Prototype
Не кэшируйте prototypes, используйте prototypeManager для их индексации, когда они нужны. Вы можете хранить их по их ID. При использовании полей данных, содержащих строки ID prototype, используйте ProtoId<T>. Например, поле данных для списка ID prototype должно выглядеть так:
Enums vs Prototypes
Использование enums для игровых типов крайне не рекомендуется. Всегда используйте prototypes вместо enums. Пример: «виды» или «типы» игровых инструментов должны использовать prototypes вместо enums.Ресурсы
Звуки
При указании полей данных звука используйтеSoundSpecifier.
Следует избегать прямого указания путей к звукам и вместо этого использовать SoundCollectionSpecifier, когда это возможно.
Пример кода C# (нажмите, чтобы развернуть)
Пример кода C# (нажмите, чтобы развернуть)
Пример prototype в YAML (нажмите, чтобы развернуть)
Пример prototype в YAML (нажмите, чтобы развернуть)
Спрайты и текстуры
При указании полей данных спрайта или текстуры используйтеSpriteSpecifier.
Пример кода C# (нажмите, чтобы развернуть)
Пример кода C# (нажмите, чтобы развернуть)
Пример prototype в YAML (нажмите, чтобы развернуть)
Пример prototype в YAML (нажмите, чтобы развернуть)
RSI meta.json (нажмите, чтобы развернуть)
RSI meta.json (нажмите, чтобы развернуть)
- Порядок полей должен быть:
version -> license -> copyright -> size -> states. - JSON не должен быть минифицирован и должен следовать обычным правилам качества JSON (египетские скобки и т.д.). Все новые JSON-файлы должны иметь отступ в 4 пробела. Существующие файлы следует изменить на отступ в 4 пробела, если вы их модифицируете (исправляйте по ходу). Никогда не используйте табуляцию для отступов.
EntityUid в логах
При использованииEntityUid в логах для администраторов используйте метод IEntityManager.ToPrettyString(EntityUid).
Пример лога админа с сущностями (нажмите, чтобы развернуть)
Пример лога админа с сущностями (нажмите, чтобы развернуть)
Опциональные сущности
Если вам нужно передавать «опциональные» сущности, используйте nullableEntityUid.
Никогда не используйте EntityUid.Invalid для обозначения отсутствия EntityUid, всегда используйте null с nullable, чтобы у нас были проверки во время компиляции.
Например: EntityUid? uid
Компоненты
Модификаторы доступа к данным компонента
Все данные в компонентах должны быть публичными.Сеттеры свойств компонентов
В свойствах не должно быть сеттеров с какой-либо логикой. Вместо этого создайте метод сеттера в вашей entity system и примените атрибут[Friend(...)] к компоненту, чтобы только эта система могла его изменять.
Ваш компонент может использовать свойства с логикой сеттера для интеграции с ViewVariables (пока у нас не появится лучшая система).
Ограничения доступа к компонентам
Атрибут[Access(...)] позволяет указать, какие типы могут читать или изменять данные в вашем классе, запрещая всем остальным типам изменять их.
Компоненты должны указывать свои ограничения доступа, когда это возможно, обычно разрешая изменять свои данные только entity systems, которые их оборачивают.
Наследование Shared Component
Если shared component наследуется серверным и клиентским аналогами, он должен быть помечен как abstract.Entity Systems
Игровая логика
Игровая логика всегда должна быть в entity systems, а не в компонентах. Компоненты должны только хранить данные.Proxy Methods
Когда возможно, старайтесь использовать proxy methods изEntitySystem вместо использования свойства EntityManager.
Примеры (нажмите, чтобы развернуть)
Примеры (нажмите, чтобы развернуть)
Сигнатура публичного API метода
Все публичные методы Entity System API, которые имеют дело с сущностями и игровой логикой, всегда должны следовать очень конкретной структуре. Все соответствующиеEntity<T?> и EntityUid должны идти первыми.
T? в Entity<T?> означает тип компонента, необходимый от сущности.
Вопросительный знак ? должен присутствовать в конце, чтобы пометить тип компонента как nullable.
Затем должны идти любые аргументы, которые вы хотите.
Первое, что вы должны сделать в теле метода — вызвать Resolve для UID сущности и компонентов.
Пример (нажмите, чтобы развернуть)
Пример (нажмите, чтобы развернуть)
Resolve выполняет несколько полезных проверок. В режиме DEBUG он проверяет, действительно ли переданная ссылка на компонент (если не null) принадлежит указанной сущности.
Этот хелпер также по умолчанию логирует ошибку, если у сущности отсутствует какой-либо из компонентов, которые вы пытались разрешить.
Это логирование ошибки можно отключить, передав false в аргумент logMissing хелпера. Вы можете отключить логирование ошибок для разрешения опциональных компонентов, методов с паттерном TryX и т.д.
Обратите внимание, что хелпер Resolve также имеет перегрузки для разрешения 2, 3 или даже 4 компонентов одновременно.
Если вам нужно разрешить компоненты для нескольких сущностей или более 4 компонентов для одной сущности, вам нужно будет выполнить несколько вызовов Resolve.
Extension Methods
Extension methods (те, у которых есть явныйthis для первого аргумента) никогда не должны использоваться на любых классах, напрямую связанных с симуляцией — это означает EntityUid, компоненты или entity systems. Extension methods на EntityUid используются по всей кодовой базе, однако это плохая практика, и их следует заменять на публичные методы entity system.
Зависимости от других систем
Внутри entity system предпочтительнее использовать dependency на систему вместо разрешения системы через IoCManager. Например, вместо:События
Method Events vs Entity System Methods
Method Events — это события, которые вы вызываете, когда хотите выполнить определённое действие. Пример:DamageableSystem.ChangeDamage() внутренне вызывает ChangeDamageEvent, который затем обрабатывается любыми подписчиками…
Убедитесь, что отписка от событий происходит при выключении систем. Proxy-методы, такие как
Subs.CVar() или SubscribeLocalEvent, уже заботятся об этом. Обратите внимание, что не нужно отписываться внутри менеджеров, так как их время жизни гарантирует, что при их выключении остальная часть клиента/сервера также выключается, что делает отписку ненужной.Именование событий
-
Всегда добавляйте суффикс
Eventк вашим событиям. Пример:DamagedEvent,AnchorAttemptEvent… -
Всегда называйте ваш обработчик событий так:
OnXEventПример:OnDamagedEvent,OnAnchorAttemptEvent…
Struct by-ref events
События всегда должны быть struct, а не class, и всегда должны вызываться по ref. Если возможно, они также должны быть readonly, если это применимо. У них также должен быть атрибут [ByRefEvent]. На практике это выглядит так:C# Events vs EventBus Events
EventBus обычно следует использовать вместо C# events, где это возможно. C# events могут вызывать утечки, особенно при использовании с компонентами, которые могут быть созданы или удалены в любое время. C# events следует использовать для событий вне симуляции, таких как события UI. Однако не забывайте всегда отписываться от них!Async vs Events
Для таких вещей, как DoAfter, всегда используйте события вместо async. Async для любого кода игровой симуляции следует избегать любой ценой, так как он обычно «заразен», не может быть сериализован (в случае DoAfter, например) и обычно приводит к неприятному коду. События, с другой стороны, хорошо вписываются в остальную архитектуру игры, и хотя они не так удобны в написании, они определённо гораздо более легковесны.UI
XAML и UI, определённые в C#
Вы всегда должны использовать XAML вместо UI, полностью определённых в коде C#. Расширение существующих UI на C# допустимо, но они должны быть конвертированы в будущем.Производительность
Iterator Methods vs возврат коллекций
Всегда используйте iterator methods вместо создания новой коллекции и её возврата в вашем методе. Однако имейте в виду, что iterator methods выделяют много памяти. Если вам нужно минимизировать выделения, используйте struct iterators.Sealed Classes
Ваш класс должен быть помечен какabstract, static, sealed или [Virtual]. Это необходимо, чтобы случайно не сделать классы наследуемыми, когда они не должны быть, и может немного улучшить производительность при доступе или вызове виртуальных членов.
Используйте sealed, если класс не должен наследоваться, [Virtual] для обычного поведения C# (отключает предупреждение компилятора), static для классов, которые не нужно инстанцировать, или abstract, если класс предназначен для наследования, но не для самостоятельного инстанцирования.
События вместо обновлений
Где возможно, ваша система должна выполнять код в ответ на событие, а не обновляться каждый тик. Ваш код может занимать всего 0.5% времени CPU, но когда 100 систем делают это — это уже излишне.Захват переменных
При использовании лямбд или локальных функций обязательно избегайте захвата переменных. Если вы добавляете метод, который принимает Func delegate, убедитесь, что есть перегрузка, которая позволяет вызывающему передавать собственные данные.Пример того, как не надо (нажмите, чтобы развернуть)
Пример того, как не надо (нажмите, чтобы развернуть)
Пример того, как надо (нажмите, чтобы развернуть)
Пример того, как надо (нажмите, чтобы развернуть)
Field Deltas
Field deltas позволяют отправлять по сети только определённые поля компонента вместо всего состояния. Это делается добавлениемfieldDeltas: true в ваш атрибут AutoGenerateComponentState:
Когда использовать field deltas
Field deltas отлично подходят, когда:- Ваш компонент имеет поля, которые меняются с разной скоростью
- Обычно изменяется только подмножество полей
- У вас много сетевых полей, и вы не хотите отправлять их все каждый раз
Пометка полей как изменённых
Когда вы изменяете поле и хотите отправить по сети только его, используйтеDirtyField вместо Dirty:
TimeSpans
Использование TimeSpans
Вы всегда должны использоватьTimeSpan вместо float для определения статических периодов времени, таких как интервалы. Циклы обновления должны сравнивать с CurTime вместо накопления frametime.
Обработка приостановленных сущностей
При работе с полямиTimeSpan, которые изменяются во время выполнения (например, таймеры или отсчёты), вам нужно правильно обрабатывать приостановку сущностей. SS14 предоставляет два важных механизма для этого.
AutoGenerateComponentPause и AutoPausedField
Атрибуты[AutoGenerateComponentPause] и [AutoPausedField] работают вместе, чтобы автоматически корректировать поля TimeSpan при возобновлении сущности:
[AutoGenerateComponentPause]применяется к классу компонента и автоматически генерирует код для обработки возобновления.[AutoPausedField]применяется к отдельным полямTimeSpanвнутри этого компонента, которые должны быть скорректированы при возобновлении сущности.
DataField типа TimeSpan, которые изменяются другими системами во время выполнения, например, таймеры или перезарядки.
Пример использования (нажмите, чтобы развернуть)
Пример использования (нажмите, чтобы развернуть)
TimeOffsetSerializer
TimeOffsetSerializer используется для сериализации значений TimeSpan, которые смещены относительно текущего игрового времени.
- Он автоматически смещает
TimeSpanна текущее игровое время во время сериализации/десериализации - Если сущность приостановлена, используется время, на которое сущность была приостановлена, как точка отсчёта
- Он предотвращает непреднамеренное сохранение временных смещений на картах во время маппинга (prototypes всегда сериализуются как ноль)
AutoPausedField, TimeOffsetSerializer всегда должен использоваться для полей TimeSpan, изменяемых во время выполнения, которые представляют абсолютное время, а не длительность.
Пример использования (нажмите, чтобы развернуть)
Пример использования (нажмите, чтобы развернуть)
Именование
Shared типы
Shared типы должны иметь префиксShared только в том случае, если существуют серверные и/или клиентские унаследованные типы с тем же именем.
Пример:
- Если
FooComponentсуществует только в shared, ему не нужен префикс. - Если
BarComponentсуществует в shared, server и client, shared тип должен иметь префиксShared:SharedBarComponent.
Физика
Anchoring
Всегда используйте anchoringTransformComponent через методы системы.
Вы можете использовать anchoring статического тела PhysicsComponent, но только если вы знаете, что делаете, и можете обосновать свой выбор перед anchoring через transform.
Соглашения по YAML
- Каждый
- typeкомпонента должен быть вместе без пустых строк между ними - Разделяйте prototypes одной пустой строкой.
- Поля
name:иdescription:никогда не должны иметь кавычек, если только пунктуация в имени/описании не требует их использования; в этом случае используйте ”. Например:
- Не указывайте текстуры в абстрактных prototypes/родителях.
- Объявляйте первый блок prototype в таком порядке:
type>abstract>parent>id>categories>name>suffix>description>components. - Используйте инлайн-списки для categories и обычные списки для всего остального:
- Новые компоненты не должны иметь отступа при добавлении в секцию
components:. ТакНе так - То же правило применяется к любому другому списку или словарю, например:
- Когда это имеет смысл, размещайте более общие/движковые компоненты ближе к верху списка компонентов, а более специфичные — ближе к низу. Например,
YAML и именование полей данных
PascalCase используется для ID и имён компонентов.
Всё остальное, даже имена типов prototype, использует camelCase.
prefix.Something НИКОГДА не должен использоваться для ID.
Сущности
Пожалуйста, структурируйте сущности с компонентами следующим образом для лучшей читаемости YAML:Суффиксы Entity Prototype
Используйтеsuffix в prototypes; это суффикс только для меню спавна, который позволяет различать prototypes без изменения фактического имени prototype. Вы можете использовать это так:


Локализация
Каждая строка, предназначенная для игрока, должна быть локализована.Именование ID локализации
- ID локализации всегда в
kebab-caseи никогда не должны содержать заглавных букв. - ID локализации должны быть как можно более специфичными, чтобы избежать пересечения с другими ID.
Так
Не так
Внутри симуляции или вне симуляции
В целом, весь код в игре должен быть разделён в зависимости от того, находится ли он внутри «симуляции» или вне её. «Симуляция» — это всеобъемлющий термин, означающий «содержимое самой игры». Например, следующие вещи находятся «внутри» симуляции:- Практически всё, касающееся сущностей: взаимодействия, физика, atmos и т.д.
- IC чат
- Состояние раунда (лобби, игра, после игры)
- OOC чат
- Adminhelp
- Голосования админов
- Практически всё, что общается с внешним сервисом, таким как база данных или Discord webhook
Игровой сервер в настоящее время уже автоматически делает такую паузу, когда нет онлайн-игроков, для экономии ресурсов. Это не чисто теоретическая ситуация! Но, возможно, её трудно наблюдать в данный момент.
| Вещь, которую вы хотите сделать | внутри симуляции | вне симуляции |
|---|---|---|
| «Место по умолчанию» для синглтон-кода. | Создайте EntitySystem | Используйте manager: создайте новый класс, зарегистрируйте его в IoC и вызывайте из EntryPoint или подобного. |
| Проверка прошедшего времени | IGameTiming.CurTime | IGameTiming.RealTime, (R)Stopwatch, DateTime и т.д. |
| Отправка пользовательских сетевых сообщений | Networked entity events | Пользовательские NetMessage |