如何在不强制进行层次结构的情况下使对象彼此交互和通信?


9

我希望这些问题能使我的问题更清楚-但是,我完全理解它们是否愿意,所以请让我知道是否是这种情况,然后我将尝试使自己更加清楚。

认识BoxPong,这是我做的非常简单的游戏,以熟悉面向对象的游戏开发。拖动框以控制球并收集黄色的东西。
制作BoxPong可以帮助我提出一个基本问题:除其他事项外,我如何才能具有相互交互的对象?换句话说,有没有一种方法使对象不是分层的而是共存的?(我将在下面进一步详细介绍。)

我怀疑对象共存的问题是一个普遍的问题,因此我希望有一个既定的解决方法。我不想重新发明方形齿轮,因此我想寻找的理想答案是“这是一种通常用于解决您的问题的设计模式”。

尤其是在像BoxPong这样的简单游戏中,很明显,在同一级别上存在或应该存在少量对象。有一个盒子,有一个球,有一个收藏品。我可以用面向对象的语言表达的所有内容,尽管看起来似乎都是严格的HAS-A关系。这是通过成员变量完成的。我不能只是开始ball并让它做它的事情,我需要它永久地属于另一个对象。我已经设定,使游戏的主要对象一个箱子,并依次盒子一个球,并一个比分反超。每个对象也都有一个update()方法,该方法计算位置,方向等,我走了类似的方法还有:我所说的主要游戏对象的更新方法,它调用了所有儿童的更新方法,它们依次调用所有的更新方法他们的孩子。这是我看到的制作面向对象游戏的唯一方法,但我认为这不是理想的方法。毕竟,我不会完全把球看作是属于盒子的东西,而是要处于同一水平并与之交互。我想可以通过将所有游戏对象都转换成主要游戏对象的成员变量来实现,但是我看不出有什么解决方法。我的意思是……撇开明显的混乱,如何让球和盒子互相认识,也就是互动?

还存在对象之间需要传递信息的问题。我有很多为SNES编写代码的经验,您几乎一直可以访问整个RAM。假设您要为Super Mario World定制敌人,并且希望它删除Mario的所有硬币,然后只存储零以解决$ 0DBF,这没问题。没有任何限制说敌人无法访问玩家的状态。我想我被这种自由宠坏了,因为对于C ++之类的东西,我经常发现自己在想如何使其他对象(甚至全局对象)可以访问值。
以BoxPong为例,如果我希望球从屏幕边缘弹起怎么办?widthheight是的属性Game类,ball可以访问它们。我可以传递这些类型的值(通过构造函数或需要它们的方法),但是这只是对我的不好的做法。

我想我的主要问题是我需要对象彼此了解,但是我看到的唯一方法是严格的层次结构,这是丑陋且不切实际的。

我听说过C ++上的“朋友类”,并且知道它们是如何工作的,但是如果它们是最终解决方案,那么为什么我看不到friend每个C ++项目中都充斥着关键字呢?并不是每种OOP语言都存在这个概念吗?(我最近才了解到的函数指针也是如此。)

预先感谢您提供任何形式的答案-再次提醒您,如果有部分内容对您没有意义,请告诉我。


2
许多游戏行业已转向实体组件系统体系结构及其变体。它与传统的面向对象的方法不同,但是它运作良好,并且一旦概念陷入困境就有意义。Unity使用它。实际上,Unity仅使用实体组件部分,但基于ECS。
Dunk 2015年

Mediator设计模式解决了允许类在彼此之间互不了解的情况下相互协作的问题。你看过了吗?
导演2015年

Answers:


13

通常,如果同一级别的对象彼此认识,结果将非常糟糕。一旦对象彼此了解,它们便被捆绑或彼此耦合。这使得它们难以更改,难以测试,难以维护。

如果有一些对象“上方”了解这两个对象并可以设置它们之间的交互,那么效果会更好。知道两个同级的对象可以通过依赖项注入,事件或消息传递(或任何其他解耦机制)将它们绑定在一起。是的,这导致了一些人为的等级设置,但是它比当事物只是随意地相互作用时所得到的意大利面条混乱要好得多。这在C ++中仅更为重要,因为您还需要一些东西来拥有对象的生命周期。

简而言之,您可以通过临时访问将所有对象并排放置在一起来做到这一点,但这不是一个好主意。层次结构提供顺序和明确的所有权。要记住的主要事情是,代码中的对象不一定是现实生活(甚至是游戏)中的对象。如果游戏中的对象没有很好的层次结构,则不同的抽象可能会更好。


2

以BoxPong为例,如果我希望球从屏幕边缘弹起怎么办?width和height是Game类的属性,我需要球才能访问它们。

没有!

我认为您遇到的主要问题是,从字面上看,您对“面向对象编程”的看法有点过头。在OOP中,对象不是“事物”而是“思想”,表示“球”,“游戏”,“物理”,“数学”,“日期”等。它们都是有效的对象。也不需要对象“知道”任何东西。例如,Date.Now().getTommorrow()将询问计算机今天是星期几,应用不可思议的日期规则找出明天的日期,并将其返回给呼叫者。该Date对象不知道其他任何事情,只需要根据需要从系统请求信息。此外,Math.SquareRoot(number)除了如何计算平方根的逻辑外,无需了解其他任何信息。

