As described in Game-Loop RFC, action simulation will be realized using a quantum approach: Simulated time will be incremented globally by steps. All action occuring inside this quantum will be simulated during the same global simulation pass.
Note that there is no hard correspondance between game time and real time. Under heavy load, simulated time will slow down. Under such conditions, Server response time should stay globally constant, but simulated quanta length will become smaller.
Quantum length need only to be fixed at the beginning of the simulation pass. This mean quantum length can be dynamicly determined according to game load by the server.
The purpose of this section is to describe how an Entity controller communicates its will to the game.
Each entity controller will have to maintain a list of the actions the entity is trying to do in the immediate future. This 'TODO-List' will be called here a Decision Tree (DT).
The Decision Tree is part of the Entity model. It will be accessible to both the action simulation code and the entity controller with different level of priviledges. In other words, the object will present two different interfaces, one for the controller (limited access) and one to the interaction engine (full access).
The controller will access through the DT:
Current Action during last turn, and progress indicator in the case of a long, multiple turn action.
Ordered list of already planned actions by the controller.
Ordered list of failed/impossible actions during last turn.
The action simulation engine will at each turn:
Evaluate planned actions for execution through the use of predicates. Accepted actions are started. Process goes on until action simulation time meet TURN_DURATION or all planned actions are processed.
Update the current action reference in the DT. If all planned actions are finished (failed or successful), then there is no current action. If an action is not finished at the end of the turn, it becomes the current action, and its progress attribute should be available to the controller. If an action finish exactly at the end of the turn and there is a next one, which is accepted by the predicates, then the latter becomes the new current action, with a null progress attribute.
The list of failed actions is updated as well. This is needed because the controller needs to differentiate between actions that could not be performed due to time constraints, actions that appeared to be impossible when the entity tried to perform them, and actions that were started and just failed without any clear stimuli being emitted.
The design that we came up with makes use of the following design patterns. Next, we motivate this choice.
Command
Dynamic Linkage
Object Pool
Actions are implemented using the Command pattern. This will insure:
modularity since every Action implements the same "Action" interface (proposed name, not necessarily definitive),
extensibility since new Actions can be created by implementing the defined interface or extending an already-created Action and overriding the methods required by the Action interface,
flexibility since for a given Action, a better implementation can be created by having to solely re-write the implementation of the methods defined by the Action interface without having to modify the rest of the game engine. We can therefore provide incremental functionality very easily.
The Command pattern already gives us much of the required flexibility. However, we also need the Dynamic Linkage pattern to be able to add an Action to an Action Pool at runtime or even dynamically modify an Action implementation. The Dynamic Linkage pattern allows us to dynamically load Actions without requiring that the game engine or Actions know each other. We will however need to refine this pattern a little bit to adapt it to our needs. Moreover, by using a special Class loader, we will be able to load Actions from a local or remote repository. For more information on the Dynamic Linkage pattern, see "Patterns in Java, volume 1" by Mark Grand.
We still have to address memory and interactivity requirements. We need to be able to share Actions between a large number of Entities in a memory- and speed-efficient way. A possible solution is to use the Object Pool pattern (as described by Mark Grand, see above). An Object Pool manages reusable instances. In our case, Action Pools will be implemented as Object Pools. How does it work and why is it efficient? Not too complex:
Object Pools act as containers of reusable instances. Specific methods allow to get and release an instance from and to the pool.
Object Pools can be shared by several clients (here Entities). The only requirement is to acquire a reference to a given Object Pool to use it. In our case, Entities will be initialized with a list of all Action Pools to which it belongs for each Influence.
Retrieving an Action, once the appropriate Action Pool has been located, will be call a get-type method. However, this method will ask the pool if an Action object is available to be reused. If yes, no need to create a new one and it is returned directly to the Entity that asked for it. If no, a new Action is created and returned to the Entity. This new Action is then added to the pool.
Several options are available concerning the management of reusable instances. We can define a max number of Action of a given type for each Action Pool to reduce memory requirements and initialize the pool with a given number of instances (possibly the max, in which case no instance is likely to be created). This system is very flexible and accommodate for memory and speed needs. It is possible to augment the needed memory to accelerate the process by pooling a greater number of instances to avoid the overhead of instantiation or limit the number of pre-instantiated objects to reduce memory footprint.
Using pools of reusable instances can be a huge performance factor by avoiding instantiation overhead most of the time but also by preventing garbage collection to happen until needed. Moreover, we don't need to create an Action instance per Entity which also reduces the memory footprint.
It could be also possible to use the Cache Management pattern (see Grand), to decrease lookup and increase speed performance but this will increase memory requirements.
This system seems to answer our needs rather efficiently. However, we will need to experiment to find if it keeps it promises when implemented.
Once the pertinent Action object has been found, control is transferred to it by the game through the perform command. This function is the base of the command pattern. As parameters, it takes:
Entity[] invokers: a set of subject entities, trying to perform the action.
Entity[] receivers: a set of target entities, object of the action.
Object[] parameters: a set of extra parameters, which completely depends on the Action. They are provided by the controller. For each action it knows, an Entity controller will know the type of its required parameters. If an action receive bad parameters, perform will throw an ActionBadParameterException.
Concrete action simulation will be done by:
Impacting state of various entities in the game.
Sending stimuli (directly or indirectly while impacting entities).
Sending events.
DT Action objects provided by Instances (and then Java compiled) will be able to use the following interfaces:
Read Access to the Properties, the Capabilities, the Skills of any Entities. Write access when meaningful.
Full Access to the Decision Tree of any entity.
Full Access to the Stimuli Dispatching system.
Full Access to the Event Dispatching system (if applicable).
Some actions will require coordination between two or more entities. For example Buying something requires that the shopkeeper is able to sell its products and not busy doing something else. Joining a group requires that the existing group is offering the newcomer to join.
In such cases, two actions will work in pair. The "Buy" action will only succeed if the shopkeeper is performing the "Sell" action. The Buy action will simply look at the current action of the Shopkeeper entity. If it finds a "Sell" Action there, It will contact this action, using a second method: Object [] interact(Object []).
For example, the interaction between the buyers and sellers could looks like:
Buy -> Sell.interact("BuyProposal","WoodArmor", 12 credits);
Buy <- Sell ("BuyResponse","OK", "WoodArmor", 12 credits);
The Buy actions then does the transaction, effectively removing the Wood armor from the shop inventory to the traveller's one. Money is transferred to the shop.
This proposal is just a beginning to define real interactions between entities. More use cases need to be reviewed, like:
Flying gnome falling on the ground. Will he get killed?
Adventurer joining a group.
Simple tavern fight.
Carrying someone on your shoulder. (up and down).
Two magicians casting a spell together, effectively joining their power.
Two character trying to pick up an object at approximatively the same time.
Another difficult point to design in which case an action being performed will be interrupted. We can do the following remarks:
At the end of each turn, the controllers are given the opportunity to update their Decision Tree. In the case the entity is performing a long action, the controller is free to cancel it. The cancel decision will go through the predicate system to check if it's acceptable. Some action are beyond the will of the controller: a falling gnome can not decide to stop falling. A dying character can not decide to stop his agony...
An aggressive action from another entity can interrupt the current action. (This is exactly the definition of an aggressive action). The target entity will be only able to react at the next turn.
The subject entity can enter or quit an influence. If it impacts the current action, most of the time, this means that the action will become impossible and will be interrupted. This is exactly the use case of the flying gnome. When the gnome leaves the spell influence, it loose the ability to fly and as a consequence, the action "Fly". The remove process check wheter the current action is affected. In this case, it is. The "Fly" action is interrupted, and a "Fall" action is scheduled for the next turn.