zoukankan      html  css  js  c++  java
  • Cocos2d-x入门之旅[1]HelloWorld

    在游戏开发过程中,你可能需要一个主菜单,几个关卡和一个END的界面,如何组织管理这些东西呢?

    和其他游戏引擎类似,Cocos也使用了场景(Scene) 这个概念,我们的HelloWorld界面就是一个场景

    一部电影或是番剧,就是由不同地点或不同时间线组成的,这些部分就是一个又一个的场景

    参考:https://www.cnblogs.com/NightFrost/p/11688854.html

    场景的存储结构

    为了解释场景的结构,我们先不看我们过于简单的helloworld场景,看下面这个官方文档的场景:

    这是一个主菜单场景,这个场景是由很多小的对象拼接而成,所有的对象组合在一起,形成了你看到的结果

    场景是被渲染器(renderer)画出来的,渲染器负责渲染精灵和其它的对象进入屏幕,那渲染器怎么知道什么东西要渲染在后,什么东西要渲染在前呢?

    答案是通过场景图(Scene Graph)实现

    场景图(Scene Graph)

    Cocos2d-x使用场景图(Scene Graph)这一数据结构来安排场景内渲染的对象,场景内所有的节点(Node)都包含在一个树(tree)上:

    Cocos2d-x使用 中序遍历,先遍历左子树,然后根节点,最后是右子树

    中序遍历下图的节点,能得到 A, B, C, D, E, F, G, H, I 这样的序列

    现在我们再看这个游戏场景:

    分解这场景为5个部分

    抽象成数据结构就是:

    z-order

    树上的每个元素都会存储一个z-order,z-order为负的元素,z-order为负的节点会被放置在左子树,非负的节点会被放在右子树,实际开发的过程中,你可以按照任意顺序添加对象,他们会按照你指定的 z-order 自动排序

    在 Cocos2d-x 中,通过 SceneaddChild() 方法构建场景图

    // Adds a child with the z-order of -2, that means
    // it goes to the "left" side of the tree (because it is negative)
    scene->addChild(title_node, -2);
    
    // When you don't specify the z-order, it will use 0
    scene->addChild(label_node);
    
    // Adds a child with the z-order of 1, that means
    // it goes to the "right" side of the tree (because it is positive)
    scene->addChild(sprite_node, 1);
    

    渲染时 z-order 值大的节点对象后绘制值小的节点对象先绘制,如果两个节点对象的绘制范围有重叠,z-order 值大的可能会覆盖 z-order 值小的,这才实现了我们的需求

    HelloWorld场景

    现在我们回看我们运行出来的HelloWorld场景,并且具体到代码操作

    场景中有一个我们自己的图片,一个关闭按钮,一个HelloWorld的字样,这些东西都是在HelloWorld::init()中生成的

    场景初始化

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

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

    关闭按钮的生成

    相关代码如下

    bool HelloWorld::init()
    {
    	...
            
        /////////////////////////////
        // 2. add a menu item with "X" image, which is clicked to quit the program
        //    you may modify it.
    
        // add a "close" icon to exit the progress. it's an autorelease object
        auto closeItem = MenuItemImage::create(
                                               "CloseNormal.png",
                                               "CloseSelected.png",
                                               CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));
    
    	if (closeItem == nullptr ||
            closeItem->getContentSize().width <= 0 ||
            closeItem->getContentSize().height <= 0)
        {
            problemLoading("'CloseNormal.png' and 'CloseSelected.png'");
        }
        else
        {
            float x = origin.x + visibleSize.width - closeItem->getContentSize().width/2;
            float y = origin.y + closeItem->getContentSize().height/2;
            closeItem->setPosition(Vec2(x,y));
        }
    
        // create menu, it's an autorelease object
        auto menu = Menu::create(closeItem, NULL);
        menu->setPosition(Vec2::ZERO);
        this->addChild(menu, 1);
        
    	...
    }
    

    cocos里很多对象在生成的时候都会使用create这个静态工厂方法,我们创建图片精灵的时候就用到了auto mySprite = Sprite::create("xxxxxx.png"),HelloWorld这个场景也不例外

    MenuItemImage的create方法传入默认状态的close按钮的图片点击状态下的close按钮的图片以及一个回调,回调指的是程序对按钮被按下这个事件做出的响应,看不懂没关系,照着写就好

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

    然后就是计算出x和y的值,也就是右下角的按钮的坐标,getContentSize()获得对象的尺寸,最后使用setPosition设置按钮的坐标

    if (closeItem == nullptr ||
        closeItem->getContentSize().width <= 0 ||
        closeItem->getContentSize().height <= 0)
    {
        problemLoading("'CloseNormal.png' and 'CloseSelected.png'");
    }
    else
    {
        float x = origin.x + visibleSize.width - closeItem->getContentSize().width/2;
        float y = origin.y + closeItem->getContentSize().height/2;
        closeItem->setPosition(Vec2(x,y));
    }
    

    但是按钮是不可以直接添加到场景中的,按钮需要依赖菜单,也就是Menu对象

    我们创建一个包含了closeItem的菜单,并设置坐标为(0,0),最后才能使用addChild将菜单添加到场景中

    // create menu, it's an autorelease object
    auto menu = Menu::create(closeItem, NULL);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menu, 1); 
    

    字体的生成

    bool HelloWorld::init()
    {
    	...
            
        auto label = Label::createWithTTF("Hello World", "fonts/Marker Felt.ttf", 24);
        //Label::createWithTTF(显示的字符串,字体,字体大小);
        if (label == nullptr)
        {
            problemLoading("'fonts/Marker Felt.ttf'");
        }
        else
        {
            // position the label on the center of the screen
            label->setPosition(Vec2(origin.x + visibleSize.width/2,
                                    origin.y + visibleSize.height - label->getContentSize().height));
    
            // add the label as a child to this layer
            this->addChild(label, 1);
        }
    	
        ...
    }
    

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

    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上方的,验证了 z-order 值大的节点对象后绘制,值小的节点对象先绘制,先渲染的被压在后渲染的物体下面

    精灵的生成

    bool HelloWorld::init()
    {
    	...
            
    	auto sprite = Sprite::create("sinnosuke.png");
        if (sprite == nullptr)
        {
            problemLoading("'HelloWorld.png'");
        }
        else
        {
            // position the sprite on the center of the screen
            sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));
    		// Vec2(visibleSize.width/4 + origin.x, visibleSize.height/2 + origin.y)
            // add the sprite as a child to this layer
            this->addChild(sprite, 0);
        }
    	
        ...
    }
    

    更简单了,使用一张图片生成一个精灵,同样也是加到场景中,最后要记得return true

    深入探索场景

    场景入口

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

    Copybool AppDelegate::applicationDidFinishLaunching() {
        // initialize director
        auto director = Director::getInstance();
    	
        ...
        
        // create a scene. it's an autorelease object
        auto scene = HelloWorld::createScene();
    
        // run
        director->runWithScene(scene);
    
        return true;
    }
    

    Director类是一个单例类,使用getInstance可以获得它的实例,(单例模式保证系统中应用该模式的类一个类只有一个对象实例)我们需要Director实例来运行运行HelloWorld场景(通过runWithScene),并让HelloWorld以及HelloWorld的子节点工作

    Node类

    Node类是HelloWorld场景里我们使用的大部分类的基类(其实Scene类也是一个Node)

    游戏世界中的对象实际上大部分都是Node,就像我们一开始提到的,Node和Node通过父子关系联系起来,形成一棵树,父节点使用addChild将子节点加到自己管理的子节点队列中,游戏运行的时候,导演Director就会遍历这些Node让他们进行工作

    比如我们的HelloWorld场景:HelloWorld场景是根节点,精灵sprite,文本label,菜单menu是HelloWorld的子节点,按钮closeItem是菜单menu的子节点

    Ref类

    Ref类是用于引用计数的类,负责对象的引用计数,Ref类是Node类的基类,也就是说所有的Node都是使用cocos2dx的引用计数内存管理系统进行内存管理的,这也是为什么我们生成对象不是用new和delete,而是用create生成对象的原因

    简单来说,引用计数法的理论是,当对象被引用的时候,对象的引用计数会+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

    #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

    CopySprite* 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

    autorelease是Ref类的方法,查看一下它的定义

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

    又看到了getInstance,说明PoolManager也是一个单例类,这段代码的意思很明显,将Ref加入到当前内存池中管理

    我们在后续的开发中经常需要客制化create,只要我们的create能满足上面三个功能即可

  • 相关阅读:
    Sgu294He's Circles
    [HNOI2008]Card洗牌
    传球游戏
    [BZOJ1478]Sgu282 Isomorphism
    [POJ2154]Color
    [ZOJ1961]Let it Bead
    BZOJ1257 [CQOI2007]余数之和sum
    BZOJ1192 [HNOI2006]鬼谷子的钱袋
    BZOJ4614 [Wf2016]Oil
    BZOJ3209 花神的数论题
  • 原文地址:https://www.cnblogs.com/zhxmdefj/p/11689693.html
Copyright © 2011-2022 走看看