Audio3D

Page updated :

The page you are currently viewing does not support the selected display language.

本 Tips の注意点

このサンプルは以下のサイトで公開されているプログラムをもとに解説しています。 分かりやすいように若干コードを変えたり日本語で説明しているようにしていますが、 基本的には元コードをそのまま使用しているので、実際にゲームプログラムに採用する場合は適時修正して使用してください。

参考元サイト

また、MonoGame や XNA についてある程度基本知識を持っている前提で説明していますので 初歩的な部分については MonoGame TipsXNA Tips を参照してください。

特に数学としてベクトル三角関数行列などは必須なのである程度どういうものかは知っておいてください。

環境

プラットフォーム
  • Windows 10
  • コードを流用すれば MonoGame 対応の他のプラットフォームでも動作可
Visual Studio
  • Visual Studio 2019
.NET Core
  • 3.1
MonoGame
  • 3.8

サンプルについて

カメラの位置をもとに、犬の位置猫の位置 からそれぞれ鳴き声が聞こえてきます。 猫は原点の周りをぐるぐると周り、プレイヤーはキーでカメラの位置を操作できます。 カメラの位置や猫の位置が変わるごとに音の聞こえ方が変わることを確認してみて下さい。 イヤホンを使うを分かりやすいかと思います。筆者は5.1チャンネルでは未確認です。

操作方法

操作内容 キーボード ゲームパッド(XInput) マウス タッチ
カメラの前進後退 ↑↓ 左スティック(上下) - -
カメラの向き変更 ←→ 左スティック(左右) - -
ゲームの終了 Esc Back - -

準備するもの

  • 犬の鳴き声音声ファイル
  • 猫の鳴き声音声ファイル3つ
  • 犬の画像ファイル
  • 猫の画像ファイル
  • 地面の画像ファイル

公式サイトの元サンプルではビルド済みのコンテンツ .xnb ファイルを使用しています。 公式サイトのサンプルをそのまま使用する場合は MGCB は使わずに直接 Visual Studio のソリューションに追加して、ビルド時にコピーするようにして下さい。

元サンプルがなぜこの方法を取っているのかについてはわかりませんが、おそらく古いバージョンのプロジェクトをそのまま移行しているからではないかと思います。 もちろん MGCB を使って同じファイルを生成しても問題ありません。一応本サイトでダウンロードできるプロジェクトは MGCB 版に修正しています。

プログラム

全コードはプログラムをダウンロードして参照してください。

本サンプルは実行時のサウンド再生の見た目を分かりやすくしつつ汎用的にコードが使えるように作成されているため、コードが少し冗長になっています。 Audio3D のみの使用であればコードの記述はかなり少なくなるのですが、一応元サンプルに沿って説明しています。 あまり重要でない部分についてはさらっと流します。

プロジェクトは以下のコードファイルで構成されています。

  • Audio3DGame
  • AudioManager
  • Cat
  • Dog
  • IAudioEmitter
  • Program
  • QuadDrawer
  • SpriteEntity

QuadDrawer クラス

このクラスは矩形ポリゴンを描画するためのヘルパークラスです。 矩形ポリゴンは主に 3D のスプライト(ビルボード)を表示するためによく使用されます。 本 Tips では猫と犬のビルボード、また地面の矩形表示にも使用されています。

このクラスは Audio3D とは関係ありません。

フィールド

readonly GraphicsDevice _graphicsDevice;
readonly AlphaTestEffect _effect;
readonly VertexPositionTexture[] _vertices;

ポリゴンを描画するために最低限必要な情報です。 モデルデータを用意せず頂点データからポリゴンを描画します。

コンストラクタ

/// <summary>
/// 新しい四辺形の描画ワーカーを作成します。
/// </summary>
public QuadDrawer(GraphicsDevice device)
{
  _graphicsDevice = device;

  _effect = new AlphaTestEffect(device);

  _effect.AlphaFunction = CompareFunction.Greater;
  _effect.ReferenceAlpha = 128;

  // 4つの頂点の配列を事前に割り当てます。
  _vertices = new VertexPositionTexture[4];

  _vertices[0].Position = new Vector3(1, 1, 0);
  _vertices[1].Position = new Vector3(-1, 1, 0);
  _vertices[2].Position = new Vector3(1, -1, 0);
  _vertices[3].Position = new Vector3(-1, -1, 0);
}

