Cocos2d-x入门之旅[1]场景
在游戏开发过程中,你可能需要一个主菜单,几个关卡和一个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 中,通过 Scene
的 addChild()
方法构建场景图
// 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
类的初始化函数,然后获得visibleSize
和origin
备用
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里进行了什么操作呢?
- 使用
new
生成对象 - 使用
init
初始化对象 - 使用
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]场景的更多相关文章
- Cocos2d-x入门之旅[4]场景
我们之前讲了场景图(Scene Graph) 的概念,继续之前你先要知道 场景图决定了场景内节点对象的渲染顺序 渲染时 z-order 值大的节点对象会后绘制,值小的节点对象先绘制 HelloWorl ...
- Swift语言入门之旅
Swift语言入门之旅 学习一门新的计算机语言,传统来说都是从编写一个在屏幕上打印"Hello world"的程序開始的.那在 Swift,我们使用一句话来实现它: printl ...
- cocos2d基础入门
HelloCpp中Classes目录下放开发者自己的类: win32:平台相关,coco2d已默认创建:coco2d-x目录下,samples/cpp/HelloCpp/(工程根目录)图片放置位置:根 ...
- nginx入门系列之应用场景介绍
目录 HTTP服务器 反向代理服务器 作为一个虚拟主机下多个应用的反向代理 作为多个虚拟主机的反向代理 负载均衡器 简单轮训策略 最小连接数策略 客户端IP哈希策略 服务器权重策略 邮件代理服务器 官 ...
- Cocos2d-x入门之旅[2]精灵
精灵就是cocos是屏幕上移动的对象,能被我们控制,比如我们HelloWorld场景的这个图片就是精灵(Sprite) 怎么才算精灵?你能控制它的,就是一个精灵,否则就只是一个节点(Node) 准确的 ...
- Cocos2d-x入门之旅[3]动作
Cocos通过动作(Action)可以让精灵动起来,把数个动作组成序列(Sequence)就能让精灵做出连续的动作,在动作中我们可以改变精灵的位置,旋转角度,缩放比例,等等 动作(Action) 首先 ...
- Cocos2d-x入门之旅
Cocos通过动作(Action)让精灵动起来,把数个动作组成序列(Sequence)就能让精灵做出连续的动作,在动作中我们可以改变精灵的位置,旋转角度,缩放比例,等等 动作(Action)# 首先我 ...
- IOS-swift5.1快速入门之旅
快速之旅 传统表明,新语言中的第一个程序应在屏幕上打印“Hello,world!”字样.在Swift中,这可以在一行中完成: print("Hello, world!") // P ...
- 云原生 - 体验Istio的完美入门之旅(一)
作者:justmine 头条号:大数据达摩院 微信公众号:大数据处理系统 创作不易,在满足创作共用版权协议的基础上可以转载,但请以超链接形式注明出处. 为了方便大家阅读,可以关注头条号或微信公众号,后 ...
随机推荐
- Java日志框架总结
1. 前言 从写代码开始,就陆陆续续接触到了许多日志框架,较常用的属于LOG4J,LogBack等.每次自己写项目时,就copy前人的代码或网上的demo.配置log4j.properties或者lo ...
- 【Redis】主从复制
一.概述 1.redis的复制功能是支持多个数据库之间的数据同步.一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数据库,而从 ...
- Zookeeper内部实现分布式数据一致性(底层系统模型)(一)
Zookeeper的几个概念:(接下来将从这几个概念书写Zookeeper的内部工作流程) 数据模型 节点特性 版本 Watcher ACL <1> 数据模型: Zookeeper的视图很 ...
- 洛谷 P1091合唱队列
吾王剑之所指,吾等心之所向 ——<Fate/stay night> 题目:https://www.luogu.org/problem/P ...
- 神奇的 SQL 之层级 → 为什么 GROUP BY 之后不能直接引用原表中的列
前言 开心一刻 感觉不妙呀,弟弟舔它! 不该舔的,舔到怀疑人生了...... GROUP BY 后 SELECT 列的限制 标准 SQL 规定,在对表进行聚合查询的时候,只能在 SELECT 子句中写 ...
- Docker竟然还能这么玩?商业级4G代理搭建实战!
时间过得真快,距离这个系列的上一篇文章<商业级4G代理搭建指南[准备篇]>发布的时间已经过了两个星期了,上个星期由于各种琐事缠身,周二开始就没空写文章了,所以就咕咕咕了. 那么在准备篇中, ...
- FreeSql (二十)多表查询 WhereCascade
WhereCascade 多表查询时非常方便,有了它可以很轻松的完成类型软删除,租户条件的功能. IFreeSql fsql = new FreeSql.FreeSqlBuilder() .UseCo ...
- jquery ajax到servlet出现中文乱码(utf-8编码下)
个人遇到的该问题有两大类: 第一类很普遍,就是jsp页面编码没有规定,servlet中接收参数没有转码,response没有使用setContentType()和setCharacterEncodin ...
- 【学习笔记】python3核心技术与实践--如何逐步突破,成为python高手
众所周知,Facebook 的主流语言是 Hack(PHP 的进化版本).不过,我敢拍着胸脯说,就刚入职的工程师而言,100 个里至少有 95 个,以前都从未用过 Hack 或者 PHP.但是,这些人 ...
- linux文件系统分区、格式化、挂载、卷标挂载、永久挂载
思想不放松你的行为就不会放松,你的行为放松了,说明你的思想放松了.