游戏中绘画与逻辑的分离


11

我是一个刚刚开始玩游戏开发的开发人员。我是.NET专家,所以我迷上了XNA,现在正在为iPhone使用Cocos2d。我的问题确实更笼统。

假设我正在构建一个简单的Pong游戏。我要上BallPaddle堂课。来自商业世界的发展,我的第一个直觉是在这两个类中的任何一个中都没有任何绘图或输入处理代码。

//pseudo code
class Ball
{
   Vector2D position;
   Vector2D velocity;
   Color color;
   void Move(){}
}

球类中没有任何东西可以处理输入或处理绘图。然后,我将有另一个班级,Game班级或我的班级Scene.m(在Cocos2d中)将球更新,并且在游戏循环中,它将根据需要操纵球。

事实是,在许多有关XNA和Cocos2d的教程中,我看到了这样的模式:

//pseudo code
class Ball : SomeUpdatableComponent
{
   Vector2D position;
   Vector2D velocity;
   Color color;
   void Update(){}
   void Draw(){}
   void HandleInput(){}
}

我的问题是,对吗?这是人们在游戏开发中使用的模式吗?它以某种方式与我习惯的一切背道而驰,让我的Ball类做所有事情。此外,在第二个示例中,我Ball知道如何移动,如何使用Paddle?是否Ball需要了解Paddle?在我的第一个示例中,Game该类将同时引用Ball和和Paddle,然后将这两个都发送给某个CollisionDetection管理器或其他东西,但是如果每个单独的组件自己完成所有工作,那么我该如何处理各种组件的复杂性呢?(我希望我有道理.....)


2
不幸的是,在许多游戏中确实是如此。我不会认为这是最佳做法。您正在阅读的许多教程可能都是针对初学者的,因此他们不会开始向读者介绍模型/视图/控制器或轭模式。
tenpn 2011年

@tenpn,您的意见似乎表明您认为这不是好习惯,我想知道您是否可以进一步解释?我一直认为这是最合适的安排,因为那些方法包含的代码可能对每种类型都非常特定。但我本人经验不足,所以我很想听听您的想法。
sebf 2011年

1
@sebf如果您正在进行面向数据的设计,那么它将不起作用,而如果您喜欢面向对象的设计,它将不会起作用。参见Bob叔叔的SOLID原理:butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
tenpn 2011年

@tenpn。链接的文章和文档非常有趣,谢谢!
sebf 2011年

Answers:


10

我的问题是,对吗?这是人们在游戏开发中使用的模式吗?

游戏开发人员倾向于使用任何可行的方法。它并不严格,但嘿,它可以发货了。

它以某种方式与我习惯的一切背道而驰,让我的Ball类做所有事情。

因此,对于您所拥有的东西,更通用的用法是handleMessages / update / draw。在大量使用消息的系统中(它具有正反两方面的优点和缺点),游戏实体会捕获其关心的任何消息,对这些消息执行逻辑,然后进行绘制。

注意,draw()调用实际上可能并不意味着该实体调用了putPixel或它内部的任何东西。在某些情况下,这可能仅意味着更新数据,这些数据随后将由负责绘图的代码进行轮询。在更高级的情况下,它通过调用渲染器公开的方法来“绘制”自身。在我现在正在研究的技术中,对象将使用renderer调用进行绘制,然后渲染线程在每个帧中组织所有对象进行的所有调用,针对状态更改进行优化,然后将整个对象绘制出来(所有这种情况发生在与游戏逻辑不同的线程上,因此我们获得了异步渲染)。

责任的下放取决于程序员。对于简单的乒乓游戏,有意义的是每个对象都完全处理自己的图形(完成了低级调用)。这是风格和智慧的问题。

此外,在第二个示例中,我的Ball知道如何移动,我将如何使用Paddle处理碰撞检测?球需要了解球拍吗?

因此,解决此问题的一种自然方法是让每个对象都将其他每个对象作为某种类型的参数来检查碰撞。这伸缩性很差,并且可能会产生怪异的结果。

