对游戏对象的每个实例使用DrawableGameComponent的缺点是什么?


25

我在很多地方都读过,应该将DrawableGameComponents保存为诸如“关卡”之类的东西或某种类型的管理器,而不是使用它们,例如,用于字符或图块(就像这个人在这里说的)。但是我不明白为什么会这样。我读了这篇文章,对我来说很有意义,但这只是少数。

我通常不会过多地关注这类事情,但是在这种情况下,我想知道为什么大多数人都认为这不是路要走。也许我想念一些东西。


我很好奇为什么两年后您从我的答案中删除了“已接受”标记?通常我不会在意-但是在这种情况下,它会引起VeraShackle 对我对气泡顶部的回答的令人反感的错误陈述:(
Andrew Russell

@AndrewRussell我不久前读了这个帖子(由于一些回应和投票),并认为我没有资格就您的答案是否正确发表意见。正如您所说,我不认为VeraShackle的答案应该是投票最多的答案,所以我再次将您的答案标记为正确。
Kenji Kina

1
我可以将我的答案浓缩如下:“ 使用DrawableGameComponent将您锁定到一组方法签名和一个特定的保留数据模型中。最初,这些选择通常是错误的选择,并且锁定严重阻碍了代码的未来发展。 “但是让我告诉你这里发生了什么……
安德鲁·罗素

1
DrawableGameComponent是核心XNA API的一部分(再次:主要是出于历史原因)。新手程序员使用它是因为它“存在”并且“有效”,而且他们并不了解。他们看到了我的回答,告诉他们他们是“ 错误的 ”,并且-由于人类心理的本性-拒绝了这一选择,而选择了另一个“方面”。碰巧是VeraShackle的有争议的非回答;因为我没有立即指出来(见那些评论),所以碰巧有了动力。我觉得我最终的反驳仍然足够。
安德鲁·罗素

1
如果有人有兴趣upvotes的基础上,质疑我的回答的合法性:虽然这是事实,(能源办是aflush与上述新手)VeraShackle的答案已经设法获得对夫妇更在最近几年upvotes比我的,不要忘了我在整个网络(主要在XNA上)还有数千笔支持。我已经在多个平台上实现了XNA API。我是使用XNA的专业游戏开发人员。我的回答的谱系是无可挑剔的。
安德鲁·罗素

Answers:


22

“游戏组件”是XNA 1.0设计的早期部分。它们应该像WinForms的控件一样工作。这个想法是,您将能够使用GUI将它们拖放到您的游戏中(有点像添加非可视控件,例如计时器等)并在其上设置属性。

因此,您可能拥有诸如Camera或FPS计数器之类的组件?

你看?不幸的是,这个想法在现实世界的游戏中并没有真正起作用。游戏所需的组件类型并不是真正可重用的。您可能有一个“玩家”组件,但它与其他游戏的“玩家”组件大不相同。

我想正是因为这个原因,并且由于时间太短,该GUI才在XNA 1.0之前发布。

但是API仍然可用... 这就是为什么您不应该使用它的原因:

基本上,归结为最容易编写和阅读以及调试和维护的游戏开发人员。在您的加载代码中执行以下操作:

player.DrawOrder = 100;
enemy.DrawOrder = 200;

远不只是简单地这样做:

virtual void Draw(GameTime gameTime)
{
    player.Draw();
    enemy.Draw();
}

尤其是在现实世界的游戏中,您可能最终会遇到以下情况:

GraphicsDevice.Viewport = cameraOne.Viewport;
player.Draw(cameraOne, spriteBatch);
enemy.Draw(cameraOne, spriteBatch);

GraphicsDevice.Viewport = cameraTwo.Viewport;
player.Draw(cameraTwo, spriteBatch);
enemy.Draw(cameraTwo, spriteBatch);

(当然,您可以使用类似的Services体系结构共享相机和spriteBatch 。但这出于相同的原因也是个坏主意。)

这种体系结构-或缺少它 -更轻巧,更灵活!

我只需更改几行即可轻松更改绘制顺序,添加多个视口或添加渲染目标效果。要在其他系统中执行此操作,需要我考虑所有分布在多个文件中的组件的工作方式以及顺序。

这有点像声明式和命令式编程之间的区别。事实证明,对于游戏开发而言,命令式编程更为可取。


2
我对它们的印象是基于组件的编程,该组件显然广泛用于游戏编程。
BlueRaja-Danny Pflughoeft

3
XNA游戏组件类并非真的直接适用于该模式。该模式是关于由多个组件组成对象。XNA类适用于每个对象一个组件。如果它们完全适合您的设计,则一定要使用它们。但是,你应该不会建立自己的设计身边!另请参阅此处的答案 -查找我提到的地方DrawableGameComponent
安德鲁·罗素

1
我同意GameComponent / Service体系结构并没有那么有用,听起来像Office或Web应用程序概念被迫加入了游戏框架。但是我更喜欢player.DrawOrder = 100;在命令绘制命令时使用该方法。我现在正在完成Flixel游戏,该游戏按照将对象添加到场景中的顺序绘制对象。FlxGroup系统可以缓解这种情况,但是内置某种Z索引排序会很好。
michael.bartnett 2012年

@ michael.bartnett请注意,Flixel是保留模式API,而XNA(显然,游戏组件系统除外)是立即模式API。如果您使用保留模式API或实现自己的 API,那么动态更改绘制顺序的能力绝对重要。实际上,需要即时更改绘制顺序是使某些内容保持模式的好理由(想到一个窗口系统的例子)。但是,如果您可以将某些内容编写为立即模式,那么平台API(例如XNA)就是以这种方式构建的,则应该使用IMO。
安德鲁·罗素

有关更多参考,请参考gamedev.stackexchange.com/a/112664/56
Kenji Kina

26

嗯 恐怕在这里为了保持平衡而挑战了这个。

安德鲁的答复中似乎有5项指控,所以我将依次尝试解决每项指控:

1)组件是过时且废弃的设计。

