There is a bot script for completing quests in one MMO game, which I created and designed.

The problem is that I don’t like him, since I applied an anti-pattern (without realizing it then, it was a long time ago) a Divine Object ( God Object ).

How the script works

When I wrote it, I was thinking about the finite state machine, which goes from one state after performing the next step. If a step, say, 87 is completed successfully, then the machine will proceed to the next step 88. If the step of quest 87 fades (for example, another player attacks the bot and interferes with speaking with the NPC), the step will repeat and the machine will go back to state 87. In each condition is error handling and the JumpStep team, through which you can go from state 87 to 65 or any other (to handle a death in the middle of the quest).

Problem: how to implement all this in the form of code?

I really didn’t want to create an array of objects and functions or sculpt a giant case of (aka switch in C ++), because this all eats up the RAM + when I have to add a new method, I’ll have to edit this array / case. I solved the problem using RTTI chip pascal, through which you can call the method of the object by its name . The result was a class with a bunch of steps Step1 Step2 ... Step120. Inside the machine I keep the step number and when I have to go to the next step, I simply call with a code like: call_method ('Step'+intToStr(Step_Num));

In reality, the code looks like this (S is Step):

 procedure TSE.S76;
   begin
       StepRes: = Go ('Schnain') 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 ('Schnain') 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 ('Batis') 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;

The solution is good in the sense that when adding new steps, it is not necessary to edit any cases and arrays. Just write a new method Step121 and that's it.

But I do not like this decision for many reasons:

  1. All methods are in memory even when they are not needed. Yes, windows is cleverly arranged in terms of unloading / loading executable code pages from disk as needed, but I still would not want to rely on the OS.

  2. Anti-faber "divine object" was applied, as a result, all the code was in one object, but I would like to find a good pattern for such a case that would elelegatno solve all my problems. It can not be that it was not, there are games like GTA - there are powerful scripted scenes, where with NPC anything can go wrong (like a car’s friend will block the path and he will have to bypass it or something else unforeseen ) and should be handling such cases.

  • It is worth going through a profiler and / or a memory leak detector a couple of times and check if you really have problems with memory overruns. And as noted below, you spend days on saving kilobytes, where it is absolutely not critical. - Kromster

1 answer 1

It seems to me that the State Machine is ideally suited to your case - the very state machine that you implement.

  1. Determine the class for each screen.
  2. Write a generic IStateRunner interface with the Run: boolean method. Let all classes from item 1 implement this interface.
  3. Now determine the state tree itself of the following form: type Node = class IStateRunner^ runner; Node^ transitionOnSuccess; Node^ transitionOnFailure; end; type Node = class IStateRunner^ runner; Node^ transitionOnSuccess; Node^ transitionOnFailure; end;
  4. Build the tree itself (its elements).

Now your control code will be simple:

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

(I haven’t written in Pascal for a long time, maybe I was mistaken with the syntax).

Benefits:

  1. The code of each level is separated from other levels.
  2. For the transition and general logic of the upper level, you have a separate, simple code.

Disadvantages:

  1. Perhaps flexibility is lost (somewhere there is more than one transition for success? The transition depends on history?) For it, you will have to write “virtual” states or apply some more tricks.
  2. The code that binds classes to a tree may turn out to be low-browsing and non-scripting.
  • Yes, the state putter is not bad. However, I did not understand why to return from Boolean states when it is possible to directly return the next state. And you propose at the initialization stage to cost a tree. The tree will be big. The script will go from one state to another within 1-2 weeks of continuous work. I think this is a slightly wasteful disposal of memory. In addition, there are only 170 steps. This means that you will have to create 170 classes of CState1 .. CState170. As it is not very elegant. About virtual states did not understand at all what kind of animal is this? - mikserok
  • @mikserok: You can return the state directly. But at the same time, each state will have to know about each other, and this is somehow not very. - VladD
  • 2
    170 instances of the object? This is a very small tree. Do you feel sorry for two kilobytes of memory? - VladD
  • "Virtual state" - well, for example, a state that keeps inside pointers to several other states, and in the Run method, it starts one or the other according to its internal algorithm. - VladD
  • Classes correspond exactly to your assignment. If you have exactly 170 "rooms", then you need 170 separate classes that implement their logic. Another thing is that you can share code between classes using helper classes. - VladD