このクラスは汎用的に使えるので、Game クラスの初期化処理でインスタンスを作成しておいて GraphicsDevice を受け取ります。

ここではスプライトの描画に必要なエフェクトの設定と、矩形ポリゴンに必要な4頂点の位置を設定しています。 大きさについては描画時にスケールすればいいので -1~1 にしています。

DrawQuad メソッド

/// <summary>
/// 3Dワールドの一部として四角形を描画します。
/// </summary>
public void DrawQuad(Texture2D texture, float textureRepeats, Matrix world, Matrix view, Matrix projection)
{
  // 指定されたテクスチャとカメラマトリックスを使用するようにエフェクトを設定します。
  _effect.Texture = texture;

  _effect.World = world;
  _effect.View = view;
  _effect.Projection = projection;

  // 指定された数のテクスチャの繰り返しを使用するように頂点配列を更新します。
  _vertices[0].TextureCoordinate = new Vector2(0, 0);
  _vertices[1].TextureCoordinate = new Vector2(textureRepeats, 0);
  _vertices[2].TextureCoordinate = new Vector2(0, textureRepeats);
  _vertices[3].TextureCoordinate = new Vector2(textureRepeats, textureRepeats);

  // 矩形を描画します。
  _effect.CurrentTechnique.Passes[0].Apply();

  _graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, _vertices, 0, 2);
}

渡されたテクスチャーを矩形にセットして描画します。ここは基本処理なので特筆すべき点はありません。

1点あるとすれば、引数に textureRepeats という変数が指定できるようになっており、テクスチャーの座標を変えられるようになっています。 VertexPositionTexture.TextureCoordinate に 0~1 の範囲を超えて設定した場合、デフォルトではテクスチャーが繰り返して描画されるようになっているので、 それを利用してタイルなどを繰り返し並べられるような表現ができるようになっています。 実際、地面の市松模様は単純な白黒画像を複数並べているように見せています。

IAudioEmitter インターフェース

/// <summary>
/// 3D サウンドを再生するエンティティの位置と速度を検索するために AudioManager が使用するインターフェイス。
/// </summary>
public interface IAudioEmitter
{
  Vector3 Position { get; }
  Vector3 Forward { get; }
  Vector3 Up { get; }
  Vector3 Velocity { get; }
}

音を発するエンティティとして定義しています。 今回音を発するエンティティは犬と猫なので、それぞれのクラスで継承しています。 Emitter として必要な情報は以下の通りです。

プロパティ 用途
Position エンティティの位置
Forward エンティティが向いている方向(ベクトル)
Up エンティティの上方向。人でいうと頭が存在する方向
Velocity エンティティの移動速度。ドップラー値を計算するときに使用される。

SpriteEntity クラス

3D スプライト(ビルボード)を表現するためのクラスです。エミッターでもあるので IAudioEmitter を継承しています。 犬や猫はこのクラスを継承します。

プロパティ

/// <summary>エンティティの3D位置を取得または設定します。</summary>
public Vector3 Position { get; set; }

/// <summary>エンティティが向いている方向を取得または設定します。</summary>
public Vector3 Forward { get; set; }

/// <summary>このエンティティの上方向を取得または設定します。</summary>
public Vector3 Up { get; set; }

/// <summary>このエンティティの移動速度を取得または設定します。</summary>
public Vector3 Velocity { get; protected set; }

/// <summary>このエンティティの表示に使用されるテクスチャを取得または設定します。</summary>
public Texture2D Texture { get; set; }

エンティティの位置情報などを持っています。基本的にはエミッターとしての情報を持っています。 描画エンティティでもあるので表示する画像(Texture)も持っています。

Update メソッド

/// <summary>
/// エンティティの位置を更新し、サウンドを再生できるようにします。
/// </summary>
public abstract void Update(GameTime gameTime, AudioManager audioManager);

エンティティの位置やサウンドを再生する処理を記述しますが、派生先クラスで実装します。

Draw メソッド

