Cocos

Quick-Cocos2d-x初学者游戏教程11

本章主要讲解物体碰撞检测之间的原理,以及具体的实现方法。

碰撞检测

本游戏使用物理引擎的一个重要目的是为了让碰撞检测更方便,使用物理引擎可以进行精确的碰撞检测,而且执行的效率也很高。

在 Quick 3.3final 版本中,所有事件均有事件派发器统一管理,物理引擎的碰撞事件也不例外。它由 cc.EventListenerPhysicsContact 的实例来监听。

监听事件分类

碰撞监听事件有以下几种:

  • cc.Handler.EVENT_PHYSICS_CONTACT_BEGIN
    它是碰撞刚发生时触发的事件,并且在此次碰撞中只会被调用一次。我们可以通过返回 true 或者 false 来决定物体是否发生碰撞。
    需要注意的是,当这个事件的回调函数返回 flase 时,EVENT_PHYSICS_CONTACT_PRESOLVEEVENT_PHYSICS_CONTACT_POSTSOLVE将不会被触发,但EVENT_PHYSICS_CONTACT_SEPERATE必定会触发。

  • cc.Handler.EVENT_PHYSICS_CONTACT_PRESOLVE
    它在碰撞接触后的每帧都会调用。在该事件的回调函数中我们可以计算碰撞处理的一些属性,比如弹力,速度等。同样它可以通过返回 true 或者 false 来决定物体是否发生碰撞。

  • cc.Handler.EVENT_PHYSICS_CONTACT_POSTSOLVE
    发生在碰撞计算完毕的每个步骤(帧),你可以在此做一些碰撞的后续处理,比如安全的移除某个物体等。

  • cc.Handler.EVENT_PHYSICS_CONTACT_SEPERATE
    发生在碰撞结束两物体分离时,同样只会被调用一次。

下面我们来看看添加碰撞监听的代码,如下所示:

local contactListener = cc.EventListenerPhysicsContact:create()   -- 1
contactListener:registerScriptHandler(onContactBegin, cc.Handler.EVENT_PHYSICS_CONTACT_BEGIN) -- 2
contactListener:registerScriptHandler(onContactSeperate, cc.Handler.EVENT_PHYSICS_CONTACT_SEPERATE) -- 3
local eventDispatcher = cc.Director:getInstance():getEventDispatcher() -- 4
eventDispatcher:addEventListenerWithFixedPriority(contactListener, 1) -- 5
  1. 创建物体碰撞检测事件监听器对象(contactListener);
  2. 设置监听器的碰撞开始函数 onContactBegin;
  3. 设置监听器的碰撞分离函数 onContactSeperate;
  4. 监听器设置完毕,需要把它加入到引擎导演的事件分发器中。所以这里获取游戏的事件分发器(eventDispatcher);
  5. 将检测事件监听器(contactListener)添加到事件分发器(eventDispatcher)中,这样就可以触发碰撞检测事件。addEventListenerWithFixedPriority 方法是指定固定的事件优先级注册监听器,事件优先级决定事件响应的优先级别,值越小优先级越高。

掩码属性

在讲解 onContactBegin 和 onContactSeperate 函数之前,这里我们需要清楚地一点。即在默认情况下,物理引擎中的物体是不会触发碰撞回调事件的。也就是说,上面代码中的 onContactBegin 和 onContactSeperate 方法永远都不会调用到。

为什么啦? O(∩_∩)O~呵呵,咱不卖关子。因为每个 cc.PhysicsBody 都具有三个掩码属性,两个刚体能不能碰撞,能不能发送接触事件信息,都依靠于这三个参数的值。

所以,为了更好地解决刚才遇到的问题,下面我们先来了解下刚体的这三个 mask 属性。

  • CategoryBitmask:32位整型,刚体的类别掩码。它定义了一个物体所属类别,每一个物体在场景中都能被分配到多达32位不同的类别。默认值为0xFFFFFFFF。

  • ContactTestBitmask:32位整型,刚体的接触测试掩码。当两个物体接触时,用一个物体的类别掩码与另一个物体的接触测试掩码做“逻辑与”运行,如果结果为非零值,引擎才会新建 PhysicsContact 对象,发送碰撞事件。那么才发送碰撞事件消息。
    ContactTestBitmask 的设计是为了优化性能,并不是所有物体之间的碰撞我们都关心,所有这个 ContactTestBitmask 的默认值为0x00000000。

  • CollisionBitmask:32位整型,刚体的碰撞掩码。当两个物体接触后,用一个物体的碰撞掩码与另一个物体的类别掩码执行“逻辑与”运算,如果结果为非零值,那么该物体能够对另一个物体的碰撞发生反应。这里的“碰撞反应”会表现为一个物体受到另外物体的碰撞,而改变运动方向。默认值为0xFFFFFFFF。

总结:

  1. CategoryBitmask 是其它两个掩码比较的基础。
  2. CategoryBitmask & ContactTestBitmask 决定是否发送事件消息。
  3. CategoryBitmask & CollisionBitmask 决定是否产生刚体反弹效果。
  4. ContactTestBitmask 和 CollisionBitmask 互相之间没有联系。

注:每个 mask 都有对应的 get 和 set 接口来获取或者修改mask。
另外,发生碰撞和发送事件消息是不同的概念,前者是直观地一种表现-碰撞反弹,后者是一种消息机制,就是说是否调用碰撞事件的回调函数。

回到我们的游戏,根据需求我们配置了一份各类 Node 的掩码属性表,如下所示:

节点 类别掩码 接触测试掩码 碰撞掩码
玩家:Player 0111 1111 1001
心心:Heart 0001 0100 0001
鸟:Bird 0010 0010 1000
飞艇:Airship 0100 0100 1000
地面 1000 0001 0011
天界 1000 0000 0001

首先,以玩家和心心为例,我们希望它们发生碰撞,并希望发送事件消息。所以,玩家的类别掩码0111 逻辑&与 心心的碰撞掩码0001结果为0001(不为0),发生碰撞;且玩家的类别掩码0111 逻辑&与 心心的接触测试掩码0100结果为0100(不为0),发送事件消息。反之,心心的类别掩码0001 & 玩家的碰撞掩码1001结果为0001(不为0),发生碰撞;且心心的类别掩码0001 逻辑&与 玩家的接触测试掩码1111结果为0001(不为0),发送事件消息。所以,玩家和心心两个物体相互接触时,它们会发生碰撞反弹,同时会发出事件消息。

我们再举一例,这次以玩家和鸟为例,我们希望它们不发生碰撞,但希望发送事件消息。所以,玩家的类别掩码0111 逻辑&与 鸟的碰撞掩码1000结果为0000(为0),不发生碰撞;且玩家的类别掩码0111 逻辑&与 鸟的接触测试掩码0010结果为0010(不为0),发送事件消息。反之,鸟的类别掩码0010 & 玩家的碰撞掩码1001结果为0000(为0),不发生碰撞;且鸟的类别掩码0010 逻辑&与 玩家的接触测试掩码1111结果为0010(不为0),发送事件消息。所以,玩家和鸟两个物体相互接触时,它们不发生碰撞反弹,但会发出事件消息。

设置属性

为了更方便碰撞检测,我们给各个节点设置一个标签。标签的定义我们可以放在 config.lua 文件中。定义如下所示:

GROUND_TAG   = 1
HEART_TAG = 2
BIRD_TAG = 3
AIRSHIP_TAG = 4
PLAYER_TAG = 5

根据以上的掩码属性和标签属性,我们在游戏项目中要设置好这些属性,如对于 Player,我们要在绑定刚体的地方加上如下的一段代码,即 Player:ctor()方法中:

body:setCategoryBitmask(0x0111)
body:setContactTestBitmask(0x1111)
body:setCollisionBitmask(0x1001)

self:setTag(PLAYER_TAG)

其他节点同理,这里就不一一添加了。

碰撞实现

下面我们给出碰撞检测的整段函数,程序中我们把它封装在了 addCollision 方法中,如下所示:

function GameScene:addCollision()

local function contactLogic(node)
-- 4
if node:getTag() == HEART_TAG then
-- 给玩家增加血量,并添加心心消除特效,下章会加上
node:removeFromParent()
-- 5
elseif node:getTag() == GROUND_TAG then
-- 减少玩家20点血量,并添加玩家受伤动画,下章会加上

elseif node:getTag() == AIRSHIP_TAG then
-- 减少玩家10点血量,并添加玩家受伤动画

elseif node:getTag() == BIRD_TAG then
-- 减少玩家5点血量,并添加玩家受伤动画
end
end

local function onContactBegin(contact) -- 1
-- 2
local a = contact:getShapeA():getBody():getNode()
local b = contact:getShapeB():getBody():getNode()
-- 3
contactLogic(a)
contactLogic(b)

return true
end

local function onContactSeperate(contact) -- 6
-- 在这里检测当玩家的血量减少是否为0,游戏是否结束。
end

local contactListener = cc.EventListenerPhysicsContact:create()
contactListener:registerScriptHandler(onContactBegin, cc.Handler.EVENT_PHYSICS_CONTACT_BEGIN)
contactListener:registerScriptHandler(onContactSeperate, cc.Handler.EVENT_PHYSICS_CONTACT_SEPERATE)
local eventDispatcher = cc.Director:getInstance():getEventDispatcher()
eventDispatcher:addEventListenerWithFixedPriority(contactListener, 1)
end

之前提到过的 onContactBegin 和 onContactSeperate 函数我们都写在了 addCollision 方法中,而 contactLogic(node) 方法则是碰撞检测的逻辑函数。

下面我们依次来看看这些代码,请顺着编号的顺序来看:

  1. onContactBegin 方法是碰撞开始时触发的回调函数。只有当场景中两个碰撞的物体的CategoryBitmask & ContactTestBitmask 不为0时才调用。
  2. 获取发生了碰撞的两个节点。
  3. 调用 contactLogic 方法检测碰撞逻辑。因为两个物体发生碰撞时,可能是A碰了B,也可能是B来碰了A,所以这里我们调用了两次 contactLogic 方法。
  4. 在 contactLogic 方法中,当被检测节点的标签为 HEART_TAG,即心心时,我们将在这里给玩家增加血量,添加心心消除特效,并且从屏幕中移除被撞的心心。
  5. 当被检测节点的标签为地面(GROUND_TAG)、飞艇(AIRSHIP_TAG)、鸟(BIRD_TAG)时,减少玩家相应地血量,并添加玩家受伤动画。其中给玩家增加血量,添加心心消除特效等我们下章再讲。
  6. onContactSeperate 方法是碰撞分离时触发的方法,在这里检测当玩家的血量减少是否为0,游戏是否结束。

