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):
Node
types (impossible without modifying original code)Node
types with abandonHave 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.
So after some research into Java 8 Lambdas and how they can be constructed reflectively, I came up with the idea of creating a BiConsumer
from the Method
object I had obtained reflectively, with the first argument being the instance on which the method should be invoked and the second argument being the actual argument of the method:
private static <T, U> BiConsumer<T, U> createConsumer(Method method) throws Throwable {
BiConsumer<T, U> consumer = null;
final MethodHandles.Lookup caller = MethodHandles.lookup();
final MethodType biConsumerType = MethodType.methodType(BiConsumer.class);
final MethodHandle handle = caller.unreflect(method);
final MethodType type = handle.type();
CallSite callSite = LambdaMetafactory.metafactory(
caller,
"accept",
biConsumerType,
type.changeParameterType(0, Object.class).changeParameterType(1, Object.class),
handle,
type
);
MethodHandle factory = callSite.getTarget();
try {
//noinspection unchecked // This is manually checked with exception handling.
consumer = (BiConsumer<T,U>) factory.invoke();
}catch (ClassCastException e) {
LOGGER.log(Level.WARNING, "Unable to cast to BiConsumer<T,U>", e);
}
return consumer;
}
Once this BiConsumer
has been created, it is cached in a HashMap
using the parameter type and method name as a key. It can then be invoked like so:
consumer.accept(nodeTask, node);
This method of invocation almost entirely eliminates the invocation overhead incurred by reflection, but it does have a couple of issues/constraints:
BiConsumer
, only one parameter may be passed into the method (the first argument to the accept
method must be the instance that should have the method invoked on it).
HashMap
lookup).I could clarify this code a bit by using a custom functional interface (something like an Invoker
class instead of Java's BiConsumer
) but as of right now it does exactly what I want with the performance I want.
I think that if you cannot have a static factory class then it is a tough problem. If a static factory is allowed, then perhaps this short example might provide some ideas.
This sort of approach allows for run-time insertion of INode instances into the tree (WorldNode), however, it doesn't answer how these concrete INodes are created. I would hope you would have some kind of factory pattern.
import java.util.Vector;
public class World {
public static void main(String[] args) {
INode worldNode = new WorldNode();
INode zoneNode = new ZoneNode();
zoneNode.addNode(new GoblinNode());
zoneNode.addNode(new GoblinNode());
zoneNode.addNode(new GoblinNode());
zoneNode.addNode(new GoblinNode());
worldNode.addNode(zoneNode);
worldNode.addNode(new ZoneNode());
worldNode.addNode(new ZoneNode());
worldNode.addNode(new ZoneNode());
worldNode.runTasks(null);
}
}
interface INode {
public void addNode(INode node);
public void addTask(ITask node);
public Vector<ITask> getTasks();
public void runTasks(INode parent);
public Vector<INode> getNodes();
}
interface ITask {
public void execute();
}
abstract class Node implements INode {
private Vector<INode> nodes = new Vector<INode>();
private Vector<ITask> tasks = new Vector<ITask>();
public void addNode(INode node) {
nodes.add(node);
}
public void addTask(ITask task) {
tasks.add(task);
}
public Vector<ITask> getTasks() {
return tasks;
}
public Vector<INode> getNodes() {
return nodes;
}
public void runTasks(INode parent) {
for(ITask task : tasks) {
task.execute();
}
for(INode node : nodes){
node.runTasks(this);
}
}
}
class WorldNode extends Node {
public WorldNode() {
addTask(new WorldTask());
}
}
class WorldTask implements ITask {
@Override
public void execute() {
System.out.println("World Task");
}
}
class ZoneNode extends Node {
public ZoneNode() {
addTask(new ZoneTask());
}
}
class ZoneTask implements ITask {
@Override
public void execute() {
System.out.println("Zone Task");
}
}
class GoblinNode extends Node {
public GoblinNode() {
addTask(new GoblinTask());
}
}
class GoblinTask implements ITask {
@Override
public void execute() {
System.out.println("Goblin Task");
}
}
Output:
World Task
Zone Task
Goblin Task
Goblin Task
Goblin Task
Goblin Task
Zone Task
Zone Task
Zone Task
The reflection idea is fine - you'll just need to cache the lookup result based on argument types.
The visitor pattern can be expanded by user program. For example, given the classic Node
and Visitor
definitions in visitor pattern, user can define
MyNode, MyVisitor
interface MyVisitor extends Visitor
{
void visit(MyNode m);
void visit(MyNodeX x);
...
}
interface MyNode extends Node
{
@Override default void accept(Visitor visitor)
{
if(visitor instanceof MyVisitor)
acceptNew((MyVisitor) visitor);
else
acceptOld(visitor);
}
void acceptNew(MyVisitor visitor);
void acceptOld(Visitor visitor);
}
class MyNodeX implements MyNode
{
@Override public void acceptNew(MyVisitor visitor)
{
visitor.visit(this);
}
@Override public void acceptOld(Visitor visitor)
{
visitor.visit(this);
}
}
// problematic if MyNodeX extends NodeX; requires more thinking
In general, I don't like visitor pattern; it is quite ugly, rigid, and intrusive.
Basically, the problem is that given a node type and a task type, lookup a handler. We can solve this by a simple map of (node,task)->handler
. We can invent some APIs for bind/lookup handlers
register(NodeX.class, TaskY.class, (x,y)->
{
...
});
or with anonymous class
new Handler<NodeX, TaskY>() // the constructor registers `this`
{
@Override public void handle(NodeX x, TaskY y)
...
To invoke a task on a node,
invoke(node, task);
// lookup a handler based on (node.class, task.class)
// if not found, lookup a handler on supertype(s). cache it by (node.class, task.class)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With