组件设计模式的想法早在XNA之前就已存在-本质上,这只是制造对象的决定,而不是总体游戏对象(其自身的Update和Draw行为的所在地)的决定。对于其组件集合中的对象,游戏将在适当的时间自动调用这些方法(以及其他方法)-至关重要的是,游戏对象本身不必“知道”任何此代码。如果您希望在不同的游戏(以及不同的游戏对象)中重用游戏类,则从根本上讲,对象的这种分离仍然是一个好主意。快速浏览一下来自AppHub的最新的XNA4.0(专业生产)演示,证实了我的印象,即Microsoft仍然(在我看来非常坚决)落后于此。

2)Andrew不能想到超过2个可重用的游戏类。

我可以。实际上我可以想到负载!我刚刚检查了我当前的共享库,它包含80多个可重用的组件和服务。为了给您增添风味,您可以:

  • 游戏状态/屏幕管理器
  • 粒子发射器
  • 可绘制图元
  • AI控制器
  • 输入控制器
  • 动画控制器
  • 广告牌
  • 物理与碰撞经理
  • 摄影机

任何特定的游戏构想都需要至少5-10件东西-可以保证-并且它们在具有一行代码的全新游戏中可以完全发挥作用。

3)通过显式绘制调用的顺序控制绘制顺序比使用组件的DrawOrder属性更可取。

好吧,我基本上同意安德鲁对此的看法。但是,如果将所有组件的DrawOrder属性保留为默认值,则仍可以通过将它们添加到集合中的顺序来显式控制其绘制顺序。在“可见性”方面,这实际上与手动绘制调用的显式顺序相同。

4)自己调用对象的Draw方法的负载比通过现有框架方法调用它们更“轻巧”。

为什么?另外,在最琐碎的项目中,无论在哪种情况下,您的Update逻辑和Draw代码都将使您的方法调用完全蒙上阴影。

5)调试时,最好将代码放在一个文件中,而不要放在单个对象的文件中。

好吧,首先,当我在Visual Studio中调试时,就磁盘上的文件而言,断点的位置现在几乎是不可见的-IDE处理该位置时没有任何麻烦。但也请考虑一下您的Skybox绘图有问题。与在Game对象中的(可能很长)绘制方法中定位skybox绘制代码相比,使用Skybox的Draw方法真的要麻烦得多吗?不,不是真的-实际上,我认为事实恰恰相反。

