简析HelloWorld场景

以前使用cocos2d-x 3.14的时候,HelloWorld并不是一个场景类,而是一个图层类,当时的HelloWorld::createScene()是长这样的

Scene* HelloWorld::createScene()
{
    auto scene = Scene::create();
    auto layer = HelloWorld::create();
    scene->addChild(layer);
    return scene;
}

而现在的3.17的HelloWorld::createScene()长这样

Scene* HelloWorld::createScene()
{
    return HelloWorld::create();
}

区别就是HelloWorld本身已经是一个场景了,不需要另外生成一个场景再将HelloWorld加到场景中作为子节点

HelloWorld的布局

HelloWorld场景中有一个cocos的logo,一个关闭按钮,一个HelloWorld的字样,这些小物体都是在HelloWorld::init()中生成的
cocos2d-x 系统学习cocos(1)-LMLPHP

基类的初始化

我们向HelloWorld场景添加东西之前,需要先调用基类Scene类的初始化函数,然后获得一下visibleSize和origin备用

bool HelloWorld::init()
{

    if ( !Scene::init() )
    {
        return false;
    }
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();
.................
}

关闭按钮的生成

bool HelloWorld::init()
{
.................
    auto closeItem = MenuItemImage::create(
                                           "CloseNormal.png",
                                           "CloseSelected.png",
                                           CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));

    float x = origin.x + visibleSize.width - closeItem->getContentSize().width/2;
    float y = origin.y + closeItem->getContentSize().height/2;
    closeItem->setPosition(Vec2(x,y));

    auto menu = Menu::create(closeItem, NULL);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menu, 1);
.................
}

这里的代码可能看起来会很复杂,其实不然,我们可以发现cocos里很多对象在生成的时候都会使用create这个静态工厂方法,HelloWorld这个场景也不例外,create将生成游戏对象所需要的参数填进去构造一个对象的指针返回

MenuItemImage的创建

MenuItemImage的create方法传入默认状态的close按钮的图片点击状态下的close按钮的图片以及一个回调,回调指的是程序对按钮被按下这个事件做出的响应,使用CC_CALLBACK_1宏加上一个void(T::*)(Ref*)类型的成员函数的函数指针以及调用这个成员函数的对象的指针组成一个回调,看不懂没关系,我们照着写就好
然后就是计算出x和y的值,也就是右下角的按钮的坐标,getContentSize()获得对象的尺寸,最后使用setPosition设置按钮的坐标

Menu的创建

注意,按钮是不可以直接添加到场景中的,按钮需要依赖菜单,也就是Menu对象,所以我们创建一个包含了closeItem的菜单,并设置坐标为(0,0),最后才能使用addChild将菜单添加到场景中

字体的生成

bool HelloWorld::init()
{
.................
    auto label = Label::createWithTTF("Hello World", "fonts/Marker Felt.ttf", 24);
    label->setPosition(Vec2(origin.x + visibleSize.width/2,
                                origin.y + visibleSize.height - label->getContentSize().height));
    this->addChild(label, 1);
.................
}

这个也很好理解,createWithTTF返回一个Label对象的指针,显示的字符串字体字体大小作为函数的参数,也是使用addChild添加到场景中,这里的1比0高一层,我们试着把文本的坐标设置到场景中央

bool HelloWorld::init()
{
.................
    auto label = Label::createWithTTF("Hello World", "fonts/Marker Felt.ttf", 24);
    label->setPosition(Vec2(origin.x + visibleSize.width/2,
                                origin.y + visibleSize.height/2));
    this->addChild(label, 1);
.................
}

cocos2d-x 系统学习cocos(1)-LMLPHP

文本是在logo上方的,证明图层越高,渲染得越晚,先渲染的被压在后渲染的物体下面

精灵的生成

bool HelloWorld::init()
{
.................
    auto sprite = Sprite::create("HelloWorld.png");
    sprite->setPosition(Vec2(visibleSize.width / 2 + origin.x, visibleSize.height / 2 + origin.y));
    this->addChild(sprite, 0);
    return true;
.................
}

这个就更简单了,使用一张图片生成一个精灵,同样也是加到场景中,最后要记得return true,如果init函数不返回true的话程序会崩掉的

深入探索HelloWorld场景

我一直都认为cocos2dx是学习c++的一个非常好的教材,cocos2dx使用了很多面向对象的特性,c++的特性,还有一些设计模式的思想,对一个新手程序员的综合性成长有很大的帮助

游戏开始的地方

首先,游戏场景的入口是导演类的runWithScene,打开AppDelegate.cpp,找到AppDelegate::applicationDidFinishLaunching()函数,我们可以看到这样的代码

bool AppDelegate::applicationDidFinishLaunching() {
    auto director = Director::getInstance();
..............
    auto scene = HelloWorld::createScene();

    director->runWithScene(scene);

    return true;
}

