Модуль описывает создание, модификацию и применение урона к сущностям PRUnitySDK. Основной компонент состояния — HealthComponent, а данные об атаке передаются через интерфейс IDamageProvider.
- Оружие или другой источник формирует
IDamageProvider. IDamageable.TakeDamage()принимает атакующего, оружие и провайдер урона.- Pre-хуки DamageHookEvent изменяют провайдер или отменяют урон.
- HealthComponent фиксирует здоровье, состояние смерти и DamageOutcome; затем вызываются post-хуки.
- Локальные события компонента и глобальные
CombatEventsуведомляют остальные системы.
При использовании хитбокса вызов перенаправляется в HealthComponent сущности, указанной в EntityLink. Перед передачей вызывается GetHandledDamage(), поэтому специализированный хитбокс может добавить собственный модификатор урона.
| Тип | Назначение |
|---|---|
DamageData |
Значение, тип, источник, отдача, болевой шок и применённые модификаторы |
IDamageProvider |
Источник DamageData |
DamageBase |
Базовая реализация провайдера |
CommonDamage |
Универсальный простой урон |
Damages |
Создание урона нужного вида одной строкой |
DamageOverTimeCoroutine |
Периодический урон: горение, отравление, кровотечение |
DamageResultExtensions |
result.IsApplied() — принят ли результат Damaged или Killed |
DamageOutcome |
Подробный итог: участники удара и их места, WasApplied, AppliedDamage, HealthLost, WasHealthReduced, WasCritical |
IDamageable |
Контракт объекта, принимающего урон; TakeDamage отдаёт DamageOutcome |
IHealthEntity |
Контракт носителя здоровья: Kill, Revive, Spawn, IsAlive |
IHealthProvider |
Ссылка сущности на её HealthComponent; используется поиском живых и мёртвых |
HealthComponent |
Здоровье, смерть, лечение и возрождение сущности |
EntityHitBoxBase |
Перенаправление попадания от коллайдера к сущности |
UnitHitBox |
Зона тела и множитель урона |
DamageResistanceComponent |
Сопротивления сущности различным типам урона |
DamageHookEvent |
Изменение или блокировка урона до применения |
DamageRules, DamageRule, MultiplyDamageRule |
Правила урона игры: проект, сцена, код — через хук урона |
CombatEvents |
Глобальные события попадания и убийства |
Добавьте HealthComponent на тот же GameObject, где расположен наследник EntityBase. Начальное здоровье при Start() устанавливается равным MaxHealth.
Флаг Is Immortal: по сущности бьют, она реагирует на удары, но сломать её нельзя.
Удар получает результат Damaged, а AppliedDamage содержит полный урон после модификаторов, даже если он больше текущего HP. Например, удар на 250 по бессмертной цели со 100 HP даёт AppliedDamage == 250, HealthBefore == HealthAfter == 100 и HealthLost == 0.
Is Block Damage — другое: заблокированный урон не проходит, попадание приходит с Blocked, эффекты молчат.
| Флаг | Удар засчитан | Эффекты | Здоровье | Может умереть |
|---|---|---|---|---|
| обычная сущность | да | да | тратится | да |
Is Immortal |
да | да | не тратится | нет |
Is Block Damage |
нет (Blocked) |
нет | не тратится | нет |
OnHealthChange у бессмертной сущности не поднимается: здоровье не менялось. Факт удара приходит через OnDamageProcessed, через него работают эффекты урона.
Из кода: health.SetImmortal(true).
Для вынесенного на дочерний объект коллайдера добавьте:
EntityLink— автоматически найдётEntityBaseв родительских объектах;- наследника
EntityHitBoxBase, напримерUnitHitBox; - ссылку на используемый
Colliderв Inspector.
Полная настройка и пример вызова из raycast-оружия приведены в HitBox/README.md.
У UnitHitBox настройте HitGroup, DamageMultiplier и при необходимости IsCritical. Попадание в голову автоматически получает флаг Critical.
Пример множителей в стиле классического шутера:
| Зона | Пример множителя |
|---|---|
| Голова | 4.0 |
| Грудь, руки | 1.0 |
| Живот | 1.25 |
| Ноги | 0.75 |
Это стартовые значения: баланс задаётся на каждом хитбоксе в Inspector.
DamageOutcome outcome = target.TakeDamage(
attacker,
weapon,
Damages.Bullet(25f, knockBack: 4f));TakeDamage возвращает DamageOutcome. Результат, урон и изменение здоровья доступны
прямо в возвращённом объекте. Если хитбокс не нашёл здоровье цели, он возвращает
DamageOutcome.NotHandled(...); встроенные реализации не возвращают null.
Итог знает, кто и чем ударил:
if (outcome.WasApplied)
scoreboard.AddHit(outcome.Attacker, outcome.Victim, outcome.Weapon, outcome.AppliedDamage);Attacker— атакующий; у урона от окружения пуст;Victim— сущность, которая приняла удар;Weapon— оружие; у урона без оружия пуст;DamageProvider— провайдер в том виде, в каком он дошёл до здоровья;AttackerPosition,VictimPosition— где они стояли в момент удара.
Места — снимок на момент удара, а не позиция живого объекта: после смерти или уничтожения снаряда спросить уже нельзя. По ним берут направление отброса, точку посмертного эффекта и разбор того, где игроков убивают.
Места пусты, если участник не стоит на сцене, например у урона от окружения.
Участники заполнены у любого результата, включая Miss, Blocked и NotHandled. У отказов DamageData пуст, а то, чем пытались ударить, показывает DamageProvider. Собственная реализация IDamageable, когда обработать урон некому, передаёт участников в DamageOutcome.NotHandled(attacker, victim, weapon, damage).
Наследников на каждый вид заводить не нужно: сопротивления, зональные множители и события читают флаги DamageType. Сам DamageType комбинируется: один удар может быть огненным, взрывным и по площади.
Выражает это Damages:
Damages.Fire(10f);
Damages.Poison(5f);
Damages.Of(30f, DamageType.Fire | DamageType.Explosion, knockBack: 5f);Именованные методы есть на каждый вид из DamageType: Common, Fall, Bullet,
Fire, Ice, Electric, Poison, Radiation, Explosion, Acid, Mental.
Составной урон и пометки вроде AreaOfEffect — через Of. Готовые данные заворачивает
обратно в провайдер Damages.From(data).
Вместо new CommonDamage(10f, 0f, DamageType.Fire) пишите Damages.Fire(10f): вид нельзя забыть или перепутать с силой отдачи.
Critical через Damages обычно не ставят — его выставляет зональный декоратор при
попадании в критическую зону.
new CommonDamage(...) тоже остаётся рабочим.
Если известна точка или конкретный коллайдер попадания, используйте соответствующую перегрузку TakeDamage(). Она дополнительно вызовет OnHitVector или OnHitCollider.
Возможные результаты:
NotHandled— подходящий обработчик не найден;Miss— попадание отменено или сущность недоступна для урона;Blocked— урон заблокирован;Damaged— попадание принято, в том числе при нулевом уроне или бессмертии;Killed— урон привёл к смерти.
У перечисления один extension: result.IsApplied(). У подробного итога есть свойство
outcome.WasApplied. Оба принимают Damaged и Killed.
| Случай | outcome.WasApplied |
outcome.WasHealthReduced |
|---|---|---|
| Обычная цель, положительный урон | да | да |
Цель с Is Immortal |
да | нет |
| Нулевой урон | да | нет |
Miss, Blocked, NotHandled |
нет | нет |
if (outcome.WasApplied)
PlayHitEffects();
if (outcome.WasHealthReduced)
stats.AddDamageDealt(outcome.HealthLost);AppliedDamage используют эффекты и всплывающие числа. HealthLost показывает
фактическую потерю HP для статистики и прогресса. У обычной цели оба значения
ограничены её оставшимся здоровьем; у бессмертной AppliedDamage содержит весь
принятый урон, а HealthLost равен нулю.
Для ручного создания данных можно использовать фабричный метод:
DamageData data = DamageData.Create(damage: 15f, painShock: 20f);
data.DamageType = DamageType.Fire | DamageType.AreaOfEffect;
data.DamageSource = attacker;
IDamageProvider damage = new CommonDamage(data);DamageType является набором флагов. Несколько попаданий одной атаки можно объединить через CombineDamageData(), передав общий DamageId.
Дополнительные поля результата:
RawDamage— исходное значение до зональных множителей и защиты;AbsorbedDamage— сколько урона поглотили сопротивления;HitGroup— зона попадания.
DamageSource вручную указывают только для урона, который приходит не через
TakeDamage. В обычном конвейере его проставляет HealthComponent из атакующего,
если провайдер оставил поле пустым.
Оружие необязательно: TakeDamage принимает null. Оружие только передаётся в DamageHookEvent.Weapon и события как пометка «чем ударили».
Шипы, лава, падение, отравление, таран сущностью — всё это происходит само по себе, и предъявить там нечего. Для таких случаев есть перегрузки без аргумента оружия:
health.TakeDamage(trap, Damages.Of(20f, DamageType.Fall));
hitBox.TakeDamage(spikes, Damages.Common(15f), hit.point);Перегрузки читаются яснее, чем TakeDamage(attacker, null, damage): null в вызове выглядит как забытый аргумент.
Оружие заводят, когда ударивший предмет нужно назвать отдельно от атакующего: брошенный камень, ловушка с именем в списке убийств. Сущность-источник уже передана как attacker, отдельная обёртка ей не нужна.
Горение, отравление и кровотечение описывает DamageOverTimeCoroutine: эффект тикает во времени, а не даёт разовый расчёт, как IDamageProvider.
var burn = new DamageOverTimeCoroutine(
target: health,
attacker: attacker,
weapon: weapon,
damage: Damages.Fire(3f),
tickInterval: 1f,
tickCount: 5);
burn.Execute();tickCount: DamageOverTimeCoroutine.Infinite тикает, пока эффект не остановят
(burn.Stop()) или цель не выйдет из-под урона.
Время игровое, пауза учитывается. Отсчёт идёт по PRTime.GameDeltaTime и встаёт на WaitPause: замедление растягивает горение вместе с игрой, а пауза не сбрасывает ожидание.
Первый тик приходит через интервал, а не сразу.
Перед каждым тиком проверяется, можно ли ещё бить. Цель жива, не блокирует урон, принимает его (CanTakeDamage), а её сущность на сцене и не в пуле. Если проверка не прошла, эффект заканчивается.
Владельца корутины можно не задавать. Тогда она живёт на глобальном хосте и не обрывается вместе с тем, кто её запустил. Уничтоженную цель эффект замечает сам — по Unity-null.
Каждый тик получает свой DamageId. Пометку DamageType.TimeBased эффект ставит сам.
MultiplyDamageDecorator создаёт копию исходных данных, умножает урон и по умолчанию добавляет флаг Critical:
IDamageProvider baseDamage = new CommonDamage(20f);
IDamageProvider criticalDamage = new MultiplyDamageDecorator(
baseDamage,
multiply: 2f,
addCriticalFlag: true);
target.TakeDamage(attacker, weapon, criticalDamage);Применённый модификатор записывается в DamageData.AppliedModifiers, поэтому один идентификатор эффекта не применяется к одним данным повторно. По умолчанию каждый MultiplyDamageDecorator получает собственный Guid: независимые множители перемножаются. Для повторных обёрток одного эффекта передайте одинаковый modifierIdentifier; MultiplyDamageHook хранит такой идентификатор на экземпляре компонента.
Перед проверкой блокировки HealthComponent публикует DamageHookEvent. Слушатель может:
- заменить урон через
ModifyDamage(); - изменить результат через
ModifyResult(); - полностью заблокировать попадание через
BlockDamage(); - отметить промах через
MissDamage().
Хуки следует использовать для брони, иммунитетов, командных правил и других эффектов, которые должны сработать до изменения здоровья.
Слушателей на одну сущность может быть несколько. Порядок задаёт Order: меньшее значение вызывается раньше.
Смысл диапазонов Order:
| Диапазон | Что делает хук | Примеры |
|---|---|---|
| до −100 | решает, пускать ли удар вообще | DamageConditionBase (−200) |
| −100…−1 | отменяет удар по состоянию цели | InvulnerabilityComponent (−100) |
| 0 | правит число урона | DamageResistanceComponent, MultiplyDamageHook |
| больше 0 | смотрит на итог, ничего не меняя | счётчики, логи, аналитика |
Правило: сначала отказ, потом число, потом наблюдение. Хук, который отменяет удар, стоит раньше тех, кто считает урон.
Внутри одного значения порядок не определён. Двум хукам, которые спорят за одно и то же, нужны разные Order.
BlockDamage() и MissDamage() ставят Supercede. Остальные слушатели должны его проверять, чтобы не работать впустую:
public void HandleHook(DamageHookEvent eventArgs)
{
if (eventArgs.Result == HookResult.Supercede)
return;
...
}Хуки обычно вешают на цель. Правила урона (папка Rules) описывают правила игры целиком, например «боты не ломают блоки» или «игроки не бьют друг друга», без компонентов на каждой цели. Правило видит любой удар: касание, ближний бой, снаряд, взрыв.
Один на всю игру. Правила приходят из трёх мест и дальше неотличимы:
| Откуда | Как | Сколько действует |
|---|---|---|
| проект | PRUnitySDK.Settings.DamageRules → Rules |
на всех сценах; ставятся на старте SDK |
| сцена | компонент DamageRuleComponent |
пока компонент включён |
| код | DamageRules.Instance.Add(rule, owner) / Remove(owner) |
пока не снимут |
Правила работают через хук урона: они стоят в общем порядке Order вместе с условиями, неуязвимостью и сопротивлениями. Сервис спрашивается на каждом ударе.
Владелец (owner) нужен, чтобы снять ровно свои правила: режим снимает свои, сцена —
свои, чужие остаются.
// на время боя с боссом: игроки не бьют друг друга
var rule = new MultiplyDamageRule(0f) { Attackers = PlayerTypeFlags.Human };
rule.AddVictimType(EntityTypeEnumerations.Player);
DamageRules.Instance.Add(rule, this);
// бой кончился
DamageRules.Instance.Remove(this);DamageRule — обычный объект ([SerializeReference]): его кладут в настройки, на сцену или создают в коде. Фильтры общие:
| Фильтр | Что задаёт | Пусто |
|---|---|---|
Attackers (PlayerTypeFlags) |
чьи удары: игроков, ботов, NPC | любой атакующий, в том числе не игрок |
Victim Types (виды сущностей) |
по кому | любая цель |
Готовое правило — MultiplyDamageRule:
Action |
Что происходит |
|---|---|
Multiply |
урон умножается; 0 — удар без урона: попадание засчитано, эффекты играют, цель цела |
Block |
удар отклонён (Blocked), эффекты урона не играют |
Order следует таблице выше: Multiply — на 0, Block принято ставить на −200
(MultiplyDamageRule.CreateBlock() так и делает). Order читается при добавлении правила в сервис: чтобы его поменять, снимите правило и добавьте снова.
Своё правило — наследник DamageRule с методом Apply; фильтры и место в очереди
хуков даёт база и сервис.
«Кто кого бьёт» по сторонам и командам — не набор правил, а матрица в настройках:
см. Core/@Entity/Sides. SDK сам ставит для неё
правило EntitySideDamageRule рядом с правилами проекта.
Пример — «боты бьют блоки, но не ломают» (игра PickBox): MultiplyDamageRule
с Attackers = AI, Victim Types — виды блоков, Multiplier = 0.
Добавьте DamageResistanceComponent на объект с EntityBase и заполните список Resistances. Каждое правило содержит маску типов и множитель:
0— полный иммунитет;0.5— половина урона;1— без изменения;- значение больше
1— уязвимость.
Если в маске правила указано несколько флагов, оно срабатывает при совпадении любого из них. Подходящие правила применяются последовательно, поэтому их множители перемножаются.
Пример настроек:
| Типы | Множитель | Результат |
|---|---|---|
Bullet |
0.8 | Защита от пуль 20% |
Fire |
0 | Иммунитет к огню |
Poison, Radiation |
1.5 | Уязвимость к яду и радиации |
Локальные события HealthComponent:
OnHealthChange;OnEntityDead;OnRevive;OnHitVector;OnHitCollider;OnSpawn;OnScaleChanged.
OnDamageProcessed передаёт DamageOutcome после любой попытки нанесения урона. Он содержит участников удара, результат, здоровье до и после, засчитанный и поглощённый урон, фактическую потерю HP. Последний результат также доступен через LastDamageOutcome.
OnHit — попадание, которое стоит показать игроку: эффекты, звук, цифра урона. Приходит
сразу после локального OnDamageProcessed, только для засчитанного удара и только если
урон больше нуля. Удар без урона (правило урона умножило его на 0) приходит, если проект
включил PRUnitySDK.Settings.Health.ZeroDamageIsHit. Промах, блок и смерть через Kill
не приходят никогда. То же решение даёт HealthComponent.IsHit(outcome) — им пользуются те,
кому попадание приходит не из события.
На OnHit подписаны HealthSound и эффекты урона. Подписчикам не нужно проверять ноль: настройка проекта решает за всех.
OnHitVector и OnHitCollider передают тот же DamageOutcome. Точка и коллайдер лежат в нём (HitPoint, HitCollider), атакующий и оружие — там же:
health.OnHitCollider += outcome => PlayImpact(outcome.HitCollider, outcome.Attacker);Глобальные события:
IOnTakeDamageEvents.OnTakeDamage()— применённый урон, включая смертельный;IEntityKillEvent.OnKill()— убийство сущности: смертельным уроном или командойKill;IDamageProcessedEvents.OnDamageProcessed()— любая завершённая попытка, включаяMiss,BlockedиNotHandled.
TakeDamageEvent содержит Outcome, Result, AppliedDamage и снимок итогового DamageData. EntityKillEventArgs также содержит тот же Outcome, поэтому обработчику убийства доступны тип урона, зона попадания и фактически снятое здоровье (HealthLost).
Пример глобального слушателя:
public sealed class CombatLog : PRMonoBehaviour, IDamageProcessedEvents
{
public void OnDamageProcessed(DamageProcessedEvent args)
{
PRLog.WriteDebug(
this,
$"{args.Attacker} -> {args.Victim}: " +
$"{args.Outcome.Result}, damage={args.Outcome.AppliedDamage}");
}
}Порядок обработки и уведомлений:
- Pre-хуки изменяют или отменяют урон.
- Фиксируются HP, состояние смерти, убийца и
LastDamageOutcome. - Post-хуки получают
DamageHookEvent.Outcomeс окончательным результатом. - Вызывается локальный
OnHealthChange; при смерти —DeathHandle, скрытие объекта иOnEntityDead. - Вызывается локальный
OnDamageProcessed. Если исход — попадание (IsHit), следом вызывается локальныйOnHit. - Публикуется глобальный
OnTakeDamage; при смерти —OnKill. - Публикуется глобальный
OnDamageProcessed. - Перегрузка с точкой/коллайдером вызывает
OnHitVector/OnHitCollider.
События попадания вызываются для всех результатов, кроме Miss, включая Blocked и NotHandled. Сам факт вызова OnHitCollider не означает списания HP — проверяйте outcome.Result.
BlockDamage, MissDamage и обычный Supercede подавляют оригинальное действие. Обычный Supercede без причины даёт NotHandled. У отказов вызываются только локальное и глобальное OnDamageProcessed (и событие попадания по правилу выше). Нулевой урон даёт Damaged с AppliedDamage == 0; если нужен эффект только при потере HP, проверяйте WasHealthReduced.
Post-хук предназначен для наблюдения: изменение его DamageProvider или DamageResult не переигрывает уже завершённую попытку. Используйте Outcome как окончательный результат. Переключение бессмертия в post-хуке также не меняет событие здоровья завершённого удара. StopPropagation останавливает дальнейшие хуки, включая post-этап, но сам по себе не отменяет оригинальное действие.
Локальные подписчики вызываются независимо: исключение логируется, остальные подписчики и EventBus продолжают получать события. Ошибка хука логируется; уже применённый урон всё равно публикует уведомления. До применения ошибка завершает попытку как NotHandled. Последующие хуки после исключения не выполняются.
DamageOutcome.DamageData и TakeDamageEvent.Damage возвращают защитную копию. Изменение полученного объекта или AppliedModifiers не меняет результат для других подписчиков.
В Inspector доступен раздел Unity Events. Для каждого C#-события есть
сериализованный аналог с суффиксом Unity, вызываемый сразу после него:
| UnityEvent | Аргументы |
|---|---|
OnEntityDeadUnity |
убийца и погибшая сущность (IEntity, IEntity) |
OnReviveUnity |
кто оживил (IEntity) |
OnSpawnUnity |
позиция (Vector3) |
OnScaleChangedUnity |
трансформ (Transform) |
OnHealthChangeUnity |
HealthChangedEventArgsBase |
OnDamageProcessedUnity |
DamageOutcome |
OnHitColliderUnity |
DamageOutcome |
OnHitVectorUnity |
DamageOutcome |
Можно назначать обычные Inspector-действия без аргументов, например запуск звука
или эффекта, либо методы с совпадающей динамической сигнатурой. Интерфейсные
аргументы передаются во время вызова, а не задаются как ссылки в Inspector.
Из кода используется AddListener/RemoveListener.
Условия срабатывания совпадают с C#-событиями: лечение вызывает изменение здоровья,
а результат Miss не вызывает события попадания. Исключение в UnityEvent логируется
и не прерывает последующие этапы обработки урона и CombatEvents. Внутри одного
UnityEvent исключение может прервать оставшиеся обработчики этого UnityEvent —
это стандартное поведение Unity.
Убийство без урона — зона смерти, скрипт, истёкший таймер — идёт через Kill:
DamageOutcome outcome = health.Kill(deadZoneEntity, force: true);
if (outcome.Result == DamageResult.Killed)
ShowDeathMessage(outcome.Victim);Kill(killer, weapon = null, force = false)— убийца попадает вKillerи в события;KillByGame(force = false)— от имени игры. Это не перегрузкаKill: компонент Unity неявно приводится кbool, иKill(npc)попадал бы в неё — принудительная смерть без убийцы;Suicide()— сущность убивает себя сама.
Все три возвращают DamageOutcome: Killed при смерти, Blocked при бессмертии без
force, либо NotHandled, если сущность уже мертва.
AppliedDamage в таком итоге равен нулю при полной потере HP — урона никто не наносил,
поэтому эффекты попадания и всплывающие числа молчат. DamageData и DamageProvider
пусты, участники заполнены.
Команда минует расчёт урона: хуки и IsBlockDamage не проверяются.
Обычный Kill учитывает IsImmortal; force: true обходит бессмертие.
DeadZone использует принудительный вызов. Повторный вызов для уже мёртвой сущности
не публикует смерть ещё раз.
SetImmortal(bool) сохраняет HP при принятых атаках. SetBlockDamage(bool) отклоняет
атаки с результатом Blocked, без события OnHit. Оба метода возвращают компонент
и позволяют настроить защиту цепочкой: health.SetImmortal(true).SetBlockDamage(true).
Уведомления те же, что у смертельного урона, кроме глобального OnTakeDamage: его такая
смерть не публикует, чтобы не попасть в статистику нанесённого урона. Порядок:
OnHealthChange → DeathHandle и OnEntityDead → локальное OnDamageProcessed
и LastDamageOutcome → глобальные OnKill и OnDamageProcessed.
Убийство, в том числе сущностью себя или другой сущностью, делается через Kill, а не TakeDamage с большим числом: урон проходит через хуки, сопротивления и бессмертие.
SetDead(killer) — защищённая точка переопределения для сущности с особым порядком
смерти. Уведомления она не рассылает, их отправляет вызывающий.
Добавьте HealthSound на объект с HealthComponent или на его дочерний объект и заполните
списки Damage Clips и Death Clips. Playback Order выбирает последовательное
воспроизведение по кругу или случайный клип; пустые элементы списка пропускаются, у каждого
списка своя очередь. Если здоровье находится в другом месте, укажите его в поле
Health Component. Min Pitch и Max Pitch задают случайную высоту звука; одинаковые
значения дают фиксированный pitch. Громкость и pitch общие для обоих списков.
Компонент подписывается на локальные события здоровья при включении и снимает подписки при выключении. На одно событие играет один звук:
- попадание (
OnHit) играет звук урона: урон больше нуля, включая удар по бессмертной цели, а приHealth.ZeroDamageIsHit— и удар без урона; - смерть играет звук смерти, в том числе смерть через
Kill; - если
Death Clipsпуст, смертельный удар звучит как обычный урон, аKillпроходит без звука, потому что урона в нём нет; - промах, блок и лечение звука не дают.
Для позиции используется точка попадания, затем место жертвы из DamageOutcome.
AddHealth() лечит только живую сущность и не превышает MaxHealth. При успешном лечении вызывается OnHealthChange.
Методы Revive() восстанавливают сущность, при необходимости меняют позицию и поворот, включают скрытый при смерти объект и вызывают OnRevive.
Место сущности задаётся через EntityBase.SetPositionAndRotation. Revive() без аргументов оживляет на месте по Entity.Position, а не по позиции корня (см. «Позиция сущности» в Core/@Entity/README.md).
InitHealth() выдаёт полное здоровье и уведомляет подписчиков через OnHealthChange. Этим же методом сущность возвращают на сцену из InitializeEntity() при восстановлении спрятанных и при выдаче из пула.
TickDamageBrainдаёт только ритм тиков и объявленabstract: кого бить, знает наследник — он же заполняетAttackerиDamageProviderи наносит урон вAttack(). Для урона по одной известной цели объект на сцене не нужен вовсе, этоDamageOverTimeCoroutine.KnockBackPower,PainShockиIActiveDamageModifierхранят данные для внешних обработчиков;HealthComponentсамостоятельно их не применяет.- Отрицательные, NaN и бесконечные Damage, RawDamage или AbsorbedDamage отклоняются как NotHandled. Для лечения используйте AddHealth с положительным значением.
- Создавайте новый
DamageIdдля каждой независимой атаки. - Указывайте
DamageSourceтолько там, где урон не проходит черезTakeDamage. - Не изменяйте общий
DamageDataиз нескольких систем одновременно; используйтеClone()или декораторы. - Проверяйте
DamageOutcome.WasAppliedилиDamageOutcome.Result, если после попадания требуется отдельная реакция.