在 GameScene 的 ctor 中调用 addCollision 方法后,你就可以实现碰撞检测了,此时只有玩家与心心碰上了才有反应。

本章说了太多细节,所以由于篇幅的原因,本章就此结束,咱们下章继续。

转载请注明出自:http://shannn.com/archives/435

Quick-Cocos2d-x初学者游戏教程10

在我们的游戏中,我们除了添加奖励品外,还需要添加一些必要的障碍物来丰富游戏逻辑,增加游戏难度,所以本章我们将继续上章的内容——添加游戏障碍物。游戏中,障碍物是不止一种,这里有飞行的鸟,有上下移动的飞艇。

创建障碍物-飞艇

其实创建飞艇的逻辑和前面创建心心的逻辑是一样的,只不过这里我想让飞艇不停的上下移动,一方面做点带感的效果出来,另一方面也可以增加游戏难度。

看过之前教程的童鞋,现在应该懂得怎样创建这样的一个飞艇了吧。所以下面我们直接给出它的定义:

local Airship = class("Airship", function()
return display.newSprite("#airship.png")
end)

local MATERIAL_DEFAULT = cc.PhysicsMaterial(0.0, 0.0, 0.0)

function Airship:ctor(x, y)

local airshipSize = self:getContentSize() -- 得到Airship自身的尺寸大小

local airshipBody = cc.PhysicsBody:createCircle(airshipSize.width / 2,
MATERIAL_DEFAULT)

self:setPhysicsBody(airshipBody)
self:getPhysicsBody():setGravityEnable(false)

self:setPosition(x, y)

local move1 = cc.MoveBy:create(3, cc.p(0, airshipSize.height / 2))
local move2 = cc.MoveBy:create(3, cc.p(0, -airshipSize.height / 2))
local SequenceAction = cc.Sequence:create( move1, move2 )
transition.execute(self, cc.RepeatForever:create( SequenceAction ))
end

return Airship

再强调一点的是:这里我们在创建刚体时把它的密度,反弹力、摩擦力都设为0是为了在碰撞的时候不发生任何物理形变。

密度是用来计算物体质量的,它可以是等于零或大于零的正数。摩擦力经常会设置在0.0到1.0之间,0.0表示没有摩擦力,1.0会产生强摩擦。弹性系数的值通常设置到0.0到1.0之间,0.0表示物体不会弹起,1.0表示物体会完全反弹,即称为弹性碰撞。

创建障碍物-鸟

这里我们要创建一个飞行的小鸟。创建它与创建心心唯一不同的是:它是一个动态的游戏对象。下面是 Bird 的定义:

local Bird = class("Bird", function()
return display.newSprite("#bird1.png")
end)

local MATERIAL_DEFAULT = cc.PhysicsMaterial(0.0, 0.0, 0.0)

function Bird:ctor(x, y)

local birdBody = cc.PhysicsBody:createCircle(self:getContentSize().width / 2,
MATERIAL_DEFAULT)

self:setPhysicsBody(birdBody)
self:getPhysicsBody():setGravityEnable(false)

self:setPosition(x, y)

local frames = display.newFrames("bird%d.png", 1, 9)
local animation = display.newAnimation(frames, 0.5 / 9)
animation:setDelayPerUnit(0.1)
local animate = cc.Animate:create(animation)

self:runAction(cc.RepeatForever:create(animate))
end
return Bird

封装加载函数

现在我们已经创建了心心、飞艇,还有鸟。虽然种类不多,但我已经不想再创建其他的了,反正原理都差不多,所以还是给大家留个自由发挥和创作的机会吧。

创建好游戏对象后,我们接下来要做的就是把它们都加载到场景中。这一过程你可以效仿上一章加载心心的方法来加载另外的两种游戏对象。不过如果你的要求更高,那你一定会想把这些函数封装一下,就如下列代码所示:

function BackgroundLayer:addBody(objectGroupName, class)
local objects = self.map:getObjectGroup(objectGroupName):getObjects()
local dict = nil
local i = 0
local len = table.getn(objects)

for i = 0, len-1, 1 do
dict = objects[i + 1]

if dict == nil then
break
end

local key = "x"
local x = dict["x"]
key = "y"
local y = dict["y"]

local sprite = class.new(x, y)
self.map:addChild(sprite)
end
end

addBody函数抽象出对象组的名字和类类型作为参数,这样我们就可以通过它来加载各种不同的游戏对象了。

self:addBody("heart", Heart)
self:addBody("airship", Airship)
self:addBody("bird", Bird)

加载这些对象时,不要忘了在 BackgroundLayer 文件中载入相应的文件:

local Heart = require("app.objects.Heart")
local Airship = require("app.objects.Airship")
local Bird = require("app.objects.Bird")

此时运行游戏,你就可以在场景中看见各种不同的游戏对象了。

something

给鸟添加飞行效果

目前这些障碍物和奖励品都只是随着背景滚动,显得游戏了无生趣,所以接下来我们来给鸟添加一个动态的飞行效果,让玩家可以明显的感受到它是朝前飞的。

那下面我们就来看看怎样实现吧。

1、首先,我们先给 BackgroundLayer 定义一个table数组来存放游戏中所有的鸟,即在ctor方法中加入如下的变量:

self.bird = {}

2、然后,在创建鸟这个游戏对象时,我们需要把所有的鸟都添加到定义的 self.bird 数组中。所以在 addBody 方法中,我们需要加上一则判断:

local sprite = class.new(x, y)
self.map:addChild(sprite)

if objectGroupName == "bird" then
table.insert(self.bird, sprite)
end

即当创建的对象是鸟(bird)时,就把该对象插入 self.bird。

3、接着,我们在 BackgroundLayer 中添加如下的一个函数:

function BackgroundLayer:addVelocityToBird()
local dict = nil
local i = 0
local len = table.getn(self.bird)

for i = 0, len-1, 1 do
dict = self.bird[i + 1]
if dict == nil then
break
end

local x = dict:getPositionX()
if x <= display.width - self.map:getPositionX() then
if dict:getPhysicsBody():getVelocity().x == 0 then
dict:getPhysicsBody():setVelocity(cc.p(-70, math.random(-40, 40)))
else
table.remove(self.bird, i + 1)
end
end
end
end

在该函数中,我们遍历 self.bird 数组中的所有 Bird 对象,当检测到某个 Bird 对象刚好要进入屏幕,且还没给过它任何速度时,我们会给它一个向左的速度,这个速度的范围从(-70, -40)到(-70, 40)。通俗一点就是说: Bird 对象将在横坐标上有一个大小为70,方向向左的速度;在纵坐标上有一个大小在(-40, 40)之间,方向不定的速度。

其中math.random(-40, 40)可以产生-40到40的随机数。为了不产生相同的随机数,我们需要在MyApp:run()中“种”一棵随机数种子,即添加如下的一行代码:

math.randomseed(os.time())

当已经给过某些 Bird 对象速度时,我们要把该对象从 self.bird 数组中移除,这样可以减短遍历数组的时间。table.remove(table, pos)函数将删除并返回 table 数组中位于 pos 位置上的元素。

总的来讲,addVelocityToBird 函数的目的就是在小鸟进入屏幕时给它一个速度,让它朝着游戏角色冲过来。

4、最后,因为我们需要不停的遍历 self.bird 数组、不停的检测是否给小鸟加上速度,所以我们需要在刷新屏幕时调用以上的 addVelocityToBird() 函数。

那就偷个懒,直接在 scrollBackgrounds(dt) 函数的最后面添加下列函数:

self:addVelocityToBird()

现在注释掉GameScene:ctor()方法中的self.world:setDebugDrawMask(cc.PhysicsWorld.DEBUGDRAW_ALL)和添加Player的代码,那我们可以得到如下的一个游戏效果:

fly

补充

因为游戏中的Player、Heart、Airship、Bird都是刚体,所以现在把这些刚体放在同一个物理世界是很容易发生碰撞、反弹、旋转、被挤出场景等等问题的。所以,这里我们要补充的就是怎样有效的解决这些问题,当然,制定具体的解决方案还需要结合碰撞检测来做,这个我们下章会讲。

setDynamic(dynamic):如果你想你的刚体固定不动,那么你可以调用该函数。如游戏中的心心,它在物理世界中就是个相对固定不动的对象,所以我们可以设置它的刚体属性dynamic为false,即 body:setDynamic(false) 。

setRotationEnable(enable):如果你想你的刚体不旋转,那么你可以调用该函数。如 Player、Airship 和 Bird 对象,它们在物理世界中是 Dynamic(动态)的,但我们不希望它们在动的过程中重心不稳发生旋转,所以,我们可以给 Player 刚体添加这样的属性加以约束,即:body:setRotationEnable(false)。

好了,今天就说到这里吧,这两章说的有点啰嗦,下章我们会加快进度,给大家讲讲如何给游戏添加触摸事件,并编写碰撞检测的逻辑。

