2014年8月12日
教程:构建关卡选择场景
今天的教程演示了如何为游戏构建关卡选择场景。关卡选择屏幕在游戏中很常见,这些游戏分为多个关卡,玩家可以从这些关卡恢复游戏或选择重玩以获得最高分。
设置
本教程模块建立在 Corona Composer API 之上。它假设我们有一个场景模块 game.lua
,当选择特定关卡时加载该模块,以及一个名为 menu.lua
的场景,如果玩家决定不玩关卡,则会显示该场景。
我们还需要一个可以在多个场景中访问的通用“数据”表。以下代码加载一个 mydata.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 |
local M = {} M.maxLevels = 50 M.settings = {} M.settings.currentLevel = 1 M.settings.unlockedLevels = 4 M.settings.soundOn = true M.settings.musicOn = true M.settings.levels = {} -- 这些行只是为了预先填充表。 -- 实际上,你的应用程序可能会在每个关卡解锁并且保存分数/星级时创建一个关卡条目。 -- 也许这发生在你的游戏关卡结束时,或者在游戏关卡之间的场景中。 M.settings.levels[1] = {} M.settings.levels[1].stars = 3 M.settings.levels[1].score = 3833 M.settings.levels[2] = {} M.settings.levels[2].stars = 2 M.settings.levels[2].score = 4394 M.settings.levels[3] = {} M.settings.levels[3].stars = 1 M.settings.levels[3].score = 8384 M.settings.levels[4] = {} M.settings.levels[4].stars = 0 M.settings.levels[4].score = 10294 -- 关卡数据成员 -- .stars -- 每个关卡获得星星 -- .score -- 关卡的分数 return M |
这看起来比实际情况复杂。下半部分只是为了本教程的目的而预先填充表 - 这将使我们能够看到结果,而无需围绕它构建一个功能齐全的游戏。关键的表成员在顶部
.maxLevels
— 游戏的关卡最大数量(包含在关卡选择屏幕中)。.settings
— 将保存各种玩家数据和其他数据的数据表。.settings.currentLevel
— 玩家当前正在玩的关卡。.settings.unlockedLevels
— 玩家达到的(完成的)最高关卡。.settings.soundOn
/.settings.musicOn
— 玩家音频偏好的布尔值。.settings.levels
— 跟踪玩家每个关卡进度的表。在本教程中,只有.stars
很重要,但我们也可以跟踪玩家每个关卡的分数。
可以使用 loadsave 模块 的 saveTable()
调用轻松保存整个表
1 |
loadsave.saveTable( myData.settings, "settings.json" ) |
场景设置
现在基本数据模块已配置完毕,让我们看一下核心场景
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
local composer = require( "composer" ) local scene = composer.newScene() local widget = require( "widget" ) -- 需要“全局”数据表 -- 这将包含相关数据,例如当前关卡、最大关卡数、获得的星星数等。 local myData = require( "mydata" ) -- 声明矢量星星的顶点(对于实际游戏,图像可能是更可取的)。 local starVertices = { 0,-8,1.763,-2.427,7.608,-2.472,2.853,0.927,4.702,6.472,0.0,3.0,-4.702,6.472,-2.853,0.927,-7.608,-2.472,-1.763,-2.427 } -- 按钮处理程序,用于取消关卡选择并返回菜单 local function handleCancelButtonEvent( event ) if ( "ended" == event.phase ) then composer.gotoScene( "menu", { effect="crossFade", time=333 } ) end end -- 按钮处理程序,用于进入选定的关卡 local function handleLevelSelect( event ) if ( "ended" == event.phase ) then -- “event.target”是按钮,“.id”是一个数字,表示要进入哪个关卡。 -- “game”场景将使用此设置来确定要加载哪个关卡。 -- 这也可以通过传递参数来完成。 myData.settings.currentLevel = event.target.id -- 清除游戏场景,以便我们有一个新的开始 composer.removeScene( "game", false ) -- 进入游戏场景 composer.gotoScene( "game", { effect="crossFade", time=333 } ) end end -- 声明 Composer 事件处理程序 -- 在场景创建时... function scene:create( event ) local sceneGroup = self.view -- 创建背景 local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight ) background:setFillColor( 1 ) background.x = display.contentCenterX background.y = display.contentCenterY sceneGroup:insert( background ) -- 使用 scrollView 来包含关卡按钮(为了支持多个完整屏幕)。 -- 因为它只会垂直滚动,所以锁定水平滚动。 local levelSelectGroup = widget.newScrollView({ width = 460, height = 260, scrollWidth = 460, scrollHeight = 800, horizontalScrollDisabled = true }) -- “xOffset”、“yOffset”和“cellCount”用于在网格中定位按钮。 local xOffset = 64 local yOffset = 24 local cellCount = 1 -- 定义用于保存按钮的数组 local buttons = {} -- 从“myData”表中读取“maxLevels”。循环遍历它们,并为每个生成一个按钮。 for i = 1, myData.maxLevels do -- 创建一个按钮 buttons[i] = widget.newButton({ label = tostring( i ), id = tostring( i ), onEvent = handleLevelSelect, emboss = false, shape="roundedRect", width = 48, height = 32, font = native.systemFontBold, fontSize = 18, labelColor = { default = { 1, 1, 1 }, over = { 0.5, 0.5, 0.5 } }, cornerRadius = 8, labelYOffset = -6, fillColor = { default={ 0, 0.5, 1, 1 }, over={ 0.5, 0.75, 1, 1 } }, strokeColor = { default={ 0, 0, 1, 1 }, over={ 0.333, 0.667, 1, 1 } }, strokeWidth = 2 }) -- 将按钮放置在网格中,并将其添加到滚动视图中 buttons[i].x = xOffset buttons[i].y = yOffset levelSelectGroup:insert( buttons[i] ) -- 检查玩家是否已完成此关卡。 -- '.unlockedLevels' 值跟踪最大解锁关卡。 -- 但是,首先要检查是否已设置此值。 -- 如果未设置(新用户),则此值应为 1。 -- 如果关卡已锁定,则禁用按钮并使其淡出。 if ( myData.settings.unlockedLevels == nil ) then myData.settings.unlockedLevels = 1 end if ( i <= myData.settings.unlockedLevels ) then buttons[i]:setEnabled( true ) buttons[i].alpha = 1.0 else buttons[i]:setEnabled( false ) buttons[i].alpha = 0.5 end -- 为每个关卡生成获得的星星,但仅当: -- a. 存在“levels”表 -- b. “levels”表内部有一个“stars”值 -- c. 星星的数量大于 0(无需绘制零颗星星)。 local star = {} if ( myData.settings.levels[i] and myData.settings.levels[i].stars and myData.settings.levels[i].stars > 0 ) then for j = 1, myData.settings.levels[i].stars do star[j] = display.newPolygon( 0, 0, starVertices ) star[j]:setFillColor( 1, 0.9, 0 ) star[j].strokeWidth = 1 star[j]:setStrokeColor( 1, 0.8, 0 ) star[j].x = buttons[i].x + (j * 16) - 32 star[j].y = buttons[i].y + 8 levelSelectGroup:insert( star[j] ) end end -- 计算下一个按钮的位置。 -- 本教程绘制 5 个横向排列的按钮。 -- 它还基于按钮的宽度和高度以及与左侧的初始偏移量进行间距。 xOffset = xOffset + 75 cellCount = cellCount + 1 if ( cellCount > 5 ) then cellCount = 1 xOffset = 64 yOffset = yOffset + 45 end end -- 将 scrollView 放入场景并居中。 sceneGroup:insert( levelSelectGroup ) levelSelectGroup.x = display.contentCenterX levelSelectGroup.y = display.contentCenterY -- 创建一个取消按钮,用于返回到菜单场景。 local doneButton = widget.newButton({ id = "button1", label = "取消", onEvent = handleCancelButtonEvent }) doneButton.x = display.contentCenterX doneButton.y = display.contentHeight - 20 sceneGroup:insert( doneButton ) end -- 在场景显示时... function scene:show( event ) local sceneGroup = self.view if ( event.phase == "did" ) then end end -- 在场景隐藏时... function scene:hide( event ) local sceneGroup = self.view if ( event.phase == "will" ) then end end -- 在场景销毁时... function scene:destroy( event ) local sceneGroup = self.view end -- Composer 场景监听器 scene:addEventListener( "create", scene ) scene:addEventListener( "show", scene ) scene:addEventListener( "hide", scene ) scene:addEventListener( "destroy", scene ) return scene |
深入研究
所有这些都基于标准的 Composer 场景。尽管代码有注释,但让我们检查每个部分及其功能
- 为了使本教程保持简单,没有所需的图形资源。相反,我们使用 display.newPolygon() API 生成矢量星形。此 API 需要一个顶点数组,因此我们将其定义在场景的主代码块中,以防需要在其他地方使用它。
- 接下来的两个函数处理按钮事件。由于我们的按钮使用
onEvent
处理程序,我们需要测试阶段以确保代码只执行一次。在每个处理程序函数中,我们简单地使用composer.gotoScene()
跳转到相应的场景。 - 在
handleLevelSelect()
函数内部,我们在myData.settings
表中设置currentLevel
值,以便可以从game.lua
场景轻松访问。我们还调用 composer.removeScene() 来确保每次游戏场景都全新加载。 - 其余的魔法发生在
scene:create()
事件处理程序中。首先,我们创建一个背景,将其填充设置为白色,并在屏幕上居中。然后我们创建一个 widget.newScrollView() 来容纳按钮,以防我们有超过一整屏的按钮。按照设计,我们可以在屏幕上容纳大约 20 个按钮,而无需滚动。 - 接下来,我们初始化一些变量,这些变量用于计算每个按钮在屏幕上的位置(在滚动视图内)。这些变量包括按钮的 x 和 y 坐标(
xOffset
和yOffset
)以及当前行中按钮的计数(cellCount
)。cellCount
变量从 1 开始,本教程将在每行放置 5 个按钮。 - 对于游戏中的最大关卡数(
myData.maxLevels
),我们执行一个循环,并为每个按钮生成一个 widget.newButton()。本教程使用基于矢量的方法生成按钮形状 - 在本例中为圆角矩形。 - 在循环中,我们还将按钮的文本标签设置为数组的索引,从而按顺序编号。我们使用相同的值来设置按钮的
id
值,该值在玩家选择关卡时使用。 - 下一段代码测试是否已经传入解锁关卡的数量。解锁的关卡将以全彩色显示,并启用按钮的触摸处理程序,而锁定的关卡将淡出并禁用。
- 如果我们想显示每个关卡获得的星星数,我们现在可以生成它们。在实际制作的游戏中,我们可能会使用图像而不是星形多边形。无论哪种方式,我们都会从保存的数据(
myData.settings.level.stars
)中读取获得的星星数,并为每个星星生成一个。每个星星都相对于按钮的 x 和 y 值定位。 - 接下来,我们递增
xOffset
以将下一个按钮进一步定位到右侧。我们还递增行中的按钮数,如果该值超过每行的按钮数,则我们将xOffset
重置为该行的第一个位置,递增cellCount
变量,并添加绘制下一行的像素数(yOffset
)。 - 循环结束后,我们将整个滚动视图插入到场景的视图组中,并将其居中放置在屏幕上。
结论
虽然本模块的某些部分可能看起来很复杂,但大多数过程都很简单。当然,对于实际制作的游戏,需要对变量和数据结构进行某些调整。同时,如果您想在本教程中尝试使用代码库,请在此处下载 here。
julien
发布于 15:21, 8 月 12 日你难道不需要在 scene:destroy 中删除背景和按钮吗?
这是否意味着需要在 create 之外声明它们?
Rob Miracle
发布于 17:49, 8 月 12 日Composer(以及以前仍然使用的 Storyboard)将自动删除已添加到场景视图组(即 self.view 或本地化的“sceneGroup”)的显示对象。如果场景被删除,那么这些对象将被释放。你只需要担心可能仍在运行的计时器、native.* 对象、转换、可能仍在播放并具有回调的音频和运行时事件侦听器。像这样的作为显示对象一部分的简单触摸和点击侦听器由对象本身处理。
如果你的代码需要,你可能需要在 create scene 之外访问这些项目,但对于本例,没有任何需要这样做的事情。例如,被触摸的按钮对象作为 event.target 传递给事件处理程序,因此我仍然可以从该函数访问单个按钮。
Rob
Mo
发布于 21:32, 8 月 12 日太棒的教程了!谢谢你 Rob。这正是我们所有人为游戏实现所需要的类型。我可以建议另一个对制作游戏的人有帮助的教程吗?除了这个关卡选择教程之外,我很想看到一个解释如何制作那些通过向左或向右滑动来选择项目的菜单的教程。它们通常用于选择增强道具或世界。通常它们是水平排列的多个框,你可以通过手指滑动向左/向右滚动。
不确定我是否表达清楚了 🙂
但是非常感谢你这个很棒的教程!
Mo.
Rob Miracle
发布于 16:55, 8 月 13 日好主意 Mo!我会看看我能想出什么。
Rob
Peter Dwyer
发布于 02:12, 8 月 13 日讽刺似乎是我生活中最近的一个奇怪主题。我实际上在过去几周写了其中一个,因为我想要某种选择关卡的方法,并且拒绝购买一些预制包来做本质上简单的任务。当然,如果你是一名游戏设计师和开发者,你很快就会设法将简单变成一场消耗战。从关卡组到自由漫游选择地图,如糖果粉碎和植物大战僵尸 2,添加了所有的功能。
>_<
Mo
发布于 23:37, 8 月 14 日@Rob。太酷了,谢谢!
Mo
Jeff Zivkovic
发布于 16:52, 8 月 17 日很棒的教程。
但我认为我错过了一些关于使用 composer 的非常基础的知识。我不知道创建会触发侦听器的预期场景的语法。我一直在寻找使用 composer 的示例,这样我就可以看到语法和用法,以实际创建场景并让这个关卡选择运行起来。
Jeff Zivkovic
发布于 16:53, 8 月 17 日有人可以指导我找到一个使用 composer 的完整应用程序示例吗?
非常感谢,
Jeff
Jeff Zivkovic
发布于 16:58, 8 月 17 日抱歉。没事了。我找到了 Corona 下载中附带的示例。
感觉很不好意思。
Jeff Zivkovic
发布于 06:07, 8 月 18 日好的。基本上运行起来了。
但我不知道为什么我看不到按钮的笔画和填充。我可以看到星星和单词取消。当点击时(也就是当它处于“over”模式时),关卡编号会短暂显示。但是按钮的圆角矩形是白色背景上的白色。有什么想法吗?
谢谢,
Jeff
Rob Miracle
发布于 15:33, 8 月 18 日请在论坛中提出这个问题。我们可能需要你发布代码,而论坛评论不是一个好的地方。
Rob
Lori
发布于 07:58, 10 月 10 日我能够设置按钮,但我在让它们转到正确的关卡时遇到了问题。我的游戏中每个关卡的游戏面板都是唯一的,所以我为每个关卡都有场景:level1.lua, level2.lua 等。我正在尝试修改按钮处理程序代码来执行此操作。下面是我认为需要修改的代码。我尝试了 if 语句,例如 if buttons.id == 1 then...我会将单词“game”更改为“level1”。我很感谢任何人的帮助
-- 按钮处理程序以转到选定的关卡
local function handleLevelSelect( event )
if ( “ended” == event.phase ) then
-- ‘event.target’ 是按钮,‘.id’ 是一个数字,指示要转到哪个关卡。
-- ‘game’ 场景将使用此设置来确定要加载哪个关卡。
-- 这也可以通过传递参数来完成。
myData.settings.currentLevel = event.target.id
-- 清理游戏场景,以便我们重新开始
composer.removeScene( “game”, false )
-- 转到游戏场景
composer.gotoScene( “game”, { effect=”crossFade”, time=333 } )
end
end
Rob Miracle
发布于 19:46, 10 月 10 日尝试
composer.gotoScene( “level” .. tostring(event.target.id), { effect=”crossFade”, time=333 } )
Sobh
发布于 16:03, 6 月 13 日嗨
谢谢教程
我正面临一个问题。我正在尝试使用图像将 fillColor 更改为
defaultFile = “lock.png”,
overFile = “lock.png”,
以便根据关卡是否通过来放置锁定或解锁图像
但无论我做什么,defaultFile 和 overFile 都不起作用。
我删除了 fillColor,但它仍然显示白色!
Sobh
发布于 04:38, 6 月 19 日好的,我找到了问题
对于遇到此问题的任何人
你可以删除 shape 行并创建一个带有锁定形状的矩形图像,并将其用作默认文件。
Guest
发布于 05:57, 6 月 5 日我下载了文件,当我在模拟器中打开它时,会出现错误消息
“错误:运行时错误
14:54:00.051 找不到模块 ‘utility’
14:54:00.051 没有字段 package.preload[‘utility’]”
你在教程中没有提到任何关于它的内容?
Rob Miracle
发布于 11:15, 6 月 5 日在 game.lua 中有一行:local utility = require(“utility”)
只需删除该行,一切就都准备好了。
Jonas
发布于 08:59, 8 月 27 日你好!感谢你这个很棒的教程!我编程游戏玩得很开心!🙂
我在我的游戏中做了这个,功能完全相同,我复制粘贴了代码,我唯一更改的是等于 “i” 的 “id” 变量,所以现在它是一个数字,而不是一个字符串。但是有些事情出错了:当我单击一个关卡按钮时,例如 3,它会将 id 存储在 myData.settings.currentLevel 中,它变为 3。但是当 game.lua 加载时,gameData.lua 中的所有变量都会重置为文件中的原始变量。所以 gameData.settings.currentLevel 再次变为 1。你知道我哪里做错了吗?谢谢!
Jonas
发布于 09:02, 8 月 27 日抱歉,我的评论中有错字,第二次我说的是 “gameData” 而不是 “myData”,我的意思是 “myData”。
Rob Miracle
发布于 11:08, 8 月 28 日这可能最好在论坛中提出的问题。首先,将其更改为 .i 而不是 .id 以使其成为一个数字是不必要的步骤。你可以只将 id 设置为数字。你是否在 game.lua 中读取保存的设置,并且可能没有在 levelselect.lua 中保存设置?这会导致这个问题。通常,你只需要在 main.lua 中加载设置,因为 myData 表将在整个应用程序中持续存在。但是,你应该在每次进行需要保留的更改时保存设置。