Cocos2d-x,Cocos

「Cocos游戏开发大赛」官网上线,引发手游创作新浪潮

6月19日, "Cocos游戏开发大赛"官网正式上线,手游开发者们可开始报名、上传作品。你与众不同的创意,或文艺或风趣的设计,都将在本次cocos游戏开发大赛中,获得专业人士的指导或认可。全球最大的苹果开发者中文社区CocoaChina是本次大赛举办方,其权威性以及专业性,充分保证了比赛的公平、有效、公正。

在全球,超过50万的游戏开发者使用cocos开发高质量手游,而在中国,cocos引擎更是行业先行者,超过70%的游戏开发者是它的忠实粉丝,是实至名归的国内手游引擎NO1。能吸引如此数量以及体量的游戏开发者,源于cocos的开源、跨平台特性。如今随着硬件发展,游戏玩家多屏互动的趋势越来越明显,cocos引擎的跨平台特性,十分契合这一游戏市场发展趋势,开发者基于cocos引擎开发的手游,更易推广和运营。

企业、创业团队、个人均可作为参赛主体参加本次大赛,大赛给你一个平台,令你创意和才华齐飞,技术与大奖兼得。

Cocos游戏开发大赛基于cocos引擎,不限平台,分为校园组和专业组,就算你的技术还不足以和技术大拿们PK,但仍然有机会赢取丰厚的奖品,还等什么,赶紧上传作品吧。

整装待发参赛之前,游戏开发小伙伴们一定要了解大赛评审规则,这是必须装备。大赛分为初赛、复赛和决赛三个阶段:

初赛:作品成功提交后,大赛主办方根据参赛规则对所提交的作品进行筛选。凡符合参赛规则的作品将全部入围初赛。

复赛:对全部入围初赛的作品进行网络票选,同时大赛评委会根据游戏的创意、美术、市场潜力、技术难度综合评审。最终得分计算规则为:评选页面的有效票数的60%+评委会评审分数的40%=每个作品的复赛分数。取专业组/校园组各组的前50名进入决赛。

决赛:专业组/校园组各组的前50名作品,由大赛特邀专家评委,按照创意(30%)、美术(20%)、市场潜力(20%)、技术难度(30%)的占分比例评选出特等奖以及各个奖项;

奖项说明:校园组/专业组各类奖项之间不可兼得。

如赛程安排,6月19号,本次大赛初赛拉开帷幕,参赛者们可以开始上传自己精心设计的手游作品了,但再次发送友情提示帖:作品必须基于cocos引擎(Cocos2d-x、Cocos2d-JS、Cocos2d-lua/Quick-Cocos2d-x均可)。

请登陆大赛官网:http://www.cocoachina.com/game/cocos/了解详情,提交作品,赢取大奖!

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。

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

原创: @涵紫任珊

怎样制作基于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游戏的所有功能,这里只选取其中有代表的一些功能来实现,有兴趣的同学欢迎在本游戏的基础上进行扩展。本游戏最终将要实现的功能是这样的:

  1. 点击商品项和其他障碍物所在层(这里统称第2层)上的瓦片,将出现一个清理界面,该界面上有一把叉子,玩家点击叉子可将点击的瓦片从地图中移除,不过点击土块系瓦片除外。
  2. 点击空土块瓦片将出现上节中创建的播种界面,玩家选取播种层中的农作物选项,就可以在土块上栽种相应的种子。
  3. 种子种下去的同时,该处瓦片上将会生成一个计时面板,用于纪录种子多久后成熟。如果该处瓦片再次处于未选中状态,那这个计时面板将会隐藏起来,直到种子完全成熟后该处的计时界面才会被销毁。
  4. 种子成熟后,瓦片上的农作物将会变为相应的成熟作物。如果这时选中这些成熟的作物,那么将出现一个收获界面。收取作物后瓦片又将变为什么都没有的空土块。

以上就是对单独瓦片进行操作的全部功能,后面我们将会一一实现。

播种

上一章已经创建好播种界面,所以我们接下来首先来实现播种这项功能。
1、 根据游戏功能需求,定义如下的一个枚举类型来标示玩家所点击的瓦片类型:

typedef enum // 定义瓦片类型。如果需要扩展,可这这里定义更多类型。
{
    GROUD = 1,
    GROUD_CROP = 2,
    CROP_HARVEST,
    OTHER
} TileType;

2、 接着在玩家开始触碰屏幕的地方(既onTouchesBegan函数中)判断所触碰瓦片的类型。如下,在onTouchesBegan函数的if (gid != 0)中添加以下一段代码:

    switch (gid)
    {
        case 9: // 触碰到的是空土块
            tileType = TileType::GROUD;
            break;
        // 触碰到的是种了农作物幼苗的土块
        case 18:  
        case 20:
        case 22:
            tileType = TileType::GROUD_CROP;
            break;
        // 触碰到种植有成熟农作物的土块
        case 19:
        case 21:
        case 23:
            tileType = TileType::CROP_HARVEST;
            break;
        // 其他情况,也就是可以被清除的其他瓦片类型
        default:
            tileType = TileType::OTHER;
            break;
        }

以上代码中的gid对应了下图中的瓦片项。
iso-test-128

3、 当玩家松开手指的时候,创建播种界面。
在onTouchesEnded函数的if(press)中添加如下的代码:

    if(tileType == GROUD)
    {
        auto panel = SeedChooseLayer::create();
        panel->setPosition(screenPos);
        this->addChild(panel, 10, SEEDPANEL_TAG);
    }

其中SEEDPANEL_TAG是播种界面的标示,我们可以用它来判断播种界面是否存在,是否移除该界面等操作。其他界面也同样有着自己的标示,如下所示:

#define SEEDPANEL_TAG           888
#define HARVESTPANEL_TAG        889
#define REMOVEPANEL_TAG         890

4、 创建播种界面后,我们就该播种了。
不过首先请打开瓦片地图文件,我们来整理下瓦片层:在最底层的地图层上只放置草地瓦片,在草地层之上的层用来放置障碍物和商品项,最上层置空,用来设置提醒项。同时,修改之前的图层名字,规范代码逻辑(不然以后自己都看不懂自己写的什么),如下图所示:
p2
打开游戏主场景GameScene,添加update函数,并实现它。如下所示:

void GameScene::update(float dt)
{
    // 通过标示得到场景中的子节点
     auto seedPanel = this->getChildByTag(SEEDPANEL_TAG);
    // 判断瓦片是否为空土块,并且是否已经创建好了播种界面
    if(tileType == GROUD && seedPanel!= NULL)
    {
        // 得到选择的农作物类型
        auto type = ((SeedChooseLayer*)seedPanel)->getCurrType();
        // 根据农作物类型种植相应的农作物
        switch (type)
        {
            case WHEAT:
                map->getLayer("goodsLayer")->setTileGID(18, touchObjectPos);
                this->removeChild(seedPanel);
                break;
            case CORN:
                map->getLayer("goodsLayer")->setTileGID(20, touchObjectPos);
                this->removeChild(seedPanel);
                break;
            case CARROT:
                map->getLayer("goodsLayer")->setTileGID(22, touchObjectPos);
                this->removeChild(seedPanel);
                break;            
            default:
                break;
        }
    }
}

不要忘了在init函数中加上scheduleUpdate(),这样才会每一帧都调用update()。

