Quick-Cocos2d-x-3.2中示例Coinfilp解析

前提:本文笔者使用是的Quick-Cocos2d-x v3.2-RC0和Cocos Code IDE进行学习的。

首先,用 play3 新建一个工程(名字大家就任意起吧)

1412911045745011.png

工程建立完成后,大家进去自己Quick-Cocos2d-x v3.2-RC0所在目录,然后进入...\quick\samples\coinflip 中,将当中的两个文件夹res和src复制下来。

(其中,res存放游戏资源,src存放lua代码)

1412911081454553.png

然后进入刚才我们新建的工程中,将当中的 res 和src 替换为刚才复制的res和src。

接着使用play3运行我们新建的工程,是不是与示例中的coinflip一样了?

打开Cocos Code IDE,将该工程导入,就可以进行代码的查看和修改了!


一、基础的main.lua、config.lua 和MyApp.lua

先打开src下的main.lua

1
2
3
4
5
6
7
8
9
function __G__TRACKBACK__(errorMessage)
     print( "----------------------------------------" )
     print( "LUA ERROR: "  .. tostring(errorMessage) ..  "\n" )
     print(debug.traceback( "" , 2))
     print( "----------------------------------------" )
end
  
-- 启动后执行MyApp脚本, 并执行当中的 run() 方法
require( "app.MyApp" ). new ():run()

每个新建的工程的main.lua都一样,不需要改动,我们只要知道它是程序lua脚本的启动文件就够了。接着我们沿着最后一句代码 require("app.MyApp").new():run() ,打开MyApp.lua, 观察当中的run()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function MyApp:run()
     -- 设置资源搜索路径
     cc.FileUtils:getInstance():addSearchPath( "res/" )
      
     --创建图片缓存
     --在config中
     --GAME_TEXTURE_DATA_FILENAME  =  "AllSprites.plist"
     --GAME_TEXTURE_IMAGE_FILENAME =  "AllSprites.png"
     display.addSpriteFrames(GAME_TEXTURE_DATA_FILENAME, GAME_TEXTURE_IMAGE_FILENAME)
  
     -- 预加载音频文件
     for  k, v in pairs(GAME_SFX)  do
         audio.preloadSound(v)
     end
  
     -- 进入场景
     self:enterMenuScene()
end

/

GAME_TEXTURE_DATA_FILENAME

GAME_TEXTURE_IMAGE_FILENAME

GAME_SFX

都是config.lua中定义得全局变量

//



在run()方法当中主要做了三件事:

1、设置资源搜索路径;

2、加载游戏资源;

3、进入主场景;

也许你会说在这个脚本文件里找不到  GAME_TEXTURE_DATA_FILENAME 和  GAME_TEXTURE_IMAGE_FILENAME 相关代码,你应该打开config.lua ,所有的 配置信息 和 宏定义 都在里面(个人觉得统一写在config.lua中更方便查找,比C++更容易读懂)


同时MyApp.lua中还封装了所有场景的切换方法,更加方便了管理和后续的编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function MyApp:enterMenuScene()
     self:enterScene( "MenuScene" , nil,  "fade" , 0.6, display.COLOR_WHITE)
end
  
function MyApp:enterMoreGamesScene()
     self:enterScene( "MoreGamesScene" , nil,  "fade" , 0.6, display.COLOR_WHITE)
end
  
function MyApp:enterChooseLevelScene()
     self:enterScene( "ChooseLevelScene" , nil,  "fade" , 0.6, display.COLOR_WHITE)
end
  
function MyApp:playLevel(levelIndex)
     self:enterScene( "PlayLevelScene" , {levelIndex},  "fade" , 0.6, display.COLOR_WHITE)
end


二、第一个场景MenuScene

了解了基础的几个脚本后,我们跟着 self:enterMenuScene() 这个句代码,进入第一个场景MenuScene,场景界面如下

1412911200791580.png

打开src\app\scenes\MenuScene.lua

1
2
local AdBar = import( "..views.AdBar" )
local BubbleButton = import( "..views.BubbleButton" )

开头两句其实作用是跟C++里的导入是一样的,因此,很明显该场景中将会用到这个两个自定义的类。


往下看

1
2
3
local MenuScene =  class ( "MenuScene" , function()
     return  display.newScene( "MenuScene" )
end)

class方法有两个参数,第一个参数是类名。第二参数可以通过两种形式传递参数,一种是传入一个函数,另一种方式是传入一个Quick的类或者是Lua对象。

当传入函数时,新创建的类会以传入的函数作为 构造函数。当传入的是一个对象时,会以传入的对象为父类派生下来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function MenuScene:ctor()
      
     -- 1、背景的添加
     self.bg = display.newSprite( "#MenuSceneBg.png" , display.cx, display.cy)
     self:addChild(self.bg)
  
     -- 2、信息条的添加
     self.adBar = AdBar. new ()
     self:addChild(self.adBar)
  
     -- 3、“更过游戏”按钮
     self.moreGamesButton = BubbleButton. new ({
             image =  "#MenuSceneMoreGamesButton.png" ,
             sound = GAME_SFX.tapButton,
             prepare = function()
                 audio.playSound(GAME_SFX.tapButton)
                 self.moreGamesButton:setButtonEnabled( false )
             end,
             listener = function()
                 app:enterMoreGamesScene()
             end,
         })
         :align(display.CENTER, display.left + 150, display.bottom + 300)
         :addTo(self)
      
     -- 4、“开始”按钮
     self.startButton = BubbleButton. new ({
             image =  "#MenuSceneStartButton.png" ,
             sound = GAME_SFX.tapButton,
             prepare = function()
                 audio.playSound(GAME_SFX.tapButton)  -- 播放特效声
             self.startButton:setButtonEnabled( false )  -- 先关闭menu功能,这样防止在这个menu item还没做完动作又被玩家点上别的按钮上了
             end,
             listener = function()
                 app:enterChooseLevelScene()  -- 该方法在MyApp.lua中
             end,
         })
         :align(display.CENTER, display.right - 150, display.bottom + 300) -- 设置锚点,X坐标,Y坐标
         :addTo(self) -- 添加到该场景中
  