/// <summary>
/// エンティティをビルボードスプライトとして描画します。
/// </summary>
public void Draw(QuadDrawer quadDrawer, Vector3 cameraPosition, Matrix view, Matrix projection)
{
  Matrix world = Matrix.CreateTranslation(0, 1, 0) *
                 Matrix.CreateScale(800) *
                 Matrix.CreateConstrainedBillboard(Position, cameraPosition, Up, null, null);

  quadDrawer.DrawQuad(Texture, 1, world, view, projection);
}

犬と猫は画像や位置は違いますがそれ以外については描画の仕組みは全く同じなのでここに記述しています。

各座標変換とテクスチャーを QuadDrawer に渡して矩形ポリゴンを表示させています。 座標変換は基本知識なので説明は割愛します。

Matrix.CreateConstrainedBillboard メソッドとカメラの情報を使用すると簡単にビルボードを表現できるので有効活用しましょう。

Dog クラス

犬の描画、鳴き声の再生を行うクラスです。SpriteEntity クラスを継承しています。 犬は 3D 空間内では固定位置にとどまっています。

フィールド

/// <summary>サウンドを開始または停止するまでの時間。</summary>
TimeSpan _timeDelay = TimeSpan.Zero;

/// <summary>現在再生中のサウンド(ある場合)。</summary>
SoundEffectInstance _activeSound = null;

_timeDelay は鳴き声を再生する間隔で使用します。

SoundEffectInstance _activeSound はサウンドを再生するためのインスタンスです。 SoundEffectInstance クラスはサウンド再生でよく見ると思いますが、3Dサウンドでもこれをそのまま使えます。 ちなみに _activeSound はのちの AudioManager クラスから参照を受け取るだけですので、Dot クラス側で破棄はしないようにします。

Update メソッド

/// <summary>
/// 犬の位置を更新し、音を鳴らします。
/// </summary>
public override void Update(GameTime gameTime, AudioManager audioManager)
{
  // エンティティを固定位置に設定します。
  Position = new Vector3(0, 0, -4000);
  Forward = Vector3.Forward;
  Up = Vector3.Up;
  Velocity = Vector3.Zero;

  // 時間遅延がなくなった場合は、ループ音を開始または停止します。 これは通常は永久に続きますが、6秒の遅延後に停止し、さらに4秒後に再起動します。
  _timeDelay -= gameTime.ElapsedGameTime;

  if (_timeDelay < TimeSpan.Zero)
  {
    if (_activeSound == null)
    {
      // 現在再生中のサウンドがない場合は、トリガーします。
      _activeSound = audioManager.Play3DSound("DogSound", true, this);

      _timeDelay += TimeSpan.FromSeconds(6);
    }
    else
    {
      // それ以外の場合は、現在のサウンドを停止します。
      _activeSound.Stop(false);
      _activeSound = null;

      _timeDelay += TimeSpan.FromSeconds(4);
    }
  }
}

本サンプルでは犬はずっと同じ位置に居続けるので最初の4パラメータを決め打ちで指定してあります。 描画は基本クラスでやってくれます。

_timeDelay は次に鳴き声を再生する、停止するの残り時間として使用されています。

AudioManager は後程説明しますが、Play3DSound に鳴き声の名称とループ情報、エミッターである自身の情報を渡すことにより 3D 空間で再生してくれます。

Dog クラスは鳴き声をループ再生する想定で作られているので、再生したり停止したりの分岐処理などはただの調整なのであまり深く考える必要はありません。

Cat クラス

猫の描画、鳴き声の再生を行うクラスです。SpriteEntity クラスを継承しています。 猫は原点の周りをぐるぐる回るように動いています。

フィールド

/// <summary>次の音を鳴らすまでの時間。</summary>
TimeSpan _timeDelay = TimeSpan.Zero;

/// <summary>サウンドバリエーションから選択するための乱数ジェネレータ。</summary>
static readonly Random _random = new Random();

_timeDelay は Dog クラスと同じ、次に鳴き声を発するまでの残り時間で使用されています。

猫の鳴き声は3種類ありランダムで選択されて再生されるようになっていますが、おまけ要素なので Audio3D とは関係ありません。

Update メソッド