5、 当玩家触摸到屏幕其他地方时,一定要把已有的播种界面清除,以确保场景中最多只有一个播种界面。所以回到onTouchesBegan函数中,清除已有播种界面。

    auto seedPanel = this->getChildByTag(SEEDPANEL_TAG);
    if(seedPanel){
        this->removeChild(seedPanel);
    }

现在运行我们的代码,就可以像下图一样在土地上播种庄稼了。

p3

清理瓦片

接下来我们创建一个清理界面,用来移除除土块以外的其他瓦片。它同播种界面原理一样,其定义如下:

class RemoveLayer: public Layer
{
public:
    virtual bool init() override;
    bool onTouchBegan(Touch *touch, Event *event);
    CREATE_FUNC(RemoveLayer);
    CC_SYNTHESIZE(bool, remove, Remove);

private:
    Sprite* fork;
};

该界面只有一个叉子供玩家选择,所以我们用布尔类型的变量来标示玩家是否选中它。其事件回调函数代码如下:

bool RemoveLayer::onTouchBegan(Touch *touch, Event *event)
{
    auto target = static_cast(event->getCurrentTarget());

    Point locationInNode = target->convertTouchToNodeSpace(touch);
    Size size = target->getContentSize();
    Rect rect = Rect(0, 0, size.width, size.height);

    if (rect.containsPoint(locationInNode))
    {
        target->setOpacity(180);
        // 判断是否选中叉子
        if (target == fork)
        {
            remove = true;
        }
        return true;
    }
    return false;
}

实现了清理界面后,按照创建和销毁播种界面的思路把清理界面添加到游戏场景中,既在onTouchesEnded中创建清理面板,在onTouchesBegan中销毁该清理面板,并在update函数中检测更新:

    if(NULL != removePanel && tileType == OTHER)
    {
        if(((RemoveLayer*)removePanel)->getRemove() == true)
        {
            map->getLayer("goodsLayer")->removeTileAt(touchObjectPos);
            this->removeChild(removePanel);
        }
    }

这样一来,我们就可以移除地图中的看不顺眼的瓦片了。

收获

运行游戏,当前效果如下图所示:

p4

此时点击长有幼苗的土块,将会出现一个记录庄稼成熟时间的面板出现,当时间到时,庄稼将会成熟。这个时候如果再点击庄稼,玩家就可以所获它了。

由于计时面板的创建有别于其他小界面的创建,所以这里我们先跳过这一步,先来创建收获面板。
收获界面同清理界面基本是一致的,它将会在庄稼成熟的时候被创建,其定义如下:

class HarvestLayer: public Layer
{
public:
    virtual bool init() override;
    bool onTouchBegan(Touch *touch, Event *event);
    CREATE_FUNC(HarvestLayer);
    CC_SYNTHESIZE(bool, harvest, Harvest);

private:
    Sprite* harvestSprite;
};

变量harvest用来标示玩家是否选中收获项,HarvestLayer的实现很简单,这里就不赘述了。

本章所用资源已更新,点此可进行下载,文章中的源代码将在本系列教程结束时上传,敬请期待。

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

原创: @涵紫任珊

怎样制作基于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章

上一章中,我们实现了商品的拖动操作,把商店的商品拖动到了地图上,但本章将实现拖动商品的校对检测,同时将实现瓦片的触摸控制(移动、销毁),播种、除草、收获。

前面我们拖动商品到什么地方,它就跟着移动到什么地方,如下图网格里的示意图所示。但一般情况下,为了更加形象的体现出瓦片地图这一特性,一般都不能让待入列的瓦片那些肆掠的移动,所以接下来我们将实现拖动商品项的细节调整。

p1

优化拖动操作

首先,先在头文件中设置两个变量用于记录鼠标/手指当前和在此之前的移动坐标(这个坐标指地图坐标)。

    Vec2 currPos;
    Vec2 perPos;

接着修改moveCheck函数,如下代码所示:

void GameScene::moveCheck(Vec2 position, int tag)
{
    auto mapSize = map->getMapSize();
    auto tilePos = this->convertTotileCoord(position);    
    canBliud = false; 
    // 1
    perPos = currPos;

    if( tilePos.x >= 0 && tilePos.x <= mapSize.width - 1 && tilePos.y >= 0 && tilePos.y <= mapSize.height - 1)
    {
        currPos = tilePos;
        int gid = map->getLayer("2")->getTileGIDAt(tilePos);
        if (gid == 0){
            buyTarget->setTexture(move_textures[tag]);           
            canBliud = true;
        }
        else
        {
            buyTarget->setTexture(move_textures_en[tag]);
            canBliud = false;
        }
        // 2
        auto screenPos = this->convertToScreenCoord(tilePos);
        buyTarget->setPosition(screenPos);     
        // 3
        if(perPos != currPos){
            map->getLayer("3")->removeTileAt(perPos);
            map->getLayer("3")->setTileGID(17, currPos);
        }
    }
    // 4
    else{
        buyTarget->setPosition(position);
        buyTarget->setTexture(move_textures_en[tag]);
        map->getLayer("3")->removeTileAt(perPos);
        canBliud = false;
    }   
}
  1. 给之前坐标赋值,让它等于当前坐标.
  2. 把当前地图坐标转换为屏幕坐标,同时设置商品项的位置到这个屏幕坐标上。这里convertToScreenCoord函数将完成这一转换。这样一来,screenPos坐标就将会是固定的一些值。注意:不要忘了删掉SpriteCallback函数中设置商品项位置的函数段。
  3. 鼠标/手指移动到哪里,就在那里的瓦片上设置一个可以标识它的记号,同时当移动到别的地方,要删除之前位置上的记号。
  4. 当tilePos超出地图范围时,让商品项跟着鼠标/手指的移动而移动,同时移除perPos位置上的记号。

convertToScreenCoord函数中的数学公式其实就是convertTotileCoord函数中数学原理的一个反推公式,其代码如下:

Vec2 GameScene::convertToScreenCoord(Vec2 position)
{
    auto mapSize = map->getMapSize();
    auto tileSize = map->getTileSize();
    auto tileWidth = map->getBoundingBox().size.width / map->getMapSize().width;
    auto tileHeight = map->getBoundingBox().size.height / map->getMapSize().height;

    auto variable1 = (position.x + mapSize.width / 2 - mapSize.height) * tileWidth * tileHeight ;
    auto variable2 = (-position.y + mapSize.width / 2 + mapSize.height) * tileWidth * tileHeight ;

    int posx = (variable1 + variable2) / 2 / tileHeight;
    int posy = (variable2 - variable1) / 2 / tileWidth;

    return Point(posx, posy);
}

最后还缺什么啦,我们来用手指想一下。对的,一般当拖动商品项到一个“不空”的瓦片上或地图之外的区域时,将会提示此处不可放。所以,接下来我们的工作来加这段提示的代码吧。

在SpriteCallback方法的if( canBliud == true ){}函数段后加上如下代码:

    else{
        // 得到放手时鼠标/手指的屏幕坐标,这个坐标是相对于地图的。所以计算它时应该要考虑到地图的移动和缩放。
        auto endPos =Vec2((widget->getTouchEndPos().x - bgOrigin.x)/bgSprite->getScale(), (widget->getTouchEndPos().y - bgOrigin.y)/bgSprite->getScale());
        // 把上面得到的屏幕坐标转换围地图坐标
        auto coord = convertTotileCoord( endPos);
        // 再把地图坐标转换为固定的一些屏幕坐标
        auto screenPos = this->convertToScreenCoord(coord);
        // 创建提醒项,把它设置在screenPos处
        auto tips = Sprite::create("tip.png");
        tips->setPosition(screenPos);
        bgSprite->addChild(tips);
        // 让提醒项出现一段时间后移除它
        tips->runAction(Sequence::create(DelayTime::create(0.1f),                                                CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, tips)),                                                NULL));
    }

