第三章 — 赋予生命

上一页 | 下一页

我们的项目开始成形,但它还不是一个游戏。让我们给它注入一些生命!

创建小行星

小行星的创建将由一个函数处理。此函数将作为我们**游戏循环**的一部分定期运行(调用),游戏循环是一个重复调用的函数,用于处理各种游戏功能。

像我们之前的函数一样,我们以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。此表现在用作存储新小行星的地方。要将新小行星实例插入表中,我们可以使用内置的Lua 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 生成一个介于 13 之间的随机整数。使用只有一个参数 3math.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 的值将为 123。使用它,我们可以实现一个条件if-then结构来处理这三种情况。在 Lua 中,该结构以这种基本形式开始

    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()1500 之间随机选择一个值,有效地使小行星出现在内容区域顶部和向下大约一半距离之间的某个位置——毕竟,我们不希望任何小行星来自无法射击它们的地方!

移动

现在我们有了起点,我们需要告诉小行星它应该移动到哪里。这次我们将使用另一个物理命令: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 和您指示的值之间随机生成一个整数。当使用两个参数调用时,该命令会在两个指定值之间随机生成一个整数,例如在上面的第一个实例中介于 40120 之间。

如果我们决定现在调用/运行此函数,我们可能会看到一颗小行星从左侧缓慢地穿过屏幕,但也可能不会。为什么?因为我们还没有为屏幕的另外两侧添加条件情况!请记住,Lua 会在 13 之间随机选择一个数字,但目前我们只处理 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

这段代码的大部分内容应该很容易理解

  1. 我们在 mainGroup 显示组内创建一个新的激光对象,使用 5 作为帧号,因为激光是图像表配置中的第 5 帧。

  2. 我们将其作为传感器类型对象 (isSensor=true) 添加到物理引擎中。

  3. 我们通过将其 isBullet 属性设置为 true 来指示激光应被视为“子弹”。这使得对象进行连续碰撞检测,而不是在世界时间步长进行周期性碰撞检测。因为我们的激光将非常快速地穿过屏幕,这将有助于确保它不会在没有注册碰撞的情况下“穿过”任何小行星。

  4. 最后,我们为对象分配一个 myName 属性 "laser",与飞船和小行星类似,这在检测碰撞时很有用。

放置

新的激光对象现已加载,但我们尚未正确定位它。在这种情况下,我们不能使用静态位置,因为飞船最终将通过玩家的控制左右移动。幸运的是,通过将其 xy 值设置为飞船的 xy 值,可以很容易地将新激光定位在与飞船完全相同的位置

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 =之后指定的函数没有名称。在 Lua 中,这称为匿名函数。这些函数可用作“临时”函数,或者作为另一个函数的参数所需的函数等。虽然我们可以编写一个专用函数来移除激光并通过 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

请注意,与我们之前的函数不同,此函数在其名称后面的括号中包含关键字 event。正如您在 BalloonTap 项目中学到的那样,Solar2D 主要是一个事件驱动的框架,其中信息会在特定事件期间分派给事件监听器

具体来说,对于此例程,event 参数(表格)告诉我们用户正在触摸/拖动的对象,触摸在内容空间中的位置,以及其他一些信息。随着您继续学习并研究现有的代码示例,您会经常看到这个 event 参数,因此现在熟悉它是一个好主意。

在函数内部,为了使事情更清晰一些,让我们将局部变量 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" 阶段(您可以猜到),我们移动飞船!这只需设置飞船的x 位置即可完成,但请仔细观察 — 这就是我们的 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,我们轻松获得对表格中每个小行星的引用,因为循环从头到尾迭代。

  • 下一个块是一个多条件语句,用于检查在循环迭代期间引用的 小行星的位置。基本上,使用此语句,我们通过检查其 xy 属性来检查小行星是否已大幅偏离屏幕的任何边缘:
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 ),将从视觉上移除屏幕上的小行星。但是,仅此命令**不会**将小行星从 Lua 内存中释放。为什么?

因为我们在 asteroidsTable 表中存储了对小行星的**额外**引用,所以 Lua 无法释放分配给小行星对象的内存,直到该引用被移除。这就是我们执行第二个命令的原因,table.remove( asteroidsTable, i ),紧随其后。这有效地删除了额外的引用,并且由于没有其他对该对象的持久引用,Lua 垃圾收集过程可以自动释放其分配的内存。

这样就完成了我们的小行星清理工作!基本上,在游戏循环的每次迭代中,我们使用 for 循环检查已经大幅漂移到屏幕边界之外的小行星。然后,将每个“失效”的小行星从屏幕**和** asteroidsTable 存储表中移除,从而保持小行星总数和内存使用量较低。

循环计时器

尽管您可能将游戏循环想象成在应用程序运行时的每一帧(高达每秒 60 次)更新的代码/功能,但这对每个应用程序来说并不一定都是正确的。因为我们的游戏循环是一个标准函数,所以我们可以完全控制它运行的频率和时间。

有几种不同的方法可以重复运行游戏循环函数。在这个游戏中,我们显然不想每秒生成 60 个新的小行星,而且也不需要经常执行清理任务。因此,我们将实现一个**重复计时器**。

紧跟在 gameLoop() 函数之后(在其结束 end 语句之后),添加以下命令

            display.remove( thisAsteroid )
            table.remove( asteroidsTable, i )
        end
    end
end

gameLoopTimer = timer.performWithDelay( 500, gameLoop, 0 )

让我们剖析一下这一行

  1. 首先,我们声明 gameLoopTimer 占位符变量(先前已声明)将与我们的计时器关联。这允许我们在以后需要时使用该变量作为暂停或取消计时器的引用/句柄。

  2. 接下来,我们调用 timer.performWithDelay()。这个便捷的命令告诉 Solar2D 在指定的毫秒数后执行某些操作。计时器对各种游戏功能都很有用,所以请熟悉它们!

  3. 在括号内,我们首先输入计时器触发之前要等待的毫秒数(延迟)。这里我们使用 500,这恰好是半秒,但您可以尝试其他值。像 250 这样的较低数字会使小行星生成更快并增加游戏的难度,但不要将此值设置得太低,否则小行星会生成太快并挤满屏幕。

  4. 下一个参数是计时器触发时将被调用/运行的函数。显然,我们在这里指定 gameLoop

  5. 最后,我们包含可选的**迭代次数**参数,其值为 0。如果省略此参数,计时器将只触发一次然后停止。如果您包含此参数,计时器将重复该迭代次数,但不要被这种情况下的 0 所迷惑——我们**不是**告诉计时器“运行零次”而是永远重复。基本上,传递 0-1 将导致计时器无限期地重复(除非我们告诉它暂停/停止)。

全部完成!游戏循环已完成,我们有一个计时器来重复且无限期地运行它。保存您修改后的 main.lua 文件,重新启动模拟器,您应该会看到新的 小行星开始出现,并在屏幕上稳定地漂移和旋转。我们的游戏现在真正开始焕发生机了!

碰撞处理

是时候处理碰撞了!最初,我们只检测特定的碰撞

  1. 当激光与小行星碰撞时。
  2. 当小行星与飞船碰撞时。

碰撞在成对的对象之间报告,可以使用对象侦听器在对象上**局部**检测,或使用运行时侦听器**全局**检测。不同的游戏需要不同的方法,但这里有一个一般准则

  • 局部碰撞处理最适合用于一对多碰撞场景,例如一个玩家对象可能与多个敌人碰撞,道具等。

  • 全局碰撞处理最适合用于多对多碰撞场景,例如多个英雄角色可能与多个敌人碰撞。

虽然这个游戏只有一个玩家对象(飞船),但似乎最佳选择是**局部**碰撞处理。但是,游戏还需要检测多个激光和多个小行星之间的碰撞,因此**全局**碰撞处理是更好的选择。

恢复飞船

在我们进入碰撞处理之前,我们需要一个函数,可以在飞船与小行星碰撞后调用该函数来恢复飞船。在我们的游戏中,我们将模仿经典的街机游戏,在新飞船淡入视野时,它会暂时无敌——毕竟,让玩家在没有机会躲避 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

让我们检查一下这个函数的内容

  1. 第一个命令,ship.isBodyActive = false,有效地将飞船从物理模拟中移除,使其停止与其他物体交互。我们这样做(暂时)是为了让飞船在淡回视野时,碰撞的小行星不会触发另一个碰撞响应。

  2. 接下来的两行只是将飞船重新定位在底部中心屏幕。

  3. 最后一个命令现在可能没有完全理解,但在我们进一步添加“死亡”功能后就会理解。本质上,这个 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

这相对简单,您应该能从前面的一些基本概念中认出它

  1. 与触摸事件类似,碰撞也有不同的**阶段**,在本例中是 "began""ended""began" 碰撞阶段是您需要处理的最常见的阶段,但在某些情况下,检测 "ended" 阶段也很重要。现在不用太担心这个——在这里,我们只是通过将我们的功能包装在一个条件子句中来隔离 "began" 阶段。

  2. 为了简化整个函数,我们使用局部变量 obj1obj2 来引用碰撞中涉及的两个对象。使用全局方法检测碰撞时,这些对象由 event.object1event.object2 引用。

激光与小行星

让我们处理我们的第一个碰撞条件:激光和小行星。还记得我们在创建每个对象时如何为其分配一个 myName 属性吗?此属性现在成为检测哪两种对象类型发生碰撞的关键。这里,开头的条件子句检查 obj1obj2myName 属性。如果这些值是 "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 设置或获取物理物体的当前活动状态。