/// <summary>
/// 猫の位置を更新し、音を鳴らします。
/// </summary>
public override void Update(GameTime gameTime, AudioManager audioManager)
{
  // 猫を大きな円で動かします。
  double time = gameTime.TotalGameTime.TotalSeconds;

  float dx = (float)-Math.Cos(time);
  float dz = (float)-Math.Sin(time);

  Vector3 newPosition = new Vector3(dx, 0, dz) * 6000;

  // エンティティの位置と速度を更新します。
  Velocity = newPosition - Position;
  Position = newPosition;
  if (Velocity == Vector3.Zero)
  {
    Forward = Vector3.Forward;
  }
  else
  {
    Forward = Vector3.Normalize(Velocity);
  }

  Up = Vector3.Up;

  // 時間遅延がなくなった場合は、別の単発音をトリガーします。
  _timeDelay -= gameTime.ElapsedGameTime;

  if (_timeDelay < TimeSpan.Zero)
  {
    // 異なる3つのサウンドバリエーション(CatSound0、CatSound1、CatSound2)からランダムに選択します。
    string soundName = "CatSound" + _random.Next(3);

    audioManager.Play3DSound(soundName, false, this);

    _timeDelay += TimeSpan.FromSeconds(1.25f);
  }
}

猫は原点の周りを円を描くように回るので、三角関数を利用して円の軌跡を動くように処理しています。 それに伴い Position (位置), Forward (向き), Velocity (速度) が逐一変わるようになっています。 そのため、プログラムを実行するとカメラを動かさなくても猫の鳴き声が周りをぐるぐる回っているのが分かるかと思います。

_timeDelay 以降のコードは一定間隔で猫の鳴き声をランダムで選択肢て再生している処理です。 audioManager.Play3DSound メソッドにエミッターである自身を渡しているので、 音の再生位置が逐一変わっていくのが分かると思います。

AudioManager クラス

ようやくここで Audio3D の本質となるクラスとなります。 3D サウンドには音を聞く人(リスナー)と音を発する物(エミッター)から成り立っています。 エミッターはすでに犬や猫で定義していますので、AudioManager クラスではリスナーを定義しています。 通常エミッターは複数存在するのに対し、リスナーは1人となります。

このクラスは GameComponent を継承し、Game クラスでコンポーネントに登録して使用する形になっています。

ActiveSound クラス

/// <summary>
/// アクティブな3Dサウンドを追跡し、アタッチされているエミッターオブジェクトを記憶するための内部ヘルパークラス。
/// </summary>
private class ActiveSound
{
  public SoundEffectInstance Instance;
  public IAudioEmitter Emitter;
}

コードの一番下に定義していますが、再生中の状態を保持するためのクラスです。 再生中のサウンドインスタンスと再生しているエミッターの情報を持っています。

フィールド

// このマネージャーにロードされるすべてのサウンドエフェクトのリスト。
static string[] _soundNames =
  {
    "CatSound0",
    "CatSound1",
    "CatSound2",
    "DogSound",
  };

/// <summary>音を聞くリスナーの情報です。これは通常カメラに一致するように設定されます。</summary>
public AudioListener Listener { get; } = new AudioListener();

/// <summary>AudioEmitter は、3Dサウンドを生成しているエンティティを表します。</summary>
readonly AudioEmitter _emitter = new AudioEmitter();

/// <summary>再生可能なすべての効果音を保存します。</summary>
readonly Dictionary<string, SoundEffect> _soundEffects = new Dictionary<string, SoundEffect>();

/// <summary>現在再生中のすべての3Dサウンドを追跡します。また、再生が終わったインスタンスの破棄にも使用します。</summary>
readonly List<ActiveSound> _activeSounds = new List<ActiveSound>();

_soundNames は読み込む音声ファイル名(アセット名)を定義しています。サンプルプログラムなので、最初に一括で音声ファイルを読み込むために定義しています。 読み込んだサウンドデータは _soundEffects に保管します。

AudioListener Listener がリスナーの定義になります。リスナーの情報は Game クラスからセットするので public で定義しています。

AudioEmitter _emitter はサウンド再生に 3D を適用させる際のエミッターの定義です。 本サンプルではサウンドを再生させる際に各エミッターオブジェクトの値を _emitter にセットしているのでインスタンスとしては1つを共有している形になります。 もちろんオブジェクトごとに AudioEmitter を持っても構いません。

_activeSounds は先ほど定義していた ActiveSound クラスで再生中のサウンドに関する情報を持っています。 エミッターの情報が常に変わるため、位置情報などを更新するためと、再生し終わったサウンドインスタンスを破棄するために使用します。

