I'm working on a small game template, with a world comprised of nodes like so:
World
|--Zone
|----Cell
|------Actor
|------Actor
|--------Item
Where a World
can contain multiple Zone
objects, a Zone
can contain multiple Cell
objects, and so on.
Each of these implements the Node
interface, which has a few methods like getParent
, getChildren
, update
, reset
and so on.
I want to be able to execute a given Task
on a single node or recursively down the tree from a node (as specified by the Task
).
To compound this issue, I would like this to be a "pluggable" system, meaning I want players/developers to be able to add new types to the tree on the fly. I had also considered casting from the base types:
public void doTask(Actor node)
{
if(!(node instanceof Goblin)) { return; }
Goblin goblin = (Goblin) node;
}
Initially I was drawn to use the Visitor Pattern to take advantage of double dispatch, allowing each routine (Visitor) to act according to the type of Node
being visited. However, this caused a few complications, specifically when I want to add a new Node
type to the tree.
As an alternative, I wrote a utility class that uses reflection to find the most specific method applicable to the Node
.
My concern now is performance; since there will be a fairly large number of reflective lookups and calls, I'm worried that the performance of my game (which could have hundreds or thousands of these calls per second) will suffer.
Which seems to solve the problem of both patterns, but makes the code for each new Task
uglier.
The way I see it, I have three options for allowing this dynamic dispatch (unless I'm missing something obvious/obscure, which is why I'm here):
- Visitor Pattern
- Pros
- Double Dispatch
- Performance
- Clean Code in tasks
- Cons
- Difficult to add new
Node
types (impossible without modifying original code) - Ugly code during invocation of tasks
- Difficult to add new
- Pros
- Dynamic Invocation using Reflection
- Pros
- Can add new
Node
types with abandon - Very customizable tasks
- Clean Code in tasks
- Can add new
- Cons
- Poor performance
- Ugly code during invocation of tasks
- Pros
- Casting
- Pros
- More performant than reflection
- Potentially more dynamic than Visitor
- Clean code during invocation of tasks
- Cons
- Code smell
- Less performant than Visitor (no double dispatch, casting in each invocation)
- Ugly code in tasks
- Pros
Have I missed something obvious here? I'm familiar with many of the Gang of Four patterns, as well as the ones in Game Programming Patterns. Any help would be appreciated here.
To be clear, I'm not asking which of these is the "best". I'm looking for an alternative to these approaches.
Task
? I hadn't thought of that, but I don't really see how it applies. – Emylenode.getTasks().forEach(t - > t.doTask(node))
but at runtime the type of the Node isn't known, which is why thedoTask
method to be called is chosen reflectively with the node's specific type. TheTask
interface can accept multiple types, but if a developer using the library adds a new Node type it won't be a part of the interface – EmyleItem
is really aWorld
?). If you want this structure to be truly runtime dynamic, you are going to be forced to decouple your data model (the nodes) from the behavior (the tasks). Once the data and behavior is decoupled, you are essentially message passing (which is why I suggested akka). – Perfumery