音频系统

MonoGame中,音频API把声音分为两类,SoundEffect(音效)和Song(音乐),二者各自对应不同的使用场景、加载方式和播放API。这篇笔记我们对二者的分工及如何使用进行简单介绍。

SoundEffect vs Song

SoundEffect:SoundEffect的定位是短促音效,比如游戏中的枪声、碰撞声等,它会完整解码并加载到内存中,播放延迟极低,多个音效可以同时播放。音效通常使用wav格式。

Song:Song是背景长音乐(互联网上其实大家都叫BGM),它是流式解码和播放的,播放延迟比音效高,此外MonoGame中同时只能播放一个音乐。音乐常用mp3、ogg格式。

二者划分的逻辑很清晰,音效要求即时触发、多路并发,十个敌人同时开枪就得有十个枪声,所以全解码进内存随取随用;音乐则是单路长时间播放,遵循一般流媒体文件的处理方式,流式解码和播放。

SoundEffect

使用音效需要将.wav文件加入MGCB内容管道,然后我们就可以使用ContentManager的Load加载并播放它了,音效是SoundEffect类型,调用Play方法即可播放。下面例子代码中,我们实现了点击鼠标播放枪声音效功能。

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGame.Extended.Input;

namespace Gacfox.Demo.DemoGame;

public class Game1 : Game
{
    private GraphicsDeviceManager _graphics;
    private SpriteBatch _spriteBatch;

    private SoundEffect _rifleFireSoundEffect;

    public Game1()
    {
        _graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
        IsMouseVisible = true;
    }

    protected override void Initialize()
    {
        base.Initialize();
    }

    protected override void LoadContent()
    {
        _spriteBatch = new SpriteBatch(GraphicsDevice);

        _rifleFireSoundEffect = Content.Load<SoundEffect>("rifleFire");
    }

    protected override void Update(GameTime gameTime)
    {
        MouseExtended.Update();

        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
            Exit();

        if (MouseExtended.GetState().WasButtonPressed(MouseButton.Left))
        {
            _rifleFireSoundEffect.Play();
        }

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        base.Draw(gameTime);
    }
}

Play方法有多个重载,完整版有3个参数。

_jumpSound.Play(
    volume: 0.8f,  // 音量,取值0~1,默认值1f
    pitch: 0.1f,   // 音调,控制声音的播放频率和速度,取值-1~1,±1为升降八度,标准值是1f
    pan: -0.5f     // 声像,控制声音在立体声或环绕声场中的左右位置分布,取值-1(完全左声道)~1(完全右声道),标准值0是居中
);

pitch参数有个经典的实战技巧,给同类音效加一点随机音调偏移,能有效避免重复感。比如脚步声明明只有一个音频文件,每次播放时pitch随机取-0.05到0.05之间的值,听起来就像每一步都略有不同,这样可以增加环境音效真实感。

float pitch = (float)(_random.NextDouble() * 0.1 - 0.05);
_rifleFireSoundEffect.Play(1f, pitch, 0f);

此外,关于音量,MonoGame中也可以通过一个静态属性SoundEffect.MasterVolume全局调整音效音量,这非常适合用于实现音效音量滑块的控制逻辑。

SoundEffect.MasterVolume = 0.5f;

SoundEffectInstance

前面介绍的Play方法是触发后就不管了的一次性播放,如果需要循环播放、暂停、中途停止,就需要创建SoundEffectInstance实例对象。下面例子代码中,我们实现按住鼠标循环播放音效,松开鼠标立即停止,此外还加入了之前的随机音调机制。

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGame.Extended.Input;
using System;

namespace Gacfox.Demo.DemoGame;

public class Game1 : Game
{
    private GraphicsDeviceManager _graphics;
    private SpriteBatch _spriteBatch;

    private readonly Random _random = new();
    private SoundEffect _rifleFireSoundEffect;
    private SoundEffectInstance _rifleFireInstance;

    public Game1()
    {
        _graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
        IsMouseVisible = true;
    }

    protected override void Initialize()
    {
        base.Initialize();
    }

    protected override void LoadContent()
    {
        _spriteBatch = new SpriteBatch(GraphicsDevice);

        _rifleFireSoundEffect = Content.Load<SoundEffect>("rifleFire");
        _rifleFireInstance = _rifleFireSoundEffect.CreateInstance();
        _rifleFireInstance.IsLooped = true;
    }

    protected override void Update(GameTime gameTime)
    {
        MouseExtended.Update();

        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
            Exit();

        MouseStateExtended mouseState = MouseExtended.GetState();
        if (mouseState.WasButtonPressed(MouseButton.Left))
        {
            _rifleFireInstance.Pitch = (float)(_random.NextDouble() * 0.1 - 0.05);
            _rifleFireInstance.Play();
        }
        if (mouseState.WasButtonReleased(MouseButton.Left))
        {
            _rifleFireInstance.Stop();
        }

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        base.Draw(gameTime);
    }
}

当然,对于枪声上面代码实现方式可能不太理想,它比较适合持续性的激光枪之类的声音,如果想实现真正按住扫射但每发子弹独立音效的效果,可以用定时器在按住期间每隔N毫秒Play一次来实现,这比上面代码实现的方式更可控。

3D音效

即使是2D游戏中,声音同样有方位感的需求,例如右侧传来爆炸声、远处的瀑布声若隐若现等。SoundEffectInstance.Apply3D可以基于发射者(Emitter)和听者(Listener)的位置计算音量衰减和声像。下面代码中,Position是三维向量(对于2D游戏,Z固定设置为0即可),在音源就绪后调用Apply3D方法,3D空间参数能立刻正确应用。

var listener = new AudioListener { Position = playerPosition };
var emitter = new AudioEmitter { Position = waterfallPosition };

_waterfallInstance.Play();
_waterfallInstance.Apply3D(listener, emitter);

MediaPlayer

对于音乐的.mp3和.ogg文件,我们通常需要将其加入MGCB内容管道,音乐使用MediaPlayer静态类播放。下面例子代码中,点击鼠标可以开始播放音乐。

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using MonoGame.Extended.Input;

namespace Gacfox.Demo.DemoGame
{
    public class Game1 : Game
    {
        private GraphicsDeviceManager _graphics;
        private SpriteBatch _spriteBatch;

        private Song _bgm;

        public Game1()
        {
            _graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            IsMouseVisible = true;
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        protected override void LoadContent()
        {
            _spriteBatch = new SpriteBatch(GraphicsDevice);

            _bgm = Content.Load<Song>("music");
        }

        protected override void Update(GameTime gameTime)
        {
            MouseExtended.Update();

            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            if (MouseExtended.GetState().WasButtonPressed(MouseButton.Left))
            {
                if (MediaPlayer.State != MediaState.Playing)
                {
                    MediaPlayer.Play(_bgm);
                }
            }

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            base.Draw(gameTime);
        }
    }
}

MonoGame中,MediaPlayer是全局唯一的音乐播放器,它同时只能播一首Song,Play新音乐就会自动顶掉旧的音乐,多次Play同一个音乐效果就是多次完全从头开始播放同一个音乐,这种设计意味着我们不需要管理音乐实例,但也做不了一些复杂的多音轨效果了,不过对绝大多数独立游戏来说MediaPlayer也已经绰绰有余。此外,MediaPlayer没有内置淡入淡出,但用Volume配合Update插值,几行代码就能实现该效果,这里就不展开介绍了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。