运行你的游戏,你会发现它的效果如下图所示:

p2

对单独的瓦片进行操作

商品项的拖动效果已经近乎完美,接下来是该对单独的瓦片进行操作了。经营模式游戏中,当玩家触碰到地图瓦片时,将可以进行相应的操作,比如:触碰到树木将可以砍掉它;触碰到土地将可以播撒小麦、土豆、玉米等农作物,如果上成熟状态可以收获它们;触碰到建筑物时将可以修整,拆除它等等。如果玩家长按某个瓦片,我们还能移动它。

反正在这类游戏中对瓦片的操作是非常繁琐的,接下来我们就来一步一步的攻克它吧。最终的目标是做一个类似下图(《全面农场》)的效果。

p3 p4 p5

接下来我们需要先判断玩家的触碰是长按还是短按,然后在考虑其他的问题。

判断短按长按

Cocos2d-x中是没有长按事件的触发机制的,所以如果想实现长按的检测,需要自己实现。本教程中通过schedule来实现长按操作,其原理如下:

当玩家触碰到屏幕上的瓦片(除了草地,既瓦片地图的最底层)时,开始计时。如果在计时过程中发生了触摸移动或者异常中断了触摸响应,那么就取消本次计时。在计时过程中,如果达到预定时间,那么则执行相应的函数。最后在结束本次触摸时(抬起鼠标/手指),结束计时。

根据以上阐述的原理,我们需要一个变量来判断是否继续计时,所以请先在头文件中定义如下的变量:

bool press;
Vec2 touchObjectPos;

其中touchObjectPos由于记录触摸到的瓦片的地图坐标。

接下来找到onTouchesBegan函数,在函数中添加如下的一段代码:

    if(touches.size() == 1)
    {
        auto touch = touches[0];
        auto screenPos = touch->getLocation();       
        auto mapSize = map->getMapSize();       
        Vec2 pos;
        pos.x = (screenPos.x - bgOrigin.x)/bgSprite->getScale();
        pos.y = (screenPos.y - bgOrigin.y)/bgSprite->getScale();
        auto tilePos = this->convertTotileCoord(pos);        
        if( tilePos.x >= 0 && tilePos.x <= mapSize.width - 1 && tilePos.y >= 0 && tilePos.y <= mapSize.height - 1){
            int gid = map->getLayer("2")->getTileGIDAt(tilePos);
            // 如果瓦片地图的"2"层上tilePos处存在其他瓦片,则执行以下代码。
            if (gid != 0)
            {
                touchObjectPos = tilePos;
                map->getLayer("3")->setTileGID(17, tilePos);
                this->schedule(schedule_selector(GameScene::updatePress), 2);
                // longPress在开始按下的时候为true,如果移动,取消,则为false,在抬起的时候如果变量为true,那么执行schedule中的updatePress函数
                press = true;
            }
        }
    }

schedule的原理我就不废话了,不清楚的同学可以查看官方文档调度器(scheduler)一文。updatePress函数如下:

void GameScene::updatePress(float t)
{
    // 取消计时
    this->unschedule(schedule_selector(GameScene::updatePress));
    if(press)
    {
        log("是长按");
        map->getLayer("3")->removeTileAt(touchObjectPos);
        press = false;
    }
}

接下来在onTouchesMoved函数中添加如下代码:

    press = false;
    map->getLayer("3")->removeTileAt(touchObjectPos);
    this->unschedule(schedule_selector(GameScene::updatePress));

最后添加一个用于出来结束触摸响应的onTouchesEnded事件函数,其代码如下所示:

void GameScene::onTouchesEnded(const std::vector&touches, Event  *event)
{
    this->unschedule(schedule_selector(GameScene::updatePress));

    Size winSize = Director::getInstance()->getWinSize();
    if(touches.size() == 1)
    {
        auto touch = touches[0];
        auto screenPos = touch->getLocation();
        if(press)
        {
            log("是短按。此处应该创建相应的操作面板了");

            map->getLayer("3")->removeTileAt(touchObjectPos);
            press = false;
        }
    }
}

创建操作面板

对于一个模拟经营类游戏来说,操作面板之多,所以本教程就不大动干戈挨个的创建了,象征性的例举几个例子就行,毕竟这只是一篇教程,而不是商业项目,所以感兴趣的同学请跟着教程思路自己去拓展一下。

下面以播种面板为例,创建一个播种面板SeedChooseLayer,它继承于Layer,其定义如下所示:

// 定义农作物类型
typedef enum
{
    WHEAT = 0,
    CORN = 1,
    CARROT,
    NOTHING
} CropsType;

class SeedChooseLayer: public Layer
{
public:
    virtual bool init() override; 
    // 重载触摸回调函数
    bool onTouchBegan(Touch *touch, Event *event);
    CC_SYNTHESIZE(CropsType, currType, CurrType);// 选中的作物类型
    CREATE_FUNC(SeedChooseLayer);

private:
    Sprite* wheat;
    Sprite* corn;
    Sprite* carrot;
};

对去播种面板层的触摸响应,这里和地图的响应略有不同,它是单点触摸事件,所以以上重载的是用于出来单点事件的onTouchBegan函数(触摸点击开始事件)。

接下来,我们来实现SeedChooseLayer的各个方法。如下是init的实现。

bool SeedChooseLayer::init()
{
    if (!Layer::init())
    {
        return false;
    }
    currType = CropsType::NOTHING;

    auto bgSprite = Sprite::create("chooseBg.png");
    bgSprite->setAnchorPoint(Vec2(1, 0));
    this->addChild(bgSprite);

    wheat = Sprite::create("corn.png");
    wheat->setAnchorPoint( Point(0, 0));
    wheat->setPosition(Point(0, 0));
    bgSprite->addChild(wheat);

    corn = Sprite::create("wheat.png");
    corn->etPosition(Point(bgSprite->getContentSize().width/2, bgSprite->getContentSize().height/2));
    bgSprite->addChild(corn);

    carrot = Sprite::create("carrot.png");
    carrot->setAnchorPoint( Point(1, 1));
    carrot->setPosition(Point(bgSprite->getContentSize().width, bgSprite->getContentSize().height));
    bgSprite->addChild(carrot);

    // 创建事件监听器,OneByOne表示单点
    auto touchListener = EventListenerTouchOneByOne::create();
    // 设置是否向下传递触摸,true表示不向下触摸。
    touchListener->setSwallowTouches(true);
    touchListener->onTouchBegan = CC_CALLBACK_2(SeedChooseLayer::onTouchBegan, this);

    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, wheat);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener->clone(), corn);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener->clone(), carrot);
    return true;
}

在init函数中,我们初始化了播种面板中种子的布局,并且创建绑定了触摸事件。这里要注意:在面板中,我们设置了三个农作物选项供玩家选择,每一个选项都要处理触摸响应事件。所以,绑定需要绑定到每个选项上。然而当我们再次使用事件监听器的时候,需要使用 clone() 方法重新克隆一个,因为每个监听器在添加到事件调度器中时,都会为其添加一个已注册的标记,这就使得它不能够被添加多次。

