创建倒计时器

Corona 新开发者可能会遇到的一个挑战是如何处理时间。跟踪时间的方式有很多种  — 你可以读取自1970 年 1 月 1 日以来的秒数(os.time()),可以使用应用程序启动以来的微秒计时器(system.getTimer()),或者你可以另辟蹊径,使用 enterFrame 监听器来跟踪应用程序的“滴答声”。当然,还有 timer API,允许你在未来的特定时间触发事件。

这些主题在 利用时间和日期 教程中进行了详细讨论,因此我们在这里不再赘述  — 相反,本教程将演示如何创建一个可用于多种类型游戏的可视化倒计时器

初始设置

让我们从一些基本的设置代码开始

local secondsLeft = 600  -- 10 minutes * 60 seconds

local clockText = display.newText( "10:00", display.contentCenterX, 80, native.systemFont, 72 )
clockText:setFillColor( 0.7, 0.7, 1 )

基本上,我们执行两个简单的任务

  1. 我们首先定义一个变量 secondsLeft,它将保存倒计时剩余的秒数。要确定秒数,只需将分钟数乘以 60,然后选择性地添加额外的秒数。例如,5 分 45 秒的计时器需要 secondsLeft 值为5*60 + 45,即 345

  2. 接下来,我们创建一个 display.newText() 对象(clockText)来在屏幕上绘制剩余时间。每次我们更改时间时,我们都会更新此对象的 text 属性以直观地更改其读数。

时间更新函数

如果运行上面的代码,你会在屏幕上看到一个大的 10:00 显示。这是一个好的开始,但它实际上什么也没做!让我们先编写一个更新可视时间显示的函数来解决这个问题

local function updateTime( event )

    -- Decrement the number of seconds
    secondsLeft = secondsLeft - 1

    -- Time is tracked in seconds; convert it to minutes and seconds
    local minutes = math.floor( secondsLeft / 60 )
    local seconds = secondsLeft % 60

    -- Make it a formatted string
    local timeDisplay = string.format( "%02d:%02d", minutes, seconds )
    
    -- Update the text object
    clockText.text = timeDisplay
end

让我们更详细地探讨这个函数

  1. 此函数的第一步至关重要  — 从 secondsLeft 中减去 1。如果我们不这样做,计时器就不会倒计时!可选地,我们可以在此行之后添加一个条件测试,以查看 secondsLeft 是否等于 0,如果是,则触发某个事件,指示时钟已达到零。

  2. 要计算分钟数,我们需要反转用于将时间转换为秒数的时间计算,因此我们只需将剩余秒数除以 60。这将为我们提供一个分数,但我们不需要显示“分钟”部分的分数。因此,我们使用 math.floor() 生成一个整数,然后将该值存储为 minutes 变量。对于 seconds 变量,我们确实需要小数部分,因为这将是显示中的可视“秒数”。这很容易使用模数运算符 (%) 来完成 —本质上secondsLeft % 60将为我们提供不带分钟的秒数。

  3. 为了使时间以典型的时间格式 (MM:SS) 显示,我们需要格式化字符串。我们可以为此使用 os.date(),但 string.format() 可以更轻松地完成这项工作。使用它,我们可以指定具有一定格式的占位符,然后传入值来填充这些占位符。出于我们的目的,字符串格式为 "%02d:%02d",这可能看起来很神秘,但实际上非常基本。% 符号定义了占位符的开头,02d 表示我们想要一个整数d 代表十进制并使用两个 (2) 字符空格来显示它。前导 0 表示如果数字太小而无法放入两个字符空间中,则在其前面加上足够的零以匹配请求的格式  — 换句话说,如果我们的 seconds 值为 7,它将被格式化并显示为 07。最后,string.format() 每个占位符取一个变量,从左到右填充值。通过首先传入 minutes,然后传入 seconds,我们得到传统的 MM:SS 类型显示。

注意
  • : 不是格式的一部分  — 它只是用来在分钟和秒之间放置一个冒号。

  • 要进一步了解字符串格式,请参阅 格式化字符串值 教程。

  1. 最后,我们将与 string.format() 命令关联的 timeDisplay 值传递给显示对象的 text 属性,以直观地更新其读数。

运行计时器

现在我们有了一个更新函数,让我们调用 timer.performWithDelay() 来启动它

-- Run the timer
local countDownTimer = timer.performWithDelay( 1000, updateTime, secondsLeft )

对于此命令,第一个参数是计时器第一次执行之前的延迟时间。在这种情况下,我们希望每秒触发一次,但我们需要以毫秒 (1000) 为单位指定该时间。接下来,每次执行时,我们调用上面的 updateTime 函数。最后,由于我们显然希望计时器在倒计时器的生命周期内每秒执行一次,因此我们指定了之前计算的 secondsLeft 值。

