复合模式/实体系统与传统面向对象编程(OOP)

18

我正在开发一个用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个或更多组件(并修改/交叉修改它们的子组件-将我们带回到面向对象编程)的“元组件”。 或者也许我完全想错了。 我吗?

4个回答

7

看起来你对组件模式有些误解。

组件只包含数据,不包含代码。如果你的组件中有代码,那么它就不再是组件,而是更复杂的东西。

因此,例如,你应该能够轻松地共享你的CharacterAnimationRender和CharacterStatistics:

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

...但这些组件之间并没有必要知道彼此的存在。当你谈论组件之间的“依赖关系”时,我怀疑你已经迷失了方向。一个整数结构体如何“依赖于”另一个整数结构体?它们不能。它们只是数据块。

...

回到你在开头的担忧,你最终会得到:

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()”,然后将其编辑为Alien版本,例如更改该组件中的一些内部标志以表明它正在使用AI系统中的“Alien”函数。


嗨,Adam,感谢您回答这个问题:D - 我已经适当地分离了代码的数据和逻辑(正如您在博客中提到的那样)。事实上,“Position()”,“Movement()”等是我称之为控制器的东西,它们“依赖”于数据模型(您可以称之为组件,这些组件是仅包含私有数据成员的简单类(在位置的情况下,private double x;private double y;)- 我在控制器内部完成所有这些链接。 - David Titarenco

2

大卫,

首先感谢您的完美问题。

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

例如,如果您无法实现通用类Movement并需要AlienAIMovement和KeyboardMovement,则可能应该使用访问者模式。但在重构成千上万行代码之前,请检查您是否可以执行以下操作。

有没有机会编写接受BaseEntity类型参数的Movement类?也许所有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());

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


我喜欢 new Movement(Alien) 这个想法,但我认为这样做会破坏实体系统模式,至少从上面第一篇文章和评论的阅读来看。 - David Titarenco

2
Ents 似乎是专门为您想要的功能而设计的。如果您仍然想要自己的库,您可以至少从它的设计中学到一些东西。在我看来,之前的答案都显得笨重,并且创建了大量不必要的对象。

0

我认为你在这里采用了错误的方法。模式应该根据你的需求进行调整,而不是相反。简单总比复杂好,如果你感觉某些东西不对劲,这意味着你应该退回几步,或许从头开始。

对我来说,这已经有了代码异味:

BaseEntity Alien = new BaseEntity();
Alien.addComponent(new Position(), new AlienAIMovement(), new RenderAlien(), new ScriptAlien(), new Target());

我期望面向对象的代码看起来像这样:
Alien alien = new AlienBuilder()
    .withPosition(10, 248)
    .withTargetStrategy(TargetStrategy.CLOSEST)
    .build();

//somewhere in the main loop
Renderer renderer = getRenderer();
renderer.render(alien);

当您为所有实体使用通用类时,您将拥有一个非常通用且难以使用的API来处理您的对象。

此外,将Position、Movement和Renderer等内容放在同一组件下感觉不太对。Position不是组件,它是属性。Movement是行为,而Renderer与您的领域模型无关,它是图形子系统的一部分。组件可以是汽车轮、外星人的身体部位和枪支。

游戏开发是一件非常复杂的事情,很难一次性做到完美。从头开始重写代码,从错误中学习并感受自己所做的事情,而不仅仅是尝试从某篇文章中制定模式。如果您想更好地掌握模式,可以尝试其他东西,而不是游戏开发。例如编写一个简单的文本编辑器。


6
我认为游戏实体/组件并不一定要面向对象,它们应该是数据驱动的。至少从我阅读上面第一篇文章得出的结论来看是这样的。我还阅读了很多实现实体系统/组件模式的开源代码(http://gamadu.com/temp/es.zip,http://code.google.com/p/spartanframework/,等等),看起来我也是以同样的方式实现的。 - David Titarenco
1
“从头开始重写你的代码”几乎总是糟糕的建议。这也表明你不熟悉ECP模式。 - vaughandroid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接