我们的项目开始成形,但它还不是一个游戏。让我们给它注入一些生命!
小行星的创建将由一个函数处理。此函数将作为我们**游戏循环**的一部分定期运行(调用),游戏循环是一个重复调用的函数,用于处理各种游戏功能。
像我们之前的函数一样,我们以local function
end
命令关闭函数
local function updateText() livesText.text = "Lives: " .. lives scoreText.text = "Score: " .. score end local function createAsteroid() end
在函数内部,我们首先创建一个名为 newAsteroid
的小行星的新实例,并像往常一样以 local
作为前缀。该对象本身就是一个图像,就像我们迄今为止创建的所有其他内容一样,取自我们之前加载的同一图像表 (objectSheet
)
local function createAsteroid() local newAsteroid = display.newImageRect( mainGroup, objectSheet, 1, 102, 85 ) end
由于在任何给定时间屏幕上都会有很多小行星,我们需要一种方法来跟踪它们。正如您在上一章中回忆的那样,我们初始化了几个变量,其中包括名为 asteroidsTable
的表。此表现在用作存储新小行星的地方。要将新小行星实例插入表中,我们可以使用table.insert()
命令。此命令只需要表的名称 (asteroidsTable
) 和要插入的对象/值,在本例中为我们刚刚创建的 newAsteroid
对象
local function createAsteroid() local newAsteroid = display.newImageRect( mainGroup, objectSheet, 1, 102, 85 ) table.insert( asteroidsTable, newAsteroid ) 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 } ) end
与上一章中的飞船对象一样,我们采用了一种快捷方式,即向所有小行星添加一个圆形物理体 (radius=40
),即使小行星图像并非完全是圆形。之后,您将学习如何向任何对象添加精确的
最后,让我们为小行星分配一个 myName
属性,值为 "asteroid"
。稍后,在检测碰撞时,知道此对象是小行星将简化操作。
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" end
现在我们在屏幕上有一个新的小行星,让我们设置它的原点。在我们的游戏中,小行星将来自屏幕的左侧、右侧或顶部 - 小行星从后面偷偷靠近是不公平的!
鉴于三个可能的原点,我们需要 Lua 生成一个介于 1
和 3
之间的随机整数。使用只有一个参数 3
的 math.random()
命令可以轻松完成此操作
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 ) end
执行此命令后,局部变量 whereFrom
的值将为 1
、2
或 3
。使用它,我们可以实现一个条件if
-then
local whereFrom = math.random( 3 ) if ( whereFrom == 1 ) then end end
请注意 Lua 语言的一个重要区别:当您**赋值**给变量时,您使用一个等号 (=
)。但是,如果您在条件语句中进行**比较**,则必须使用两个等号 (==
) 来表示您正在检查是否相等而不是赋值。
要告诉 Lua 您已完成条件结构,请使用关键字 end
。
比较周围的括号是可选的,但许多程序员使用它们是为了清晰起见或构建更复杂的
让我们使用第一个条件,whereFrom == 1
,
if ( whereFrom == 1 ) then -- From the left newAsteroid.x = -60 newAsteroid.y = math.random( 500 ) end end
由于这颗小行星将来自左侧,因此我们将其 x
属性设置为 -60
。这应该足以确保在小行星首次创建时,即使是小行星的一部分对玩家也不可见(它完全在屏幕外)。至于 y
属性,我们再次使用 math.random()
在 1
和 500
之间随机选择一个值,有效地使小行星出现在内容区域顶部和向下大约一半距离之间的某个位置——毕竟,我们不希望任何小行星来自无法射击它们的地方!
现在我们有了起点,我们需要告诉小行星它应该移动到哪里。这次我们将使用另一个物理命令:object:setLinearVelocity()
。此命令类似于我们在上一个项目中使用的 object:applyLinearImpulse() 命令,但它不是对对象施加突然的“推力”,而是简单地设置对象以稳定、一致的方向移动。
在上一行之后直接添加突出显示的行
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 ) ) end end
这可能看起来很复杂,但 object:setLinearVelocity()
只需要两个数字,分别表示**x** 和 **y** 方向上的速度。我们在这里使用的唯一技巧是 math.random()
来随机化这些值,以便每个小行星都以略微不同的方向移动。
请注意,我们这次使用**两个**参数调用 math.random()
,而之前我们只使用一个参数调用它。当使用一个参数调用时,该命令会在 1
和您指示的值之间随机生成一个整数。当使用两个参数调用时,该命令会在两个指定值之间随机生成一个整数,例如在上面的第一个实例中介于 40
和 120
之间。
如果我们决定现在调用/运行此函数,我们可能会看到一颗小行星从左侧缓慢地穿过屏幕,但也可能不会。为什么?因为我们还没有为屏幕的另外两侧添加条件情况!请记住,Lua 会在 1
和 3
之间随机选择一个数字,但目前我们只处理 1
的情况,因此我们当前的代码只有 ⅓ 的机会生成小行星。
以下两个条件将完成小行星可以起源的三个可能侧面。将多个条件添加到同一个if
-then
elseif
开头,而**不是**以 if
开头。观察这些添加
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 end
有了这些行,所有三个条件都得到了妥善处理。现在,当调用/运行此函数时,小行星将随机出现在三个指定区域之一中,并开始在屏幕上移动。
让我们用一个附加命令来结束 createAsteroid()
函数,以增加视觉趣味。为了使小行星在太空中移动时围绕其中心点缓慢旋转,我们可以应用随机量的扭矩(旋转力)。在if
-then
end
语句之后),添加以下突出显示的命令
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
没有射击的射击游戏是什么?
让我们的飞船发射激光类似于加载小行星,但这次我们将使用一种方便且强大的方法来移动它们,称为**过渡**。本质上,过渡是一种动画方法,它允许您在定义的时间量内更改对象的“状态”——这可以包括位置、比例、旋转、不透明度等等。您甚至可以在一行中执行多个过渡效果,并指定“缓动”算法以使过渡以
我们稍后会详细讨论过渡。首先,让我们添加将创建新激光对象的函数
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" end
这段代码的大部分内容应该很容易理解
我们在 mainGroup
显示组内创建一个新的激光对象,使用 5
作为帧号,因为激光是图像表配置中的第 5 帧。
我们将其作为传感器类型对象 (isSensor=true
) 添加到物理引擎中。
我们通过将其 isBullet
属性设置为 true
来指示激光应被视为“子弹”。这使得对象进行连续碰撞检测,而不是在世界时间步长进行周期性碰撞检测。因为我们的激光将非常快速地穿过屏幕,这将有助于确保它不会在没有注册碰撞的情况下“穿过”任何小行星。
最后,我们为对象分配一个 myName
属性 "laser"
,与飞船和小行星类似,这在检测碰撞时很有用。
新的激光对象现已加载,但我们尚未正确定位它。在这种情况下,我们不能使用静态位置,因为飞船最终将通过玩家的控制左右移动。幸运的是,通过将其 x
和 y
值设置为飞船的 x
和 y
值,可以很容易地将新激光定位在与飞船完全相同的位置
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 end
这将水平和垂直正确定位激光,但还有一个问题需要解决。因为此函数在飞船加载**之后**创建新的激光,并且两个对象都是 mainGroup
显示组的一部分,所以在分层方面,激光将在视觉上出现在飞船上方(前面)。显然这看起来很傻,所以让我们用以下突出显示的命令将它推到飞船后面
newLaser.x = ship.x newLaser.y = ship.y newLaser:toBack() end
object:toBack()
命令将对象发送到其自身显示组的最后面,但这不一定是整个父 舞台组的最后面。上述命令会将激光对象发送到其显示组 (mainGroup
) 的后面,但它仍然会出现在包含在 backGroup
中的背景图像的前面。
如前所述,我们将使用**过渡**将激光向上移动到屏幕上。大多数过渡都是使用 transition.to()
命令执行的。在其最简单的形式中,此命令接受一个对象引用(例如激光)和一个参数表,这些参数将在指定的时间内更改。
将以下突出显示的行添加到 fireLaser()
函数中
newLaser.x = ship.x newLaser.y = ship.y newLaser:toBack() transition.to( newLaser, { y=-40, time=500, } ) end
如您所见,第一个参数是要进行过渡的对象 (newLaser
)。对于第二个参数,我们包含一个表格,其中可以包含过渡的各种属性。这里,我们设置 y=-40
,表示激光的垂直目标位置,略微超出屏幕的上边缘。我们还设置了一个自定义的 time
参数为 500
。对于过渡,时间(持续时间)应始终以毫秒为单位指定 — 请记住 1 秒等于 1000 毫秒,因此此过渡将在 ½ 秒内完成。
太好了!新的激光现在将正确地出现在与飞船相同的位置(视觉上也在其后面),并在屏幕上向上移动。只剩下最后一件事要实现了,而且这非常重要:清理。在任何应用程序中,从游戏中移除不再需要的对象至关重要。如果不这样做,应用程序最终会变得非常缓慢,耗尽内存并崩溃 — 这对玩家来说可不是什么好体验!
清理的方法有很多种,具体取决于情况。对于激光,我们将使用一种非常方便的方法,称为 onComplete
回调。作为 transition.to()
和其他几个命令中的一个选项,它告诉 Solar2D 您希望在某些事情“完成”时调用一个函数。这非常适合移除已完成过渡的激光,因此让我们扩展 transition.to()
命令以包含一个 onComplete
回调。
newLaser.x = ship.x newLaser.y = ship.y newLaser:toBack() transition.to( newLaser, { y=-40, time=500, onComplete = function() display.remove( newLaser ) end } ) end
简单地说,此添加会在过渡完成时运行一个函数。在函数内部,我们唯一需要的命令是 display.remove( newLaser )
,它会从舞台上移除激光对象。除此之外,Lua 的
眼尖的人会注意到在onComplete =
onComplete
回调调用它,但在这种情况下使用匿名函数更容易。
我们快完成射击机制了 — 让我们通过为飞船分配一个 "tap"
事件监听器来结束,这样玩家就可以实际发射激光了。在 fireLaser()
函数之后(在其结束 end
语句之后),添加以下命令:
transition.to( newLaser, { y=-40, time=500, onComplete = function() display.remove( newLaser ) end } ) end ship:addEventListener( "tap", fireLaser )
让我们检查一下代码的结果。保存修改后的 main.lua
文件,重新启动模拟器,然后尝试点击/单击飞船以查看它是如何发射激光的。现在我们有所进展了!
在这个游戏中,除了发射激光外,玩家还可以触摸并沿着屏幕底部拖动飞船。为了处理这种类型的移动,我们需要一个函数来处理触摸/拖动事件。让我们以通常的方式创建此函数:
ship:addEventListener( "tap", fireLaser ) local function dragShip( event ) end
在函数内部,为了使事情更清晰一些,让我们将局部变量 ship
设置为等于 event.target
。在触摸/点击事件中,event.target
是被触摸/点击的对象,因此将此局部变量设置为对飞船对象的引用将为我们节省一些输入工作。
local function dragShip( event ) local ship = event.target end
触摸事件与点击事件不同,它根据用户触摸的状态有四个不同的阶段:
"began"
— 表示触摸已在对象上开始(屏幕上的初始触摸)。"moved"
— 表示触摸位置已在对象上移动。"ended"
— 表示触摸已在对象上结束(从屏幕上抬起触摸)。"cancelled"
— 表示系统取消了对触摸的跟踪(不要与 "ended"
混淆)。为了方便起见,让我们在本地设置触摸事件的阶段 (event.phase
):
local function dragShip( event ) local ship = event.target local phase = event.phase end
通过在本地设置阶段,我们可以使用if
-then
"began"
阶段将分派给我们的函数。在这种条件下,我们将触摸焦点设置在飞船上 — 本质上,这意味着飞船对象将在整个持续时间内“拥有”触摸事件。当焦点在飞船上时,游戏中的其他对象将不会检测到来自此特定触摸的事件。
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 ) end end
紧接着,让我们存储触摸相对于飞船的起始“偏移”位置。从概念上讲,如此处所示,触摸可以发生在对象边界内的各个位置。在我们的代码中,event.x - ship.x
event.x
) 与飞船x 位置 (ship.x
) 之间的水平偏移量。我们将其设置为飞船的一个属性 (touchOffsetX
),以便在下一阶段使用。
if ( "began" == phase ) then -- Set touch focus on the ship display.currentStage:setFocus( ship ) -- Store initial offset position ship.touchOffsetX = event.x - ship.x end end
对于 "moved"
阶段touchOffsetX
属性发挥作用的地方。如果我们忽略此偏移量并简单地将飞船的x 位置设置为 event.x
,则其中轴将跳到屏幕上的确切触摸点,并且如上图所示,飞船边界内的触摸点可能不完全位于中心。幸运的是,将偏移值考虑在内将产生平滑、一致的拖动效果。
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 end end
对于此游戏,飞船的移动将仅限于左右移动,因此我们只处理沿x 轴的变化。如果您创建一个游戏,其中一个对象可以在屏幕上四处拖动,那么您也应该模仿y 轴的这个偏移概念。例如,在 "began"
情况下,存储起始 y 偏移量:
-- Store initial offset position ship.touchOffsetX = event.x - ship.x ship.touchOffsetY = event.y - ship.y
然后,在 "moved"
阶段,设置对象的 y 位置:
-- Move the ship to the new touch position ship.x = event.x - ship.touchOffsetX ship.y = event.y - ship.touchOffsetY
最终的条件情况包括 "ended"
和 "cancelled"
阶段。"ended"
阶段表示用户释放了对对象的触摸,而 "cancelled"
阶段表示系统取消/终止了触摸事件。通常,这两个阶段都可以在同一个条件块中处理。对于这个游戏,我们只需释放飞船上的触摸焦点:
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 end
让我们用另一个命令完成 dragShip()
触摸监听器函数:
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
正如注释所示,这个简短但重要的命令告诉 Solar2D 触摸事件应该在此对象上“停止”并且不传播到底层对象。这在您可能有多个具有触摸事件检测的重叠对象的更复杂的应用程序中至关重要。添加return true
我们快完成移动机制了 — 让我们为飞船分配一个 "touch"
事件监听器,以便玩家可以左右触摸/拖动它。在 dragShip()
函数之后(在其结束 end
语句之后),添加以下命令:
return true -- Prevents touch propagation to underlying objects end ship:addEventListener( "touch", dragShip )
让我们检查一下代码的结果。保存修改后的 main.lua
文件,重新启动模拟器,并尝试触摸和拖动飞船。
许多游戏都包含某种类型的游戏循环来处理信息的更新、检查/更新游戏对象的状态等。虽然完全可以在 Solar2D 中构建一个游戏而无需实现游戏循环,但我们将在这里使用一个游戏循环来说明这个概念。
游戏循环函数通常很短 — 它本身不包含大量的代码,而是通常调用其他函数来处理特定的重复功能。我们的游戏循环将用于创建新的小行星并清理“死亡”的小行星。
首先,在您的 main.lua
文件中,在您已经编写的代码之后创建核心游戏循环函数:
ship:addEventListener( "touch", dragShip ) local function gameLoop() end
现在添加以下行以简单地调用我们在本章前面编写的 createAsteroid()
函数。实际上,每次游戏循环迭代时,它都会生成一个新的小行星。
local function gameLoop() -- Create new asteroid createAsteroid() end
对于此游戏,让我们通过循环遍历 asteroidsTable
表格来移除已经漂移到屏幕外的小行星。还记得我们何时将此表格声明为存储每个小行星的引用的地方吗?当时它可能看起来不相关,但现在它完全发挥作用了!
要循环遍历表格,我们将使用 Lua for
循环。本质上,for
循环允许我们使用索引变量从起始数字到结束数字向上或向下计数。
将以下突出显示的命令添加到您的 gameLoop()
函数中:
local function gameLoop() -- Create new asteroid createAsteroid() -- Remove asteroids which have drifted off screen for i = #asteroidsTable, 1, -1 do end end
请注意,Lua for
循环类似于函数和条件语句,以熟悉的 end
命令结束。
在这种情况下,我们需要从 asteroidsTable
表格中的小行星数量开始倒计时(递减),但有一个小问题:随着新小行星的创建和玩家摧毁其他小行星,数量会不断变化.幸运的是,Lua 提供了一种方便的方法来计算表格中元素的数量,只需在表格名称前面加上 #
即可完成:
#asteroidsTable
如您所见,我们在 for
循环中使用了这种方法,它采用指示的形式 — 基本上,Lua 使用索引 i
,从 #asteroidsTable
开始,1
停止-1
计数
在 for
循环内部,我们必须包含每次循环迭代时都应处理的代码。如果表格中有十个小行星,则循环将迭代十次。如果表格中只有一个小行星,它将迭代一次.
将以下突出显示的行添加到 for
循环内部:
-- 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 thisAsteroid = asteroidsTable[i]
本质上,thisAsteroid
被设置为括号 ([]
) 内索引号处的表格项,因此通过使用 for
循环的索引 i
,我们轻松获得对表格中每个小行星的引用,因为循环从头到尾迭代。
x
和 y
属性来检查小行星是否已大幅偏离屏幕的任何边缘:if ( thisAsteroid.x < -100 or thisAsteroid.x > display.contentWidth + 100 or thisAsteroid.y < -100 or thisAsteroid.y > display.contentHeight + 100 ) then
首先,我们使用 display.remove()
命令将小行星从屏幕上移除
display.remove( thisAsteroid )
其次,我们使用 Lua 的table.remove()
命令将小行星从 asteroidsTable
表中移除。此命令简单地从指定索引处的表中移除一个项目,在本例中为循环i
table.remove( asteroidsTable, i )
理解基本的 Lua 内存管理及其与显示对象的关系非常重要。上面的第一个命令,display.remove( thisAsteroid )
因为我们在 asteroidsTable
表中存储了对小行星的**额外**引用,所以 Lua 无法释放分配给小行星对象的内存,直到该引用被移除。这就是我们执行第二个命令的原因,table.remove( asteroidsTable, i )
这样就完成了我们的小行星清理工作!基本上,在游戏循环的每次迭代中,我们使用 for
循环检查已经大幅漂移到屏幕边界之外的小行星。然后,将每个“失效”的小行星从屏幕**和** asteroidsTable
存储表中移除,从而保持小行星总数和内存使用量较低。
尽管您可能将游戏循环想象成在应用程序运行时的每一帧(高达每秒 60 次)更新的代码/功能,但这对每个应用程序来说并不一定都是正确的。因为我们的游戏循环是一个标准函数,所以我们可以完全控制它运行的频率和时间。
有几种不同的方法可以重复运行游戏循环函数。在这个游戏中,我们显然不想每秒生成 60 个新的小行星,而且也不需要经常执行清理任务。因此,我们将实现一个**重复计时器**。
紧跟在 gameLoop()
函数之后(在其结束 end
语句之后),添加以下命令
display.remove( thisAsteroid ) table.remove( asteroidsTable, i ) end end end gameLoopTimer = timer.performWithDelay( 500, gameLoop, 0 )
让我们剖析一下这一行
首先,我们声明 gameLoopTimer
占位符变量(先前已声明)将与我们的计时器关联。这允许我们在以后需要时使用该变量作为暂停或取消计时器的引用/句柄。
接下来,我们调用 timer.performWithDelay()
。这个便捷的命令告诉 Solar2D 在指定的毫秒数后执行某些操作。计时器对各种游戏功能都很有用,所以请熟悉它们!
在括号内,我们首先输入计时器触发之前要等待的毫秒数(延迟)。这里我们使用 500
,这恰好是半秒,但您可以尝试其他值。像 250
这样的较低数字会使小行星生成更快并增加游戏的难度,但不要将此值设置得太低,否则小行星会生成太快并挤满屏幕。
下一个参数是计时器触发时将被调用/运行的函数。显然,我们在这里指定 gameLoop
。
最后,我们包含可选的**迭代次数**参数,其值为 0
。如果省略此参数,计时器将只触发一次然后停止。如果您包含此参数,计时器将重复该迭代次数,但不要被这种情况下的 0
所迷惑——我们**不是**告诉计时器0
或 -1
将导致计时器无限期地重复(除非我们告诉它暂停/停止)。
全部完成!游戏循环已完成,我们有一个计时器来重复且无限期地运行它。保存您修改后的 main.lua
文件,重新启动模拟器,您应该会看到新的 小行星开始出现,并在屏幕上稳定地漂移和旋转。我们的游戏现在真正开始焕发生机了!
是时候处理碰撞了!最初,我们只检测特定的碰撞
碰撞在成对的对象之间报告,可以使用对象侦听器在对象上**局部**检测,或使用运行时侦听器**全局**检测。不同的游戏需要不同的方法,但这里有一个一般准则
局部碰撞处理最适合用于
全局碰撞处理最适合用于
虽然这个游戏只有一个玩家对象(飞船),但似乎最佳选择是**局部**碰撞处理。但是,游戏还需要检测多个激光和多个小行星之间的碰撞,因此**全局**碰撞处理是更好的选择。
在我们进入碰撞处理之前,我们需要一个函数,可以在飞船与小行星碰撞后调用该函数来恢复飞船。在我们的游戏中,我们将模仿经典的街机游戏,在新飞船淡入视野时,它会暂时无敌——毕竟,让玩家在没有机会躲避 incoming 小行星的情况下连续死亡是不公平的!
在您已经编写的代码之后,添加以下突出显示的函数
gameLoopTimer = timer.performWithDelay( 500, gameLoop, 0 ) 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
让我们检查一下这个函数的内容
第一个命令,ship.isBodyActive = false
接下来的两行只是将飞船重新定位在
最后一个命令现在可能没有完全理解,但在我们进一步添加“死亡”功能后就会理解。本质上,这个 transition.to()
命令会在四秒钟内将飞船淡回到完全不透明度 (alpha=1
)。它还包括对匿名函数的 onComplete
回调。此函数将飞船恢复为活动的物理体,并将 died
变量重置为 false
。
接下来,让我们编写碰撞函数的基础
end } ) end local function onCollision( event ) if ( event.phase == "began" ) then local obj1 = event.object1 local obj2 = event.object2 end end
这相对简单,您应该能从前面的一些基本概念中认出它
与触摸事件类似,碰撞也有不同的**阶段**,在本例中是 "began"
和 "ended"
。"began"
碰撞阶段是您需要处理的最常见的阶段,但在某些情况下,检测 "ended"
阶段也很重要。现在不用太担心这个——在这里,我们只是通过将我们的功能包装在一个条件子句中来隔离 "began"
阶段。
为了简化整个函数,我们使用局部变量 obj1
和 obj2
来引用碰撞中涉及的两个对象。使用全局方法检测碰撞时,这些对象由 event.object1
和 event.object2
引用。
让我们处理我们的第一个碰撞条件:激光和小行星。还记得我们在创建每个对象时如何为其分配一个 myName
属性吗?此属性现在成为检测哪两种对象类型发生碰撞的关键。这里,开头的条件子句检查 obj1
和 obj2
的 myName
属性。如果这些值是 "laser"
和 "asteroid"
,我们就知道哪两种对象类型发生了碰撞,我们可以继续处理结果。
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 end end end
使用全局方法检测碰撞时,无法确定碰撞中涉及的“第一个”和“第二个”对象。换句话说,obj1
可能是激光,obj2
是小行星,或者它们可能是翻转的。这就是我们构建一个
在条件子句内,让我们首先简单地通过 display.remove()
移除这两个对象。虽然一个花哨的爆炸效果会很棒,但这个项目只是展示了如何处理碰撞——你以后如何扩展它取决于你的想象力!
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 ) end end end
接下来,让我们从 asteroidsTable
表中移除被摧毁的小行星,这样游戏循环就不需要再担心它了。为此,我们使用另一个 for
循环来迭代 asteroidsTable
,找到小行星的实例并将其移除
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 end end end
在这个循环中,我们还利用了一个称为**中断**的小效率技巧,由 break
命令执行。因为我们只寻找一个特定的小行星,所以一旦该小行星被移除,循环可以立即中断/停止,从而有效地停止任何进一步的处理工作。
最后,为了奖励玩家摧毁小行星,我们将 score
变量增加 100
并更新 scoreText
文本对象以反映新的值
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 end end end
现在让我们处理第二个碰撞条件:小行星和飞船。在第一个条件之后将以下条件添加到if-then
elseif
是因为我们正在向同一个语句添加另一个可能的条件
-- 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 end end end
在这个条件子句中,让我们从一个额外的if-then
elseif ( ( obj1.myName == "ship" and obj2.myName == "asteroid" ) or ( obj1.myName == "asteroid" and obj2.myName == "ship" ) ) then if ( died == false ) then end end end end
开始
此条件检查可能看起来有点奇怪,但我们需要确认飞船尚未被摧毁。随着游戏的进行,或者小行星生成速度更快,飞船完全有可能几乎同时被两颗小行星击中。在这种情况下失去两条命显然是不公平的,因此我们检查 died
的值,只有当它是 false
时才继续。if-then
died = true
elseif ( ( obj1.myName == "ship" and obj2.myName == "asteroid" ) or ( obj1.myName == "asteroid" and obj2.myName == "ship" ) ) then if ( died == false ) then died = true end end end end
接下来,我们将从 lives
变量中减去一条命,并更新 livesText
文本对象以反映新的值
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 end end end end
最后,让我们包含一个条件语句来检查玩家是否还有命
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
在这个语句的开头子句中,如果 lives
等于 0
,我们 פשוט מסירים את הספינה לחלוטין. 这就是您可以显示“游戏结束”消息或执行其他操作的地方,但现在我们将保持各种可能性。
在默认的 else
子句中(玩家至少还有一条命),我们通过将其 alpha
属性设置为 0
来使飞船不可见。这与我们之前编写的 restoreShip()
函数相关联,在该函数中,当飞船淡回视野时,transition.to()
命令将飞船的 alpha
转换回 1
。在那行之后,我们实际上在一秒钟的延迟后调用了 restoreShip()
函数——这会在飞船开始淡回视野之前产生轻微的延迟。
我们所有的碰撞逻辑现在都已到位,但除非我们将它连接起来,否则绝对不会发生任何事情!由于我们决定使用全局方法实现碰撞,因此只需一个命令即可告诉 Solar2D 它应该侦听应用程序的每个运行时帧中的新碰撞。
紧跟在 onCollision()
函数之后(在其结束 end
语句之后),添加以下命令
end end end Runtime:addEventListener( "collision", onCollision )
此命令类似于之前的事件侦听器,我们在其中将 "tap"
或 "touch"
侦听器类型添加到特定对象。在这里,我们只是将 "collision"
侦听器类型添加到全局 Runtime
对象。
就是这样!保存您的 main.lua
文件,重新启动模拟器,您的游戏就完成了——玩家可以完全控制飞船,小行星继续生成并在屏幕上移动,分数和生命都被计算在内,我们基本上有一个功能齐全的游戏!
如果您错过了什么,完整的程序可以在这里下载。这个项目比前一个稍微复杂一些,所以下载原始源代码并与您创建的代码进行比较可能会有所帮助。
我们在本章中涵盖了很多概念。以下是概述:
命令/属性 | 描述 |
---|---|
table.insert() | 将给定值插入到表中。 |
table.remove() | 从指定索引处的表中删除一个项目。 |
math.random() | 返回一个 |
object:setLinearVelocity() | 设置物体线性速度的 **x** 和 **y** 分量。 |
object:applyTorque() | 对物理物体施加旋转力。 |
object.isBullet | 一个布尔值,指示一个物体是否应该被视为“子弹”。 |
object:toBack() | 将目标对象移动到其父组的视觉后方。 |
transition.to() | 使用可选的缓动算法对显示对象进行动画处理(过渡)。 |
display.remove() | 如果对象或组不为 nil ,则将其删除。 |
object:setFocus() | 将特定显示对象设置为所有未来点击事件的目标"touch" 和 "tap" ) |
timer.performWithDelay() | 延迟后调用指定的函数。 |
object.isBodyActive | 设置或获取物理物体的当前活动状态。 |
⟨ 第二章 — 向上 & 向前 | 第四章 — 创建场景 ⟩