Какой шаблон применить для автомата прохождения квестов с большим числом шагов?

Рейтинг: 2Ответов: 1Опубликовано: 03.05.2015

Есть скрипт для бота, для прохождения квестов в одной игре ММО игре мною созданный и спроектированный.

Проблема в том, что он мне не нравится, так как я применил анти-паттерн (сам того тогда не осознавая, дело было давненько) Божественный Объект (God Object).

Как работает скрипт

Когда я его писал, я думал о конечном автомате, который переходит из одного состояния после выполнения очередного шага. Если шаг, скажем, 87 выполнится успешно, то автомат перейдёт к следующему шагу 88. Если шаг квеста 87 сфейлится (например, на бота нападёт другой игрок и помешает поговорить с NPC), то шаг повторится и автомат снова перейдет в состояние 87. В каждом состоянии возможна обработка ошибок и команда JumpStep, благодаря которой можно перейти из состояния 87 в 65 или любое другое (для обработки случая смерти в середине квеста).

Проблема: как это всё реализовать в виде кода?

Создавать массив объектов и функций или лепить гигантский case of (aka switch в С++) мне очень не хотелось, потому что это все кушает оперативку + когда придется добавлять новый метод, придётся редактировать этот array/case. Я решил проблему используя RTTI фишку паскаля, благодаря которой можно вызвать метод объекта по его имени. Получился класс с кучей шагов Step1 Step2 ... Step120. Внутри автомата храню номер шага и когда надо перейти к следующему шагу, просто вызываю кодом вида: call_method ('Step'+intToStr(Step_Num));

В реальности код выглядит вот так (S - это Step):

procedure TSE.S76;
  begin
      StepRes:=Go('Шнаин') and (Q([NPC_Shnain,DLG_Ischeznuvshiy_Sakum__v_protsesse_,1,1]) or WaitQS(Qu20,-1));
  end;
  {----------------------}
  const Qu21=10334;
  procedure TSE.S77;
  begin
      StepRes:=Go('Шнаин') and (Q([NPC_Shnain,DLG_Osmotr_kholma_Vetryanyykh_Melynits,1,1]) or WaitQS(Qu21,1));
  end;
  {----------------------}
  procedure TSE.S78;
  begin
    StepRes:=true;
    if PosOutRange('Gludio',5000) then
      StepRes:=Escape(TravelSoe);
    StepRes:=StepRes and Go('Батис') and (Q([NPC_Batis, DLG_Osmotr_kholma_Vetryanyykh_Melynits__v_protsesse_,1,1]) or WaitQS(Qu21,-1));
    if stepRes then
      prs['DqusetWeapon']:=QEvents.LastItemAdd;
  end;

Решение хорошо в том плане, что при добавлении новых шагов не надо редактировать всякие кейсы и массивы. Просто пишешь новый метод Step121 и всё.

Но такое решение мне не нравится по многим причинам:

  1. Все методы находятся в оперативной памяти даже когда они не нужны. Да, windows хитро устроена в плане выгрузки\подгрузки исполняемых страниц кода с диска по мере надобности, но всё-таки полагаться на OS в таком вопросе не хотелось бы.

  2. Применен антипатерн "божественый объект", в результате весь код в одном объекте, а хотелось бы найти хороший паттерн для такого случая который эллегатно решал все мои проблемы. Не может быть, чтобы его не было, есть же игры типа GTA - там же мощные заскриптованые сцены, где с NPC все что угодно может пойти не так (типа другу главного героя перегородит путь машина и ему придётся её обходить или еще что-нить непредвиденное) и должна быть обработка таких случаев.

Ответы

▲ 2

Мне кажется, к вашему случаю идеально подходит State Machine — тот самый конечный автомат, который вы реализуете.

  1. Определите по классу на каждый экран.
  2. Напишите общий интерфейс IStateRunner с методом Run: boolean. Пусть все классы из пункта 1 имплементируют этот интерфейс.
  3. Теперь определите само дерево состояний примерно такого вида: type Node = class IStateRunner^ runner; Node^ transitionOnSuccess; Node^ transitionOnFailure; end;
  4. Постройте само дерево (его элементы).

Теперь ваш управляющий код будет прост:

Node^ state := initialState;
while (state <> finalState)
begin
    result = state^.runner^.Run();
    if (result) then
        state := state^.transitionOnSuccess;
    else
        state := state^.transitionOnFailure;
end

(я давненько не писал на паскале, может быть, ошибся с синтаксисом).

Преимущества:

  1. Код каждого уровня отделён от других уровней.
  2. Для перехода и общей логики верхнего уровня у вас отдельный, простой код.

Недостатки:

  1. Возможно, потеряна гибкость (где-то есть более одного transition'а при успехе? переход зависит от истории?) Для неё придётся писать «виртуальные» состояния или применять ещё какие-нибудь трюки.
  2. Код, связывающий классы в дерево, может оказаться малообозрим и нескриптуем.