第 7 章 — 声音和音乐

上一页 | 下一页

音效和背景音乐是游戏体验的重要组成部分。 正确使用这些组件可以将枯燥的游戏变成引人入胜的冒险!

预加载和流式播放

在 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 中加载它们

  1. 首先,在场景可访问的代码区域,您已经预先声明了一些变量,添加以下前向引用
local backGroup
local mainGroup
local uiGroup

local explosionSound
local fireSound


local function updateText()
    livesText.text = "Lives: " .. lives
    scoreText.text = "Score: " .. score
end
  1. 接下来,找到 scene:create() 函数,并在其 end 行之前添加以下突出显示的命令
    ship:addEventListener( "tap", fireLaser )
    ship:addEventListener( "touch", dragShip )

    explosionSound = audio.loadSound( "audio/explosion.wav" )
    fireSound = audio.loadSound( "audio/fire.wav" )
end

现在,当场景首次加载时,声音文件将被加载到变量句柄 explosionSoundfireSound 中。

请注意,我们不是仅指定文件名,而是在其后附加 audio/,因为我们的音频文件位于 audio 子文件夹中。

播放声音

加载声音后,我们现在可以在需要时使用 audio.play() 播放它们

  1. 每当激光击中小行星时都应该播放爆炸声。 该事件在 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
  1. 每当小行星撞到飞船时也应该播放爆炸声。 该事件由 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
  1. 最后,让我们添加发射激光器的音效。 在 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
  1. 保存您修改后的 game.lua 文件。

添加背景音乐

为了进一步增强游戏,让我们添加背景音乐。 在我们游戏的整个跨度中,有三个场景。 我们可以为每个场景播放相同的音乐,但最好在每个场景中播放不同的音轨并为动作设定基调。 例如,在菜单场景中,可以播放更被动的曲目,但是当我们进入动作激烈的游戏场景时,播放快节奏的音轨会有所帮助。 最后,当游戏结束时,您可能想要一些更庄严的东西。

加载音乐

如上所述,背景音乐文件往往很大,因此最好使用 audio.loadStream()。 让我们使用类似于音效的方法

  1. game.lua 的代码区域中,您已经声明了两个声音的前向引用,为音乐曲目添加一个前向引用场景可访问的现在,在 scene:create() 函数的末尾附近,您调用 audio.loadSound() 加载了两个音效,添加一个 audio.loadStream() 命令以开始在您刚刚声明的 musicTrack 变量上流式传输音乐
local explosionSound
local fireSound
local musicTrack
  1. 现在是播放音乐的时候了! 这次我们将更深入地了解音频设置与**通道管理**。 基本上,对于我们的音效,我们只是让音频库选择一个空闲通道来播放任何新的声音实例。 然而,对于音乐,通常可以保留一个特定通道并在该通道上播放所有背景音乐 — 毕竟,您不太可能希望同时播放多个音乐文件,并且它们会相互重叠并发生音频冲突。 通过为音乐保留一个专用通道,我们可以在整个游戏中将其用于所有背景音乐。
    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 文件添加一个简单的命令。 在您选择的编辑器中打开该文件,并在

  1. composer.gotoScene( "menu" )命令之前,添加以下内容基本上,
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 的整体音量。 当您从

  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" 阶段条件中,添加以下行

  1. 这个 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.luagame.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() 释放与句柄关联的音频内存。