cocos2d-x 系统学习cocos(1)

简析HelloWorld场景

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

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

而如今的3.17的HelloWorld::createScene()长这样c++

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

区别就是HelloWorld自己已是一个场景了,不须要另外生成一个场景再将HelloWorld加到场景中做为子节点程序员

HelloWorld的布局

HelloWorld场景中有一个cocos的logo,一个关闭按钮,一个HelloWorld的字样,这些小物体都是在HelloWorld::init()中生成的
设计模式

基类的初始化

咱们向HelloWorld场景添加东西以前,须要先调用基类Scene类的初始化函数,而后得到一下visibleSize和origin备用app

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的create方法传入默认状态的close按钮的图片点击状态下的close按钮的图片以及一个回调,回调指的是程序对按钮被按下这个事件作出的响应,使用CC_CALLBACK_1宏加上一个void(T::*)(Ref*)类型的成员函数的函数指针以及调用这个成员函数的对象的指针组成一个回调,看不懂不要紧,咱们照着写就好
而后就是计算出x和y的值,也就是右下角的按钮的坐标,getContentSize()得到对象的尺寸,最后使用setPosition设置按钮的坐标布局

注意,按钮是不能够直接添加到场景中的,按钮须要依赖菜单,也就是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);
.................
}

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

精灵的生成

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的命令,保证该文件只编译一次,也能够达到咱们的目的

相关文章
相关标签/搜索