Skip to content

Latest commit

 

History

History
308 lines (225 loc) · 19 KB

README.md

File metadata and controls

308 lines (225 loc) · 19 KB

Conditions

Условия — переиспользуемые проверки с единым контрактом. Условие отвечает на вопрос и ничего не меняет: «открыта ли цель», «хватает ли кубков», «выполнен ли набор правил».

Основные типы

Тип Назначение
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

Ассеты могут спрашивать, пока игра ещё не собрана; регистрация выполняется при готовности SDK. Повторная регистрация заменяет прежнее правило, так сцена переопределяет его. Правило от имени сцены снимайте через Unregister: иначе замыкание держит её объекты, а ответ считается по тому, чего уже нет.

Правило про недонастроенное

Пустое условие разрешает, а не запрещает. Пустой список, пустая ссылка, невыбранный ресурс — всё это считается выполненным.

Забытая настройка открывает объект чуть раньше задуманного, а не запирает его навсегда без ошибки в логе.

Evaluate зовут часто

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.