绑定好触摸事件后,接下来需要实现具体的触摸回调。在onTouchBegan函数中我们要做的是:当玩家触碰到某选项时,重新设置其透明度,向玩家表明该项是被选中的;并且确定其选择的作物类型,方便我们后面代码的获取和使用。其代码如下图所示:

bool SeedChooseLayer::onTouchBegan(Touch *touch, Event *event)
{
    auto target = static_cast(event->getCurrentTarget());   // 1
    Point locationInNode = target->convertTouchToNodeSpace(touch);   // 2
    // 3
    Size size = target->getContentSize();
    Rect rect = Rect(0, 0, size.width, size.height);   
    // 4
    if (rect.containsPoint(locationInNode))
    {
        target->setOpacity(180);
        // 5
        if (target == wheat)
        {
            currType = CropsType::WHEAT;
        }else if(target == corn)
        {
            currType = CropsType::CORN;
        }else if(target == carrot)
        {
            currType = CropsType::CARROT;
        }else{
            currType = CropsType::NOTHING;
        }
        return true;
    }
    return false;
}
  1. 返回触摸事件当前作用的目标节点。
  2. 把touch对象中保存的屏幕坐标转换到GL坐标,再转换到目标节点的本地坐标下。
    在Node对象中有几个函数可以做坐标转换。convertToNodeSpace方法可以把世界坐标转换到当前node的本地坐标系中;convertToWorldSpace方法可以把基于当前node的本地坐标系下的坐标转换到世界坐标系中;convertTouchToNodeSpace这个函数可以把屏幕坐标系转换到GL坐标系,再转换到父节点的本地坐标下。
  3. 计算目标节点的矩形区域。
  4. 判断触碰点在不在目标节点的矩形区域内,即判断是否被选中。
  5. 根据选择的目标确定作物的类型。

以上我们就已经实现了播种界面的创建了,最后在onTouchesEnded函数中判断为短按的地方加上如下代码就可以实现播种界面的显示了。

panel = SeedChooseLayer::create();
panel->setPosition(screenPos);
this->addChild(panel, 10, SEEDPANEL_TAG);

效果如下图所示:

p6

本章资源已上传,点此可进行下载,文章中的源代码将在本系列教程结束时上传,敬请期待。

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

原创: @涵紫任珊

怎样制作基于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章

本章让我们回顾一下上一章所做的事情,有点忘了!呵呵,对了,就是下图的效果:

result

上一章,我们通过Cocos Studio编辑器简单的制作了一个能够自动适应多分辨率的UI,而在本章,我们将教会大家如何拖动滚动层中的商品选项到地图中,效果如下图所示:

result1

这里,我们第一步需要做的事是,把滚动层中的物品换成我们需要的商品,然后再完成拖动操作(把选中的商品拖动到TMX地图中空白的(没有其他障碍瓦片的)位置处)。下面就一起开始跟着做吧。

设置商品项

在Cocos Studio中,每个商品的信息都如下左图所示,包含了选项背景、商品描述、价格等等一些相同的属性。

item

所以在程序中,我们可以通过遍历滚动层中所有的子项来获得各属性的值,并重新设置它。这样一来,我们先在程序中定义如下的一些变量:

// 商品图样
const char* shop_textures[8] =
{
    "shopItem/Item1.png", "shopItem/Item2.png", "shopItem/Item3.png", "shopItem/Item4.png", "shopItem/Item5.png", "shopItem/Item6.png", "shopItem/Item1.png", "shopItem/Item2.png"
};

// 拖动过程中,选中项正常的纹理图,这个正常是能被添加在拖动位置
const char* move_textures[8] =
{
    "shopItem/moveItem1.png", "shopItem/moveItem2.png", "shopItem/moveItem3.png", "shopItem/moveItem4.png", "shopItem/moveItem5.png", "shopItem/moveItem6.png", "shopItem/moveItem1.png", "shopItem/moveItem2.png"
};

// 拖动过程中,选中项不正常的纹理图
const char* move_textures_en[8] =
{
    "shopItem_en/Item1.png", "shopItem_en/Item2.png", "shopItem_en/Item3.png", "shopItem_en/Item4.png", "shopItem_en/Item5.png", "shopItem_en/Item6.png", "shopItem_en/Item1.png", "shopItem_en/Item2.png"
};

// 商品描述
const char* shop_info[8] =
{
    "土地", "破树", "烂树", "烂草", "破屋", "破地", "婆婆", "反正破",
};

// 所值价格
const int shop_money[8] =
{
    20, 40, 60, 100, 120, 99, 50, 200,
};

再在initUI函数中添加如下的一段代码:

auto shop_scrollView = dynamic_cast(panel_shop->getChildByName("scrollview_shop"));

for (int i = 0; i < shop_scrollView->getChildren().size(); ++i)
    {
        Layout* shop_layout = static_cast(shop_scrollView->getChildren().at(i));
        shop_layout->setTag(SHOP_ITEM_LAYOUT_TAG + i);

        ImageView* buy_Sprite = static_cast(shop_layout->getChildByName("shopitem"));
        buy_Sprite->loadTexture(shop_textures[i]);

        TextField* info = static_cast(shop_layout->getChildByName("info"));
        info->setText(shop_info[i]);

        TextField* money = static_cast(shop_layout->getChildByName("money_image")->getChildByName("money"));
        money->setText(std::to_string(shop_money[i]));
    }

这样我们就可以设置好每个商品项了。再次运行程序,就会是下面这样的效果:

setItem

是不是很简单,哈哈,接下来继续看看怎样拖到商品项里的商品。

拖到操作

在Cocos Studio中,每个商品项中的“shopitem”是一个可交互的ImageView(前面我们自己设置的),我们可以通过给它绑定一个TouchEvent回调,让它像按钮一样可以处理更多不同的响应事件。

所以,回到initUI函数,为buy_Sprite绑定一个回调函数:

buy_Sprite->addTouchEventListener(CC_CALLBACK_2(GameScene::SpriteCallback, this));

其中,SpriteCallback回调方法和之前的menuShopCallback方法类似,需要根据TouchEvent事件的类型(按下、移动、抬起、取消),进行相应的逻辑处理。

拖动操作大概的过程是,按住某个商品,把它往游戏场景中拖,如果拖到的地方,该处的某个地图层上又没有其他的障碍物“瓦片”(我们把tmx地图的每一块图块都叫做瓦片),那我们放手时就在这里生成一个对应的瓦片商品。

具体的实现如下所示:

