Игровая логика: проверки перед изменением
Команды с явным результатом, неизменным состоянием при отказе и понятными уведомлениями.
Одна команда — один контракт
TryApplyDamage принимает положительный урон. Он возвращает false для нуля, отрицательного значения и уже уничтоженной цели. При отказе здоровье не меняется. Большой положительный урон доводит здоровье до нуля, а не уводит его в отрицательные значения.
HealthModel.cs
using System;
public interface IDamageable
{
bool TryApplyDamage(int amount);
}
public sealed class HealthModel : IDamageable
{
public int Maximum { get; }
public int Current { get; private set; }
public HealthModel(int maximum)
{
if (maximum <= 0) throw new ArgumentOutOfRangeException(nameof(maximum));
Maximum = maximum;
Current = maximum;
}
public bool TryApplyDamage(int amount)
{
if (amount <= 0 || Current == 0) return false;
Current = amount >= Current ? 0 : Current - amount;
return true;
}
}Проверки находятся внутри модели, поэтому одинаковы для UI, попадания снаряда и теста. Контроллер не должен сначала сам менять здоровье, а потом отдельно решать, допустимо ли действие.
Где вызывать
В полном примере HitController вызывает команду по событию игрового действия. Для этой операции не нужен опрос в _Process. Непрерывное движение, физику и обработку времени размещайте в соответствующих callbacks, когда задача действительно требует каждого кадра.
DamageReceiver отправляет HealthChanged только после успешной команды. Нельзя отнять здоровье напрямую, вызвать сигнал вручную и считать, что проверка TryApplyDamage уже выполнена.
Пример отказа — часть поведения
| Ситуация | Результат |
|---|---|
| 100 здоровья, урон 25 | true, остаётся 75 |
| Урон 0 или -1 | false, значение сохраняется |
| Урон int.MaxValue | true, остаётся 0 |
| Повторный урон мёртвой цели | false |
| В HitController нет цели, цель удалена или ждёт удаления | false |
Эти правила относятся к примеру. Лечение, броня, неуязвимость и сетевые полномочия требуют собственных явных правил — название интерфейса их не добавляет.