Сущность — это любой значимый объект игрового мира: игрок, питомец, шляпа, портал,
подбираемый контейнер. EntityBase даёт им общий идентификатор, единое описание
(вид, имя, иконка, локализация, качество), регистрацию в глобальном реестре, правила
уничтожения и работу с пулом.
Всё, что должно быть найдено по типу, посчитано, сохранено, уничтожено по окончании раунда или показано в UI, — сущность. Декорации и служебные компоненты — нет.
| Папка | Что внутри |
|---|---|
EntityBase/ |
EntityBase и три варианта под разные способы задать описание |
EntityMetadata/ |
Вид, имя, иконка, локализация, качество и механизм переопределения |
EntityManager/ |
EntityTracker — глобальный реестр сущностей, выдача Id, статистика |
EntityInstance/ |
Данные экземпляра: запись EntityInstanceData и интерфейс IEntityInstance |
EntityStats/ |
Базовые характеристики, персональные модификаторы и расчёт итоговых |
EntityContainer/ |
Подбираемые контейнеры: ресурс, действие |
Player/ |
IPlayer, PlayerBase, PlayerTracker, команды |
Initialize/ |
ReadySignal — сигнал готовности с подпиской «поздних» слушателей |
Pickup/ |
Интерфейсы подбора |
| корень | IEntity, EntityLink, время жизни, параметры уничтожения, слой времени |
Самый короткий путь — компонент Entity: его вешают на префаб и выбирают ассет описания.
Кода не требуется вовсе, потому что вид, имя, иконку и переводы сущность берёт из описания.
// Класс нужен, только когда у сущности появляется своё поведение.
public class Portal : Entity
{
[SerializeField] private Transform target;
}Если описанию нужны собственные поля, заводят наследника EntityMetadataBase и берут
EntityBase<TMetadata> с ним:
public class Player : EntityBase<PlayerMetadata>
{
// Metadata типизирован — видны поля PlayerMetadata.
}Обязательного к объявлению не осталось ничего: тип и имя приходят из описания, контейнер собирается базовым классом. Переопределить можно и то и другое — сущностям с определением это и нужно, см. ниже.
| Возможность | Как работает |
|---|---|
| Идентификатор | Id выдаётся при регистрации в трекере, из Start() |
| Регистрация | Автоматически в Start(), снятие — при уничтожении |
| Вид | EntityType из описания; сущности с определением задают его в коде |
| Описание | Description — имя, иконка, локализация и качество |
| Уничтожение | DestroyEntity() с учётом пула и настройки EntityDisposeAction |
| Пул | Реализация IPoolable через PoolBehaviour |
| Пауза и время | Наследуется от PRMonoBehaviour; слой времени — GetTimeScaleLayer() |
| Мониторы | Автоматически подхватывает AnimatorPauseMonitor |
| Класс | Откуда берётся описание | Когда использовать |
|---|---|---|
Entity |
Ассет EntityMetadata, ссылка на префабе |
Обычный случай: объект сцены, портал, дашборд |
EntityBase<TMetadata> |
Ассет своего типа | Когда описанию нужны собственные поля |
EntityDefinition<T> |
Типизированный ассет-определение | Питомцы, шляпы — всё, у чего есть каталог |
RuntimeEntityBase |
Поля прямо на компоненте | Контейнеры, дропы, временные объекты |
EntityDefinition<T> — основной путь для контентных сущностей: PetEntity, HatEntity
и сущности проектного слоя построены именно так, а их определения дополнительно поставляют
модификаторы характеристик. Вид такие сущности объявляют в коде: их ассет описывает
позицию каталога, а не вид.
Id выдаётся при регистрации в реестре, а регистрация происходит в Start().
Это важно: в Awake() и InitializationComponents() идентификатора ещё нет.
protected override void InitializationComponents()
{
base.InitializationComponents();
// Здесь Id == 0 — использовать его нельзя.
}
public override void OnReadyScene()
{
// Здесь Id уже назначен.
}Подробности о том, в какой именно реестр попадает сущность и как ведут себя идентификаторы, — в разделе Реестры.
Описание и определение отвечают на вопрос «что это за сущность» и общие для всех её копий.
Уровень, улучшения, прочность принадлежат одному экземпляру. Их хранит запись
EntityInstanceData, а сущность, которой она нужна, реализует IEntityInstance.
Остальных сущностей это не касается.
| Член записи | Что хранит |
|---|---|
InstanceId |
Идентификатор экземпляра. Сохраняется между запусками, в отличие от IEntity.Id |
DefinitionId |
Идентификатор определения, по которому сущность создают заново |
Set / Get / TryGet |
Свои значения по ключам EnumerationType<T> |
Changed |
Значение записали или забыли. Запись того же значения событие не поднимает |
IsValid() |
Оба идентификатора на месте: по такой записи предмет можно восстановить |
Fits(definitionId) |
Запись относится к сущности этого определения |
В запись идёт постоянное: то, что должно пережить сохранение и перенос в инвентарь. То, что верно только сейчас — «в руках», «идёт откат», текущая цель, — остаётся полями самой сущности и в запись не попадает.
public static class PickaxeInstanceKeys
{
public static readonly EnumerationType<long> Level = new(nameof(Level));
}
public class PickaxeEntity : EntityDefinition<PickaxeDefinition>, IEntityInstance
{
public EntityInstanceData Instance { get; private set; }
public long Level => Instance.Get(PickaxeInstanceKeys.Level, 1);
public EntityInstanceData CreateInstance()
{
return EntityInstanceData.Create(Definition.Id);
}
public void SetInstance(EntityInstanceData data)
{
Instance = data;
RecalculateStats(); // всё, что выводится из записи, считается здесь
}
}Запись ставится одним методом — SetInstance. EntityBase зовёт его перед каждой
InitializeEntity() при создании сущности и выдаче из пула, поэтому в InitializeEntity()
запись уже есть, а сущность из пула не приходит с уровнем прошлой жизни.
Сущность с сохранённой записью создают через EntityInstanceSpawn:
var entity = EntityInstanceSpawn.Create(
(EntityInstanceData)saved.Clone(),
() => PRUnitySDK.Managers.ObjectPool.ShowEntity(definition.Prefab, parent));Тогда база поставит именно эту запись вместо новой, и InitializeEntity() посчитает всё
по сохранённым значениям. Создавать сущность внутри функции можно как угодно: пулом
или Instantiate.
Запись достаётся ровно одной сущности — той, которую вернула функция:
| Что произошло внутри функции | Итог |
|---|---|
| Сущность начала жизнь и её определение совпало с записью | запись стоит до InitializeEntity() |
| Вместе с ней проснулась дочерняя сущность другого определения | та получает свою новую запись |
| Вместе с ней проснулась вложенная сущность того же определения | вложенная запись не берёт, она достаётся внешней |
| Запись раньше забрала соседняя сущность того же определения | той ставится новая запись, эта переходит вернувшейся; обе уже прошли InitializeEntity не со своей записью — в лог уходит предупреждение |
| Сущность создана выключенной и жизнь не начала | запись ставится следом, через SetInstance, и переживает её первый Awake |
| Вернулась сущность другого определения | ошибка в лог, запись ей не передаётся |
Экземпляр с таким InstanceId уже живёт на сцене |
ошибка в лог, функция создания не вызывается, Create возвращает null |
Удалась ли передача, проверяют сравнением: ReferenceEquals(entity.Instance, record). Тот, кто
после создания убирает запись из своего контейнера, делает это только при совпадении —
иначе предмет пропал бы вместе с ошибкой.
Вложенные вызовы Create друг другу не мешают: после внутреннего возвращается запись внешнего.
SetInstance можно позвать и у живой сущности, но InitializeEntity() к этому времени
уже прошла. Поэтому всё, что сущность выводит из записи — характеристики, вид, подписи, —
пересчитывают в SetInstance, а не в InitializeEntity().
Сущность, возвращённая из спрятанных (RestoreHideEntities), остаётся тем же экземпляром:
InitializeEntity() проходит заново, а запись и InstanceId не меняются.
У записи один владелец. Отдавая запись сущности, инвентарь убирает её у себя; подбирая сущность, забирает запись обратно. Два места, которые правят один и тот же объект записи, дают предмет, существующий дважды.
| Нужно | Чем |
|---|---|
| Отдать запись сущности или контейнеру | сам объект записи, прежний владелец о нём забывает |
| Сохранить состояние | Clone() — снимок того же экземпляра, InstanceId прежний |
| Сделать второй такой же предмет | Duplicate() — новая запись с новым InstanceId |
Коллекцию записей в ProjectData при клонировании (MethodHookStage.Cloning) копируют
глубоко, через Clone() каждой записи: иначе снимок, ждущий записи на диск, меняется
вместе с живыми предметами.
Живёт ли экземпляр сущностью на сцене, отвечает EntityInstanceOwners.Find(instanceId).
Своего учёта он не ведёт, а спрашивает EntityTracker; сущности в пуле не считаются.
Контейнер зовёт его, прежде чем принять запись, и отказывает, если владелец нашёлся.
Сущность, которая отдаёт запись сама, передают в except, а после передачи ставят ей
новую запись: entity.SetInstance(entity.CreateInstance()).
Изменения значений сущность узнаёт из события записи, а не из каждого места, которое их пишет:
public void SetInstance(EntityInstanceData data)
{
if (Instance != null)
Instance.Changed -= RecalculateStats;
Instance = data;
Instance.Changed += RecalculateStats;
RecalculateStats();
}Отписываются там же, где от остальных событий сущности, — в UnRegisterEventsOnDestroy().
Подписчики не сохраняются и в Clone() / Duplicate() не переходят.
Контейнер (подставка, инвентарь) хранит запись целиком и не знает, какие в ней значения. Новое свойство экземпляра добавляется ключом у сущности, данные контейнера не меняются.
Запись сериализуется в ProjectData как есть. Поддерживаются long, float, bool,
string и DateTime. IIdentifiable запись не реализует: конвертер сохранения пишет
от таких объектов один Id.
Описание отвечает на пять вопросов: какого сущность вида, как называется, как выглядит,
как переводится и какого качества. Вид (EntityType) лежит в самом ассете, поэтому Entity работает без кода.
Description — собранное описание живой сущности. Он же поддерживает переопределение:
базовое описание берётся из ассета или определения, а поверх может лечь второе.
string title = entity.Description.GetName();
Sprite icon = entity.Description.GetIcon();
string text = entity.Description.GetLocalization();
QualityType q = entity.Description.GetQuality();Порядок разрешения для каждого поля: функция-override → Override-описание → Base-описание.
Функции-override (NameOverride, SpriteOverride, …) позволяют подменить одно поле,
не трогая остальные, — например, показать в UI имя конкретного бота.
EntityBase<TMetadata> собирает контейнер сам: базовым берёт свой ассет, переопределением —
IEntityMetadataProvider, найденный на объекте.
У RuntimeEntityBase базовым описанием служит сама сущность: имя, иконка и перевод лежат
полями прямо в ней, отдельного ассета не нужно. Переопределение она ищет так же, но
пропускает себя.
Готовая реализация переопределения — компонент EntityMetadataProvider: повесьте его на объект
сущности и укажите ассет EntityMetadataBase. Так одному экземпляру можно дать собственное имя
или иконку, не заводя отдельный тип сущности.
Когда менять нужно одно поле, целый ассет описания заводить незачем — на объект сущности вешают компонент на нужную часть:
| Компонент | Что подменяет |
|---|---|
EntitySpriteOverride |
Иконку (GetIcon) |
EntityNameOverride |
Служебное имя (GetName) |
EntityLocalizationOverride |
Подпись для игрока (GetLocalization) |
Каждый реализует IEntityDescriptionOverride и в Apply записывает свою функцию-override.
Локализация переопределяет источник перевода, а не готовую строку: подпись игроку отдают
в SetLocalization, и LocalizationObserver перечитывает её по провайдеру при смене языка.
Читать подпись для UI надо через Description.GetLocalizationProvider() — метаданные напрямую
(GetMetadata()) переопределений экземпляра не знают.
Значение читается в момент запроса, поэтому его можно менять и по ходу игры, а подпись
переживает смену языка. EntityLocalizationOverride берёт источник первым заданным, как
LocalizationObserver: ключ в базе локализации либо перевод прямо на объекте.
Компоненты собирает сама сущность в ApplyDescriptionOverrides() сразу после сборки
описания. Ищутся они только на объекте сущности: у вложенной сущности своё описание.
Своё переопределение добавляют тем же способом — реализацией IEntityDescriptionOverride;
у EntityDescription есть ещё SetQualityOverride.
Под обычными полями сущности идёт раздел «Описание»: он показывает, откуда описание
берётся, и разворачивает его прямо здесь — искать ассет по проекту не нужно. У каждого
блока подписана область действия: правка общего описания меняет его всем сущностям сразу,
а EntityMetadataProvider — только этому экземпляру. Если поле под описание есть, а ссылка
пустая, рядом появляется кнопка «Создать описание».
Рисование живёт в EntityDescriptionSection. На один тип Unity должен быть один редактор: в проекте это PREntityInspector. Он наследует NaughtyInspector, без этого атрибуты вроде Button на сущностях не работают.
Собрать описания всего проекта, проверить их и увидеть, кто ими пользуется, можно в окне
PRUnitySDK/Windows/Entity metadata — см. Editor.
Компонент, лежащий на дочернем объекте, находит свою сущность через EntityLink:
[RequireComponent(typeof(EntityLinkBase))]
public class HealthBar : PRMonoBehaviour
{
private EntityLinkBase link;
protected override void InitializationComponents()
{
link = GetComponent<EntityLinkBase>();
base.InitializationComponents();
}
private void Refresh()
{
if (link.Entity == null)
return;
// link.Entity — сущность этого объекта
}
}EntityLink ищет сущность сам: сначала в родителях, затем на своём объекте. Ссылку можно
задать и вручную в инспекторе — тогда автопоиск её не перезапишет. Типизированный вариант
EntityLinkBase<T> даёт LinkedEntity нужного типа и TryGetEntity(out T).
Это же основной способ для систем SDK связать компонент с сущностью — так работает,
например, EntityTimeScaleDriver, который берёт слой времени из связанной сущности.
Сущность бывает двух устройств: она либо сама корневой объект, либо контейнер, внутри
которого лежит настоящий объект — так устроен игрок. EntityGameObject в обоих случаях
указывает на то, что считается сущностью, а RootEntityObject — на корень иерархии.
Отсюда правило: позицию спрашивают и задают через сущность, а не через transform.
| Член | Что делает |
|---|---|
Position |
Где сущность на самом деле — позиция EntityGameObject |
Rotation |
Поворот EntityGameObject |
SetPosition(position) |
Ставит сущность в точку |
SetPositionAndRotation(position, rotation) |
То же плюс разворот |
entity.SetPosition(spawnPoint); // в точке окажется сущность, переедет вся иерархияДвигается при этом корень — на разницу между тем, где сущность сейчас, и тем, где она должна оказаться:
RootEntityObject.transform.position += position - Position;Поставить в точку корень — значит промахнуться на величину смещения вложенного объекта. Подвинуть только вложенный объект — оторвать его от собственной иерархии. Когда сущность и есть корень, обе операции вырождаются в обычное присваивание.
В SetPositionAndRotation сначала применяется поворот, и смещение считается уже после него.
Этим пользуются пул (ShowEntity досылает выданную сущность на место), спавнеры,
HealthComponent.Revive и PlayerBase.Teleport. Свой код, который спавнит или переносит
сущность, должен идти тем же путём.
EntityTracker хранит все сущности, включая игроков. PlayerTracker остаётся отдельным
реестром игроков: он ведёт Player ID, локальные слоты, счётчики и события входа/выхода.
| Реестр | Кто туда попадает | Доступ |
|---|---|---|
EntityTracker |
Все зарегистрированные сущности, включая игроков | PRUnitySDK.Trackers.Entities |
PlayerTracker |
Игроки (PlayerBase и наследники) |
PRUnitySDK.Trackers.Players |
Игроки регистрируются через PlayerTracker.Register(). После проверки доступности локального
слота он добавляет игрока в общий реестр. Entity ID выдаётся один раз общим трекером; если
сущность уже зарегистрирована там, её ID сохраняется. Player ID назначается отдельно.
Уход игрока через любой из двух реестров удаляет его из обоих и освобождает локальный слот.
EntityTracker.Clear() уничтожает также игроков и снимает их с PlayerTracker.
PlayerTracker.Clear() уничтожает только игроков и удаляет их из общего реестра.
Последовательная очистка обоих реестров не уничтожает игрока повторно.
| Момент | Что происходит |
|---|---|
Awake / InitializationComponents |
Регистрации ещё нет, Id == 0 |
Start |
Регистрация, выдача Id, сущность в реестре |
| Уход в пул | Регистрация сохраняется, меняется InPool |
| Возврат из пула | Повторной регистрации нет, Id остаётся прежним |
OnDestroy |
Снятие с регистрации через UnRegisterEventsOnDestroy() |
EntityIdGenerator выдаёт сквозной Entity ID. Освобождённые Entity ID не переиспользуются.
Player ID переиспользуются, а локальным игрокам зарезервированы значения
LocalPlayerOneId и LocalPlayerTwoId.
var tracker = PRUnitySDK.Trackers.Entities;
var all = tracker.GetEntities();
var visiblePlayers = tracker.GetEntities(
EntityTypeEnumerations.Player, EntitySearchFlags.Visible);
var living = tracker.GetEntities(
EntitySearchFlags.Visible | EntitySearchFlags.Alive);
var ofMetadata = tracker.GetEntities(metadata, EntitySearchFlags.NotInPool);
var ofDefinition = tracker.GetEntities(definition, EntitySearchFlags.Visible);
var containers = tracker.GetEntities<ContainerEntityBase>(EntitySearchFlags.Visible);
// Можно одновременно ограничить вид и описание.
var specific = tracker.GetEntities(entityType, EntitySearchFlags.Visible, metadata);
long count = tracker.GetEntitiesCount(metadata, EntitySearchFlags.Visible);EntityType сравнивается как Enumeration; CLR-тип в generic-поиске учитывает наследников
и реализации интерфейса. Метаданные сравниваются по ссылке с Description.Base, Description.Override и
исходным EntityBase<EntityMetadata>.Metadata. Поэтому сущность с EntityDefinition<T>
находится как по её конкретному Definition, так и по общему ассету EntityMetadata.
Definition уже реализует IEntityMetadata и передаётся в тот же метод поиска.
Переопределение иконки или имени не исключает сущность из поиска по исходному ассету.
Два разных ассета с одинаковым именем не совпадают.
| Флаг | Условие |
|---|---|
None |
Все существующие зарегистрированные сущности, включая скрытые и пул |
OnScene |
Существующее свойство IEntity.OnScene; у EntityBase это активность иерархии |
InPool / NotInPool |
Объект в пуле / вне пула; NotInPool включает скрытые |
Visible |
OnScene && !InPool, без проверки камеры |
NotVisible |
Обратное Visible, включая отключённые объекты и пул |
Hide |
Неактивен, вне пула, настроен Hide |
HideWire |
Неактивен, вне пула, настроен HideWire |
HideWirePolygons |
Неактивен, вне пула, настроен HideWirePolygons |
Hidden |
Неактивен, вне пула, настроен любой из трёх Hide-режимов |
Alive / Dead |
IHealthProvider.Health.IsAlive() возвращает true / false |
Все условия объединяются через «и», кроме Hide-режимов: их маска выбирает любой указанный
режим. Например, HideWire | HideWirePolygons | Dead найдёт мёртвые сущности,
неактивные вне пула с одним из двух настроенных Hide-режимов. Visible | Hidden или Alive | Dead вернут пустой список.
Hide, HideWire и HideWirePolygons выбирают неактивные сущности вне пула
по настроенному действию EntityBase.DisposeAction. Причину отключения поиск
не отслеживает: отключённый вручную объект с такой настройкой тоже подходит под
соответствующий Hide-флаг. Восстановленная активная сущность и объект в пуле не подходят.
Alive и Dead требуют IHealthProvider у самой сущности и существующий
HealthComponent в его свойстве Health.Объекты без здоровья не подходят ни под один из этих двух флагов.
Уничтоженные Unity-объекты исключаются из любого поиска.
GetEntities() возвращает новый список; GetEntitiesCount(...) с фильтром считает
без создания списка. Остаются доступны GetExact*Count, GetInherited*Count и статистика
RegisteredEntity. GetEntitiesCount() без аргументов по-прежнему возвращает
число регистраций; для подсчёта только существующих объектов передайте EntitySearchFlags.None.
EditMode-тесты поиска и синхронизации реестров находятся в
EntityManager/Editor/EntityTrackerSearchTests.cs и включаются символом PRSDK_TESTS.
entity.DestroyEntity(); // по настройке компонента
entity.DestroyEntity(new EntityDestroyOptions { FullDestroy = true }); // безусловноПоведение задаёт поле EntityDisposeAction в инспекторе:
Destroy— объект уничтожается;HideInPool— объект возвращается в пул;Hide— объект выключается и остаётся на сцене.HideWire— объект выключается, а его место занимает каркасная визуальная копия без коллайдеров и игровых компонентов.
Для HideWire копируются видимые MeshRenderer и SkinnedMeshRenderer. У читаемых мешей
показываются рёбра треугольников. Для мешей без Read/Write используется сетка на поверхности.
Копия повторяет иерархию и трансформации рендереров на момент уничтожения; анимация
и игровая логика на ней не выполняются.
Размер каркаса задаёт PRUnitySDK.Settings.Entity.WireScale. Original (по умолчанию) — размер,
с которым сущность появилась: блок, раздутый эффектом попадания в момент смерти, оставит
каркас обычного размера. AtDeath — размер на момент смерти, со всем, что на него повлияло.
Настройка касается корня сущности (EntityGameObject), дочерние объекты копируются как есть.
FullDestroy = true игнорирует эту настройку и уничтожает объект даже из пула.
Если выбран HideInPool, но объект создан не через пул, SDK напишет предупреждение
и уничтожит его, утечки не будет.
Объект, поднятый из пула, повторно проходит InitializeEntity(): при первом создании
инициализация идёт обычным путём, при переиспользовании — через PoolBehaviour.
Всё, что должно сбрасываться между использованиями, кладите именно туда, а не в Awake.
Спрятанные (Hide) сущности возвращает на сцену общее событие:
EntityEvents.RestoreHideEntities();Его получают все сущности сразу; каждая проверяет свою настройку и включается, если была
спрятана через Hide или HideWire. В режиме HideWire перед включением удаляется
визуальная копия. Выключенный объект событие тоже получает.
Вернувшаяся сущность повторно проходит InitializeEntity() — так же, как поднятая из пула.
Отдельного метода и настройки под это нет.
Восстановление не спрашивает, есть ли что чинить. Сущность, оставшаяся на сцене, проходит ровно то же, что и вернувшаяся: место, поворот, размер, инициализация.
| Что возвращается | Откуда берётся |
|---|---|
| Позиция и поворот | запомнены при появлении сущности |
| Размер | localScale у EntityGameObject на тот же момент |
| Всё остальное | InitializeEntity() |
Место запоминается до первой инициализации: она уже может сущность подвинуть.
Размер берётся у EntityGameObject, а не у корня иерархии.
InitializeEntity() выполняется и у нетронутой сущности. Для LeafEntity это значит новую случайную текстуру и новый поворот при каждом восстановлении.
Скорости Rigidbody восстановление не трогает.
Базовые значения хранит EntityStatsBase — ScriptableObject со словарём «стат → число»:
public class PlayerStats : EntityStatsBase<PlayerStatsEnumeration> { }Итоговое значение считает EntityStatsUtils, применяя три слоя по порядку:
EntityStatsBase → персональные модификаторы → GameRules → результат
Персональные модификаторы описывает StatModifier, а собирает их с дочерних объектов
сущности StatModifierCollector — компонент, который вешается рядом с EntityLinkBase
и обновляется при смене экипировки. Передать модификаторы можно им либо делегатом
Func<Enumeration, float, float> — тогда стат считается без компонента на сцене,
например в редакторе или в тестах.
// Обычный путь: модификаторы даёт сборщик на префабе
int jumps = EntityStatsUtils.GetStatInt(
PlayerStatsEnumeration.JumpCount, Core.Stats, statCollector, 1);
// Без сборщика: модификатор — это преобразование «ключ и базовое значение → итог»
float speed = EntityStatsUtils.GetStat(
PlayerStatsEnumeration.WalkSpeed, Core.Stats, (stat, value) => value * 1.2f);GetStatLong() дополнительно защищает от выхода за границы типа и от NaN.
Округление всегда происходит после всех модификаторов и правил, а не в промежутке.
Важно: GetStatInt округляет к ближайшему целому. Для дробных величин (время, доли
секунды, множители) используйте GetStat — иначе значение вроде 0.15 превратится в 0.
Устройство модификаторов, сборщик и известные ограничения — в EntityStats.
IPlayer расширяет IEntity очками, убийствами, смертями, командой, типом управления
(Human / AI / NPC) и характеристиками. PlayerBase добавляет события смены ника,
инициализации, изменения очков и учёт атакующего.
PlayerBase реализует IHealthProvider. При инициализации он запоминает существующий
HealthComponent на своём объекте или добавляет его, если в префабе компонента нет.
Для локальных игроков и ботов устанавливаются IsImmortal = true и IsBlockDamage = true:
атаки блокируются и здоровье не расходуется. Обычный KillByGame() возвращает Blocked.
DeadZone вызывает Kill(this, force: true), поэтому зона смерти продолжает убивать
игроков и запускать возрождение. Эти признаки сохраняются после Revive.
PlayerTracker ведёт отдельный учёт: сколько всего игроков, сколько людей, сколько
локальных. Локальным игрокам выделены фиксированные идентификаторы
(LocalPlayerOneId, LocalPlayerTwoId), а MaxLocalPlayer зависит от устройства —
на десктопе допускается двое, на мобильном один.
Команды — DefaultTeam, RedTeam, BlueTeam с фиксированными GUID из TeamGuids.
ContainerEntityBase — сущность, которую игрок подбирает касанием:
| Тип | Что выдаёт |
|---|---|
ResourceContainer |
Ресурс в заданном количестве — через IPickupResource |
ActionContainer |
Выполняет действие IconActionBase |
Кто может подобрать, задаётся флагами PlayerTypeFlags — например, чтобы AI не собирал
монеты. После успешного подбора контейнер уничтожает себя по своим правилам пула.
Подбор идёт через PR-хуки (PROnTriggerEnter, PROnCollisionEnter), поэтому во время
логической паузы не срабатывает. Касание при открытом окне не теряется: триггер сработает сразу после снятия паузы.
ReadySignal решает задачу «подписаться на событие, которое могло уже произойти»:
PRUnitySDK.ReadySignal.SubscribeOnReady(() =>
{
// Выполнится сразу, если SDK уже готов, иначе — в момент готовности.
});Подписчик, пришедший после SetReady(), вызывается немедленно и синхронно. Дубликаты
подписок отсекаются, после срабатывания список очищается, а исключение в одном callback'е
не мешает остальным. Это правильный способ дождаться готовности вместо проверок в Update.
GameEventEntity — псевдосущность для урона и событий, у которых нет источника на сцене
(падение с высоты, утопление, зона). У неё Id == -1, она всегда «на сцене» и не попадает
в пул. Нужна там, где API требует IEntity, а виновника не существует.
RuntimeEntityBase использует собственные метаданные как основу и ищет внешний
IEntityMetadataProvider среди остальных компонентов объекта. При отсутствии
метаданных EntityDescription.GetQuality() возвращает QualityType.Common.
OnScene учитывает активность всей иерархии. Успешный подбор ресурса проверяет isTaken.
FullDestroy снимает объект с учёта пула. Повторный возврат не добавляет объект в очередь второй раз.
- Переопределение описания задаётся только в инспекторе.
EntityMetadataProviderхранит ссылку на ассет, аEntityDescriptionсобирается один раз при инициализации сущности: подменить описание в рантайме черезSetEntityMetadata()можно лишь до её регистрации. - Сущность без описания попадает в вид
Unknown.EntityBase<TMetadata>берёт вид из ассета, и незаполненная ссылка даёт не ошибку, аEntityTypeEnumerations.Unknown: Раздел «Описание» в инспекторе сущности предупреждает об этом сразу. EntityManager— пустой класс. НаследникSingletonProviderBase, не содержит ничего и никем не используется; фактический реестр —EntityTracker, доступный черезEntityService.InstanceиPRUnitySDK.Trackers.Entities.Idнедоступен доStart(). Регистрация идёт изStart(), поэтому вAwakeиInitializationComponentsидентификатор равен нулю.- Регистрация не снимается при уходе в пул. Скрытая в пуле сущность остаётся в реестре;
различать её нужно по
InPool, а не по факту присутствия в списке. - Снимки коллекций аллоцируют.
Entities,PlayersиTrackerBase.Elementsкаждый раз возвращают новыйListчерезToList(). ВUpdateтакие обращения дают постоянный мусор — кешируйте результат или считайте черезGetExact*Count. - Поиск стата линейный.
EntityStatsBase<TEnum>.TryGetперебирает весь словарь и на каждой итерации вызываетToEnumeration(). Для статов, читаемых каждый кадр, это заметно. - Логика
DestroyEntityнепрозрачна. Четыре ветки со сложными условиями иthrow new NotImplementedException()в конце: добавление нового значенияEntityDisposeActionбез своей ветки приведёт к исключению в рантайме. IEntity.gameObjectобъявлен со строчной буквы, как Unity-свойство. Для не-Unity реализаций (GameEventEntity) объект создаётся фабрикой по требованию.
- Trackers — общий контракт реестров, камеры, окна и уведомители
- PRMonoBehaviour — базовый lifecycle, пауза, физические хуки
- PropertyContainer — персональные модификаторы характеристик
- GameRules — глобальные ограничения статов
- PRTimeScale — слои времени и
EntityTimeScaleDriver - Enumeration — ключи типов, статов и слоёв