在上一章中,我们学习了场景管理的基础知识。在本章中,我们将转换原始游戏文件,main_original.lua
game.lua
场景。
正如您在上一章中所了解到的,在一个
Composer 要求您以略微不同的方式思考。正确使用它需要您考虑场景生命周期函数 — scene:create()
、scene:show()
、scene:hide()
和scene:destroy()
—
将此概念视为电影场景:如果导演正在过渡到一个场景 — 淡入、将摄像机平移到一个点等 — 场景中的演员通常不会开始表演,直到场景“准备好”并聚焦。同样的方法也适用于 Composer 场景。例如,我们已经添加了生成小行星并使其运动的命令,但在我们将要创建的 game.lua
场景中,这些命令只会在场景完全显示在屏幕上后才会运行。
让我们开始吧!第一个攻击点是
复制标准的scene-template.lua
将此**副本**重命名为 game.lua
并将其放在您的 StarExplorer
项目文件夹中。
在单独的编辑器窗口/选项卡中打开 main_original.lua
和 game.lua
。您将从 main_original.lua
复制几个代码块到 game.lua
,因此同时打开这两个 Lua 文件会很方便。
由于此游戏显然仍然会利用物理引擎,因此没有理由将该设置推迟到以后。将您的物理设置命令从 main_original.lua
复制并粘贴到game.lua
的 空间中,紧跟在场景初始化之后
local composer = require( "composer" ) local scene = composer.newScene() -- ----------------------------------------------------------------------------------- -- Code outside of the scene event functions below will only be executed ONCE unless -- the scene is removed entirely (not recycled) via "composer.removeScene()" -- ----------------------------------------------------------------------------------- local physics = require( "physics" ) physics.start() physics.setGravity( 0, 0 )
之前,我们在这些命令之后使用了 math.randomseed() 命令。但是,您可能还记得我们在修改后的 main.lua
文件中game.lua
。
接下来我们需要图像表单配置。让我们将其粘贴在物理命令下方
local physics = require( "physics" ) physics.start() physics.setGravity( 0, 0 ) -- Configure image sheet local sheetOptions = { frames = { { -- 1) asteroid 1 x = 0, y = 0, width = 102, height = 85 }, { -- 2) asteroid 2 x = 0, y = 85, width = 90, height = 83 }, { -- 3) asteroid 3 x = 0, y = 168, width = 100, height = 97 }, { -- 4) ship x = 0, y = 265, width = 98, height = 79 }, { -- 5) laser x = 98, y = 265, width = 14, height = 40 }, } } local objectSheet = graphics.newImageSheet( "gameObjects.png", sheetOptions )
在图像表单设置之后,粘贴来自 main_original.lua
的以下局部变量
local objectSheet = graphics.newImageSheet( "gameObjects.png", sheetOptions ) -- Initialize variables local lives = 3 local score = 0 local died = false local asteroidsTable = {} local ship local gameLoopTimer local livesText local scoreText
在我们最初的版本中,我们创建了三个显示组来对我们的游戏对象进行排序和分层:backGroup
、mainGroup
和 uiGroup
。我们将继续使用它们,但由于我们使用的是 Composer,因此需要进行一些小的修改。
在上一章中,您学习了如何将场景对象插入到场景的**视图**组 (sceneGroup
) 中。现在,一个需要理解的重要概念是显示组实际上可以插入到其他显示组中!因此,我们可以维护原始游戏中的三个显示组,**并将**它们放入场景的视图组中。
为了方便这一点,我们将推迟实际创建我们的三个组,直到我们创建场景。但是,我们现在仍然需要使用您之前学到的**前向声明**方法来定义变量。因此,不要将每个变量与 display.newGroup() 关联,暂时让它们未定义
local ship local gameLoopTimer local livesText local scoreText local backGroup local mainGroup local uiGroup
在我们最初的版本中,我们在初始化显示组后立即创建了背景、飞船、生命文本和分数文本。现在,因为我们只为显示组创建了局部引用,所以我们必须将这些操作推迟到稍后,在 scene:create()
函数内部。
当我们继续 melewati titik ini di `main_original.lua`, kita sampai pada fungsi lokal yang memberdayakan fungsionalitas inti game kita. Pada dasarnya, ini dapat disalin langsung ke file `game.lua` Anda, ditempel langsung di bawah variabel yang baru saja kita definisikan.
Pertama, salin fungsi `updateText()`
local backGroup local mainGroup local uiGroup local function updateText() livesText.text = "Lives: " .. lives scoreText.text = "Score: " .. score end
Ikuti ini dengan fungsi `createAsteroid()`
local function updateText() livesText.text = "Lives: " .. lives scoreText.text = "Score: " .. score end local function createAsteroid() local newAsteroid = display.newImageRect( mainGroup, objectSheet, 1, 102, 85 ) table.insert( asteroidsTable, newAsteroid ) physics.addBody( newAsteroid, "dynamic", { radius=40, bounce=0.8 } ) newAsteroid.myName = "asteroid" local whereFrom = math.random( 3 ) if ( whereFrom == 1 ) then -- From the left newAsteroid.x = -60 newAsteroid.y = math.random( 500 ) newAsteroid:setLinearVelocity( math.random( 40,120 ), math.random( 20,60 ) ) elseif ( whereFrom == 2 ) then -- From the top newAsteroid.x = math.random( display.contentWidth ) newAsteroid.y = -60 newAsteroid:setLinearVelocity( math.random( -40,40 ), math.random( 40,120 ) ) elseif ( whereFrom == 3 ) then -- From the right newAsteroid.x = display.contentWidth + 60 newAsteroid.y = math.random( 500 ) newAsteroid:setLinearVelocity( math.random( -120,-40 ), math.random( 20,60 ) ) end newAsteroid:applyTorque( math.random( -6,6 ) ) end
Fungsi berikutnya, `fireLaser()`, harus mengikuti
newAsteroid:applyTorque( math.random( -6,6 ) ) end local function fireLaser() local newLaser = display.newImageRect( mainGroup, objectSheet, 5, 14, 40 ) physics.addBody( newLaser, "dynamic", { isSensor=true } ) newLaser.isBullet = true newLaser.myName = "laser" newLaser.x = ship.x newLaser.y = ship.y newLaser:toBack() transition.to( newLaser, { y=-40, time=500, onComplete = function() display.remove( newLaser ) end } ) end
Sekarang amati baris tepat setelah fungsi `fireLaser()` di `main_original.lua`
ship:addEventListener( "tap", fireLaser )
Ini menciptakan fungsi event listener untuk `ship`, tetapi kita belum menciptakan `ship`! Jadi, kita harus menunda perintah ini sampai nanti, setelah pembuatan objek kapal yang sebenarnya.
Saat Anda menyalin/menempelkan beberapa fungsi berikutnya, lewati perintah yang segera mengikutinya. Secara khusus, **hilangkan baris berikut** saat Anda menyalin kode dari `main_original.lua` ke `game.lua`
ship:addEventListener( "tap", fireLaser )
ship:addEventListener( "touch", dragShip )
gameLoopTimer = timer.performWithDelay( 500, gameLoop, 0 )
Runtime:addEventListener( "collision", onCollision )
Pada dasarnya, sisa dari
local function dragShip( event ) local ship = event.target local phase = event.phase if ( "began" == phase ) then -- Set touch focus on the ship display.currentStage:setFocus( ship ) -- Store initial offset position ship.touchOffsetX = event.x - ship.x elseif ( "moved" == phase ) then -- Move the ship to the new touch position ship.x = event.x - ship.touchOffsetX elseif ( "ended" == phase or "cancelled" == phase ) then -- Release touch focus on the ship display.currentStage:setFocus( nil ) end return true -- Prevents touch propagation to underlying objects end local function gameLoop() -- Create new asteroid createAsteroid() -- Remove asteroids which have drifted off screen for i = #asteroidsTable, 1, -1 do local thisAsteroid = asteroidsTable[i] if ( thisAsteroid.x < -100 or thisAsteroid.x > display.contentWidth + 100 or thisAsteroid.y < -100 or thisAsteroid.y > display.contentHeight + 100 ) then display.remove( thisAsteroid ) table.remove( asteroidsTable, i ) end end end local function restoreShip() ship.isBodyActive = false ship.x = display.contentCenterX ship.y = display.contentHeight - 100 -- Fade in the ship transition.to( ship, { alpha=1, time=4000, onComplete = function() ship.isBodyActive = true died = false end } ) end local function onCollision( event ) if ( event.phase == "began" ) then local obj1 = event.object1 local obj2 = event.object2 if ( ( obj1.myName == "laser" and obj2.myName == "asteroid" ) or ( obj1.myName == "asteroid" and obj2.myName == "laser" ) ) then -- Remove both the laser and asteroid display.remove( obj1 ) display.remove( obj2 ) for i = #asteroidsTable, 1, -1 do if ( asteroidsTable[i] == obj1 or asteroidsTable[i] == obj2 ) then table.remove( asteroidsTable, i ) break end end -- Increase score score = score + 100 scoreText.text = "Score: " .. score elseif ( ( obj1.myName == "ship" and obj2.myName == "asteroid" ) or ( obj1.myName == "asteroid" and obj2.myName == "ship" ) ) then if ( died == false ) then died = true -- Update lives lives = lives - 1 livesText.text = "Lives: " .. lives if ( lives == 0 ) then display.remove( ship ) else ship.alpha = 0 timer.performWithDelay( 1000, restoreShip ) end end end end end -- ----------------------------------------------------------------------------------- -- Scene event functions -- -----------------------------------------------------------------------------------
Kerja bagus! Sekarang kita memiliki semua
Sekarang kita perlu benar-benar **membuat** adegan itu. Untuk adegan menu di bab sebelumnya, kita membuat latar belakang, judul, dan dua tombol dalam `scene:create()`. Untuk adegan ini, kita akan membuat latar belakang, kapal, dan dua objek teks. Selain itu, kita akan menjeda mesin fisika dan benar-benar membuat tiga grup tampilan yang diperlukan untuk melapisi objek game kita.
Mari kita mulai dengan fisika. Di dalam fungsi `scene:create()`, tambahkan perintah `physics.pause()` sebagai berikut
-- ----------------------------------------------------------------------------------- -- Scene event functions -- ----------------------------------------------------------------------------------- -- create() function scene:create( event ) local sceneGroup = self.view -- Code here runs when the scene is first created but has not yet appeared on screen physics.pause() -- Temporarily pause the physics engine end
Apa tujuan dari perintah ini pada titik ini dalam siklus hidup adegan? Ingatlah bahwa adegan permainan kita tidak benar-benar ada di layar pada saat ini dan, karena kita tidak ingin permainan dimulai dulu, kita akan segera menjeda mesin fisika. Ini memungkinkan kita untuk membuat objek, menetapkan badan fisik mereka, dan memposisikan mereka, tetapi mereka tidak akan terpengaruh secara fisik sampai kita
Selanjutnya kita perlu membuat tiga grup tampilan yang sebelumnya hanya kita definisikan sebagai referensi penerusan. Tambahkan baris yang disorot berikut ini
function scene:create( event ) local sceneGroup = self.view -- Code here runs when the scene is first created but has not yet appeared on screen physics.pause() -- Temporarily pause the physics engine -- Set up display groups backGroup = display.newGroup() -- Display group for the background image sceneGroup:insert( backGroup ) -- Insert into the scene's view group mainGroup = display.newGroup() -- Display group for the ship, asteroids, lasers, etc. sceneGroup:insert( mainGroup ) -- Insert into the scene's view group uiGroup = display.newGroup() -- Display group for UI objects like the score sceneGroup:insert( uiGroup ) -- Insert into the scene's view group end
Di sini, selain membuat grup, kita juga **menyisipkan** setiap grup ke dalam grup **tampilan** adegan Composer (`sceneGroup`) menggunakan `sceneGroup:insert()`. Ini adalah bagaimana kita menanamkan grup tampilan asli kita ke dalam adegan.
Seperti yang Anda ingat, sebagian besar Solar2D menampilkan API objek menerima variabel grup tampilan yang valid sebagai pintasan sebaris yang mudah untuk memasukkan objek ke dalam grup itu. Namun, display.newGroup() adalah salah satu pengecualian untuk pintasan ini — Anda tidak bisa begitu saja memberikan referensi grup sebaris untuk memasukkan grup tampilan baru ke dalam grup yang sudah ada. Sebagai gantinya, Anda harus menggunakan perintah `object:insert()`.
Dengan grup di tempatnya, mari kita buat latar belakangnya
-- Set up display groups backGroup = display.newGroup() -- Display group for the background image sceneGroup:insert( backGroup ) -- Insert into the scene's view group mainGroup = display.newGroup() -- Display group for the ship, asteroids, lasers, etc. sceneGroup:insert( mainGroup ) -- Insert into the scene's view group uiGroup = display.newGroup() -- Display group for UI objects like the score sceneGroup:insert( uiGroup ) -- Insert into the scene's view group -- Load the background local background = display.newImageRect( backGroup, "background.png", 800, 1400 ) background.x = display.contentCenterX background.y = display.contentCenterY end
Jika Anda memeriksa area kode yang dapat diakses adegan di dekat bagian atas `game.lua`, Anda akan melihat bahwa kita **tidak** menyertakan referensi penerusan ke `latar belakang` melaluilatar belakang lokal
Sekarang mari kita buat kapal dan kedua objek teks
-- Load the background local background = display.newImageRect( backGroup, "background.png", 800, 1400 ) background.x = display.contentCenterX background.y = display.contentCenterY ship = display.newImageRect( mainGroup, objectSheet, 4, 98, 79 ) ship.x = display.contentCenterX ship.y = display.contentHeight - 100 physics.addBody( ship, { radius=30, isSensor=true } ) ship.myName = "ship" -- Display lives and score livesText = display.newText( uiGroup, "Lives: " .. lives, 200, 80, native.systemFont, 36 ) scoreText = display.newText( uiGroup, "Score: " .. score, 400, 80, native.systemFont, 36 ) end
Tidak seperti latar belakang, kita membuat tiga objek ini menggunakan **referensi penerusan** variabel — `ship`, `livesText`, dan `scoreText` — yang kita sertakan sebelumnya di
Pada dasarnya, Anda dapat membuat referensi penerusan di
Perhatikan bahwa kita masih memasukkan objek ke dalam grup tampilan yang tepat seperti `backGroup`, `mainGroup`, dan `uiGroup`. Ini adalah prosedur yang benar karena semua grup tersebut dimasukkan ke dalam grup **tampilan** adegan dan sekarang menjadi anak dari grup induk tersebut.
Bergerak maju — ingat bagaimana kita menunda penambahan event listener `ship` "tap" dan ` "touch"` di
-- Display lives and score livesText = display.newText( uiGroup, "Lives: " .. lives, 200, 80, native.systemFont, 36 ) scoreText = display.newText( uiGroup, "Score: " .. score, 400, 80, native.systemFont, 36 ) ship:addEventListener( "tap", fireLaser ) ship:addEventListener( "touch", dragShip ) end
Itu dia! Objek adegan awal kita sekarang akan dibuat — meskipun di luar layar — tepat sebelum Composer melanjutkan ke fungsi berikutnya dalam siklus hidup adegan, `scene:show()`.
Di adegan menu, kita tidak perlu menggunakan `scene:show()` atau fungsi pendampingnya `scene:hide()`, tetapi di adegan permainan ini kita perlu. Pada titik ini, masih ada beberapa aspek penting yang belum kita salin dari `main_original.lua` — terutama, kita belum mengaktifkan deteksi tabrakan atau memulai loop permainan untuk menelurkan asteroid. Untungnya, `scene:show()` dapat digunakan untuk menggerakkan semuanya!
Meskipun kita tidak menggunakannya untuk adegan menu, perintah composer.gotoScene() memungkinkan Anda untuk menentukan **efek transisi** seperti memudar, meluncur masuk dari tepi layar,
Faktor penting untuk dipahami
Panggilan pertama terjadi ketika adegan siap untuk ditampilkan, pada dasarnya setelah setiap perintah dalam `scene:create()` telah dieksekusi. Dalam hal ini, `event.phase` adalah ` "will"`, yang secara efektif menunjukkan bahwa adegan "akan ditampilkan" dan efek transisi akan segera terjadi.
Panggilan kedua terjadi segera **setelah** adegan ditampilkan — pada dasarnya, ketika transisi adegan telah selesai. Dalam hal ini, `event.phase` adalah ` "did"`, yang berarti adegan "telah ditampilkan" dan efek transisi selesai.
Perhatikan bahwa template adegan sudah berisi pernyataan kondisional untuk memeriksa setiap fase `scene:show()`
-- show() function scene:show( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is still off screen (but is about to come on screen) elseif ( phase == "did" ) then -- Code here runs when the scene is entirely on screen end end
Sekarang setelah Anda memahami konsep ini, cukup tempel kode dari file `main_original.lua` Anda ke dalam klausa kondisional yang tepat. Untuk game ini, pada dasarnya kita ingin memulai permainan — menelurkan asteroid, mendeteksi tabrakan, dll. — setelah adegan sepenuhnya ditampilkan di layar
-- show() function scene:show( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is still off screen (but is about to come on screen) elseif ( phase == "did" ) then -- Code here runs when the scene is entirely on screen physics.start() Runtime:addEventListener( "collision", onCollision ) gameLoopTimer = timer.performWithDelay( 500, gameLoop, 0 ) end end
Pada dasarnya, perintah-perintah ini menyelesaikan hal berikut:
physics.start()
重新启动物理引擎(记住我们在 scene:create()
中暂停了它)。让我们检查一下代码的结果!保存修改后的 game.lua
文件,然后重新启动模拟器。正如预期的那样,您将看到菜单屏幕,但现在我们可以继续了。点击/单击**播放**按钮,假设您到目前为止所做的一切都正确,Composer 应该会进入 game.lua
场景,该场景的播放方式与我们最初版本的游戏相同。
目前游戏中存在一个重大缺陷。当玩家的生命值耗尽时,小行星将继续积聚,并且无法重新开始游戏。这意味着我们需要调整我们的 game.lua
代码,以便当玩家的生命值耗尽时 Composer 退出场景。
当我们打算退出游戏场景时,请记住 gameLoopTimer
计时器仍在运行,会生成小行星并移除scene:hide()
函数内停止。
与 scene:show()
类似,scene:hide()
将被调用**两次**,并且区别再次通过 event.phase
提供。基本上,scene:hide()
调用/阶段的工作方式如下
第一次调用发生在场景即将隐藏时event.phase
为 "will"
,有效地指示场景“将隐藏”并且过渡效果即将发生。
第二次调用发生在场景完全离开屏幕**之后**。在这种情况下,event.phase
为 "did"
,表示场景“已隐藏”并且过渡效果已完成。
对于此场景,在 scene:hide()
的 "will"
和 "did"
阶段条件中,我们将通过添加三个命令来“撤消”一些操作
-- hide() function scene:hide( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is on screen (but is about to go off screen) timer.cancel( gameLoopTimer ) elseif ( phase == "did" ) then -- Code here runs immediately after the scene goes entirely off screen Runtime:removeEventListener( "collision", onCollision ) physics.pause() end end
这些命令本质上与我们在 scene:show()
函数中所做的相反
gameLoopTimer
关联的计时器来停止游戏循环。physics.pause()
暂停物理引擎。请注意我们是如何有意使用 scene:hide()
的**两个**阶段的。第一个命令可以在场景开始隐藏之前发生phase == "will"
)physics.pause()
)。
我们的 scene:hide()
函数现已完成,但我们需要采取最后几个步骤来确保它实际被调用。从逻辑上讲,除非您有意导致场景隐藏,否则场景永远不会隐藏,并且当您告诉 Composer 转到其他场景(composer.gotoScene()
)时,就会发生这种情况。
在我们的游戏中,我们希望在玩家死亡且没有剩余生命时返回菜单 - 或者最终进入高分场景 - 此条件已在我们之前的代码中部分考虑到了,因此我们只需要在同一位置添加一个附加命令即可。
在 game.lua
中,找到您的 onCollision
函数,并在其末尾附近找到此条件块
-- Update lives lives = lives - 1 livesText.text = "Lives: " .. lives if ( lives == 0 ) then display.remove( ship ) else ship.alpha = 0 timer.performWithDelay( 1000, restoreShip ) end end end end end
现在,直接在display.remove( ship )
if ( lives == 0 ) then display.remove( ship ) timer.performWithDelay( 2000, endGame ) else ship.alpha = 0 timer.performWithDelay( 1000, restoreShip ) end end end end end
timer.performWithDelay() 命令并不是什么新鲜事 - 此实例只是在 2000 毫秒(2 秒)后调用函数 endGame()
。当然,我们实际上还没有编写 endGame()
函数,所以现在让我们编写它。直接在 onCollision()
函数**上方**添加此新函数
local function endGame() composer.gotoScene( "menu", { time=800, effect="crossFade" } ) end local function onCollision( event ) if ( event.phase == "began" ) then
目前,此函数只是通过调用 composer.gotoScene() 返回菜单。
这次我们包含了一个额外的可选表,其中包含场景过渡效果的参数。在此表中,我们指定了 800 毫秒的效果持续时间(time
)和 "crossFade"
的 effect
属性。这
希望玩家们想再次玩游戏!默认情况下,Composer 会将场景缓存在内存中,以便在重新访问场景时节省处理能力。因此,即使此时隐藏了您的游戏场景,它也基本保持您离开时的状态。如果您再次玩游戏,场景将重新出现,并开始生成新的小行星。不幸的是,存在一些问题
根据游戏的不同,清理场景以重新开始可能需要一些工作。在这个游戏中,我们需要“撤消”我们在 scene:create()
中所做的一些事情,并删除 asteroidsTable
表中包含的对旧小行星的引用。我们还需要在 scene:show()
中重置 score
、lives
和飞船的可见性。这些都不是特别复杂,但是有没有更简单的方法来重置场景呢?幸运的是,Composer 提供了一种方法
composer.removeScene( "game" )
本质上,此命令会移除并销毁 game.lua
场景,就好像它从未存在过一样。这样做,您将失去上面提到的缓存优势,但对于大多数场景来说,以编程方式单独重置每个方面是不值得的。
使用这种简化的方法,让我们修改我们的 scene:hide()
函数
-- hide() function scene:hide( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is on screen (but is about to go off screen) timer.cancel( gameLoopTimer ) elseif ( phase == "did" ) then -- Code here runs immediately after the scene goes entirely off screen Runtime:removeEventListener( "collision", onCollision ) physics.pause() composer.removeScene( "game" ) end end
此添加应该很清楚 - 我们只需调用composer.removeScene( "game" )
scene:hide()
的 "did"
阶段内,在场景完全过渡到屏幕外后有效地销毁场景。
是时候测试我们的更改了!保存您修改后的 `game.lua` 文件,然后重新启动模拟器。现在您应该能够重复玩游戏,并且每次都能获得干净的重置。
本章更加详细,需要将许多内容复制到 `game.lua` 中非常具体的位置。如果您的代码未按预期工作,请将其与本章源文件捆绑的 `game.lua` 文件进行比较。
在本章的前面,您了解了场景**过渡效果**。我们已经将一个应用于 `endGame()` 函数中的 `composer.gotoScene()` 命令,所以现在让我们在游戏场景出现时应用一个。
如果您还记得,游戏场景只能通过菜单场景中的**播放**按钮访问,因此我们将修改该文件
在您的 `StarExplorer` 文件夹中打开 `menu.lua` 文件。
找到 `gotoGame()` 函数,并将持续时间为 800 毫秒的 `“crossFade”` 效果添加到 composer.gotoScene() 命令中
local function gotoGame() composer.gotoScene( "game", { time=800, effect="crossFade" } ) end
local function gotoGame() composer.gotoScene( "game", { time=800, effect="crossFade" } ) end local function gotoHighScores() composer.gotoScene( "highscores", { time=800, effect="crossFade" } ) end
太好了!现在游戏场景 - 以及高分场景,一旦我们创建它 - 将以一个漂亮的
以下是我们在本章中涵盖的概念的摘要
命令/属性 | 描述 |
---|---|
physics.pause() | 暂停物理引擎。 |
object:insert() | 将对象插入到组中。 |
timer.cancel() | 取消使用 timer.performWithDelay() 启动的计时器操作。 |
object:removeEventListener() | 从对象中移除事件侦听器。 |
composer.removeScene() | 移除特定的 Composer 场景。 |
⟨ 第 4 章 — 创建场景 | 第 6 章 — 实现高分 ⟩