修复视觉对齐

如果运行上面的整个代码示例,你会注意到时间显示正常工作,但在倒计时时视觉对齐方面存在问题。这是因为我们用于文本对象的默认字体 (native.systemFont) 包含宽度不同的数字字符。例如,108 窄,这会导致整个文本字符串在计时器倒计时时 awkward地向左或向右移动。

虽然看起来只需在文本对象上设置 锚点 就可以解决问题,但它不会  —因为最左边最右边MM:SS 格式的字符会随着计时器倒计时而改变,无论你如何锚定它,显示都会始终移动。

幸运的是,有两种方法可以解决这个问题,每种方法都相当简单。

等宽字体

最简单的解决方案是对文本对象使用等宽字体。本质上,等宽字体中的所有字符(包括数字)都占用完全相同的“宽度”,而不管其视觉外观如何。这将防止计时器显示向左或向右移动,因为就 Corona 而言,等宽字体中的 108 或其他任何内容占用相同的屏幕空间。

可以从各种资源获得许多等宽字体,包括 Google Fonts。获得适合你视觉游戏设计的等宽字体后,你可以按照 使用自定义字体 指南中的简单步骤在 Corona 项目中使用它。

基于精灵的显示

等宽字体的一个潜在问题是,根据字体设计,某些字符周围的水平空间似乎比其他字符多。例如,与“较宽”的数字08相比,数字 1 在周围数字之间似乎有太多的空间,这使得计时器显示看起来有点奇怪。

这个问题可以通过对计时器显示中的每个元素使用精灵来解决。这允许你创建更具吸引力的视觉读数并且它允许你动态控制每个数字相对于周围数字的位置,有效地使读数的间距更加一致。

要实现精灵,我们首先需要一个类似于下面的图像,其中包含所有 10 个数字以及一个冒号来分隔分钟和秒

创建图像后,让我们修改上面的代码示例

  1. 首先,删除 clockText 文本对象并设置一个图像表
local secondsLeft = 10 * 60  -- 10 minutes * 60 seconds

local sheetOptions = {
    frames = {
        { x=0, y=0, width=24, height=48 },    -- 1
        { x=24, y=0, width=34, height=48 },   -- 2
        { x=58, y=0, width=28, height=48 },   -- 3
        { x=86, y=0, width=36, height=48 },   -- 4
        { x=122, y=0, width=30, height=48 },  -- 5
        { x=152, y=0, width=38, height=48 },  -- 6
        { x=190, y=0, width=34, height=48 },  -- 7
        { x=224, y=0, width=36, height=48 },  -- 8
        { x=260, y=0, width=38, height=48 },  -- 9
        { x=298, y=0, width=40, height=48 },  -- 0
        { x=338, y=0, width=22, height=48 }   -- :
    },
    sheetContentWidth = 360,
    sheetContentHeight = 48
}
local sheet = graphics.newImageSheet( "timer-digits.png", sheetOptions )

本教程不会解释上述配置的细节  —如果你需要进一步了解该主题,请参阅 图像表 指南。本质上,这段新代码定义了图像表的(共 11 个)并创建了一个图像表 (sheet)。

  1. 接下来,定义一个基本的精灵序列并在屏幕上创建/定位冒号对象
local digitSequence = { name="digits", start=1, count=11 }

local colon = display.newSprite( sheet, digitSequence )
colon.x, colon.y = display.contentCenterX, 80
colon:setFrame( 11 )

同样,本教程不会介绍精灵设置,所有这些都在 精灵动画 指南中进行了概述。基本上,我们定义了一个简单的11 帧序列 (digitSequence),创建我们的第一个精灵 (colon),将其定位在屏幕上,然后将其设置为 11(请注意,冒号是图像表中的第 11 帧)。

  1. 在本例中,冒号对象将保持“锁定”在适当位置,所有其他数字都将围绕它定位。我们需要 4 个额外的数字,所以我们现在创建它们
local minutesSingle = display.newSprite( sheet, digitSequence )
minutesSingle.x, minutesSingle.y = 0, 80
minutesSingle.anchorX = 1

local minutesDouble = display.newSprite( sheet, digitSequence )
minutesDouble.x, minutesDouble.y = 0, 80
minutesDouble.anchorX = 1

local secondsDouble = display.newSprite( sheet, digitSequence )
secondsDouble.x, secondsDouble.y = 0, 80
secondsDouble.anchorX = 0

local secondsSingle = display.newSprite( sheet, digitSequence )
secondsSingle.x, secondsSingle.y = 0, 80
secondsSingle.anchorX = 0

