在游戏开发过程中,你可能需要一个主菜单,几个关卡和一个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的创建

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对象

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能满足上面三个功能即可

Cocos2d-x入门之旅[1]场景的更多相关文章

  1. Cocos2d-x入门之旅[4]场景

    我们之前讲了场景图(Scene Graph) 的概念,继续之前你先要知道 场景图决定了场景内节点对象的渲染顺序 渲染时 z-order 值大的节点对象会后绘制,值小的节点对象先绘制 HelloWorl ...

  2. Swift语言入门之旅

    Swift语言入门之旅  学习一门新的计算机语言,传统来说都是从编写一个在屏幕上打印"Hello world"的程序開始的.那在 Swift,我们使用一句话来实现它: printl ...

  3. cocos2d基础入门

    HelloCpp中Classes目录下放开发者自己的类: win32:平台相关,coco2d已默认创建:coco2d-x目录下,samples/cpp/HelloCpp/(工程根目录)图片放置位置:根 ...

  4. nginx入门系列之应用场景介绍

    目录 HTTP服务器 反向代理服务器 作为一个虚拟主机下多个应用的反向代理 作为多个虚拟主机的反向代理 负载均衡器 简单轮训策略 最小连接数策略 客户端IP哈希策略 服务器权重策略 邮件代理服务器 官 ...

  5. Cocos2d-x入门之旅[2]精灵

    精灵就是cocos是屏幕上移动的对象,能被我们控制,比如我们HelloWorld场景的这个图片就是精灵(Sprite) 怎么才算精灵?你能控制它的,就是一个精灵,否则就只是一个节点(Node) 准确的 ...

  6. Cocos2d-x入门之旅[3]动作

    Cocos通过动作(Action)可以让精灵动起来,把数个动作组成序列(Sequence)就能让精灵做出连续的动作,在动作中我们可以改变精灵的位置,旋转角度,缩放比例,等等 动作(Action) 首先 ...

  7. Cocos2d-x入门之旅

    Cocos通过动作(Action)让精灵动起来,把数个动作组成序列(Sequence)就能让精灵做出连续的动作,在动作中我们可以改变精灵的位置,旋转角度,缩放比例,等等 动作(Action)# 首先我 ...

  8. IOS-swift5.1快速入门之旅

    快速之旅 传统表明,新语言中的第一个程序应在屏幕上打印“Hello,world!”字样.在Swift中,这可以在一行中完成: print("Hello, world!") // P ...

  9. 云原生 - 体验Istio的完美入门之旅(一)

    作者:justmine 头条号:大数据达摩院 微信公众号:大数据处理系统 创作不易,在满足创作共用版权协议的基础上可以转载,但请以超链接形式注明出处. 为了方便大家阅读,可以关注头条号或微信公众号,后 ...

随机推荐

  1. EF Core 通过延迟加载获取导航属性数据

    EF 6及以前的版本是默认支持延迟加载(Lazy Loading)的,早期的EF Core中并不支持,必须使用Include方法来支持导航属性的数据加载. 当然在EF Core 2.1及之后版本中已经 ...

  2. spring的嵌套事务

    转自http://www.iteye.com/topic/35907 在所有使用 spring 的应用中, 声明式事务管理可能是使用率最高的功能了, 但是, 从我观察到的情况看, 绝大多数人并不能深刻 ...

  3. 【Redis】哨兵机制

    一.概述 什么是哨兵机制 二.环境配置 2.1 虚拟机 2.2 安装Redis 2.3 配置主从复制 2.4 配置哨兵 2.5 测试 2.6 疑惑(待解决) 一.概述 什么是哨兵机制 Redis的哨兵 ...

  4. Maven生成项目站点

    概述 Maven不仅仅是一个自动化构建工具和一个依赖工具,还能够帮助聚合项目信息.POM可以包含各种项目信息.如项目描述.版本控制系统地址.缺陷跟踪系统地址.许可证信息.开发者信息等. 另Maven社 ...

  5. windows2008r2安装笔记

    安装win7主题 控制面板 - 程序 - 打开或关闭windows功能 - 功能 - 添加功能: 在选择功能里勾选 桌面体验(会添加必要功能),安装就行了. 1.安装好后,个性化时,发现win7主题为 ...

  6. SpringBoot自定义过滤器的两种方式及过滤器执行顺序

    第一种 @WebFilter + @ServletComponentScan 注解 1.首先自定义过滤器 如下自定义过滤器 ReqResFilter 必须实现  javax.servlet.Filte ...

  7. springboot 定时器 Schdule

    定时器:定时启动任务,执行代码 1.在启动类中加入注解: 2.创建一个类,并且在这个类上加入注解:@Component 3.定义一个方法,在方法上加入注解:@Scheduled(cron=" ...

  8. Knative 实战:三步走!基于 Knative Serverless 技术实现一个短网址服务

    短网址顾名思义就是使用比较短的网址代替很长的网址.维基百科上面的解释是这样的: 短网址又称网址缩短.缩短网址.URL 缩短等,指的是一种互联网上的技术与服务,此服务可以提供一个非常短小的 URL 以代 ...

  9. 操作DOM会影响WEB应用的性能

    平时在工作中,要优化自己开发的WEB应用的性能,一般是遵循以下几个原则: 1.减少网络请求. 2.压缩.合并静态资源文件,以此来减轻网络传输的带宽压力和资源消耗. 3.代码逻辑层面上的性能优化.比如减 ...

  10. aclocal-1.14: 未找到命令

    WARNING: 'aclocal-1.14' is missing on your system. 一条命令解决 autoreconf -ivf