コンストラクタ

public AudioManager(Game game) : base(game) { }

GameComponent クラスを継承しているので Game インスタンスを受け取り基本クラスに渡しています。

Initialize メソッド

/// <summary>
/// オーディオマネージャを初期化します。
/// </summary>
public override void Initialize()
{
  // ゲームの世界のスケールと一致するように、3Dオーディオのスケールを設定します。
  // DistanceScale は、離れるにつれて音量が変化する音の量を制御します。
  // DopplerScale は、サウンドを通過するときにピッチが変化するサウンドの量を制御します。
  SoundEffect.DistanceScale = 2000;
  SoundEffect.DopplerScale = 0.1f;

  // すべての効果音をロードします。
  foreach (string soundName in _soundNames)
  {
    _soundEffects.Add(soundName, Game.Content.Load<SoundEffect>(soundName));
  }

  base.Initialize();
}

GameComponent クラスを継承しているので初期化時に自動的に呼ばれます。

SoundEffect.DistanceScaleSoundEffect.DopplerScale は 3D サウンド専用の static パラメータです。

SoundEffect.DistanceScale を大きくすると遠くでも音が聞こえるようになります。

SoundEffect.DopplerScale はドップラー効果の影響力です。数値が大きいほどドップラー効果が大きくなります。

_soundNames で定義したアセット名をループで読み込み _soundEffects に保管します。

Dispose メソッド

/// <summary>
/// 効果音データをアンロードします。
/// GameComponent として登録すればゲーム終了時に自動的に呼ばれます。
/// </summary>
protected override void Dispose(bool disposing)
{
  try
  {
    if (disposing)
    {
      foreach (SoundEffect soundEffect in _soundEffects.Values)
      {
        soundEffect.Dispose();
      }

      _soundEffects.Clear();
    }
  }
  finally
  {
    base.Dispose(disposing);
  }
}

GameComponent クラスを継承しているのでゲーム終了時に自動的に呼ばれます。

読み込んだサウンドアセットをすべて破棄しています。

Update メソッド

/// <summary>
/// 3Dオーディオシステムの状態を更新します。
/// </summary>
public override void Update(GameTime gameTime)
{
  // 現在再生中のすべての3Dサウンドをループします。
  int index = 0;

  while (index < _activeSounds.Count)
  {
    ActiveSound activeSound = _activeSounds[index];

    if (activeSound.Instance.State == SoundState.Stopped)
    {
      // 音の再生が終了した場合は廃棄してください。
      activeSound.Instance.Dispose();

      // アクティブリストから削除します。
      _activeSounds.RemoveAt(index);
    }
    else
    {
      // サウンドがまだ再生されている場合は、3D設定を更新します。
      Apply3D(activeSound);

      index++;
    }
  }

  base.Update(gameTime);
}

GameComponent クラスを継承しているので更新処理で自動的に呼ばれます。

現在再生中のサウンドをチェックし、再生中の場合 Apply3D メソッドを呼んでエミッターに合わせてサウンドの位置情報を更新します。 再生が終わったサウンドがあればインスタンスを破棄しています。

Play3DSound メソッド

/// <summary>
/// 新しい3Dサウンドを設定し再生します。
/// </summary>
public SoundEffectInstance Play3DSound(string soundName, bool isLooped, IAudioEmitter emitter)
{
  ActiveSound activeSound = new ActiveSound();

  // インスタンスを生成し、インスタンス、エミッターを設定します。
  activeSound.Instance = _soundEffects[soundName].CreateInstance();
  activeSound.Instance.IsLooped = isLooped;

  activeSound.Emitter = emitter;

  // 3D 位置を設定します。
  Apply3D(activeSound);

  activeSound.Instance.Play();

  // このサウンドがアクティブであることを保存します。
  _activeSounds.Add(activeSound);

  return activeSound.Instance;
}

外部から 3D サウンドを再生させるための処理です。 再生するサウンド名、ループするかどうか、エミッター情報を引数に持ちます。

サウンドを再生する場合は、_soundEffects に保持しているサウンドリソースから新しいサウンドインスタンスを作成します。 SoundEffectInstance はループの可否も持てるのでループ情報もセットします。