end

ctor() 相当于构造函数 或者说 是Cococs2d-x里的init(),一旦new,就会调用ctor() ,

在该ctor()中初始化的该场景的界面布置:

1、背景的添加;

2、信息条的添加;

3、“更过游戏”按钮;

4、“开始”按钮;


1、背景的添加

其实十分简单,创建一精灵,将其位置设置在场景的中心点(锚点已经默认为精灵的中心点了),然后将其添加进场景就OK了。

需要注意一点的就是在Quick中使用图片,如果使用的图片是以#开头的话表示是从SpriteFrameCache中读取,如果没有使用#开头的话表示是直接从文件读取。(还记得在MyApp.lua中我们已经加载了图片缓存了吗?)

附:display.width和display.height表示屏幕宽度

    display.cx和display.cy表示屏幕的x轴中间位置和y轴中间位置

    display.left和display.right表示屏幕的最左边和最右边(x轴坐标为0和display.width的点)

    display.top和display.bottom表示屏幕的顶部和底部(y轴坐标为0和display.height的点)

    display.CENTER、display.LEFT_TOP、display.CENTER_TOP等分别表示node的锚点位置。


2、信息条的添加

打开src\app\views\AdBar.lua  观察下面代码

1
2
3
4
5
6
7
8
9
10
11
12
-- 信息条(界面最下方那条)
-- 一个进行了二次封装的精灵
-- align:锚点,X坐标,Y坐标
local AdBar = {}
  
function AdBar. new ()
     local sprite = display.newSprite( "#AdBar.png" )
     sprite:align(display.BOTTOM_CENTER, display.cx, display.bottom)
     return  sprite
end
  
return  AdBar

在AdBar中,一旦调用了new() 方法,将自动创建并返回一个设置好精灵帧、锚点、X坐标和Y坐标的精灵。

(二次封装的目的除了外部的方便调用外,最大的动能就是代码的复用!所以可以预测到后面的编写中必定将会继续用到这个信息条)


3、4、两个按钮的添加,都是使用自定义的 汽包按钮类 BubbleButton (我直译,错了请见谅,六级还没过T.T) ,将这个自定义按钮这个篇是讲不完了,但大家先观察好代码,记住创建气泡按钮传入的参数是个表table ,表里的元素有四个:
{ imgae = XXX, sound = XXX, prepare = XXX, listener = XXX }

  • image是一张图片,

  • sound是一个音频文件,

  • prepare和listener都是一个方法。

