以正确的方式实现模型-视图-控制器

Posted

技术标签:

【中文标题】以正确的方式实现模型-视图-控制器【英文标题】:Implementing Model-View-Controller the right way 【发布时间】:2010-11-14 03:46:13 【问题描述】:

在 OS X 上使用 Objective-C/Cocoa 开发游戏,我完成了原型,尽其所能完成。这是一团乱麻,这是我的第一个游戏,但一切正常。我一直在阅读有关将事物组合在一起的最佳方法,而 MVC 似乎是最有意义的,但我有点困惑。

从哪里开始?用控制器?这对我来说似乎最有意义,但它是如何开始的?在我的原型混乱中,我拥有从视图的init 开始并从那里开始的所有内容。我可以对控制器做同样的事情,然后把需要的东西放在init 中吗?或者还有什么我可以用来做这件事的吗?如果是从init开始的,我怎么init控制器呢?

我将如何设置游戏世界?我目前使用两个数组,一个用于世界(墙、地板、门、水、熔岩等),一个用于项目(我将添加第三个用于字符)。地图(一个.plist)被加载,然后对象被创建并添加到它所属的数组中。数组去哪儿了?在原型中,它们也是视图的一部分,所以我想你可以说我将两者(视图和控制器)结合在一起。是否会为每个地图创建一个 Map 对象?会有一个包含所有地图的 Maps 对象吗?

这一切如何协同工作?玩家按下一个键,在游戏中移动角色。视图将处理输入,对吗?你会将它发送到控制器,它会检查地图/其他数组中的所有内容(墙壁、怪物等),然后返回结果吗?或者你会把它发送给玩家,它会去控制器,它会做所有的检查,然后返回结果?

我以为我的脑子里已经很好地布置了它,但我想得越多,我的想法就越不牢固,我就越困惑。如果您认为它会更有效地传达要点,请务必毫不犹豫地画出一些东西。

如果您花时间阅读所有这些内容,感谢您的耐心等待。根据我收集到的信息,大多数编写代码的人不使用任何类型的设计。在阅读了这篇文章之后,我明白为什么有些人会避免它,这很令人困惑,人们似乎认为这不值得花时间。我个人认为优势完全超过了劣势(有吗?),并且只有在每次想要实现新功能时都不必完全重写的方式来保持事物的组织性才有意义。没有设计你就不会建造房子、汽车或电器,为什么没有设计你会编写复杂的程序?

我问这个问题是因为我想以正确的方式做这件事,而不是用黑客和半途而废的方式来“胜利”。

【问题讨论】:

我要加 300 赏金。我想要一个该死的好答案。 如果您需要我澄清一些事情,或者您需要更多细节,请在这里告诉我,我非常愿意。 很抱歉第一次给出一个蹩脚的答案,我没有太多时间,但认为演示文稿可能会有所帮助:-) 【参考方案1】:

您的主要困惑似乎是在应用启动期间事物是如何构建的。很多人对此感到困惑,因为感觉像是在发生某种魔法,但让我看看我能不能打破它。

    应用由用户启动 C main() 函数被调用 main() 调用 NSApplicationMain() NSApplicationMain() 加载 MainMenu.nib(或 info.plist 中指定的另一个 nib) Nib 加载会初始化 nib 中定义的所有对象(包括应用程序委托) 笔尖加载使笔尖中定义的所有连接 Nib 加载在它刚刚创建的所有对象上调用 awakeFromNib -applicationWillFinishLaunching:在应用程序委托中调用。 NSApplication(或Info.plist中指定的子类)被初始化 -applicationDidFinishLaunching:在应用程序委托中调用。

请注意,应用程序委托是在 NSApplication(子)类之前初始化的。这就是为什么 application(Will|Did)FinishLaunching: 接受通知,而不是作为其代表的 NSApplication。

这样做的一般后果是您的视图是通过 nib 为您创建的,而您的控制器是作为 nib 启动的副作用而创建的,因为它们往往是 nib 中的根级别对象,或者是 nib 的文件所有者。您的模型通常是在应用程序委托中创建的,或者是在首次访问它时延迟初始化的单例。

【讨论】:

应用程序委托没有在 Info.plist 中指定,也没有被 NSApplicationMain() 实例化。它在 Nib 中序列化并通过 File's Owner 连接到 NSApp。也无法发送 -applicationWillFinishLaunching: 在 NSApp 初始化之前,因为应用程序还不存在到 willFinishLaunching。 您是正确的,它没有在 Info.plist 中声明,我将更改它并适当地重新排序。 applicationWillFinishLaunching:绝对可以在应用程序初始化之前发送,这就是它的记录行为:“在应用程序对象初始化之前由默认通知中心发送。” 所以,我刚刚测试过,似乎 NSApp 是在 applicationWillFinishLaunching: 之前初始化的,但这并不一定是真的,这不是记录在案的行为,我很确定这不是它在旧版本的 OS X 中工作。【参考方案2】:

您可能对我给 ACCU '09 的演示文稿感兴趣 -“Adopting Model-View-Controller in Cocoa and Objective-C”。