void GameScene::SpriteCallback(cocos2d::Ref* pSender, Widget::TouchEventType type)
{
    Size winSize = Director::getInstance()->getWinSize();
    // 获得所选择的Widget,和它的父Widget(也就是商品项)   
    Widget* widget = static_cast(pSender);
    Widget* parent = static_cast(widget->getParent());
    // 得到商品项的标记
    int tag = parent->getTag();

    // 根据TouchEventType类型进行逻辑处理
    switch (type)
    {
        // 按下手指/鼠标时,放大选中得商品
        case Widget::TouchEventType::BEGAN:
            widget->runAction( EaseElasticInOut::create( ScaleTo::create(0.1f, 1.5), 0.2f));
            break;

        case Widget::TouchEventType::MOVED:
            // 滑动手指/鼠标时,如果购物滚动面板是弹出状态,就先把它收起来 
            if(comeOut == true)
            {
                panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(-panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(- panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                comeOut = false;
            }
            // 如果buyTarget为空,就先创建它
            if( buyTarget== NULL ){                
                buyTarget = Sprite::create(move_textures[tag - SHOP_ITEM_LAYOUT_TAG]);
                buyTarget->setAnchorPoint(Vec2(0.5f, 0));
                 // 把buyTarget添加到bgSprite上,这样buyTarget的位置就是相对于bgSprite了。
                bgSprite->addChild(buyTarget, 10);
            }
            // 移动buyTarget 
            else{                          
                Vec2 pos;
                // 因为buyTarget的位置是相对于地图的,所以我们需要考虑到它的移动和缩放。
                pos.x = (widget->getTouchMovePos().x - bgOrigin.x)/bgSprite->getScale();
                pos.y = (widget->getTouchMovePos().y - bgOrigin.y)/bgSprite->getScale();

                buyTarget->setPosition(pos);
                // 检测是否可以创建商品,这个后面会讲解。
                moveCheck(pos, tag - SHOP_ITEM_LAYOUT_TAG);
            }            
            break;
        // 抬起  
        case Widget::TouchEventType::ENDED:
            // 还原放大的widget
            widget->runAction( EaseElasticInOut::create(ScaleTo::create(0.1f, 1), 0.2f));
            // 移除buyTarget
            if(buyTarget != NULL)
            {
                buyTarget->removeFromParent();
                buyTarget= NULL;
            }            
            canBliud = false;
            break;
        // 取消触摸
        case Widget::TouchEventType::CANCELED:
            // 还原放大的widget
            widget->runAction( EaseElasticInOut::create(ScaleTo::create(0.1f, 1), 0.2f));
            // 生成瓦片
            if( canBliud == true )
            {
                // 得到放手时位置
                auto endPos =Vec2((widget->getTouchEndPos().x - bgOrigin.x)/bgSprite->getScale(), (widget->getTouchEndPos().y - bgOrigin.y)/bgSprite->getScale());
                // 在convertTotileCoord( endPos)位置上设置(生成)一个GID为9 + tag - SHOP_ITEM_LAYOUT_TAG的瓦片,这对应着滚动层中的商品,如下图所示。
                map->getLayer("2")->setTileGID(9 + tag - SHOP_ITEM_LAYOUT_TAG, convertTotileCoord( endPos));
                canBliud = false;
            }
            // 移除buyTarget
            if(buyTarget != NULL)
            {
                buyTarget->removeFromParent();
                buyTarget= NULL;
            }  
            // 弹出购物滚动面板          
            if(comeOut == false)
            {
                panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                comeOut = true;
            }
            break;
        default:
            break;
    }
}

以上代码在触摸到可交互的商品时,先获取到它父节点的标记,这样我们就知道它是哪种商品了。再根据事件类型,进行以下的逻辑处理:

  • 触摸开始时,为所选择的商品(widget)添加一个放大的缓动效果,证明它是被选中了的。当然如果触摸开始到结束的时间太短,是不会有这个效果的。
  • 拖到widget时,先像在menuShopCallback中一样,收起弹出的购物滚动面板;然后创建一个临时的购买目标项(buyTarget),让它跟着手指的移动而移动,在此同时,检测它目前所在的地图位置上是否可以创建一个商品,关于这一点,后面会更详细的说明。
  • 正常完成触摸后,还原放大的widget,同时移除buyTarget。
  • 非正常取消触摸时,同样先还原放大的widget,移除buyTarget,弹出购物滚动面板。然后最重要的一点:在地图上“创建”选中的商品。这点我们也留到后面说明。

这里TouchEventType::ENDED在正常触摸完后,手指离开屏幕得时候触发,而TouchEventType::CANCELED则是在非正常触摸完结束时触发,比如手指没正常离开时候来了个电话,或者滑动过程中滑出了Widget范围抬起手指。

另外,地图图块已更新为:

iso-test-128

不用在意最后两个瓦片和前面的相同,这是本人的原因,因为实在是找不到资源了,做一点算一点吧,有机会再改。

上述检测以及在地图上生成选中商品的过程,都需要我们把Cocos2d-x坐标转化为瓦片地图对应的地图坐标。因为只要这样我们才能获得TMX地图层中每块瓦片的信息。关于瓦片地图的详细介绍请参考瓦片地图一文。

下面是把屏幕坐标转换为地图坐标的一段函数的实现:

Vec2 GameScene::convertTotileCoord(Vec2 position)
{
    // 得到瓦片地图的瓦片尺寸,对于本游戏是(30 * 30)
    auto mapSize = map->getMapSize();
    // 计算当前缩放下,每块瓦片的长宽
    auto tileWidth = map->getBoundingBox().size.width / map->getMapSize().width;
    auto tileHeight = map->getBoundingBox().size.height / map->getMapSize().height;
    // 把position转换为瓦片坐标,确保得到的是整数
    int posx = mapSize.height - position.y / tileHeight + position.x / tileWidth - mapSize.width / 2;
    int posy = mapSize.height - position.y / tileHeight - position.x / tileWidth + mapSize.width / 2;

    return Point(posx, posy);
}

convertTotileCoord方法将返回一个45度地图的坐标(这个坐标可能是超出正常取值范围的,它未经约束),其中参数position必须是一个相对于bgSprite(也就是瓦片地图 map)的位置值,因为地图的位置可能会有滚动和缩放的变化;参数tag是传入的标记值。

convertTotileCoord方法的注释已给出,其中最难理解的应该是将屏幕坐标转换为瓦片地图坐标的数学公式,其数学原理请阅读:www.gandraxa.com/isometric_projection.aspx

检测(moveCheck)的实现:

void GameScene::moveCheck(Vec2 position, int tag)
{
    auto mapSize = map->getMapSize();
    // 将position转化为地图坐标
    auto tilePos = this->convertTotileCoord(position);

    // canBliud是用于判断是否可生成瓦片的变量
    canBliud = false;

    // 约束tilePos的范围。如果tilePos在正确取值范围内(菱形内)
    if( tilePos.x >= 0 && tilePos.x <= mapSize.width - 1 && tilePos.y >= 0 && tilePos.y<= mapSize.height - 1)
    {
        // 前半段map->getLayer("2")是取得地图中名称为“2”的图层,而getTileGIDAt(tilePos)则是得到tilePos坐标上瓦片的GID标示。(GID标示为0时,表示该处没有任何的瓦片。)
        int gid = map->getLayer("2")->getTileGIDAt(tilePos);
        // 该处没有其他障碍瓦片时
        if (gid == 0) 
        {    // 设置拖动过程中正常的buyTarget纹理
            buyTarget->setTexture(move_textures[tag]);
            canBliud = true;
        }
        // 该处有障碍瓦片时,把buyTarget设置为move_textures_en中颜色偏红的纹理
        else{
            buyTarget->setTexture(move_textures_en[tag]);
            canBliud = false;
        }
    }
    // 如果位置在地图以外(四个角),同样把buyTarget设置为move_textures_en中颜色偏红的纹理
    else{
        buyTarget->setTexture(move_textures_en[tag]);
        canBliud = false;
    }
}

如果一切正常,此时我们运行游戏已经可以把购物面板中的商品无付费的拖动到场景中了,当然细节上还没考虑太多,后面再慢慢考虑。

但这儿还有一个问题,就是随着地图的移动和缩放,你会发现有时手指移动的位置和商品的位置不一致,就像下图这样:

err

找了半天,发现产生这个Bug的原因还是bgOrigin的问题。所以,果断回到onTouchesMoved函数,把下面一段代码:

    // 更新原点位置
    if( pos.x >= bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x
        || pos.x <= -bgSpriteCurrSize.width + winSize.width + bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x)
    {
        diff.x = 0;
    }
    if( pos.y >= bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y
        || pos.y <= -bgSpriteCurrSize.height + winSize.height + bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y)
    {
        diff.y = 0;
    }
    bgOrigin += diff;

修改为了:

    Vec2 off = pos - currentPos;        
    bgOrigin += off;

这样代码逻辑才更加严谨,是之前疏忽了。
好的,这章就算完成了,下面给一张清晰的游戏效果图。

result

点击这里下载资源。

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

原创: @涵紫任珊

怎样制作基于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章

经过前面两章的学习,我们已经可以掌握了地图的创建和加载,并且可以对它进行缩放,移动等基本的操作了。那么在本章,我们将通过Cocos Studio编辑器快速地制作一款能自动适应多分辨率的UI系统,享受一下Cocos2d-x的GUI系统和Cocos Studio所带来的便捷。

一直以来,如何快速地制作UI是开发游戏中不得不遭遇的一个问题,Cocos2d-x一路经历了1.0时期的固定位置和2.0的相对位置,一直到现在3.0中全新的GUI机制。尽管3.0版本引擎中引进了全新的,且功能强大的GUI机制,但如果要手动编码实现相关GUI的显示和布局问题,这还是将会是一件特别繁琐的事情。好在Cocos Studio对Cocos2d-x新的GUI系统进行了全面的支持,我们可以很方便的利用它来实现UI系统的设计。

result

本章的目标是实现如上图所示的效果(高清截图臣妾做不到,猿媛们请见谅)。选择左下角的购物车按钮会弹出一个购物滚动列表,下面就让我们一起来学习吧。

制作UI界面

开始之前,首先还是简单介绍下Cocos Studio,扫扫盲。Cocos Studio是一套基于Cocos2d-x的免费游戏开发工具集,它能帮助开发者快速创建游戏资源,将大部分繁琐的游戏开发工作使用编辑器来快速制作,进一步帮助游戏开发者减短开发周期、提高开发效率。

由于各个版本的Cocos Studio功能上略有不同,所以开发项目时最好采用对应的版本进行学习。本教程中采用的是Windows版本的Cocos Studio 1.5.0.1,其下载地址可在http://cn.cocos2d-x.org/download中找到。

下载安装好Cocos Studio以后,运行应用程序你将发现Windows版本Cocos Studio中核心的四个编辑器:UI编辑器、动画编辑器、场景编辑器和数据编辑器,它们分别用于处理游戏中的UI界面、动画资源、游戏场景和游戏数据。但因为我们只需要使用Cocos Studio来完成UI界面的制作,所以打开Cocos Studio后,选择UI Editor(UI编辑器)项,新建一个UI工程。下面就让我们一起来创建UI界面吧。

1)首先,因为我们的UI资源是按着1136

640的分辨率来制作的,所以先在快捷菜单栏中设置画布大小为1136

640。如下:

p2

2)接着选择 文件->导入资源 导入所需的资源文件,也可直接将资源拷入UI工程的Resources文件夹目录下,然后刷新资源面板。本教程所使用的资源已共享,可点击这里下载。

