О проектировании гибкой системы способностей персонажей в играх
Система способностей персонажа пожалуй самая требовательная к гибкости в игре. Невозможно на этапе проектирования предсказать какие заклинания появятся в финальной версии или последующих обновлениях. Этот пост будет о том, как я абстрагировал процесс исполнения способностей.
Сама по себе способность есть ни что иное, как набор действий. Минимальный интерфейс способности состоит из одного метода: «применить», но не всё так просто и о сложностях под катом.
Всякая способность начинается с серии проверок возможно ли ее применить. Среди них обычные такие, как проверка перезарядки, наличия маны, проверка расстояния и прочие. Уже тут видно, что не все проверки нужны всем способностям. Например, существуют способности, применяемые на любом расстоянии. То есть разные способности требуют разные наборы проверок перед исполнением. Однако видно, что многие из проверок будут повторяться, а зачастую для многих способностей требуется один и тот же набор проверок.
Итого части проверок будут логически повторяться, а значит должны изменяться согласовано, то есть сразу во всех местах. При этом наборы частей проверок в общем случае будут различны.
Если части проверок выделить в отдельные объекты, реализующие один интерфейс и выстроить в односвязный список, то получится шаблон цепочка обязанностей.
В случае успешной проверки в звене, запустится проверка в следующем звене, если следующего звена нет, то всю проверку можно считать успешной. Кроме самой проверки, звено может содержать также и обработчик ошибки. Например, если при проверке наличия маны, оказалось, что ее недостаточно, то звено может оповестить игрока об этом.
Используя цепочку обязанностей, для способности [Мощный выстрел] мы без труда можем вставить дополнительное звено, проверяющее одет ли на персонажа лук или звено, проверяющее, что персонаж имеет уровень здоровья ниже 30%, для способности [Второе дыхание].
Откатимся назад и вспомним, что существуют цепочки проверок, одинаковые для многих способностей. Давайте выделим сущность запроса выполнения способности и каждую из видов цепочек проверок опишем своим классом.
От запроса требуется только составить цепочку обязанностей, запустить ее и отменить, когда игрок даст соответствующую команду.
Составлять цепочки будем уже в реализациях запросов.
На текущий момент мы уже научились составлять гибкие проверки возможности исполнения способностей. Теперь надо в случае удачной проверки всё же выполнить способность.
Я предпочел это сделать не меняя интерфейсов, добавив последнее всегда успешное звено, которое побочным эффектом выполняет способность. Вот его примерная реализация:
public class TerminalChecker: ICastChecker {
CastChecker next { get; set; }
ISkill skill;
public TerminalChecker(ISkill skill) {
this.skill = skill;
}
public bool check() {
skill.cast();
return true;
}
}
Такая реализация позволяет нам делать запросы ассинхронными. Это полезно, когда нам нужна дополнительная информация от пользователя. Например, способность должна применяться на некоторую область, которую выберет игрок с помощью мыши. Игру в это время останавливать, конечно, нельзя.
Теперь нам нужно сопоставить запросы со способностями. Сделаем это, конечно, с помощью полиморфизма, добавив свойство в интерфейс способности. На данном этапе мы расширили способность до такого интерфейса:
После всей проделанной работы, давайте подумаем что такое способность. В текущей реализации это набор действий, которому предшествует ряд проверок. Заметьте, что на высоком уровне мы никак не зависим от конкретной игровой логики. При изначальной задумке описать систему способностей применительно к заклинаниям, мы получили систему, которая по определенным правилам дает или не дает нам совершать произвольные действия.
Благодаря этому свойству данной системой можно описать любую модификацию игрового мира. Например, торговую сделки или команду строительства здания.
Давайте еще раз взглянем на всё в целом
На данном примере способность Sprint является обычной способностью без цели, класс, реализующий запрос для таких способностей это NontargetCastRequest, который в свою очередь составляет цепочку проверок из ManaChecker, CooldownChecker и TerminalChecker.
Вызывающий код не зависит от деталей реализации этой системы, то есть мы не поломаем игровую логику, добавив или изменив способность.
Это и есть система способностей персонажа в минимальном виде. В этой моделе не хватает средств оповещения вызывающего кода, передачи способностей в пользовательский интерфейс и прочих мелочей жизни. О них можете подумать самостоятельно.