Cocos2d-x,Cocos

2D游戏特效动画的播放机制

特效动画的播放机制

本文转载自逆水行舟的博客,版权归作者所有。

gitblog-logo

一. 简介

动画在2D游戏里用得十分广泛, 根据这些动画的特点,我们可以大概归为3类

1. 粒子动画

这种动画是由几百甚至上千个粒子构成, 所有粒子都共享一个纹理, 这些粒子都是从一个发射器发出, 加以一定的随机因素, 在不同发射速度和重力等外力作用下,每个粒子呈现不一样的运动状态, 大量粒子可以组合成各种各样不一样的效果, 比如烟花, 火焰. 粒子动画的实现一般都会使用批次渲染和对象池来保证性能.

2. 骨骼动画

这种动画通常用于表现有多个动作的角色, 它通常是由骨骼(bone)和绑定在骨骼上的蒙皮(skin/mesh)构成.

动画师通常在spine(2d)或者3dmax等工具里面对骨骼动作进行设计, 同时对蒙皮进行编辑.

3. 特效动画

特效动画不需要或者难以使用骨骼进行表达, 比如一个刀光效果或者一闪一闪的星星, 我们可以使用最原始的实现方式, 对动画的每一帧都画一张图片, 依次连续展示这些图片就可以达到动画效果.

但是这种方法实现的动画过于浪费空间和内存. 其中有非常多的特效我们可以通过关键帧动画的方式来实现, 常使用Flash工具进行关键帧动画的设计.

本文中下面只讨论关键帧动画的实现.

阅读全文»

子龙山人带你学习OpenGL ES 2.0 :使用VBO索引

大家好,我是子龙山人。我现在在厦门 Cocos2D-X 团队做游戏引擎开发。

我是一个技术 Geek,我喜欢研究好玩的技术,同时我也是一个 Emacs 党。

欢迎大家一起交流。

我的个人主页:http://zilongshanren.com/


上一篇文章中,我们介绍了uniform和模型-视图-投影变换,相信大家对于OpenGL ES 2.0应该有一点感觉了。在这篇文章中,我们不再画三角形了,改为画四边形。下篇教程,我们就可以画立方体了,到时候就是真3D了,哈哈。

为什么三角形在OpenGL教程里面这么受欢迎呢?因为在OpenGL的世界里面,所有的几何体都可以用三角形组合出来。我们的四边形也一样,它可以用两个三角形组合出来。

阅读全文»

子龙山人带你学习OpenGL ES 2.0 :初识MVP

大家好,我是子龙山人。我现在在厦门 Cocos2D-X 团队做游戏引擎开发。

我是一个技术 Geek,我喜欢研究好玩的技术,同时我也是一个 Emacs 党。

欢迎大家一起交流。

我的个人主页:http://zilongshanren.com/


上一篇文章中,我在介绍vertex shader的时候挖了一个坑:CC_MVPMatrix。它其实是一个uniform,每一个cocos2d-x预定义的shader都包含有这个uniform, 但是如果你在shader里面不使用这个变量的话,OpenGL底层会把它优化掉。

但是,CC_MVPMatrix是在什么时候设置进来的呢?我在shader里面明明没有看到它,它从哪儿来的?别急,请继续往下读。

初识Uniform

在回答上面几个问题之前,让我们先来介绍一下什么是uniform。简单来说,uniform是shader里面的一种变量,它是由外部程序设置进来的,它不像vertex的attribute,每个顶点都有一份数据。除非你显式地调用glUniformXXX函数来修改这个uniform的值,否则它的值恒定不变。接下来,让我们修改myFragmentShader.frag,给它添加一个新的uniform数据:

阅读全文»

子龙山人带你学习OpenGL ES 2.0 :编写自己的shader

大家好,我是子龙山人。我现在在厦门 Cocos2D-X 团队做游戏引擎开发。

我是一个技术 Geek,我喜欢研究好玩的技术,同时我也是一个 Emacs 党。

欢迎大家一起交流。

Emacs 交流:https://slackin-emacs-cn.herokuapp.com/

Cocos2D-x 交流: Cocos2D-X Github issue system. https://github.com/cocos2d/cocos2d-x/issues/new

我的 Email: guanghui8827@gmail.com

我的个人主页:http://zilongshanren.com/


上篇文章中,我给大家介绍了如何在cocos2d-x里面绘制一个三角形,当时我们使用的是cocos2d-x引擎自带的shader和一些辅助函数。在本文中,我将演示一下如何编写自己的shader,同时,我们还会介绍VBO(顶点缓冲区对象)和VAO(顶点数组对象)的基本用法。

阅读全文»

子龙山人带你学习OpenGL ES 2.0 :你的第一个三角形

大家好,我是子龙山人。我现在在厦门 Cocos2D-X 团队做游戏引擎开发。

我是一个技术 Geek,我喜欢研究好玩的技术,同时我也是一个 Emacs 党。

欢迎大家一起交流。

Emacs 交流:https://slackin-emacs-cn.herokuapp.com/

Cocos2D-x 交流: Cocos2D-X Github issue system. https://github.com/cocos2d/cocos2d-x/issues/new

我的 Email: guanghui8827@gmail.com

我的个人主页:http://zilongshanren.com/

简介

欢迎来到我的 OpenGLES 系列教程,这个系列的教程我打算以时下最流行的开源游戏引擎 Cocos2D-X 为基础来介绍 OpenGLES。 我写作这个系列教程的原因是,一方面对我自己学习 OpenGLES 的一个总结,另一方面也希望通过一些具体的、 容易理解的示例,让大家更好地学习 OpenGLES。

阅读全文»

[Bugfix]CCFileUtils 解析 UTF8+BOM 字节文件时“key not found”的问题

存在 bug 的版本

Quick-Cocos2dx-Community 3.6 Release 以及 Cocos2d-x 3.3 final中。

bug 将在 Quick-Cocos2dx-Community 3.6.1 中修正。

bug 描述

CCFileUtils.cpp 文件中有一个用于帮助解析文本的 DictMaker 类。当解析带有 UTF8+BOM 的文件时,程序会首先调用 DictMaker 类的 textHandler 方法来解析文本。如下所示:

阅读全文»

Cocos2d-x3.2的渲染流程

本文转载自CSDN,原文作者:cbbbc,原文链接Cocos2d-x3.2的渲染流程

最近几天,我都在学习如何在Cocos2d-x3.2中使用OpenGL来实现对图形的渲染。在网上也看到了很多好的文章,我在它们的基础上做了这次的我个人认为比较完整的总结。当你了解了Cocos2d-x3.2中对图形渲染的流程,你就会觉得要学会写自己的shader才是最重要的。

第一,渲染流程从2.x到3.x的变化

在2.x中,渲染过程是通过递归渲染树(Rendering tree)这种图关系来渲染关系图。递归调用visit()函数,并且在visit()函数中调用该节点的draw函数渲染各个节点,此时draw函数的作用是直接调用OpenGL代码进行图形的渲染。由于visit()和draw函数都是虚函数,所以要注意执行时的多态。那么我们来看看2.x版本中CCSprite的draw函数,如代码1。

代码1:

//这是cocos2d-2.0-x-2.0.4版本的CCSprite的draw函数  
void CCSprite::draw(void)
{
CC_PROFILER_START_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");
CCAssert(!m_pobBatchNode, "If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");
CC_NODE_DRAW_SETUP();
ccGLBlendFunc( m_sBlendFunc.src, m_sBlendFunc.dst );
if (m_pobTexture != NULL)
{
ccGLBindTexture2D( m_pobTexture->getName() );
}
else
{
ccGLBindTexture2D(0);
}
//
// Attributes
//
ccGLEnableVertexAttribs( kCCVertexAttribFlag_PosColorTex );
#define kQuadSize sizeof(m_sQuad.bl)
long offset = (long)&m_sQuad;
// vertex
int diff = offsetof( ccV3F_C4B_T2F, vertices);
glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE, kQuadSize, (void*) (offset + diff));
// texCoods
diff = offsetof( ccV3F_C4B_T2F, texCoords);
glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, kQuadSize, (void*)(offset + diff));
// color
diff = offsetof( ccV3F_C4B_T2F, colors);
glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (void*)(offset + diff));
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
CHECK_GL_ERROR_DEBUG();
#if CC_SPRITE_DEBUG_DRAW == 1
// draw bounding box
CCPoint vertices[4]={
ccp(m_sQuad.tl.vertices.x,m_sQuad.tl.vertices.y),
ccp(m_sQuad.bl.vertices.x,m_sQuad.bl.vertices.y),
ccp(m_sQuad.br.vertices.x,m_sQuad.br.vertices.y),
ccp(m_sQuad.tr.vertices.x,m_sQuad.tr.vertices.y),
};
ccDrawPoly(vertices, 4, true);
#elif CC_SPRITE_DEBUG_DRAW == 2
// draw texture box
CCSize s = this->getTextureRect().size;
CCPoint offsetPix = this->getOffsetPosition();
CCPoint vertices[4] = {
ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+s.height)
};
ccDrawPoly(vertices, 4, true);
#endif // CC_SPRITE_DEBUG_DRAW

CC_INCREMENT_GL_DRAWS(1);

CC_PROFILER_STOP_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");
}