从哪里开始?随着 控制器?这似乎使 对我来说最有意义,但它是怎么回事 开始了吗?

创建一个新的 Cocoa 应用程序项目,您会看到模板已经提供了一个控制器类 - 它是应用程序委托类。现在查看MainMenu.xib。有一个应用程序委托的实例,它连接到“文件所有者”对象的delegate 出口。在这种情况下,NSApplication 是文件的所有者;这就是想要解开 MainMenu 的东西。所以这确实是应用程序的委托。

这意味着我们有一个控制器对象,可以与NSApplication 实例通信,并且可以与XIB 中的所有其他对象有出口。这使它成为设置应用程序初始状态的好地方 - 即成为应用程序的“入口点”。实际上入口点应该是-applicationDidFinishLaunching: 方法。一旦应用程序完成了使您的应用程序进入稳定运行状态所需的所有内容,就会调用它 - 换句话说,Cocoa 很高兴它完成了它需要做的事情,其他一切都取决于您。

-applicationDidFinishLaunching: 是您要创建或恢复初始模型的地方,它是应用程序状态的表示(如果该类比适合您的应用程序,您也可以将其视为表示用户的文档 -基于文档的应用程序仅比默认应用程序复杂一点)并告诉视图如何向用户表示事物。在许多应用程序中,您不需要在应用程序启动时加载整个模型;首先,它可能会很慢并且使用的内存比您需要的多,其次,第一个视图可能不会显示有关模型的每一点。因此,您只需加载所需的位,以便向用户显示发生了什么;您正在控制视图和模型之间的交互。

如果您需要在不同的视图中显示其他信息 - 例如,如果您的主视图是主视图并且您需要显示详细信息编辑器 - 那么当用户告诉您他们想要做什么时,您需要设置向上。他们通过执行一些操作来告诉您,您可以在应用程序委托中处理这些操作。然后创建一个新的控制器来支持新的视图,并告诉它从哪里获取它需要的模型信息。您可以将其他 View 对象保存在单独的 XIB 中,因此仅在需要时才加载它们。

我将如何设置游戏世界?一世 目前使用两个数组,一个用于 世界(墙壁,地板,门,水, 熔岩等),一个用于物品 (我将添加第三个 人物)。地图(.plist)是 加载,然后对象是 创建并添加到数组中 属于。数组去哪儿了?在 原型,它们也是 视图,所以我想你可以说我 结合两者(视图和控制器) 一起。会有 Map 对象吗 为每张地图创建?会不会有一个 Maps 对象包含所有 地图?

我们可以通过分析您上面的陈述来确定我们正在建模的对象 - 您可能没有意识到,但您已经勾勒出了一个规范 :-)。有一个包含墙壁、门等的世界,所以我们知道我们需要这些对象,并且它们应该属于一个 World 对象。但我们也有物品和角色——它们如何与世界互动?一个地方可以包含水和人物吗?如果是这样,也许世界是由 Locations 组成的,每个 Location 都可以有墙或门或其他任何东西,也可以有物品和角色。请注意,如果我这样写,似乎该项目属于该位置,而不是该项目的位置。我会说“垫子上有一只猫”而不是“猫下面有垫子”。

因此,只需考虑您希望您的游戏世界代表什么,以及游戏中事物之间的关系。这称为领域建模,因为您是在描述游戏世界中的事物,而不是试图描述软件世界中的事物。如果有帮助,请写下几句话描述游戏世界,然后像我在上一段中所做的那样查找动词和名词。

现在你的一些名词会成为软件中的对象,一些会成为其他对象的属性。动词将是动作(即方法)。但无论哪种方式,如果您首先考虑要建模的内容,而不是直接跳到软件上,会更容易思考。

这一切如何协同工作?这 玩家按下一个键,移动 游戏中的角色。该视图将 正在处理输入,对吗?将 你把它发送给控制器,它 会检查一切(墙壁, 怪物等)在地图/其他 数组,然后返回结果?要么 你会把它发送给播放器吗? 将转到控制器,该控制器 会做所有的检查,并且 然后返回结果?

我喜欢遵循“告诉,不要问”的政策,即您命令对象做某事,而不是要求它为您提供信息以做出决定。这样,如果行为发生变化,您只需要修改被告知的对象。对于您的示例,这意味着 View 处理按键事件(之所以这样做是因为它们由 NSControl 处理),并且它告诉 Controller 该事件发生了。假设 View 收到一个“左箭头”按键,而 Controller 决定这意味着玩家应该向左移动。我只会告诉玩家“向左移动”,并让玩家理清向左移动意味着撞墙或怪物时会发生什么。

为了解释我为什么要这样做,假设您在游戏 1.1 中添加了玩家游泳的能力。现在播放器有一些ableToSwim 属性,所以您需要更改播放器。如果您告诉玩家向左移动,那么您更新玩家以了解在水上向左移动的含义,具体取决于他们是否会游泳。相反,如果控制器询问玩家向左移动并做出决定,那么控制器需要知道询问是否能够游泳,并且需要知道在水附近意味着什么。就像游戏中可能与玩家交互的任何其他控制器对象一样,iPhone 游戏中的控制器也是如此;-)。