Director类是一个单例类,使用getInstance可以获得它的实例,单例实际上是通过把构造函数私有化,把对象的访问权限交给一个静态函数实现的,一般我们会使用懒加载来使用这种单例,扯远了,也就是说Director使用了单例模式
Director通过runWithScene运行HelloWorld场景,并让HelloWorld以及HelloWorld的子节点工作

Node类

Node类是HelloWorld场景里我们使用的大部分类的基类,事实上Scene类也是一个Node,很好理解,游戏世界中的对象实际上大部分都是Node,Node和Node通过父子关系联系起来,游戏里的对象的模型是一棵树,父节点使用addChild将子节点加到自己管理的子节点队列中,游戏运行的时候,导演就会遍历这些Node让他们进行工作,比如说HelloWorld场景,HelloWorld场景是根节点,精灵sprite,文本label,菜单menu是HelloWorld的子节点,按钮closeItem是菜单menu的子节点

Ref类

Ref类是用于引用计数的类,负责对象的引用计数,Ref类是Node类的基类,也就是说所有的Node都是使用cocos2dx的引用计数内存管理系统进行内存管理的,这也是为什么我们生成对象不是用new和delete,而是用create生成对象的原因。这里涉及到了GC的知识。简单来说,引用计数法的理论是,当对象被引用的时候,对象的引用计数会+1,取消引用的时候就-1,当计数为0的时候就将对象销毁,感兴趣可以了解一下智能指针RAII

create

这个函数我们可以认为它是一个工厂,这个工厂把我们生成对象之前需要做的工作先做好了,在文章达到最开头有这样一段代码

Scene* HelloWorld::createScene()
{
    return HelloWorld::create();
}

然后HelloWorldScene.h是这样的

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"

class HelloWorld : public cocos2d::Scene
{
public:
    static cocos2d::Scene* createScene();

    virtual bool init();

    void menuCloseCallback(cocos2d::Ref* pSender);

    CREATE_FUNC(HelloWorld);
};

#endif

诶,奇怪了,为什么没有看到create函数,难道是在基类里?嗯呣,静态成员函数是不能继承的,所以问题出现在CREATE_FUNC上,没错!我们看看CREATE_FUNC的定义

#define CREATE_FUNC(__TYPE__) \
static __TYPE__* create() \
{ \
    __TYPE__ *pRet = new(std::nothrow) __TYPE__(); \
    if (pRet && pRet->init()) \
    { \
        pRet->autorelease(); \
        return pRet; \
    } \
    else \
    { \
        delete pRet; \
        pRet = nullptr; \
        return nullptr; \
    } \
}

可以看出来,CREATE_FUNC是一个可以让你偷懒不用手动编写create函数的宏
当然有的类需要客制化create,比如说Sprite的create

Sprite* Sprite::create()
{
    Sprite *sprite = new (std::nothrow) Sprite();
    if (sprite && sprite->init())
    {
        sprite->autorelease();
        return sprite;
    }
    CC_SAFE_DELETE(sprite);
    return nullptr;
}

create里进行了什么操作呢?
1.使用new生成对象
2.使用init初始化对象
3.使用autorelease将这个Ref类交给引用计数系统管理内存
看到这个init我们是不是想到了什么,HelloWorld场景的布局就是在init中实现的,而init由create调用,也就是说,在HelloWorld进行create的时候就已经将文本,按钮,精灵等物件创建并加入到场景中,而这些物件也是通过create创建的,也就是说,场景创建的时候会调用所有物件的init,这样我们对cocos2dx的游戏流程是不是有了更深的理解
autorelease是Ref类的方法,查看一下它的定义

Ref* Ref::autorelease()
{
    PoolManager::getInstance()->getCurrentPool()->addObject(this);
    return this;
}

又看到了getInstance,说明PoolManager也是一个单例类,这段代码的意思很明显,将Ref加入到当前内存池中管理
我们在后续的开发中经常需要客制化create,只要我们的create能满足上面三个功能即可

总结

这节我们通过研究cocos2dx新工程自带的HelloWorld代码了解到了很多东西,设计模式,GC,游戏对象结构的设计思路,还有c++的各种小知识,用宏偷懒啦,宏保护避免重复编译啦
嗯?宏保护是什么?

宏保护

这里顺便讲一下宏保护这个小知识点,宏保护使用来避免.h文件被重复编译的,这里以HelloWorldScene.h为例

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__
............
#endif

#这个符号开头的代码是预处理命令,在程序编译之前会进行预处理工作,这几行代码的意思是,如果没有定义__HELLOWORLD_SCENE_H__这个符号就定义__HELLOWORLD_SCENE_H__并且编译到endif为止的内容,当__HELLOWORLD_SCENE_H__被定义过一次,预处理器下一次遇到这条预处理命令的时候就不会再把下面的代码作为编译目标,当然现在vs有一条#pragma once的命令,保证该文件只编译一次,也可以达到我们的目的

10-17 10:11