3)此时对象结构面板中只有如下的一个根节点。

p3

我们需要设置一下根节点的属性,如下图所示:

p4

其中,Cocos Studio的子控件布局的方式有四种,在图中依次排开,它们分别是:

  1. 绝对布局:子控件的位置由其坐标决定,其优点是布局灵活性大,缺点是在做全屏多分辨率的情况下不太适用。
  2. 相对布局:子控件的位置由其相对于父控件的纵横两个方向的位置决定,它还允许子控件相对于父控件的位置做偏移。
  3. 线性横向布局:线性布局的一种,子控件在父控件上呈平行结构依次排列。
  4. 线性纵向布局:线性布局的一种,子控件在父控件上呈垂直结构依次排列。

因为我们需要制作一款能自动适应多分辨率的界面,所以绝对布局是不可行的,这里需要把子控件布局设置为相对布局,也就是上图中所选择的第二个布局选项。

再者,去掉“交互”项的√勾选,因为UI将位于地图背景的上层,如果我们勾选了交互项,那么地图背景层的触摸事件就将遭到拦截,我们就无法对它做拖动和缩放操作了。交互项的意思简单点说就是此控件是否拦截输入、触摸等消息。

最后,将背景颜色调为无颜色,并勾选“自适应分辨率”项,这样整个根节点大小就会随着屏幕大小的变化而变化了。

4)接下来向场景中拖入一个图片控件,以它为例,我们来看看如何设置子控件的属性,如下图所示:

p5

因为我们需要让图片的大小随着分辨率的变化而变化,所以把尺寸的模式从Auto修改为了Custom,并把图片的尺寸设为它原始尺寸大小(280

82)。这样在1136

640的分辨率下就显示了原始大小。

勾选百分比选框,这样在其他分辨率下,比如480

320下,图片显示的尺寸就变成了((480 / 1136

280)

(320 / 640

82))的大小,保证了显示效果的一致性。但需要说明的是,如果非1136 * 640比例的分辨率下,图片难免会出现拉伸。

将名字属性修改为便于识别的名称,养成良好的习惯。

在控件布局中选择其横向布局为左边,纵向布局为上边,这样图片就会紧贴根节点的左上角了。调整边缘属性可改变图片相对于左上角的位置坐标。

5)同样的方法添加其他控件,注意层级关系,并一定要勾选百分比项。

6)下面我们来制作按下购物车按钮所弹出的商店部分。

首先,向场景中拖入一层容器,命名为panel_shop,控件布局时让它停靠在购物车按钮(button_shop)的左侧,因为shop_button本身就在屏幕的最左下角,所以新建的panel_shop位于屏幕渲染区域之外的。如下图所示:

p6

向panel_shop中添加一个滚动层控件,设置其属性,如下图所示:

p7

这里我们把滚动层的大小设置为了panel_shop的2/3,仅仅时为了后面在程序中好控制它们的显示。把滚动层的布局方式设置为线性纵向布局,同时勾选“交互”项,这样做的原因为了让我们能滚动该滚动层。滚动区域的大小应该比滚动层本身大,这样才能让滚动层滚动起来。所以我们把滚动区域的高调整为800。

接着在滚动层内创建一个如下的panel,并为其添加如下的子控件。

p8

快速复制上图控件并调整位置,清理滚动层的背景颜色,将滚动层的颜色设置为“无颜色”,将panel_shop层的颜色设为白色。

7)这时,改变画布大小试试,如果一切正常,那么这个画布里的几乎所有内容都会根据当前的分辨率进行适配。

你也看到啦,我们说的是几乎,那还有什么不能进行适配啦?比如:字体大小,滚动层滚动区域大小等等,它们对并勾选“自适应分辨率”项是不敏感的,这让人有点头疼,这可能是Cocos Studio存在的一个bug吧。所以为了避免一些不必要的问题,我们把画布大小改为程序中所设置的design resolution,既480*320,同时把滚动层滚动区域大小调整为一个比较合适的大小。

p9

制作完成后勾选滚动层的“裁剪”属性,这样滚动层超出自身区域的内容将不会显示。

8)打开导出对话框,选择导出路径,按默认配置导出资源。

代码中加载

Cocos2d-x 使用UI框架的步骤:

1)Cocos2d-x 导入Cocos Studio的UI编辑器文件(json和资源)
2)编码获取/创建控件,绑定控件
3)使用控件做逻辑处理:比如,按钮、ScrollView等等控件的响应事件。