让每个类实现某种可碰撞的接口,然后使用一段高级代码,在游戏的更新阶段循环遍历所有可折叠对象并处理碰撞,并根据需要设置对象位置。

同样,您只需要对如何委派游戏中不同事物的责任感有所了解(或下定决心)。渲染是一种单独的活动,可以由物体在真空中进行处理。碰撞和物理学通常不能做到。

在我的第一个示例中,Game类将同时引用Ball和Paddle,然后将它们都交付给CollisionDetection管理器或其他工具,但是如果每个单独的组件都需要处理,那么我该如何处理各种组件的复杂性一切自己?

参见上面对此的探索。如果您使用的语言易于支持接口(例如Java,C#),那么这很容易。如果您使用的是C ++,这可能会很有趣。我赞成使用消息传递来解决这些问题(类会处理可能的消息,而忽略其他消息),其他一些人则喜欢组件组成。

同样,无论哪种方法,最适合您的方法。


2
哦,永远记住:YAGNI(您将不需要它)在这里非常重要。您可能会浪费大量时间来尝试使用理想的灵活系统来处理所有可能的问题,然后再也无法完成任何工作。我的意思是,如果您真的想为所有可能的结果提供良好的多人游戏主干,则可以使用CORBA,对吗?:)
ChrisE 2011年

5

我最近使用“实体系统”制作了一个简单的Space Invadors游戏。这种模式可以很好地将属性和行为分开。我花了一些迭代才能完全理解它,但是一旦设计了一些组件,使用现有组件来组成新对象就变得非常简单。

您应该阅读以下内容:

http://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/

它是一个非常博学的家伙经常更新。这也是唯一的带有具体代码示例的实体系统讨论。

我的迭代如下:

第一次迭代有一个“ EntitySystem”对象,正如Adam所描述的。但是我的组件仍然具有方法-我的“可渲染”组件具有paint()方法,而position组件具有move()方法等。当我开始充实实体时,我意识到我需要开始在两个之间传递消息组件和命令组件执行的更新……太混乱了。

因此,我回去重新阅读了T机博客。注释线程中有很多信息-他在其中确实强调了组件没有行为-行为是由实体系统提供的。这样,您不需要在组件之间传递消息并订购组件更新,因为顺序是由系统执行的全局顺序决定的。好。也许那太抽象了。

无论如何,对于迭代2,这是我从博客中收集到的内容:

EntityManager-充当组件“数据库”,可以查询包含某些类型的组件的实体。它甚至可以由内存数据库支持以快速访问...有关更多信息,请参阅t-machine第5部分。

EntitySystem-每个系统本质上只是在一组实体上运行的一种方法。每个系统将使用实体的组件x,y和z来完成工作。因此,您将向管理器查询包含x,y和z组件的实体,然后将结果传递给系统。

实体-只是一个ID,如long。实体是将一组组件实例组合为一个“实体”的实体。

组件-一组字段...。没有行为!当您开始添加行为时,即使在一个简单的Space Invadors游戏中,它也开始变得混乱。

编辑:顺便说一句,“ dt”是自上次主循环调用以来的增量时间

所以我的主要Invadors循环是这样的:

Collection<Entity> entitiesWithGuns = manager.getEntitiesWith(Gun.class);
Collection<Entity> entitiesWithDamagable =
manager.getEntitiesWith(BulletDamagable.class);
Collection<Entity> entitiesWithInvadorDamagable = manager.getEntitiesWith(InvadorDamagable.class);

keyboardShipControllerSystem.update(entitiesWithGuns, dt);
touchInputSystem.update(entitiesWithGuns, dt);
Collection<Entity> entitiesWithInvadorMovement = manager.getEntitiesWith(InvadorMovement.class);
invadorMovementSystem.update(entitiesWithInvadorMovement);

Collection<Entity> entitiesWithVelocity = manager.getEntitiesWith(Velocity.class);
movementSystem.move(entitiesWithVelocity, dt);
gunSystem.update(entitiesWithGuns, System.currentTimeMillis());
Collection<Entity> entitiesWithPositionAndForm = manager.getEntitiesWith(Position.class, Form.class);
collisionSystem.checkCollisions(entitiesWithPositionAndForm);

