【问题标题】:The composite pattern/entity system and traditional OOP复合模式/实体系统和传统的OOP
【发布时间】:2011-02-09 06:30:10
【问题描述】:

我正在开发一个用 Java 编写的小游戏(但问题与语言无关)。因为我想探索各种设计模式,所以我挂断了 Composite pattern/Entity 系统(我最初读到的是 herehere)作为典型深层层次继承的替代方案。

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

这是上面第二篇文章的图片:

我喜欢对象(游戏实体,或任何您想称呼它们的名称)具有最少组件集的方式,并且推断的想法是您可以编写如下代码:

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)。或者,也许我在想它是完全错误的。我是吗?

【问题讨论】:

    标签: java design-patterns language-agnostic composite


    【解决方案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 }
    

    ...但是没有必要让它们意识到彼此的存在。当您谈论组件之间的“依赖关系”时,我怀疑您迷路了。一个整数结构如何“依赖”另一个整数结构?他们不能。它们只是数据块。

    ...

    回到你最开始关心的问题,你最终会得到:

    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 系统中的“外星人”功能。

    【讨论】:

    • 嗨,亚当,感谢您回答这个问题:D - 我适当地拆分了我的代码的数据和逻辑(实际上正如您在博客中提到的那样)。事实上,Position()Movement() 等是我所说的“依赖”数据模型的控制器(我猜你可以称之为组件,它们是仅包含私有数据成员的简单类(在位置的情况,private double x;private double y;) - 我在内部与控制器进行所有这些链接。
    【解决方案2】:

    大卫,

    首先感谢您提出完美的问题。

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

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

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

    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) 的想法,但我认为这样做会破坏实体系统模式,至少从阅读上面的第一篇文章和 cmets 来看是这样。
    【解决方案3】:

    Ents 似乎旨在完全按照您的意愿行事。如果您仍然想要自己的库,您至少可以从它的设计中学习。

    IMO,以前的答案似乎都很笨拙,并创建了大量不必要的对象。

    【讨论】:

      【解决方案4】:

      我认为您在这里使用了错误的方法。应该采用模式来满足您的需求,反之亦然。简单总是比复杂好,如果你觉得有些事情不正常,这意味着你应该退后几步,也许从头开始。

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

      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 等内容放在同一个 Component 下感觉是错误的。位置不是一个组件,它是一个属性。运动是一种行为,而渲染器是与你的领域模型完全无关的东西,它是图形子系统的一部分。组件可以是汽车的轮子、外星人的身体部位和枪支。

      游戏开发是一件非常复杂的事情,而且很难从第一次尝试就做好。从头开始重写你的代码,从你的错误中学习并感受你在做什么,而不是仅仅尝试从一些文章中定制一个模式。如果你想在模式方面做得更好,你应该尝试游戏开发以外的其他东西。比如写一个简单的文本编辑器。

      【讨论】:

      • 我不认为游戏实体/组件本身是面向对象的,它们是数据驱动的。至少从我在阅读上面的第一篇文章中收集到的信息。我还阅读了很多实现实体系统/组件模式的开源代码(gamadu.com/temp/es.zipcode.google.com/p/spartanframework 等),似乎我正在以相同的方式实现它。
      • “从头开始重写代码”几乎总是糟糕的建议。很明显你不熟悉 ECP 模式。
      猜你喜欢
      • 2013-02-14
      • 1970-01-01
      • 2012-09-01
      • 1970-01-01
      • 2020-02-24
      • 1970-01-01
      • 1970-01-01
      • 2020-03-02
      • 1970-01-01
      相关资源
      最近更新 更多