那么我们也看看3.x中Sprite的draw函数,如代码2。

代码2:

void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)  
{
// Don't do calculate the culling if the transform was not updated
_insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;
if(_insideBounds)
{
_quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1, transform);
renderer->addCommand(&_quadCommand);
#if CC_SPRITE_DEBUG_DRAW
_customDebugDrawCommand.init(_globalZOrder);
_customDebugDrawCommand.func = CC_CALLBACK_0(Sprite::drawDebugData, this);
renderer->addCommand(&_customDebugDrawCommand);
#endif //CC_SPRITE_DEBUG_DRAW
}
}

从代码1和代码2的对比中,我们很容易就发现2.x版本中的draw函数直接调用OpengGL代码进行图形渲染,而3.x版本中draw的作用是把RenderCommand添加到CommandQueue中,至于这样做的好处是,实际的渲染API进入其中一个与显卡直接交流的有独立线程的RenderQueue。

从Cocos2d-x3.0开始,Cocos2d-x引入了新的渲染流程,它不像2.x版本直接在每一个node中的draw函数中直接调用OpenGL代码进行图形渲染,而是通过各种RenderCommand封装起来,然后添加到一个CommandQueue队列里面去,而现在draw函数的作用就是在此函数中设置好相对应的RenderCommand参数,然后把此RenderCommand添加到CommandQueue中。最后在每一帧结束时调用renderer函数进行渲染,在renderer函数中会根据ID对RenderCommand进行排序,然后才进行渲染。

下面我们来看看图1、图2,这两个图形象地表现了Cocos2d-x3.x下RenderCommand的封装与传递与及RenderCommand的排序。

图1:

图1

图2:

图2

上面所说的各个方面都有点零碎,下面就对渲染的整个流程来一个从头到尾的梳理吧。下面是针对3.2版本的,对于2.x版本的梳理不做梳理,因为我用的是3.2版本。

首先,我们Cocos2d-x的执行是通过Application::run()来开始的,如代码3,此代码目录中在xx\cocos2d\cocos\platform\对应平台的目录下,这是与多平台实现有关的类,关于如何实现多平台的编译,你可以参考《cocos2d-x3.2源码分析(一)类FileUtils--实现把资源放在Resources文件目录下达到多平台的引用》 中我对平台编译的分析。以防篇幅过长,只截取了重要部分,如需详解,可以直接查看源码。

代码3:

int Application::run()  
{
...
director->mainLoop();
...
}

从代码3中,它明显的启发着我们要继续追寻Director::mainLoop()函数。在Director中mainLoop()为纯函数,此子类DisplayLinkDirector才有其实现,如代码4。

代码4:

void DisplayLinkDirector::mainLoop()  
{
<span><span class="comment">//只有一种情况会调用到这里来,就是导演类调用end函数</span><span> </span></span>
if (_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop = false;
<span><span class="comment">//清除导演类</span><span></span></span>
purgeDirector();
}
else if (! _invalid)
{ <span><span class="comment">//绘制</span><span> </span></span>
drawScene();
//清除当前内存池中对象,即池中每一个对象--_referenceCount
PoolManager::getInstance()->getCurrentPool()->clear();
}
}

mainLoop是主线程调用的循环,其中drawScene()是绘制函数,接着我们继续追寻它的代码,如代码5。

代码5:

void Director::drawScene()  
{
<span><span class="comment">//计算间隔时间</span><span> </span></span>
calculateDeltaTime();

//忽略该帧如果时间间隔接近0
if(_deltaTime < FLT_EPSILON)
{
return;
}

if (_openGLView)
{
_openGLView->pollInputEvents();
}

//tick before glClear: issue #533
if (! _paused)
{
_scheduler->update(_deltaTime);
_eventDispatcher->dispatchEvent(_eventAfterUpdate);
}

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

/* to avoid flickr, nextScene MUST be here: after tick and before draw.
XXX: Which bug is this one. It seems that it can't be reproduced with v0.9 */
if (_nextScene)
{
setNextScene();
}

pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

// draw the scene
if (_runningScene)
{
_runningScene->visit(_renderer, Mat4::IDENTITY, false);
_eventDispatcher->dispatchEvent(_eventAfterVisit);
}

// draw the notifications node
if (_notificationNode)
{
_notificationNode->visit(_renderer, Mat4::IDENTITY, false);
}

if (_displayStats)
{
showStats();
}

_renderer->render();
_eventDispatcher->dispatchEvent(_eventAfterDraw);

popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

_totalFrames++;

// swap buffers
if (_openGLView)
{
_openGLView->swapBuffers();
}

if (_displayStats)
{
calculateMPF();
}
}

从代码5中,我们看见visit()和render()函数的调用。其中visit()函数会调用draw()函数来向RenderQueue中添加RenderCommand,那么就继续追寻visit()的代码,如代码6。

代码6:

void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags)  
{
// quick return if not visible. children won't be drawn.
if (!_visible)
{
return;
}

uint32_t flags = processParentFlags(parentTransform, parentFlags);

// IMPORTANT:
// To ease the migration to v3.0, we still support the Mat4 stack,
// but it is deprecated and your code should not rely on it
Director* director = Director::getInstance();
director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW, _modelViewTransform);
int i = 0;
if(!_children.empty())
{
sortAllChildren();
// draw children zOrder < 0
for( ; i < _children.size(); i++ )
{
auto node = _children.at(i);

if ( node && node->_localZOrder < 0 )
node->visit(renderer, _modelViewTransform, flags);
else
break;
}
// self draw
this->draw(renderer, _modelViewTransform, flags);

for(auto it=_children.cbegin()+i; it != _children.cend(); ++it)
(*it)->visit(renderer, _modelViewTransform, flags);
}
else
{
this->draw(renderer, _modelViewTransform, flags);
}

director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

// FIX ME: Why need to set _orderOfArrival to 0??
// Please refer to https://github.com/cocos2d/cocos2d-x/pull/6920
// reset for next frame
// _orderOfArrival = 0;
}

从代码6中,我们可以看到“ auto node = _children.at(i);和node->visit(renderer, _modelViewTransform, flags);”,这段代码的意思是先获取子节点,然后递归调用节点的visit()函数,到了没有子节点的节点,开始调用draw()函数。那么我们看看draw()函数代码,如代码7。

代码7:

void Node::draw(Renderer* renderer, const Mat4 &transform, uint32_t flags)  
{
}

好吧,从代码7中,我们看到Node的draw什么都没有做,是我们找错地方?原来draw()是虚函数,所以它执行时执行的是该字节类的draw()函数。确实是我们找错地方了。那么我们分别看DrawNode::draw()、Sprite::draw()。

代码8:

void DrawNode::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)  
{
_customCommand.init(_globalZOrder);
_customCommand.func = CC_CALLBACK_0(DrawNode::onDraw, this, transform, flags);
renderer->addCommand(&_customCommand);
}

void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
// Don't do calculate the culling if the transform was not updated
_insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;

if(_insideBounds)
{
_quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1, transform);
renderer->addCommand(&_quadCommand);
#if CC_SPRITE_DEBUG_DRAW
_customDebugDrawCommand.init(_globalZOrder);
_customDebugDrawCommand.func = CC_CALLBACK_0(Sprite::drawDebugData, this);
renderer->addCommand(&_customDebugDrawCommand);
#endif //CC_SPRITE_DEBUG_DRAW
}
}

从代码8中,我们可以看到在draw()函数向RenderQueue中添加RenderCommand,当然有的类的draw()不是向RenderQueue中添加RenderCommand,而是直接使用OpenGL的API直接进行渲染,或者做一些其他的事情。

那么当draw()都递归调用完了,我们来看看最后进行渲染的Renderer::render() 函数,如代码9。

代码9:

void Renderer::render()  
{
//Uncomment this once everything is rendered by new renderer
//glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

//TODO setup camera or MVP
_isRendering = true;

if (_glViewAssigned)
{
// cleanup
_drawnBatches = _drawnVertices = 0;

//Process render commands
//1. Sort render commands based on ID
for (auto &renderqueue : _renderGroups)
{
renderqueue.sort();
}
visitRenderQueue(_renderGroups[0]);
flush();
}
clean();
_isRendering = false;
}

从代码9中,我们看到“renderqueue.sort()",这是之前所说的对命令先排序,然后才进行渲染,“visitRenderQueue( _renderGroups[0])”就是来进行渲染的。那么我们接着看看void Renderer::visitRenderQueue(const RenderQueue& queue)的代码,如代码10。

代码10:

void Renderer::visitRenderQueue(const RenderQueue& queue)  
{
ssize_t size = queue.size();

for (ssize_t index = 0; index < size; ++index)
{
auto command = queue[index];
auto commandType = command->getType();
if(RenderCommand::Type::QUAD_COMMAND == commandType)
{
flush3D();
auto cmd = static_cast<QuadCommand*>(command);
//Batch quads
if(_numQuads + cmd->getQuadCount() > VBO_SIZE)
{
CCASSERT(cmd->getQuadCount()>= 0 && cmd->getQuadCount() < VBO_SIZE, "VBO is not big enough for quad data, please break the quad data down or use customized render command");

//Draw batched quads if VBO is full
drawBatchedQuads();
}

_batchedQuadCommands.push_back(cmd);

memcpy(_quads + _numQuads, cmd->getQuads(), sizeof(V3F_C4B_T2F_Quad) * cmd->getQuadCount());
convertToWorldCoordinates(_quads + _numQuads, cmd->getQuadCount(), cmd->getModelView());

_numQuads += cmd->getQuadCount();

}
else if(RenderCommand::Type::GROUP_COMMAND == commandType)
{
flush();
int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
visitRenderQueue(_renderGroups[renderQueueID]);
}
else if(RenderCommand::Type::CUSTOM_COMMAND == commandType)
{
flush();
auto cmd = static_cast<CustomCommand*>(command);
cmd->execute();
}
else if(RenderCommand::Type::BATCH_COMMAND == commandType)
{
flush();
auto cmd = static_cast<BatchCommand*>(command);
cmd->execute();
}
else if (RenderCommand::Type::MESH_COMMAND == commandType)
{
flush2D();
auto cmd = static_cast<MeshCommand*>(command);
if (_lastBatchedMeshCommand == nullptr || _lastBatchedMeshCommand->getMaterialID() != cmd->getMaterialID())
{
flush3D();
cmd->preBatchDraw();
cmd->batchDraw();
_lastBatchedMeshCommand = cmd;
}
else
{
cmd->batchDraw();
}
}
else
{
CCLOGERROR("Unknown commands in renderQueue");
}
}
}

从代码10中,我们看到RenderCommand类型有QUAD_COMMAND,CUSTOM_COMMAND,BATCH_COMMAND, GROUP_COMMAND,MESH_COMMAND五种,这些类型的讲解在下一节。

从代码10中,好像没有与OpenGL相关的代码,有点囧。其实这OpenGL的API调用是在Renderer::drawBatched Quads()、BatchCommand::execute()中。在代码10中,我们也看到在QUAD_COMMAND类型中调用了drawBatchedQuads(),如代码11。在CUSTOM_COMMAND中调用了CustomCommand::execute(),如代码12。在BATCH_COMMAND中调用了BatchCommand::execute(),如代码13。在MESH_COMMAND类型中调用了MeshCommand::preBatchDraw()和MeshCommand::batchDraw()。至于GROUP_COMMAND类型,就递归它组里的成员。

代码11:

void Renderer::drawBatchedQuads()  
{
//TODO we can improve the draw performance by insert material switching command before hand.

int quadsToDraw = 0;
int startQuad = 0;

//Upload buffer to VBO
if(_numQuads <= 0 || _batchedQuadCommands.empty())
{
return;
}

if (Configuration::getInstance()->supportsShareableVAO())
{
//Set VBO data
glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]);

// option 1: subdata
// glBufferSubData(GL_ARRAY_BUFFER, sizeof(_quads[0])*start, sizeof(_quads[0]) * n , &_quads[start] );

// option 2: data
// glBufferData(GL_ARRAY_BUFFER, sizeof(quads_[0]) * (n-start), &quads_[start], GL_DYNAMIC_DRAW);

// option 3: orphaning + glMapBuffer
glBufferData(GL_ARRAY_BUFFER, sizeof(_quads[0]) * (_numQuads), nullptr, GL_DYNAMIC_DRAW);
void *buf = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
memcpy(buf, _quads, sizeof(_quads[0])* (_numQuads));
glUnmapBuffer(GL_ARRAY_BUFFER);

glBindBuffer(GL_ARRAY_BUFFER, 0);

//Bind VAO
GL::bindVAO(_quadVAO);
}
else
{
#define kQuadSize sizeof(_quads[0].bl)
glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]);

glBufferData(GL_ARRAY_BUFFER, sizeof(_quads[0]) * _numQuads , _quads, GL_DYNAMIC_DRAW);

GL::enableVertexAttribs(GL::VERTEX_ATTRIB_FLAG_POS_COLOR_TEX);

// vertices
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, vertices));

// colors
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, colors));

// tex coords
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, texCoords));

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO[1]);
}

//Start drawing verties in batch
for(const auto& cmd : _batchedQuadCommands)
{
auto newMaterialID = cmd->getMaterialID();
if(_lastMaterialID != newMaterialID || newMaterialID == QuadCommand::MATERIAL_ID_DO_NOT_BATCH)
{
//Draw quads
if(quadsToDraw > 0)
{
glDrawElements(GL_TRIANGLES, (GLsizei) quadsToDraw*6, GL_UNSIGNED_SHORT, (GLvoid*) (startQuad*6*sizeof(_indices[0])) );
_drawnBatches++;
_drawnVertices += quadsToDraw*6;

startQuad += quadsToDraw;
quadsToDraw = 0;
}

//Use new material
cmd->useMaterial();
_lastMaterialID = newMaterialID;
}

quadsToDraw += cmd->getQuadCount();
}

//Draw any remaining quad
if(quadsToDraw > 0)
{
glDrawElements(GL_TRIANGLES, (GLsizei) quadsToDraw*6, GL_UNSIGNED_SHORT, (GLvoid*) (startQuad*6*sizeof(_indices[0])) );
_drawnBatches++;
_drawnVertices += quadsToDraw*6;
}

if (Configuration::getInstance()->supportsShareableVAO())
{
//Unbind VAO
GL::bindVAO(0);
}
else
{
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
_batchedQuadCommands.clear();
_numQuads = 0;

代码12:

void CustomCommand::execute()  
{
if(func)
{
func();
}
}

代码13:

void BatchCommand::execute()  
{
// Set material
_shader->use();
_shader->setUniformsForBuiltins(_mv);
GL::bindTexture2D(_textureID);
GL::blendFunc(_blendType.src, _blendType.dst);

// Draw
_textureAtlas->drawQuads();
}

从代码11、代码12、代码13中,我们都看到了这些函数中对OpenGl的API调用来进行渲染。其中特别提醒一下,在CustomCommand::execute()中直接调用的函数是我们设置的回调函数。在这个函数中,我们可以自己使用OpenGL的API进行图形的渲染。这就在第三节中讲如何在Cocos2d-x中自己设置渲染功能中向_customCommand添加的函数。在这里我先给出简便的方式,_customCommand.func = CC_CALLBACK_0(HelloWorld::onDraw, this)。

以上就是把一个完整的渲染的流程都梳理了一片,下面我给出了流程图,如图3。

图3:

图3

第二,RenderCommand的类型

这里的类型讲解主要参考这篇文章中关于RenderComman的类型讲解。

QUAD_COMMAND:QuadCommand类绘制精灵等。

所有绘制图片的命令都会调用到这里,处理这个类型命令的代码就是绘制贴图的openGL代码,下一篇文章会详细介绍这部分代码。

CUSTOM_COMMAND:CustomCommand类自定义绘制,自己定义绘制函数,在调用绘制时只需调用已经传进来的回调函数就可以,裁剪节点,绘制图形节点都采用这个绘制,把绘制函数定义在自己的类里。这种类型的绘制命令不会在处理命令的时候调用任何一句openGL代码,而是调用你写好并设置给func的绘制函数,后续文章会介绍引擎中的所有自定义绘制,并自己实现一个自定义的绘制。

BATCH_COMMAND:BatchCommand类批处理绘制,批处理精灵和粒子

其实它类似于自定义绘制,也不会再render函数中出现任何一句openGL函数,它调用一个固定的函数,这个函数会在下一篇文章中介绍。

GROUP_COMMAND:GroupCommand类绘制组,一个节点包括两个以上绘制命令的时候,把这个绘制命令存储到另外一个_renderGroups中的元素中,并把这个元素的指针作为一个节点存储到_renderGroups[0]中。

第三,如何在Cocos2d-x中自己设置渲染功能

1.第一种方法针对的是整个图层的渲染。

重写visit()函数,并且在visit()函数中直接向CommandQueue添加CustomCommand,设置好回调函数,这个比较直接,如代码14,代码14是子龙山人《基于Cocos2d-x学习OpenGL ES 2.0》第一篇中的部分代码。或者重写draw()函数,并且在draw()函数中向CommandQueue添加CustomCommand,设置好回调函数,这个就比较按照正规的流程走。

代码14:

void HelloWorld::visit(cocos2d::Renderer *renderer, const Mat4 &transform, bool transformUpdated)  
{
Layer::draw(renderer, transform, transformUpdated);

//send custom command to tell the renderer to call opengl commands
_customCommand.init(_globalZOrder);
_customCommand.func = CC_CALLBACK_0(HelloWorld::onDraw, this);
renderer->addCommand(&_customCommand);


}
void HelloWorld::onDraw()
{
//question1: why the triangle goes to the up side
//如果使用对等矩阵,则三角形绘制会在最前面
Director::getInstance()->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
Director::getInstance()->loadIdentityMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
Director::getInstance()->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
Director::getInstance()->loadIdentityMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);

auto glProgram = getGLProgram();

glProgram->use();

//set uniform values, the order of the line is very important
glProgram->setUniformsForBuiltins();
auto size = Director::getInstance()->getWinSize();

//use vao
glBindVertexArray(vao);

GLuint uColorLocation = glGetUniformLocation(glProgram->getProgram(), "u_color");

float uColor[] = {1.0, 1.0, 1.0, 1.0};
glUniform4fv(uColorLocation,1, uColor);
// glDrawArrays(GL_TRIANGLES, 0, 6);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE,(GLvoid*)0);
glBindVertexArray(0);
CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1, 6);
CHECK_GL_ERROR_DEBUG();
Director::getInstance()->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
Director::getInstance()->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

}