乍一看,它有点不可思议,但它的灵活性却难以置信。优化也很容易。对于不同的组件类型,您可以具有不同的后备数据存储区,以加快检索速度。对于“ form”类,您可以为其添加四叉树以加快访问速度以进行碰撞检测。

我像你; 我是一个经验丰富的开发人员,但没有编写游戏的经验。我花了一些时间研究开发模式,这个模式引起了我的注意。它绝不是唯一的处理方法,但我发现它非常直观且强大。我相信图案系列“游戏编程精粹”的书6正式讨论- http://www.amazon.com/Game-Programming-Gems/dp/1584500492。我自己还没有看过任何书籍,但听说它们是游戏编程的实际参考资料。


1

编写游戏程序时,没有必须遵循的明确规则。只要您在整个游戏中都遵循相同的设计模式,我就会发现两种模式都可以接受。您会惊讶于游戏中还有更多的设计模式,其中有些甚至不在OOP之上。

现在,根据我的个人喜好,我更喜欢按行为将代码分开(绘制一个类/线程,更新一个类/线程),同时具有最少的对象。我发现并行化比较容易,而且我经常发现自己同时处理更少的文件。我会说这是一种更程序化的处理方式。

但是,如果您来自商业世界,那么编写一些知道如何为自己做所有事情的类可能会更自在。

再一次,我建议您写出您觉得最自然的东西。


我想你误解了我的问题。我是说我不喜欢自己做所有事情的课程。我跌倒了,就像一个球应该只是一个球的逻辑表示,但是它对游戏循环,输入处理等
一无所知

公平地讲,在业务编程中,如果将其归结起来,有两种训练:加载,持久化并自行执行逻辑的业务对象,以及DTO + AppLogic +存储库/持久层...
Nate

1

“球”对象具有属性和行为。我发现将对象的独特属性和行为包含在自己的类中是很有意义的。球对象更新的方式与球拍更新的方式不同,因此有道理,这些是唯一的行为,可以保证将其包含在类中。与绘图相同。桨与球的绘制方式通常存在独特的区别,我发现修改单个对象的绘制方法以适应这些需求比在另一类中解决条件评估要容易得多。

就对象和行为的数量而言,Pong是一款相对简单的游戏,但是当您进入数十个或数百个独特的游戏对象时,您可能会发现第二个示例更容易将所有内容模块化。

大多数人确实使用输入类,其结果可通过Service或静态类提供给所有游戏对象。


0

我认为您在商业世界中可能会习惯于将抽象和实现分开。

在游戏开发者世界中,您通常需要考虑速度。您拥有的对象越多,就会发生更多的上下文切换,这可能会降低当前速度,具体取决于当前负载。

这只是我的猜测,因为我是一名游戏开发人员,但还是专业的开发人员。


0

不,这不是“正确”或“错误”,只是一种简化。

除非您使用的是强制将表示和逻辑分开的系统,否则某些业务代码是完全相同的。

这里是一个VB.NET示例,其中在GUI事件处理程序中编写了“业务逻辑”(添加数字)-http: //www.homeandlearn.co.uk/net/nets1p12.html

如果每个单独的组件都能自己完成所有工作,那么我该如何处理各种组件的复杂性呢?

如果并且当它变得如此复杂时,则可以将其排除在外。

class Ball
{
    GraphicalModel ballVisual;
    void Draw()
    {
        ballVisual.Draw();
    }
};

0

根据我的开发经验,除非有与您一起工作的团队接受过实践,否则没有正确或错误的做事方式。我遇到了反对开发者的反对,他们反对将行为,外观等分离出来。我确信他们会同意我对编写包含所有阳光下的课程的保留意见。请记住,您不仅必须编写它,而且还必须明天和将来读取您的代码,调试它,并可能在下一次迭代中以它为基础。您应该选择一条适合您(和团队)的路径。选择其他人使用的路线(对您来说是违反直觉的)会使您减速。

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.