前两个对象代表分钟数字,后两个对象代表。不用担心每个对象的初始 x 位置为 0  — 在此增强版本中,我们实际上将在现有的 updateTime() 函数中使用修改来设置每个数字的帧位置。但是请注意,我们确实为这四个对象指定了锚点,这是一个重要的方面,可确保每个数字与其周围元素保持适当的间距。

  1. 现在让我们开始修改 updateTime() 函数以利用我们到目前为止所做的更改。首先,用一个简单的条件语句包围 secondsLeft 计算  — 这将防止 secondsLeft 在步骤 5 中执行的计时器显示的第一次“初始化”时递减。
local function updateTime( event )

    if ( event ~= "init" ) then
        -- Decrement the number of seconds
        secondsLeft = secondsLeft - 1
    end

接下来,在函数的下方,删除clockText.text = timeDisplay命令,因为该对象不再存在。在其位置添加以下突出显示的代码块

    -- Time is tracked in seconds; convert it to minutes and seconds
    local minutes = math.floor( secondsLeft / 60 )
    local seconds = secondsLeft % 60

    -- Make it a formatted string
    local timeDisplay = string.format( "%02d:%02d", minutes, seconds )
    
    -- Get the individual new value of each element in time display
    local md = tonumber( string.sub( timeDisplay, 1, 1 ) )
    local ms = tonumber( string.sub( timeDisplay, 2, 2 ) )
    local sd = tonumber( string.sub( timeDisplay, 4, 4 ) )
    local ss = tonumber( string.sub( timeDisplay, 5, 5 ) )
    if ( md == 0 ) then minutesDouble:setFrame( 10 ) else minutesDouble:setFrame( md ) end
    if ( ms == 0 ) then minutesSingle:setFrame( 10 ) else minutesSingle:setFrame( ms ) end
    if ( sd == 0 ) then secondsDouble:setFrame( 10 ) else secondsDouble:setFrame( sd ) end
    if ( ss == 0 ) then secondsSingle:setFrame( 10 ) else secondsSingle:setFrame( ss ) end

    -- Reposition digits around central colon
    minutesSingle.x = colon.contentBounds.xMin
    minutesDouble.x = minutesSingle.contentBounds.xMin
    secondsDouble.x = colon.contentBounds.xMax
    secondsSingle.x = secondsDouble.contentBounds.xMax
end

本质上,第一个块确定时间读数中每个数字的值,并将它们相应地设置为四个变量mdmssdss。例如,如果格式化的 timeDisplay 字符串是 05:36,则这些变量将是0536分别。

使用这四个值,每个精灵的将被设置为正确的值。需要注意的是,对于每个值,如果变量等于 0,我们将帧设置为 10,这实际上是图像表中的0数字。否则,我们将帧设置为变量的值,该值与图像表中的表示形式相对应。

在第二个代码块中,我们重新定位所有四个数字,从中心的冒号向外移动。基本上,minutesSingle 表示冒号左侧的数字。该数字被推到冒号的左侧,然后 minutesDouble 被推到 minutesSingle 的左侧。类似地,secondsDouble 表示冒号右侧的数字,因此它被推到冒号的右侧。最后,secondsSingle 被推到 secondsDouble 的右侧。所有这些看起来可能很复杂,但是使用 contentBounds 属性和锚点使其表现良好,消除了计时器倒计时时数字之间的多余间距。

  1. 最后的修改是“初始化”计时器显示。虽然我们可以在步骤 3 中完成此操作,但这会导致重复设置帧和定位代码,而我们刚刚将这些代码添加到 updateTime() 函数中。因此,我们将使用一个小技巧,在计时器开始之前显式运行 updateTime() — 但是,由于我们不希望此初始化从 secondsLeft 变量中减去 1,因此我们将字符串值 "init" 传递给函数。
-- Initialize timer to set/position the digit sprites
updateTime( "init" )

-- Run the timer
local countDownTimer = timer.performWithDelay( 1000, updateTime, secondsLeft )

正如您所记得的,我们在步骤 4(第 46 行)中为此添加了一个条件阻塞器。此 "init" 参数将在我们调用第 76 行上的 updateTime() 函数时存在,而不是在第 79 行上的 countDownTimer 计时器调用该函数时存在。因此,我们有效地将 updateTime() 函数用于两个目的:将计时器显示初始化为第 1 行设置的值,以及在计时器倒计时时设置每个数字的帧/位置。

就是这样!最后一行保持不变,计时器将在 1 秒后开始倒计时。通过这些修改,我们现在有了一个彩色精灵渲染的计时器显示,更好的是,对齐和间距不一致的问题都解决了!

结论

虽然处理时间起初可能看起来很困难,但在许多情况下,这只是简单的整数数学和基本基于时间的API 的使用。希望本教程为您入门奠定了基础!