再生中の状態として ActiveSound を作成しているのでそこに音の再生オブジェクトであるエミッター情報をセットします。 3D の位置情報をサウンドに適用させる場合は Emitter から必要な値をもらいます。

Apply3D メソッドは次で説明しますが、エミッターから 3D の情報をサウンドに適用させる処理になります。

3D サウンドの基本的な処理としては 3D の情報をセットした状態で SoundEffectInstance.Play で再生を開始し、 後は音の再生が終わるまで定期的に 3D の情報を更新していく形になります。

Apply3D メソッド

/// <summary>
/// 3Dサウンドの位置と速度の設定を更新します。
/// </summary>
private void Apply3D(ActiveSound activeSound)
{
  _emitter.Position = activeSound.Emitter.Position;
  _emitter.Forward = activeSound.Emitter.Forward;
  _emitter.Up = activeSound.Emitter.Up;
  _emitter.Velocity = activeSound.Emitter.Velocity;

  activeSound.Instance.Apply3D(Listener, _emitter);
}

リスナーとエミッターを使用して指定したサウンドインスタンスに 3D 効果を適用する処理となります。

ここでは毎回共通インスタンスの _emitter に値をセットしていますが、 すでに AudioEmitter が用意されているなら SoundEffectInstance.Apply3D メソッドにリスナーとエミッターを渡すだけで適用されます。

Audio3DGame クラス

最後に Game クラスの内容を説明します。

フィールド

readonly GraphicsDeviceManager _graphics;
readonly AudioManager _audioManager;
readonly SpriteEntity _cat;
readonly SpriteEntity _dog;

/// <summary>地面の描画に使用するテクスチャーです。</summary>
Texture2D _checkerTexture;

QuadDrawer _quadDrawer;

Vector3 _cameraPosition = new Vector3(0, 512, 0);
Vector3 _cameraForward = Vector3.Forward;
Vector3 _cameraUp = Vector3.Up;
Vector3 _cameraVelocity = Vector3.Zero;

KeyboardState _currentKeyboardState;
GamePadState _currentGamePadState;

AudioManagerGameComponents に登録しますが、個別にアクセスするのでフィールドとして持っておきます。

他は犬、猫のエンティティ、地面描画用のテクスチャ、矩形描画ヘルパー(QuadDrawer)、カメラ情報、入力情報を定義しています。

コンストラクタ

public Audio3DGame()
{
  Content.RootDirectory = "Content";

  _graphics = new GraphicsDeviceManager(this);

  _audioManager = new AudioManager(this);

  // AudioManager を Components に追加して自動的に Update メソッドが呼ばれるようにします。
  Components.Add(_audioManager);

  _cat = new Cat();
  _dog = new Dog();
}

AudioManager を生成して GameComponents に登録します。

Cat クラスと Dog クラスも生成しておきます。

LoadContent メソッド

/// <summary>
/// グラフィックコンテンツをロードします。
/// </summary>
protected override void LoadContent()
{
  _cat.Texture = Content.Load<Texture2D>("CatTexture");
  _dog.Texture = Content.Load<Texture2D>("DogTexture");

  _checkerTexture = Content.Load<Texture2D>("checker");

  // 四角形ポリゴンを描画するためのクラス
  _quadDrawer = new QuadDrawer(_graphics.GraphicsDevice);
}

それぞれ描画に必要なテクスチャーを読み込みます。

QuadDrawerGraphicsDevice が必要なのでここで生成しています。

Update メソッド

/// <summary>
/// ゲームがロジックを実行できるようにします。
/// </summary>
protected override void Update(GameTime gameTime)
{
  HandleInput();

  UpdateCamera();

  // 新しいカメラの位置について AudioManager に伝えます。
  _audioManager.Listener.Position = _cameraPosition;
  _audioManager.Listener.Forward = _cameraForward;
  _audioManager.Listener.Up = _cameraUp;
  _audioManager.Listener.Velocity = _cameraVelocity;

  // ゲームエンティティに動き回って音を鳴らすように伝えます。
  _cat.Update(gameTime, _audioManager);
  _dog.Update(gameTime, _audioManager);

  base.Update(gameTime);
}

HandleInput メソッドと UpdateCamera メソッドはのちで説明するデバイス入力の取得とカメラ操作処理です。

カメラの位置とリスナーの位置はほとんどの場合において同じなのでカメラ更新処理後にリスナーに同じ値をセットしています。

