Система способностей персонажа пожалуй самая требовательная к гибкости в игре. Невозможно на этапе проектирования предсказать какие заклинания появятся в финальной версии или последующих обновлениях. Этот пост будет о том, как я абстрагировал процесс исполнения способностей.

Сама по себе способность есть ни что иное, как набор действий. Минимальный интерфейс способности состоит из одного метода: «применить», но не всё так просто и о сложностях под катом.

image

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

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

Если части проверок выделить в отдельные объекты, реализующие один интерфейс и выстроить в односвязный список, то получится шаблон цепочка обязанностей.

image

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

Используя цепочку обязанностей, для способности [Мощный выстрел] мы без труда можем вставить дополнительное звено, проверяющее одет ли на персонажа лук или звено, проверяющее, что персонаж имеет уровень здоровья ниже 30%, для способности [Второе дыхание].

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

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

image

Составлять цепочки будем уже в реализациях запросов.

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

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

public class TerminalChecker: ICastChecker {
    CastChecker next { get; set; } 
    ISkill skill;
    public TerminalChecker(ISkill skill) {
        this.skill = skill;
    }
    public bool check() { 
        skill.cast();
        return true; 
    }
}

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

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

image

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

Благодаря этому свойству данной системой можно описать любую модификацию игрового мира. Например, торговую сделки или команду строительства здания.

Давайте еще раз взглянем на всё в целом

image

На данном примере способность Sprint является обычной способностью без цели, класс, реализующий запрос для таких способностей это NontargetCastRequest, который в свою очередь составляет цепочку проверок из ManaChecker, CooldownChecker и TerminalChecker.

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

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

Комментарии (9)


  1. Leopotam
    05.06.2019 12:58

    А можно посмотреть в сторону ECS (не обязательно штатной реализации) и понять, что там это все реализуется даже проще.


    1. Brightori
      05.06.2019 19:01

      :) если вам нравится парадигма ECS это не означает что большинству она проще и удобней. Кстати я так понимаю есть ECS фреймворк леопотам? )


      1. Leopotam
        05.06.2019 19:10

        Есть много реализаций, в том числе и моя. Есть Entitas, Svelto, EgoEcs и т.п. Смысл в том, что в случае ecs размазывание обработки последовательно с пре/пост-процессингом получается просто и непринужденно, без использования ООП.


        1. Brightori
          05.06.2019 19:20

          Все реализации кроме нативной не дадут того прироста производительности, и по факту являются надстройкой над монобехом. А реализация скиллов всегда строится на системе предикатов, её не избежать как в ECS так и в других парадигмах, и нормальная система в целом атомарна и даёт возможность создавать предикаты геймдизайнерам. Просто описанную автором систему можно оспорить и покритиковать. И скажем так, более композитные решения будут более удачны, но это не аргумент в целом за использование ECS, и их можно также легко решить в ООП.


          1. Leopotam
            05.06.2019 19:26

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

            Очень смелое и абсолютно голословное утверждение. Мое поделие как и Entitas не имеет никаких зависимостей от юнити и может использоваться в .net core безо всяких монобехов, например, в серверной реализации или вообще сторонних проектах (например, мод кастомных ранений для gta5). И да, джобы делаются через штатный мультитред без проблем.


            1. Brightori
              05.06.2019 19:29

              Там наверху тэг Unity. Соответственно речь об этом. Как фреймворк работает вне Unity — отдельная песня. Теперь зайдем с другой стороны — можете ли вы утверждать что ваш фреймворк быстрее нативного ECS и даст результат лучше чем DOTS в целом?


              1. Leopotam
                05.06.2019 19:36

                Для начала можно пройтись по ссылкам, там есть тесты производительности. Ну и можно зайти с другой стороны — когда unity-ecs позволит передавать без адского бойлерплейта, занимающего треть кода, инстансы классов дальше для работы (после джобов и т.п) — вот тогда можно будет рассматривать ихнее поделие как рабочий вариант. Сейчас это — исключительно one-task-performance-only решение для ускорения одной задачи, из-за которой потом приходится страдать и выковыривать данные обратно из NativeArray и т.п. Все коммунити-проекты (тот же Entitas) являются multipurpose-решениями и позволяют строить всю архитектуру приложения на них.


  1. Ualde
    06.06.2019 09:54

    Зашел из-за картинки, какая ностальгия.
    Спасибо)


  1. tangro
    06.06.2019 14:19

    Я помню читал статью как для подобных вещей применялся паттерн Chain of Responsibilities. Там всё было просто: клепается куча блочков типа «броня», «физический урон», «магический урон», «усилитель от химии», «бафы класса персонажа» и т.д. Каждый блок принимает на вход и отдаёт на выход какое-то число (урона, хила, скорости). При каждой попытке воздействия игрока на что-то строится цепочка из нужных блоков, на вход ей даётся число — на выходе получается другое число. Всё. Элементарно добавляется всё, что угодно: заклинания, бафы от территории, времени дня, текущего игрового момента и т.д.