cocos2d里面如何实现MVC(1)
前言
众所周知,MVC如今极为流行,在网络上随便搜索一下,到处都能看到它的身影。最初,MVC在J2EE领域崭露头角,随后在Rails框架中得到广泛应用,接着.NET也引入了这一模式,在iOS开发中更是强制要求使用MVC。然而,我们所编写的程序,是否真正完全符合MVC模式呢?这很难一概而论,取决于个人对MVC的理解程度。
由于MVC实在太过火爆,Cocos2d社区里也展开了关于如何在Cocos2d中实现MVC的热烈讨论,支持者与反对者皆有。今天,就让我们一同探究Cocos2d中的MVC,看看它是否真的实用。
什么是MVC
Model-View-Controller (MVC) 在Web应用开发中十分流行,它是一种组合设计模式,目前广泛应用于带有图形交互用户界面的程序开发。像Ruby On Rails、Django和ASP.NET MVC等不同语言平台的Web开发框架,都遵循同样的原则——将用户表示层和逻辑层分离。
关注点分离(SoC)是现代软件工程方法中一个非常重要的设计理念。该理念强调,在遇到实际问题时,要划分不同的关注点并将它们隔离开来,这样能提高代码的重用度,进而获得更好的鲁棒性、可适配性和可维护性。这些软件属性对于软件质量来说至关重要。
Cocos2d本身并非基于MVC理念设计,但这并不妨碍我们在游戏开发中使用MVC。实现方式多种多样,在这篇博文中,我将分享自己在Cocos2d中实现MVC的方法,并在最后编写一个使用Cocos2d + MVC的简单游戏demo。
现有问题
在Cocos2d中,CCSprite、CCLayer、CCScene等类都是CCNode的子类。通常,使用Cocos2d开发游戏时,会按照以下步骤实现游戏逻辑:
- 通过应用程序代理类(AppDelegate)初始化第一个CCScene。
- 在CCScene中实例化一个或多个CCLayer,并将它们作为子节点添加进去。
- 在CCLayer中实例化一个或多个CCSprite,同样调用addChild方法添加。
- CCScene处理用户输入(如触摸事件和加速计变化),同时更新CCLayer和CCSprite的属性,例如更改CCSprite的位置、让其运行一个或多个动作等。
- CCScene运行一个游戏循环(通常每1/60秒更新一次),CCLayer和CCSprite在这个游戏循环中进行更新和执行游戏逻辑。
这个过程看似简单,能快速开发出游戏,这也是Cocos2d如此流行的原因。然而,随着游戏逻辑变得越来越复杂,代码的维护难度也会急剧增加。其中最突出的问题是,CCScene类承担的职责过多,它既要处理用户交互,又要负责游戏逻辑(逻辑层)和画面显示(表示层)。根据SoC原则,这种设计显然不合理,我们应该将职责分离,让一个类只负责一件事情,这样代码才更易于维护。
模型(Model)
MVC将一个系统划分为以下几个组件:
- Model:负责与领域相关的逻辑处理代码,可视为逻辑层或领域层。
- View:仅负责界面显示。
- Controller:负责处理用户交互。
下面让我们从Model开始介绍。在我正在制作的一个平台游戏中,Model包含以下部分类(仅为一部分):
Player类
- 属性:包含玩家的位置、当前速度(x轴速度、y轴速度)等。
- 处理逻辑:包含与玩家相关的处理逻辑,如跑步、行走、跳跃等。
- 更新方法:包含一个update方法,该方法会在游戏主循环的每一帧刷新时被调用,主要负责更新玩家的Model。
Platform类
- 属性:包含平台的位置、宽度、高度等。
- 处理逻辑:包含与平台相关的处理逻辑,如倾塌等。
- 更新方法:包含一个update方法,该方法会在游戏主循环的每一帧刷新时被调用,主要负责更新平台的Model。
GameModel类
- 属性:包含一些游戏世界的属性,如重力等。
- 方法:包含一些执行游戏逻辑的方法。
- 更新方法:包含一个update方法,该方法会在每一帧刷新时被游戏循环调用,用于更新自身状态,并触发游戏世界中其他对象相应地更新状态。
你可能会问,有些属性(如位置、宽度、高度等)可以直接从CCSprite中获取,为何还要在Model中重复定义?其实,这种说法有对有错。说对是因为这些属性确实相似,可以直接使用;说不对是因为Model可能使用不同的计量单位,如米,而非像素(像Box2D就不是使用像素作为单位)。在我的Model中,使用的是米,你也可以使用英尺或其他单位。渲染引擎对于Model来说是透明的,Model无需关心。
视图(View)
根据MVC原则,View只负责界面显示,在Cocos2d中实现MVC时,它是相对简单的一部分。如果有一个Model,可以使用CCLayer,并添加一些CCSprite或其他Cocos2d类来处理显示问题。将Model和View分离的好处是,无需将Model的属性直接映射到View的属性上。例如,玩家在x轴方向上移动,但你希望它始终距离屏幕左边10px,此时可以移动CCLayer,而不是直接移动Sprite。
在显示Model对象时,需要考虑单位问题。如果使用米作为计量单位,在渲染时必须将其转换为像素(可以像Box2D一样定义一个PTM_RATIO)。那么,Model如何与View交互呢?可以从Controller中获取View,或者将GameModel制作成单例,使用静态方法进行处理。
控制器(Controller)
Controller负责将View和Model联系起来,其主要职责是处理用户输入。由于需要实例化Model和View,在Controller中完成这些操作非常合适。我将Controller类继承自CCScene类,并创建一个初始的Controller类,由AppDelegate进行实例化。
然而,这里存在一个问题:触摸事件由CCLayer处理,而在我的设计中,CCLayer的角色是View,我不希望View处理用户输入。因此,需要通过委托(delegate)将View的引用传递给Controller,然后通过委托执行Controller的触摸事件处理代码,以此处理View中的触摸事件。
这样,Controller类就能处理来自View的用户事件了。它可以根据用户输入操作Model,通过修改Model的属性或调用Model的方法来实现。在更新完Model后,View也需要得到通知并进行更新。所有这些操作都在游戏循环中完成,实际上游戏循环就是一个Controller。Controller的职责只是调用View的update方法,后续的更新操作则由View完成。