从代码14中,我们看到重写visit()函数,在visit()函数中直接向RenderQueue添加RenderCommand,即“renderer->addCommand(&_customCommand);”,由于此RenderCommand类型为CustomCommand,所以要添加处理图形渲染的回调函数,即“_customCommand.func = CC_CALLBACK_0(HelloWorld::onDraw, this);”,这行代码就是添加回调函数的,onDraw()函数中调用OpengGL的API渲染图形。关于func是如何被调用,可以参考上面的代码12上下文的分析。

3.2.第二种方法针对个别精灵。

有时候,我们只要对个别精灵进行特效的处理,这个精灵需要使用我们自己编写的Shader,而图层其他的元素按默认处理就行了。这时候就需要第二种方法了。设置好Shader,向精灵添加Shader,最后在重写draw函数,在draw函数中进行特效的处理,如代码15,代码15是《捕鱼达人3》教程第二节的代码。

代码15:

bool FishLayer::init()  
{
...省略了不相关的代码。
// 将vsh与fsh装配成一个完整的Shader文件。
auto glprogram = GLProgram::createWithFilenames("UVAnimation.vsh", "UVAnimation.fsh");
// 由Shader文件创建这个Shader
auto glprogramstate = GLProgramState::getOrCreateWithGLProgram(glprogram);
// 给精灵设置所用的Shader
m_Sprite->setGLProgramState(glprogramstate);

//创建海龟所用的贴图。
auto textrue1 = Director::getInstance()->getTextureCache()->addImage("tortoise.png");
//将贴图设置给Shader中的变量值u_texture1
glprogramstate->setUniformTexture("u_texture1", textrue1);
//创建波光贴图。
auto textrue2 = Director::getInstance()->getTextureCache()->addImage("caustics.png");
//将贴图设置给Shader中的变量值u_lightTexture
glprogramstate->setUniformTexture("u_lightTexture", textrue2);

//注意,对于波光贴图,我们希望它在进行UV动画时能产生四方连续效果,必须设置它的纹理UV寻址方式为GL_REPEAT。
Texture2D::TexParams tRepeatParams;
tRepeatParams.magFilter = GL_LINEAR_MIPMAP_LINEAR;
tRepeatParams.minFilter = GL_LINEAR;
tRepeatParams.wrapS = GL_REPEAT;
tRepeatParams.wrapT = GL_REPEAT;
textrue2->setTexParameters(tRepeatParams);
//在这里,我们设置一个波光的颜色,这里设置为白色。
Vec4 tLightColor(1.0,1.0,1.0,1.0);
glprogramstate->setUniformVec4("v_LightColor",tLightColor);
//下面这一段,是为了将我们自定义的Shader与我们的模型顶点组织方式进行匹配。模型的顶点数据一般包括位置,法线,色彩,纹理,以及骨骼绑定信息。而Shader需要将内部相应的顶点属性通道与模型相应的顶点属性数据进行绑定才能正确显示出顶点。
long offset = 0;
auto attributeCount = m_Sprite->getMesh()->getMeshVertexAttribCount();
for (auto k = 0; k < attributeCount; k++) {
auto meshattribute = m_Sprite->getMesh()->getMeshVertexAttribute(k);
glprogramstate->setVertexAttribPointer(s_attributeNames[meshattribute.vertexAttrib],
meshattribute.size,
meshattribute.type,
GL_FALSE,
m_Sprite->getMesh()->getVertexSizeInBytes(),
(GLvoid*)offset);
offset += meshattribute.attribSizeBytes;
}

//uv滚动初始值设为0
m_LightAni.x = m_LightAni.y = 0;
return true;
}

void FishLayer::draw(Renderer* renderer, const Mat4 &transform, uint32_t flags)
{
if(m_Sprite)
{
//乌龟从右向左移动,移出屏幕后就回到最右边
auto s = Director::getInstance()->getWinSize();
m_Sprite->setPositionX(m_Sprite->getPositionX()-1);
if(m_Sprite->getPositionX() < -100)
{
m_Sprite->setPositionX(s.width + 10);
}

auto glprogramstate = m_Sprite->getGLProgramState();
if(glprogramstate)
{
m_LightAni.x += 0.01;
if(m_LightAni.x > 1.0)
{
m_LightAni.x-= 1.0;
}
m_LightAni.y += 0.01;
if(m_LightAni.y > 1.0)
{
m_LightAni.y-= 1.0;
}
glprogramstate->setUniformVec2("v_animLight",m_LightAni);
}
}
Node::draw(renderer,transform,flags);
}

从代码15中,我们可以看到先使用OpengGL的API创建自己的Shader,然后再把m_sprite的Shader设置为自己的Shader即“m_Sprite->setGLProgramState(glprogramstate);”,这是给精灵设置所用的Shader,这就是针对个别的精灵,而不是整个图层。接着在draw()中,如果精灵已生成,每次调用draw()函数都改变Shader中参数,以达到特别的效果。

Cocos2d-x之自定义控件ScrollBar的设计

本文来自 Sharing_Li(李小华)的投稿,在此特别感谢他!

我们在使用cocos2dx的TableView和ScrollView的时候,如果要显示的内容非常多,我们不方便确认当前浏览的内容处于什么位置,也不方便快速浏览。这时我们需要一个滚动条来帮忙,但cocos2dx里面没有这个控件,所以呢,这里我给大家设计了一个滚动条控件ScrollBar,可以非常方便的使用。讲解之前,先看看效果图吧:

20150113220247440

看了效果图之后,我们来确认下功能需求:

1、通过滑动TableView或ScrollView,右边的滑块也跟着滑动;

2、TableView或ScrollView滑到底时,右边的滑块也滑到底了;

3、当点击右边的滑块滑动时,左边的TableView或ScrollView也跟着滑动;

4、滑块滑到底时,TableView或ScrollView也滑到底了;

5、当点击右边的滑块背景时,即示例黄色部分,TableView或ScrollView和滑块都跟者滑动;

6、当TableView或ScrollView的内容动态增加时,滑块的大小也动态改变;

7、控件水平和垂直都可以使用,示例只展示了垂直效果,水平同理;

大致的功能就这么多啦,那么就来看看代码怎么写吧,我们定义一个类ScrollBar:

ScrollBar.h头文件

#ifndef _SCROLL__BAR__H_
#define _SCROLL__BAR__H_

#include "cocos2d.h"
#include "cocos-ext.h"

USING_NS_CC;
USING_NS_CC_EXT;

enum SclBarDirection
{
	DIR_NODIR = 0,
	DIR_VERTICAL,
	DIR_HORIZENTAL,
};

class ScrollBar : public cocos2d::Layer
{
public:
	ScrollBar();
	~ScrollBar();
	/**
	* 因为九宫图不能缩小到比实际图片要小,所以传入的图片的实际大小要足够小,否则slider的大小会有问题
	*/
	static ScrollBar * create(Scale9Sprite * bar_bg,Scale9Sprite * bar_slider,TableView * tableView,SclBarDirection dir);
	static ScrollBar * create(const char * bar_bgFile,const char * bar_sliderFile,TableView * tableView,SclBarDirection dir);
	bool myInit(Scale9Sprite * bar_bg,Scale9Sprite * bar_slider,TableView * tableView,SclBarDirection dir);

protected:
	virtual bool onTouchBegan(Touch* touch, Event* pEvent);
	virtual void onTouchMoved(Touch *pTouch, Event *pEvent);
	virtual void onTouchEnded(Touch *pTouch, Event *pEvent);

	virtual void update(float dt) override;
	/**
	* 动态改变slider的大小
	*/
	void updateSlider();

private:
	TableView * m_pTarget;
	Scale9Sprite * m_pBg;
	Scale9Sprite * m_pSlider;
	SclBarDirection m_direction;
	Size m_preContentSize;
	Size m_viewSize;
	bool m_sliderTouched;
	Vec2 m_firstTouch;
	Vec2 m_sliderCurPos;
	Vec2 m_targetCurPos;
};

#endif

代码中已给出了部分注释,我们用了九宫图Scale9Sprite来显示滑块和滑块背景图片,因为Scale9Sprite在缩放时,图片效果很好,不会因为拉伸而使得图片效果变质。值得注意的是,如果你的图片的实际大小是size这么大,那么Scale9Sprite不能缩小到比size小,而相反的会放大。所以传入的图片要足够的小,下面再来看看具体的实现:

首先初始化数据:

/**
* 初始化各个数据
*/
bool ScrollBar::myInit(Scale9Sprite * bar_bg,Scale9Sprite * bar_slider,TableView * tableView,SclBarDirection dir)
{
	if (!Layer::init())
	{
		return false;
	}
	
	m_pBg = bar_bg;
	m_pSlider = bar_slider;
	m_pTarget = tableView;
	m_direction = dir;
	m_preContentSize = m_pTarget->getContainer()->getContentSize();
	m_viewSize = m_pTarget->getViewSize();
	
	if (m_direction == DIR_VERTICAL)
	{
		m_pBg->setContentSize(Size(m_pBg->getContentSize().width,m_viewSize.height));
		m_pBg->setPosition(Vec2(m_pBg->getContentSize().width / 2,0));
		m_pSlider->setPositionX(m_pBg->getContentSize().width / 2);
	} 
	else if (m_direction == DIR_HORIZENTAL)
	{
		m_pBg->setContentSize(Size(m_viewSize.width,m_pBg->getContentSize().height));
		m_pBg->setPosition(Vec2(0,-m_pBg->getContentSize().height / 2));
		m_pSlider->setPositionY(-m_pBg->getContentSize().height / 2);
	}
	
	this->addChild(m_pBg,0);

	this->updateSlider();

	this->addChild(m_pSlider,1);

	this->scheduleUpdate();

	auto listenerT = EventListenerTouchOneByOne::create();
	listenerT->onTouchBegan = CC_CALLBACK_2(ScrollBar::onTouchBegan,this);
	listenerT->onTouchMoved = CC_CALLBACK_2(ScrollBar::onTouchMoved,this);
	listenerT->onTouchEnded = CC_CALLBACK_2(ScrollBar::onTouchEnded,this);
	listenerT->setSwallowTouches(false);
	Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listenerT,this);

	return true;
}

我们来看看updateSlider函数如何改变滑块slider的大小:

void ScrollBar::updateSlider()
{
	float ratio = 0.0;
	if (m_direction == DIR_VERTICAL)
	{
		 ratio = m_viewSize.height / m_preContentSize.height;
		 m_pSlider->setContentSize(Size(m_pSlider->getContentSize().width,m_viewSize.height * ratio));
	} 
	else if (m_direction == DIR_HORIZENTAL)
	{
		ratio = m_viewSize.width / m_preContentSize.width;
		m_pSlider->setContentSize(Size(m_viewSize.width * ratio,m_pSlider->getContentSize().height));
	}
	//如果要显示的内容的尺寸比视图大小小,则隐藏滑块slider
	this->setVisible( !(ratio >= 1) );
}

我弄了一个定时器,来监听TableView或ScrollView的滑动,即偏移:

void ScrollBar::update(float dt)
{
	//判断当前内容是否有增减,因为内容的增减会影响ContenSize,从而修改slider的大小
	auto curContentSize = m_pTarget->getContainer()->getContentSize();
	if ( !(fabsf(curContentSize.height - m_preContentSize.height) <= 0.00001)  || 
		!(fabsf(curContentSize.width - m_preContentSize.width) <= 0.00001) )
	{
		m_preContentSize = curContentSize;
		this->updateSlider();
	}

	//设置slider的位置
	if (m_direction == DIR_VERTICAL)
	{
		//调整滑块的位置
		auto curOffset = m_pTarget->getContentOffset() + (m_preContentSize - m_viewSize) / 2;
		auto sliderOffset = curOffset.y / (m_viewSize.height - curContentSize.height) * 
			(m_viewSize.height - m_pSlider->getContentSize().height);
		//判断滑块是否滑出界限
		if (fabsf(sliderOffset) > (m_viewSize.height - m_pSlider->getContentSize().height) / 2)
		{
			return ;
		}
		m_pSlider->setPositionY(sliderOffset);
	}
	else if (m_direction == DIR_HORIZENTAL)
	{
		auto curOffset = m_pTarget->getContentOffset() - (m_preContentSize - m_viewSize) / 2;
		auto sliderOffset = -curOffset.x / (m_viewSize.width - curContentSize.width) * 
			(m_viewSize.width - m_pSlider->getContentSize().width);
		if (fabsf(sliderOffset) > (m_viewSize.width - m_pSlider->getContentSize().width) / 2)
		{
			return ;
		}
		m_pSlider->setPositionX(sliderOffset);
	}
}

注意的是:TableView或ScrollView的可滑动大小和滑块的可滑动大小不一样,所以二者要想同步的话,要成比例滑动。

再来看看滑块的滑动以及滑块背景点击这一块的实现:

先看看onTouchBegan:

bool ScrollBar::onTouchBegan(Touch* touch, Event* pEvent)
{
	m_sliderCurPos = m_pSlider->getPosition();
	m_targetCurPos = m_pTarget->getContentOffset();
	auto touchPoint = touch->getLocation();
	m_firstTouch = touchPoint;
	//将触摸点转为在当前子层下的坐标
	touchPoint = this->convertToNodeSpace(touchPoint);
	//只响应点击了滑块背景的触摸
	if (!m_pBg->getBoundingBox().containsPoint(touchPoint))
	{
		return false;
	}
	//如果先点击了滑块,则设置标志
	if (m_pSlider->getBoundingBox().containsPoint(touchPoint))
	{
		m_sliderTouched = true;
	}
	else//如果没有点击滑块,则点击的是滑块背景图
	{
		if (m_direction == DIR_VERTICAL)
		{
			//通过调整m_pTarget的偏移,从而调整了滑块slider的位置,因为update函数会一直监听m_pTarget的偏移
			auto offset = touchPoint.y - m_sliderCurPos.y;
			if (touchPoint.y <= 0)
			{
				offset += m_pSlider->getContentSize().height / 2;
			} 
			else
			{
				offset -= m_pSlider->getContentSize().height / 2;
			}
			auto newOff = m_targetCurPos.y + offset / (m_pSlider->getContentSize().height - m_viewSize.height) 
				* (m_preContentSize.height - m_viewSize.height);
			m_pTarget->setContentOffset(Vec2(0,newOff));
		}
		else if (m_direction == DIR_HORIZENTAL)
		{
			auto offset = touchPoint.x - m_sliderCurPos.x;
			if (touchPoint.x <= 0)
			{
				offset += m_pSlider->getContentSize().width / 2;
			} 
			else
			{
				offset -= m_pSlider->getContentSize().width / 2;
			}
			auto newOff = m_targetCurPos.x + offset / (m_viewSize.width - m_pSlider->getContentSize().width) 
				* (m_preContentSize.width - m_viewSize.width);
			m_pTarget->setContentOffset(Vec2(newOff,0));
		}
	}
	return true;
}

这里有一点要注意的时,我么不需要在触摸函数中修改滑块的位置,因为我们通过修改ScrollView或TableView的偏移,从而间接地改变了滑块的位置,所以我们只需要正确的设置好ScrollView或TableView的位置就可以了,update函数会帮我们解决滑块的位置。

再来看看onTouchMoved:

void ScrollBar::onTouchMoved(Touch *pTouch, Event *pEvent)
{
	//只响应点击了滑块的移动
	if (m_sliderTouched)
	{
		auto offPos = pTouch->getLocation() - m_firstTouch;
		if (m_direction == DIR_VERTICAL)
		{
			//通过调整m_pTarget的偏移,从而调整了滑块slider的位置,因为update函数会一直监听m_pTarget的偏移
			auto newOff = m_sliderCurPos.y + offPos.y;
			//判断滑块是否滑出界限
			if (fabsf(newOff) > (m_viewSize.height - m_pSlider->getContentSize().height) / 2)
			{
				(newOff < 0 ? (newOff = (m_pSlider->getContentSize().height - m_viewSize.height) / 2) : 
					(newOff = (m_viewSize.height - m_pSlider->getContentSize().height) / 2));
			}
			newOff -= m_sliderCurPos.y;
			m_pTarget->setContentOffset(Vec2(0,
				m_targetCurPos.y + newOff / (m_pSlider->getContentSize().height - m_viewSize.height) 
				* (m_preContentSize.height - m_viewSize.height)));
		}
		else if (m_direction == DIR_HORIZENTAL)
		{
			auto newOff = m_sliderCurPos.x + offPos.x;
			if (fabsf(newOff) > (m_viewSize.width - m_pSlider->getContentSize().width) / 2)
			{
				(newOff < 0 ? (newOff = (m_pSlider->getContentSize().width - m_viewSize.width) / 2) : 
					(newOff = (m_viewSize.width - m_pSlider->getContentSize().width) / 2));
			}
			newOff -= m_sliderCurPos.x;
			m_pTarget->setContentOffset(Vec2(m_targetCurPos.x + newOff / (m_viewSize.width - m_pSlider->getContentSize().width) 
				* (m_preContentSize.width - m_viewSize.width),0));
		}
	}
}

最后,我们看看onTouchEnded:

void ScrollBar::onTouchEnded(Touch *pTouch, Event *pEvent)
{
	m_sliderTouched = false;
}

很简单,就一句,还原下滑块slider的触摸状态就可以了。到这里,自定义控件ScrollBar已经实现了。那么我们在来看看在代码中如何使用ScrollBar。同样也很简单,看下面的示例:

    m_tableView = TableView::create(this,viewSize);  
    m_tableView->ignoreAnchorPointForPosition(false);  
    m_tableView->setAnchorPoint(Vec2(0.5,0.5));  
    m_tableView->setPosition(Vec2(viewSize.width / 2,viewSize.height / 2));  
    m_tableView->setDirection(ScrollView::Direction::VERTICAL);  
    m_tableView->setDelegate(this);  
    m_tableView->setVerticalFillOrder(TableView::VerticalFillOrder::TOP_DOWN);  
    m_tableView->reloadData();  
    pView->addChild(m_tableView);  
  
    auto scrollBar_vr = ScrollBar::create("scrollbar/vr_slider_bg.png","scrollbar/vr_slider.png",m_tableView,DIR_VERTICAL);  
    scrollBar_vr->setPosition(Vec2(viewSize.width,viewSize.height / 2));  
    pView->addChild(scrollBar_vr,2);  

