Условия — переиспользуемые проверки с единым контрактом. Условие отвечает на вопрос и ничего не меняет: «открыта ли цель», «хватает ли кубков», «выполнен ли набор правил».
| Тип | Назначение |
|---|---|
ConditionResult |
Исход проверки, локализованное описание отказа и иконка |
ICondition |
Контракт с методом Evaluate(ConditionContextBase context) и Evaluate() без контекста |
ConditionContextBase |
Обстоятельства проверки: для кого и при каких данных |
ConditionContextEmpty |
Контекст без обстоятельств — для правил состояния мира |
ConditionActorContext |
Контекст с участником: инициатор или атакующий |
ConditionBase |
Основа условий-ассетов |
ResourceCondition |
Ассет: накопленный ресурс сравнивается с числом |
ConditionCollection |
Ассет-набор из других условий-ассетов |
AllCondition |
Встроенное: выполнено, когда выполнены все вложенные |
AnyCondition |
Встроенное: выполнено, когда выполнено хотя бы одно |
NotCondition |
Переворачивает вложенное условие |
AlwaysCondition |
Встроенное: выполнено всегда — для слота, где условие нужно, а спрашивать нечего |
AssetCondition |
Мостик: подставляет условие-ассет туда, где ждут встроенное |
ResourceInlineCondition |
Встроенная форма того же правила |
RegisteredCondition |
Выбирает правило, объявленное кодом, по имени |
ConditionRegistry |
Реестр правил кода: имя → ответ |
ConditionEnumerations |
Набор имён правил; игра дополняет его partial-частью |
ConditionComparison |
Как сравнивать: >=, >, ==, !=, <=, < |
ConditionMatch |
Сколько вложенных условий должно выполниться: все или любое |
IConditionProvider |
Владелец отдаёт своё условие наружу — для показа игроку |
ConditionDescription |
Подпись условия частями: перевод, аргументы, иконка |
ConditionDescriptions |
Собирает подпись по условию; сюда же регистрируют свои |
ConditionLabels |
Тексты требований, свой на каждое сравнение |
[SerializeReference, ReferenceSelector]
[Tooltip("Когда по этой цели можно бить. Пусто - всегда.")]
private ICondition condition;Дизайнер открывает объект, выбирает в выпадашке реализацию и настраивает её прямо там. Ассет под каждый порог заводить не нужно.
Те же, что у действий, и выбираются по тому же признаку:
| Способ | Когда | Как объявляется | Набор |
|---|---|---|---|
| встроенное | условие уникально для этого объекта | [SerializeReference, ReferenceSelector] ICondition |
AllCondition / AnyCondition |
| ассет | одна настройка нужна в разных местах | наследник ConditionBase |
ConditionCollection |
Ассет в слот SerializeReference напрямую не кладётся: это ссылка на объект Unity, а не
встроенный объект. Для этого есть AssetCondition — встроенное условие, которое спрашивает
ассет. Через него общее правило попадает и в список AllCondition, и в любой другой слот.
Evaluate(context) получает обстоятельства проверки — наследника ConditionContextBase.
Новый контекст добавляется, не меняя существующие условия.
| Контекст | Кто передаёт |
|---|---|
ConditionContextEmpty.Instance |
правила состояния мира; его подставляет Evaluate() |
ConditionActorContext |
хуки срабатывания и урона — с инициатором или атакующим |
Участник — context.Actor, у пустого контекста он null.
Условие, которому нужны особые данные, приводит контекст к своему типу. Не тот тип значит «данных нет»: условие отвечает так же, как на пустой слот.
Составные условия передают тот же контекст каждому вложенному правилу.
У правила с настройками бывает обе формы, и различаются они именем:
| Форма | Класс | Где живёт |
|---|---|---|
| ассет | ResourceCondition |
файлом, ссылаются откуда угодно |
| встроенная | ResourceInlineCondition |
в слоте владельца |
Приставка Inline у встроенного, чистое имя у ассета — так же устроены действия:
OpenURLAction против AddResourceInlineAction.
Обе формы самостоятельные: у каждой свои поля прямо в ней, без промежуточных блоков и выбора типа из выпадашки.
Расчёт общий: лежит статическими методами у ассета (Matches, GetMissing, GetBalance),
а публичная проверка Evaluate возвращает результат.
Поля продублированы в обеих формах: ассет и встроенный объект Unity сохраняют их по-разному.
Trophies10.asset ResourceCondition кубки >= 10
Trophies100.asset ResourceCondition кубки >= 100
Trophies1000.asset ResourceCondition кубки >= 1000
Цели ссылаются на эти файлы через AssetCondition, и порог правится в одном месте
сразу для всех, кто на него смотрит.
Набор собирают на двух уровнях: в инспекторе владельца или ассетом.
В инспекторе владельца — AllCondition, AnyCondition, NotCondition. Это обычные
встроенные условия, поэтому вкладываются друг в друга:
AllCondition
├── ResourceInlineCondition кубки >= 100
└── NotCondition
└── AssetCondition «идёт обучение»
Ассетом — ConditionCollection: именованный набор из других условий-ассетов
с переключателем «все» или «любое». «Открыто во втором мире» — это и уровень,
и пройденный туториал, и купленный ключ. Собрав их один раз, дальше ссылаются
на набор, а не перечисляют составляющие в каждом месте.
Наборы вкладываются в наборы. Ссылку на самого себя набор пропускает, иначе проверка ушла бы в бесконечную рекурсию.
Правило по ресурсу не привязано к «набрал столько-то» — сравнение выбирается (в обеих формах):
Comparison |
Читается как |
|---|---|
GreaterOrEqual (по умолчанию) |
накоплено столько же или больше |
Greater |
строго больше |
Equal |
ровно столько |
NotEqual |
не столько |
LessOrEqual |
столько же или меньше |
Less |
строго меньше |
Обычное правило открытия — GreaterOrEqual. Обратные нужны подсказкам и предупреждениям:
подсказка висит, пока кубков Less 5; сообщение «кончились ключи» — это Equal 0.
Ноль не отсекается коротким путём: «ресурса нет вовсе» — законное условие. Незаданным считается только пустая ссылка на ресурс.
Missing — сколько не хватает до выполнения. Осмысленно там, где ресурс копят
(Greater, GreaterOrEqual); у остальных накопление к выполнению не ведёт,
и число всегда ноль.
Для правил без настроек — «туториал пройден», «идёт бой с боссом», «первый запуск» — есть реестр:
public partial class ConditionEnumerations
{
public static readonly Enumeration TutorialFinished = new(nameof(TutorialFinished));
}
// при готовности SDK
ConditionRegistry.Instance.Register(
ConditionEnumerations.TutorialFinished,
() => ProjectPropertiesManager.Instance.GetBool("tutorial.finished"));Правило, которому нужны обстоятельства проверки, регистрируют функцией с контекстом:
ConditionRegistry.Instance.Register(MyConditions.CanUseButton,
context => context.Actor != null && context.Actor.GetComponent<PlayerBase>() != null);В инспекторе такое правило выбирает RegisteredCondition — обычное встроенное условие,
поэтому оно так же вкладывается в списки и наборы.
Имена лежат в ConditionEnumerations: набор пустой, игра дополняет его partial-частью,
как и остальные Enumeration в проекте. Имена берите оттуда, а не пишите строкой:
опечатка молча оставит ассет без правила.
Правило с числом («сто кубков») настраивается в инспекторе через ResourceCondition,
а не в коде: в реестре порог перестанет быть делом дизайнера.
Здесь единственное место, где о недонастроенном сообщают: имя выбрано, но правила за ним нет. Значит, это опечатка или регистрация не состоялась. В остальных случаях пустая настройка значит «правила нет».
Ответ всё равно ConditionResult.Success: недонастроенное не запирает. В логе ошибка,
игра не встаёт. Жалоба на одно имя пишется один раз.
Ассеты могут спрашивать, пока игра ещё не собрана; регистрация выполняется при готовности SDK.
Повторная регистрация заменяет прежнее правило, так сцена переопределяет его.
Правило от имени сцены снимайте через Unregister: иначе замыкание держит её объекты,
а ответ считается по тому, чего уже нет.
Пустое условие разрешает, а не запрещает. Пустой список, пустая ссылка, невыбранный ресурс — всё это считается выполненным.
Забытая настройка открывает объект чуть раньше задуманного, а не запирает его навсегда без ошибки в логе.
Evaluate вызывают на каждый удар и каждый кадр. Ответ должен быть дешёвым и не менять
состояние игры: условие, которое что-то делает, нельзя использовать для простой проверки.
Правило по ресурсу спрашивает баланс каждый раз, а не запоминает его при старте: накопить нужное могут по ходу игры, и цель должна открыться сразу.
Правило по ресурсу ходит в WalletService, как и магазин, а не в ResourceManager напрямую.
Missing даёт готовое число нехватки, например «нужно ещё сорок».
Условие возвращает ConditionResult с исходом и причиной отказа. Игроку нужно объяснить,
чего и сколько не хватает.
IConditionProvider — владелец условия отдаёт его наружу:
public class DoorLock : PRMonoBehaviour, IConditionProvider
{
[SerializeReference, ReferenceSelector] private ICondition condition;
public ICondition Condition => condition;
}Подпись спрашивает условие у компонента, не зная его типа. Владелец решает, пускать ли удар или срабатывание, и интерфейс не рисует.
В проекте его реализуют условия урона и срабатывания — DamageCondition
и TriggerCondition из private-слоя.
ConditionDescriptions.Get(condition) собирает подпись — ConditionDescription:
перевод, аргументы функцией и иконку. Готовой строки не возвращает: подпись висит
на экране, и язык могут сменить. Для конкретного участника используйте
ConditionDescriptions.Get(condition, context).
Правила по ресурсу описаны из коробки, в обеих формах. Подпись берётся из ConditionLabels
и зависит от сравнения: «нужно набрать» и «должно остаться не больше» — разные требования.
Подпись набора описывает одно вложенное условие — то, которое ещё не выполнено.
Своё условие объявляет описание само:
ConditionDescriptions.Register<BossDefeatedCondition>(
condition => new ConditionDescription(BossLabels.Defeat, icon: condition.BossIcon));Незнакомое условие остаётся без подписи: табличка над целью не создаётся, в лог пишется сообщение.
Сам показ — дело интерфейса: ConditionPlateInstaller в приватном слое
(Components/ConditionPlate).
Evaluate(context) и Evaluate() возвращают ConditionResult.
Используйте .IsSuccess для разрешения и .IsFailed для отказа.
default считается отказом без причины.
public ConditionResult Evaluate(ConditionContextBase context)
{
return CanEnter(context.Actor)
? ConditionResult.Success
: ConditionResult.Fail(DoorLabels.KeyRequired);
}Fail(provider, args, icon) принимает локализованный текст, функцию аргументов
и необязательную иконку. Провайдер задаётся для English, Russian и Turkey.
Fail(this, context) откладывает построение описания до обращения к FailureDescription.
Описание берёт ConditionDescriptions, включая зарегистрированные. При частых проверках
ресурса описание не создаётся. Для UI возьмите FailureDescription один раз, затем
передайте Text и Args в SetLocalization.
AllCondition и All-набор возвращают первый отказ; Any-варианты — первый отказ,
только если не выполнено ни одного правила. Проверка заканчивается сразу после
определившего исход правила. AssetCondition сохраняет результат ассета.
Отказ NotCondition описывает запрет, а не невыполненное требование внутреннего
условия, которое в этом случае как раз выполнено.
Пустые настройки успешны. Реестр принимает также функции с ConditionResult:
ConditionRegistry.Instance.Register(MyConditions.CanEnter,
context => CanEnter(context.Actor)
? ConditionResult.Success
: ConditionResult.Fail(DoorLabels.KeyRequired));Bool-перегрузки регистрации остаются; при отказе дают общее локализованное описание.
При миграции замените возвращаемый bool на ConditionResult, а потребители bool-проверок
переведите на .IsSuccess / .IsFailed.