因此,总而言之-请在这里仔细研究安德鲁的论点。我不认为他们实际上为编写纠缠的游戏代码提供了实质依据。解耦的组件模式(明智地使用)的好处是巨大的。


2
我只是注意到了这篇文章,所以我必须强烈反驳:对于可重用的类,我没有任何问题!(可能有绘制和更新方法等。)由于失去了对绘制/更新顺序的明确控制(您在#3中同意),我在使用()提供游戏体系结构方面遇到了特定问题。他们的接口(您不解决)。DrawableGameComponent
Andrew Russell

1
关于第4点,我使用“轻量级”一词来表示性能,而我实际上显然是指体系结构的灵活性。您剩下的第一点,第二点,尤其是第五点似乎是在架起荒谬的稻草人,我认为大多数/所有代码都应该被塞进您的主要游戏类中!在这里阅读我的答案,以获得我对体系结构的一些更详细的想法。
Andrew Russell

5
最后:如果您要对我的回答发表回应,说我错了,那么对我的回答也添加回复是有礼貌的,这样我就可以看到有关它的通知,而不必偶然它2个月后。
安德鲁·罗素

我可能误会了,但这如何回答这个问题?我应该为我的精灵使用GameComponent吗?
最佳


4

我不同意安德鲁的观点,但我也认为一切都存在时间和地点。我不会使用Drawable(GameComponent)Sprite或Enemy这样简单的东西。但是,我会将其用于SpriteManagerEnemyManager

回到安德鲁关于绘制和更新顺序的内容,我认为Sprite.DrawOrder对于许多精灵来说,这将非常令人困惑。而且,它相对容易设置,DrawOrder并且UpdateOrder对于数量较少(或合理)的Manager而言(Drawable)GameComponents

我认为,如果Microsoft确实在那种僵化的地方,没有人使用它们,Microsoft将会废除它们。但是他们没有,因为如果角色正确,他们可以完美地工作。我自己使用它们来开发Game.Services在后台更新的内容。


2

我也在考虑这个问题,它发生在我身上:例如,为了窃取链接中的示例,在RTS游戏中,您可以使每个单元成为游戏组件实例。好的。

但是您也可以制作一个UnitManager组件,该组件保留一个单位列表,并根据需要绘制和更新它们。我想您首先要将单位放入游戏组件的原因是划分代码,那么此解决方案是否可以充分实现该目标?


2

在评论中,我发布了我的答案旧答案的精简版:

使用可以DrawableGameComponent将您锁定到一组方法签名和特定的保留数据模型中。这些一开始通常是错误的选择,并且锁定严重阻碍了代码的未来发展。

在这里,我将通过发布当前项目中的一些代码来说明为什么会这样。


维护游戏世界状态的根对象具有如下所示的Update方法:

void Update(UpdateContext updateContext, MultiInputState currentInput)

有多个进入点Update,具体取决于游戏是否在网络上运行。例如,可以将游戏状态回滚到先前的时间点,然后重新进行模拟。

还有一些其他类似更新的方法,例如:

void PlayerJoin(int playerIndex, string playerName, UpdateContext updateContext)

在游戏状态内,有一个要更新的演员列表。但这不仅仅是一个简单的Update调用。在处理物理学之前和之后,还有其他的过程。还有一些花哨的东西可以用来延迟演员的产卵/破坏以及关卡转换。

除了游戏状态外,还有一个UI系统需要独立管理。但是,UI中的某些屏幕也需要能够在网络环境中运行。


对于绘画,由于游戏的性质,有一个自定义的排序和剔除系统,它从以下方法中获取输入:

virtual void RegisterToDraw(SortedDrawList sortedDrawList, Definitions definitions)
virtual void RegisterShadow(ShadowCasterList shadowCasterList, Definitions definitions)

然后,一旦确定要绘制的内容,就会自动调用:

virtual void Draw(DrawContext drawContext, int tag)

tag参数是必需的,因为某些角色可以保留其他角色-因此需要能够在所保留的角色的上方和下方进行绘制。分拣系统可确保以正确的顺序进行。

还可以绘制菜单系统。还有一个分层系统,用于在HUD和游戏周围绘制UI。


现在将这些要求与DrawableGameComponent提供的内容进行比较:

public class DrawableGameComponent
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
    public int UpdateOrder { get; set; }
    public int DrawOrder { get; set; }
}

