Проектирование гибкой системы способностей в играх с использованием цепочки ответственности

Проектирование гибкой системы способностей в играх с использованием цепочки ответственности

7 августа 2025 г.

Введение: Почему системы способностей должны быть гибкими

В разработке игры система способностей часто является одним из самых требовательных компонентов с точки зрения гибкости. На этапе дизайна практически невозможно предсказать, какие заклинания, способности или навыки будут существовать в окончательной версии - или какие обновления будут представлены в будущем.

Эта статья о том, как я подошел к этой неопределенности, абстрагируя процесс выполнения способностей.

По своей сути способность - это не что иное, как набор действий. Интерфейс минималистичной способности может состоять из одного метода, подобногоapply()Полем Но на практике все так редко так просто. Сложность не входит в называние способности - она заключается в определении того, может ли способность использовать или должна использоваться вообще.

Чтобы справиться с этой сложностью и обеспечить будущее расширение, нам нужен гибкий, модульный подход, который отделяет выполнение способностей из тех условий, при которых он может или не может продолжаться. Это заставляет нас переосмыслить, как структурировать логику способности с нуля.

Эта статья является частью более широкой дискуссии в моей книгеSafe By Design: Исследования в области архитектуры и выразительности программного обеспеченияПолем Если вам это нравится, вы можете бесплатно насладиться полным чтением на GitHub.

Первый слой: способность проверяет как цепные компоненты

Каждая способность начинается с ряда чеков, которые определяют, можно ли ее использовать. Эти проверки обычно такие вещи, как:

  • Является ли способность от перезарядки?
  • У персонажа достаточно маны?
  • Цель в диапазоне?

Сразу же становится очевидным, что не каждая способность нуждается в каждой проверке. Например, некоторые способности могут не потребовать маны или могут быть использованы на любом расстоянии.

Это означает, что разные способности требуют разных наборов предварительных условий. Тем не менее, многие из этих проверок повторно используются по нескольким способностям. Перезарядки, мана и диапазоны распространены в десятках заклинаний. Если эти проверки дублируются повсюду, любое изменение их логики должно применяться во многих местах - создание хрупкости.

Чтобы избежать дублирования и обеспечить гибкость, мы можем извлечь каждый чек в его собственный объект, реализующий общий интерфейс. Затем мы связываем их вместе в одной, заказанной цепочке.

Это классическая цепочка ответственности.

Вот как может выглядеть такой интерфейс:

interface CastChecker {
    CastChecker nextChecker { get; set; }
    bool check();
}

Введите полноэкранную режим выхода из полноэкранного режима

А вот пример простой цепочки:

CooldownChecker → ManaChecker → CastRangeCheckerКаждый шашки выполняет определенную проверку и, если успешно, передает управление следующим в цепочке.

Эта структура позволяет использовать повторное использование, рекомбинацию и централизованные изменения - основу действительно гибкой системы.

Выполнение цепочки: последовательная проверка и обработка ошибок

После того, как мы собрали цепочку объектов касхекеров, система может обработать их последовательно, чтобы подтвердить, может ли способность использоваться.

Каждый шахтер в цепи следует той же логике:

  • Если его собственное условие терпит неудачу, оно останавливает цепь и сообщает об ошибке (например, «недостаточно маны»).
  • Если условие проходит, оно вызывает следующую проверку, продолжая процесс проверки.

Вот простой контур реализации:

bool CastChecker.check() {
    if (!thisConditionIsMet()) {
        showErrorMessageToPlayer();
        return false;
    } else if (nextChecker != null) {
        return nextChecker.check();
    } else {
        return true;
    }
}

Введите полноэкранную режим выхода из полноэкранного режима

Этот дизайн представляет несколько ключевых преимуществ:

1. Компонируемые и поддерживаемые проверки:Вы можете создать пользовательский конвейер проверки на способность без переписывания общей логики. Например:

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

