我正在开发一个小型游戏模板,其中包含如下节点的世界:
World
|--Zone
|----Cell
|------Actor
|------Actor
|--------Item
World
可以包含多个Zone
个对象,Zone
可以包含多个Cell
个对象,依此类推。
其中每个都实现了Node
界面,其中包含一些方法,如getParent
,getChildren
,update
,reset
等等。
我希望能够在单个节点上执行给定的Task
,或者从节点递归地执行树(由Task
指定)。
为了解决这个问题,我希望这是一个“可插拔”系统,这意味着我希望玩家/开发人员能够动态地向树中添加新类型。我还考虑过基类型的转换:
public void doTask(Actor node)
{
if(!(node instanceof Goblin)) { return; }
Goblin goblin = (Goblin) node;
}
最初我被吸引使用Visitor Pattern来利用双重调度,允许每个例程(访问者)根据被访问的Node
的类型行事。但是,这会导致一些复杂情况,特别是当我想向树中添加新的Node
类型时。
作为替代方案,我写了一个utility class,它使用反射来查找适用于Node
的最具体方法。
My concern now is performance;因为会有相当多的反思性查询和调用,我担心我的游戏性能(每秒可能有数百或数千次这样的调用)会受到影响。
这似乎解决了这两种模式的问题,但却使每个新Task
的代码更加丑陋。
我看到它的方式,我有三个选项允许这种动态调度(除非我遗漏了一些明显/模糊的东西,这就是为什么我在这里):
Node
类型(无法修改原始代码)Node
类型并放弃我错过了一些明显的东西吗?我熟悉许多四人组模式,以及Game Programming Patterns中的模式。这里有任何帮助。
要明确的是,我不是在问这些是“最好的”。我正在寻找这些方法的替代方案。
答案 0 :(得分:3)
因此,在对Java 8 Lambdas进行一些研究以及如何反思构造之后,我提出了从我已经反射性地获得的BiConsumer
对象创建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;
}
创建此BiConsumer
后,使用参数类型和方法名称作为键将其缓存在HashMap
中。然后可以像这样调用它:
consumer.accept(nodeTask, node);
这种调用方法几乎完全消除了反射引起的调用开销,但确实存在一些问题/约束:
BiConsumer
,因此只能将一个参数传递给方法(accept
方法的第一个参数必须是应该在其上调用方法的实例)。
HashMap
查找的常量。)我可以通过使用自定义功能接口(类似于Invoker
类而不是Java的BiConsumer
)来澄清这段代码,但是现在它完全符合我想要的性能I想。
答案 1 :(得分:1)
我认为如果你不能拥有静态工厂类,那么这是一个棘手的问题。如果允许静态工厂,那么这个简短的例子可能会提供一些想法。
这种方法允许将INode实例的运行时插入到树(WorldNode)中,但是,它并没有回答如何创建这些具体的INode。我希望你会有某种工厂模式。
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");
}
}
输出:
World Task
Zone Task
Goblin Task
Goblin Task
Goblin Task
Goblin Task
Zone Task
Zone Task
Zone Task
答案 2 :(得分:1)
反思的想法很好 - 你只需要根据参数类型缓存查找结果。
访问者模式可以通过用户程序扩展。例如,给定访问者模式中的经典Node
和Visitor
定义,用户可以定义
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
一般来说,我不喜欢访客模式;它是相当丑陋,僵硬和侵入性的。
基本上,问题在于给定节点类型和任务类型,查找处理程序。我们可以通过(node,task)->handler
的简单地图来解决这个问题。我们可以为绑定/查找处理程序创建一些API
register(NodeX.class, TaskY.class, (x,y)->
{
...
});
或匿名类
new Handler<NodeX, TaskY>() // the constructor registers `this`
{
@Override public void handle(NodeX x, TaskY y)
...
要在节点上调用任务,
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)
答案 3 :(得分:0)
如果您正在寻找表现 - 访客模式是可行的方式。我个人甚至不会想出反射作为解决方案,因为它似乎不真实和过于复杂。转换工作,但在OO环境中,它通常被认为是代码气味,应该避免(例如使用访问者模式)。
另一个重要方面是可读性 vs 可写性:访问者模式可能需要更多工作,并且在添加节点时需要更多维护,但它绝对更容易阅读和通常也更容易理解。反思在两个方面都是禁忌,而演员也是代码嗅觉。