复合模式/实体系统和传统 OOP

我正在开发一个用Java编写的小游戏(但问题是与语言无关的)。由于我想探索各种设计模式,因此我一直关注复合模式/实体系统(我最初在这里这里读到过),作为典型的深度分层继承的替代方案。

现在,在写了几千行代码之后,我有点困惑。我认为了解模式,我喜欢使用它。我认为它非常酷,像星巴克一样,但它感觉它提供的好处有些短暂,并且(最让我烦恼的)在很大程度上取决于你的粒度。

这是上面第二篇文章中的图片:enter image description here

我喜欢对象(游戏实体,或任何你想称呼它们的东西)具有最少的组件集的方式,推断的想法是你可以编写看起来像这样的代码:

BaseEntity Alien = new BaseEntity();
BaseEntity Player = new BaseEntity();

Alien.addComponent(new Position(), new Movement(), new Render(), new Script(), new Target());
Player.addComponent(new Position(), new Movement(), new Render(), new Script(), new Physics());

..这将是真的很好...但实际上,代码最终看起来像这样。

BaseEntity Alien = new BaseEntity();
BaseEntity Player = new BaseEntity();

Alien.addComponent(new Position(), new AlienAIMovement(), new RenderAlien(), new ScriptAlien(), new Target());
Player.addComponent(new Position(), new KeyboardInputMovement(), new RenderPlayer(), new ScriptPlayer(), new PhysicsPlayer());

似乎我最终拥有一些由较小组件组成的非常专业的组件。通常,我必须制作一些具有其他组件依赖关系的组件。毕竟,如果你没有位置,你怎么能渲染呢?不仅如此,你最终渲染玩家、外星人和手榴弹的方式也可能有根本的不同。你不能有一个组件来决定所有三个,除非你做一个非常大的组件(在这种情况下......为什么你还要使用复合模式?

举另一个现实生活中的例子。我的游戏中有角色可以装备各种装备。装备一件装备后,一些统计数据会发生变化,以及视觉上显示的内容。以下是我现在的代码:

billy.addControllers(new Movement(), new Position(), new CharacterAnimationRender(), new KeyboardCharacterInput());

billy.get(CharacterAnimationRender.class).setBody(BODY.NORMAL_BODY);
billy.get(CharacterAnimationRender.class).setFace(FACE.BLUSH_FACE);
billy.get(CharacterAnimationRender.class).setHair(HAIR.RED_HAIR);
billy.get(CharacterAnimationRender.class).setDress(DRESS.DRAGON_PLATE_ARMOR);

上述内容仅影响视觉上显示的内容。因此,我显然需要制作另一个处理齿轮统计数据的组件。但是,我为什么要做这样的事情:CharacterAnimationRender.class

billy.addControllers(new CharacterStatistics());

billy.get(CharacterAnimationRender.class).setBody(BODY.NORMAL_BODY);
billy.get(CharacterStatistics.class).setBodyStats(BODY_STATS.NORMAL_BODY);

当我可以制作一个控制器/组件来处理统计数据的分布以及视觉变化时?CharacterGearStuff

底线,我不确定这应该如何帮助生产力,因为除非你想手动处理所有事情,否则你仍然必须创建依赖于2个以上组件的“元组件”(并修改/交叉修改它们的所有子组件 - 把我们带回OOP)。或者也许我完全搞错了。我是吗?


答案 1

听起来你稍微误解了组件模式。

组件仅是数据,没有代码。如果你的组件中有代码,它就不再是一个组件了——它是更复杂的东西。

因此,例如,您应该能够轻易地分享您的CharacterAnimationRender和CharacterStatistics,例如:

CharacterStats { int BODY }
CharacterGameStats { ...not sure what data you have that affects gameplay, but NOT the rendering... }
CharacterVisualDetails { int FACE, int HAIR }

...但这些没有必要意识到彼此的存在。当你谈论组件之间的“依赖关系”的那一刻,我怀疑你已经迷失了。一个 int 结构如何“依赖于”另一个 int 结构?他们不能。它们只是数据块。

...

回到你一开始的担忧,你最终会得到:

Alien.addComponent(new Position(), new AlienAIMovement(), new RenderAlien(), new ScriptAlien(), new Target());
Player.addComponent(new Position(), new KeyboardInputMovement(), new RenderPlayer(), new ScriptPlayer(), new PhysicsPlayer());

...太完美了。假设您已经正确编写了这些组件,那么您已经将数据拆分为易于阅读/调试/编辑/代码的小块。

但是,这是在猜测,因为您尚未指定这些组件内部的内容...例如,AlienAIMovement - 这是什么?通常,我希望你有一个“AIMovement()”,然后编辑它以使其成为外星人的版本,例如,更改该组件中的一些内部标志以使用AI系统中的“外星人”功能。


答案 2

大卫

首先感谢您的完美提问。

我理解您的问题,并认为您没有正确使用该模式。请阅读这篇文章: http://en.wikipedia.org/wiki/Composite_pattern

例如,如果您无法实现一般类运动,并且需要AlienAIMovement和键盘移动,则可能应该使用访问者模式。但是在开始重构数千个代码行之前,请检查是否可以执行以下操作。

是否有机会编写接受 BaseEntity 类型的参数的类 Movement?可能所有运动实现之间的区别只是一个参数,标志或什么?在这种情况下,您的代码将如下所示:

Alien.addComponent(new Position(), new Movement(Alien), new Render(Alien), new Script(Alien), new Target());

我认为它不是那么糟糕。

如果不可能,请尝试使用工厂创建实例,因此

Alien.addComponent(f.createPosition(), f.createMovement(Alien), f.createRender(Alien), f.createRenderScript(Alien), f.createTarget());

我希望我的建议有所帮助。