前提:本文笔者使用是的Quick-Cocos2d-x v3.2-RC0和Cocos Code IDE进行学习的。
首先,用 play3 新建一个工程(名字大家就任意起吧)
工程建立完成后,大家进去自己Quick-Cocos2d-x v3.2-RC0所在目录,然后进入...\quick\samples\coinflip 中,将当中的两个文件夹res和src复制下来。
(其中,res存放游戏资源,src存放lua代码)
然后进入刚才我们新建的工程中,将当中的 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,场景界面如下
打开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多看看效果,感觉更爽
粗略一看之后是不是觉得代码很多很乱很烦啊?其中仔细看看就会发现这个脚本里面只有一个方法,就是 new(param)
而其作用很明显:对参入的参数进行捕获,创建一个PushButton, 并封装进动作效果
整个方法内代码很多,头一次看会很乱,我先隐藏一部分代码(为了方便理解,我改动了一点点顺序,但对程序没有丝毫影响)
大家就会发现该方法其实只干了4件事情,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-- 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场景,如下图:
真是单调的界面啊,我们还是打开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。
但是我没还打算讲解这个场景的内容,而且跳过它,先从游戏场景PlayLevelScene开始讲解。原因有二:
1、游戏关卡选择场景ChooseLevelScene使用的控件并不是单纯的Quick框架定义的控件,而且自己封装的(比起上篇的自定义按钮BubbleButton难度不是一个级别),我打算另外单独讲解;
2、另外一个原因就是提前进入进行游戏的场景PlayLevelScene了解该游戏的核心不是更让人提起精神吗?所以我想先讲PlayLevelScene
那么我们就随便选择一个等级的关卡进入游戏场景吧。
我们以视觉分析该场景的组成元素:
-
有好多的银币;
-
每个硬币下面有一块半透明的方块;
-
背景图;
-
一个后退键;
-
一信息条;
-
还有一个“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,找到这几个:
大家可以看到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 显示对象
///
-- 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
-- 三个参数,第一个是时间,第二个是X方向的缩放倍数,第三个是Y方向的缩放倍数
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 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
//
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,请展开想象力,它应该变成这样:
3、布局的方式,因为批量渲染对象的缘故,场景的中心已经相当于坐标(0,0)了,解析太难,所以我上课时手工画了图:
这幅图解决了很多我想要解释的东西,希望大家看得懂(小正方形里面的除了坐标外,那个数字是绘制的顺序)。其实一开始我也看不太懂,是依靠添加的 print("("..x,y..")") 这句代码去了解到各个方块的坐标,进而明白整个布局的。
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
|
当关卡完成通关时,就为这个方法,一个下降的胜利横幅:
下篇我们再继续该游戏的最后一部分:ChooseLevelScene。