创建你的TablewView或ScrollView后,只需要创建ScrollBar,设置位置,添加到父节点共三步就可以轻松完成。

这次的内容就讲完了,有疑惑的可以留言。

Demo资源下载地址:ScrollBarDemo

Cocos2d-x之TableView和ScrollView的混合使用

本文来自 Sharing_Li(李小华)的投稿,在此特别感谢他!

玩过《开心消消乐》这款游戏的人,应该知道里面有这样一处设计,如下图:



我们可以左右滑动界面,也可以上下滑动界面,左右滑动的时候不能上下滑动,上下滑动的时候不能左右滑动。这种效果可以用TableView和ScrollView来组合实现,即先弄一个ScrollView,然后把2个TableView当作内容放入这个ScrollView中就可以了,这种UI设计也应用在《开心消消乐》其好友信件中,只不过多了一个TableView。

接下来将进行代码讲解,cocos2dx的版本是3.2,先展示一下实现之后的效果图:

11
看完效果图,再看正文,定义一个类:CombineView

头文件:CombineView.h

#ifndef __COMBINE_VIEW_H__
#define __COMBINE_VIEW_H__

#include "cocos2d.h"
#include "extensions/cocos-ext.h"

USING_NS_CC;
USING_NS_CC_EXT;

enum Table
{
	Table_Left = 0,
	Table_Center,
	Table_Right
};

class CombineView : public Layer,TableViewDataSource,TableViewDelegate
{
public:
	CombineView();
	~CombineView();

	virtual bool init();
	static cocos2d::Scene * create();

	virtual Size tableCellSizeForIndex(TableView *table, ssize_t idx);
	virtual TableViewCell* tableCellAtIndex(TableView *table, ssize_t idx);
	virtual ssize_t numberOfCellsInTableView(TableView *table);
	virtual void tableCellTouched(TableView* table, TableViewCell* cell);

	virtual void scrollViewDidScroll(ScrollView* view);
	virtual void scrollViewDidZoom(ScrollView* view);

public:
	void SetTouch(bool isTouched);
	//对scrollview的调整
	void adjustScrollView(float offset);
private:
	ScrollView * m_scrollView;
	TableView * m_leftTable;
	TableView * m_centerTable;
	TableView * m_rightTable;
	//scrollview当前显示的页数
	int m_curPage;
	//第一个触摸点
	Vec2 m_firstPoint;
	//scrollview的偏移
	Vec2 m_offset;
	//判断第一次滑动方向
	bool m_horizontal;
	bool m_vertical;
	//View的大小
	Size m_viewSize;
};

#endif // !__COMBINE_VIEW_H__

再看看cpp文件的实现,这里对主要的代码进行讲解,想要完整代码和资源,请到文章末尾点击下载(0下载积分)。

我们写代码,要养成初始化成员变量的习惯,这样可以避免一些意想不到的错误。同时记住不用的资源要记得释放。

CombineView::CombineView()
{
	m_scrollView = NULL;
	m_leftTable = NULL;
	m_centerTable = NULL;
	m_rightTable = NULL;
	m_curPage = 0;
	m_firstPoint = Vec2(0,0);
	m_offset = Vec2(0,0);
	m_vertical = false;
	m_horizontal = false;
	m_viewSize = Size(0,0);
}

如效果图所示,我们要搞一个scrollview,这家伙呢,怀了5个月的三胞胎,分别是三个tableview。为了区别这三个儿子(喂,你怎么知道都是男的而不是女的),我们要给他们取名字,因为他们仨要共用一个函数即tableCellAtIndex,如果不取名,怎么知道谁是老二老三呢, 如头文件中定义的枚举类。

        m_scrollView = ScrollView::create();
	m_scrollView->setViewSize(m_viewSize);
	m_scrollView->setContentOffset(Point::ZERO);
	m_scrollView->setDelegate(this);
	m_scrollView->setDirection(ScrollView::Direction::HORIZONTAL);
	m_scrollView->setAnchorPoint(Point::ZERO);
	m_scrollView->setPosition(Vec2::ZERO);
	m_scrollView->setTouchEnabled(false);//因为我们不需要scrollview的触摸,因为太糟糕~
	pView->addChild(m_scrollView);
	
	//添加内容
	auto pContainer = Layer::create();
	pContainer->setContentSize(Size(m_viewSize.width * 3,m_viewSize.height));
	pContainer->setAnchorPoint(Point::ZERO);
	pContainer->setPosition(Vec2::ZERO);
	m_scrollView->setContainer(pContainer);

	//添加tabelview
	auto containerSize = pContainer->getContentSize();
	m_leftTable = TableView::create(this, ViewSize);
	m_leftTable->setTag(Table_Left);
	m_leftTable->ignoreAnchorPointForPosition(false);
	m_leftTable->setAnchorPoint(Vec2(0.5,0.5));
	m_leftTable->setPosition(Vec2(containerSize.width / 6,containerSize.height / 2));
	m_leftTable->setDirection(ScrollView::Direction::VERTICAL);
	m_leftTable->setDelegate(this);
	m_leftTable->setVerticalFillOrder(TableView::VerticalFillOrder::TOP_DOWN);
	m_leftTable->reloadData();
	pContainer->addChild(m_leftTable);

	m_centerTable = TableView::create(this, ViewSize);	
	m_centerTable->setTag(Table_Center);
	m_centerTable->ignoreAnchorPointForPosition(false);
	m_centerTable->setAnchorPoint(Vec2(0.5,0.5));
	m_centerTable->setPosition(Vec2(containerSize.width / 2,containerSize.height / 2));
	m_centerTable->setDirection(ScrollView::Direction::VERTICAL);
	m_centerTable->setDelegate(this);
	m_centerTable->setVerticalFillOrder(TableView::VerticalFillOrder::TOP_DOWN);
	m_centerTable->reloadData();
	pContainer->addChild(m_centerTable);

	m_rightTable = TableView::create(this, ViewSize);
	m_rightTable->setTag(Table_Right);
	m_rightTable->ignoreAnchorPointForPosition(false);
	m_rightTable->setAnchorPoint(Vec2(0.5,0.5));
	m_rightTable->setPosition(Vec2(containerSize.width / 6 * 5,containerSize.height / 2));
	m_rightTable->setDirection(ScrollView::Direction::VERTICAL);
	m_rightTable->setDelegate(this);
	m_rightTable->setVerticalFillOrder(TableView::VerticalFillOrder::TOP_DOWN);
	m_rightTable->reloadData();
	pContainer->addChild(m_rightTable);

然后我们再来看看触摸函数的实现,首先是touchbegan:

        auto listenerT = EventListenerTouchOneByOne::create();

	listenerT->onTouchBegan = [=](Touch * touch,Event * pEvent){
		m_firstPoint = touch->getLocation();
		m_offset = m_scrollView->getContentOffset();
		if (!m_scrollView->getBoundingBox().containsPoint(m_firstPoint))
		{
			return false;
		}
		return true;
	};

简洁明了(.......),然后再看touchmoved:

        listenerT->onTouchMoved = [=](Touch * touch,Event * pEvent){
		auto movePoint = touch->getLocation();
		auto distance = movePoint.x - m_firstPoint.x;

		if ((distance > 0 && this->m_curPage == 0) || (distance < 0 && this->m_curPage == 2))
		{
			return;
		}

		//限制滑动方向,避免scorll和table同时滑动
		if (fabs(movePoint.y - m_firstPoint.y) / fabs(distance) > 0.7 || m_vertical)
		{
			if (!m_horizontal)
			{
				m_vertical = true;
			}
			return;
		}
		else //水平
		{
			if (!m_vertical)
			{
				m_horizontal = true;
			}
		}
		if (m_horizontal)
		{
			this->SetTouch(false);
		}
		m_scrollView->setContentOffset(Vec2(distance + m_offset.x,0));
	};

这一段代码的意思是:如果你先垂直滑动,那么就将m_vertical设置为true,这样你就不能水平滑动了;如果你先水平滑动,就将m_horizontal设置为true,因而调用函数SetTouch,对着三个孩子tableview唱摇篮曲,要他们乖乖睡觉不要乱动。然后再来看看touchended:

       listenerT->onTouchEnded = [=](Touch * touch,Event * pEvent){
		auto endPoint = touch->getLocation();
		auto distance = endPoint.x - m_firstPoint.x;
		//优化滑动效果
		bool flag = false;
		if (fabsf(distance) < 60)
		{
			flag = true;
			if (distance < 0)
			{
				m_curPage--;
			}
			else if (distance > 0)
			{
				m_curPage++;
			}
		}

		//限制滑动方向,避免scroll和table同时滑动
		if (m_vertical)
		{
			m_vertical = false;
			if (flag)
			{
				if (distance > 0)
				{
					m_curPage--;
				}
				else if (distance < 0)
				{
					m_curPage++;
				}
			}
			
			return ;
		}
		else
		{
			this->SetTouch(true);
		}

		this->adjustScrollView(distance);
		m_horizontal = false;
	};