2. читаемый поток:Поскольку каждая проверка является автономной, его логика остается сосредоточенной и понятной. Интерфейс Castchecker позволяет добавлять новые условия без изменения существующих.

3. Централизованная обработка ошибок:Каждая шашка может сообщить о своей причине сбоя - дает четкую, целевую обратную связь для игрока.

Эта модульность - то, что отличает систему от специальной логики проверки. Мы больше не пишем гиганта, если операторы или переключатели. Вместо этого мы собираем такие способности, как блоки LEGO - комбинируя многоразовые, проверяемые предметы.

Абстракция через SkillCastRequest

Теперь, когда мы рассмотрели, как проверить способность с использованием цепочки шашек, нам нужно подумать о том, как на самом деле способность выполняться - и, что более важно, как представлять это выполнение как абстрактный, независимый процесс.

Давайте представим новый интерфейс: SkillCastRequest.

Этот интерфейс не заботится о том, является ли способность мгновенным огненным шаром или многофазным ритуалом. Он просто представляет «запрос на выполнение действия» и разоблачает стандартный способ начать или отменить его:

interface SkillCastRequest {
    void startRequest();
    void cancelRequest();
}

Введите полноэкранную режим выхода из полноэкранного режима

Эта абстракция позволяет нам относиться к логике исполнения как к первоклассному гражданину в нашей архитектуре.

Вместо того, чтобы иметь каждую способность непосредственно встроить свою собственную сложную логику выполнения (анимации, задержки, вводные окна и т. Д.), Мы разделяем ее в объект многократного использования запроса.

Преимущества этого подхода:

  • Воспространенность: та же самая логика запроса (например, зарядная полоса или входная последовательность) может использоваться для нескольких навыков.
  • Прерываемость: запросы могут быть приостановлены, отменены или перезапущены независимо от системы способностей.
  • Асинхронность: поскольку StartRequest () ничего не возвращает, он может легко поддерживать Couroutine-подобные или управляемые событиями потоки.

По сути, эта абстракция отделяет то, что делает навык от того, как он инициируется - критическое различие для создания гибких систем игрового процесса.

TerminalChecker и выполнение навыка

Теперь у нас есть два мощных инструмента в нашем наборе инструментов:

Цепочка кастеккеров, которая подтверждает, можно ли использовать навык. SkillCastRequest, который инкапсулирует процесс выполнения этого навыка. Но как мы связываем их вместе, чтобы гарантировать выполнение, только если все проверки проходят?

Вот где входит терминалхер.

Это специальный узел в цепочке - всегда помещается в конце - чья задача состоит в том, чтобы вызвать фактический вызов StartRequest (), когда все предыдущие проверки достигают успеха.

Пример:

class TerminalChecker implements CastChecker {
    CastChecker nextChecker = null;
    SkillCastRequest request;

    bool check() {
        request.startRequest();
        return true;
    }
}

Введите полноэкранную режим выхода из полноэкранного режима

В полной цепи это может выглядеть так:

COOLDOWNCHECKER → MANACHECKER → RANGECHECKER → TerminalCheckeronly Если первые три прохода проверки начнутся запрос.

Зачем разделять окончательное исполнение?

  • Сохраняет обязанности в чистоте.Каждая проверка только проверяет; Только последний узел запускает выполнение.
  • Легче повторно использовать.Вы можете создавать различные терминальные чековые для различных типов выполнения (например, сетевые запросы, мгновенные локальные эффекты, отсроченные эффекты).
  • Поддерживает асинхронные операции.Например, некоторые навыки могут включать зарядку, нацеливание или ожидание ввода перед разрешением. Объект запроса может справиться с этим без загрязнения логики проверки.
  • Этот последний шаг переворачивает разрыв между способностью работать и продолжить и запустить его.

Если вы наслаждаетесь этим до сих пор, то есть гораздо больше вКнига- Тот же тон, просто глубже. Это прямо здесь, если вы хотите заглянуть.

