音频系统
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插值,几行代码就能实现该效果,这里就不展开介绍了。