这一段代码的意思是:if (fabsf(distance) < 60)这个if语句是对滑动效果的优化,如果滑动很小距离,那么就忽视这次滑动,视图还是老样子,效果图如下:

22

这下应该一目了然了吧,接下来的代码是判断是先垂直滑动还是水平滑动,如果是先垂直,则直接return,return之前呢要还原m_curPage的值。如果是先水平,则要把三个熟睡的孩子搞醒。然后是对scrollview最终显示界面的调整:

void CombineView::adjustScrollView(float offset)
{
	if (offset < 0)
	{
		m_curPage++;
	}
	else if (offset > 0)
	{
		m_curPage--;
	}

	if (m_curPage < 0)
	{
		m_curPage = 0;
	}
	else if (m_curPage > 2)
	{
		m_curPage = 2;
	}

	auto adjustPoint = Vec2(-m_viewSize.width * m_curPage,0);
	m_scrollView->setContentOffsetInDuration(adjustPoint,0.1f);
}

未列出的部分代码如下:

TableViewCell* CombineView::tableCellAtIndex(TableView *table, ssize_t idx)
{
	auto cell = table->dequeueCell();
	auto cellSize = this->tableCellSizeForIndex(table, idx);
	auto tag = table->getTag();

	if (!cell)
	{
		cell = new TableViewCell();
		cell->autorelease();
		Sprite * pCellBg = NULL;
		Label * pNum = NULL;
		Sprite * pIcon = NULL;
		switch (tag)
		{
		case Table_Left:
			{
				pCellBg = Sprite::create("combineview/cell.png");
				pNum = Label::createWithTTF("1","fonts/Marker Felt.ttf",20);
				pIcon = Sprite::create("combineview/book.png");
			}
			break;
		case Table_Center:
			{
				pCellBg = Sprite::create("combineview/cell2.png");
				pNum = Label::createWithTTF("2","fonts/Marker Felt.ttf",20);
				pIcon = Sprite::create("combineview/plane.png");
			}
			break;
		case Table_Right:
			{
				pCellBg = Sprite::create("combineview/cell3.png");
				pNum = Label::createWithTTF("3","fonts/Marker Felt.ttf",20);
				pIcon = Sprite::create("combineview/setting.png");
			}
		default:
			break;
		}
		pCellBg->setPosition(Vec2(cellSize.width / 2,cellSize.height / 2));
		cell->addChild(pCellBg);
		pNum->setColor(Color3B(255,0,0));
		pNum->setPosition(Vec2(cellSize.width * 0.1,cellSize.height / 2));
		cell->addChild(pNum);
		pIcon->setPosition(Vec2(cellSize.width * 0.85,cellSize.height / 2));
		pIcon->setScale(0.2);
		cell->addChild(pIcon);
	}
	return cell;
}
void CombineView::SetTouch(bool isTouched)
{
	m_leftTable->setTouchEnabled(isTouched);
	m_centerTable->setTouchEnabled(isTouched);
	m_rightTable->setTouchEnabled(isTouched);
}

最后,完了。。。。。。。。。。才怪!

代码其实有问题,我故意留了一个bug,不知道大家发现没,这个bug不解决的话,程序跑起来会崩溃的。如果按照我之前的代码来运行的话,会在tableCellAtIndex函数中崩溃,这是为什么呢?因为我们在创建tableview的时候,给每个tableview设置tag并没有成功,那为什么没成功呢?因为我们还没设置好tag的时候,tableCellAtIndex这斯就跑起来了,我们通过table->getTag(),其实是取不到tag的,既然取不到,那么之后就不能创建图片文字,会调用空指针,所以程序就BOOM了。那么罪魁祸首就是TableView::create(this,ViewSize);这个家伙了,我们调试跟踪进源码,如下:

TableView* TableView::create(TableViewDataSource* dataSource, Size size, Node *container)
{
    TableView *table = new TableView();
    table->initWithViewSize(size, container);
    table->autorelease();
    table->setDataSource(dataSource);
    table->_updateCellPositions();
    table->_updateContentSize();

    return table;
}

倒数第二句table->_updateContentSize();里面会调用tableCellAtIndex这个函数。那么找到问题了该怎么解决呢,难懂要改源码?不用,我们可以这样创建tableview,如下:

        //m_rightTable = TableView::create(this, ViewSize);	
	m_rightTable = new TableView();
	m_rightTable->initWithViewSize(m_viewSize, NULL);
	m_rightTable->autorelease();
	m_rightTable->setDataSource(this);

那么为什么不把table->_updateCellPositions();也搞进来,因为这是保护成员函数,所以不能访问,而且也用不上,以后遇到类似的问题也可以这样解决。然后把三个tableview改过来就ok啦。

代码及资源下载处:combineview

怎样制作基于Cocos2d-x的SLG游戏-第7章

原创: @涵紫任珊

怎样制作基于Cocos2d-x的SLG游戏-第1章
怎样制作基于Cocos2d-x的SLG游戏-第2章
怎样制作基于Cocos2d-x的SLG游戏-第3章
怎样制作基于Cocos2d-x的SLG游戏-第4章
怎样制作基于Cocos2d-x的SLG游戏-第5章
怎样制作基于Cocos2d-x的SLG游戏-第6章
怎样制作基于Cocos2d-x的SLG游戏-第7章

本章我们将继续学习SLG游戏的制作,本章教程将是该系列游戏的最后一章。在开始之前,我先要对各位翘首以盼等候很久的童鞋们说声抱歉,由于此前事情太多,所以这一系列文章一拖再拖,迟迟没有更新,还望见谅啦!

下面我们回到正题,在上一篇文章中,我们已经讲到了各种操作界面的创建,接下来我们来创建其中相比下较难一点的计时面板,也可以说是计时层。最终我们要实现的游戏效果如下图所示:

png.jpg

计时面板类

这个计时面板是在播种的时候同未成熟幼苗一起创建的,每当播洒一颗种子时,该处坐标上就会创建一个隐藏的计时面板;当该处坐标再次处于选中状态,那么隐藏的计时面板就会显示出来;直到时间耗尽时,该计时面板会从场景中移除。

定义计时面板类

对于计时面板来说,需要注意以下几点:

  • 首先,这里计时面板是一个用进度条表示时间进度的控件项;
  • 其次,对于不同的农作物而言,它们的成熟时间,标题标示都是不相同的;
  • 在一个计时面板的生命周期中,我们需要不停的更新面板状态:进度条不断的减短,时间不断的减少;
  • 最后在时间耗尽时,移除该面板。

效果如下:
result
根据以上分析结果,计时面板的定义如下:

class TimingLayer: public Layer
{
public:
    virtual bool init() override;
    static TimingLayer* create(Vec2 pos, CropsType type);
    virtual void updateProgressBar(float dt);// 更新

    CC_SYNTHESIZE(bool, timeOut, TimeOut);    // 记录时间是否有用
    CC_SYNTHESIZE(Vec2, timingLayerPos, TimingLayerPos);// 标示TimingLayer的位置
    void setParam(CropsType type);            // 设置TimingLayer属性
private:
    ProgressTimer* progressBar;             // 进度条
    Label* nameLabel;                        // 显示标示了计时面板所属类型的文本项
    Label* timeLabel;                        // 显示剩余时间的文本项

    int counter;                            // 计时项
    int growUpTime;                            // 农作物成熟所需时间,也就是计时面板的生命周期
    float percent;                            // 进度条的百分比
};

我们通过create(Vec2 pos, CropsType type)两个属性来创建计时面板,其一是坐标项,用它来标示计时面板的“位置”,每个地图位置上至多只能有一个计时面板;其二是农作物类型,通过它可以确定计时面板的时间和标题项。

计时面板的实现

首先我们先来看看TimingLayer的初始化,如下:

bool TimingLayer::init()
{
   if (!Layer::init())
    {
        return false;
    }
    counter= 1;
    percent = 100;
    // 1
    auto progressBarBg = Sprite::create("progressBg.png");
    progressBarBg->setAnchorPoint(Vec2(0, 0));
    progressBarBg->setPosition(Vec2(0,  0 ));
    addChild(progressBarBg);
    // 2
    progressBar = ProgressTimer::create(Sprite::create("progressBar.png"));
    progressBar->setType(ProgressTimer::Type::BAR);
    progressBar->setMidpoint(Point(0, 0.5f));
    progressBar->setBarChangeRate(Point(1, 0));
    progressBar->setPercentage(percent);
    progressBar->setAnchorPoint(Point(0.5f, 0.5f));
    progressBar->setPosition(Point(progressBarBg->getContentSize().width / 2,  progressBarBg->getContentSize().height /2 ));
    progressBarBg->addChild(progressBar);
    // 3
    nameLabel = Label::createWithBMFont("fonts/Font30.fnt"," ");
    nameLabel->setPosition(Vec2(progressBarBg->getContentSize().width / 2,  progressBarBg->getContentSize().height ));
    nameLabel->setAnchorPoint(Vec2(0.5f, 0.0f));
    progressBarBg->addChild(nameLabel);
    // 4
    timeLabel = Label::createWithBMFont("fonts/Font30.fnt","");
    timeLabel->setPosition(Vec2(progressBarBg->getContentSize().width / 2,  - progressBarBg->getContentSize().height ));
    timeLabel->setAnchorPoint(Vec2(0.5f, 0.0f));
    progressBarBg->addChild(timeLabel);
    // 5
    this->schedule(schedule_selector(TimingLayer::updateProgressBar), 1.0f);
    return true;
}
  1. 创建并添加进度条的背景图片,这样进度条看着才更明显。
  2. 创建进度条并把它添加到背景图片上,这里ProgressTimer是Cocos2d-x自带的进度条类。
    • ProgressTimer有两种类型:一种是环形,一种是条形,使用ProgressTimer时我们需要通过setType方法指明它所属类型。
    • 另外,setMidpoint方法设置进度条的起始点,(0,y)表示最左边,(1,y)表示最右边,(x,1)表示最上面,(x,0)表示最下面。
    • setBarChangeRate方法用来设置进度条变化方向的,如果不用变化的方向,则设置该方向为0,否则设置为1。所以(1,0)表示横方向,(0,1)表示纵方向。
    • ProgressTimer有一个非常最要的percentage属性。它代表了当前进度条的进度值,这也是为什么我们在类中定义了percent属性的原因。如果要让一个进度条正常的显示出来,那么percentage的值必须大于0。setPercentage方法能设置ProgressTimer的percentage值。
  3. 创建标示了计时面板所属类型的文本项,也就是用它来显示计时面板的标题。这里通过Label的createWithBMFont函数来创建,其中Font30.fnt字体是我通过Glyph Designer(一款Mac下的字库图集制作工具,Windows下可使用Hiero和BMFont)生成的位图字体,里面包含了本游戏所需的全部字符。 createFont.png
  4. 创建并添加显示剩余时间的文本项。
  5. 每秒更新一次updateProgressBar函数。

初始化TimingLayer的后,接下来我们来创建TimingLayer:

TimingLayer* TimingLayer::create(Vec2 pos, CropsType type)
{
    TimingLayer *pRet = new TimingLayer();
    if (pRet && pRet->init())
    {
        pRet->setTimingLayerPos(pos);
        pRet->setParam(type);      
        pRet->autorelease();
        return pRet;
    }
    else
    {
        delete pRet;
        pRet = NULL;
        return NULL;
    }
}

其中setTimingLayerPos方法设置TimingLayer所处的位置,setParam方法根据传入的CropsType类型设置TimingLayer的相关属性,如下代码所示:

void TimingLayer::setParam(CropsType type)
{
    switch (type) {
        case WHEAT:
            nameLabel->setString("小麦");// 标题标示
            growUpTime = 60;            // 成熟时间
            break;
        case CORN:
            nameLabel->setString("玉米");
            growUpTime = 120;
            break;
        case CARROT:
            nameLabel->setString("胡萝卜");
            growUpTime = 180;
            break;          
        default:
            break;
    }
}

最后,我们来看看如何更新TimingLayer的进度条等状态,代码如下:

void TimingLayer::updateProgressBar(float dt)
{
    counter++;
    percent = 100 - float(counter) / float(growUpTime ) * 100; // 1
    if(percent <= 100 && percent > 0 ) // 2
    {
        progressBar->setPercentage(percent);
        timeLabel->setString(std::to_string(growUpTime - counter) + " 秒");
    }
    else // 3
    {
        this->setTimeOut( true);
        this->unschedule(schedule_selector(TimingLayer::updateProgressBar));
        this->removeFromParent();
    }
}
  1. 注意计算百分比时,计数器/成熟时间的值应该是一个“小数”类型,所以要将它们转换成float类型再进行计算。
  2. 当percent的值在0到100的范围内时,更新进度条的百分比,同时更新显示的剩余时间。
  3. 当percent超出范围时,设置timeOut属性为true,停止更新updateProgressBar函数,并且移除TimingLayer。

现在我们的计时面板就算创建好了,接下来我们就可以在播种时创建一个这样的计时器面板了。

添加计时面板

回到主场景,我们在GameScene中定义一个向量来保存游戏中所有的TimingLayer,这样遍历该向量可以控制所有计时面板的显示和隐藏。

Vector timingVector;

在播种的时候,计时面板会同农作物幼苗一起“创建”,所以下面我们在播种的地方创建相应的计时面板,即像下面一样修改update函数:

        switch (type)
        {
            case WHEAT:
            {
                map->getLayer("goodsLayer")->setTileGID(18, touchObjectPos);
                createTimingLayer(WHEAT);
                this->removeChild(seedPanel);
            }
                break;
            case CORN:
            {
                map->getLayer("goodsLayer")->setTileGID(20, touchObjectPos);
                createTimingLayer(CORN);
                this->removeChild(seedPanel);
            }
                break;
            case CARROT:
            {
                map->getLayer("goodsLayer")->setTileGID(22, touchObjectPos);
                createTimingLayer(CARROT);
                this->removeChild(seedPanel);
            }
                break;
            default:
                break;
        }

其中createTimingLayer函数是用于创建计时面板的方法,其代码段如下所示:

void GameScene::createTimingLayer(CropsType type)
{
    auto timingLayer = TimingLayer::create(touchObjectPos, type); // 1
    auto screenPos = this->convertToScreenCoord(touchObjectPos); // 2
    timingLayer->setPosition(screenPos);
    timingLayer->setVisible(false);  // 3
    bgSprite->addChild(timingLayer, 10); // 4

    this->timingVector.pushBack(timingLayer); // 5 
}
  1. 在touchObjectPos位置上创建一个type类型的计时面板。
  2. 将touchObjectPos转换成场景坐标,并设置计时面板的位置到该处。
  3. 创建的计时面板初始状态下是不可以见的,所以要把它隐藏起来。
  4. convertToScreenCoord方法转换的场景坐标是相对于整个地图的,所以我们把这个计时面板添加到地图的背景上。
  5. 把创建的计时面板压入向量中,便于统一管理。

显示隐藏计时面板

现在当我们“播种”的时候就可以为每一个幼苗一同创建对应的计时器了。这些计时面板都是隐藏的,那么在此之后,我们需要在再次选中幼苗时,让计时面板能显示出来。所以要在以下两个地方做些调整。
1、在触摸开始时,即onTouchesBegan函数中确定所有的计时面板是隐藏的:

    for (TimingLayer* timingLayerTemp : timingVector)
    {
        timingLayerTemp->setVisible(false);
    }

2、在触摸结束时,即onTouchesEnded函数中加上以下判断:

    else if(tileType == GROUD_CROP)
    {
        for (int i = 0; i < timingVector.size(); i++)
       {
           auto temp = timingVector.at(i);
           auto pos = temp->getTimingLayerPos();
           if( pos == touchObjectPos)
           {        
               temp->setVisible(true);
           }
        }
    }

更新成熟的农作物

到现在为止,运行游戏你会发现一切都差不多趋于完整,接下来当时间耗尽时,我们就该收获庄稼了。所以我们在Update函数中加入以下函来更新农作物的状态,便于后面可以收割。

void GameScene::updateRipeCrop()
{
    for (int i = 0; i < timingVector.size(); i++)
    {
        auto temp = timingVector.at(i);
        if( temp->getTimeOut())  
        {
            auto pos = temp->getTimingLayerPos(); 
            auto gid = map->getLayer("goodsLayer")->getTileGIDAt( pos); 
            switch (gid) 
            {
                case 18:
                {
                    map->getLayer("goodsLayer")->setTileGID(19, pos);
                    tileType = TileType::CROP_HARVEST;
                }
                    break;
                case 20:
                {
                    map->getLayer("goodsLayer")->setTileGID(21, pos);
                    tileType = TileType::CROP_HARVEST;
                }
                    break;
                case 22:
                    map->getLayer("goodsLayer")->setTileGID(23, pos);
                    tileType = TileType::CROP_HARVEST;
                    break;
                default:
                    break;
            }
            timingVector.eraseObject(temp); 
        }
    }
}

updateRipeCrop函数遍历了计时面板向量timingVector,它通过得到每个计时面板的timeOut属性来判断农作物是否成熟。如果成熟则通过得到相应计时面板的位置来改变该位置处的瓦片块状态,从而实现将幼苗换成成熟的状态,这里18,20,22分别表示幼苗时期的农作物,19,21,23是成熟的农作物,这种写法当初没想太多,但请各位小伙们不要跟我学,:]嘻嘻,这样太不规范,久了会忘的,最好还是用标记标示,这里我动动嘴皮子就不改了。

收获农作物

最后,经过一番辛勤的劳作之后,是时候收获成果了。

所以此时我们就可以在检查到type等于CROP_HARVEST的时候时,创建收获界面了。这里收获农作物同移除瓦片(即RemoveLayer)的功能超级相似,相信大家依葫芦画瓢就可以实现,相关的代码也已上传,所以也就不在过多赘述了。

最终我们已经实现了之前所承诺的《全面农场》的功能,如下图所示: res3.jpg 播种、计时、收获整个操作的效果图如下:
result1

总结

目前,整个游戏已经有了大致的雏形,接下来需要做的逻辑原理大同小异,所以我就不再废话。我想对于一个游戏Demo来说,该SLG游戏教学的目的已经差不多达到了吧,感兴趣的同学请自行扩展,本游戏的源码我已上传到Github仓库,点击可以下载,同时也欢迎大家克隆、斧正、提交pr。

?>