打开GameScene.cpp添加头文件

#include "cocostudio/CCSGUIReader.h"
#include "ui/CocosGUI.h"

using namespace ui;

编写initUI函数,先简单初始化下游戏UI,如下列代码所示:

void GameScene::initUI()
{
    playerLayout = static_cast(cocostudio::GUIReader::getInstance()->widgetFromJsonFile("Ui/Ui_1.json"));
    addChild(playerLayout, 10);

    // 获取商店层和购物车按钮
    panel_shop = dynamic_cast(playerLayout->getChildByName("panel_shop"));   
    shop_btn = dynamic_cast

按钮是要有响应函数的,又由于它是Widget的一个子类,所以采用的TouchEvent的回调方式。这里所说的TouchEvent响应主要是使用在Widget上的,可以将其看做是函数回调的一个扩展,为更多的响应处理提供可能。下面是按钮回调的实现:

void GameScene::menuShopCallback(cocos2d::Ref* pSender, Widget::TouchEventType type)
{
    Size winSize = Director::getInstance()->getWinSize();

    switch (type)
    {
        case Widget::TouchEventType::BEGAN:  // 按下按钮
            // 为shop_btn添加一个缩放效果
            shop_btn->runAction( EaseElasticInOut::create(Sequence::create( ScaleBy::create(0.1f, 2),ScaleBy::create(0.2f, 0.5f), NULL), 0.5f));
            break;
        case Widget::TouchEventType::MOVED:  // 移动按钮
            break;
        case Widget::TouchEventType::ENDED:  // 放开按钮
            if(comeOut == false)  // 弹出
            {
                // 为panel_shop和shop_btn添加一个弹性的移动效果,移动的距离是(panel_shop->getContentSize().width / 3 * 2, 0)个单位。
                panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                comeOut = true;
            }
            else if(comeOut == true) // 缩回
            {
                panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(-panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(- panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                comeOut = false;
            }
            break;

        case Widget::TouchEventType::CANCELED:  // 取消点击
            break;

        default:
            break;
    }

}

效果图:

p10

当商店层和购物车按钮弹出时,如果触碰到了它们范围之外的区域,它们应该缩回。所以,在onTouchesBegan函数中需要添加如下的一段代码。

void GameScene::onTouchesBegan(const std::vector&touches, Event  *event)
{
    Size winSize = Director::getInstance()->getWinSize();
    if(comeOut == true)
    {
        panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(-panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
        shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(- panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
        comeOut = false;
    }
}

好的,现在我们已经简单的实现了UI的显示了。下一章我们将替换滚动层中的商品项和信息,并拖动商品到地图。

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

原创: @涵紫任珊

怎样制作基于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章

上一章中,我们使用TiledMap制作了一张简单的地图,并把它加入到了程序中,紧接着本章将实现地图的双指缩放和单指移动功能。

是不是觉得这一功能很接地气,呵呵,其实在很多类似的大地图背景游戏中这是非常常见和必要的一项功能,玩家可以通过滑动屏幕实现地图的滚动预览,同时可以通过两个手指的拉伸和聚拢实现地图背景的放大缩小。下面,就跟着我们一起来实现吧。

p1

双指缩放,单指拖动的实现

Cocos2d-x中有自己的一套事件分发机制,如果你还不是很清楚,可先阅读Cocos2d-x事件分发机制一文。

在Cocos2d-x 3.x 中,实现触摸响应的一般流程如下:

  1. 重载触摸回调函数
  2. 创建并绑定触摸事件
  3. 实现触摸回调函数

具体实现如下:

1、首先,在GameScene.h文件中声明成员函数。

virtual void onTouchesBegan(const std::vector &touches, cocos2d::Event *event);
virtual void onTouchesMoved(const std::vector &touches, cocos2d::Event *event);

2、在GameScene.cpp文件的init函数中创建并绑定触摸事件。

// 1 创建一个事件监听器
auto listener = EventListenerTouchAllAtOnce::create();
// 2 绑定触摸事件
listener->onTouchesBegan = CC_CALLBACK_2(GameScene::onTouchesBegan, this);// 触摸开始时触发
listener->onTouchesMoved = CC_CALLBACK_2(GameScene::onTouchesMoved, this);// 触摸移动时触发
// 3 添加监听器
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, bgSprite);
  1. 在使用触摸事件时,我们首先需要创建一个事件监听器,事件监听器包含了触摸事件、键盘响应事件、加速记录事件、鼠标响应事件和自定义事件。其中的触摸监听类型触摸事件又分为 EventListenerTouchOneByOne(单点触摸) 和 EventListenerTouchAllAtOnce(多点触摸) 两种。
  2. 让监听器绑定事件处理函数。上面绑定的onTouchesBegan和onTouchesMoved分别响应的是触摸点击开始事件和移动事件。与之相关的还有onTouchEnded和onTouchCancelled两个事件处理函数,但目前我们的游戏还不需要(也有可能不会用到),所以这里就不用实现了。
  3. 监听器创建完成后需要把它绑定给_eventDispatcher事件分发器,_eventDispatcher 是 Node 的属性,通过它我们可以统一管理当前节点(如:场景、层、精灵等)的所有事件分发情况。
    将事件监听器 listener 添加到事件调度器_eventDispatcher中有两种方法,即如下的两个函数:
    void addEventListenerWithSceneGraphPriority(EventListener* listener, Node* node);              
    void addEventListenerWithFixedPriority(EventListener* listener, int fixedPriority);
    

    两者的主要区别在于它们加入到事件分发器中的优先级的差异。其中的使用 addEventListenerWithSceneGraphPriority 方法添加的事件监听器优先级固定为0;而使用 addEventListenerWithFixedPriority 方法添加的事件监听器的优先级则可以自己设置,但不可以设置为 0,因为这个是保留给 SceneGraphPriority使用的。

3、最后在GameScene.cpp文件中实现触摸回调函数
一旦玩家开始触碰屏幕,我们的程序就会开始调用相应的触摸事件处理函数来处理相应的逻辑,所以现在我们就可以来完成这部分的逻辑了。

实现中有以下几个需要注意的问题:

  • 需要判断触碰是单点还是多点,如果是多点,那么就缩放;是单点,就拖动。
  • 节点缩放的参考点默认是其锚点。显然,对于一个大地图背景来说,如果不实时改变它的锚点位置和本身位置,那它的缩放必然不会按照选取的区域进行缩放,必然会出现类似下图的情况。

p2

(上图中,背景图片的锚点在蓝点的位置,当我们想放大红圈所圈的那棵树时,如果只是简单的改变背景的放大倍率,那一定会出现上图的第二种情况(目标会向右上角偏移);但如果我们把背景的锚点和位置都设置到目标处,那就会像第三中情况一样,得到一个比较好的放大效果。)

  • 当缩放到一定程度,如缩小到与可视区域一样时,为了避免出现空白的区域,我们需要做一些处理。同时地图不能无止境的放大或缩小,需要有一定的范围来约束。比如,放大到它本身的4倍时,应该停止放大。
  • 拖动地图移动时,地图不能移出可视区域,这里需要做边界控制。

掌握了这些注意事项以后,现在我们就可以开始具体的行动了。

首先,在GameScene.h中定义如下的变量:

Sprite* bgSprite;
Vec2 bgOrigin;

bgSprite是地图背景,需要缩放和移动的对象都是其子节点,这样我们就可以通过操作它来实现缩放和移动了。bgOrigin用于记录bgSprite的初始原点位置。

接着,我们跳转到GameScene.cpp的init()方法,修改之前添加地图背景的方法,同时初始化bgOrigin。如下代码所示:

mapLayer = Layer::create();
this->addChild(mapLayer,-1);
 
bgSprite = Sprite::create("2.jpg");
bgSprite->setAnchorPoint(Vec2::ZERO);
bgSprite->setPosition(Vec2::ZERO),
bgOrigin = Vec2(Vec2::ZERO);
mapLayer->addChild(bgSprite);
 
auto treeSprite = Sprite::create("1.png");
treeSprite->setAnchorPoint(Vec2::ZERO);
treeSprite->setPosition(Vec2::ZERO),
treeSprite->setScale(2);
bgSprite->addChild(treeSprite, 2);
 
auto map = TMXTiledMap::create("mymap8.tmx");
map->setAnchorPoint(Vec2::ZERO);
map->setPosition(Vec2::ZERO),
bgSprite->addChild(map, 1);

因为对层而言,它相比于其他的节点来说,其锚点、位置、大小都不好控制,所以我们需要通过另外的节点(比如这里的bgSprite)来执行后面的缩放和移动等动作。

最后压轴来了,实现触摸事件的处理函数如下:

void GameScene::onTouchesMoved(const std::vector&touches, Event  *event)
{
    auto winSize = Director::getInstance()->getWinSize();
    if(touches.size() > 1)        // 多点进行缩放
    {
        // 得到当前两触摸点
        auto point1 = touches[0]->getLocation();
        auto point2 = touches[1]->getLocation();
        // 计算两点之间得距离
        auto currDistance = point1.distance(point2);
        // 计算两触摸点上一时刻之间得距离
        auto prevDistance = touches[0]->getPreviousLocation().distance(touches[1]->getPreviousLocation());
        // 两触摸点与原点的差向量,pointVec1和pointVec2是相对于bgSprite的位置
        auto pointVec1 = point1  - bgOrigin;
        auto pointVec2 = point2  - bgOrigin;
        // 两触摸点的相对中点
        auto relMidx = (pointVec1.x + pointVec2.x) / 2 ;
        auto relMidy = (pointVec1.y + pointVec2.y) / 2 ;
        // 计算bgSprite的锚点
        auto anchorX = relMidx / bgSprite->getBoundingBox().size.width;
        auto anchorY = relMidy / bgSprite->getBoundingBox().size.height;
        // 相对屏幕的中点
        auto absMidx = (point2.x + point1.x) / 2 ;
        auto absMidy = (point2.y + point1.y) / 2 ;
 
        // 缩放时,为了避免出现空白的区域,需要做以下的边界处理。        
        // 当bgSprite快要进入到屏幕时,修改bgSprite的位置(既absMidx和absMidy)。
        if(  bgOrigin.x > 0)
        {
            absMidx -= bgOrigin.x;
        }
        if( bgOrigin.x < -bgSprite->getBoundingBox().size.width + winSize.width )
        {
            absMidx +=  -bgSprite->getBoundingBox().size.width + winSize.width - bgOrigin.x;
        }
        if( bgOrigin.y > 0 )
        {
            absMidy -= bgOrigin.y;
        }
        if( bgOrigin.y < -bgSprite->getBoundingBox().size.height + winSize.height )
        {
            absMidy +=  -bgSprite->getBoundingBox().size.height + winSize.height - bgOrigin.y;
        }
        // 重设bgSprite锚点和位置
        bgSprite->setAnchorPoint(Vec2(anchorX, anchorY));
        bgSprite->setPosition(Vec2(absMidx, absMidy));
        // 根据两触摸点前后的距离计算缩放倍率
        auto scale = bgSprite->getScale() * ( currDistance / prevDistance);
        // 控制缩放倍率在1~4倍之间,最小倍率不能太小,不让背景将不能填充满整个屏幕。
        scale = MIN(4,MAX(1, scale));
        bgSprite->setScale(scale);
        // 更新原点位置
        bgOrigin = Vec2(absMidx, absMidy) - Vec2(bgSprite->getBoundingBox().size.width * anchorX, bgSprite->getBoundingBox().size.height * anchorY) ;
    }
    else if(touches.size() == 1)        // 单点进行移动
    {
        // 单点时,touches中只有一个Touch对象,所以通过touches[0]就可以得到触摸对象
        auto touch = touches[0];
        // 计算滑动过程中的滑动增量
        auto diff = touch->getDelta();       
        // 得到当前bgSprite的位置
        auto currentPos = bgSprite->getPosition();
        // 得到滑动后bgSprite应该所在的位置
        auto pos = currentPos + diff;
        // 得到此刻bgSprite的尺寸
        auto bgSpriteCurrSize = bgSprite->getBoundingBox().size;
 
        //边界控制,约束pos的位置
        pos.x = MIN(pos.x, bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x);
        pos.x = MAX(pos.x, -bgSpriteCurrSize.width + winSize.width + bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x);
        pos.y = MIN(pos.y, bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y);
        pos.y = MAX(pos.y, -bgSpriteCurrSize.height + winSize.height + bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y);
        // 重设bgSprite位置
        bgSprite->setPosition(pos);
 
        // 更新原点位置
        if( pos.x >= bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x
           || pos.x <= -bgSpriteCurrSize.width + winSize.width + bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x)
        {
            diff.x = 0;
        }
        if( pos.y >= bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y
           || pos.y <= -bgSpriteCurrSize.height + winSize.height + bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y)
        {
            diff.y = 0;
        }
        bgOrigin += diff;
    }
}

以上就是onTouchesMoved函数的实现方法了,原理已在注释中解释清楚,所以我想理解起来已经不会很难。下面给出一张示意图帮助大家理解:

p3

下图是缩放过程中刚好出现空白的区域时的图形示意图:

p4

此时空白的区域的宽等于 -bgSprite->getBoundingBox().size.width + winSize.width - bgOrigin.x,所以我们把背景的位置向右移动-bgSprite->getBoundingBox().size.width + winSize.width - bgOrigin.x个单位就可以避免这种情况的出现。

代码中有一点需要注意的是,在缩放过程中,bgSprite的尺寸不断变化的,所以计算起锚点或进行边界处理时,一定要用它缩放后的尺寸宽高来计算,而不能是它本身的宽高。 所以代码中计算bgSprite的尺寸我们用getBoundingBox函数来获得经过缩放和旋转之后的外框盒大小,而不用getContentSize函数来获得节点原始的大小。

iOS端多点触碰默认是关闭的,所以需要在AppController.mm 程序启动回调中启用多点触摸才可以,具体方法是在以下的函数段后加入[eaglView setMultipleTouchEnabled:YES];

如下所示:

CCEAGLView *eaglView = [CCEAGLView viewWithFrame: [window bounds]
                                 pixelFormat: kEAGLColorFormatRGBA8
                                 depthFormat: GL_DEPTH24_STENCIL8_OES
                          preserveBackbuffer: NO
                                  sharegroup: nil
                               multiSampling: NO
                             numberOfSamples: 0];
 
[eaglView  setMultipleTouchEnabled:YES];

总的来说,要想很好的实现这一功能不是容易的,以上就是我们实现了的一种方法,虽然细节上还有一些问题,也未在真机上测试,但还是希望能对大家的学习有所帮助。如果你有更好的方法实现,也可以提出来,大家一起进步学习。

下章将讲解如何利用CocoStudio来制作一个支持多分辨率的UI系统。

?>