使用代码(MeunScene.lua中的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 3、“更过游戏”按钮
     self.moreGamesButton = BubbleButton. new ({
             image =  "#MenuSceneMoreGamesButton.png" ,
             sound = GAME_SFX.tapButton,
             prepare = function()
                 audio.playSound(GAME_SFX.tapButton)
                 self.startButton:setButtonEnabled( false )
             end,
             listener = function()
                 app:enterMoreGamesScene()
             end,
         })
         :align(display.CENTER, display.left + 150, display.bottom + 300)
         :addTo(self)

现在我们来正式了解下这个自定义的按钮。

打开src\app\views\BubbleButton.lua, 看看代码,这个就不贴代码,贴的太多就磨灭大家的看帖动力了。还贴图更有动力,大家也用play3多看看效果,感觉更爽

56_371891_4498f3cf8a79e00.png

粗略一看之后是不是觉得代码很多很乱很烦啊?其中仔细看看就会发现这个脚本里面只有一个方法,就是 new(param)

而其作用很明显:对参入的参数进行捕获,创建一个PushButton, 并封装进动作效果

整个方法内代码很多,头一次看会很乱,我先隐藏一部分代码(为了方便理解,我改动了一点点顺序,但对程序没有丝毫影响)

大家就会发现该方法其实只干了4件事情,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function BubbleButton. new (params)//5.3 命名参数
     -- 1、创建PushButton,只设置了normal情况下图片(既是传入参数中的image的值)
     local button =  cc.ui.UIPushButton. new ({normal = params.image}) 
     
     -- 2、将传入的回调函数先用listener变量保存起来,    
     local listener = params.listener
      
     -- 3、重新定义了传入参数中 params.listener的执行内容
     params.listener = function(tag)    
      
     -- 4、设置按钮点击响应事件
     button:onButtonClicked(function(tag)
      
     -- 返回该按钮
     return  button
end

1、创建按钮:没什么好讲的,大家这么聪明肯定都懂得。但是还是建议大家多看看Quick的框架源码,里面的有中文注释,很好理解,对学习Quick帮助极大,路径就在Quick-Cocos2d-x v3.2-RC0所在目录的quick-cocos2d-x-3.2rc0\quick\framework 里。

2、捕获传入参数中的listener元素:由本地变量listener保存起来(我好讨厌名字一样啊,有时老不小心就看错!)。为什么要找个本地变量来存储呢?等下你就知道了

3、重新定义传入参数中的元素listener:有人也许会问:要重新定义,原来传入的有什么意思呢? 往上看看第2点,现在知道为什么要本地变量listener保存了吧。   现在我们把第3点的代码展开,看看里面做些什么事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
--  3、重新定义了参数表中 params.listener的执行内容
     params.listener = function(tag)        
         -- (1)如果参数中 prepare的值不为空,则先执行params.prepare()
         if  params.prepare then
             params.prepare()
         end
//

if conditions then

    then-part

end;

//
  
         -- (2)zoom1和zoom2是两个效果函数,主要是move和scale两个动作,
         local function zoom1(offset,  time , onComplete)
             local x, y = button:getPosition()
             local size = button:getContentSize()
             size.width = 200
             size.height = 200
  
             local scaleX = button:getScaleX() * (size.width + offset) / size.width
             local scaleY = button:getScaleY() * (size.height - offset) / size.height
  
             transition.moveTo(button, {y = y - offset,  time  time })
             transition.scaleTo(button, {
                 scaleX     = scaleX,
                 scaleY     = scaleY,
                 time        time ,
                 onComplete = onComplete,
             })
         end
  
         local function zoom2(offset,  time , onComplete)
             local x, y = button:getPosition()
             local size = button:getContentSize()
             size.width = 200
             size.height = 200
  
             transition.moveTo(button, {y = y + offset,  time  time  / 2})
             transition.scaleTo(button, {
                 scaleX     = 1.0,
                 scaleY     = 1.0,
                 time        time ,
                 onComplete = onComplete,
             })
         end
  
     --  (3)动作效果方法的组合使用
         --  设置按钮的点击功能无效,防止在还没做完动作又被玩家点上该按钮
         button:setButtonEnabled( false )
  
         -- 执行动作效果,    一系列的缩放效果之后,再开启按钮功能,最后执行之前的回调函数,这样一个动画按钮就新鲜出炉了。
         zoom1(40, 0.08, function()
             zoom2(40, 0.09, function()
                 zoom1(20, 0.10, function()
                     zoom2(20, 0.11, function()
                         button:setButtonEnabled( true )   --  动作效果结束后,开启按钮的点击功能
                         listener(tag)                                    --  执行原先的params.listener的方法
                     end)
                 end)
             end)
         end)
                  
     end


重新定义了的param,listener 还是一个方法,该方法里的代码可以分成三部分:

(1)执行param.prepare这个元素所指向的方法;

1
2
3
if  params.prepare then
                    params.prepare()
            end

该部分用了if判断语句,如果params.prepare 为空则不会执行,所以根据实际情况,传入参数中的元素prepare并不是一个必填项。


(2)定义了两个本地的效果方法zoom1和zoom2;

zoom1和zoom2是两个效果函数,主要是move和scale两个动作,这个两个内容不难,我就不讲解了,大家看看理解下就好


(3)动作效果方法的组合使用;

使用zoom1和zoom2进行组合使用,达到气泡效果的动作。难度不大但需要注意几点:

A、使用回调函数,使动作连续

B、执行动作效果前,先关闭该PushButton的按钮功能,防止在还没做完动作又被玩家点上该按钮:button:setButtonEnabled(false) , 效果结束了后在开启它的按钮功能

C、在动作效果结束时,还在要执行之前我们用本来变量listener保存下来的方法,就是这个句代码:listener(tag),因为动作效果是我们附加的视觉效果,listener的方法才我们真正效果,不要忘本了哦


4、设置按钮点击响应事件:代码展开如下

1
2
3
button:onButtonClicked(function(tag)
         params.listener(tag)
end)

是不是很简单,就是执行了我们刚才重新定义了的params.listener,params.listener已经把所有需要做的事都做了:执行params.prepare、动作效果、还有最初的params.listener所定义的方法(如今在本地变量listener里)。

现在再重新梳理一下,传入参数{ imgae = XXX, sound = XXX, prepare = XXX, listener = XXX }

  • image:是用于创建PushButton的图片,必须

  • sound:在BubbleButton.lua并没用到,但它其实是按钮被点击时的效果声音,等下再解释

  • prepare:是一个必定会被执行到的方法,但可以置空,大家就根据实际情况决定要不要写,应该写些什么。根据它的的名字,我理解这个元素是按钮被点击前需要做的事。

  • listener :同样是一个必定被执行的方法

再回到MenuScen.lua的ctor()的方法中,找到使用了BubbleButton的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--“开始”按钮
     self.startButton = BubbleButton. new ({
             image =  "#MenuSceneStartButton.png" ,
             sound = GAME_SFX.tapButton,
             prepare = function()
                 audio.playSound(GAME_SFX.tapButton)  -- 播放特效声
                 --self.startButton:setButtonEnabled( false )  
                 self.moreGamesButton:setButtonEnabled( false ) -- 先关闭功能,这样防止还没做完动作又被玩家点上别的按钮上了
             end,
             listener = function()
                 app:enterChooseLevelScene()  -- 该方法在MyApp.lua中
             end,
         })
         :align(display.CENTER, display.right - 150, display.bottom + 300) -- 设置锚点,X坐标,Y坐标
         :addTo(self) -- 添加到该场景中

注释应该很清楚了,最后我想提出一下自己的改动:

看上面的代码,是不是有一句与原代码不同?就是注释掉了self.startButton:setButtonEnabled(false),并添加了self.moreGamesButton:setButtonEnabled(false), 请让我解释一下,原先的self.startButton:setButtonEnabled(false) 这句代码的作用关闭startButton这个按钮的点击,防止这个按钮在还没做完动作又被玩家点上,但是看过BubbleButton.lua里面的代码后发现关闭按钮功能的代码已经内嵌在其中了,这样就发生了代码功能重复,所以我认为 self.startButton:setButtonEnabled(false) 这句代码是多余。

而添加了self.moreGamesButton:setButtonEnabled(false) 这句目的很简单,因为在MenuScene的场景一共就两个由BubbleButton 创建出来的按钮:startButton和moreGamesButton, 我希望在点击其中一个按钮时,另外的一个按钮的点击功能会被禁用,用户体验才更好(同时在另外一个按钮代码也应该做相应的修改)


以上只是我个人的理解,不知道对不对,大家多担当,多给建议

同时,对于这个二次封装的按钮,大家也可以看看这位大神讲解http://cn.cocos2d-x.org/tutorial/show?id=1350


二、进入MoreGamesScene场景

完全了解MenuScene这个场景后,我们随着startButton和moreGamesButton这两个按钮进入其他场景,继续学习吧。

首先,点击moreGamesButton,我们将进入MoreGamesScene场景,如下图:

1413012658121825.png

真是单调的界面啊,我们还是打开src\app\scenes\MoreGamesScene.iua来直接看代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- 导入AdBar.lua
local AdBar = import( "..views.AdBar" )
  
-- 创建一个名为MoreGamesScene的场景类
local MoreGamesScene =  class ( "MoreGamesScene" , function()
     return  display.newScene( "MoreGamesScene" )
end)
  
function MoreGamesScene:ctor()
     -- 1、背景
     self.bg = display.newSprite( "#MenuSceneBg.png" , display.cx, display.cy)
     self:addChild(self.bg)
      
     --2、信息条
     self.adBar = AdBar. new ()
     self:addChild(self.adBar)
  
     --3、后退按钮
     cc.ui.UIPushButton. new ( "#BackButton.png" )
         :align(display.CENTER, display.right - 100, display.bottom + 120)
          -- 添加点击的响应事件
         :onButtonClicked(function()
             app:enterMenuScene()  -- 回到MenuScene中
         end)
         :addTo(self)
end
  
return  MoreGamesScene

1、背景:创建一精灵,并添加到场景中;

2、信息条:在上一篇中已经大胆预测它会再出现了,果不其然啊,不到多少,大家自己看代码吧;

3、后退按钮:使用是Quick框架封装好的PushButton按钮创建,源码在quick-3.2rc0-win\quick-cocos2d-x-3.2rc0\quick\framework\cc\ui,大家要养成多查看框架代码的习惯,用法里面讲得很清楚;


这个场景太简单了,没什么好讲,我们还是默默的按后退键吧。

一、进入游戏场景PlayLevelScene

接着上篇的内容,回到MenuScene后,点击StartButton进入游戏关卡选择场景ChooseLevelScene。

1413183142396622.png

但是我没还打算讲解这个场景的内容,而且跳过它,先从游戏场景PlayLevelScene开始讲解。原因有二:

1、游戏关卡选择场景ChooseLevelScene使用的控件并不是单纯的Quick框架定义的控件,而且自己封装的(比起上篇的自定义按钮BubbleButton难度不是一个级别),我打算另外单独讲解;

2、另外一个原因就是提前进入进行游戏的场景PlayLevelScene了解该游戏的核心不是更让人提起精神吗?所以我想先讲PlayLevelScene


那么我们就随便选择一个等级的关卡进入游戏场景吧。

1413183183433872.png

我们以视觉分析该场景的组成元素:

  • 有好多的银币;

  • 每个硬币下面有一块半透明的方块;

  • 背景图;

  • 一个后退键;

  • 一信息条;

  • 还有一个“Level:6”的等级文本;

好,带着这些“视觉认知”,我们打开PlayLevelScene.lua,看看其中的ctor()函数是怎样定义界面的吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
local Levels = import( "..data.Levels" )
local Board = import( "..views.Board" )
local AdBar = import( "..views.AdBar" )
  
local PlayLevelScene =  class ( "PlayLevelScene" , function()
     return  display.newScene( "PlayLevelScene" )
end)
  
function PlayLevelScene:ctor(levelIndex)
     -- 1、背景
     local bg = display.newSprite( "#PlayLevelSceneBg.png" )
     -- make background sprite always align top
     bg:setPosition(display.cx, display.top - bg:getContentSize().height / 2)
     self:addChild(bg)
  
     -- 2、标题
     local title = display.newSprite( "#Title.png" , display.left + 150, display.top - 50)
     title:setScale(0.5)
     self:addChild(title)
  
     -- 3、信息条
     local adBar = AdBar. new ()
     self:addChild(adBar)
  
     -- 4、等级文本
     local label = cc.ui.UILabel. new ({
         UILabelType = 1,
         text  = string.format( "Level: %s" , tostring(levelIndex)),
         font  =  "UIFont.fnt" ,
         x     = display.left + 10,
         y     = display.bottom + 120,
         align = cc.ui.TEXT_ALIGN_LEFT,
     })
     self:addChild(label)
  
     -- 5、硬币板
     self.board = Board. new (Levels.get(levelIndex))
     self.board:addEventListener( "LEVEL_COMPLETED" , handler(self, self.onLevelCompleted))
     self:addChild(self.board)
  
  
     -- 6、后退按钮
     cc.ui.UIPushButton. new ({normal =  "#BackButton.png" , pressed =  "#BackButtonSelected.png" })
         :align(display.CENTER, display.right - 100, display.bottom + 120)
         :onButtonClicked(function()
             app:enterChooseLevelScene()
         end)
         :addTo(self)
End

哈,看样子猜中很多啊,出差错的地方就是那个“硬币板”,所以我认为所以的硬币与半透明方块都在内嵌在这个“硬币板”中。先不管它了,我们从头分析起:背景、标题、信息条;这三个没什么好讲得,前面都已经理解基本能独立看懂与使用,重点我们先讲讲等级文本和学学文本控件的用法。


了解UILabel的用法

根据cc.ui.UILabel.new,我打开quick-cocos2d-x-3.2rc0\quick\framework\cc\ui找到了UILabel.lua,打开它,我们就可以来查看源码了!我们找到创建UILabel.类的这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--[[--
quick UILabel控件
]]
  
local UILabel
UILabel =  class ( "UILabel" , function(options)
     if  not options then
         return
     end
  
     if  1 == options.UILabelType then
         return  UILabel.newBMFontLabel_(options)
     elseif not options.UILabelType or 2 == options.UILabelType then
         return  UILabel.newTTFLabel_(options)
     else
         printInfo( "UILabel unkonw UILabelType" )
     end
end)

因为UILabelType=1,根据上面代码,我们知道了接下来执行的应该是UILabel.newBMFontLabel_(options),接着我们在UILabel.lua找到了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
--[[--
  
用位图字体创建文本显示对象,并返回 LabelBMFont 对象。
  
BMFont 通常用于显示英文内容,因为英文字母加数字和常用符号也不多,生成的 BMFont 文件较小。如果是中文,应该用 TTFLabel。
  
可用参数:
  
-    text: 要显示的文本
-    font: 字体文件名
-    align: 文字的水平对齐方式(可选)
-    x, y: 坐标(可选)
  
~~~ lua
  
local label = UILabel:newBMFontLabel({
     text =  "Hello" ,
     font =  "UIFont.fnt" ,
})
  
~~~
  
@param table params 参数表格对象
  
@ return  LabelBMFont LabelBMFont对象
  
]]
function UILabel.newBMFontLabel_(params)
     return  display.newBMFontLabel(params)
end

找到了这段代码加注释后就完全明白UILabel的用法了,根本就不需我解释了啊!所以,希望大家以后看到类似不懂的情况,也要懂得这样找框架源码,里面的注释极其强大,对我们菜鸟的学习很有帮助的。


对于后退按钮,大家也可以用这种方法,学会UIPushButton的用法,这里不做过多讲解。


二、游戏进行场景的重要组件:Level.lua、Coin.lua

重新看会“硬币板”的那段代码,我知道知道接下要去打开src\app\views\Board.lua,在Board.lua这个脚本上出现了两个代码:

1
2
local Levels = import( "..data.Levels" )
local Coin   = import( "..views.Coin" )

明显的,我们先应该了解Levels.lua和Coin.lua后再来看Board.lua会更清晰易懂。


1、Level.lua

好,那我们从Level.lua入手:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
local Levels = {}
  
Levels.NODE_IS_WHITE  = 1        -- 金色面                                 正                                        
Levels.NODE_IS_BLACK  = 0        -- 白色面 。。。写BLACK是干嘛,色盲?    反
Levels.NODE_IS_EMPTY  =  "X"       -- 空,就是没有硬币
  
local levelsData = {}
  
levelsData[1] = {
     rows = 4,               -- 行数
     cols = 4,                -- 列类
     grid = {                  -- 网格,也就是硬币状态的布局
         {1, 1, 1, 1},
         {1, 1, 0, 1},
         {1, 0, 0, 0},
         {1, 1, 0, 1}
     }
}
--中间省略了99个关卡布局数据
  
function Levels.numLevels()
     -- 长度操作符#用于返回一个数组或者线性表的最后的一个索引值 即size
     return  #levelsData
end
  
function Levels.get(levelIndex)
     -- assert  (v [, message])
     --参数:
     --v:当表达式v为nil或 false 将触发错误,
     --message:发生错误时返回的信息,默认为 "assertion failed!"
     -- 确保levelIndex >= 1 and levelIndex <= #levelsData,如果不是,将输出错误信息 :levelsData.get() - invalid levelIndex %s, 
     assert (levelIndex >= 1 and levelIndex <= #levelsData, string.format( "levelsData.get() - invalid levelIndex %s" , tostring(levelIndex)))
     return  clone(levelsData[levelIndex])
end
  
return  Levels

Level.lua脚本相对简单,就是游戏关卡的硬币布局相关的数据,总共有100个levelsData,量真大!理解请看代码注释,应该不难懂。但想更大家统一下思路,反面后面思路混乱。

就是对硬币状态的规定:

  • 正面—金色面—1—Levels.NODE_IS_WHITE

  • 反面—银色面—0—Levels.NODE_IS_BLACK

  • 空—X—Levels.NODE_IS_EMPTY


2、Coin.lua

打开Coin.lua,先看前面一份的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local Levels = import( "..data.Levels" )
  
local Coin =  class ( "Coin" , function(nodeType)
     -- 先设定为1,正面
     local index = 1
     -- 判断 
     if  nodeType == Levels.NODE_IS_BLACK then
         index = 8  -- 8是反面
     end
      
     -- 依靠index选择创建的coin是用哪张图片资源
     local sprite = display.newSprite(string.format( "#Coind.png" , index))//翻转效果是由一组图片实现的
     -- 为该精灵添加一个属性,是否为正面
     sprite.isWhite = index == 1
     return  sprite
end)

思路是这样的:以传入参数决定决定index的值,再已index的值创建精灵,最后继续依靠index的值决定sprite.isWhite此属性。

从此可看出index的值很重要,起决定性作为,但它为什么是1或8呢?

好,现在是解密时间!这回打开res\AllSprites.plist,找到这几个:

1413183289987793.png

大家可以看到Coin0001到Coin0008是硬币由正面(金)到反面(银)的8张图片,所以上面代码才会以index=1代表正面,index=8代表反面。


接着我们把Coin.lua剩下需要解析的代码看完:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-- 创建并执行硬币翻转特效的函数
function Coin:flip(onComplete)
  
     -- 1、创建并执行翻转动画
     -- 创建8张精灵帧,1到8 是 正面到反面的翻转               
     -- 注意:第四个参数,当其为 false ,按正常引索创建,为 true 是,按递减引索的方式创建
     local frames = display.newFrames( "Coind.png" , 1, 8, not self.isWhite)
//newFrames以特定模式创建一个包含多个图像帧对象的数组。
     -- 以上面创建的8张精灵帧集 创建动画,第二个参数是每一桢动画之间的间隔时间
     local animation = display.newAnimation(frames, 0.3 / 8)
     self:playAnimationOnce(animation,  false , onComplete)  -- 播放动画
内部封装
@param CCNode target 显示对象
@param CCNode animation 动作对象
@param boolean removeWhenFinished 播放完成后删除显示对象
@param function onComplete 播放完成后要执行的函数
@param number delay 播放前等待的时间
@return table 动作表格对象
function transition.playAnimationOnce(target, animation, removeWhenFinished, onComplete, delay)
///

  
  
     -- 2、翻转后的一系列的缩放动作
     self:runAction(transition.sequence({
         -- 两个参数:第一个参数是时间(s),第二个参数是缩放倍数
         cc.ScaleTo:create(0.15, 1.5),
         cc.ScaleTo:create(0.1, 1.0),
         cc.CallFunc:create(function()
             local actions = {}
             local scale = 1.1
             local  time  = 0.04
             for  i = 1, 5  do //4.3 控制结构语句
                 -- 三个参数,第一个是时间,第二个是X方向的缩放倍数,第三个是Y方向的缩放倍数
// -- 长度操作符#用于返回一个数组或者线性表的最后的一个索引值 即size
                 actions[#actions + 1] = cc.ScaleTo:create( time , scale, 1.0)
                 actions[#actions + 1] = cc.ScaleTo:create( time , 1.0, scale)
                 scale = scale * 0.95
                 time  time  * 0.8
             end
             actions[#actions + 1] = cc.ScaleTo:create(0, 1.0, 1.0)
             self:runAction(transition.sequence(actions))
         end)
     }))
  
  /ccspawn?

     -- 3、翻转结束后,修改“是否为正面”这个属性
     self.isWhite = not self.isWhite
end

1、创建并执行翻转动画:先分清一点,动画与动作是不一样的哦!这样的动画是所刚才看过的Coin0001到Coin0008所创建的帧动画。首先,先创建动画要播放的精灵帧集合

1
localframes=display.newFrames( "Coind.png" ,1,8,notself.isWhite)

为了学习我们去找源码吧!里面肯定解析清楚用法了,还记得怎么找吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
--[[--
以特定模式创建一个包含多个图像帧对象的数组。
  
~~~ lua
  
-- 创建一个数组,包含 Walk0001.png 到 Walk0008.png 的 8 个图像帧对象
local frames = display.newFrames( "Walkd.png" , 1, 8)
  
-- 创建一个数组,包含 Walk0008.png 到 Walk0001.png 的 8 个图像帧对象
local frames = display.newFrames( "Walkd.png" , 1, 8,  true )
  
~~~
  
@param string pattern 模式字符串
@param integer begin 起始索引
@param integer length 长度
@param boolean isReversed 是否是递减索引
  
@ return  table 图像帧数组
  
]]
function display.newFrames(pattern, begin, length, isReversed)

简单明了,只要注意一点,就是第四个参数,我们是以sprite.isWhite该属性为基础,很代码更为灵活与智能:是正面的话就正序创建精灵帧集合(正面到反面),是反面的话则相反。精灵帧集合创建完成后,再以其为基础创建动画,再播放动画就OK了!不懂的地方记得查框架源码!!

2、翻转后的一系列的缩放动作:一系列的动作组合。

3、翻转结束后,修改“是否为正面”这个属性:切记这一步很重要,该属性是判断硬币当前的状态的一个接口,对于该游戏的核心玩法和动画的创建都起的决定的作用。


OK,本篇先讲到这里,在了解完Board.lua的组件,下篇我们在开好好谈论下Board.lua吧,谢谢。


一、承载一切的“硬币板” Board.lua

(1)ctor 

承接上篇,游戏核心内容与玩法都集中在了这块Board上,我们直接上其ctor分代码先:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
local Levels = import( "..data.Levels" )
local Coin   = import( "..views.Coin" )
  
local Board =  class ( "Board" , function()
     return  display.newNode()
end)
  
local NODE_PADDING   = 100     --半透明的正方形方块的边长 PADDING 内边距
local NODE_ZORDER    = 0       --半透明方块的Z层数
local COIN_ZORDER    = 1000    --银币的Z层数
  
function Board:ctor(levelData)
     cc.GameObject.extend(self):addComponent( "components.behavior.EventProtocol" ):exportMethods()
//加入事件协议
/
cc/GameObject.lua——————————
function GameObject.extend(target) 
//装饰函数 返回装饰过的target 。 extend延伸;扩大;推广 对参数target装饰 给他增加方法checkComponent等
    target.components_ = {}
    function target:
checkComponent(name)
        return self.components_[name] ~= nil
    end
    function target:
addComponent(name)
        local component = Registry.newObject(name)
        self.components_[name] = component
        component:bind_(self)
//结合;装订;捆绑
        return component
    end
function Registry.newObject(name, ...)
    local cls = Registry.classes_[name]
    if not cls then
        -- auto load
        pcall(function()
            cls = require(name)
            Registry.add(cls, name)
        end)
    end
    assert(cls ~= nil, string.format("Registry.newObject() - invalid class \"%s\"", tostring(name)))
    return cls.new(...)
end
    function target: removeComponent (name)
        local component = self.components_[name]
        if component then component:unbind_() end
        self.components_[name] = nil
    end
    function target:
getComponent (name)
        return self.components_[name]
    end
    return target
end
/

     --从指定的图像文件创建并返回一个批量渲染对象。
     --只要绘制的图像在指定的图像文件中,无论绘制多少图像只用了 1 次 OpenGL draw call
     --反正就是提高机器效率的一种方法,但之后的精灵添加方式有点改变
     self.batch = display.newBatchNode(GAME_TEXTURE_IMAGE_FILENAME)
//
function display.newBatchNode(image, capacity)
    return CCSpriteBatchNode:create(image, capacity or 100) //批渲染节点 会把纹理放入纹理缓存 从而实现同一纹理精灵只用了 1 次 OpenGL draw call
end
  • 创建一个CCSpriteBatchNode对象,通过传递一个包含所有spritebatch的名字作为参数,并把它加入到当前场景之中。
  • 接下来,你从batch中创建的任何sprite,你应该把它当作CCSpriteBatchNode的一个孩子加进去。只要sprite包含在batch中,那么就没问题,否则会出错。后面加的CCSprite的名字,要不跟 batchNode创建的时候是同一个图片名字,要不就是 CCSpriteFrameCache里面的大图的小图的名字,否则会有问题。其实就是同一个纹理贴图 CCTexture2D  ,比如下面的tabletop.png必须是 table.png的小图。
//
     self.batch:setPosition(display.cx, display.cy)
     self:addChild(self.batch)
  
     -- 捕获数据
     self.grid = clone(levelData.grid)
     self.rows = levelData.rows
     self.cols = levelData.cols
     -- 硬币的集合,之后创建在硬币板上的硬币都应该存入该集合中(实际上是个 表 数据)
     self.coins = {}
     -- 属性:正在进行翻转动画的数量,这个属性在下面方法将被用到
     self.flipAnimationCount = 0
    
  
     -- 板的X,Y坐标的起始值                math.floor 向下取整
     local offsetX = -math. floor (NODE_PADDING * self.cols / 2) - NODE_PADDING / 2
     local offsetY = -math. floor (NODE_PADDING * self.rows / 2) - NODE_PADDING / 2
     -- create board, place all coins 
     -- 创建板,并放置所有的硬币(包括半透明块)
     for  row = 1, self.rows  do
         local y = row * NODE_PADDING + offsetY
         for  col = 1, self.cols  do
             local x = col * NODE_PADDING + offsetX
             -- 每个硬币都有一块 半透明块
             local nodeSprite = display.newSprite( "#BoardNode.png" , x, y)
             -- 因为上面用了display.newBatchNode(GAME_TEXTURE_IMAGE_FILENAME)
             -- 所以添加精灵的方式有点不同了,把精灵添加到batch中,然后batch会一次性将所有精灵绘制
             -- 又因为精灵将加进batch中,而且上面已经把batch的Position设置为场景的中点了
             -- 现在相当于场景的中间坐标为(0.0)
             self.batch:addChild(nodeSprite, NODE_ZORDER)  -- 加入batch
  
             -- 银币是放置在半透明块之上的
             local node = self.grid[row][col]
             -- 如果node不是代表“空”,则创建硬币
             if  node ~= Levels.NODE_IS_EMPTY then
                 local coin = Coin. new (node)
                 coin:setPosition(x, y)
                 coin.row = row
                 coin.col = col
                 self.grid[row][col] = coin             -- 用新创建出来的硬币替换grid对应位置的上的值
                 self.coins[#self.coins + 1] = coin     -- 将硬币放入集合中
                 self.batch:addChild(coin, COIN_ZORDER) -- 加入batch
                 print( "(" ..x,y.. ")" )                 -- 注意:这句代码是我自己加的!
             end
         end
     end
  
     -- 设置监听器
     self:setNodeEventEnabled( true )
     self:setTouchEnabled( true )
     self:addNodeEventListener(cc.NODE_TOUCH_EVENT, function(event)
         return  self:onTouch(event.name, event.x, event.y)
     end)
      
end

注意点:

1、ctor里使用了批量渲染对象NodeBatch,使用它有一个好处就绘图提高效率,但在使用时需要注意,之后绘制的一切都应该添加到批量渲染对象中,然后批量渲染对象会一次将它内部的图像绘制到场景中。  同时,相对坐标有点改变,以批量渲染对象为中心(请结合上面的代码注释理解)

2、内置属性,ctor里增加了五个属性:

1
2
3
4
5
self.grid = clone(levelData.grid)
self.rows = levelData.rows
self.cols = levelData.cols
self.coins = {}
self.flipAnimationCount = 0


其中grid属性用法要格外留意,期初的作用是捕获传入参数里的grid,是像这样的:

1
2
3
4
5
6
grid = {
      {1, 0, 0, 1},
      {0, 1, 1, 0},
      {0, 1, 1, 0},
      {1, 0, 0, 1}
}

之后依靠捕获的到参数去创建出不同状态的硬币(正面或者反面)local node = self.grid[row][col],创建后,又将硬币去替换grid对应位置的值 self.grid[row][col] = coin,请展开想象力,它应该变成这样:

  56_371891_48881e2dc7698fd.png

3、布局的方式,因为批量渲染对象的缘故,场景的中心已经相当于坐标(0,0)了,解析太难,所以我上课时手工画了图:

1413424462286764.png

这幅图解决了很多我想要解释的东西,希望大家看得懂(小正方形里面的除了坐标外,那个数字是绘制的顺序)。其实一开始我也看不太懂,是依靠添加的 print("("..x,y..")") 这句代码去了解到各个方块的坐标,进而明白整个布局的。

1413424512877144.png

4、设置触摸监听器


(2)Board.lua 里的方法

1、onTouch 

既然设置的触摸监听器,必定有回调函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Board:onTouch(event, x, y)
     if  event ~=  "began"  or self.flipAnimationCount > 0 then  return  end//~= 同 !=
  
     local padding = NODE_PADDING / 2
     -- 遍历硬币集合里的所有硬币
     for  _, coin in ipairs(self.coins)  do
         -- 取得coin的x,y坐标,但还是相对batch而已的坐标值
         local cx, cy = coin:getPosition()
         -- 将其转化为正常场景的坐标值
         cx = cx + display.cx
         cy = cy + display.cy
         --判断点击位置是否在该硬币的范围内
         if  x >= cx - padding
             and x <= cx + padding
             and y >= cy - padding
             and y <= cy + padding then
             -- 如果是则执行翻转
             self:flipCoin(coin,  true )
             break
         end
     end
end

遍历硬币集合里面的所有硬币,并将硬币的坐标转化为相对场景的坐标,再判断点击的点是否在硬币的范围为,若是的话则执行翻转。


2、flipCoin 翻转硬币

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
-- 翻转硬币,被点击的硬币与它四个方向相邻的硬币都会翻转
function Board:flipCoin(coin, includeNeighbour)
     if  not coin or coin == Levels.NODE_IS_EMPTY then  return  end
     --属性:正在进行翻转动画的数量      +1
     self.flipAnimationCount = self.flipAnimationCount + 1
      
     -- 执行翻转动作(传入参数是一个函数)
     coin:flip(function()
         self.flipAnimationCount = self.flipAnimationCount - 1
         -- 重新设置硬币的Z层数,防止这个硬币之前是被点击过Z层数是+1的
         self.batch:reorderChild(coin, COIN_ZORDER)
         -- 属性:正在进行翻转动画的数量 为0 证明翻转都结束了
         if  self.flipAnimationCount == 0 then
             --每次翻转完就检测一次游戏是否结束
             self:checkLevelCompleted()
         end
     end)
      
     -- 如果includeNeighbour为 true 才会使四个方向的硬币翻转
     if  includeNeighbour then
         -- 播放特效声
         audio.playSound(GAME_SFX.flipCoin)
         -- 改变点击中的硬币的Z层数,向上加一层,是为四周执行放大效果的硬币将其遮掩
         self.batch:reorderChild(coin, COIN_ZORDER + 1)
         -- 延迟0.25s才执行
         self:performWithDelay(function()
             --这四个flipCoin就没有设置includeNeighbour这个参数了
             self:flipCoin(self:getCoin(coin.row - 1, coin.col))   --下
             self:flipCoin(self:getCoin(coin.row + 1, coin.col))   --上
             self:flipCoin(self:getCoin(coin.row, coin.col - 1))   --左
             self:flipCoin(self:getCoin(coin.row, coin.col + 1))   --右
         end, 0.25)
     end
end


3、getCoin 得到硬币

1
2
3
4
5
6
7
-- 得到指定行列的硬币
function Board:getCoin(row, col)
     -- 注意:在ctor中,已经把创建的Coin与在grid对应的值替换了,所以可以用这种方法得到
     if  self.grid[row] then
         return  self.grid[row][col]
     end
end


4、checkLevelCompleted 检测关卡完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 检测函数,检测该是否关卡通关,所有硬币coin的isWhite属性为 true 时通关
function Board:checkLevelCompleted()
     local count = 0
     -- 遍历所有硬币

//
第一,数值for循环:

for var=exp1,exp2,exp3 do

    loop-part

end

for将用exp3作为step从exp1(初始值)到exp2(终止值),执行loop-part。其中exp3可以省略,默认step=1


第二,范型for循环:

前面已经见过一个例子:

-- print all values of array 'a' 

for i,v in ipairs(a) do print(v) end //i 是下标名 v是数组元素值

范型for遍历迭代子函数返回的每一个值。

再看一个遍历表key的例子:

-- print all keys of table 't'

for k in pairs(t) do print(k) end //k是表值


/

     for  _, coin in ipairs(self.coins)  do   //_仅仅是下标名 无特殊意义
         if  coin.isWhite then count = count + 1 end
     end
     -- 当count的数值与硬币集合里的值相等时证明游戏完成
     if  count == #self.coins then
         -- completed
         self:setTouchEnabled( false )
         self:dispatchEvent({name =  "LEVEL_COMPLETED" })
//发出事件 quick独有消息处理中心
     end
end

这样,Board.lua 基本就了解完了。


二、Board.lua的使用

回到PlayLevelScene.lua,找到下面使用Board.lua的代码:

1
2
3
self.board = Board. new (Levels.get(levelIndex))
self.board:addEventListener( "LEVEL_COMPLETED" , handler(self, self.onLevelCompleted))
self:addChild(self.board)

使用很简单,但其中调用到一个方法我们还没讲到就是:onLevelCompleted

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 游戏结束响应函数
function PlayLevelScene:onLevelCompleted()
     -- 播放特效声
     audio.playSound(GAME_SFX.levelCompleted)
      
     -- 胜利横幅
     local dialog = display.newSprite( "#LevelCompletedDialogBg.png" )
     dialog:setPosition(display.cx, display.top + dialog:getContentSize().height / 2 + 40)
     self:addChild(dialog)
      
     -- 胜利横幅的动作
     transition.moveTo(dialog, { time  = 0.7, y = display.top - dialog:getContentSize().height / 2 - 40, easing =  "BOUNCEOUT" })
end

当关卡完成通关时,就为这个方法,一个下降的胜利横幅:

1413424702877534.png

下篇我们再继续该游戏的最后一部分:ChooseLevelScene。



来源网址:http://www.cocoachina.com/bbs/read.php?tid=233838

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值