Corona 新开发者可能会遇到的一个挑战是如何处理时间。跟踪时间的方式有很多种 — 你可以读取自
这些主题在 利用时间和日期 教程中进行了详细讨论,因此我们在这里不再赘述 — 相反,本教程将演示如何创建一个可用于多种类型游戏的可视化倒计时器。
让我们从一些基本的设置代码开始
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 )
基本上,我们执行两个简单的任务
我们首先定义一个变量 secondsLeft
,它将保存倒计时剩余的秒数。要确定秒数,只需将分钟数乘以 60
,然后选择性地添加额外的秒数。例如,5 分 45 秒的计时器需要 secondsLeft
值为5*60 + 45
345
。
接下来,我们创建一个 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
让我们更详细地探讨这个函数
此函数的第一步至关重要 — 从 secondsLeft
中减去 1
。如果我们不这样做,计时器就不会倒计时!可选地,我们可以在此行之后添加一个条件测试,以查看 secondsLeft
是否等于 0
,如果是,则触发某个事件,指示时钟已达到零。
要计算分钟数,我们需要反转用于将时间转换为秒数的时间计算,因此我们只需将剩余秒数除以 60
。这将为我们提供一个分数,但我们不需要显示“分钟”部分的分数。因此,我们使用 math.floor() 生成一个整数,然后将该值存储为 minutes
变量。对于 seconds
变量,我们确实需要小数部分,因为这将是显示中的可视“秒数”。这很容易使用模数%
) 来完成 —secondsLeft % 60
为了使时间以典型的时间格式 (MM:SS
) 显示,我们需要格式化字符串。我们可以为此使用 os.date()
,但 string.format() 可以更轻松地完成这项工作。使用它,我们可以指定具有一定格式的占位符,然后传入值来填充这些占位符。出于我们的目的,字符串格式为 "%02d:%02d"
,这可能看起来很神秘,但实际上非常基本。%
符号定义了占位符的开头,02d
表示我们想要一个整数d
代表十进制)2
) 字符空格来显示它。前导 0
表示如果数字太小而无法放入两个字符空间中,则在其前面加上足够的零以匹配请求的格式 — 换句话说,如果我们的 seconds
值为 7
,它将被格式化并显示为 07
。最后,string.format()
每个占位符取一个变量,从左到右填充值。通过首先传入 minutes
,然后传入 seconds
,我们得到传统的 MM:SS
类型显示。
:
不是格式的一部分 — 它只是用来在分钟和秒之间放置一个冒号。
要进一步了解字符串格式,请参阅 格式化字符串值 教程。
string.format()
命令关联的 timeDisplay
值传递给显示对象的 text
属性,以直观地更新其读数。现在我们有了一个更新函数,让我们调用 timer.performWithDelay() 来启动它
-- Run the timer local countDownTimer = timer.performWithDelay( 1000, updateTime, secondsLeft )
对于此命令,第一个参数是计时器第一次执行之前的延迟时间。在这种情况下,我们希望每秒触发一次,但我们需要以毫秒 (1000
) 为单位指定该时间。接下来,每次执行时,我们调用上面的 updateTime
函数。最后,由于我们显然希望计时器在倒计时器的生命周期内每秒执行一次,因此我们指定了之前计算的 secondsLeft
值。
如果运行上面的整个代码示例,你会注意到时间显示正常工作,但在倒计时时视觉对齐方面存在问题。这是因为我们用于文本对象的默认字体 (native.systemFont
) 包含宽度不同的数字字符。例如,1 比 0 或 8 窄,这会导致整个文本字符串在计时器倒计时时 awkward地向左或向右移动。
虽然看起来只需在文本对象上设置 锚点 就可以解决问题,MM:SS
格式的字符会随着计时器倒计时而改变,无论你如何锚定它,显示都会始终移动。
幸运的是,有两种方法可以解决这个问题,每种方法都相当简单。
最简单的解决方案是对文本对象使用等宽字体。本质上,等宽字体中的所有字符(包括数字)都占用完全相同的“宽度”,而不管其视觉外观如何。这将防止计时器显示向左或向右移动,因为就 Corona 而言,等宽字体中的 1 与 0、8 或其他任何内容占用相同的屏幕空间。
可以从各种资源获得许多等宽字体,包括 Google Fonts。获得适合你视觉游戏设计的等宽字体后,你可以按照 使用自定义字体 指南中的简单步骤在 Corona 项目中使用它。
等宽字体的一个潜在问题是,根据字体设计,某些字符周围的水平空间似乎比其他字符多。例如,与“较宽”的数字
这个问题可以通过对计时器显示中的每个元素使用精灵来解决。这允许你创建更具吸引力的视觉读数并且它允许你动态控制每个数字相对于周围数字的位置,有效地使读数的间距更加一致。
要实现精灵,我们首先需要一个类似于下面的图像,其中包含所有 10 个数字以及一个冒号来分隔分钟和秒
创建图像后,让我们修改上面的代码示例
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 )
本教程不会解释上述配置sheet
)。
local digitSequence = { name="digits", start=1, count=11 } local colon = display.newSprite( sheet, digitSequence ) colon.x, colon.y = display.contentCenterX, 80 colon:setFrame( 11 )
同样,本教程不会介绍精灵设置,所有这些都在 精灵动画 指南中进行了概述。基本上,我们定义了一个简单的digitSequence
),创建我们的第一个精灵 (colon
),将其定位在屏幕上,然后将其帧设置为 11
(请注意,冒号是图像表中的第 11 帧)。
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()
函数中使用修改来设置每个数字的帧和位置。但是请注意,我们确实为这四个对象指定了锚点,这是一个重要的方面,可确保每个数字与其周围元素保持适当的间距。
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
本质上,第一个块确定时间读数中每个数字的值,并将它们相应地设置为四个变量md
、ms
、sd
和 ss
)timeDisplay
字符串是 05:36
,则这些变量将是0
、5
、3
和 6
使用这四个值,每个精灵的帧将被设置为正确的值。需要注意的是,对于每个值,如果变量等于 0
,我们将帧设置为 10
,这实际上是图像表中的0数字。否则,我们将帧设置为变量的值,该值与图像表中的表示形式相对应。
在第二个代码块中,我们重新定位所有四个数字,从中心的冒号向外移动。基本上,minutesSingle
表示冒号左侧的数字。该数字被推到冒号的左侧,然后 minutesDouble
被推到 minutesSingle
的左侧。类似地,secondsDouble
表示冒号右侧的数字,因此它被推到冒号的右侧。最后,secondsSingle
被推到 secondsDouble
的右侧。所有这些看起来可能很复杂,但是使用 contentBounds
属性和锚点使其表现良好,消除了计时器倒计时时数字之间的多余间距。
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 秒后开始倒计时。通过这些修改,我们现在有了一个彩色
虽然处理时间起初可能看起来很困难,但在许多情况下,这只是简单的整数数学和基本