Привязывание навыка и запроса

Теперь мы разделили логику способностей на два различных домена:

  • Логика валидации - обрабатывается цепочкой касхекеров
  • Логика выполнения - инкапсулирована в SkillCastRequest

Но как мы представляем настоящий навык - то, что игрок может активировать?

Просто: мы связываем обе части вместе под унифицированным интерфейсом.

ОпределениеSkillинтерфейс:

interface Skill {
    string name;
    SkillCastRequest request;
    CastChecker checker;

    bool cast() {
        return checker.check();
    }
}

Введите полноэкранную режим выхода из полноэкранного режима

Когда игрок пытается использовать навык:

  1. Метод CAST () называется.
  2. Цепочка шашки выполняется.
  3. Если достигнут окончательный терминалхер, он начинает SkillCastRequest. Этот дизайн дает нам полное разделение проблем:
  4. Имя и метаданные способности живут в объекте навыка.
  5. Логика проверки живет в своей цепочке шашки.
  6. Логика выполнения живет в запросе. Почему это мощно:
  7. Вы можете повторно использовать шашки и запросы по нескольким навыкам.
  8. Вы можете динамически собирать или поменять детали во время выполнения.
  9. Вы можете подкласс или объекты навыков, чтобы добавить ведение журнала, отслеживание восстановления, аналитику или многопользовательскую синхронизацию - без изменения базовой структуры. Это превращает ваши навыки в чистый состав данных + поведения, что делает их идеальными для дизайнеров, моддеров и процедурной генерации.

Пример и вывод: универсальная структура выполнения

Давайте сложим все это вместе с конкретным примером: Teleportationskill.

Телепортация - это идеальный случай, потому что она нарушает общие предположения:

  • Это не требует маны.
  • Это не может быть использовано в бою.
  • Это требует, чтобы игрок стоял на телепортационной площадке.
  • У него длинный перезарядка.
  • Он должен ждать, пока игрок подтвердит пункт назначения.

Используя нашу архитектуру, это сложное поведение не является проблемой.

Мы собираем это так:

Шашки:

  • Couldownchecker
  • Uncombatchecker (пользовательская логика: игрок должен быть вне бою)
  • SurfaceChecker (проверяет игрок на правильной поверхности)
  • TerminalChecker (запускает запрос)

Запрос:

  • TeleportationRequest, который:
  • Открывает пользовательский интерфейс выбора пункта назначения
  • Ожидает подтверждения
  • Перемещает персонажа

Объект навыка:

Skill teleport = new Skill(
    name = "Teleport",
    checker = new CooldownChecker(
        next = new InCombatChecker(
            next = new SurfaceChecker(
                next = new TerminalChecker(request = teleportationRequest)
            )
        )
    ),
    request = teleportationRequest
);

Введите полноэкранную режим выхода из полноэкранного режима

Весь этот навык полностью декларативный и композиционный. Нет тесной связи, нет дублированной логики. Если позже мы хотим использовать одно и то же поведение телепортации для врагов или элементов - мы просто подключаем тот же запрос.

Последние мысли

Разделяя проверку, выполнение и композицию:

  • Мы получаем модульность: каждый компонент проверен и заменяется.

  • Мы получаем расширяемость: добавление новых чеков или стилей выполнения является тривиальным.

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

    Сводная диаграмма

    Skill ├── name: "Teleport" 
          ├── checker: 
          │    └── CooldownChecker 
          │         └── InCombatChecker 
          │              └── SurfaceChecker 
          │                   └── TerminalChecker → request.startRequest() 
          └── request: TeleportationRequest
    

Это универсальная структура не только для заклинаний или атак, но и для любого игрового механика, где действие зависит от условий.

Вы можете использовать это для создания навыков деревьев, систем использования предметов, механики взаимодействия - что -нибудь, где «Могу я это сделать?» Должен быть оценен, прежде чем «сделать это».


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE