
What is it and why?
When designing, a developer may encounter a problem: creatures and objects may have different abilities in different combinations. Frogs jump and swim, ducks swim and fly, but not with a weight, and frogs can fly with a branch and ducks. Therefore, it is convenient to move from inheritance to composition and add capabilities dynamically. The need to animate the flying frogs led to an unjustified abandonment of the ability methods and bringing their code to the teams in one of the implementations. Here she is:
class CastSpellCommand extends Command { constructor (source, target, spell) { this.source = source; this.target = target; this.spell = spell; } execute () { const spellAbility = this.source.getAbility(SpellCastAbility);
What can be done?
Consider several approaches of different nature:
Observer
class Executor extends Observer {} class Animator extends Observer {}
The classic, well-known solution for programmers. You only need to change it as little as possible to check the values returned by observers:
this.listeners.reduce((result, listener) => result && listener(action), true)
Disadvantage: Observers must subscribe to events in the correct order.
If you do error handling, the animator will also be able to show animations of failed actions. You can pass the previous value to the observers, conceptually the solution remains the same. Whether observer methods or callback functions are called, whether a normal cycle is used instead of convolution - the details are not so significant.
Leave as is
And in fact. The current approach has both disadvantages and advantages:
- Verifying the ability to execute a command requires the execution of a command
- Arguments in varying order, conditions, method prefixes are hard-wired.
- Cyclic dependencies (command <spell <command)
- Additional entities for each action (method replaced by method, class and its constructor)
- Excessive knowledge and actions of a separate team: from game mechanics to timing errors and direct manipulation of other people's properties
- The interface is misleading (execute not only causes, but also adds commands via addChildren; which obviously does the opposite)
- Dubious need and implementation of recursive commands as such
- The dispatcher class, if any, does not perform its functions.
- [+] Allegedly, the only way to animate in practice, if the animation needs complete data (indicated as the main reason)
- [+] Probably other reasons.
Some of the shortcomings can be dealt with separately, but the rest require more fundamental changes.
ad hoc
- The conditions for the execution of the team, especially the game mechanics, must be removed from the teams and drawn up separately. Conditions can change in runtime, and the selection of inactive buttons in gray is encountered in practice long before the work on the animation begins, not to mention the logic. To avoid copying, it may make sense to store general conditions in prototype abilities.
- Return methods, in combination with the previous item, there is no need for such checks:
const spellAbility = this.source.getAbility(SpellCastAbility);
The Javascript engine itself will show the correct TypeError when an error method is called. - The team also does not need such knowledge:
healthAbility.health = Math.max( 0, resultHealth );
- To solve the problem of arguments that change places, they can be passed to the object.
- Although the calling code is not available for study, it seems that most of the drawbacks are due to the non-optimal way of invoking game actions. For example, button handlers refer to some specific entities. Therefore, replacing them in the handlers with specific commands seems quite natural. If there is a dispatcher, it is much simpler to call animation after the action; you can transfer the same information to it, so there will be no lack of data.
Turn
To show the animation of the action after the execution of the action, it is enough to add them to the queue and run something like in solution 1.
[ [ walkRequirements, walkAction, walkAnimation ], [ castRequirements, castAction, castAnimation ],
It does not matter which entities lie in the array: functions booted with the necessary parameters, instances of user-defined classes or ordinary objects.
The value of such a decision is simplicity and transparency; it is easy to make a sliding window for viewing the last N commands.
Good for prototyping and debugging.
Doubler class
Making an animation class for ability.
class MovementAbility { walk (...args) {
If you cannot make changes to the calling class, inherit from it or decorate the desired method so that it invokes an animation. Or we transfer animation instead of ability, they have the same interface.
Well suited when you actually need the same set of methods, you can automatically check and test them.
Combinations of methods
const AnimatedMovementAbility = combinedClass(MovementAbility, { ['*:before'] (method, ...args) {
It would be an interesting opportunity with native language support.
It is good to use if this option turns out more productive, although a proxy is actually needed.
Proxy
Wrapping abilities in proxies, catching methods in the getter.
new Proxy(new MovementAbility, {})
Disadvantage: many times slower than normal calls, which is not so significant for animation. On a server processing millions of objects, a slowdown would be noticeable, but no animation is needed on the server.
Promise
You can construct chains from Promise, but there is another option (ES2018):
for await (const action of actionDispatcher.getActions()) {
getActions returns an asynchronous iterator over an action. The next iterator method returns the Deferred Promise of the next action. After processing the events from the user and the server, call resolve (), create a new promise.
Better team
Create objects like this:
{actor, ability, method, options}
The code comes down to checking and calling the ability method with parameters. The easiest and most productive option.
Note