_cat_dog の Update メソッドを呼んで移動や鳴き声の処理をそれぞれ行います。

Draw メソッド

/// <summary>
/// ゲームが描画する必要があるときに呼び出されます。
/// </summary>
protected override void Draw(GameTime gameTime)
{
  var device = _graphics.GraphicsDevice;

  device.Clear(Color.CornflowerBlue);

  device.BlendState = BlendState.AlphaBlend;

  // カメラ行列を計算します。
  var view = Matrix.CreateLookAt(_cameraPosition, _cameraPosition + _cameraForward, _cameraUp);
  var projection = Matrix.CreatePerspectiveFieldOfView(1, device.Viewport.AspectRatio, 1, 100000);

  // チェッカーグラウンドポリゴンを描画します。
  var groundTransform = Matrix.CreateScale(20000) * Matrix.CreateRotationX(MathHelper.PiOver2);

  _quadDrawer.DrawQuad(_checkerTexture, 32, groundTransform, view, projection);

  // ゲームエンティティを描画します。
  _cat.Draw(_quadDrawer, _cameraPosition, view, projection);
  _dog.Draw(_quadDrawer, _cameraPosition, view, projection);

  base.Draw(gameTime);
}

カメラ情報からビュー変換と射影変換を生成します。

地面については QuadDrawer で描画させる矩形は垂直に表示されるので、水平になるように回転させて適度に拡大しています。 _quadDrawer.DrawQuad メソッドの第2引数にはテクスチャーのリピート数を指定しており、32x32 枚並んで見えるように指定しています。

_cat と _dog については内部でビルボード処理を行っているのでカメラの位置情報を渡して描画しています。

HandleInput メソッド

/// <summary>
/// ゲームを終了するための入力を処理します。
/// </summary>
void HandleInput()
{
  _currentKeyboardState = Keyboard.GetState();
  _currentGamePadState = GamePad.GetState(PlayerIndex.One);

  // 終了を確認します。
  if (_currentKeyboardState.IsKeyDown(Keys.Escape) ||
      _currentGamePadState.Buttons.Back == ButtonState.Pressed)
  {
    Exit();
  }
}

キーボード、ゲームパッドの情報取得、ゲーム終了判定を行っています。

UpdateCamera メソッド

/// <summary>
/// カメラを動かすための入力を処理します。
/// </summary>
void UpdateCamera()
{
  const float turnSpeed = 0.05f;
  const float accelerationSpeed = 4;
  const float frictionAmount = 0.98f;

  // 左または右に曲がります。
  float turn = -_currentGamePadState.ThumbSticks.Left.X * turnSpeed;

  if (_currentKeyboardState.IsKeyDown(Keys.Left)) turn += turnSpeed;
  if (_currentKeyboardState.IsKeyDown(Keys.Right)) turn -= turnSpeed;

  _cameraForward = Vector3.TransformNormal(_cameraForward, Matrix.CreateRotationY(turn));

  // 前方または後方に加速します。
  float accel = _currentGamePadState.ThumbSticks.Left.Y * accelerationSpeed;

  if (_currentKeyboardState.IsKeyDown(Keys.Up)) accel += accelerationSpeed;
  if (_currentKeyboardState.IsKeyDown(Keys.Down)) accel -= accelerationSpeed;

  _cameraVelocity += _cameraForward * accel;

  // 現在の位置に速度を追加します。
  _cameraPosition += _cameraVelocity;

  // 摩擦力を加えます。
  _cameraVelocity *= frictionAmount;
}

カメラの移動制御を行っています。

加速度や摩擦などの計算も含まれていますが、おそらく 3D サウンドでのドップラー効果を考慮してだと思います。 計算自体については特殊なことはしていないので説明は割愛します。

まとめ

長々と説明を入れていきましたが、3D サウンドの再生を行う方法については AudioManager クラスを参照すれば大抵は事足りるはずです。 実際、やってることは SoundEffectInstance に 3D 情報をセットしているぐらいですので、難しいことはフレームワーク側がほとんどやってくれます。 3D サウンドの部分だけをサンプルで纏めたとしたら Game クラスに少し処理を書いて終わりになるはずです。 ですが、元ネタをベースに Tips を書いてますのでいろいろと長くなってしまいました。