一、最后的前言
依旧使用久方法:顺藤摸瓜,一点一点发现,打开ChooseLevelScene.lua吧
local AdBar = import("..views.AdBar") local LevelsList = import("..views.LevelsList") local ChooseLevelScene = class("ChooseLevelScene", function() return display.newScene("ChooseLevelScene") end) function ChooseLevelScene:ctor() -- 背景 local bg = display.newSprite("#OtherSceneBg.png") -- make background sprite always align top 用于对齐顶部 bg:setPosition(display.cx, display.top - bg:getContentSize().height / 2) self:addChild(bg) -- 标题 local title = display.newSprite("#Title.png", display.cx, display.top - 100) self:addChild(title) -- 信息条 local adBar = AdBar.new() self:addChild(adBar) -- create levels list 创建等级列表 local rect = cc.rect(display.left, display.bottom + 180, display.width, display.height - 280) self.levelsList = LevelsList.new(rect) self.levelsList:addEventListener("onTapLevelIcon", handler(self, self.onTapLevelIcon)) self:addChild(self.levelsList) -- 后退按钮 cc.ui.UIPushButton.new({normal = "#BackButton.png", pressed = "#BackButtonSelected.png"}) :align(display.CENTER, display.right - 100, display.bottom + 120) :onButtonClicked(function() app:enterMenuScene() end) :addTo(self) end function ChooseLevelScene:onTapLevelIcon(event) audio.playSound(GAME_SFX.tapButton) app:playLevel(event.levelIndex) end function ChooseLevelScene:onEnter() self.levelsList:setTouchEnabled(true) end return ChooseLevelScene ``` 不难,唯一需要继续深入的就是这段代码:-- create levels list 创建等级列表 local rect = cc.rect(display.left, display.bottom + 180, display.width, display.height - 280) self.levelsList = LevelsList.new(rect) self.levelsList:addEventListener("onTapLevelIcon", handler(self, self.onTapLevelIcon)) self:addChild(self.levelsList) ``` 其实,想要顺着代码往看下,我们将会把要这个游戏剩下的五个脚本关联起立: LevelsList.lua LevelsListCell.lua PageControl.lua ScrollView.lua ScrollViewCell.lua 只要明白了这五个脚本,就懂得了该游戏的关卡选择场景是如何建立的,进而整个游戏也就完整了解了一边。先让大家了解下我对这最后的五个脚本理解的关系: LevelsList 继承自→ PageControl 继承自→ ScrollView LevelsListCell 继承自→ ScrollViewCell LevelsList 在内部使用了 LevelsListCell 让我们先从简单的讲起,就是cell(单元),先明白,该游戏运用时,一个页面就是一个cell(单元),如下图: 包含了1到16有一个看不见的矩形,它就一个cell(单元),你向左滑动一下就会进入下一个cell(单元):17到32 二、cell(单元) 从ScrollViewCell.lua开始:local ScrollViewCell = class("ScrollViewCell", function(contentSize) local node = display.newNode() -- 该类基础只是一个节点 if contentSize then node:setContentSize(contentSize) end -- 根据传入参入确立该节点的ContentSize node:setNodeEventEnabled(true) --设置该节点可以 为特定事件设置处理函数 cc(node):addComponent("components.behavior.EventProtocol"):exportMethods() return node end) function ScrollViewCell:onTouch(event, x, y) end function ScrollViewCell:onTap(x, y) end -- 退出时调用 function ScrollViewCell:onExit() --移除所用的事件响应事件 self:removeAllEventListeners() end return ScrollViewCell ``` ScrollViewCell.lua并不复杂,因为它只是作为一个“骨架”存在,很多功能并没有实现,所以需要我们再去扩展,所以有了LevelsListCell.lua,一定有人觉得为什么不直接写一个LevelsListCell.lua就够了呢?反正我们就扩展一个类而已,干嘛要“骨架”呢? 这我回答不好,只能说这样编程遵循了良好的设计模式,对以后程序的改良有很大的好处。 回到正题,进入LevelsListCell.lua:local ScrollViewCell = import("..ui.ScrollViewCell") -- 继承自ScrollViewCell local LevelsListCell = class("LevelsListCell", ScrollViewCell) function LevelsListCell:ctor(size, beginLevelIndex, endLevelIndex, rows, cols) local rowHeight = math.floor((display.height - 340) / rows) -- 每行的高度 local colWidth = math.floor(display.width * 0.9 / cols) -- 每列的宽度 -- 使用了批量渲染对象,在Board.lua也用过 local batch = display.newBatchNode(GAME_TEXTURE_IMAGE_FILENAME) self:addChild(batch) self.pageIndex = pageIndex -- button集合 self.buttons = {} -- 第一个将要加入的button的X,Y坐标 local startX = (display.width - colWidth * (cols - 1)) / 2 local y = display.top - 220 -- 捕获传入参数beginLevelIndex local levelIndex = beginLevelIndex -- 添加button,每个cell(单元)是16个按钮哦 for row = 1, rows do local x = startX for column = 1, cols do -- button的一系列处理 local icon = display.newSprite("#LockedLevelIcon.png", x, y) batch:addChild(icon) icon.levelIndex = levelIndex self.buttons#self.buttons + 1] = icon -- 等级标签 local label = cc.ui.UILabel.new({ UILabelType = 1, text = tostring(levelIndex), font = "UIFont.fnt" }) :align(cc.ui.TEXT_ALIGN_CENTER, x, y - 4) self:addChild(label) -- 处理完一个button后,重置数据,为添加下一个button做准备 x = x + colWidth levelIndex = levelIndex + 1 if levelIndex > endLevelIndex then break end end y = y - rowHeight if levelIndex > endLevelIndex then break end end -- add highlight level icon self.highlightButton = display.newSprite("#HighlightLevelIcon.png") self.highlightButton:setVisible(false) self:addChild(self.highlightButton,100) end -- 触摸响应函数 function LevelsListCell:onTouch(event, x, y) if event == "began" then local button = self:checkButton(x, y) if button then self.highlightButton:setVisible(true) self.highlightButton:setPosition(button:getPosition()) end elseif event ~= "moved" then self.highlightButton:setVisible(false) end end -- 触摸敲中button的响应函数 function LevelsListCell:onTap(x, y) local button = self:checkButton(x, y) if button then self:dispatchEvent({name = "onTapLevelIcon", levelIndex = button.levelIndex}) end end -- 获取传入参数X,Y坐标所对应的button function LevelsListCell:checkButton(x, y) local pos = cc.p(x, y) -- 遍历所有的button for i = 1, #self.buttons do local button = self.buttons* -- 如果传入的点在该button的方位内,则返回该button if cc.rectContainsPoint(button:getBoundingBox(), pos) then return button end end return nil end return LevelsListCell ``` 扩展后的Cell,在ctor中对Cell的视觉界面进行了排版和布局,同时还有两个响应函数:onTouch(event, x, y)和onTap(x, y) 这里我要特别解释一句代码: self:dispatchEvent({name = "onTapLevelIcon", levelIndex = button.levelIndex}) 因为我研究了很久(因为百度不到解释,源码又看不太懂T.T), 意思就委派一个Event,其中name属性是必须,作为事件的名字,但要触发该事件时就需要用到,而其他属性可以任意。这句代码结束后就相当LevelsListCell 这个类有了一个Event,可以看做一个表event={{name = "onTapLevelIcon", levelIndex = button.levelIndex}} 三、滚动控件ScrollView.lualocal ScrollView = class("ScrollView", -- 创建一个控件基础的节点 function(rect) if not rect then rect = cc.rect(0, 0, 0, 0) end local node = display.newClippingRegionNode(rect) -- 剪裁区域的节点 node:setNodeEventEnabled(true) cc(node):addComponent("components.behavior.EventProtocol"):exportMethods() return node end) ScrollView.DIRECTION_VERTICAL = 1 -- 垂直方向 ScrollView.DIRECTION_HORIZONTAL = 2 -- 水平方向 -- 参数:区域,拖动方向 function ScrollView:ctor(rect, direction) assert(direction == ScrollView.DIRECTION_VERTICAL or direction == ScrollView.DIRECTION_HORIZONTAL, "ScrollView:ctor() - invalid direction") self.dragThreshold = 40 -- 拖动最小临界值 self.bouncThreshold = 140 -- 拖动到能翻页的最小临界值 self.defaultAnimateTime = 0.4 -- 默认动画时间 self.defaultAnimateEasing = "backOut" -- 默认的ease self.direction = direction -- 滚动方向 self.touchRect = rect -- 触摸矩形 self.offsetX = 0 -- X轴偏移量 self.offsetY = 0 -- Y轴偏移量 self.cells = {} -- 单元集合 self.currentIndex = 0 -- 当前索引(对应self.cells) -- 为self的节点事件cc.NODE_EVENT 设置处理函数(返回一个id表示注册成功) self:addNodeEventListener(cc.NODE_EVENT, function(event) if event.name == "enter" then self:onEnter() elseif event.name == "exit" then self:onExit() end end) -- create container layer 创建一个容器层 self.view = display.newLayer() self:addChild(self.view) -- 为self.view的触摸事件cc.NODE_TOUCH_EVENT 设置处理函数 self.view:addNodeEventListener(cc.NODE_TOUCH_EVENT, function(event) return self:onTouch(event.name, event.x, event.y) end) end ---- 一些简单的对外接口函数 -- 得到当前索引所指向的单元 function ScrollView:getCurrentCell() if self.currentIndex > 0 then return self.cells else return nil end end -- 得到当前索引 function ScrollView:getCurrentIndex() return self.currentIndex end -- 设置当前索引 function ScrollView:setCurrentIndex(index) self:scrollToCell(index) end -- 添加单元 function ScrollView:addCell(cell) self.view:addChild(cell) self.cells#self.cells + 1] = cell self:reorderAllCells() self:dispatchEvent({name = "addCell", count = #self.cells}) end -- 根据索引插入一个单元 function ScrollView:insertCellAtIndex(cell, index) self.view:addChild(cell) table.insert(self.cells, index, cell) self:reorderAllCells() self:dispatchEvent({name = "insertCellAtIndex", index = index, count = #self.cells}) end -- 根据索引移除一个单元 function ScrollView:removeCellAtIndex(index) local cell = self.cells cell:removeSelf() table.remove(self.cells, index) self:reorderAllCells() self:dispatchEvent({name = "removeCellAtIndex", index = index, count = #self.cells}) end -- 得到容器层 function ScrollView:getView() return self.view end -- 得到可触摸矩形 function ScrollView:getTouchRect() return self.touchRect end -- 设置可触摸矩形 function ScrollView:setTouchRect(rect) self.touchRect = rect self:dispatchEvent({name = "setTouchRect", rect = rect}) end -- 得到剪裁后的矩形 function ScrollView:getClippingRect() return self:getClippingRegion() end -- 设置剪裁后的矩形 function ScrollView:setClippingRect(rect) self:setClippingRegion(rect) -- 委派事件 ,名字:setClippingRect self:dispatchEvent({name = "setClippingRect", rect = rect}) end -- 滚动cell(单元) 传入参数,index当前页码, 后面三个为跳转Cell时执行动作的参数 function ScrollView:scrollToCell(index, animated, time, easing) -- cell(单元)总数 local count = #self.cells if count < 1 then self.currentIndex = 0 return end -- 对index进行判断与处理 if index < 1 then index = 1 elseif index > count then index = count end self.currentIndex = index -- 根据滚动方向,设置偏移量 local offset = 0 for i = 2, index do local cell = self.cells* local size = cell:getContentSize() if self.direction == ScrollView.DIRECTION_HORIZONTAL then offset = offset - size.width else offset = offset + size.height end end self:setContentOffset(offset, animated, time, easing) -- 委派事件 self:dispatchEvent({name = "scrollToCell", animated = animated, time = time, easing = easing}) end -- 检测容器层self.view是否允许触摸 function ScrollView:isTouchEnabled() return self.view:isTouchEnabled() end -- 设置容器层是否允许触摸响应(默认是false) function ScrollView:setTouchEnabled(enabled) self.view:setTouchEnabled(enabled) -- self:setTouchEnabled(enabled) self:dispatchEvent({name = "setTouchEnabled", enabled = enabled}) end ---- events 事件处理函数 -- Began function ScrollView:onTouchBegan(x, y) self.drag = { currentOffsetX = self.offsetX, currentOffsetY = self.offsetY, startX = x, startY = y, isTap = true, } local cell = self:getCurrentCell() cell:onTouch(event, x, y) return true end -- Moved function ScrollView:onTouchMoved(x, y) local cell = self:getCurrentCell() if self.direction == ScrollView.DIRECTION_HORIZONTAL then if self.drag.isTap and math.abs(x - self.drag.startX) >= self.dragThreshold then self.drag.isTap = false cell:onTouch("cancelled", x, y) end if not self.drag.isTap then self:setContentOffset(x - self.drag.startX + self.drag.currentOffsetX) else cell:onTouch(event, x, y) end else if self.drag.isTap and math.abs(y - self.drag.startY) >= self.dragThreshold then self.drag.isTap = false cell:onTouch("cancelled", x, y) end if not self.drag.isTap then self:setContentOffset(y - self.drag.startY + self.drag.currentOffsetY) else cell:onTouch(event, x, y) end end end -- Ended 里面调用了onTouchCancelled(x, y)与onTouchEndedWithTap(x, y) function ScrollView:onTouchEnded(x, y) if self.drag.isTap then self:onTouchEndedWithTap(x, y) else self:onTouchEndedWithoutTap(x, y) end self.drag = nil end -- Cancelled function ScrollView:onTouchCancelled(x, y) self.drag = nil end -- 触摸结束时,结束点敲中按钮 function ScrollView:onTouchEndedWithTap(x, y) local cell = self:getCurrentCell() cell:onTouch(event, x, y) cell:onTap(x, y) -- 该函数的启动将会触发许多函数!! end -- 触摸结束时,结束点没有敲中按钮 function ScrollView:onTouchEndedWithoutTap(x, y) error("ScrollView:onTouchEndedWithoutTap() - inherited class must override this method") end -- onTouch 根据事件调用 Began、Moved、Ended与Cancelled function ScrollView:onTouch(event, x, y) if self.currentIndex < 1 then return end if event == "began" then -- 判断起始的触摸点是否在可触摸矩形中,若不在则直接返回不做任何操作 if not cc.rectContainsPoint(self.touchRect, cc.p(x, y)) then return false end return self:onTouchBegan(x, y) elseif event == "moved" then self:onTouchMoved(x, y) elseif event == "ended" then self:onTouchEnded(x, y) else -- cancelled self:onTouchCancelled(x, y) end end ---- private methods 私有方法 -- 重新整理所有的单元 function ScrollView:reorderAllCells() -- 1、设置每个Cell(单元)的position,注意cell的锚点在(0,0) local count = #self.cells local x, y = 0, 0 local maxWidth, maxHeight = 0, 0 for i = 1, count do local cell = self.cells* cell:setPosition(x, y) -- 根据滚动方向确定下个cell的position if self.direction == ScrollView.DIRECTION_HORIZONTAL then local width = cell:getContentSize().width if width > maxWidth then maxWidth = width end x = x + width else local height = cell:getContentSize().height if height > maxHeight then maxHeight = height end y = y - height end end -- 2、重置数据 if count > 0 then if self.currentIndex < 1 then self.currentIndex = 1 elseif self.currentIndex > count then self.currentIndex = count end else self.currentIndex = 0 end -- 3、添加完所有Cell后,根据滚动方向,设置好容器层的大小 local size if self.direction == ScrollView.DIRECTION_HORIZONTAL then size = cc.size(x, maxHeight) else size = cc.size(maxWidth, math.abs(y)) end self.view:setContentSize(size) end -- 根据偏移量触发动作 function ScrollView:setContentOffset(offset, animated, time, easing) local ox, oy = self.offsetX, self.offsetY local x, y = ox, oy if self.direction == ScrollView.DIRECTION_HORIZONTAL then self.offsetX = offset x = offset local maxX = self.bouncThreshold local minX = -self.view:getContentSize().width - self.bouncThreshold + self.touchRect.width if x > maxX then x = maxX elseif x < minX then x = minX end else self.offsetY = offset y = offset local maxY = self.view:getContentSize().height + self.bouncThreshold - self.touchRect.height local minY = -self.bouncThreshold if y > maxY then y = maxY elseif y < minY then y = minY end end -- 根据传入的动作参数执行动作 if animated then transition.stopTarget(self.view) transition.moveTo(self.view, { x = x, y = y, time = time or self.defaultAnimateTime, easing = easing or self.defaultAnimateEasing, }) else -- 如果animated这项为空,就没有动作了,直接使用setPosition跳转到下一个Cell self.view:setPosition(x, y) end end -- onExit function ScrollView:onExit() -- 移除所有指定类型的事件处理函数 self:removeAllEventListeners() end return ScrollView ``` 该脚本代码很多,可能比较麻烦看,但基本可以为四大部分: ctor,在里面设置了基本属性 对外接口函数,大部分为对属性进行操作的简单接口函数 events 事件处理函数 private methods 私有方法 剩下的需要大家看着注释慢慢理解了,我担心越说越混乱 四、PageControl** PageControl继承自ScrollView,并且改动很小,只是重写一个方法,就是:onTouchEndedWithoutTap(x, y),原本该方法只在未点中时输出信息,但重写后会根据触摸偏移量进行翻页动作local ScrollView = import(".ScrollView") -- 继承自ScrollView local PageControl = class("PageControl", ScrollView) -- 重新定义了触摸点未敲中button时的函数,会根据触摸的偏移量执行翻页的动作 function PageControl:onTouchEndedWithoutTap(x, y) local offsetX, offsetY = self.offsetX, self.offsetY local index = 0 local count = #self.cells if self.direction == ScrollView.DIRECTION_HORIZONTAL then offsetX = -offsetX local x = 0 local i = 1 while i <= count do local cell = self.cells* local size = cell:getContentSize() if offsetX < x + size.width / 2 then index = i break end x = x + size.width i = i + 1 end if i > count then index = count end else local y = 0 local i = 1 while i <= count do local cell = self.cells* local size = cell:getContentSize() if offsetY < y + size.height / 2 then index = i break end y = y + size.height i = i + 1 end if i > count then index = count end end self:scrollToCell(index, true) end return PageControl ``` 五、最终的成品LevelsList** LevelsList继承自PageControl ,重写了两个方法:ctor和scrollToCell,与其说是重写不如说是为他们增加了功能,两个方法都首先执行了父类的方法,在ctor增加了按钮的布局添加与指示灯,在scrollToCell中增加了指示灯的动作效果。 这就是指示灯: 同时,扩增了一个方法onTapLevelIcon,作用是委派事件。local LevelsListCell = import(".LevelsListCell") local Levels = import("..data.Levels") local PageControl = import("..ui.PageControl") -- 继承自PageControl local LevelsList = class("LevelsList", PageControl) LevelsList.INDICATOR_MARGIN = 46 -- 指示灯的间隔 function LevelsList:ctor(rect) --先执行方法定义好的方法 LevelsList.super.ctor(self, rect, PageControl.DIRECTION_HORIZONTAL) -- 1、add cells -- 默认是4行4列,横的是行,竖的是列 local rows, cols = 4, 4 -- 如果分辨率的高超过1000,而变成5行 if display.height > 1000 then rows = rows + 1 end -- 总页数 :7页,也就是7个cell local numPages = math.ceil(Levels.numLevels() / (rows * cols)) local levelIndex = 1 -- 使用for循环添加7个cell for pageIndex = 1, numPages do -- 每个cell最后一个关卡的等级 local endLevelIndex = levelIndex + (rows * cols) - 1 -- 如果cell的最后一个关卡等级大于Levels.lua里设定好的100个等级话的,那么endLevelIndex就为100,也就是第7个cell if endLevelIndex > Levels.numLevels() then endLevelIndex = Levels.numLevels() end -- 创建cell local cell = LevelsListCell.new(cc.size(display.width, rect.height), levelIndex, endLevelIndex, rows, cols) cell:addEventListener("onTapLevelIcon", function(event) return self:onTapLevelIcon(event) end) self:addCell(cell) --重置数据,为下个循环添加cell做准备 levelIndex = endLevelIndex + 1 end -- 2、add indicators 添加指示灯 local x = (self:getClippingRect().width - LevelsList.INDICATOR_MARGIN * (numPages - 1)) / 2 local y = self:getClippingRect().y + 20 self.indicator_ = display.newSprite("#LevelListsCellSelected.png") self.indicator_:setPosition(x, y) self.indicator_.firstX_ = x for pageIndex = 1, numPages do local icon = display.newSprite("#LevelListsCellIndicator.png") icon:setPosition(x, y) self:addChild(icon) x = x + LevelsList.INDICATOR_MARGIN end self:addChild(self.indicator_) end -- 重写scrollToCell方法,添加指示灯的动作效果 function LevelsList:scrollToCell(index, animated, time) --1、先执行方法定义好的方法 LevelsList.super.scrollToCell(self, index, animated, time) --2、指示灯动作效果代码 transition.stopTarget(self.indicator_) local x = self.indicator_.firstX_ + (self:getCurrentIndex() - 1) * LevelsList.INDICATOR_MARGIN if animated then time = time or self.defaultAnimateTime transition.moveTo(self.indicator_, {x = x, time = time / 2}) else self.indicator_:setPositionX(x) end end -- 点中图标是委派一个事件 function LevelsList:onTapLevelIcon(event) self:dispatchEvent({name = "onTapLevelIcon", levelIndex = event.levelIndex}) end return LevelsList ``` 六、使用 回到最初的代码:local rect = cc.rect(display.left, display.bottom + 180, display.width, display.height - 280) self.levelsList = LevelsList.new(rect) self.levelsList:addEventListener("onTapLevelIcon", handler(self, self.onTapLevelIcon)) self:addChild(self.levelsList) ``` 使用很简单了,只要传入一个矩形参数就足够了,其他的东西在LevelsListCell和LevelsList里都已经扩展好了。 让我们看一下这句代码self.levelsList:addEventListener("onTapLevelIcon", handler(self, self.onTapLevelIcon))找到self.onTapLevelIcon:function ChooseLevelScene:onTapLevelIcon(event) audio.playSound(GAME_SFX.tapButton) app:playLevel(event.levelIndex) end ``` 这个方法就把我们之前第四篇所讲的一切联系上了,进入游戏进行场景了! 七、再讲讲dispatchEvent 先说清楚,这个地方完全了看着这个游戏代码自我理解,不太能讲得明白,大家可以参考这篇文章:http://www.cocoachina.com/bbs/read.php?tid=235255 还是继续看这句代码:self.levelsList:addEventListener("onTapLevelIcon", handler(self, self.onTapLevelIcon)) 问题来了,事件"onTapLevelIcon"哪里来的呢?回到levelsList.lua发现:-- 点中图标是委派一个事件 function LevelsList:onTapLevelIcon(event) self:dispatchEvent({name = "onTapLevelIcon", levelIndex = event.levelIndex}) end ``` 并且在ctor中发现了使用它的代码:cell:addEventListener("onTapLevelIcon", function(event) return self:onTapLevelIcon(event) end) ``` 意思就是LevelsList里的onTapLevelIcon(event)函数一定执行,就会触发名为"onTapLevelIcon"的Event,同时返回一个参数event={ name = "onTapLevelIcon",levelIndex = event.levelIndex}} 但在这里,我们又发现了Cell里也有一个"onTapLevelIcon"事件,好吧,乖乖回去LevelsListCell-- 触摸敲中button的响应函数 function LevelsListCell:onTap(x, y) local button = self:checkButton(x, y) if button then self:dispatchEvent({name = "onTapLevelIcon", levelIndex = button.levelIndex}) end end ``` 而这个onTap(x, y)是在何时被调用的呢?答案就在ScrollView:-- 触摸结束时,结束点敲中按钮 function ScrollView:onTouchEndedWithTap(x, y) local cell = self:getCurrentCell() cell:onTouch(event, x, y) cell:onTap(x, y) -- 该函数的启动将会触发许多函数!! end ``` 讲得这么乱,总结起来就当触摸结束时,结束点敲中按钮,将会触发事件,层层递进,最终进入游戏场景! 写完了写完了终于写完了,虽然最后这篇写得最烂,只能靠注释,大家见谅。 最后附上我写了一堆注释的源码:http://yun.baidu.com/share/link?shareid=190898915&uk=1812511185*