Action filtering
Complexities involved
-
Parent intervention. If I attack someone locked into a chest,
the attack should (in most cases) be cancelled by the chest. And
the TempleOfEternalPeace should reduce damage of all attacks to 1...
-
Contradicting filters. I may be using an AlwaysHitSword and
you may be using an AlwaysBlockShield. What happens?
-
Unexpected filters. Very unexpected items can affect my combat
(such as the magic anti-battle song being sung by the bard, the slippery
floor, the magic OrcThrasherAmulet worn around my neck, and a WimpCurse
cast on me last week that makes my attacks very weak).
-
Performance. We don't want to have to recursively ask each child
for a filter for each action, since there can be very many children.
We also want to avoid lots of unnecessary filter-registering every time
you take a step in the game (since activities occur quite seldom compared
to how often you take step in a direction...)
A design that may work
We use an ActionFilter interface with these methods:
public Action filterAction(Action action);
public int getPriority(); //or something like that
The filter can do lots of things: modify the action, change the action
to a completely new one, cancel the action, or even queue new actions (kind
of like "reactions"...).
ActionFilters can be "combined". In practice this will probably
mean they are connected in priority order. To simplify this I'll
use an ActionFilterCollection (implements ActionFilter) with these
methods:
public void addFilter(ActionFilter filter);
public void removeFilter(ActionFilter filter);
public Action filterAction(Action action);
public int getPriority();
So if you stick lots of ActionFilters into an ActionFilterCollection,
you can treat the ActionFilterCollection as a single ActionFilter (while
it internally mainains a sorted list of ActionFilters). When filterAction
is called, the ActionFilterCollection will allow all it's ActionFilters
to filter the action in priority order. This algorithm can be improved,
it doesn't really matter how it works. As long as it in one way or
another executes multiple filtering.
Each item has an ActionFilterCollection object which any other item
can add or remove filters from.
Note: the difference between an ActionFilter and an EventFilter is that
an ActionFilter may modify what actually happens - EventFilter modifies
only the information about what happened. Let's say Joe is talking
to Fred. If Joe is drunk, an ActionFilter would modify his TalkActions
to make him less understandable. If instead Fred has bad hearing,
an EventFilter would modify what Fred hears.
But this document is only about ActionFilters.
A concrete example
Combat is by definition complex since it involves more than one participant,
but here's a reasonably simple scenario. Joe strikes the Orc with
his sword, and the Orc tries to dodge but fails. The Orc's armour
protectes him a bit, so he only gets 2 damage. How is this done?
-
Joe will queue an AttackEvent (implements Action, remember).
The AttackEvent includes all information necessary about the attack - which
weapon is used, which technique is used, and all that. When the attack
comes out of the queue it is time for filtering.
-
The AttackEvent will round up all the "participants" (see below) and ask
for their ActionFilterCollection. All these will be stuck together
into a final ActionFilterCollection (the combination of all participants'
filters). In this example, this collection is empty since no participants
want to filter the result.
-
The AttackEvent will be filtered through this collection, in this case
the original action will be unmodified. So it is now verified that
Joe is attacking the orc. It is now time to figure out the result
of the action.
-
The AttackEvent wants to give the target a chance to defend itself.
So it retrieves the Orc's CombatStrategy object (or something like that)
and asks it to produce a DefenseEvent (given the AttackEvent), which
turns out to be a DodgeEvent int this case (extends DefenseEvent).
The DodgeEvent is asked to filter itself, which means that:
-
The DefenseEvent will go through steps 2-3 above. If the orc
was tied down the DodgeEvent might have been cancelled or heavily modifed,
but in this example it goes through unaffected. So the orc is allowed
to dodge unhindered. Now what, will he succeed?
-
The AttackEvent is still controlling the flow here. It now has all
the information necessary to figure out the preliminary outcome of the
attack, or the AttackResultEvent. It uses whatever formulas
and algorithms to generate an AttackResultEvent, which will contain the
AttackEvent, the DefenseEvent, and information about what happened.
In this case the attack will succeed, and 3hp damage will be inflicted
(or whatever). But wait, it's not done yet. We have now figured
out what should happen - but the AttackResultEvent needs to be (guess
what) filtered!
-
But haven't we already filtered enough? Why filter again? Well
we've filtered the actions involved in the attack - not the actual result
of the attack. The Orc might be wearing armour. Or if Joe has
the OrcThrasherSword the damage might be increased, even though it didn't
affect the chance of hitting. In most cases the participants will
be the same, so the AttackResultEvent will probably just ask the AttackEvent
and the DefenseEvent for their filters (which they already gathered and
combined), combine them, and use the resulting ActionFilterCollection to
filter the ActionResultEvent. Get it? Joe's filters are used
to filter the AttackEvent and the AttackResultEvent. The Orc's filters
are used to filter the DefenseEvent and the AttackResultEvent.
-
The armour of the orc decreases the damage from 3hp to 2hp during this
filtering process. It may get slightly damaged itself in the process.
-
Now we actually execute the ActionResultEvent. It reduces the Orc's
hp by 2. The ActionResultEvent is propagated around for all
observers to see. A passive observer might see the message "Joe slashes
at the the Orc who tries to dodge. The dodge fails and the Orc shrieks
in pain as Joe cuts deep into his armour.". Or it might simply look
like "Joe attacks Orc. Orc tries to dodge. Joe damages Orc".
The last way is simple - the ActionResultEvent simple describes the ActionEvent,
the DefenseEvent, and the ActionResultEvent (in that order).
Who are the "participants"?
This is hard-coded, but will vary for different types of actions.
In an AttackEvent the participants might be the attacker, the weapon used,
and the terrain he is standing in. In a DefenseEvent it is the defender,
the weapon or shield used (if any) and the terrain he is standing in.
In the AttackResultEvent it will be all of the above participants.
It is not really that important (as you will see below)
How do other objects ("non-participants") affect the actions?
Each Physical has an ActionFilterCollection instance variable. Anyone
can add and remove ActionFilters from this collection. So let's take
armour for example. When the Orc wears his armour, the armour will
add a "reduce damage" filter to the Orc's filter collection. The
filter will reduce damage from certain types of attacks, and possibly damage
the armour in the process. If the armour is taken off, it will automatically
remove the filter from the Orc (see the diagram further down).
What's the difference between participants and non-participants?
A non-participant can be seen as an "indirect participant" in that it can
affect the actions if it wants. Well if both participants and non-participants
can affect actions, what is the difference?
Not much, in fact. That's why it's not such a big deal who is
a participant and who isn't. The only difference is that non-participants
have to add filters to participants, while participants only have to add
filters to their own collection. Let's say Joe has the OrcThrasherSword
and attacks the Orc.
Here is a container-hierarchy style diagram.
The OrcTrasherSword is a participant, which means it will be asked
for an ActionFilter directly - so it doesn't need to put any filters in
Joe's collection. The armour worn by the Orc, however, is not a direct
participant. So it needs to explicitely add a filter to the Orc,
since the armour itself will never be asked.
The disadvantage of having to add a filter to someone else (like the
armour does) is that the filter will be used for completely different,
unrelated actions as well. If the Orc goes fishing, the "decrease
damage to myself" filter will be used (although it won't do anything).
So we might later decide that armour is almost always used in battle, and
therefore include it as a direct participant to avoid using armour-related
filters unnecessarily.
Does this design solve the complexities mentioned?
I hope so.
-
Parent intervention. This is solved if we implement
Physical.getActionFilter() in such a way that it includes both it's own
filter and that of it's parent. So let's take the temple of eternal
peace. It has a filter that decreases damage to 1. If Joe attacks
the Orc in this temple, the temple's filter will be included, since when
Joe is asked for his filters, he will append the temple's filters as well.
This is recursive, all the way to the top of the container hierarchy, although
we can code in limits for performance reaonss.. For example an Area
might not care about the filters of it's parents (so any subareas within
the temple will not automatically include the peace-keeping effect - it
has to be added explicitely).
-
Contradicting filters. This is dealt with solely inside the
ActionFilterCollection. In the simplest case, it will use the filters
in a certain order which means whichever one happens to be used last "wins".
Not so good. But the problem has to be solved in code one way or
another, and this design allows us to choose the level of complexity within
a single class, so we can improve the contradiction handling without having
to change the design.
-
Unexpected filters. This is dealt with using Physical.addActionFilter(ActionFilter
f) in which case the Physical will add the given filter to it's own collection.
Any object can add filters to any other objects, so this problem is solved.
-
Performance. The children of an object are not asked for filters
- only the parents, and there will rarely be more than 2 parents if you
stop at the Area. No filter registration is done while walking around.
This means even if every single square will affect combat differently (due
to different terrain types), nothing extra will happen as you walk around
- the square is your parent so it will get a chance to filter when you
actual start fighting.
Q & A:
Isn't it ineffective to recursively ask all parents whenever
asked for a filter? |
It can limited to whatever level we wish (depending on how advanced
we want the game to be). The Area-limit is probably a good compromise.
The good thing about it is that it is simple and not so error-prone.
Imagine the temple of internal peace. It contains a bag. If
a pixie from the astral plane suddenly warps directly into the bag, the
temple may not notice. In my design, the pixie cannot sneak past
the peace-keeping effect. |
When will I receive actionCompleted? For which actions? |
You receive actionCompleted only for eact actions that you have initiated.
In the example above this means only one actionCompleted will happen.
Joe will receive actionCompleted for his attackEvent. The Orc's DefenseEvent
and the final AttackResultEvent were automatic results of the intial AttackEvent,
so noone receives actionCompleted.
Put more simply, you only receive actionCompleted for actions that you
have actually queued. In the above example the actionCompleted will
be called after the ActionResultEvent has been propagated around. |
What is an actual item designer required to do when making a cool item |
Place ActionFilters in the right places, at the right times. |
How do the ActionFilterCollection and the Effects inside a Physical
relate to each other? |
ActionFilters respond to Actions and never find out about events that
have already occurred. ActionFilters are not in the container hierarchy.
Effects are in the container hierarchy and can receive events from their
parent. If an Effect wants to filter actions as well, it can implement
ActionFilter and add itself to the ActionFilterCollection as well.
Another possibility is to change so that Effect is not a member
of the container hierarchy, and is added directly to the ActionFilterCollection
(ála Julio?). This may be simpler and more consistent.
An effect is an effect is an effect, regardless of whether it modifies
the actions or listens to the events. |
What about rules? |
Many ActionFilters will be Rules that implement the ActionFilter interface.
ActionFilter is what makes the rule system fit into the core. |
Whew, that was long...
Henrik
Kniberg
Last updated: