持续动作

在某些应用中,您需要在用户触摸屏幕时执行一些持续动作。这可能包括玩家按住“开火”按钮时太空飞船发射激光,或者常见的大多数 2D 平台游戏中都有的“移动按钮”。

对于初学者来说,这个过程可能比较难以理解,所以让我们探索一些实现持续动作的技术。

启用多点触控

大多数需要持续动作的游戏也需要多点触控,允许多个手指同时操作屏幕上的多个控件。例如,一个同时具有“移动”按钮和“跳跃”按钮的 2D 平台游戏通常会让玩家用左手拇指控制一组按钮,用右手拇指控制另一组按钮。屏幕上的控件。

点击/触摸/多点触控指南中所述,默认情况下多点触控是禁用的,但启用它很简单

-- Activate multitouch
system.activate( "multitouch" )
注意

根据您的游戏设计,您应该仔细考虑在哪里调用此命令。虽然它可以作为 `main.lua` 中的第一行代码之一调用,但这可能不是最佳选择 — 例如,如果您的游戏以菜单场景开始(大多数游戏都是如此)那么您可能在该阶段不需要多点触控功能,在这种情况下,应将此命令推迟到实际需要多点触控时再调用。

现在让我们探索一些可能应用持续动作的常见元素

虚拟按钮

在明显缺乏游戏手柄等物理控制器的手机游戏中,一个常见的 UI 元素是虚拟按钮。这些按钮的范围很广,从“跳跃”按钮到“开火”按钮,几乎涵盖了游戏设计师能想到的任何东西。在某些情况下,它们提供了一种一次按下类型的行为 — 点击/触摸按钮,动作发生一次,例如跳跃。在其他情况下,只要玩家将手指按住按钮,这些按钮就会执行一个动作,例如发射连续的激光束。后一种情况就是持续动作发挥作用的地方。

创建按钮区域

我们将创建一个可以容纳一个或多个按钮的按钮区域,而不是通过类似 widget.newButton() 的方法创建专用按钮。 正如您在本教程中将学到的,这对于实现所有持续动作功能是必要的。 这也是一种将相关按钮组“分组”的便捷方式,例如将“跳跃”按钮直接放在“开火”按钮旁边。

首先,让我们创建一个新的 显示组 来包含该区域

local buttonGroup = display.newGroup()

现在,让我们为该区域创建一个可见的“按钮”,它实际上只是一个标准图像

local buttonGroup = display.newGroup()

local fireButton = display.newImageRect( buttonGroup, "fireButton.png", 64, 64 )
fireButton.x, fireButton.y = 60, display.contentHeight-60
重要
  • 请注意,此图像通过将 `buttonGroup` 指定为 display.newImageRect() 的第一个参数插入到按钮组中。

  • 再次强调,这不是一个功能按钮,而只是一个定义按钮区域内玩家触摸交互将被处理的区域的图像。 因此,它不需要添加触摸或点击事件监听器。

现在,让我们创建一个实际检测触摸操作的对象。该对象只是一个覆盖按钮图像的不可见矢量矩形,其大小由先前插入到 `buttonGroup` 中的图像(如上面的 `fireButton` 图像)自动计算。

local fireButton = display.newImageRect( buttonGroup, "fireButton.png", 64, 64 )
fireButton.x, fireButton.y = 60, display.contentHeight-60

local groupBounds = buttonGroup.contentBounds
local groupRegion = display.newRect( 0, 0, groupBounds.xMax-groupBounds.xMin+200, groupBounds.yMax-groupBounds.yMin+200 )
groupRegion.x = groupBounds.xMin + ( buttonGroup.contentWidth/2 )
groupRegion.y = groupBounds.yMin + ( buttonGroup.height/2 )
groupRegion.isVisible = false
groupRegion.isHitTestable = true
注意
  • 这个 `groupRegion` 矢量矩形的大小实际上比按钮图像在水平和垂直方向上都 200 像素。这是因为,正如本教程后面将要讨论的,我们还需要处理玩家的触摸从按钮区域内部移动到外部的情况,或者滑出的情况。虽然将矩形在按钮四周延伸这么远似乎有些过分,但这有助于确保玩家无法非常快速地将触摸从按钮上滑开或移开,并仍然导致 Solar2D 认为触摸处于活动状态。别担心 —这个大型矢量对象不会阻止触摸传播到场景中的其他对象,除非触摸点位于按钮图像的边界内。

  • 第 10 行和第 11 行,我们将矩形设置为不可见并且可点击测试。 `groupRegion.isHitTestable = true` 命令在这种情况下尤其重要,因为默认情况下,不可见对象不会检测触摸。此命令可确保它接收触摸事件。`groupRegion.isHitTestable = true`命令尤其重要,因为默认情况下,不可见对象将**不会**检测触摸。此命令确保它**将**接收触摸事件。

区域检测函数

为了检测 `groupRegion` 矩形上的触摸点何时与 `fireButton` 图像的边界相交,我们将使用一个函数。本质上,当被调用时,此函数将循环遍历插入到 `buttonGroup` 中的图像对象,并对每个对象检查触摸点是否在其内容边界内。如果它检测到触摸点任何按钮图像的边界内,它将返回对该对象的引用。

local function detectButton( event )

    for i = 1,buttonGroup.numChildren do
        local bounds = buttonGroup[i].contentBounds
        if (
            event.x > bounds.xMin and
            event.x < bounds.xMax and
            event.y > bounds.yMin and
            event.y < bounds.yMax
        ) then
            return buttonGroup[i]
        end
    end
end
注意

此代码将仅准确测试触摸点是否在按钮的矩形边缘边界内。 如果您有一个如上所示的按钮图像那样视觉上呈圆形的按钮,则这将不是完全准确的。 但是,在大多数游戏中,测试触摸点是否在按钮周围的矩形区域内就足够了。

可选地,可以调整这些条件,使其在识别活动触摸的位置方面更加(或更少)宽容。 例如,我们可以稍微减小有效区域的大小,以确保触摸确实在按钮“内部”,将所有四个边缘向内缩进 4 像素,如下所示

event.x > bounds.xMin + 4 and
event.x < bounds.xMax - 4 and
event.y > bounds.yMin + 4 and
event.y < bounds.yMax - 4

按钮监听器

现在让我们构造监听器函数来处理按钮区域对象上的触摸事件

local function handleController( event )

    local touchOverButton = detectButton( event )

    if ( event.phase == "began" ) then

        if ( touchOverButton ~= nil ) then
            if not ( buttonGroup.touchID ) then
                -- Set/isolate this touch ID
                buttonGroup.touchID = event.id
                -- Set the active button
                buttonGroup.activeButton = touchOverButton
                -- Fire the weapon
                print( "BEGIN FIRING" )
            end
            return true
        end

    elseif ( event.phase == "ended" and buttonGroup.activeButton ~= nil ) then

        -- Release this touch ID
        buttonGroup.touchID = nil
        -- Set that no button is active
        buttonGroup.activeButton = nil
        -- Stop firing the weapon
        print( "STOP FIRING" )
        return true
    end
end

让我们更详细地检查这个函数

  1. 在第 30 行,我们调用上一步中的 `detectButton()` 函数,正如您所记得的,如果触摸点与 `buttonGroup` 中任何按钮的边界相交,该函数将返回一个引用。

  2. 对于触摸的 `"began"` 阶段(第 32-44 行),如果该触摸点与按钮相交,我们首先确认没有现有的 `touchID` 属性分配给按钮组(第 35 行)—当启用多点触控时,这是一个重要的方面,因为我们不希望玩家能够同时用多个手指(触摸)操作同一个按钮。

  3. 如果此条件通过,并且此触摸是按钮组上的第一次/唯一触摸,则我们将 Solar2D 跟踪的唯一触摸 ID (`event.id`) 分配给组的 `touchID` 属性(第 37 行)。

  4. 在此之后,我们将 `buttonGroup.activeButton` 设置为按钮引用,然后执行正确的操作(在本例中,开始发射武器)。 此外,在第 43 行,我们返回 true以便触摸将**不会**传播过按钮到其后面的任何触摸敏感对象。

  5. 对于触摸的 `"ended"` 阶段(第 46-54 行),如果按钮处于活动状态,我们首先通过将 `buttonGroup.touchID` 设置为 `nil` 来“释放”与按钮组关联的触摸 ID(第 49 行)。然后,我们将 `buttonGroup.activeButton` 设置为 `nil`,然后执行正确的操作(在本例中,停止发射武器)。与 `"began"` 阶段一样,我们也返回 true返回 true触摸敏感对象。

处理滑出

以便触摸不会传播过按钮到其后面的任何触摸敏感对象。如您所见,`handleController()` 函数当前处理触摸的 `"began"` 和 `"ended"` 阶段 — 当玩家触摸按钮边界内时,我们可以开始发射武器,当玩家抬起手指时,我们可以停止发射。但是,有一个**非常**重要的情况您必须考虑到:滑出滑出

情况。在内部,当用户的触摸从对象上抬起时,Solar2D 会生成一个 `"ended"` 阶段,但这仅在触摸位置位于对象上方时才会发生。默认情况下,如果用户触摸一个对象,将其手指滑动到其内容边界之外,然后释放,Solar2D 将**不会**生成 `"ended"` 事件。因此,除非我们采取措施来解决这个问题,否则玩家可以将他们的触摸滑动到按钮区域矩形的边界之外,释放,武器将继续发射!

为了防止这种情况,我们可以使用 `"moved"` 事件阶段添加另一个检查。顾名思义,每当玩家的手指从初始触摸点移动时,都会触发此阶段。使用它,我们可以确保当玩家将触摸滑动到按钮边界之外时,武器停止发射

local function handleController( event )

    local touchOverButton = detectButton( event )

    if ( event.phase == "began" ) then

        if ( touchOverButton ~= nil ) then
            if not ( buttonGroup.touchID ) then
                -- Set/isolate this touch ID
                buttonGroup.touchID = event.id
                -- Set the active button
                buttonGroup.activeButton = touchOverButton
                -- Fire the weapon
                print( "BEGIN FIRING" )
            end
            return true
        end

    elseif ( event.phase == "moved" ) then

        -- Handle slide off
        if ( touchOverButton == nil and buttonGroup.activeButton ~= nil ) then
            event.target:dispatchEvent( { name="touch", phase="ended", target=event.target, x=event.x, y=event.y } )
            return true
        end

    elseif ( event.phase == "ended" and buttonGroup.activeButton ~= nil ) then

        -- Release this touch ID
        buttonGroup.touchID = nil
        -- Set that no button is active
        buttonGroup.activeButton = nil
        -- Stop firing the weapon
        print( "STOP FIRING" )
        return true
    end
end

基本上,使用这段附加代码,我们有条件地检查触摸点是否在按钮外部,并且 `buttonGroup.activeButton`当前不为 `nil` —第二个条件尤其重要,因为我们需要知道当滑出滑出

发生时按钮已经被按下。如果满足这两个条件,我们使用便捷的 object:dispatchEvent() 方法向同一个监听器函数分派一个“伪事件”`"ended"`,使 Solar2D 认为触摸已结束,即使玩家的手指实际上仍在触摸屏幕。

激活控制器

基本的检测代码现已完成,但控制器本身不会执行任何操作。这是因为我们还没有“激活”它!

要使其激活,只需向 groupRegion 对象添加一个标准触摸事件监听器,在每次触摸事件触发时调用 handleController() 函数。

groupRegion:addEventListener( "touch", handleController )

响应动作

根据按钮是否被按下,我们需要采取一些相关的操作。由于我们处理的是**持续**动作,事件本身应该以某种方式持续进行。

一种方法是执行一个动作(例如发射激光)在每个运行时帧上使用 "enterFrame" 监听器,但这对于大多数用途来说可能过于频繁  — 毕竟,一艘飞船真的应该发射30 或 60 发激光 在典型的射击游戏中**每秒**吗?

更实用的方法是使用**计时器**,并根据按钮状态将其打开/关闭,从而允许我们控制持续动作的速率。因此,让我们将计时器集成到现有代码中。

local fireTimer

local function fireLaser( event )
    print( "FIRE A LASER!" )
end

local function handleController( event )

    local touchOverButton = detectButton( event )

    if ( event.phase == "began" ) then

        if ( touchOverButton ~= nil ) then
            if not ( buttonGroup.touchID ) then
                -- Set/isolate this touch ID
                buttonGroup.touchID = event.id
                -- Set the active button
                buttonGroup.activeButton = touchOverButton
                -- Fire the weapon
                print( "BEGIN FIRING" )
                fireTimer = timer.performWithDelay( 100, fireLaser, 0 )
            end
            return true
        end

    elseif ( event.phase == "moved" ) then

        -- Handle slide off
        if ( touchOverButton == nil and buttonGroup.activeButton ~= nil ) then
            event.target:dispatchEvent( { name="touch", phase="ended", target=event.target, x=event.x, y=event.y } )
            return true
        end

    elseif ( event.phase == "ended" and buttonGroup.activeButton ~= nil ) then

        -- Release this touch ID
        buttonGroup.touchID = nil
        -- Set that no button is active
        buttonGroup.activeButton = nil
        -- Stop firing the weapon
        print( "STOP FIRING" )
        timer.cancel( fireTimer )
        return true
    end
end

groupRegion:addEventListener( "touch", handleController )

让我们更详细地检查突出显示的添加内容。

  1. 在第 28 行,我们前向声明一个变量 fireTimer。这将用作控制动作的计时器的持久引用。

  2. 第 30-32 行,我们添加了发射激光的基础函数 (fireLaser())。你如何实际发射激光(或执行任何持续动作)完全取决于你的游戏,所以现在我们只 print() 一个字符串用于测试。

  3. 在第 48 行,当按钮被有效按下时,我们**启动**一个新的计时器,并将其分配给我们在第 28 行创建的 fireTimer 引用。此计时器将每 100 毫秒重复一次,并在每次迭代时触发 fireLaser() 函数。

  4. 在第 69 行,当按钮被有效释放时,我们使用创建它的 fireTimer 引用**取消**计时器。

虚拟方向键

另一个常见的 UI 元素是**虚拟方向键**。这些通常由双向四向虚拟按钮排列并排或以十字形配置,类似于游戏控制器上的物理方向键。

在 Solar2D 中创建这样的控件集可以类似于上面的虚拟按钮方法,但在这种情况下,玩家通常会将手指放在控制键区域的屏幕上,只需滑动(不释放)即可激活另一个方向按钮。因此,除了滑出之外,我们还必须处理滑入玩家只需将触摸点从一个方向按钮移动到另一个方向按钮的操作。

创建控制器

这次,让我们使用**两张**图片并排来构建一个基本的双向控制器

local buttonGroup = display.newGroup()

local leftButton = display.newImageRect( buttonGroup, "leftButton.png", 64, 64 )
leftButton.x, leftButton.y = 60, display.contentHeight-60
leftButton.canSlideOn = true
leftButton.ID = "left"

local rightButton = display.newImageRect( buttonGroup, "rightButton.png", 64, 64 )
rightButton.x, rightButton.y = 136, display.contentHeight-60
rightButton.canSlideOn = true
rightButton.ID = "right"

local groupBounds = buttonGroup.contentBounds
local groupRegion = display.newRect( 0, 0, groupBounds.xMax-groupBounds.xMin+200, groupBounds.yMax-groupBounds.yMin+200 )
groupRegion.x = groupBounds.xMin + ( buttonGroup.contentWidth/2 )
groupRegion.y = groupBounds.yMin + ( buttonGroup.height/2 )
groupRegion.isVisible = false
groupRegion.isHitTestable = true

local function detectButton( event )

    for i = 1,buttonGroup.numChildren do
        local bounds = buttonGroup[i].contentBounds
        if (
            event.x > bounds.xMin and
            event.x < bounds.xMax and
            event.y > bounds.yMin and
            event.y < bounds.yMax
        ) then
            return buttonGroup[i]
        end
    end
end

此代码与上面的虚拟按钮示例类似,但有两个非常重要的区别。

  1. 对于每个按钮,我们设置一个布尔值 canSlideOn 属性,初始设置为 true。因为操作方向键的玩家通常会将他们的触摸从一个按钮滑动到另一个按钮,这将让我们处理滑入行为。

  2. 我们为每个按钮分配一个 ID 属性,值为 "left""right" — 稍后,这将帮助我们识别它代表的“方向”。

按钮监听器

现在让我们构造监听器函数来处理触摸事件,并调整第一个示例中的代码以处理多个按钮。我们将从 "began" 阶段块开始。

local function handleController( event )

    local touchOverButton = detectButton( event )

    if ( event.phase == "began" ) then

        if ( touchOverButton ~= nil ) then
            if not ( buttonGroup.touchID ) then
                -- Set/isolate this touch ID
                buttonGroup.touchID = event.id
                -- Set the active button
                buttonGroup.activeButton = touchOverButton
                -- Take proper action based on button ID
                if ( buttonGroup.activeButton.ID == "left" ) then
                    print( "LEFT" )
                elseif ( buttonGroup.activeButton.ID == "right" ) then
                    print( "RIGHT" )
                end
            end
            return true
        end

这与第一个示例类似,但我们在活动按钮的 ID 属性上添加了一些条件检查(第 48 和 50 行)以确定要采取的操作。

现在让我们扩展 "moved" 阶段块。

    elseif ( event.phase == "moved" ) then

        -- Handle slide off
        if ( touchOverButton == nil and buttonGroup.activeButton ~= nil ) then
            event.target:dispatchEvent( { name="touch", phase="ended", target=event.target, x=event.x, y=event.y } )
            return true

        -- Handle slide on
        elseif ( touchOverButton ~= nil and buttonGroup.activeButton == nil and touchOverButton.canSlideOn ) then
            event.target:dispatchEvent( { name="touch", phase="began", target=event.target, x=event.x, y=event.y, id=event.id } )
            return true
        end

    elseif ( event.phase == "ended" and buttonGroup.activeButton ~= nil ) then

        -- Release this touch ID
        buttonGroup.touchID = nil
        -- Set that no button is active
        buttonGroup.activeButton = nil
        -- Stop the action
        print( "STOP" )
        return true
    end
end

groupRegion:addEventListener( "touch", handleController )

通过此附加检查(第 64-67 行),我们检查滑入通过测试触摸点是否在按钮的边界内**并且** buttonGroup.activeButton当前为 nil  —第二个条件尤其重要,因为我们需要知道当滑入发生时按钮**未**被按下。作为第三个条件,我们通过测试 canSlideOn 属性值是否为 true 来确认按钮是否接受滑入行为。

如果满足所有条件,我们将使用 object:dispatchEvent() 方法调度一个伪事件"began" 到同一个监听器函数,使 Solar2D 认为按钮上开始了新的触摸,即使玩家的手指已经在物理上触摸屏幕。

基于帧的移动

响应与方向按钮的交互可能与典型按钮不同。通常,如果按下方向按钮,则应发生稳定且一致的动作,直到释放按钮(或与相邻按钮交互)。

连续移动角色/对象的一种方法是在运行时 "enterFrame" 函数中简单地更新其**x** 或**y** 位置。我们可以将这种方法与我们的方向控制器结合起来,创建一个简单的测试对象,编写一个基本的监听器函数,并在 handleController() 函数中包含一些“控制”代码。

local testObj = display.newRect( display.contentCenterX, display.contentCenterY, 20, 20 )
testObj.deltaPerFrame = { 0, 0 }

local function frameUpdate()
    testObj.x = testObj.x + testObj.deltaPerFrame[1]
    testObj.y = testObj.y + testObj.deltaPerFrame[2]
end
Runtime:addEventListener( "enterFrame", frameUpdate )

local function handleController( event )

    local touchOverButton = detectButton( event )

    if ( event.phase == "began" ) then

        if ( touchOverButton ~= nil ) then
            if not ( buttonGroup.touchID ) then
                -- Set/isolate this touch ID
                buttonGroup.touchID = event.id
                -- Set the active button
                buttonGroup.activeButton = touchOverButton
                -- Take proper action based on button ID
                if ( buttonGroup.activeButton.ID == "left" ) then
                    testObj.deltaPerFrame = { -2, 0 }
                elseif ( buttonGroup.activeButton.ID == "right" ) then
                    testObj.deltaPerFrame = { 2, 0 }
                end
            end
            return true
        end

    elseif ( event.phase == "moved" ) then
    elseif ( event.phase == "ended" and buttonGroup.activeButton ~= nil ) then

        -- Release this touch ID
        buttonGroup.touchID = nil
        -- Set that no button is active
        buttonGroup.activeButton = nil
        -- Stop the action
        testObj.deltaPerFrame = { 0, 0 }
        return true
    end
end

让我们更详细地探讨突出显示的代码。

  1. 第 35 和 36 行,我们创建一个简单的测试对象(矢量正方形),位于内容区域的中心。我们还为对象分配一个属性 deltaPerFrame,它是一个包含两个值的表,一个用于 **x**一个用于 **y**。我们的对象将从停止/静止状态开始,因此我们最初将这两个值都设置为 0

  2. 第 38-41 行,我们添加了一个基本函数 (frameUpdate()) 来更新对象的 **x** 和 **y** 位置,基于其 deltaPerFrame 属性中的值。然后,在第 42 行,我们通过添加一个 "enterFrame" 事件监听器来启动该函数在每个运行时帧上运行/执行。

  3. 第 58 和 60 行,我们根据按下的方向按钮更改对象的 deltaPerFrame 属性值。如果按下**左**按钮,则第一个值 (**x**) 设置为 -2,这意味着对象将在每个运行时帧上开始向左移动 2 个像素。类似地,如果按下**右**按钮,我们将第一个值设置为 2,以便对象每帧向右移动 2 个像素。请注意,如果希望对象移动得更快或更慢,可以增加/减少这些值。

  4. 最后,在第 86 行,如果玩家的触摸从方向按钮上漂移,我们将 deltaPerFrame 值重置为 0 以停止对象的移动。

基于物理的移动

连续移动角色/对象的另一种方法是通过**物理引擎**。当然,这假设对象是由物理引擎管理的物理对象,这超出了本教程的范围(如果您需要物理方面的帮助,请从物理设置指南开始)。

在集成基于物理的移动与方向控制器方面,最佳选择通常是设置对象的**线性速度** — 这是因为它以恒定的、稳定的速率将运动应用于对象,而不是堆叠力值或应用瞬时冲量。

让我们调整代码以使用物理和线性速度。

-- Set up physics engine
local physics = require( "physics" )
physics.start()

local testObj = display.newRect( display.contentCenterX, display.contentCenterY, 20, 20 )
physics.addBody( testObj, "kinematic" )

local function handleController( event )

    local touchOverButton = detectButton( event )

    if ( event.phase == "began" ) then

        if ( touchOverButton ~= nil ) then
            if not ( buttonGroup.touchID ) then
                -- Set/isolate this touch ID
                buttonGroup.touchID = event.id
                -- Set the active button
                buttonGroup.activeButton = touchOverButton
                -- Take proper action based on button ID
                if ( buttonGroup.activeButton.ID == "left" ) then
                    testObj:setLinearVelocity( -100, 0 )
                elseif ( buttonGroup.activeButton.ID == "right" ) then
                    testObj:setLinearVelocity( 100, 0 )
                end
            end
            return true
        end

    elseif ( event.phase == "moved" ) then
    elseif ( event.phase == "ended" and buttonGroup.activeButton ~= nil ) then

        -- Release this touch ID
        buttonGroup.touchID = nil
        -- Set that no button is active
        buttonGroup.activeButton = nil
        -- Stop the action
        testObj:setLinearVelocity( 0, 0 )
        return true
    end
end

更深入地探讨突出显示的代码,我们执行以下操作:

  1. 第 36 和 37 行,我们 require() 物理引擎并启动它运行。

  2. 第 39 和 40 行,我们创建一个简单的测试对象(矢量正方形),位于内容区域的中心。我们还通过为其分配运动学类型的物理实体来告诉物理引擎管理此对象。

  3. 第 56 和 58 行,我们使用object:setLinearVelocity()设置对象的线性速度。如果按下 **左** 按钮,我们为 **x** 参数分配一个值 -100,使对象开始向左移动。类似地,如果按下 **右** 按钮,我们为 **x** 参数分配一个值 100,使对象开始向右移动。请注意,如果希望对象移动得更快或更慢,可以增加/减少这些值。

  4. 最后,在第 84 行,如果玩家的触摸从方向按钮上漂移,我们将对象的线性速度值重置为 0 以停止其移动。

总结

希望本教程为在 Solar2D 中处理持续动作提供了一个基础。这种做法可能适用于许多超出所介绍的情况,你会发现,只要 немного 创造力,天空才是极限!