没有一种合理的方法可以从一个系统使用DrawableGameComponent到上述系统。(对于复杂程度不高的游戏,这并不是不寻常的要求)。

同样,非常重要的一点是要注意,这些要求并非只是在项目开始时就出现了。他们经过几个月的开发工作而演变。


人们通常会做的,如果他们沿着这DrawableGameComponent条路走,那就是开始建立在骇客之上:

  • 他们没有添加方法,而是对其自身的基本类型进行了一些难看的向下转换(为什么不直接使用它呢?)。

  • 他们没有替换用于排序和调用Draw/ 的代码,而是Update做一些非常慢的事情来即时更改订购号。

  • 最糟糕的是:它们不是在方法中添加参数,而是在不是堆栈的内存位置开始“传递”“参数”(Services对此很普遍)。这是令人费解的,容易出错的,缓慢的,易碎的,很少是线程安全的,并且如果添加网络,制作外部工具等可能会成为问题。

    这也使代码的重用变得更加困难,因为现在您的依赖项隐藏在“组件”内部,而不是在API边界发布。

  • 或者,他们几乎看到了这一切有多么疯狂,然后开始做“经理” DrawableGameComponent来解决这些问题。除了他们在“经理”边界仍然存在这些问题。

    这些“管理器”始终可以轻松地按所需顺序用一系列方法调用替换。其他任何事情都过于复杂。

当然,一旦您开始使用这些hack,一旦意识到您需要的东西超出了所能提供的范围,那么需要采取真正的工作来撤消这些hack DrawableGameComponent。您将被“ 锁定 ”。诱惑通常是继续堆积在骇客上-实际上,这会使您陷入困境,并将您可以制作的游戏类型限制为相对简单的游戏。


很多人似乎已经达到了这一点并产生了内在的反应-可能是因为他们使用DrawableGameComponent并且不想接受“错误”。因此,他们抓住了至少有可能使它“起作用” 这一事实。

当然可以使它 “起作用”!我们是程序员-使事情在非常规约束下工作实际上是我们的工作。但是这种约束不是必要的,有用的或合理的...


什么是特别flabbergasting的是,这是惊人的微不足道的侧步这整个问题。在这里,我什至会给您代码:

public class Actor
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
}

List<Actor> actors = new List<Actor>();

foreach(var actor in actors)
    actor.Update(gameTime);

foreach(var actor in actors)
    actor.Draw(gameTime);

(添加按DrawOrder和排序UpdateOrder很简单。一种简单的方法是维护两个列表并使用List<T>.Sort()。处理Load/ 也很简单UnloadContent。)

该代码实际上什么并不重要。重要的是我可以对其进行微不足道的修改

当我意识到我需要一个与之不同的计时系统gameTime,或者我需要更多的参数(或者其中UpdateContext包含约15个东西的整个对象),或者我需要一个完全不同的操作顺序进行绘制,或者我需要一些额外的方法时对于不同的更新通行证,我可以继续进行

而且我可以立即做到。我不需要费心挑选DrawableGameComponent代码。或更糟糕的是:以某种方式对其进行破解,这将在下次出现这种需求时变得更加困难。


实际上,只有一种情况是使用它的一个不错的选择DrawableGameComponent:那就是如果您实际上是为XNA 制作和发布拖放组件式的中间件。这是其最初的目的。我要补充一点- 没有人在做!

(类似地,只有Services在为制作自定义内容类型时才有意义ContentManager。)


虽然我同意您的观点,但使用DrawableGameComponent某些组件继承的东西时也没有发现任何特别错误,尤其是对于菜单屏幕(菜单,大量按钮,选项和内容)或FPS /调试覆盖图之类的东西你提到过 在这些情况下,通常不需要对绘制顺序进行细粒度的控制,并且最终需要手动编写的代码更少(这是一件好事,除非您需要编写该代码)。但是我想这几乎就是您提到的拖放方案。
Groo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.