1.4 深入学习HelloCocos2D项目
在完成了第一个HelloCocos2D项目后,如果读者不仅想看到飞机在屏幕上飞行,还想知道这一切是怎样实现的,我们不妨来一起探究其中的每一行代码。
1.4.1 初识场景和节点
要想理解HelloCocos2D这个项目,首先要了解场景(CCScene)、层(CCLayer)和节点(CCNode)的概念。
Cocos2D游戏是由不同的场景构成的,由导演(CCDirector)负责运行和切换各个场景。在Cocos2D中,CCDirector在任何一个时间点上只能运行一个场景。
所有节点的父类都是CCNode类,它包含了位置信息,但没有显示信息。它是所有其他节点类的父类,包括两个最基本的类:CCScene和CCLayer。
CCScene是一个抽象的概念,功能是根据像素坐标把物体放置在场景里相应的地方。所以任何Cocos2D场景都会用一个CCScene作为父对象。
CCLayer类本身并不做什么,它的功能是允许触摸和加速计的输入。因为大多数游戏会接受基本的触摸输入,所以CCLayer通常是第一个被加入CCScene的类。
Cocos2D的每个场景都包含一个或多个层,彼此叠加。
举例来说,通常我们刚进入游戏的时候都会看到一个初始菜单,接下来会进入游戏的主画面来战斗或完成任务,然后很快会因为种种原因看到GAMEOVER这个经典永恒的画面。而在不同的场景之中,可能会有很多的层(就像Photoshop的图层一样),每个层里又包含了很多Nodes(子节点,比如精灵、标签、菜单等),每个子节点也可能有其他的节点(比如一个精灵的里面可以存在一个子精灵)。
请记住,当需要在屏幕中添加飞机精灵的时候,要先创建一个新的精灵对象,然后把它添加成该层的子节点。
1.4.2 实现代码分析
现在逐步分析1.3节的代码是如何实现的。
1 . 主程序入口main.m
万物皆有起始,程序的运行也是。毫无疑问,任何一个使用Xcode开发的应用或游戏都是从main.m开始执行的。本例中的main.m在左侧的Supporting Files文件夹下,如图1-20所示。
main.m中的代码如代码清单1-3所示。
代码清单1-3 main.m中的代码
#import <UIKit/UIKit.h>
int main(int argc, char *argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
int retVal = UIApplicationMain(argc, argv, nil, @"AppController");
[pool release];
return retVal;
}
简单来说,主函数创建了一个NSAutoreleasePool(自动释放池),然后调用UIApplicationMain启动程序,该程序使用AppController类来执行UIApplicationDelegate的协议(protocol)。
上述代码使用了NSAutoreleasePool来管理内存。也就是说,通过给对象发送Autorelease(自动释放)消息,就不再需要担心忘记发送release消息。Autorelease Pool(自动释放池)可以确保内存中的自动释放对象最终会被释放。
注意 main.m文件的内容对于任何iOS应用或游戏都是一样的,无须对此做任何的修改。如果你看不懂这个文件,建议学习iPhone或者Objective-C开发的基础教程。
简而言之,程序启动了,接下来会调用AppController类。
在此之前,可以先看Supporting Files下的另一个文件Prefix.pch,这个头文件的作用是给编译过程加速。我们应该把不常变化的框架(Frameworks)头文件添加到前缀头文件(Prefix Header)中。这样,在编译的时候,框架的代码会被预先编译,所有的类都将可以使用这些头文件。
不过,这样做也有一个缺点:如果前缀头文件中一个头文件发生了变化,项目中的所有代码都将会重新编译。这就是为什么应该只添加那些极少或者从来都不变化的头文件到前缀头文件中。
我们可以将cocos2d.h头文件添加到前缀头文件中,因为它很少改变。不过,只有复杂一些的项目才会感受到编译速度的变化。
如果大家想了解更多关于如何使用前缀头文件减少编译时间的知识,可以参考苹果开发者文档。
2 . AppDelegate
每个iOS程序都有一个AppDelegate类,用于实现UIApplicationDelegate协议。AppDelegate在某些时间点通过从iOS接收信息来跟踪程序的状态变化。例如,可以用它确认是否有打进来的电话或者系统内存是否已经不足。在查看以上方法的具体实现代码前,先要了解AppDelegate.h中的类定义。在AppDelegate.h中定义了AppController这个类,它直接继承自NSObject,且遵循UIApplicationDelegate和CCDirectorDelegate两个协议。
程序开始运行后,收到的第一个消息就是applicationDidFinishLaunchingWithOptions,在这个方法中,可以放置项目的启动代码。Cocos2D就是在这里初始化的。
在Xcode中打开AppDelegate.m文件,让我们来详细查看并解释其中的代码细节。
先说一下在Xcode4中切换方法的技巧,如图1-21所示。单击最右侧的“No Selection”处,就可以在不同方法间跳转。这跟Xcode3.2.6是不同的,用起来一样方便。
现在切换到applicationDidFinishLaunchingWithOptions方法,如代码清单1-4所示。
代码清单1-4 applicationDidFinishLaunchingWithOptions方法
- (BOOL) application:(UIApplication)application didFinishLaunchingWithOptions:(NSDictionary )launchOptions
{
// 1.Create the main window
window_ = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
//2.Create an CCGLView with a RGB565 color buffer, and a depth buffer of 0-bits
CCGLView *glView = [CCGLView viewWithFrame:[window_ bounds]
pixelFormat:kEAGLColorFormatRGB565 //kEAGLColorFormatRGBA8
depthFormat:0 //GL_DEPTH_COMPONENT24_OES
preserveBackbuffer:NO
sharegroup:nil
multiSampling:NO
numberOfSamples:0];
director_ = (CCDirectorIOS*) [CCDirector sharedDirector];
director_.wantsFullScreenLayout = YES;
//3.Display FSP and SPF
[director_ setDisplayStats:YES];
// 4.set FPS at 60
[director_ setAnimationInterval:1.0/60];
// 5.attach the openglView to the director_
[director_ setView:glView];
// 6.for rotation and other messages
[director_ setDelegate:self];
// 7.2D projection
[director_ setProjection:kCCDirectorProjection2D];
// [director setProjection:kCCDirectorProjection3D];
// 8.Enables High Res mode (Retina Display) on iPhone 4 and maintains low res on all other devices
if( ! [director_ enableRetinaDisplay:YES] )
CCLOG(@"Retina Display Not supported");
// 9.Default texture format for PNG/BMP/TIFF/JPEG/GIF images
// It can be RGBA8888, RGBA4444, RGB5_A1, RGB565
// You can change anytime
[CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGBA8888];
// 10.If the 1st suffix is not found and if fallback is enabled then fallback //suffixes are going to searched. If none is found, it will try with the name //without //suffix
// On iPad HD : "-ipadhd", "-ipad", "-hd"
// On iPad : "-ipad", "-hd"
// On iPhone HD: "-hd"
CCFileUtils *sharedFileUtils = [CCFileUtils sharedFileUtils];
[sharedFileUtils setEnableFallbackSuffixes:NO];
// Default: NO. No fallback suffixes are going to be used
[sharedFileUtils setiPhoneRetinaDisplaySuffix:@"-hd"];
// Default on iPhone RetinaDisplay is "-hd"
[sharedFileUtils setiPadSuffix:@"-ipad"];
// Default on iPad is "ipad"
[sharedFileUtilssetiPadRetinaDisplaySuffix:@"-ipadhd"]; // Default on iPad RetinaDisplay is "-ipadhd"
// 11.Assume that PVR images have premultiplied alpha
[CCTexture2D PVRImagesHavePremultipliedAlpha:YES];
// 12. and add the scene to the stack. The director will run it when it //automatically when the view is displayed
[director_ pushScene: [IntroLayer scene]];
// 13.Create a Navigation Controller with the Director
navController_ = [[UINavigationController alloc] initWithRootViewController:director_];
navController_.navigationBarHidden = YES;
// 14.set the Navigation Controller as the root view controller
[window_ setRootViewController:navController_];
//15. make main window visible
[window_ makeKeyAndVisible];
return YES;
}
以上的代码中,第一步就是对UIWindow进行初始化,并绑定CCGLView。UIWindow被设置为全屏,而CCGLView则会接收所有的OpenGL ES命令调用。接下来,我们按照注释的编号来逐行解释。
1)创建程序的主窗口。
2)创建CCGLView对象,用于游戏中的渲染。在Cocos2D中,使用CCGLView将OpenGL ES命令发送给OpenGL ES驱动。从Cocos2D v2.0之后,CCGLView替代了之前的EAGLView,以使命名更加规范。
3)将FPS(Frames Per Second)和SPF(Second Per Frame)显示设置为开。Cocos2D会自动计算游戏的当前帧数,并显示在屏幕的左下角。在调试过程中FPS显示对我们会非常有用,当然在正式上传之前可以将其设置为NO(即不显示)。
4)将动画间距设置为每秒60次,这也是Cocos2D中的默认设置。通常情况下,Cocos2D每秒会更新屏幕中的显示60次(最高的刷新频率)。
5)将openglView绑定到director_导演对象上。
6)将director_的代理对象设置为自身,以便获取屏幕旋转等信息。
7)设置2D或3D投射。
8)启用Retina高清显示模式。
9)设置Cocos2D的纹理格式。
10)在iPad或Retina高清显示模式下,使用CCFileUtils自动添加图片的后缀。
11)设置PVR图像具备多重透明度。
12)初始化IntroLayer场景,并使用pushScene方法切换到该场景。
13)使用director_创建Navigation Controller。
14)将此导航控制器设置为根视图控制器。
15)设置主窗口可见。
在Cocos2D中,CCDirector负责游戏的循环运行,并且在游戏中渲染所有的图像。因此,它会掌控游戏的运行、暂停或停止。代码清单1-5中显示了使用CCDirector响应iPhone操作系统的各个事件,包括暂停或继续。
代码清单1-5 调用CCDirector
- (void)applicationWillResignActive:(UIApplication *)application {
if( [navController_ visibleViewController] == director_ )
[director_ pause];//1
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
if( [navController_ visibleViewController] == director_ )
[director_ resume];//2
}
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
[[CCDirector sharedDirector] purgeCachedData];//3
}
-(void) applicationDidEnterBackground:(UIApplication*)application {
if( [navController_ visibleViewController] == director_ )
[director_ stopAnimation];//4
}
-(void) applicationWillEnterForeground:(UIApplication*)application {
if( [navController_ visibleViewController] == director_ )
[director_ startAnimation];
}//5
- (void)applicationWillTerminate:(UIApplication *)application {
CC_DIRECTOR_END();} //6
- (void)applicationSignificantTimeChange:(UIApplication *)application {
[[CCDirector sharedDirector] setNextDeltaTimeZero:YES];
} //7
我们还是按照注释编号来依次解释每个方法的作用。
1)当操作系统暂停应用的执行时,调用此方法来暂停游戏和所有的计时器。当玩家在玩游戏的过程中锁定了iPad或iPhone,或是有电话打进来,或其他类似事件发生需要强迫游戏进入后台时,会调用此方法。
2)与1)中的情况相反,通常当玩家解锁iPad或iPhone,或电话已接听完毕,会调用此方法来继续游戏和所有的计时器。
3)当系统收到内存不足的警告时,会调用此方法从内存中清除未在屏幕中显示的纹理图。
注意 所有的图像文件(PNG或PVR格式)都被加载成GPU可以理解的OpenGL ES纹理,而精灵则对应着这些纹理图。Cocos2D内置了一个纹理缓存管理器来保存这些纹理图,这样可以极大地加速创建新精灵并充分利用已有的纹理图。不利的一面是,如果收到内存警报,Cocos2D会将当前未使用的纹理图全部从内存中清除。因此,当游戏的场景切换时,有时需要手动释放当前的层和场景。
4)在iOS4.0及以后的版本中,支持应用的后台运行。在这种情况下,会停止运行屏幕中的动画。
5)与4)中的情况相反,应用重新回到前台来运行。这种情况下会重新启动屏幕中的动画。
6)玩家完全退出应用且不在后台运行时,将调用此方法。该方法会终止CCDirector的控制,并从应用程序的UIWindow中解除对EAGLView的绑定。同时还将结束游戏循环,从内存中清除所有的纹理和计时器。
7)将上一次时间调用和当前事件调用间的增量时间设置为0。该方法的调用场景是:两次时间调用之间已经过了太长的时间。这通常是由iPhone重新调整了系统时间而导致的。
在代码清单1-4中,最重要的代码是applicationDidFinishLaunching方法中的如下代码:
[director_ pushScene: [IntroLayerscene]];
应用在启动AppDelegate后,创建一个CCDirector导演类对象,然后运行IntroLayer场景。接下来我们看看IntroLayer的实现。
3 . IntroLayer
首先,IntroLayer继承自CCLayer,而CCDirector只能运行CCScene对象,所以,需要实现IntroLayer的scene方法,它会把IntroLayer当作唯一的子节点添加进去,并返回一个CCScene,具体如代码清单1-6所示。
代码清单1-6 IntroLayer的scene方法
// Helper class method that creates a Scene with the IntroLayer as the only child
+(CCScene *) scene
{
// 'scene' is an autorelease object.
CCScene *scene = [CCScene node];
// 'layer' is an autorelease object.
IntroLayer *layer = [IntroLayer node];
// add layer as a child to scene
[scene addChild: layer];
// return the scene
return scene;
}
IntroLayer在以前的Cocos2D版本中是不存在的,这是Cocos2D v2.0模板新加的类,它的主要目的就是在进入游戏之前,可以显示游戏制作公司的Logo等信息。显示Logo的实现代码如代码清单1-7所示。
代码清单1-7 IntroLayer的onEnter方法
-(void) onEnter
{
[super onEnter];
// 1
CGSize size = [[CCDirector sharedDirector] winSize];
CCSprite *background;
//2
if( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ) {
background = [CCSprite spriteWithFile:@"Default.png"];
background.rotation = 90;
} else {
background = [CCSprite spriteWithFile:@"Default-Landscape~ipad.png"];
}
//3
background.position = ccp(size.width/2, size.height/2);
// 4
[self addChild: background];
// 5
[self scheduleOnce:@selector(makeTransition:) delay:1];
}
我们仍然按照注释编号来依次解释每个方法的作用。
1)获得设备支持的窗口大小。
2)判断设备是iPhone还是iPad,并据此来加载不同的背景资源图片。
3)设置背景图片的位置。
4)把背景图片添加到IntroLayer中。
5)触发一个定时器方法调用,在1秒后会调用makeTransition函数。
最后,我们看看makeTransition方法的实现,如下所示。
-(void) makeTransition:(ccTime)dt
{
[[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:1.0 scene:[HelloWorldLayer scene] withColor:ccWHITE]];
}
这个方法只有一句代码调用,就是使用CCDirector配合CCTransitionFade场景切换特效类,把HelloWorldLayer类在1秒屏幕变白后展示出来。最后,我们看看HelloWordLayer类的实现。
4 . HelloWorldLayer
HelloWorldLayer类继承自CCLayer。因为CCScene只是一个抽象的概念,默认设置场景的方法是在类里使用一个静态初始化方法+(id)scene。此方法会生成一个CCScene对象,并且将HelloWorldLayer的对象添加为场景的节点。几乎在任何情况下,CCScene都是在这里创建和使用的。
+(id)scene类方法的代码如代码清单1-8所示。
代码清单1-8 +(id)scene类方法
+(CCScene *) scene
{
// 'scene' is an autorelease object
CCScene *scene = [CCScene node];
// 'layer' is an autorelease object
HelloWorldLayer *layer = [HelloWorldLayer node];
// add layer as a child to scene
[scene addChild: layer];
// return the scene
return scene;
}
第一行代码是通过调用[CCScene node]来创建一个CCScene的实例化对象,[CCScene node]其实等同于[[[CCScene alloc]init]autorelease];接下来,创建了一个HelloWorldLayer的层节点;最后,将该层节点添加到场景中,并返回场景给调用者。
了解了+(id)scene类方法的作用,我们再来看-(id)init初始化方法的作用,其代码如代码清单1-9所示。
代码清单1-9 -(id)init初始化方法
-(id) init
{
// always call "super" init
// Apple recommends to re-assign "self" with the "super" return value
if( (self=[super init])) {
//采用静态初始化方法生成并初始化标签对象。按住option,单击labelWithString,你会了解更多的知识
CCLabelTTF *label = [CCLabelTTF labelWithString:@"Hello World" fontName:@"Marker Felt" fontSize:64];
//实例化一个CCDirector的单例,从而从CCDirector得到窗口的尺寸
CGSize size = [[CCDirector sharedDirector] winSize];
//将标签放在屏幕中央,注意ccp其实是Cocos2D对于CGPointMake的宏定义,类似的宏定义还有很多
label.position = ccp( size.width /2 , size.height/2 );
//将标签作为子节点添加到场景层中,这里应该注意,很多初学者会忘记这一点,然后发现精灵对象并未出//现在屏幕中。此外,可以在调用addChild之前或之后赋予位置信息
[self addChild: label];
//生成并初始化精灵对象
CCSprite *plane = [CCSprite spriteWithFile:@"plane.png"];
//将精灵放在屏幕中的特定位置
plane.position = ccp(size.width/2,size.height*0.7);
//将精灵作为子节点添加到场景层中
[self addChild:plane];
//定义一个moveAction动作,在3秒内移动到屏幕的左侧外
id flyAction = [CCMoveTo actionWithDuration:3.0f position:ccp(size.width+100,size.height*0.7)];
//让飞机执行这个动作
[plane runAction:flyAction];
}
//后面是初始化GameCenter的成就榜和高分榜的代码,此处先略去不讲
return self;
}
代码清单1-9中的代码注释基本上解释了每行代码的具体作用。此外,其中有些看似比较奇怪的地方,如在self = [super init]调用中,发送给super对象的init消息将返回的值赋给了self。如果大家之前有C++的编程经验,可能会对此很不理解。这是因为在Objective-C中,必须手动调用super类的init方法,不存在对父类的自动调用;而且必须把[super init]的返回值赋给self,因为有可能会得到一个空值(nil)。
[super init]的另一种写法如下,作用和上面的写法完全一样:
-(id) init {
self = [super init];
if (self != nil) { //在此添加init方法的代码
}
return self;
}
最后还有一个-(void)dealloc方法,它的作用和一般的iOS应用无异,用于销毁前面所创建的对象。
以上就是HelloCocos2D项目的详细实现过程。考虑到大家之前大多有过iOS开发的经验,这里并没有对所有类的所有实现代码进行逐行解释和分析,但通过以上三步,我们初步了解了一个Cocos2D应用的启动和运行过程,为以后开发更复杂的应用或游戏打下了坚实的基础。