因此,在您引用的示例中,“球”应该对“盒子”一无所知。盒子和球是完全不同的想法,没有权利互相交谈。但是物理引擎确实知道“盒子和球”是什么(或者至少是ThreeDShape),并且知道它们在哪里,以及它们应该发生什么。因此,如果球由于冷而收缩,则物理引擎会告诉该球实例它现在较小。

这有点像在造汽车。计算机芯片对汽车发动机一无所知,但是汽车可以使用计算机芯片来控制发动机。通过将简单的小物件组合在一起来创建稍微更大,更复杂的物件的简单想法,该物件本身可作为其他更复杂零件的组件重用。

在您的马里奥(Mario)示例中,如果您在挑战室中接触敌人并不会排放马里奥斯硬币,而是将他从该房间弹出,该怎么办?在马里奥或敌人的观念空间之外,马里奥在接触敌人时应该丢掉硬币(实际上,如果马里奥拥有一颗无敌之星,他会杀死敌人)。因此,负责马里奥碰到敌人时发生什么事情的任何对象(域/想法)是唯一需要了解任何一个对象的对象,并且应对它们中的任何一个做任何事情(在该对象允许外部驱动更改的范围内) )。

另外,关于对象调用子对象的陈述Update(),这很容易发生错误,就像Update每帧被不同的父对象多次调用会怎样?(即使您抓住了这一点,也浪费了CPU时间,这可能会使您的游戏变慢)。每个人都应该在需要时触摸他们需要的内容。如果使用的是Update(),则应使用某种形式的订阅模式,以确保所有更新每帧被调用一次(如果像Unity中那样不为您处理)

学习如何将领域思想定义为清晰,孤立,定义明确且易于使用的模块,将是您如何充分利用OOP的最大因素。


1

认识BoxPong,这是我制作的非常简单的游戏,以熟悉面向对象的游戏开发。

制作BoxPong可以帮助我提出一个基本的问题:在没有“彼此”归属的情况下,我如何才能具有相互交互的对象?

我有很多为SNES编写代码的经验,您几乎一直可以访问整个RAM。假设您要为“超级马里奥世界”(Super Mario World)定制敌人,并且希望它删除所有马里奥(Mario)的硬币,然后只存储零以解决$ 0DBF,这没问题。

您似乎错过了面向对象编程的重点。

面向对象编程是通过有选择地反转架构中的某些关键依赖关系来管理依赖关系,从而可以防止僵化,脆弱和不可重用。

什么是依赖性?依赖就是对其他东西的依赖。当您存储零以寻址$ 0DBF时,您依赖于以下事实:该地址是Mario硬币所在的位置,并且这些硬币用整数表示。您的自定义敌人代码依赖于实现Mario及其硬币的代码。如果更改了Mario将其硬币存储在内存中的位置,则必须手动更新所有引用该内存位置的代码。

面向对象的代码就是使您的代码依赖于抽象而不是细节。所以代替

class Mario
{
    public:
        int coins;
}

你会写

class Mario
{
    public:
        void LoseCoins();

    private:
        int coins;
}

现在,如果您想将Mario的硬币存储方式从int更改为long或double,或者将其存储在网络上或将其存储在数据库中,或者启动其他较长的过程,则可以在一个地方进行更改: Mario类,您的所有其他代码保持不变。

因此,当你问

如何使对象彼此交互而不必彼此“属于”?

你真的在问:

如何拥有直接相互依赖而没有任何抽象的代码?

这不是面向对象的编程。

我建议您首先阅读以下内容:http : //objectmentor.com/omSolutions/oops_what.html,然后在YouTube上搜索Robert Martin的所有内容并观看所有内容。

我的回答来自他,其中一些直接来自他。


感谢您的回答(以及您链接到的页面;看起来很有趣)。我实际上了解抽象和可重用性,但是我想我的回答并没有很好地说明这一点。但是,从您提供的示例代码中,我现在可以更好地说明我的观点!您基本上是在说敌人的物体不应该这样做mario.coins = 0;,但是mario.loseCoins();,这是正确而正确的-但我的意思是,敌人mario无论如何仍可以访问该物体?对我来说mario,成为成员变量enemy似乎并不正确。
vvye,2015年

那么简单的答案就是将Mario作为参数传递给Enemy中的函数。您可能具有像marioNearby()或AttackMario()这样的函数,它将Mario作为参数。因此,每当触发敌人和马里奥进行互动的背后逻辑时,您都将调用敌人。marioNearby(mario),这将调用mario.loseCoins();。稍后,您可能会确定有一类敌人导致马里奥仅损失一个硬币甚至获得硬币。现在,您可以在一个地方进行更改,而不会对其他代码造成副作用。
Mark Murfin

通过将Mario传递给敌人,您已经将它们耦合了。马里奥和敌人不应该知道对方甚至是东西。这就是为什么我们创建高阶对象来管理如何将简单对象耦合在一起的原因。
Tezra

@Tezra但是,这些高阶对象不是完全不可重用的吗?感觉这些对象充当了功能,它们仅作为它们所展示的过程而存在。
史蒂夫·查麦拉德

@SteveChamaillard每个程序都将至少具有一些特定的逻辑,这在任何其他程序中都没有意义,但是其想法是将这种逻辑隔离到一些高阶类中。如果您拥有马里奥,敌人和关卡等级,则可以在其他游戏中重用马里奥和敌人。如果您将敌人和马里奥直接联系在一起,那么任何需要一个游戏的人都必须同时加入另一个。
Tezra

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.