7章 : 音を鳴らしてみよう
前章では、当たり判定を実装しました。
この章では、ゲームに不可欠な音を追加します。
ゲームには、オブジェクトの動きに合わせたサウンドエフェクト(SE)やゲームの雰囲気を作り出すBGMが必要です。
以下では、SEやBGMを再生する処理を実装します。
SEを鳴らす
今回は、以下のSEを再生します。
- プレイヤーのショット音
- プレイヤーの死亡時サウンド
- 敵共通の死亡時サウンド
- まっすぐな弾を発射する敵のショット音
- 放射ショットの敵のショット音
プレイヤーのショット音を追加
Player.cs
に以下のソースコードを追加します。
using Altseed2;
namespace Tutorial
{
// プレイヤーのクラス
public class Player : CollidableObject
{
+ // ショット時の効果音
+ private Sound shotSound;
// コンストラクタ
public Player(MainNode mainNode, Vector2F position) : base(mainNode, position)
{
// 衝突判定を行うように設定
doSurvey = true;
// テクスチャを読み込む
Texture = Texture2D.LoadStrict("Resources/Player.png");
// 中心座標を設定
CenterPosition = ContentSize / 2;
// コライダの半径を設定
collider.Radius = Texture.Size.Y / 2;
+ // ショット音を読み込む
+ shotSound = Sound.LoadStrict("Resources/shot1.wav", true);
}
// 衝突時に実行
protected override void OnCollision(CollidableObject obj)
{
// 衝突対象が敵か敵の弾だったら
if (obj is Enemy || obj is EnemyBullet)
{
// 自身を親から削除
Parent.RemoveChildNode(this);
}
}
// フレーム毎に実行
protected override void OnUpdate()
{
// 移動を実行
Move();
// ショットを実行
Shot();
// CollidableObjectのOnupdate呼び出し
base.OnUpdate();
}
// 移動を行う
private void Move()
{
// 現在のX座標を取得する
var x = Position.X;
// 現在のY座標を取得する
var y = Position.Y;
// ↑キーでY座標を減少
if (Engine.Keyboard.GetKeyState(Key.Up) == ButtonState.Hold)
{
y -= 2.5f;
}
// ↓キーでY座標を増加
if (Engine.Keyboard.GetKeyState(Key.Down) == ButtonState.Hold)
{
y += 2.5f;
}
// →キーでX座標を増加
if (Engine.Keyboard.GetKeyState(Key.Right) == ButtonState.Hold)
{
x += 2.5f;
}
// ←キーでX座標を減少
if (Engine.Keyboard.GetKeyState(Key.Left) == ButtonState.Hold)
{
x -= 2.5f;
}
// テクスチャのサイズの半分を取得する
var halfSize = Texture.Size / 2;
// X座標が画面外に行かないように調整
x = MathHelper.Clamp(x, Engine.WindowSize.X - halfSize.X, halfSize.X);
// Y座標が画面外に行かないように調整
y = MathHelper.Clamp(y, Engine.WindowSize.Y - halfSize.Y, halfSize.Y);
// 調整された座標を設定
Position = new Vector2F(x, y);
}
// ショット
private void Shot()
{
// Zキーでショットを放つ
if (Engine.Keyboard.GetKeyState(Key.Z) == ButtonState.Push)
{
Parent.AddChildNode(new PlayerBullet(mainNode, Position));
+ // ショット音を鳴らす
+ Engine.Sound.Play(shotSound);
}
}
}
}
// ショット時の効果音
private Sound shotSound;
Altseed2では、音源データをSound
クラスインスタンスに格納します。
また、ファイルから音源データを読み込む場合は、以下のようにSound.LoadStrict
メソッドを用います。
// ショット音を読み込む
shotSound = Sound.LoadStrict("Resources/shot1.wav", true);
第一引数には、音源データファイルへのパスします。
そして、第二引数には、音源データを事前に解凍するか否かを指定します。
一般的に、短い音をたくさん鳴らすようなSEの場合は、事前に解凍します。
一方、BGMのような長い音源に対しては、事前に解凍せず、逐次解凍するようにします。
そして、読み込んだ音源データを再生するには、以下のようにEngine.Sound.Play
メソッドを呼び出す必要があります。
// ショット音を鳴らす
Engine.Sound.Play(shotSound);
引数として、上で作成したSound
クラスインスタンスを渡します。
この段階でビルドし、実行するとZキーでショットを放った瞬間に音が鳴ると思います。
プレイヤーの死亡時サウンドを追加
ショット音と同様にPlayer.cs
に以下のソースコードを追加します。
using Altseed2;
namespace Tutorial
{
// プレイヤーのクラス
public class Player : CollidableObject
{
// ショット時の効果音
private Sound shotSound;
// コンストラクタ
public Player(MainNode mainNode, Vector2F position) : base(mainNode, position)
{
// 衝突判定を行うように設定
doSurvey = true;
// テクスチャを読み込む
Texture = Texture2D.LoadStrict("Resources/Player.png");
// 中心座標を設定
CenterPosition = ContentSize / 2;
// コライダの半径を設定
collider.Radius = Texture.Size.Y / 2;
// ショット音を読み込む
shotSound = Sound.LoadStrict("Resources/shot1.wav", true);
}
// 衝突時に実行
protected override void OnCollision(CollidableObject obj)
{
// 衝突対象が敵か敵の弾だったら
if (obj is Enemy || obj is EnemyBullet)
{
+ // 死亡音を読み込む
+ var deathSound = Sound.LoadStrict("Resources/Explosion.wav", true);
+
+ // 死亡音を再生
+ Engine.Sound.Play(deathSound);
+
// 自身を親から削除
Parent.RemoveChildNode(this);
}
}
// フレーム毎に実行
protected override void OnUpdate()
{
// 移動を実行
Move();
// ショットを実行
Shot();
// CollidableObjectのOnupdate呼び出し
base.OnUpdate();
}
// 移動を行う
private void Move()
{
// 現在のX座標を取得する
var x = Position.X;
// 現在のY座標を取得する
var y = Position.Y;
// ↑キーでY座標を減少
if (Engine.Keyboard.GetKeyState(Key.Up) == ButtonState.Hold)
{
y -= 2.5f;
}
// ↓キーでY座標を増加
if (Engine.Keyboard.GetKeyState(Key.Down) == ButtonState.Hold)
{
y += 2.5f;
}
// →キーでX座標を増加
if (Engine.Keyboard.GetKeyState(Key.Right) == ButtonState.Hold)
{
x += 2.5f;
}
// ←キーでX座標を減少
if (Engine.Keyboard.GetKeyState(Key.Left) == ButtonState.Hold)
{
x -= 2.5f;
}
// テクスチャのサイズの半分を取得する
var halfSize = Texture.Size / 2;
// X座標が画面外に行かないように調整
x = MathHelper.Clamp(x, Engine.WindowSize.X - halfSize.X, halfSize.X);
// Y座標が画面外に行かないように調整
y = MathHelper.Clamp(y, Engine.WindowSize.Y - halfSize.Y, halfSize.Y);
// 調整された座標を設定
Position = new Vector2F(x, y);
}
// ショット
private void Shot()
{
// Zキーでショットを放つ
if (Engine.Keyboard.GetKeyState(Key.Z) == ButtonState.Push)
{
Parent.AddChildNode(new PlayerBullet(mainNode, Position));
// ショット音を鳴らす
Engine.Sound.Play(shotSound);
}
}
}
}
敵共通の死亡時サウンドを追加
すべての敵共通で再生するため、Enemy.cs
に以下のソースコードを追加します。
using Altseed2;
namespace Tutorial
{
// 敵の基礎となるクラス
public class Enemy : CollidableObject
{
// 倒された時に加算されるスコアの値
protected int score;
// プレイヤーへの参照
protected Player player;
// コンストラクタ
public Enemy(Player player, Vector2F position) : base(player.mainNode, position)
{
// 衝突判定を行うように設定
doSurvey = true;
// プレイヤーへの参照を設定
this.player = player;
}
// 衝突時に実行
protected override void OnCollision(CollidableObject obj)
{
// 衝突対象が自機弾だったら
if (obj is PlayerBullet)
{
// スコアを加算
mainNode.score += score;
// 死亡時エフェクトを再生
Parent.AddChildNode(new DeathEffect(Position));
// 自身を削除
Parent.RemoveChildNode(this);
+ // 死亡時サウンドを読み込み
+ var deathSound = Sound.LoadStrict("Resources/Explosion.wav", true);
+
+ // 死亡時サウンドを再生
+ Engine.Sound.Play(deathSound);
}
}
// フレーム毎に実行
protected override void OnUpdate()
{
// CollidableObjectのOnUpdateを実行
base.OnUpdate();
// 画面外に出たら自身を削除
RemoveMyselfIfOutOfWindow();
}
}
}
まっすぐな弾を発射する敵のショット音を追加
StraightShotEnemy.cs
に以下のソースコードを追加します。
using Altseed2;
using System;
namespace Tutorial
{
// まっすぐな弾を発射する敵
public class StraightShotEnemy : Enemy
{
// カウンタ
private int count = 0;
+ // ショット時の効果音
+ private Sound shotSound;
// コンストラクタ
public StraightShotEnemy(Player player, Vector2F position) : base(player, position)
{
// テクスチャを読み込む
Texture = Texture2D.LoadStrict("Resources/UFO.png");
// 中心座標を設定
CenterPosition = ContentSize / 2;
// 半径を設定
collider.Radius = Texture.Size.X / 2;
// 倒された時に加算されるスコアを設定
score = 20;
+ // ショット時の効果音を読み込む
+ shotSound = Sound.LoadStrict("Resources/shot2.wav", true);
}
// フレーム毎に実行
protected override void OnUpdate()
{
// カウントが150の倍数で実行
if (count % 150 == 0)
{
// プレイヤーに対するベクトルの単位ベクトルを取得
var velocity = (player.Position - Position).Normal;
// ベクトルの長さを調整(弾速になる)
velocity *= 5;
// 弾を追加
Shot(velocity);
}
// 座標を設定
Position -= new Vector2F(MathF.Sin(MathHelper.DegreeToRadian(count)) * 3.0f, 0);
// EnemyのOnUpdateを実行
base.OnUpdate();
// カウントを進める
count++;
}
// 弾を撃つ
private void Shot(Vector2F velocity)
{
// 敵弾を画面に追加
Parent.AddChildNode(new EnemyBullet(mainNode, Position, velocity));
+ // ショット音を再生
+ Engine.Sound.Play(shotSound);
}
}
}
放射ショットの敵のショット音を追加
RadialShotEnemy.cs
に以下のソースコードを追加します。
using Altseed2;
namespace Tutorial
{
// 放射ショットの敵
public class RadialShotEnemy : Enemy
{
// カウンタ変数
private int count = 0;
// 撃ち出すショットの個数
private int shotAmount;
+ // ショット時の効果音
+ private Sound shotSound;
// フレーム毎の速度
private Vector2F velocity;
// コンストラクタ
public RadialShotEnemy(Player player, Vector2F position, int shotAmount) : base(player, position)
{
// 撃ち出すショットの個数を設定
this.shotAmount = shotAmount;
// テクスチャを読み込む
Texture = Texture2D.LoadStrict("Resources/UFO.png");
// 中心座標を設定
CenterPosition = ContentSize / 2;
// 半径を設定
collider.Radius = Texture.Size.X / 2;
// スコアを設定
score = 30;
+ // ショット時の効果音を読み込む
+ shotSound = Sound.LoadStrict("Resources/shot2.wav", true);
}
// フレーム毎に実行
protected override void OnUpdate()
{
// カウントが250の倍数だったら
if (count % 250 == 0)
{
// 計算用のローカル変数
var half = shotAmount / 2;
for (int i = 0; i < shotAmount; i++)
{
// 現時点の座標からプレイヤーに向かうベクトルの単位ベクトルを取得する
var vector = (player.Position - Position).Normal;
// ベクトルを速度分掛ける
vector *= 7.0f;
// ベクトルを傾ける
vector.Degree += 30 * (i - half);
// ショットを放つ
Shot(vector);
}
}
// カウント÷100の余りが0~49だったら
if (count % 100 < 50)
{
// カウント÷100の余りが0だったら
if (count % 100 == 0)
{
// 進むベクトルを設定
velocity = (player.Position - Position).Normal * 3.0f;
}
// 速度分ベクトルを設定
Position += velocity;
}
// EnemyクラスのOnUpdateを呼び出す
base.OnUpdate();
// カウントを進める
count++;
}
// 弾を撃つ
private void Shot(Vector2F velocity)
{
// 敵弾を画面に追加
Parent.AddChildNode(new EnemyBullet(mainNode, Position, velocity));
+ // ショット音を再生
+ Engine.Sound.Play(shotSound);
}
}
}
以上を追加し、ビルドするとSEが適切なタイミングで再生されると思います。
BGMを鳴らす
次は、BGMを再生する処理を追加しましょう。
メインステージのBGMであるため、MainNode.cs
に以下のソースコードを追加します。
using Altseed2;
using System.Collections.Generic;
namespace Tutorial
{
// メインステージのクラス
public class MainNode : Node
{
// カウンタ
private int count = 0;
// 敵を格納するキュー
private Queue<Enemy> enemies = new Queue<Enemy>();
// キャラクターを表示するノード
private Node characterNode = new Node();
// プレイヤーの参照
private Player player;
// エンジンに追加された時に実行
protected override void OnAdded()
{
// キャラクターノードを追加
AddChildNode(characterNode);
// UIを表示するノード
var uiNode = new Node();
// UIノードを追加
AddChildNode(uiNode);
// 背景に使用するテクスチャ
var backTexture = new SpriteNode();
// 背景のテクスチャを読み込む
backTexture.Texture = Texture2D.LoadStrict("Resources/Background.png");
// 表示位置を奥に設定
backTexture.ZOrder = -100;
// 背景テクスチャを追加
AddChildNode(backTexture);
// プレイヤーを設定
player = new Player(this, new Vector2F(100, 360));
// キャラクターノードにプレイヤーを追加
characterNode.AddChildNode(player);
// ウェーブを初期化する
InitWave();
+ // BGMを初期化する
+ InitBGM();
}
// エンジンから削除されたときに実行
protected override void OnRemoved()
{
// 衝突判定を全てリセット
CollidableObject.objects.Clear();
}
+ // BGMを初期化
+ private void InitBGM()
+ {
+ // BGMを読み込む
+ var bgm = Sound.LoadStrict("Resources/BGM.wav", false);
+
+ // BGMのプレイ開始
+ Engine.Sound.Play(bgm);
+ }
// ウェーブの初期化
private void InitWave()
{
// enemies.Enqueue~でウェーブに敵を追加
// 追加した順番に敵が出現する
enemies.Enqueue(new ChaseEnemy(player, new Vector2F(700, 160), 2.0f));
enemies.Enqueue(new StraightShotEnemy(player, new Vector2F(600, 620)));
enemies.Enqueue(new Meteor(player, new Vector2F(910, 400), new Vector2F(-4.0f, 0.0f)));
enemies.Enqueue(new RadialShotEnemy(player, new Vector2F(400, 160), 3));
}
// フレーム毎に実行
protected override void OnUpdate()
{
// ステージの更新
UpdateStage();
// カウントを進める
count++;
}
// 敵召還関連
private void UpdateStage()
{
// カウントが100の倍数だったら
if (count % 100 == 0)
{
// 敵が残っていたら画面に追加
if (enemies.Count > 0)
{
characterNode.AddChildNode(enemies.Dequeue());
}
}
}
}
}
BGMに関する初期化をするメソッドInitBGM
を実装し、
メインステージが始まった時点で再生するため、OnAdded
で呼び出します。
BGMを再生する場合も、基本的はSEと変わらず、Sound
クラスインスタンスを用います。
しかし、このままではうまくいきません。
今回、BGMを再生するために、以下の処理が必要となります。
- ループ処理
- 再生している音の制御(一時停止、終了)
ループ処理
InitBGM
メソッドに以下の処理を追加しましょう。
using Altseed2;
using System.Collections.Generic;
namespace Tutorial
{
// メインステージのクラス
public class MainNode : Node
{
// カウンタ
private int count = 0;
// 敵を格納するキュー
private Queue<Enemy> enemies = new Queue<Enemy>();
// キャラクターを表示するノード
private Node characterNode = new Node();
// プレイヤーの参照
private Player player;
// エンジンに追加された時に実行
protected override void OnAdded()
{
// キャラクターノードを追加
AddChildNode(characterNode);
// UIを表示するノード
var uiNode = new Node();
// UIノードを追加
AddChildNode(uiNode);
// 背景に使用するテクスチャ
var backTexture = new SpriteNode();
// 背景のテクスチャを読み込む
backTexture.Texture = Texture2D.LoadStrict("Resources/Background.png");
// 表示位置を奥に設定
backTexture.ZOrder = -100;
// 背景テクスチャを追加
AddChildNode(backTexture);
// プレイヤーを設定
player = new Player(this, new Vector2F(100, 360));
// キャラクターノードにプレイヤーを追加
characterNode.AddChildNode(player);
// ウェーブを初期化する
InitWave();
// BGMを初期化する
InitBGM();
}
// エンジンから削除されたときに実行
protected override void OnRemoved()
{
// 衝突判定を全てリセット
CollidableObject.objects.Clear();
}
// BGMを初期化
private void InitBGM()
{
// BGMを読み込む
var bgm = Sound.LoadStrict("Resources/BGM.wav", false);
+ // BGMをループするように設定
+ bgm.IsLoopingMode = true;
+
+ // ループ開始位置を設定
+ bgm.LoopStartingPoint = 11.33f;
+
+ // ループ終了位置を設定
+ bgm.LoopEndPoint = 33.93f;
// BGMのプレイ開始
Engine.Sound.Play(bgm);
}
// ウェーブの初期化
private void InitWave()
{
// enemies.Enqueue~でウェーブに敵を追加
// 追加した順番に敵が出現する
enemies.Enqueue(new ChaseEnemy(player, new Vector2F(700, 160), 2.0f));
enemies.Enqueue(new StraightShotEnemy(player, new Vector2F(600, 620)));
enemies.Enqueue(new Meteor(player, new Vector2F(910, 400), new Vector2F(-4.0f, 0.0f)));
enemies.Enqueue(new RadialShotEnemy(player, new Vector2F(400, 160), 3));
}
// フレーム毎に実行
protected override void OnUpdate()
{
// ステージの更新
UpdateStage();
// カウントを進める
count++;
}
// 敵召還関連
private void UpdateStage()
{
// カウントが100の倍数だったら
if (count % 100 == 0)
{
// 敵が残っていたら画面に追加
if (enemies.Count > 0)
{
characterNode.AddChildNode(enemies.Dequeue());
}
}
}
}
}
BGMをループさせるには、Sound
クラスインスタンスのIsLoopingMode
プロパティをtrue
にします。
そして、音源の任意の区間をループさせる場合、
Sound
クラスインスタンスのLoopStartingPoint
プロパティで区間の始点Sound
クラスインスタンスのLoopEndPoint
プロパティで区間の終点
を指定する必要があります。
再生している音の制御(一時停止、終了)
MainNode.cs
に以下のソースコードを変更します。
using Altseed2;
using System.Collections.Generic;
namespace Tutorial
{
// メインステージのクラス
public class MainNode : Node
{
+ // BGMのID
+ private int? bgmID = null;
+
// カウンタ
private int count = 0;
// 敵を格納するキュー
private Queue<Enemy> enemies = new Queue<Enemy>();
// キャラクターを表示するノード
private Node characterNode = new Node();
// プレイヤーの参照
private Player player;
// エンジンに追加された時に実行
protected override void OnAdded()
{
// キャラクターノードを追加
AddChildNode(characterNode);
// UIを表示するノード
var uiNode = new Node();
// UIノードを追加
AddChildNode(uiNode);
// 背景に使用するテクスチャ
var backTexture = new SpriteNode();
// 背景のテクスチャを読み込む
backTexture.Texture = Texture2D.LoadStrict("Resources/Background.png");
// 表示位置を奥に設定
backTexture.ZOrder = -100;
// 背景テクスチャを追加
AddChildNode(backTexture);
// プレイヤーを設定
player = new Player(this, new Vector2F(100, 360));
// キャラクターノードにプレイヤーを追加
characterNode.AddChildNode(player);
// ウェーブを初期化する
InitWave();
// BGMを初期化する
InitBGM();
}
// エンジンから削除されたときに実行
protected override void OnRemoved()
{
// 衝突判定を全てリセット
CollidableObject.objects.Clear();
}
// BGMを初期化
private void InitBGM()
{
// BGMを読み込む
var bgm = Sound.LoadStrict("Resources/BGM.wav", false);
// BGMをループするように設定
bgm.IsLoopingMode = true;
// ループ開始位置を設定
bgm.LoopStartingPoint = 11.33f;
// ループ終了位置を設定
bgm.LoopEndPoint = 33.93f;
// BGMのプレイ開始
+ bgmID = Engine.Sound.Play(bgm);
- Engine.Sound.Play(bgm);
}
// ウェーブの初期化
private void InitWave()
{
// enemies.Enqueue~でウェーブに敵を追加
// 追加した順番に敵が出現する
enemies.Enqueue(new ChaseEnemy(player, new Vector2F(700, 160), 2.0f));
enemies.Enqueue(new StraightShotEnemy(player, new Vector2F(600, 620)));
enemies.Enqueue(new Meteor(player, new Vector2F(910, 400), new Vector2F(-4.0f, 0.0f)));
enemies.Enqueue(new RadialShotEnemy(player, new Vector2F(400, 160), 3));
}
// フレーム毎に実行
protected override void OnUpdate()
{
// ステージの更新
UpdateStage();
// カウントを進める
count++;
}
// 敵召還関連
private void UpdateStage()
{
// カウントが100の倍数だったら
if (count % 100 == 0)
{
// 敵が残っていたら画面に追加
if (enemies.Count > 0)
{
characterNode.AddChildNode(enemies.Dequeue());
}
}
}
}
}
再生している音は、Engine.Sound.Play
メソッドの戻り値であるint
型のIDによって、制御できます。
今回は、BGMはずっと再生させておくため、制御はしませんが、
今後、BGMを制御する場合に備えて、MainNode
クラスのbgmID
フィールドに格納しておきましょう。
以上を反映させて、ビルドを行うとBGMが再生されると思います。
まとめ
本章では、ゲームに音を加えることによって、よりゲームらしくなったかと思います。
みなさんのゲームでも、どんどん効果音をつけていってください。
次の章では、ゲーム性を高める上で重要な得点を表示させていきましょう。