音效和背景音乐是游戏体验的重要组成部分。 正确使用这些组件可以将枯燥的游戏变成引人入胜的冒险!
在 Solar2D 应用中加载音频有两种方法。 使用哪一种方法通常取决于音频文件将如何被利用。
第一种方法是使用 audio.loadSound()
命令。 这将加载并
local explosionSound = audio.loadSound( "explosion.wav" )
加载完成后,可以使用 audio.play()
命令以及通过 audio.loadSound()
创建的音频**句柄** 播放声音,次数不限。
这是理解的关键方面 — 您**不**通过直接指定文件名来播放音频文件。 而是指定分配给 audio.loadSound()
的**句柄**变量。
例如,如果我们的游戏中有四个物体同时爆炸,并且每个物体都需要播放 explosion.wav
声音,我们可以发出这些命令
audio.play( explosionSound ) audio.play( explosionSound ) audio.play( explosionSound ) audio.play( explosionSound )
换句话说,无需使用 audio.loadSound()
多次预加载相同的音频文件 — 使用上述命令,explosion.wav
声音将播放四次,并且默认情况下,每个实例都将分配给不同的音频**通道**。 然后,一旦每个实例播放完毕,音频系统将释放/清除其通道,以便可以在其上播放其他声音。
audio.loadSound()
方法非常方便,但是如果您同时加载大型音频文件或大量音频文件,则在加载时可能会出现明显的暂停/跳过。 因此,如果您需要加载大型音频文件(例如背景音乐曲目),通常最好使用下一节中讨论的**流式播放**方法。
将音频加载到应用程序中的第二种方法是 audio.loadStream()
。 这将根据需要逐渐加载和处理音频文件的小块。 此命令最适合在可能的延迟不会对应用程序的可用性产生重大影响的情况下使用。 流式播放不会占用太多内存,因此它通常是大型音频文件(例如背景音乐)的最佳选择。
local backgroundMusic = audio.loadStream( "musicTrack1.wav" )
与 audio.loadSound()
不同,使用 audio.loadStream()
加载的音频文件一次只能在一个通道上播放。 如果您需要在多个通道上流式传输相同的音频文件,则需要加载两个不同的音频句柄,例如
local backgroundMusic1 = audio.loadStream( "musicTrack1.wav" ) local backgroundMusic2 = audio.loadStream( "musicTrack1.wav" )
让我们为我们的游戏添加音效! 首先,您需要下载示例音频文件,由 Eric Matyas 提供。 在本章 源文件 的 audio
子文件夹中,您将找到以下音频文件
文件 | 用途 |
---|---|
Escape_Looping.wav |
菜单场景的音乐。 |
explosion.wav |
小行星被击中时的音效。 |
fire.wav |
飞船发射激光时的音效。 |
80s-Space-Game_Looping.wav |
游戏玩法的主要配乐。 |
Midnight-Crawlers_Looping.wav |
高分场景的音乐。 |
对于此项目,将整个 audio
子文件夹及其所有内容复制到您的 StarExplorer
项目文件夹中。 如果您计划在游戏中使用多个音频文件,将它们组织在一个子文件夹中会很有帮助。
首先,我们需要加载声音。 由于我们的音效只会在游戏过程中出现,因此我们可以在 game.lua
中加载它们
local backGroup local mainGroup local uiGroup local explosionSound local fireSound local function updateText() livesText.text = "Lives: " .. lives scoreText.text = "Score: " .. score end
scene:create()
函数,并在其 end
行之前添加以下突出显示的命令ship:addEventListener( "tap", fireLaser ) ship:addEventListener( "touch", dragShip ) explosionSound = audio.loadSound( "audio/explosion.wav" ) fireSound = audio.loadSound( "audio/fire.wav" ) end
现在,当场景首次加载时,声音文件将被加载到变量句柄 explosionSound
和 fireSound
中。
请注意,我们不是仅指定文件名,而是在其后附加 audio/
,因为我们的音频文件位于 audio
子文件夹中。
加载声音后,我们现在可以在需要时使用 audio.play()
播放它们
onCollision()
函数中检测到,因此让我们在第一个条件块中添加一个 audio.play()
命令,紧跟在移除激光和小行星的 display.remove()
命令之后local function onCollision( event ) 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 -- Remove both the laser and asteroid display.remove( obj1 ) display.remove( obj2 ) -- Play explosion sound! audio.play( explosionSound ) for i = #asteroidsTable, 1, -1 do if ( asteroidsTable[i] == obj1 or asteroidsTable[i] == obj2 ) then table.remove( asteroidsTable, i ) break end end
onCollision()
函数的第二个条件子句检测到,因此让我们在died = true
audio.play()
命令elseif ( ( obj1.myName == "ship" and obj2.myName == "asteroid" ) or ( obj1.myName == "asteroid" and obj2.myName == "ship" ) ) then if ( died == false ) then died = true -- Play explosion sound! audio.play( explosionSound ) -- Update lives lives = lives - 1 livesText.text = "Lives: " .. lives if ( lives == 0 ) then display.remove( ship ) timer.performWithDelay( 2000, endGame ) else ship.alpha = 0 timer.performWithDelay( 1000, restoreShip ) end end end end end
fireLaser()
函数的开头,添加另一个 audio.play()
命令local function fireLaser() -- Play fire sound! audio.play( fireSound ) 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 newLaser:toBack() transition.to( newLaser, { y=-40, time=500, onComplete = function() display.remove( newLaser ) end } ) end
game.lua
文件。为了进一步增强游戏,让我们添加背景音乐。 在我们游戏的整个跨度中,有三个场景。 我们可以为每个场景播放相同的音乐,但最好在每个场景中播放不同的音轨并为动作设定基调。 例如,在菜单场景中,可以播放更被动的曲目,但是当我们进入动作激烈的游戏场景时,播放
如上所述,背景音乐文件往往很大,因此最好使用 audio.loadStream()
。 让我们使用类似于音效的方法
game.lua
的代码区域中,您已经声明了两个声音的前向引用,为音乐曲目添加一个前向引用scene:create()
函数的末尾附近,您调用 audio.loadSound()
加载了两个音效,添加一个 audio.loadStream()
命令以开始在您刚刚声明的 musicTrack
变量上流式传输音乐local explosionSound local fireSound local musicTrack
explosionSound = audio.loadSound( "audio/explosion.wav" ) fireSound = audio.loadSound( "audio/fire.wav" ) musicTrack = audio.loadStream( "audio/80s-Space-Game_Looping.wav") end
要做到这一点,我们需要为音乐的 audio.play()
命令提供更多信息,并在使用专用通道之前做一些额外的工作。
首先,为了在整个游戏中为音乐保留一个通道,让我们向 main.lua
文件添加一个简单的命令。 在您选择的编辑器中打开该文件,并在
命令之前,添加以下内容
local composer = require( "composer" ) -- Hide status bar display.setStatusBar( display.HiddenStatusBar ) -- Seed the random number generator math.randomseed( os.time() ) -- Reserve channel 1 for background music audio.reserveChannels( 1 )
audio.reserveChannels( 1 )命令告诉 Solar2D 音频库保留通道
1
。 保留后,除非我们明确命令它,否则不会在通道上播放任何音频文件。1
的整体音量。 当您从
这实质上是告诉音频系统以
-- Reserve channel 1 for background music audio.reserveChannels( 1 ) -- Reduce the overall volume of the channel audio.setVolume( 0.5, { channel=1 } )
50% 音量 (0.5
)1
上的任何音频文件。 如果您觉得音乐相对于音效而言太大声或太安静,您可能需要在游戏中调整此音量。main.lua
并返回到编辑器中的 game.lua
。 当场景完全显示在屏幕上时,我们将开始播放音乐,因此在 scene:show()
函数的 "did"
阶段条件中,添加以下行
audio.play()
命令只是开始播放音乐。 它类似于我们播放音效的方式,只是它包含一个 Lua 表作为第二个参数,其中包含该命令的选项。 具体来说,channel=1
指示音频库在通道 1
上显式播放音乐,并且-- show() function scene:show( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is still off screen (but is about to come on screen) elseif ( phase == "did" ) then -- Code here runs when the scene is entirely on screen physics.start() Runtime:addEventListener( "collision", onCollision ) gameLoopTimer = timer.performWithDelay( 500, gameLoop, 0 ) -- Start the music! audio.play( musicTrack, { channel=1, loops=-1 } ) end end
loops=-1告诉音频系统无限重复(循环)该文件。
main.lua
和 game.lua
文件,然后重新启动模拟器。 玩游戏,您现在应该听到循环的音乐曲目以及音效。
与通常很短并在完成后从其通道中清除的音效不同,流式音乐通常应在您即将离开场景的适当时间停止。 这可以在 scene:hide()
函数的 `"did"` 阶段条件中轻松处理。 在 `game.lua` 中找到此代码块并添加
audio.stop( 1 )就是这样! 当场景完全离开屏幕时,通道 `1` 上播放的音乐将停止,为下一个场景中在该通道上播放不同的音乐曲目扫清道路。
audio.play()
命令
-- hide() function scene:hide( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is on screen (but is about to go off screen) timer.cancel( gameLoopTimer ) elseif ( phase == "did" ) then -- Code here runs immediately after the scene goes entirely off screen Runtime:removeEventListener( "collision", onCollision ) physics.pause() -- Stop the music! audio.stop( 1 ) composer.removeScene( "game" ) end end
无论是预加载还是流式传输,音频都会占用内存,而且它是一种**不会**由 Composer 自动管理或清理的资源。 因此,本章的最后一个重要步骤是:**处置音频**。 这就是 scene:destroy()
函数派上用场的地方,因为它是由调用 composer.removeScene() 或 Composer 本身销毁场景时触发的。
在您的 `game.lua` 文件中,找到底部的 scene:destroy()
函数。 在其中,添加三个 audio.dispose()
命令,如下所示
使用这些命令,我们有效地释放了音频文件占用的内存。
-- destroy() function scene:destroy( event ) local sceneGroup = self.view -- Code here runs prior to the removal of scene's view -- Dispose audio! audio.dispose( explosionSound ) audio.dispose( fireSound ) audio.dispose( musicTrack ) end
与 `audio.play()` 类似,请注意,我们向 audio.dispose()
命令提供了一个音频**句柄**,例如 explosionSound
。 您**不**应尝试通过简单地指示音频文件名来处置音频。
尽管我们已经在本章的 源文件 中为您完成了此操作,但请挑战自己在其他两个场景中实现音乐,使用您在上面学到的相同音频技术和场景事件概念!
场景
音乐文件 | menu.lua |
---|---|
highscores.lua |
Escape_Looping.wav |
Escape_Looping.wav |
Midnight-Crawlers_Looping.wav |
Midnight-Crawlers_Looping.wav
我们在本章中涵盖了几个与音频相关的概念 | 命令/属性 |
---|---|
描述 | audio.loadSound() |
将整个文件完全加载到内存中,并返回对音频数据的引用。 | audio.loadStream() |
加载(打开)要作为流式音频读取的文件。 | audio.reserveChannels() |
保留一定数量的通道,以便它们不会被自动分配播放。 | audio.setVolume() |
设置特定通道的音量,或设置主音量。 | audio.play() |
在通道上播放音频句柄指定的音频。 | 停止某个通道上的播放。 |
audio.dispose() | 释放与句柄关联的音频内存。 |
⟨ 第六章 — 实现高分榜 | 第八章 — 部署 ⟩