【讨论】:

我喜欢它,谢谢。但是,这仍然没有告诉我它从哪里开始。控制器是如何启动的?从 awakefromnib 的观点看?我一定缺少一些东西。 好的,所以我使用 applicationDidFinishLaunching,但是如何?我是否像使用 awakefromnib 一样使用它? Re -applicationDidFinishLaunching:它实际上只是一个被 Cocoa 回调的函数。将其视为入口点,就像 main() 在 C 程序中一样,只是您在 XIB 中创建的所有对象都可以使用。 我明白了,但是如何使用呢?此外,在与一位编程朋友讨论之后,我决定放弃这个实现并继续我的混合方法可能会少得多麻烦,而不是只清理东西。如果你有时间,我真的很想和你谈谈。目标是未保存的文档。我感谢你迄今为止所做的一切。我已经接近真相了。 我正在为您花费这么多时间写这篇文章提供答案,但我真的对这个问题一无所知,这令人失望,但我会活下去。感谢您的宝贵时间。【参考方案3】:

您可能会发现这篇文章Introduction to MVC design using C# 很有用。虽然示例是用 C# 编写的,但原则应该适用。

【讨论】:

这仍然完全没有告诉我关于哪个开始一切。甚至如何开始一切。【参考方案4】:

您应该为整个游戏创建一个模型。它应该包含除 GUI 交互之外的游戏的所有内容。另一方面,视图包含所有 GUI 内容,而对游戏流程一无所知。

重点是模型和视图应该是可重用的。模型类应该使用任何 GUI(甚至可能是控​​制台或命令行)。视图类应该能够与其他外观相似的游戏一起使用。模型和视图应该完全解耦。

然后,控制器填补了空白。它对用户输入做出反应,要求模型类执行特​​定的游戏动作,并要求视图显示新情况。控制器预计不可重复使用。这是将游戏粘合在一起的粘合剂。控制器确保模型类和视图类保持独立和可重用。

另外,不要试图从一开始就让设计完美。不要犹豫,随时重构。一个糟糕的设计决策得到纠正的速度越快,它的恶行就越少。预先设计一切意味着根本不会纠正错误的设计决策,除非您预先做出完美的设计,即使拥有数十年的经验也是不可能的。

永远记住 X Window 系统的第三条设计规则:“唯一比从一个示例进行泛化更糟糕的是从根本没有示例进行泛化。”

【讨论】:

这就是我一直在尝试做的事情,但我一直对把东西放在哪里感到困惑。 顺便说一句,您不会有整个游戏的单一模型、视图或控制器。你将拥有一个怪物模型、怪物视图、怪物控制器……一切都应该按照面向对象的目的分解。您的怪物数组将成为其他模型中模型的一部分,例如关卡模型。 @emddudley:你通常有 one 模型,但它由多个类和/或函数组成。 :-) @Sneakyness:只要你发现你犯了一个错误就尝试并重构。不要试图做出完美的设计。设计必须在代码中进行尝试。如果你不克制重构,任何糟糕的设计决策都很容易纠正。 Vog,这是一款极其复杂的游戏,我试图在设计和编码上花费一两个多小时。我知道不要尝试做出完美的设计,但我想更好地了解如何布置。碎片落到位。没有人回答我关于这一切从哪里开始或如何开始的问题。【参考方案5】:

对于您的游戏,模型将包括角色的当前位置、健康点数以及其他涉及游戏“状态”的值。它会在发生变化时通知视图,以便视图可以自行更新。视图只是向用户显示所有内容所需的代码。控制器负责响应用户输入,并在必要时更新模型。

控制器是其中的核心组件,它应该实例化模型和视图。

当玩家按下一个键时,视图应该简单地将该命令传递给控制器​​,控制器决定做什么。

你错了,大多数人都不会设计。也许是大多数业余爱好者,或者也许是在网上提出最多问题的人,但不是任何人都在从事一个甚至有些复杂的项目。专业的程序员都具有软件设计经验;没有它,他们实际上将无法完成他们的工作(编写软件来做 X)。

您的游戏听起来很复杂,足以保证像 MVC 这样的架构模式。在某些情况下,一个软件足够简单,以至于 MVC 是多余的,并且不必要地使事情复杂化,但使用 MVC 的门槛相当低。

【讨论】:

是的,我选择为我的第一个游戏/程序编写 Roguelike。事实证明这是一个艰难的选择,但我个人认为这是正确的选择。

以上是关于以正确的方式实现模型-视图-控制器的主要内容,如果未能解决你的问题,请参考以下文章

模型视图控制器中表格视图的最佳实现

以正确的方式识别 UITabBarItems 视图控制器实例

以正确的方式分配视图控制器

使用 UIScrollView 和 AutoLayout 以编程方式创建控制器无法正确调整视图大小

以正确的方式从视图中调用控制器方法

使用视图控制器淘汰嵌套的可观察对象