用helloworld讲解cocos2d-x的编程思路与要点
本文以cocos2d-x的helloworld为例,讲解cocos2d-x引擎的特点和要点,2.2为了展示新功能,把包括屏幕自适应在内的新特性相关代码加入了helloworld工程代码里,但是也增加新人的上手难度,我会避过不谈,只说关键的几句代码,对于已经了解cocos2d-x架构的朋友,本文后面的内容对你毫无帮助,可以去关注我写的《cocos2d-x提高篇》(不过此刻我或许还没写)。当然了,不可能一开始就把所有内容说清楚,刚上手的朋友要想做游戏抑或是写例子,首先要理解下两个要点。
要点一:
关于绘制。许多朋友在刚接触cocos2d-x的时候,迫不及待去寻找绘制函数,问我有没有类似drawimage()抑或是drawstring()这样的函数。这里要多说几句,cocos2d-x底层已经对绘制进行了简单的封装,我们并不需要显性的去写drawImage()这样的函数。
对于这点该如何理解呢?譬如说我们要做一个游戏,往往会把逻辑层和绘制层分开。在逻辑层,可能有很多关卡,但是当前玩家只有一个关卡,关卡里有地图也有许多敌兵。我们在逻辑层进行逻辑运算,更改敌兵的状态和坐标。之后再在绘制层把屏幕里的敌兵以及地图的可见区域画到屏幕上。这就是经典的(逻辑-绘制-逻辑-绘制)结构。
而对于刚上手的朋友可以这么理解,cocos2d-x已经把绘制的代码写好了,我们只需要在逻辑层设置。在cocos2d-x里,有scene(场景)的概念,当前scene只有一个,scene里有很多ccsprite(精灵),一个ccsprte有x,y,引用图片这些属性,Cocos2d-x会不断的把当前scene里的可见精灵绘制到屏幕上。因此,如果我们要在cocos2d-x里绘制一张背景图,首先创建一个scene,然后创建一个精灵,以某种形式将其添加到scene里,设置好引用的图片和精灵位置,最后记得将scene设置当前场景就ok了。下面看代码。
先看main.cpp
#include"main.h"
#include"../Classes/AppDelegate.h"
#include"CCEGLView.h"
USING_NS_CC;
intAPIENTRY_tWinMain(HINSTANCEhInstance,
HINSTANCEhPrevInstance,
LPTSTRlpCmdLine,
intnCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
AppDelegateapp;
CCEGLView*eglView=CCEGLView::sharedOpenGLView();
eglView->setViewName("HelloCpp");
eglView->setFrameSize(2048,1536);
eglView->setFrameZoomFactor(0.4f);
returnCCApplication::sharedApplication()->run();
}
这里我只要记住eglview->setFrameSize(w,h)函数作用是设置视图大小
vs里的main.cpp文件是win32平台下程序的入口,这里会创建一个窗口,大小为(2048,1536)
setFrameZoomFactor(0.4f)是将窗口缩小为所设大小的0.4倍,为了照顾我这样的小屏幕用户,不过这样还是很蛋疼
所以我把窗口设置为480X320,屏蔽了缩放窗口那句话
再看AppDelegate.h
#ifndef_APP_DELEGATE_H_
#define_APP_DELEGATE_H_
#include"cocos2d.h"
classAppDelegate:privatecocos2d::CCApplication
{
public:
AppDelegate();
virtual~AppDelegate();
virtualboolapplicationDidFinishLaunching();
virtualvoidapplicationDidEnterBackground();
virtualvoidapplicationWillEnterForeground();
};
#endif//_APP_DELEGATE_H_
AppDelegate是程序的控制类,里面有三个函数(applicationDidFinishLaunching,applicationDidEnterBackground,applicationWillEnterForeground)
分别在程序启动后,程序挂起进入后台,程序从后台返回时执行,定义需要我们自己实现。
看看AppDelegate.cpp里我们只看applicationDidFinishLaunching()是如何实现的
boolAppDelegate::applicationDidFinishLaunching(){
CCDirector*pDirector=CCDirector::sharedDirector();
CCEGLView*pEGLView=CCEGLView::sharedOpenGLView();
pDirector->setOpenGLView(pEGLView);
CCSizeframeSize=pEGLView->getFrameSize();
#if(CC_TARGET_PLATFORM==CC_PLATFORM_WINRT)||(CC_TARGET_PLATFORM==CC_PLATFORM_WP8)
pEGLView->setDesignResolutionSize(designResolutionSize.width,designResolutionSize.height,kResolutionShowAll);
#else
pEGLView->setDesignResolutionSize(designResolutionSize.width,designResolutionSize.height,kResolutionNoBorder);
#endif
vector<string>searchPath;
if(frameSize.height>mediumResource.size.height)
{
searchPath.push_back(largeResource.directory);
pDirector->setContentScaleFactor(MIN(largeResource.size.height/designResolutionSize.height,largeResource.size.width/designResolutionSize.width));
}
elseif(frameSize.height>smallResource.size.height)
{
searchPath.push_back(mediumResource.directory);
pDirector->setContentScaleFactor(MIN(mediumResource.size.height/designResolutionSize.height,mediumResource.size.width/designResolutionSize.width));
}
else
{
searchPath.push_back(smallResource.directory);
pDirector->setContentScaleFactor(MIN(smallResource.size.height/designResolutionSize.height,smallResource.size.width/designResolutionSize.width));
}
CCFileUtils::sharedFileUtils()->setSearchPaths(searchPath);
pDirector->setDisplayStats(true);
pDirector->setAnimationInterval(1.0/60);
CCScene*pScene=HelloWorld::scene();
pDirector->runWithScene(pScene);
returntrue;
}
前面说过cocos2d-x2.2加入了很多新特性相关的代码在helloworld例子里,这里面的一大段都是和屏幕自适应相关的,因此可以跳过。
关键代码为
开头的
CCDirector*pDirector=CCDirector::sharedDirector();
CCDirector是游戏管理类,他的实例全局一份,通过CCDirector::sharedDirector()这个静态函数取得,通过它可以取得一些游戏设置信息,也可以通过它控制当前运行哪个场景。
再看函数结尾部分
pDirector->setDisplayStats(true);
这句代码设置在屏幕上显示游戏运行的帧数,如果掉帧会立即看见
当前帧数60帧,每次渲染次数为3
pDirector->setAnimationInterval(1.0/60);
这句设置游戏每帧间隔1/60秒
CCScene*pScene=HelloWorld::scene();
pDirector->runWithScene(pScene);
最后2句话是调用HelloWorld::scene函数创建一个ccscene,然后运行这个场景(前面我说过的),之后scene里的精灵就会被绘制到屏幕上了。
第一次运行场景调用runWithScene(pScene),如果是从一个场景切换到另一个场景则要调用replaceScene(pScene);
我们的ccscene是调用HelloWorld::scene这个静态函数创建出来的,这个场景里有一张背景图一个文字label以及一个按钮
在看HelloWorldScene类之前,我们先普及另外的要点知识
要点2:
1.layer:在scene和sprite之间还有一个layer(层)的概念。一般来说,我们要创建一个或多个层,把层添加场景里,再把精灵放进层里。有了用层管理精灵
的概念后,我们在做游戏的时候就可以把一个场景分成很多层,譬如说地图层和对象层,还可以有特效层(子弹层)和ui层。层和层之间可以设置遮挡关系
,先绘制哪一层后绘制哪一层,也可以通过设置某一层的显示或不显示来屏蔽掉一层。层和层之间也可以是包含关系(父子关系),譬如说一个地图层,我们可
以分成四个小层,这四个小层分别被添加了春夏秋冬四种风格的精灵构成四种景色的地图。然后根据需要只让其中一层显示,就实现了一个场景里季节变换的效果。
同时由于layer这个类继承了cocos2d-x的触摸接口类,我们可以实现某一层的手指触摸行为。
2.node结构:CCNode(节点)是cocos2d-x里的一个比较底层的基类,我们常用的CCScene,CCLayer,CCSprite类都继承了CCNode。CCNode具有容器功能
(有个children成员,是个队列),每个ccnode都可以包含别的CCNode,通过addchild(m_other_node)函数把别的节点加为自己的子节点,这种关系成为父子关系。
上级的node称为parent(父亲),下级的node成为child。正是由于ccnode的这种节点特性,我们才可以把layer加入scene,把sprite加入layer。形成一个树形结构。
3.内存管理(引用计数器和代码风格):这一段如看不懂,直接跳过。cocos2d-x的retain和release机制(引用计数器机制)引自objective-c。在java和c#这些语言里,当一个对象不被任何人引用时,所占
内存会自动释放,在c++里程序员new出来的对象,需要自己delete。而在cocos2d-x(objective-c)里。我们创建出来的大多数对象都继承CCObject这个类,它有一个计数器。一般来说
new出来的对象,计数器为1。如果我们对其使用retain,计数器就会加1.如果我们对其使用release,计数器就会-1,当计数器为0时,对象就会被析构。和reatinrelease并行的还有一个auotorelease
的概念,如果我们将一个ccobject设为autorelease,它被被加到一个池里,当它计数器为1时它也会在某个时间段被自动释放。那么如果我们得到一个ccobject,我们怎么知道它的计数器为几,又是不是
auotorelease的呢?这里有个语法规范,只要不是通过new出来的对象,而是通过其他接口得到的(譬如helloworld::scene()),它就应该是autorelease的,并且对我们来说它的计数器为1.应为就算它
被外部retain过,它也必然会在某个时间被外部release(成对出现,保证内存不泄露),因此我们如果只有还需要使用这个对象,并且不能保证在这之前,外部都不将其release,我们就应该手动将其retain
并且在不用的时候将其release。譬如说CCDirector在切换到某个场景时,就会将其retain,确保其不会被释放,在下次切换时,将其release,这样就不会导致该场景永远不被释放(release只是表示我们不需要
这个对象了,不代表此时该对象就会被释放,因为别处可能对其retain了,还需要这个对象)。
基于这种前提,我们一般写的类,都不会把构造函数暴露,让别处直接newClassX(),而是封装一个静态函数create()确保别人能得到一个auotorelease的对它来说计数器为1的实例对象。
在create函数里,我们new出一个对象,执行初始化函数init(),并将其设为autorelease,将其返回。旧版本的cocos2d-x为了和cocos2d(objective-c版本)接口一致,采用的是类名()写法。
例如CCNode::Node()这样的写法,已经被新版抛弃,因为不适合c++。
下面看HelloWorldScene代码,先看头文件
#ifndef__HELLOWORLD_SCENE_H__
#define__HELLOWORLD_SCENE_H__
#include"cocos2d.h"
classHelloWorld:publiccocos2d::CCLayer
{
public:
virtualboolinit();
staticcocos2d::CCScene*scene();
voidmenuCloseCallback(CCObject*pSender);
CREATE_FUNC(HelloWorld);
};
#endif//__HELLOWORLD_SCENE_H__
HelloWorld是一个继承了CCLayer的类,里面有个init函数是初始化函数,有个静态函数scene会返回一个scene,这个scene里被添加了一个cclayer。
我们说了HelloWorldlayer里有一张北京,一个文字label,一个按钮,menuCloseCallback就是点击按钮后的回调函数。
create函数跑哪去了?CREATE_FUNC(HelloWorld),这是一个宏,由于大多数的类都有create静态函数,内容都是new一个对象指针,将其init(),设为auotorelease再返回,写起来很烦
代码也会变长,所以cocos2d-x给我们写好了不少这样的宏。翻译过来就是
staticHelloWorld*create()
{
HelloWorld*pRet=newHelloWorld();
if(pRet&&pRet->init())
{
pRet->autorelease();
returnpRet;
}
else
{
deletepRet;
pRet=NULL;
returnNULL;
}
}
节省了不少行代码,不过我一般懒得用宏,这么几行代码,其实顺手就写掉了。
由此可见,主要初始化内容都是写在init()函数里。那么去看cpp文件吧
#include"HelloWorldScene.h"
#include"AppMacros.h"
USING_NS_CC;
CCScene*HelloWorld::scene()
{
CCScene*scene=CCScene::create();
HelloWorld*layer=HelloWorld::create();
scene->addChild(layer);
returnscene;
}
boolHelloWorld::init()
{
if(!CCLayer::init())
{
returnfalse;
}
CCSizevisibleSize=CCDirector::sharedDirector()->getVisibleSize();
CCPointorigin=CCDirector::sharedDirector()->getVisibleOrigin();
CCMenuItemImage*pCloseItem=CCMenuItemImage::create(
"CloseNormal.png",
"CloseSelected.png",
this,
menu_selector(HelloWorld::menuCloseCallback));
pCloseItem->setPosition(ccp(origin.x+visibleSize.width-pCloseItem->getContentSize().width/2,
origin.y+pCloseItem->getContentSize().height/2));
CCMenu*pMenu=CCMenu::create(pCloseItem,NULL);
pMenu->setPosition(CCPointZero);
this->addChild(pMenu,1);
CCLabelTTF*pLabel=CCLabelTTF::create("HelloWorld","Arial",TITLE_FONT_SIZE);
pLabel->setPosition(ccp(origin.x+visibleSize.width/2,
origin.y+visibleSize.height-pLabel->getContentSize().height));
this->addChild(pLabel,1);
CCSprite*pSprite=CCSprite::create("HelloWorld.png");
pSprite->setPosition(ccp(visibleSize.width/2+origin.x,visibleSize.height/2+origin.y));
this->addChild(pSprite,0);
returntrue;
}
voidHelloWorld::menuCloseCallback(CCObject*pSender)
{
#if(CC_TARGET_PLATFORM==CC_PLATFORM_WINRT)||(CC_TARGET_PLATFORM==CC_PLATFORM_WP8)
CCMessageBox("Youpressedtheclosebutton.WindowsStoreAppsdonotimplementaclosebutton.","Alert");
#else
CCDirector::sharedDirector()->end();
#if(CC_TARGET_PLATFORM==CC_PLATFORM_IOS)
exit(0);
#endif
#endif
}
静态函数scene()返回一个scene,里面包含了一个HelloWorld(这是一个层)。
在创建HelloWorld时,是调用create函数得到的,create()函数调用了init()函数,下面看init()函数
首先执行父类的初始化函数,然后拿到屏幕大小
CCSizevisibleSize=CCDirector::sharedDirector()->getVisibleSize();
CCPointorigin=CCDirector::sharedDirector()->getVisibleOrigin();
旧版本的例子是直接拿的getwinSize()。这里说明一下,2.2版本给大家展示了屏幕自适应,也就是我们之前在appdelegate里没有说明的那一段,由于设置了屏幕自适应的关系,
虽然我们在main函数里设置了视图大小,但是并不是说整个视图都能绘制到屏幕上,可能只有中间部分绘制出来,多出去的部分就看不到了,getVisibleSize()函数就是拿到可绘制
出的部分的大小,getVisibleOrigin()则是可绘制部分左下角点相对于视图左下角点的坐标(关于自适应我之后的博文会仔细说)。
hellowWorld的例子是将视图等比放大到无黑边,这样就可能导致上下抑或左右有部分显示不出来,而拿到可显示部分的起始坐标和长宽就可以把精灵定位到屏幕的
左下(origin.x,origin.x+visibleSize.height/2),
右下(origin.x+visibleSize.width,origin.x+visibleSize.height/2),
抑或是中间(origin.x+visibleSize.width/2,origin.x+visibleSize.height/2)
CCMenuItemImage*pCloseItem=CCMenuItemImage::create(
"CloseNormal.png",
"CloseSelected.png",
this,
menu_selector(HelloWorld::menuCloseCallback));
pCloseItem->setPosition(ccp(origin.x+visibleSize.width-pCloseItem->getContentSize().width/2,
origin.y+pCloseItem->getContentSize().height/2));
CCMenu*pMenu=CCMenu::create(pCloseItem,NULL);
pMenu->setPosition(CCPointZero);
this->addChild(pMenu,1);
CCMenuItemImage这个类菜单原件的一种(是个由2张图片精灵组成的按钮),它的create函数有四个参数,前个参数是图片名,分别是点击前要显示的图片,可以点击时
显示的图片。之后的2个参数分别是点击后要执行回调的对象和回调函数的指针,menu_selector(HelloWorld::menuCloseCallback)这种写法是模仿objective-c里的selector,
menu_selector是个宏,内容其实就是把函数指针强转为CCObject的函数指针,如果看不懂也没关系,照着写就行了。
pCloseItem->setPosition(ccp(origin.x+visibleSize.width-pCloseItem->getContentSize().width/2,origin.y+pCloseItem->getContentSize().height/2));
setPosition(x,y)函数是设置Node的坐标
而getContentSize()会返回Node的大小
这里把pCloseItem放在了屏幕右下角
但是由于CCMenuItemImage没有实现cocos2d-x触摸接口,因此要把它塞进一个CCMenu里,才能点击。
CCMenu是菜单容器类,在cocos2d-x2.2里它继承了layer(之前说过layer继承了触摸接口),因此具有触摸行为,相当于一个菜单层。把菜单原件塞进CCMenu里,再把CCMenu
塞进layer里,就可以看到按钮并且可以点击了。
pMenu->setPosition(CCPointZero);
this->addChild(pMenu,1);
pMenu的坐标相对于layer是左下角点(0,0),而按钮相对于pMenu相对于pMenu是屏幕右下的位置,因此pMenu相对于layer也是屏幕右下的位置。
注意,this->addChild(pMenu,1);这里addchild函数里多传了一个数字,这个参数是Z值。一个layer里有很多精力,绘制时的遮挡关系是怎样的呢。答案是Z值大的会确保被绘制在
z值小的上面。这里传的给pMenu传的Z值是1,之后比1小的都会在pMenu下面。如果没传Z值,默认是0。
menuCloseCallback是点击按钮的回调函数,里面根据CC_TARGET_PLATFORM预定义宏的值得不同,写了不同的处理代码。
CC_TARGET_PLATFORM宏是平台标示宏,因为我们的平台是win32,因此,我们会被编译的代码只是一句
CCDirector::sharedDirector()->end();这句函数的意思是关闭程序。
CCSprite是一个关联图片的类,当我们想添加一个图片对象时,就会创建CCSprite,那么对于文字我们使用CCLabelTTF类。
CCLabelTTF*pLabel=CCLabelTTF::create("HelloWorld","Arial",TITLE_FONT_SIZE);
pLabel->setPosition(ccp(origin.x+visibleSize.width/2,origin.y+visibleSize.height-pLabel->getContentSize().height));
this->addChild(pLabel,1);
CCLabelTTF类的创建函数传入三个参数,分别是文字内容,字体,和大小。
创建出来的pLabel在屏幕中间偏上部分。
设置的Z值也为1,但是由于是后添加的,所以在菜单层之上。
CCSprite*pSprite=CCSprite::create("HelloWorld.png");
pSprite->setPosition(ccp(visibleSize.width/2+origin.x,visibleSize.height/2+origin.y));
this->addChild(pSprite,0);
最后,我们创建一个背景,并添加到场景里。
CCSprite的创建函数只要传入一个图片名即可(事实上,其实上有三个重载,但是暂时用不到)。
图片被放在屏幕正中央,有传入的Z值为0,所以会被绘制在最下面。
关于HelloWorld代码本身的解析就到此为止。
ps:注意我们创建的CCSprite由于是create接口得到到,并不是直接new出来的,那么我们应该把它看做(事实上也是)计数器为1且autorelease,那么它不会被销毁么。
事实上,由于它被加入到了layer里,而CCNode在添加一个child时会自动把child的计数器加1,移除时-1.因此不需要我们手动retain了。等切换场景导致当前场景销毁时,所有
子节点会被移除,子节点也会被销毁。
或许有许多朋友会说我这篇博文说的比较粗糙,许多相关点没有详细说。这里我要说明一下,我不打算像翻译工一样把每一句代码的意思给翻译一遍,这篇博文的
目的只是带朋友们熟悉一下cocos2d-x引擎的代码风格和框架,因此我对计数器以及node结构反而多说了几句。helloworld是比较简单的例子,但是在cocos2d-x2.2的版本里,由于开发人员想展示许多新特性,新的函数接口,加了许多相对新手来说比较难以理解的代码,加大了阅读难度,不适合在一开始就拿来说。既然写教程,就要能够帮助新人迅速上手,减少难度,一点一点把知识点传输给读者。看了这篇文章的读者如果知道了如何创建一个scene以及创建一个精灵,绘制到屏幕上,就算过关了。
之后博客会有2个分支
1:我一直坚信程序员只要能贴图就能做游戏,之后我会写几个简单的游戏例子,由简到难分好几步。最初的代码甚至不考虑屏幕分辨率也不适用动画,然后慢慢实现这些功能,每次加入几个cocos2d-x常用类抑或是机制的适用和说明。最初可能是简单的打气球之类的小游戏,最后是塔防抑或是rpg这种稍微大点的项目。跟着这条线走的朋友会学的比较轻松,唯一的敌人可能就是我博客的更新速度。
2:cocos2d-x自带的testcpp的例子其实很经典,许多朋友却懒得看,我打算一一进行讲解,到时候我的博客就有类似工具书的作用了。已经入门的朋友到时可以跟这条线。不过我不知道说明时候能更新就是了。
大家在学习cocos2d-x的时候一定要按着引擎的框架走,按cocos2d-x的风格写代码,严格遵守retainrelease机制,就不会出现代码莫名其妙编译不过去的情况了。最后祝大家晚安,下一篇博文开始要实际做游戏了,养好精神吧。