关于源代码,下一两周内我会整理了会放出来。各位稍安勿躁,( ̄艸 ̄")!

转载请注明出自:http://shannn.com/archives/431

Quick-Cocos2d-x初学者游戏教程9

上章我们创建了游戏角色,这一章,我们需要把它添加到场景,并创建其他的障碍物和奖励品。

将游戏角色加入场景

本游戏将游戏主角加入游戏场景时,我们要实现如下的一个效果:就是在场景中创建一个游戏主角Player,先把它放在屏幕以外,进入游戏场景的一开始,我们将在屏幕左边以外的地方创建一个游戏主角 Player,然后让它飞入到场景的指定位置,在这段飞的过程中,它不受重力的影响(就不会从天上掉落),且我们让背景不滚动,等到主角到了它该到的地方时我们再让它受重力影响向下落,并且让背景滚动起来。

这章我们要实现的效果如下图所示:

res

PS:gif图本身有卡顿,且角色的动画资源帧数太少(美术太忙,不给做),所以角色飞的很奇怪,下落的也很奇怪,望见谅。

根据以上需求,下面我们就开始动手把把游戏角色加入场景。

首先打开 GameScene.lua 文件,在文件开头加入如下的代码载入 Player 文件:

local Player = require("app.objects.Player")

接着在 ctor 方法中创建一个 Player 对象。

self.player = Player.new()
self.player:setPosition(-20, display.height * 2 / 3)
self:addChild(self.player)
self:playerFlyToScene()

这里 self:playerFlyToScene 方法将使创建的 Player 对象移动到场景指定位置,其定义如下:

function GameScene:playerFlyToScene()

local function startDrop()
self.player:getPhysicsBody():setGravityEnable(true)
self.player:drop()
self.backgroundLayer:startGame()
end

local animation = display.getAnimationCache("flying")
transition.playAnimationForever(self.player, animation)

local action = transition.sequence({
cc.MoveTo:create(4, cc.p(display.cx, display.height * 2 / 3)),
cc.CallFunc:create(startDrop)
})
self.player:runAction(action)
end

在这个函数中,我们先从动画缓存中取得了我们需要的动画,然后让 self.player 主角不停的播放这个动画;接着又创建了一个动作(action)让 self.player 执行。

transition.sequence() 方法能创建一个 Sequence 动作序列对象,Sequence 类型的对象能使一个 Node 顺序执行一批动作,第四章中我们也提到过这个对象。回到我们的函数来看,这里创建了一个先执行 MoveTo 动作,等 MoveTo 动作执行完后再执行 CallFunc 动作的对象。通俗一点说它的作用就是,先移动到屏幕的(display.cx, display.height * 2 / 3)点,在调用 startDrop 方法。

MoveTo 动作应该不难理解,因为第四章我们也讲过它的兄弟 MoveBy。MoveBy 动作能使节点从当前坐标点匀速直线运动到相对偏移了一定向量的位置上。而 MoveTo 则能使节点从当前坐标点匀速直线运动到指定的位置坐标上。它们的移动位置一个是相对的,一个是绝对的,这也是Cocos 引擎中所有以 To,By 为后缀的动作的主要区别。

CallFunc 动作是个函数回调动作,它用来在动作中进行方法的调用,个人觉得它是非常有必要存在的一个动作。因为很多时候,当某些动作完成后,需要执行一些数据或逻辑上的处理(比如,一个敌人播放完死亡动画后需要把它移除并清理内存),这时使用函数回调动作 CallFunc 就是再好不过的。

startDrop 是个闭包函数,在该函数中,将开启主角受重力影响的开关,并且让游戏背景滚动起来。

由于之前我们已经直接让背景滚动起来了,所以这里需要做一些修改,即把更新相关的函数移动到 startGame 方法中。如下,在 BackgroundLayer 中添加 startGame 方法:

function BackgroundLayer:startGame()

self:addNodeEventListener(cc.NODE_ENTER_FRAME_EVENT, handler(self, self.scrollBackgrounds))
self:scheduleUpdate()
end

这样,我们的游戏角色就可以在场景中跑起来了。

当然,要实现我们的目标这里还需要把物理世界的重力设置下,之前设置的是(0,0),所以不管这样物理世界中的刚体都相当于没受到重力影响一样,现在我们把它改设为(0,-98)(默认的就是这么大),这样刚体才能受到重力。

在给 Player 添加物理特性时,我们还需添加如下的代码来设置它。

self:getPhysicsBody():setGravityEnable(false)

setGravityEnable 方法可以屏蔽物理世界中的刚体受到重力的影响。

创建奖励品

接下来我们来创建游戏中的奖励品,也就是心心。

我们新建一个 lua 文件,把它命名为 Heart 并保存到 objects 文件夹中,下面是该类的定义:

local Heart = class("Heart", function()
return display.newSprite("image/heart.png")
end)

local MATERIAL_DEFAULT = cc.PhysicsMaterial(0.0, 0.0, 0.0)

function Heart:ctor(x, y)

local heartBody = cc.PhysicsBody:createCircle(self:getContentSize().width / 2,
MATERIAL_DEFAULT)

self:setPhysicsBody(heartBody)
self:getPhysicsBody():setGravityEnable(false)

self:setPosition(x, y)
end

return Heart

根据我们前面章节的讲解,这个类现在所涉及到的所有知识点大家应该都很容易理解吧,所以我就不再多说了。只需要注意的一点是:这里我们在创建刚体时把它的密度,反弹力、摩擦力都设为0是为了在碰撞的时候不发生任何物理形变。

接下来我们来把心心加入游戏场景。

将奖励品载入场景

因为游戏中的奖励品/障碍物都是在地图上,所以我们回到 BackgroundLayer 文件,在这里把心心加入背景。

在 BackgroundLayer 中添加如下的函数:

function BackgroundLayer:addHeart()
local objects = self.map:getObjectGroup("heart"):getObjects() -- 1
-- 2
local dict = nil
local i = 0
local len = table.getn(objects)
-- 3
for i = 0, len-1, 1 do
dict = objects[i + 1]
-- 4
if dict == nil then
break
end
-- 5
local key = "x"
local x = dict["x"]
key = "y"
local y = dict["y"]
-- 6
local coinSprite1 = Heart.new(x, y)
self.map:addChild(coinSprite1)
end
end

addHeart方法的作用是遍历 TiledMap 中的 heart 对象层,取得该对象层中所有对象的坐标,并在该坐标上创建一个个心心对象。下面我们具体的讲解下代码,当然,可能结合下图要更容易理解点。

object

  1. getObjectGroup 方法从地图中获取到指定的对象层(也就是个 ObjectGroup 对象组对象),对象组 ObjectGroup 中包含了多个对象,所以我们可以通过 getObjects 方法从 ObjectGroup 中获得所有的对象。objects 在这里就相当于一个存放了多个对象的数组。
  2. dict 是个临时变量,用它来存储单个的对象;table.getn 方法能得到数组的长度。
  3. 遍历 objects 数组。
  4. 如果对象 dict 为空,则跳出 for 循环。
  5. 取出相应的属性值,即对象坐标。因为对象组中的对象在 TMX 文件中是以键值对的形式存在的,所以我们可以通过它的 key 得到相应的 value。
  6. 在获取到的坐标上创建 Heart 对象,并把它添加到 TiledMap 背景层上。这样创建的心心才能跟随着背景层的滚动而滚动。

之后,在程序中调用 addHeart,我们就可以创建出一系列的奖励品了。

添加物理世界边界

现在,我们的场景里已经有角色,有心心,此时运行游戏你会发现,当角色可以受重力影响之后,它会一直往下掉落,直至落出屏幕之外,显然这是不行的,所以我们必须在要在物理场景中添加一些能阻止刚体下落或飞出屏幕的边界。

所以,我们在 BackgroundLayer 的 ctor 方法中加入了如下的一段代码:

local width = self.map:getContentSize().width
local height1 = self.map:getContentSize().height * 9 / 10
local height2 = self.map:getContentSize().height * 3 / 16

local sky = display.newNode()
local bodyTop = cc.PhysicsBody:createEdgeSegment(cc.p(0, height1), cc.p(width, height1))
sky:setPhysicsBody(bodyTop)
self:addChild(sky)

local ground = display.newNode()
local bodyBottom = cc.PhysicsBody:createEdgeSegment(cc.p(0, height2), cc.p(width, height2))
ground:setPhysicsBody(bodyBottom)
self:addChild(ground)

这里我们创建了两根和 TiledMap 背景一样长的边界线。
createEdgeSegment 方法能创建一个不受重力约束的自由线条,它有四个参数,分别表示:

  • 参数1为 cc.p 类型,表示线条的起点;
  • 参数2也为 cc.p 类型,表示线条的终点;
  • 参数3为 cc.PhysicsMaterial 类型,表示物理材质的属性,默认情况下为 cc.PHYSICSBODY_MATERIAL_DEFAULT;
  • 参数4为 number 类型,表示线条粗细。

与之类似的函数还有:createEdgeBox,createEdgePolygon,createEdgeChain。它们都能创建不受重力约束的边界。具体的参数可跳转到它们的定义查看,这里我就不多说了。

资源已更新(plist文件重新打过包),请点击下载

转载请注明出自:http://shannn.com/archives/427

Quick-Cocos2d-x初学者游戏教程8

上章载入 TiledMap 背景后,接下来的这章我们将开始引入物理引擎相关的东西,并且会开始创建我们的游戏角色。游戏地图中各类障碍物和奖励品的创建则会留到下一章。

构建物理世界

首先,物理引擎是干什么的应该不用我说吧?好吧,还是说一下(百度的):物理引擎通过为刚性物体赋予真实的物理属性的方式来计算运动、旋转和碰撞反映。所以用它来模拟真实世界的飞行、掉落等功能是具有得天独厚的优势的,这也是为什么我们的游戏要使用它的原因。

然后,我们要怎样使用物理引擎啦?在之前的 Cocos2d-x 2.x 中,游戏需要选择直接调用 Box2D 或 Chipmunk 的 API 来处理逻辑,这种方法比较复杂,需要开发人员对物理引擎和 Cocos2d-x 都非常了解才能把两者融合得很好。而在现在的 Cocos2d-x 3.x(Quick 3.x同样)版本中封装了一个全新的 Physics 接口,这个接口将 Chipmunk(默认使用 Chipmunk 作为内部物理引擎)和 Box2D 封装到引擎内部,使开发者的上层调用变得更加友好、简单。

物理引擎属较高阶一点的内容,关于它的一些基础概念,比如:刚体、形状、密度、世界等等,可查找 Chipmunk 和 Box2D 相关文档进行脑补。本教程只针对需要使用的API做一定的了解。

封装了 Physics 接口后,物理世界被融入到 Scene 中,即当创建一个场景时,可以指定这个场景是否使用物理引擎。

回到我们的游戏,我们一开始就已经认定了 GameScene 是我们的主游戏场景,所以,它需要是一个使用物理引擎的游戏场景。所以,打开 GameScene 文件,我们需要在创建它的地方把它修改为带物理世界的 scene:

display.newPhysicsScene("GameScene")

之后,你可在 ctor 方法中加入如下的一段代码:

self.world = self:getPhysicsWorld()
self.world:setGravity(cc.p(0, -98.0))
self.world:setDebugDrawMask(cc.PhysicsWorld.DEBUGDRAW_ALL)

其中,self:getPhysicsWorld()用来获取场景绑定的物理世界对象。
获取的 self.world 默认是有带重力的,大小为(0.0f, -98.0f), 但我们也可以通过的setGravity()方法来改变重力大小。
setDebugDrawMask 方法是在调试物理世界时使用的,该方法可以开启调试模,能把物理世界中不可见的body,shape,joint等元素可视化(如下图所示)。当调试结束时,需要把该功能关闭。

bug

创建游戏主角

在第六章中我们曾说过,本游戏教程将创建一个飞行的娃娃角色,所以接下来我们一起来创建一个带物理特性的游戏角色。

在开始之前,我们先在 src/app 中创建一个文件夹 objects 来存放接下来要创建的游戏对象。

接着,新建一个 lua 文件并命名为 Player,然后把他存放入 objects 文件夹中,下面稍稍定义下这个类:

local Player = class("Player", function()
return display.newSprite("#flying1.png")
end)

function Player:ctor()
end

return Player

添加物理特性

接下来我们来给游戏主角添加物理特性。

引擎封装了 Physics 接口后,Node 就自带了 body 属性,也就是每个 Sprite 都自带了 body 属性。所以我们要创建一个可受重力作用的 Sprite 是非常容易的,下面我们在 Player 的 ctor 中加入如下的一段代码就可以为它绑定一个 body:

local body = cc.PhysicsBody:createBox(self:getContentSize(), cc.PHYSICSBODY_MATERIAL_DEFAULT, cc.p(0,0))
self:setPhysicsBody(body)

这里调用cc.PhysicsBody::createBox()方法创建了一个矩形的 body,createBox 方法有三个参数,分别是:

  • 参数1为 cc.size 类型,它表示矩形 body 的尺寸大小。
  • 参数2为 cc.PhysicsMaterial 类型,表示物理材质的属性,默认情况下为 cc.PHYSICSBODY_MATERIAL_DEFAULT。 该参数也可自定义,方法如下:
    cc.PhysicsMaterial(density, restitution, friction)

    • density:表示密度
    • restitution:表示反弹力
    • friction:表示摩擦力
  • 参数3为 cc.p 类型,它也是一个可选参数,表示 body 与中心点的偏移量,默认下为cc.p(0,0)

与 createBox 方法类似的还有 createCircle(radius, material, offset),该方法可以创建一个圆形的 body,除第一个参数为半径外,其余两参数与 createBox 方法一样。

body是有很多属性的,当我们有需要时,再调用对应的方法。

添加动画

我们的游戏主角有各种不同的游戏状态(飞行、下落、死亡),所以接下来我们就来实现它的这部分功能。

这里我们通过帧动画实现游戏角色的运动,帧动画的原理是将连续的帧图像(如下图)在渲染的时候通过逐帧或插值的方式播放出来而形成的动态效果。就像翻动小人书一样。

fly

引擎中,帧动画的具体内容是依靠精灵显示出来的,为了显示动态图片,我们需要不停切换精灵显示的内容,通过把静态的精灵变为动画播放器从而实现动画效果。帧动画由帧组成,每一帧都是一个纹理,我们可以使用一个纹理序列来创建动画。

Quick 框架中,我们可以这样来播放一个动画:

-- 创建一个包含flying1.png到flying4.png的4个图像帧对象的数组
local frames = display.newFrames("flying%d.png", 1, 4)
-- 以包含图像帧的数组创建一个动画 Animation 对象
local animation = display.newAnimation(frames, 0.3 / 4)
-- 在显示对象上循环播放动画,并返回 Action 动画动作对象。
transition.playAnimationForever(self, animation)

因为我们的游戏对象有不只一种的动画,所以在本教程中,我们可以先把这些动画都添加到动画缓存,这样在需要播放相应的动画的时候,我们就可以从缓存中直接获取动画来播放了,而不用再浪费时间重新创建动画。

所以,请在 Player.lua 文件中添加如下的一段函数:

function Player:addAnimationCache()
local animations = {"flying", "drop", "die"}
local animationFrameNum = {4, 3, 4}

for i = 1, #animations do
-- 1
local frames = display.newFrames( animations[i] .. "%d.png", 1, animationFrameNum[i])
-- 2
local animation = display.newAnimation(frames, 0.3 / 4)
-- 3
display.setAnimationCache(animations[i], animation)
end
end

animations,animationFrameNum分别表示角色的三种动画和三种动画分别有的帧总数。

遍历animations时,下面一一解释下函数的作用:

  1. 创建一个包含animations[i]1.png到animations[i]animationFrameNum[i].png的图像帧对象的数组,如i = 1,就是创建一个包含flying1.png到flying4.png的图像帧对象的数组。其中..是字符串连接操作符,它可以用来连接两个字符串。当其中一个为其它类型时,它会把该类型也转为字符串。
  2. 以包含图像帧的数组创建一个动画 Animation 对象,参数 0.3 / 4 表示 0.3 秒播放完 4 桢。
  3. 将2中创建好的 animation 对象以指定的名称(animations[i])加入到动画缓存中,以便后续反复使用。也就是我们在 AnimationCache 中可以通过animations = {"flying", "drop", "die"}这三种动画的名称来查找制定的 animation 对象。

把动画载入缓存后,我们就可以写下对应的函数去执行动画了。如下:

function Player:flying()
transition.stopTarget(self)
transition.playAnimationForever(self, display.getAnimationCache("flying"))
end

function Player:drop()
transition.stopTarget(self)
transition.playAnimationForever(self, display.getAnimationCache("drop"))
end

function Player:die()
transition.stopTarget(self)
transition.playAnimationForever(self, display.getAnimationCache("die"))
end

最后在创建 Player 对象后,我们就可以调用以上相应的方法来播放指定的动画了。

转载请注明出自:http://shannn.com/archives/422

Quick-Cocos2d-x初学者游戏教程7

回顾下上章的内容,上一章我们在背景层上加了层滚动的 “TMX背景”,但只是提到了 TMX 文件的加载,草草几笔就被带过了,所以本着尽职负责(啰里八嗦)的态度,本章我们还是先来了解下 TMX 地图文件的制作,同时处理背景层上滚动的元素(奖励品和障碍物)。

好的,那么现在啰嗦开始!!!

TMX 地图文件的制作

其实吧,TMX 地图的制作是非常简单的,在本人之前的教程中也讲了好多次,都说烦了。本想直接跳过的,但是仔细想一下,如果我现在不讲,回头又会被吐槽说教程跳太快,所有还是乖乖的一一说下吧,就算凑凑篇幅,会用 TiledMap 编辑器的请绕行。

首先,请打开之前安装好的 TiledMap 编辑器,并新建一个文件。点击新建后,会跳出如下的对话框,我们在这里设置 TMX 文件的基本属性。

map1

地图方向值创建的地图的朝向,这里我们选择正常就行,它将为我们创建一副直角鸟瞰地图(90°地图)。
层格式是指层中的数据怎样存储,这里我们也选择默认的。因为地图的数据一般都有比较大的冗余,所以利用zlib 可以得到很高的压缩比。
块大小是指每个 tile 元件的大小,它需要根据提供的图块资源大小来确定,建议图块大小为32 * 32的倍数。下图就是我们的图块资源(自己PS的,累成狗,其实根据我们游戏的特征,根本不需要这么多图块的,只是想表达下怎样做图块资源,所以就忍不住PS了一张,感兴趣的童鞋可以感受下)。

ps

这里每个图块的大小都为64 * 64,所以我们把 TMX 的块大小也设为64 * 64,不要问我为什么,因为我只能说这样好拼点。

地图大小指横竖方向上图块的多少,也就是表示该新建的地图中有多少格64 * 64的 tile 元件。
所以这里我们根据游戏的设定——需要一张长长的地图资源,设置地图大小为 300 * 10。设置好后,你会发现在地图大小属性栏下面,会输出最终的地图大小(19200 * 640)。

点击确认之后,一个空白的游戏地图就创建好了,如下图所示:

map2

因为地图太长,所有你可以通过右边的迷你地图面板来调节地图的显示大小(通过鼠标中键的前后滚动)和显示区域。如你创建的地图无上图中的网格,你可以点击菜单栏的 视图-》显示网格 将该功能开启,这样可以方便我们地图的制作。

接下来在菜单栏中选择 地图-》新图块 来导入图块资源,然后会出现如下所示的对话框:

map3

我们要做的就是点击浏览按钮,将之前准备好的图块文件载入到编辑器,其他属性可以不做修改。

再接着,就是个任性的过程了。在编辑器右下角的图块面板中选中相应的图块,然后把它拖动到渲染区拼一个自己想要的地图。例如:

map0

添加对象层

上章,我们说好要用 TMX 文件来承载游戏的障碍物和奖励品的。如果直接以拼图的方式将这些物品作为图块拼入地图,这样不是不可行的,然而有个问题是,如果这些障碍物或奖励品要求是动态的啦?显然地,这就不可行了。所以现在问题又来了:我们该拿它怎么办?

别急!我要告诉你,TiledMap 支持对象(Object)和对象组(ObjectGroups)功能,同时Quick 也支持,所有我们可以在地图上创建一个对象层,用对象来标示每个障碍物和奖励品的位置。

这里对象是为了开发者方便而设计的,它并不对应于某个地图图片,只是标明了某个位置,这样开发者可以通过相关 API 获取某个对象的位置,从而在相应的位置进行绘制。对象也是可以有名字的,这样可以通过相关 API 通过名字得到对象。它常常用于添加除背景以外的游戏元素(如道具、障碍物等)。

而对象组(objectgroup)顾名思义,它只是用来把多个对象包起来并进行分组的一个结构,当对象比较多时,利用它可以方便管理。

以下是 Cocos 中 TiledMap 的大致逻辑结构:
tmx1
更多细节内容可参考瓦片地图一文。(呵呵,其实也没那么详细啦!)

接下来回到主题,我们还是来为地图添加对象层吧。正如下图所示,我们在编辑器右上角的图层面板中新建了一些不同的对象层,然后在各对象层中依次添加了一些形状的对象,意图用这些对象来表示该位置上对应的障碍物或奖励品,也就是暂时用这些小方块小圆圈来代替具体的游戏实物。在程序中,我们可以遍历指定对象层上的全部对象,然后再在这些对象的坐标处创建相应的障碍物或奖励品,这样不管是它们是静态的还是动态的,都可以很方便的。这里形状和大小并无太大关系,因为我们在程序中只需要取对象的坐标值。

map4

在制作地图的过程中,你可以勾选图层前面的选项来决定图层上的对象是否显示,这样可以避免其他图层对我们产生的视觉干扰,有利于地图的制作。

现在我们的地图就算拼好了,之后需要做的就是把它保存起来,并连同图块资源一起拷贝到项目的res/image目录下,用于游戏加载了。

这里要注意的是,.tmx文件与.png资源默认情况下需要放在同级目录下,如需修改,可以打开.tmx文件修改路径。 对象组(ObjectGroups)中的对象(Object)在TMX文件中以键值对的形式存在,因此我们也可以直接在TMX文件中对它进行修改。

tmx

此时运行游戏,你就可以看到自己拼的地图在游戏中缓缓滚动了(PS:对象层的对象在游戏中是不可见的,所以看着效果不明显)。

资源我已放到Github仓库,大家可以下载下来跟着做做,相应的资源后续章节中我们还会持续更新。

转载请注明出自:http://shannn.com/archives/411

Quick-Cocos2d-x初学者游戏教程6

上一章我们介绍了开发中会用到的辅助工具,并创建了 GameScene 场景,接下来这章我们将继续 GameScene 的传(bai)奇(bi)。不过在开始编写 GameScene 场景的代码之前,我们还是先来明确一下游戏的功能和实现方法。这样可以帮我们更好的理解并设计逻辑。下面是总结出的结论:

  1. 在 GameScene 场景中,我们将创建一个飞行的娃娃角色,这个角色是游戏的唯一主角。游戏初始状态下,这个角色有满满的生命值,但随着时间的推移,生命值会不断的减少。这里生命值我们可以以进度条的形式来展示它的多少和增减。
  2. 游戏 GameScene 场景有不止一层的滚动背景,每层的背景滚动速率不一,这样可以产生立体的滚动效果。娃娃位置不动,让背景不停的滚动,可以产生娃娃在向前飞行的视觉感受。
  3. 在滚动过程中屏幕上会时不时的出现一些障碍物和可以增加生命值的奖励品,当娃娃碰到障碍物时,生命值减少;碰到奖励品时,生命值增加。
  4. 当触碰屏幕时,娃娃将向上升至一定的高度;当不触碰屏幕时,它将向下掉落。掉落到地板时会扣除相应的生命值。所以玩家必须在按与不按之间保持一定的平衡,一方面躲避障碍物并避免触碰地板,另一方面争取多吃一点奖励品,这样才能让娃娃顺利地到达终点。
  5. 本场景我们将用物理引擎来模拟整个飞行世界,这样游戏主角的飞行问题(漂浮状态),以及它与障碍物/奖励品之间的碰撞检测就很容易实现了。

GameScene 的背景

为了产生立体的滚动视觉效果,我们决定用四层不同的背景来实现这一效果(怕累死的,可以偷懒)。当然,这里层的概念不同于引擎中的层(cc.Layer),它只是一个普通的副词而已。

  • 首先,最底层是一张固定不动的布幕背景;
  • 其次,我将在布幕背景的上层添加一层远景背景,该背景将会以较慢的速度向左滚动;
  • 然后,在远景背景的上层我们又会添加一层近景背景,该背景将会以较快的速度向左滚动;
  • 最后,在最上层,我们将放置一层以更快速度向左滚动的背景,游戏的障碍物和奖励品都会在该层背景上。所以这层背景我们将用 TliedMap 编辑器来制作,也就是用 TMX 文件来创建。

乱入一个知识点:游戏中元素的层级关系由 z-order 属性来决定,我们可以通过设置元素的z-order值来控制元素之间的渲染顺序。默认情况下,所有元素的 z-order 为0,所以当游戏元素没有指定z-order值时,游戏中的元素将按添加顺序来显示。故此,我们在添加游戏背景或其他元素时,应该要注意下它们的添加顺序或 z-order 值,不要出现遮挡的现象。

创建BackgroundLayer背景层

为了让代码结构更加清晰,接下来我们将为 GameScene 场景创建一个背景层,然后把以上的四层背景图都添加到该层上。
frame

所以,我们在 src/app 目录下新建了一个layers 文件夹,然后再新建了一个 BackgroundLayer.lua 文件,并把它保存到 src/app/layers 目录下。

以下是创建空白 BackgroundLayer 层的代码:

BackgroundLayer = class("BackgroundLayer",function()
return display.newLayer()
end)

function BackgroundLayer:ctor()
end

return BackgroundLayer

display.newLayer()方法能创建并返回一个 cc.Layer 层对象。

注意:因为我们将在其他(GameScene.lua)文件中调用 BackgroundLayer 类,所以在创建 BackgroundLayer 类时,我们并没有像创建 GameScene 一样把它定义为 local 局部型的类。

把背景层添加到GameScene场景

BackgroundLayer.lua 文件是个单独的模块文件,如果我们想要引用它,那我们需要把它加载到项目中来。

通常,载入文件到 Quick 项目可以使用以下的两种方式:

  1. 通过require()方法,该方法会搜索指定的目录,并加载文件。
  2. 通过import()方法,该方法用于处理require同目录下其他模块,在模块名前加.。

它们的详细用法参见API

这里,我们载入 BackgroundLayer 模块用 require 方法。在 MyApp.lua 中加入如下的函数:

require("app.layers.BackgroundLayer")

这样就把我们定义的 BackgroundLayer 类引入到了quick项目中,之后,我们就可以在任何地方引用这个 BackgroundLayer 模块了。

接下来我们来把 BackgroundLayer 层加入到 GameScene 场景中。

function GameScene:ctor()

local backgroundLayer = BackgroundLayer.new()
:addTo(self)
end

这里调用BackgroundLayer.new()方法实例化了一个 BackgroundLayer 对象,并把它加入到场景。这样 GameScene 场景中就有一层空白的 BackgroundLayer 层了。

框架整好以后,下面我们来向层容器里面塞东西。

添加背景

布幕背景

回到 BackgroundLayer 文件,下面我们添加如下的一段函数来为 BackgroundLayer 层添加最底层的布幕背景,这里添加固定不动的布幕背景就如添加普通精灵。

function BackgroundLayer:createBackgrounds()
-- 创建布幕背景
local bg = display.newSprite("image/bj1.jpg")
:pos(display.cx, display.cy)
:addTo(self, -4)
end

addTo方法中可以指定游戏元素的 z-order 值,本教程中,我们把布幕背景的z-order设置为-4,确保它位于场景的最下层(当然,这要确保该层中不能有比布幕背景的 z-order 值还小的元素)。

循环滚动的远/近景背景

远景背景和近景背景有着共同的特征,它们都会以一定的速度向左循环移动。所以这里我们以远景背景为例,说明下它们的实现过程。

首先,我们添加两张首尾可以拼接的图片来表示滚动的远景背景图。
BackgroundLayer:createBackgrounds()方法中加入如下的代码来添加远景背景图:

-- 创建远景背景
local bg1 = display.newSprite("image/b2.png")
:align(display.BOTTOM_LEFT, display.left , display.bottom + 10)
:addTo(self)
local bg2 = display.newSprite("image/b2.png")
:align(display.BOTTOM_LEFT, display.left + bg1:getContentSize().width, display.bottom + 10)
:addTo(self)

table.insert(self.distanceBg, bg1) -- 把创建的bg1插入到了 self.distanceBg 中
table.insert(self.distanceBg, bg2) -- 把创建的bg2插入到了 self.distanceBg 中

其中 self.distanceBg 是一个 table 类型的值,它的定义我们放在 ctor 函数中。

self.distanceBg = {}

实现滚动背景,需要做的就是不断的改变背景图片贴图横坐标,并且不断的刷新位置。所以我们定义了一个滚动背景的函数scrollBackgrounds()。

function BackgroundLayer:scrollBackgrounds(dt)

if self.distanceBg[2]:getPositionX() <= 0 then
self.distanceBg[1]:setPositionX(0)
end

local x1 = self.distanceBg[1]:getPositionX() - 50*dt -- 50*dt 相当于速度
local x2 = x1 + self.distanceBg[1]:getContentSize().width

self.distanceBg[1]:setPositionX(x1)
self.distanceBg[2]:setPositionX(x2)
end

以上的这段函数的作用就是让 self.distanceBg1 和 self.distanceBg2 的 X 坐标都向左移动50 * dt (dt是时间间隔,两帧之间的时间间隔)个单位,self.distanceBg2 紧接在 self.distanceBg1 后面。

gs

在此之后,需要添加不断执行 scrollBackgrounds() 函数的方法,以确保远景背景不断的向左移动。使用过 Cocos2d-x 的童鞋应该知道,Cocos2d-x 中可以通过重载 update 函数在每帧刷新的时候执行自己需要的一些操作。在 Quick 框架中,我们把这种事件叫做帧事件,意思是每帧刷新时都会执行的事件。

帧事件在游戏中经常用来更新游戏中的数据。下面我们将在 ctor() 函数中加入这种帧事件,用以更新背景图的坐标。代码如下:

function BackgroundLayer:ctor()
self.distanceBg = {}
self.nearbyBg = {}
self.tiledMapBg = {}

self:createBackgrounds()

self:addNodeEventListener(cc.NODE_ENTER_FRAME_EVENT, handler(self, self.scrollBackgrounds))
self:scheduleUpdate()

end

其中,addNodeEventListener 方法用于注册帧事件,scheduleUpdate 方法则启用了帧事件,只有调用了 scheduleUpdate 后,帧事件才会被触发。

此时我们再用同样的方法添加近景背景(让它每次移动的距离大一些),运行游戏时,屏幕上就会出现滚循环移动的背景了。

bg

注:以上截图不是循环的。

最上层的TMX背景

背景层中还有一个重要的滚动项,那就是容纳了障碍物和奖励品的 TMX 类型的背景。

前面章节中我们已经提到过,TiledMap 编辑器能把单个的图块拼接成一幅完整的地图,而它的最终产物就是 TMX 文件。这里我们也不要把它想的有多复杂,其实说白了,渲染出来就是一张破图而已。和 png,jpg的图片精灵外形无明显差异。

所以它滚动的原理和远/近景背景滚动的原理差不多,只不过,它不循环。我们可以用一个以上的 TMX 文件来实现滚动,当最后一个 TMX 文件刚好显示完的时候游戏就结束。这里考虑到后续的碰撞检测,所以我们只用一个 TMX 文件实现滚动。

载入 TMX 文件的代码如下,添加的位置依旧在 createBackgrounds 方法中。

self.map = cc.TMXTiledMap:create("image/map.tmx")
:align(display.BOTTOM_LEFT, display.left, display.bottom)
:addTo(self, -1)

让地图文件滚动的代码如下:

if self.map:getPositionX()  <= display.width - self.map:getContentSize().width then
self:unscheduleUpdate() -- 禁用帧事件,停止整个背景层滚动
end

local x5 = self.map:getPositionX() - 130*dt
self.map:setPositionX(x5)

好了,本周的教程就算完成了,这里我们的 tmx 文件是在下暂时随便创建的一个,下一章我们会详细地讲解如何制作 tmx 文件。

另外,关于本游戏的资源我们将会在下章好好制作 tmx 文件后一并上传。

转载请注明出自:http://shannn.com/archives/406

Quick-Cocos2d-x初学者游戏教程5

上一章我们创建了游戏的菜单场景,并讲解了一些基础元素的创建,接下来这章,我们会先让大家了解一些接下来游戏开发中需要用到的辅助工具,然后再教大家创建另一个游戏场景,并跳着到该场景中来。

工具介绍

在开始真真的写代码之前,其实早该讲讲以下这些辅助工具的。这些工具可以让我们更好更方便的实现程序中的某些功能,比如地图编辑工具、粒子编辑工具等。尽管这些工具在我之前的教程中已经不厌其烦的讲了好多次了,但是为了做到真正的初学者教程,本小节还是先来简单的介绍下这些工具的使用方法,毕竟接下来的章节中我们会陆续的用到这些工具。还是那句话,希望对初学者有所帮助。

TexturePacker

TexturePacker 是一款图片编辑打包工具,它能将我们游戏中用到的多个图片资源打包成一张大图,这样加载图片不仅能节省空间,而且还能提升速度。
TexturePacker 的下载可以到它的官方网站进行下载。

Texturepacker工具的使用很简单,它的每个设置项都给出了相应的提示信息,这里我们打包的步骤如下:

texturePacker1

  1. 将资源图片拖动到 TexturePacker 右侧窗口中。
  2. 设置导出资源的格式、位置,这里会导出两种文件,其中一个是储存图片信息属性的列表文件,该文件只能是 .plist 格式;另一个文件是打包后的图片文件,它可以是 .png,.jpg,.pvr.ccz 等等格式,但这里建议打包为pvr.ccz格式,因为使用这种图片格式的好处有两点:1、可以使你的应用程序更小,因为图片是被压缩过了的。2、你的游戏能够启动地更快。
  3. 点击 Publish 按钮导出资源。

在 Quick 中要想使用这种被打包的资源,那么我们首先需要把它载入精灵帧缓存。以下函数能实现这一目的:

display.addSpriteFrames(数据文件名, 材质文件名)
  • 材质文件名:由多张图片组成;
  • 数据文件(plist 文件):记录了图片在材质文件中的位置等信息。
    如下是用Xcode打开的 plist 文件的结构,我想这样可以让各位更加直观的感受下打包这个概念。
    plist

精灵帧缓存 SpriteFrameCache 是用来存储精灵帧 SpriteFrame 的,SpriteFrame 精灵帧对象可以用来追踪所有精灵帧缓存中精灵的信息。缓存精灵帧有助于提高程序的效率。其中 SpriteFrameCache 是一个单例模式,它不属于某个精灵,是所有精灵共享使用的。

addSpriteFrames 方法将从传人的 plist 列表文件的元数据部分获取各个纹理的纹理名,并将它载入到纹理缓存中,然后解析它属性列表里的文件。

当我们要使用精灵帧来创建一个精灵时,我们可以像前面创建普通精灵一样使用newSprite方法。但为了和直接用文件中的图片创建精灵区分开来,Quick 中规定:如果图片名以“#”字符开头,那么该图片将从SpriteFrameCache中读取,如果没有“#”开头,才表示直接从文件中读取。

关于打包文件的实际应用,我们后面会给出例子。这里你可以先把资源载入精灵帧缓存,如下在 MyApp:run() 方法中添加以下代码:

display.addSpriteFrames("image/player.plist", "image/player.pvr.ccz")

TiledMap编辑器

TiledMap 编辑器是一款地图编辑工具,我们可以利用它把单个单个的图块(或称之为瓦片)拼接成一幅完整的地图,我们也称之为瓦片地图编辑器。

TiledMap 编辑器制作的地图为 TMX 格式的文件,该文件可以被 Cocos2d-x(Quick)很好的支持。其官方下载地址为:http://www.mapeditor.org/

本游戏中,我们将用TiledMap编辑器编辑本游戏的部分地图,具体的方法我们在用到时在详细讲解。

粒子编辑器

本游戏中,后面我们会用粒子效果来渲染游戏场景,使游戏更加炫丽,所以下面我要说说这个粒子编辑器是个咋回事!

因为游戏中往往需要有到一些特殊的粒子效果(如烟花、爆炸、下雪等等),所以粒子系统这样的程序模块便在各类游戏引擎中孕育而生,Cocos2d-x(Quick)也不例外,它为我们提供了强大的粒子系统。只不过Cocos2d-x(Quick)中的粒子系统有非常多的属性需要设置和调节,使用起来还是有些复杂。

所以为了能偷懒,程序员们开发了粒子编辑器,它可以很方便的编辑出漂亮的粒子效果,让你勉去手动设置粒子属性的过程。

常用的粒子编辑器也有两种,一种是ParticleDesigner,另一种是ParticleEditor(之前我也写过一篇关于如何使用ParticleEditor编辑器相关的文章,可供大家可以参考)。

教程中我们所用的粒子编辑器是ParticleDesigner,下面是我用该编辑器调出的一个粒子效果。

lizi

注意:如果想要调出满意的粒子效果,那么需要了解整个粒子系统的组成原理,所以想自己试试的童鞋可以参考《粒子效果》一文.

以上我们就粗劣的介绍完了开发中将用到的辅助工具,接下来我们来看看如何创建新的游戏场景。

新建游戏场景

在 Sublime 编辑器中新建一个 lua 文件是非常容易的,只需要在菜单栏中选择 File-》New File 就可以新建一个文档,然后再把这个文档以 .lua 的形式保存在 src ▸ app ▸ scenes 目录下就可以了。此处我们新建一个 lua 文件,取名为 GameScene。

这个 GameScene 场景将是本游戏的逻辑场景,飞行游戏的所有逻辑部分的代码都将在本场景中实现。所以下面我们就来开始构建这样的一个游戏场景吧。

首先,根据 MainScene.lua 文件的构成形式,我们依葫芦画瓢在 GameScene.lua 文件中加入如下的一段代码,该段代码将为我们创建一个空白的游戏场景。

local GameScene = class("GameScene", function()
return display.newScene("GameScene")
end)

function GameScene:ctor()
end

function GameScene:onEnter()
end

function GameScene:onExit()
end

return GameScene

添加好以上代码后,一个简单且背景黢黑的 GameScene 场景就算创建好了。

关于 GameScene 的代码我们暂时就这样子,在下章分析完 GameScene 的构成后我们再着手编写。下面我们先来跳转到 GameScene 场景看看跳转效果。

跳转场景

我们知道,游戏运行的第一个游戏场景是 MainScene 场景,所以,现在请打开 MainScene.lua 文件,让我们来添加代码使它能跳转到 GameScene 场景,添加的位置是在点击开始按钮时触发的 onButtonClicked 函数中。代码如下所示:

cc.ui.UIPushButton.new({ normal = "image/start1.png", pressed = "image/start2.png" })
:onButtonClicked(function()
app:enterScene("GameScene", nil, "SLIDEINT", 1.0)
end)

enterScene 方法我们曾在讲解 MyApp.lua 文件时提到过,当时只是使用该方法简单的进入场景而已,而此处我们则添加了特别的切换效果。并没有深入的解析,下面我们就来看看enterScene方法的定义,如下所示:

enterScene(sceneName, args, transitionType, time, more)

它的参数分别是:

  • sceneName:表示跳转场景的场景名,也就是我们将要进入的场景的场景名。
  • args:表示跳转场景传给该场景类构造函数的参数,args需要是一个table。
  • transitionType:表示场景切换的过渡动画类型,lua中定义的过渡动画类型差不多有30种.
  • time:表示从当前场景跳转到 sceneName 场景的过渡时间。
  • more:表示过渡效果附加参数。

所以,app:enterScene("GameScene", nil, "SLIDEINT", 1.0)表示从当前场景切换到 GameScene 场景,切换的过渡动画是 SLIDEINT 类型(新场景 GameScene 从顶部进入,同时现有场景 MainScene 从底部退出),整个切换过程用时1秒。

transitionType可以是如下的一些类型:

  • crossFade:淡出当前场景的同时淡入下一个场景
  • fade:淡出当前场景到指定颜色,默认颜色为 ccc3(0, 0, 0),可用 wrapSceneWithTransition() 的最后一个参数指定颜色
  • fadeBL:从左下角开始淡出场景
  • fadeDown:从底部开始淡出场景
  • fadeTR:从右上角开始淡出场景
  • fadeUp:从顶部开始淡出场景
  • flipAngular:当前场景倾斜后翻转成下一个场景,默认从左边开始翻转,可以指定为:

    • cc.TRANSITION_ORIENTATION_LEFT_OVER 从左边开始
    • cc.TRANSITION_ORIENTATION_RIGHT_OVER 从右边开始
    • cc.TRANSITION_ORIENTATION_UP_OVER 从顶部开始
    • cc.TRANSITION_ORIENTATION_DOWN_OVER 从底部开始
  • flipX:水平翻转,默认从左往右翻转,可用的附加参数同上
  • flipY:垂直翻转,默认从上往下翻转,可用的附加参数同上
  • zoomFlipAngular:倾斜翻转的同时放大,可用的附加参数同上
  • zoomFlipX:水平翻转的同时放大,可用的附加参数同上
  • zoomFlipY:垂直翻转的同时放大,可用的附加参数同上
  • jumpZoom:跳跃放大切换场景
  • moveInB:新场景从底部进入,现有场景同时从顶部退出
  • moveInL:新场景从左侧进入,现有场景同时从右侧退出
  • moveInR:新场景从右侧进入,现有场景同时从左侧退出
  • moveInT:新场景从顶部进入,现有场景同时从底部退出
  • pageTurn:翻页效果,如果指定附加参数为 true,则表示从左侧往右翻页
  • rotoZoom:旋转放大切换场景
  • shrinkGrow:收缩交叉切换场景
  • slideInB:新场景从底部进入,直接覆盖现有场景
  • slideInL:新场景从左侧进入,直接覆盖现有场景
  • slideInR:新场景从右侧进入,直接覆盖现有场景
  • slideInT:新场景从顶部进入,直接覆盖现有场景
  • splitCols:分成多列切换入新场景
  • splitRows:分成多行切换入新场景,类似百叶窗
  • turnOffTiles:当前场景分成多个块,逐渐替换为新场景

此时刷新 player,我们就会看到跳转场景的效果。
转载请注明出自:http://shannn.com/archives/401

Quick-Cocos2d-x初学者游戏教程4

前面我们已经大概的讲解完了Quick的框架和代码结构,接下来,本章开始我们将正式进入到游戏的开发。当然在开发的过程中,如果遇到值得一提的知识点和概念,我们还是会为大家详细讲解的。

哈哈,这章的内容我加它为——开发初探,因为我们将先来实现一些基础的内容。本章将实现的效果如下图所示:

mainscene

菜单场景

从前面章节讲解的的知识点中,我们知道每个新建的 Quick 项目里都已经默认创建好了一个 mainScene 场景,所以下面我们将利用这个现成的场景,把它改造成我们需要的游戏场景。

我们的游戏将是一个简单的游戏,所以不需要太复杂。理所当然的,第一个运行的游戏场景一般都是菜单场景,所以本小节我们的目的就是构建菜单场景,在这个场景中,我们会添加必要的节点和控件。

下面乱入一点基础知识,已经了解 Cocos2d-x 基础概念的童鞋可绕行,不用听我的废话。但如果你对 Cocos2d-x 一窍不通,并且想更加深入地了解这些概念的童鞋可参考《Cocos2d-x 中的基础概念》一文。这里我们也只简单说说:
Cocos2d-x ( Quick 同样)是一款基于节点树渲染的游戏引擎,它把游戏的各个部分抽象成了导演、节点、场景、层、精灵等等一系列的概念,所以你可以把 Cocos2d-x 游戏想象成一部电影。同电影一样,Cocos2d-x 游戏中每个时刻都有一个场景在独立运行,我们通过切换不同的场景可以完成整个游戏流程,场景切换的管理由导演来执行。
一个游戏可有多个不同的游戏场景,每个场景又可包含多个不同的层(或其他节点),每层可拥有任意的游戏节点(常见的是精灵,但也可以是层、菜单、文本等等)。其基本构架如下图所示:
frame
现在还不是很理解也没有关系,因为新手都是在实际运用的过程中慢慢理解,慢慢积累出经验的。

菜单场景对于我们来说就只是一个简单的游戏场景而已,下面开始我们就来在菜单场景中挨个添加游戏对象吧!

添加背景精灵

首先,我们来给游戏添加一张背景图,背景资源大小如第二章分辨率适配时决定的一样,为 1136 x 640 的大小。

添加游戏资源时,我们必须把包括背景在内的所有图片资源都拷贝到 res 目录下(放到别处将无法被引用)。当然,为了和可能有的字体文件、音效文件等等游戏资源区分开来,你也可以像我一样,先在 res 目录下新建一个叫做 image 的文件夹,然后再把资源拷贝到这个 res/image 目录下。

拷贝资源过后,我们就可以修改代码来添加背景图片了。所以,接下来请打开 MainScene.lua 文件,在它的ctor方法中删除创建 “Hello World” 文本的那段多余代码,并添加如下的一段新代码来添加背景图片:

display.newSprite("image/main.jpg")
:pos(display.cx, display.cy)
:addTo(self)

在以上的代码中,我们使用 display.newSprite() 方法创建了一个精灵对象,并把该对象添加到了 MainScene 场景中相应的位置上。

在此之后,保存好修改过的 lua 文件,然后切换到我们项目的 player 模拟器下,按住热键 command + R ( Windows 下按 F5 )就可以刷新 player 模拟器了,此时你就可以立即看到添加了背景图片的游戏场景了。如下图所示:

bg

验证分辨率适配结果

需要注意的是,在 player 模拟器的 View 菜单下,为我们提供了数十种常见的设备分辨率,我们可以任意的切换到各种不同的分辨率上,以便我们测试游戏在不同分辨率上的显示效果。

view

大家可以试着选择一些,不过你会发现,不管我们再怎么选择,我们的屏幕始终是被背景图片铺满的,不会留有任何的黑边。这也就验证了我们之前的分辨率适配是成功的。

fblsp

添加标题并让它上下运动

接下来我们来添加游戏的标题,这个标题会如开篇效果图中显示的一样——会一直不停的上下晃动。
要添加这样的一个效果,我们同样需要在 ctor 方法中添加如下的一段代码:

local title = display.newSprite("img/title.png")
:pos(display.cx / 2 * 3, display.cy)
:addTo(self)

local move1 = cc.MoveBy:create(0.5, cc.p(0, 10))
local move2 = cc.MoveBy:create(0.5, cc.p(0, -10))
local SequenceAction = cc.Sequence:create( move1, move2 )
transition.execute(title, cc.RepeatForever:create( SequenceAction ))

在这里,我们先添加了一个精灵图片到场景上,位置在(display.cx / 2 * 3, display.cy)的坐标处。紧接着,我们让标题精灵执行了一系列的动作效果。
其中 transition.execute(target, action, args) 方法用于执行一个动作效果,它可以为原本单一的动作添加各种附加特性。其参数分别代表:

  • target: 显示对象(cc.Node)
  • action: 动作对象
  • args: table 参数表格对象

在我们的代码中,action 参数是我们自己构建的一个复合动作,它是一个永远都保持上下移动的动作。其中我们使用了3种类型的action来创建它:

  • cc.MoveBy:该动作将使节点从当前坐标点匀速直线运动到相对偏移了一定向量的位置上。其create函数的两个参数分别表示运动到指定位置所需的时间和移动的距离(偏移量),所以 move1 表示在0.5秒内向Y轴的正轴上移动10个像素,move2 表示在0.5秒内向Y轴的负方向上移动10个像素。
  • cc.Sequence: 该动作允许我们把一系列动作组合起来,并按顺序执行它们。在上面的例子中,我们创建了一个顺序执行 move1、move2的动作,这个 SequenceAction 动作会首先执行 move1,等 move1 完成后,再马上执行 move2。 这样一来一回,执行该动作的节点最终会回到原来的位置上。
  • cc.RepeatForever: 该动作是一个无限重复执行的动作。cc.RepeatForever:create( SequenceAction )表示创建了一个无限循环执行 SequenceAction 的动作。

保存文件后刷新 player,你将看到一个一直不停运动的标题栏:

title

添加按钮

下面我们继续添加按钮,其代码如下所示:

cc.ui.UIPushButton.new({ normal = "img/start1.png", pressed = "img/start2.png" })
:onButtonClicked(function()
print("start")
end)
:pos( display.cx / 2, display.cy )
:addTo(self)

在Quick中有三种不同的Button控件,分别是:UIPushButton (按钮控件)、UICheckBoxButton ( CheckButton 控件)和 UICheckBoxButtonGroup ( CheckButton 组控件)。

其中 UIPushButton 是最常用的按钮控件,它继承自UIButton,我们可以通过 cc.ui.UIPushButton.new(images, options) 方法来创建 UIPushButton。参数 images 是 table 类型,它代表各个按钮状态(正常、按下、禁用)下的图片;options 为可选参数,也是 tabl e类型,包含了是否scale9缩放,偏移flipX、flipY值等设置。

onButtonClicked 方法用于监听按钮的点击事件,当点击按钮时,将调用该方法中的代码。如上例中,当我们点击按钮时,会在控制台窗口中打印“start”的字段。同 onButtonClicked 方法类似的还有:

  • onButtonPressed(callback):用于监听按钮的按下事件
  • onButtonRelease(callback):用于监听按钮的释放事件
  • onButtonStateChanged(callback):用于监听按钮的状态改变事件

刷新 player,其效果就会出来。

btn

好了,到现在为止,本章我们的目标就算实现了,下章我们将新建其他游戏场景,敬请期待!!
转载请注明出自:http://shannn.com/archives/392

Quick-Cocos2d-x初学者游戏教程3

本章我们继续解析新建项目的代码文件,衔接不上的童鞋可以先回顾下上章的内容。那么下面我们就言简意赅直接进入正题吧!

2、main.lua

在src目录下,除了 config.lua 文件外,还有一个 main.lua 文件,这个 main.lua 是 Quick 项目的通用入口文件,它类似于 Cocos2d-x 中的 AppDelegate.h/cpp 文件,同时也类似于一般 Windows 工程中的 main 文件。

打开 main.lua 文件,其内容如下所示:

function __G__TRACKBACK__(errorMessage)
print("----------------------------------------")
print("LUA ERROR: " .. tostring(errorMessage) .. "\n")
print(debug.traceback("", 2))
print("----------------------------------------")
end

package.path = package.path .. ";src/"
cc.FileUtils:getInstance():setPopupNotify(false)
require("app.MyApp").new():run()

每个新项目的 main.lua 文件内容都是一样的,一般情况下,我们不需要改动它。
我们只需要知道,main 文件的最后一行代码中通过载入的 app.MyApp 模块创建了一个 MyApp 实例,并且还调用执行了该 MyApp 实例的 run 方法。这行代码将启动并执行 MyApp 脚本。从此处我们也可以看出,main.lua 文件是应用程序 lua 脚本的启动文件。

require 方法表示引入一个文件,使用 require 方法加载文件的过程会检查文件的 lua 语法,同时会完成被加载文件内部变量的初始化。

3、MyApp.lua

main.lua 文件启动执行 MyApp 脚本后,接下来我们就来看看“src/app”目录下的 MyApp.lua 文件,该文件将启动游戏场景,其代码段如下所示:

-- 1
require("config")
require("cocos.init")
require("framework.init")

-- 2
local MyApp = class("MyApp", cc.mvc.AppBase)

-- 3
function MyApp:ctor()
MyApp.super.ctor(self)
end

-- 4
function MyApp:run()
cc.FileUtils:getInstance():addSearchPath("res/")
cc.Director:getInstance():setContentScaleFactor( 640/CONFIG_SCREEN_HEIGHT )
self:enterScene("MainScene")
end

return MyApp

我们依次来看以上各代码行的含义:

  1. 使用 require 方法载入配置、cocos 以及 Quick 框架。这些是每个 Quick 程序都必须要加载的。
  2. 使用 class 函数定义一个 MyApp 类。
    class 方法本身是 Quick 框架中定义的一个用于创建自定义 lua 类的函数,该函数可在 Quick 引擎的 “quick/framework/functions.lua” 文件中找到。当然,如果你听了我的话,在 Sublime 下安装了 QuickDev 插件,那么你也可以在 class 字符上单击右键,跳转到它的定义(Goto Definition),这样也能找到它。
    gotodefinition
    在 “functions.lua” 中,function class(classname, super)函数有两个参数,其中 classname 表示的是类名,super 表示的是父类或者创建对象实例的函数。当 super 传入的参数是前者时,将会以传入的对象为父类派生新类,如本处;当传入的参数为后者时,会以传入的函数作为构造函数创建新类,如稍后要讲解的 MainScene 类。
    从本段代码中我们可以看到,MyApp 这个类是从 cc.mvc.AppBase 中继承而来的,而 AppBase 是 Quick 中自带的一个 MVC(面向对象的一种设计模式)的应用程序基础类,这个类为我们的应用程序提供了一些逻辑控制上的功能,比如进入场景,切换场景等等。
  3. MyApp 的ctor()函数相当于 C++ 中的构造(constructor)函数,以及init()函数。所以我们一般会在ctor()函数中初始化我们的游戏数据和信息,每当调用 XXClass.new() 创建对象实例时,也会自动执行ctor()函数。
    另外,在子类的ctor函数中必须手动调用父类的构造函数,这样才等保证子类能“继承”父类的属性和方法,正如上段代码所示。其中 XXClass.super 可以访问指定 XXClass 类的父类。
  4. 在 MyApp 的 run 方法中,这里做了三件事:
    • 首先,通过 addSearchPath 方法设置资源搜索路径。这也是为什么资源一定要放在“res”目录下的原因。
    • 然后,通过 setContentScaleFactor 方法设置资源缩放因子,这行代码是我们在上章进行分辨率适配时自己添加的。
    • 最后,因为 MyApp 类继承于 AppBase 类,因此自然地,MyApp 也同父类一样,具备了些许逻辑控制的功能。所以在 run 方法的最后,程序调用 enterScene 函数进入到名为 “MainScene” 的游戏场景。
      当然我们也可以在 enterScene 函数中设置场景切换的动画,不过为了加深大家对场景切换的印象,我们还是留到后面使用的时候再做详细的讲解。 > 注意:游戏场景默认必须放在 “src/app/scenes” 目录下,放到其他目录下是不能找到的。

4、MainScene.lua

讲的这里,大家应该很清楚 main 文件执行 require("app.MyApp").new():run() 代码的过程了吧。所以,接下来请大家打开“src/app/scenes”目录下的 MainScene.lua 文件,我们接着来看它的结构。其代码如下图所示:

local MainScene = class("MainScene", function()
return display.newScene("MainScene")
end)

function MainScene:ctor()
cc.ui.UILabel.new({
UILabelType = 2, text = "Hello, World", size = 64})
:align(display.CENTER, display.cx, display.cy)
:addTo(self)
end

function MainScene:onEnter()
end

function MainScene:onExit()
end

return MainScene

在 MainScene.lua 这个文件中,第一行代码使用 class 函数创建了一个 MainScene 场景类,同前边创建 MyApp 类不同的是,这里我们的类不是从基础类继承而来,而是通过一个函数创建出来的。

这是因为 Scene 场景对象必须是一个 C++ 的对象,而 C++ 的对象是无法直接派生出 Lua 的类的,所以我们只有用一个函数把它创建出来,然后再为它添加相应的方法。

在创建场景的函数段里,display.newScene() 方法将创建了一个新场景,并返回该 Scene 场景对象。

这里的 display 是一个重要的概念,在 Quick 中,display 模块封装了绝大部分与显示有关的功能,并负责根据 config.lua 中定义的分辨率设定计算屏幕的设计分辨率。display 模块提供了众多的方法和属性,比如创建层(display.newLayer),创建精灵(display.newSprite),以及恢复暂停切换场景等等。

总而言之,display 是个很值得我们细细探索的模块。关于它更多详情内容可参见Quick-Cocos2d-x的API文档

下面我们例举一些 display 常用的属性:

  • display.widthInPixels, display.heightInPixels: 屏幕分辨率的宽、高
  • display.width, display.height: 设计分辨率的宽、高
  • display.cx, display.cy: 设计分辨率中央的 x、y 坐标
  • display.left, display.top, display.right, display.bottom: 设计分辨率四边的坐标
  • display.c_left, display.c_top, display.c_right, display.c_bottom: 当父对象在屏幕中央时,屏幕四边的坐标
  • display.contentScaleFactor: 设计分辨率到屏幕分辨率的缩放因子,不同于内容缩放因子。

结合 display 模块提供的常量,我们可以很方便的获取屏幕的某些与现实相关的属性,如位置和颜色等等。display 模块中关于位置的常量是初始化的时候程序就已经计算好的(不信你可以查看控制台窗口中的打印信息),它们根据分辨率的不同而不同。
displayValue

MainScene 的 ctor 函数中默认添加了一个 UI 标签,这个标签的作用是在屏幕正中间添加一个“Hello World”的文本,并显示到屏幕上。

其中 cc.ui 模块中封装了大量符合脚本风格的 Cocos2d-x 控件,包含 UILabel、UIImage、UISlider 等等。所以正如上述代码一样,我们可以通过调用cc.ui.UILabel.new()来实例化一个新的 UILabel 控件。其先有参数分别表示:

  • UILabelType:创建文本对象所使用的方式。
    1 表示使用位图字体创建文本显示对象,返回对象是 LabelBMFont。
    2 表示使用 TTF 字体创建文字显示对象,返回对象是 Label。
  • text:要显示的文本。
  • size:文字尺寸。

创建好 UILabel 后,通过调用 align 方法设置文本的锚点和显示位置。最后用 addTo 方法把文本添加到场景中。

至于 MainScene 的 onEnter 和 onExit 两个方法,清楚 Cocos2d-x 原理的的童鞋应该很容易理解。其中onEnter 方法是在进入场景的时候调用的,我们可以在这里做一些初始化的工作;而 onExit 方法是在退出场景的时候调用的,在这里我们可以释放资源,重置变量。

小结

到现在为止,我们就已经解析完 src 目录下所有的 lua 文件了,总的来说,这两章所解析的 lua 文件的关系和用途如下图所示:
lc1

好了,上图把这两章讲解的废话都概括完了,我想应该够清楚明了的吧!! O(∩_∩)O~,从下章开始,我们将正式开始类el游戏的开发。

转载请注明出自:http://shannn.com/archives/387

为创业者服务,Cocos开发者沙龙成都站将盛大开启

7月2日,cocos开发者沙龙与你相约蓉城,重量级嘉宾齐聚程度天府软件园十分咖啡,为手游开发者们奉上全方位的信息盛宴(免费报名
)。届时,触控科技高级技术总监林顺、UCloud架构师幸文超、DataEye商务VP黄朝斌、Testin西南区总经理兼技术顾问朱仰林、触控科技高级商务经理杨志成将分别发表演讲,内容涵盖手游开发的几个热门话题。你不仅可以现场聆听手游大牛的精彩演讲,还可以与他们积极互动,解开心中的游戏开发之惑。

Cocos是一款开源高效的游戏引擎,在过去短短数年中已发展为全球占有率第一的手机游戏引擎,全球份额超过30%,中国份额超过70%。《梦幻西游》、《刀塔传奇》、《我叫MT》、《保卫萝卜》、《捕鱼达人》等多款超人气国内手游,以及国外《怪物弹珠》、《勇者斗恶龙:超级光芒》、《Badland》等精品手游,均采用cocos引擎开发。那么cocos引擎在3D功能和页游方面又有哪些新进展呢?这个问题的答案将在本次沙龙上,由触控科技高级技术总监林顺为你揭晓。同样来自触控科技的高级商务经理杨志成,他的演讲主题,则是关于Cocos Play的, 听完他的演讲,或许你不再难理解Cocos Play如何让手游轻松步入微端时代。

HTML5风靡了整个手游行业,因之而起的商业化浪潮一浪更比一浪高,投资界各种关于HTML5的估值神话也不断见诸报端。作为开发者,肯定也很想了解有关HTML5的信息。令人惊喜的是,DataEye商务VP黄朝斌想开发者所想,在本次沙龙上将发表一个报告,标题是《2015HTML5游戏趋势报告》,为与会者答疑解惑。

此外,Testin西南区总经理兼技术顾问朱仰林亦将发表演讲,题目是如何提高手游的开发质量和稳定性;而UCloud架构师幸文超在沙龙上的话题也具有高度实战意义:UCloud用最领先的云构建最具挑战的游戏。

Cocos团队还为开发者们准备了丰富的奖品,如Coco熊、cocos限量U盘、磁悬浮地球仪、定制移动电源以及价值2万元神秘大奖。只要你愿意来,即有机会把它们抱回家。

?>