0から2Dアクションバトルゲームを作ろう!⑩アレンジ編:ゲームの完成度を高めよう(後半)

0からアクションゲームを作ろう講座その10です。
応用編後半となります。重要度の高い要素を追加で実装していきましょう!

この章でやること

前章から引き続き応用編となっております。
8章までの学習を一通り完了されている方を対象としていますので、基本的なUnityの操作及びコーディングは説明を省くこともあります。

今回は音楽の付け方やパーティクル機能など、ぜひ覚えておきたい知識がいくつもあります。
これらを利用して、より楽しいゲームを作っていきましょう!

今回実装する要素一覧

この章で実装する追加要素とその過程で学ぶ知識です。

  • 音楽(BGM・SE)の実装
    音楽ファイルをインポートし、それを再生する方法を学びます。
  • パーティクルの実装
    パーティクルシステムの説明や実際の使い方です。
  • ボス撃破タイムの記録
    シーン切り替えを行っても破棄されないオブジェクトを作ります。
  • 新しいボスの追加
    ボスにバリエーションを持たせる方法について学び、特殊な動きをする弾の処理を書きます。

 

 

BGM・SEの実装

ほとんどのゲームにとって音楽は必要不可欠です。
Unityにはもちろん音楽の組み込みをサポートする機能があり、単純に再生するだけなら非常に簡単なプログラムで済みます。

ゲーム開発において音楽は「BGM」と「SE」の2つに大別されます。
用語の意味を知らない方はここで覚えておきましょう。

  • BGM
    「バックグラウンドミュージック」の略で、ゲーム中に流し続ける音楽です。
    ループ再生に対応している曲が望ましいです。
  • SE
    「サウンドエフェクト」の略で、つまり効果音です。
    ボタンの押下時やキャラの攻撃時など、決まったタイミングで鳴らす短い音楽です。

BGMとSEを用意しよう

まずは再生用の音楽を用意しプロジェクトにインポートする必要があります。
インポートの操作は今までと同様で、プロジェクトウィンドウへのドラッグアンドドロップで完了します。
ファイルの区別をしやすくするために、[Assets]→[Audios]フォルダ下に更に「BGM」、「SE」という名前のフォルダを作ると良いでしょう。

インポート可能なオーディオファイルには主に「.mp3」「.ogg」「.wav」等があります。
この内.oggファイルは他と比べて動作が重くなりにくいのでおすすめです。

今回音楽については講座でサンプル素材を公開できませんのでご了承ください。ゲーム内で使用しているBGMおよびSEは「魔王魂」様よりお借りしています。魔王魂は商用利用可能でクレジット表記不要のクリエイターにとって非常に有難いサイトになります。ですが、利用規約は必ず自分で目を通すようにしてください。

特にBGMやSEは選択によってゲームの雰囲気もガラッと変わるので、自分でお好みのBGMやSEを探すというのも楽しいと思います。

また、この先の実装にあたって使用する音楽とファイル名のリストは以下になりますので参考にしてください。(BGMおよびSEをこれらの名前で実装していないと、後のスクリプトでエラーが発生する箇所があります)

  • 使用するBGM名
    BGM_Title : タイトルシーンのBGM
    BGM_StageSelect : ステージセレクトシーンのBGM
    BGM_Battle : バトルシーンのBGM
  • 使用するSE名
    Decide : ボタン押下時の決定音
    Shot : プレイヤーショット発射時の効果音
    Barrier : バリア展開時の効果音
    Damaged : プレイヤー被弾時の効果音
    Victory : ボス撃破時に鳴る短い音楽
    Lose : プレイヤー消滅時に鳴る短い音楽

著作権・利用規約に要注意!

音楽はイラストと違い個人で用意するのは簡単ではない為、素材サイトなどを利用する事も多くなると思います。
その際は権利・規約への注意が必要です。

フリー素材の場合は無料で借用する事が出来ますが、内容の改変を禁止されていたりクレジット表記を必須としている場合が多いです。
他にも商用利用は規制されている場合が多いです。規約をよく読み、不安ならば著作者に確認を取りましょう。

有料素材であっても利用規約が定められているケースはほとんどなので確認は必須といえます。
また、いずれの場合も著作者を勝手に名乗る行為はNGです。

BGMを再生する

シーン毎に個別のBGMを再生するだけならプログラミングは不要です。
オブジェクトの設定だけで再生の準備が整います。

既存のオブジェクトに再生用のコンポーネントを取り付けても構いませんが、BGMの再生だけは区別してみましょう。
空のオブジェクトを作成し、名前を「SceneBGM」としてみます。
そこにAddComponentボタンから[Audio Source]を選択し取り付けます。

AudioSourceは音源の役割を果たすコンポーネントです。
スクリプトでの制御をしない場合、1つにつき1個の音楽ファイルを再生する機能を持ちます。(スクリプトを利用すれば多種の音楽を切り替えて再生も出来ます。)
インスペクターウィンドウから設定する主なパラメータを確認しましょう。

  • Audio Clip
    再生する音楽ファイルを指定します。
  • Play On Awake
    シーン開始時、またはこのコンポーネントを持つオブジェクトが出現した時に再生する設定です。
    これをオンにしない場合はスクリプトでの再生処理が必要です。
  • Loop
    音楽を終わりまで再生した時、自動的に先頭に戻って再生を続ける設定です。
  • Volume
    音量をスライダーで指定します。

他のパラメータについては慣れてきてから確認すると良いでしょう。

戦闘シーンであれば「BGM_Battle」をAudioClipにセットします。BGMの再生ですのでPlayOnAwakeとLoopはオンにしましょう。

1つのシーンで準備が出来たら、他のシーンにも同じオブジェクトを複製し、AudioClipの設定を変更しましょう。

SEを鳴らしてみよう

効果音はそれぞれ決まったタイミングで再生する必要がある為プログラミングが必要になります。
しかし、簡単な処理を追加するだけで済むので難しく考える必要はありません。

まずは「タイトル画面でボタンが押された時に効果音を再生」という処理を実装し流れを確認しましょう。

このタイミングでの再生ならTitleManagerスクリプトが処理を行うべきですね。ですので、最初はTitleManagerオブジェクトにAudioSourceコンポーネントを追加して設定を行います。

SE(効果音)の再生ですので、PlayOnAwakeとLoopの設定は不要ですのでチェックを外します。

後はスクリプトで再生の指示を出すだけです。
ボタン押下時に呼び出されるメソッドであるTransitionScene()に処理を追加します。

TitleManager.cs内TransitionScene()メソッド

	// StageSelectScene(ステージセレクト画面)へのシーン遷移を行うメソッド
	// ボタンUIの入力時に呼び出される
	public void TransitionScene ()
	{
		// StageSelectSceneをロードする
		GameObject.Find ("FadeManager").GetComponent<Fade> ().TransitionScene ("StageSelectScene");
		// ゲームオブジェクトに付いている効果音を再生する
		GetComponent<AudioSource> ().Play ();
	}

AudioSourceのPlay()メソッドを呼び出すだけで再生が行えるのが分かりましたね。
この調子で他シーンにも再生の処理を書き加えていきましょう。

ステージセレクトシーンの場合

タイトルシーンと全く同じです。
StageSelectManagerオブジェクトに先ほどと同じ設定でコンポーネントを追加し、スクリプトにも同じ処理を書き加えるだけです。

ちなみにコンポーネントはコピー&ペーストが可能です。ここで試してみても良いでしょう。

バトルシーンの場合

バトルシーンは、再生する効果音の種類が多く少々複雑です。

1つのオブジェクトに2つ以上同じコンポーネントを付けた場合はGetComponent<>()メソッドで呼び出すのもひと手間がかかります。
その場合はpublic変数を使用した方法でコンポーネントを指定しましょう。
インスペクターウィンドウ上でコンポーネントをドラッグすれば、オブジェクトの時のように直接参照をセットする事が可能です。

この方法を駆使して色々な効果音を慣らし分けましょう。

PlayerController.cs内メンバ変数宣言部

public AudioSource se_Shot;		// (効果音)ショット
public AudioSource se_Barrier;  // (効果音)バリア展開
public AudioSource se_Damaged;  // (効果音)被弾

ショット効果音であれば弾発射処理時に
se_Shot.Play ();と追加するだけでOKです。スクリプト全文は省略します。

続いて勝利時と敗北時の効果音を再生させます。これはGameManagerに担当させます。
プレイヤーの時と同様、public変数に参照をセットします。
PlayerController.cs内メンバ変数宣言部

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // UI機能の利用に必要なusing文
using UnityEngine.SceneManagement; // Scene機能の利用に必要なusing文

// ゲーム管理スクリプト
public class GameManager : MonoBehaviour {

	// メンバ変数宣言
	public GameObject imageObj_Health_1; // プレイヤー残り体力1を示すUI
	public GameObject imageObj_Health_2; // プレイヤー残り体力2を示すUI
	public GameObject imageObj_Health_3; // プレイヤー残り体力3を示すUI
	public Image      image_BossHealth;  // ボス残り体力表示UI

	public Image      image_EnergyGage;  // プレイヤー残りエネルギー表示UI

	public GameObject playerObj;		// プレイヤーオブジェクト
	public GameObject bossObj;			// ボスオブジェクト
	private bool afterFinish;			// 戦闘が終了しているかのフラグ
	private float afterFinishTime;      // 戦闘終了後の経過時間(秒)

	public Text text_Result;            // 戦闘結果表示UI

	public AudioSource se_Victory;	// (効果音)勝利
	public AudioSource se_Lose;		// (効果音)敗北

	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		//-----戦闘終了後処理-----
		if (!afterFinish)
		{ // 戦闘が終了していなければ
			if (playerObj == null)
			{ // プレイヤーか消滅していたら
				// 戦闘結果をUIに表示する
				text_Result.text = "Player Lose...";
				// 戦闘終了のフラグを立てる
				afterFinish = true;
				afterFinishTime = 0.0f;
				// 敗北効果音を再生
				se_Lose.Play ();
			}
			else if (bossObj == null)
			{ // ボスが消滅していたら
				// 戦闘結果をUIに表示する
				text_Result.text = "Player Win!!";
				// 戦闘終了のフラグを立てる
				afterFinish = true;
				afterFinishTime = 0.0f;
				// 勝利効果音を再生
				se_Victory.Play ();
			}
		}
		else
		{ // 戦闘が終了していたら
		  // 経過時間をカウント
			afterFinishTime += Time.deltaTime;
			if (afterFinishTime > 2.0f)
			{ // 戦闘終了から2秒が経過したら
				// ステージセレクト画面に遷移
				GameObject.Find ("FadeManager").GetComponent<Fade> ().TransitionScene ("StageSelectScene");
			}
		}
	}

	// プレイヤーの残り体力をUIに適用(PlayerControllerから呼び出される)
	// 引数health : 残り体力
	public void SetPlayerHealthUI (int health)
	{
		(省略)
	}

	// ボスの残り体力をUIに適用(BossControllerから呼び出される)
	// 引数health    : 残り体力
	// 引数maxHealth : 最大体力
	public void SetBossHealthUI (int health, int maxHealth)
	{
		(省略)
	}

	// プレイヤーの残りエネルギーをUIに適用(PlayerControllerから呼び出される)
	// 引数energy    : 残りエネルギー
	// 引数maxEnergy : 最大エネルギー
	public void SetEnergyUI (float energy, float maxEnergy)
	{
		(省略)
	}
}

これで音楽の実装は一通り完了です。
ユーザーの操作に対しては、何かしらの音を鳴らしてあげるとユーザーは心地良さを感じられます。ぜひ沢山の音楽でゲームを彩っていきましょう!

余談

毎回シーン中にAudioSourceを配置し、そこにAudioClipを設定しないといけない事に煩わしさを感じる場合があるかもしれません。
それはUnityのResources機能を使えば一応解決は可能です。

「Resources」という名前を付けられたフォルダは、中身の素材がシーン中に配置されていなくてもスクリプトから直接参照する事が可能です。
ただし実際はこの機能を使うのは非推奨です。Resourcesの中身はビルド時に特殊な扱いを受け、結果的としてビルド後のファイルサイズが増大する事になります。

使う時は最終手段だと思った方が良いでしょう。

 

パーティクルの実装


Unityには「パーティクルシステム」という特徴的な機能が存在します。
それはパーティクル(粒子によるアニメーション)をコンポーネント1つで実現できるというものです。

設定項目が多く複雑ですが、使いこなせばかなり応用の幅があり手軽にゲームのビジュアル性を向上させる事が出来ます。

今回は使い方の一例を体験し、その基礎に触れてみましょう。

ParticleSystemを触ってみる

ParticleSystemでは「常にアニメーションし続ける」設定と「花火のように一瞬だけ発生するアニメーション」の設定、あるいはそれらの併用が可能です。

このゲームでは「ショットが敵に命中した時、その場所で爆発する」という仕様のアニメをパーティクルで制作してみます。
プロセスとしては、一瞬だけ発生するパーティクルを空オブジェクトに付与し、それをプレハブ化してボススクリプトで生成する流れになります。

まずは空のオブジェクトを作成します。名前は「VanishParticle」等にします。
そしてParticleSystemコンポーネントを新規に取り付けます。しかし、最初は恐らく何も表示されません。
この場合はパーティクルが他のオブジェクトに隠れている事が考えられます。

パーティクルを利用する時は2Dゲーム開発であっても3Dの設定が絡む場合があります。
初期選択では発生したパーティクル(粒子)が画面奥側に広がっていってしまいます。
これはシーンビューの表示を3Dにする事で確認ができます。

ひとまずの解決策として、オブジェクトのZ座標を-1くらいにすれば一部は表示されるようになります。

また、パーティクルの1つ1つが紫色の四角で表示されてしまっています。
これは見た目の設定(Material)が未設定な為です。
ParticleSystemコンポーネントの中には沢山のタブがありますが、一番下のタブ[Renderer]をクリックして開き、
中のMaterialパラメータに何らかのマテリアルをセットすれば解決します。
デフォルトで「Default-Particle」というマテリアルが存在するので、それを選択して適用してください。

これでやっと初期の状態である「白い光が立て続けに外へと広がっていく」アニメーションが確認できます。

パーティクルの主な設定項目

それではパーティクルで重要度の高い設定項目について確認していきましょう。
設定を変えれば変化はリアルタイムで確認できるので、色々と調整をしながら確認してみてください。

  • (パーティクル名)タブ
    Duration : アニメーション1ループの長さ
    Looping : ループ設定
    Prewarm : ループ設定が有効の時、再生開始時に1ループ経ったような状態にする設定
    Start Lifetime : 粒子1つの生存時間
    Start Speed : 粒子1つの速度
    Start Size : 粒子1つの拡大率
    Start Color : 粒子の色
    Play On Awake : シーン開始時・コンポーネント生成時に再生するかの設定
  • Emissionタブ
    Rate over Time : 時間経過による粒子の生成量
    Bursts : 指定したタイミングに粒子を大量発生させる設定
  • Shapeタブ
    Shape : パーティクル発生領域の形状
    形状によって範囲の大きさや角度等を指定する

覚えておきたい設定項目など

実際にこのシステムで必要なアニメーションを制作する時、もう少し踏み込んだ設定が必要です。
まずStart LifetimeやStart Speedにはランダム性が欲しいです。

そんな時はパラメータ右端の▼ボタンから[Random Between Two Constants]を選択すると、2つの指定した値の中からランダムな値を粒子ごとにとってくれるようになります。

また、時間が経過するごとに粒子のサイズを小さくしたり大きくしたり…という設定が必要かもしれません。
その場合は[Size over Lifetime]タブの設定が役に立ちます。
タブの左端にあるチェックボックスにチェックを入れ、[Size]パラメータのグラフで変化量を変更しましょう。

目的に合った演出を実装

それでは今回の目標である「爆発するパーティクル」を作ってみましょう。

マテリアルの設定はそのままで、最初にEmissionタブの設定を変更します。
[Rate over Time]は0にします。そして「Bursts」を1つ追加しましょう。

続いてShapeタブの設定も変更します。
2DゲームですのでShape(形状)はCircleを選択するのが望ましいでしょう。
1点を中心に広がって欲しいので、Radiusには0を指定します。

先ほど説明に挙げた[Size over Lifetime]タブの設定もしましょう。
時間経過で徐々に粒子を小さくしたいのでグラフは右肩下がりのものを選択します。

後はメインタブ(パーティクル名のタブ)の設定を行います。1回きり再生のパーティクルなのでLoopingはオフにします。

StartLifetimeはランダムで0.1から1を、StartSpeedもランダムで1から2の値を取るように設定します。

後のパラメータは実際の見え方を確認しながら調整を行うとしましょう。

 

プレハブ化してスクリプトで再生

次にこのパーティクルオブジェクトを好きなタイミングで生成できるようプレハブ化しましょう。
プレハブ化が終わったら元のオブジェクトは削除し、ボススクリプトの編集に入ります。

BossController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BossController : MonoBehaviour {

	// メンバ変数宣言
	public GameManager gameManager; // ゲームマネージャー
	public GameObject bulletPrefab; // 弾のプレハブ
	public GameObject playerObj;	// プレイヤーオブジェクト
	public int health;		// 体力
	private int maxHealth;	// 最大体力
	private float time; // 経過時間(秒)

	public GameObject VanishParticlePrefab;	// 被弾パーティクルプレハブ

	// 起動時に1回だけ呼び出されるメソッド
	void Start ()
	{
		(省略)
	}

	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		(省略)
	}

	// プレイヤーへの角度計算メソッド
	float GetAngleToPlayer ()
	{
		(省略)
	}

	// 弾を発射するメソッド
	// 第1引数 speed : 速度
	// 第2引数 angle : 角度
	// 第3引数 limitTime : 生存時間(秒)
	void Shot (float speed, float angle, float limitTime)
	{
		(省略)
	}

	// 当たり判定内に他オブジェクトが侵入した際呼び出されるメソッド
	// 引数:接触オブジェクトしたオブジェクトのCollider情報
	void OnTriggerEnter2D (Collider2D collider)
	{
		// プレイヤーが発射した弾でなければ処理を終了
		if (collider.gameObject.name != "PlayerBullet")
		{ // 接触オブジェクト名がPlayerBulletで無ければ
			return; // メソッド終了
		}

		// 被弾パーティクルを発生させる
		GameObject obj = Instantiate (VanishParticlePrefab);
		// 被弾パーティクルの座標を弾の座標に変更
		obj.transform.position = collider.gameObject.transform.position;
		// 被弾パーティクルの消滅を3秒後に予約
		Destroy (obj, 3.0f);

		// 弾オブジェクトを消滅させる
		Destroy (collider.gameObject);

		// 自身の体力を1減らす
		health--;
		// 現在体力をUIに表示
		gameManager.SetBossHealthUI (health, maxHealth);
		// ボス消滅処理
		if (health <= 0)
		{// ボスの体力が0以下
			Destroy (gameObject); // 自オブジェクト消去
		}
	}
}

変数VanishParticlePrefabには先ほど用意したプレハブをセットしましょう。
Destroy()メソッドは即時の消去だけでなく、「指定した秒数後に消去」という指示にする事も可能です。


変更が完了したらテストプレイを行い、ボス被弾時にパーティクルが発生している事を確認しましょう。
中々1回では望んだ形のパーティクルを作成する事は難しいため、細かく設定を変えて繰り返し試してみると良いでしょう。

 

シーン間での情報の伝達

ここまでのゲーム開発で複数のシーンを扱ってきましたが、ゲーム中シーンの切り替えをすると全てのオブジェクトが破棄されるのでシーンをまたいでの情報の伝達が出来ませんでした。
このような状態では、タイトル画面にオプション設定の項目を追加したとしても、後のシーンではその設定情報を利用できない事になります。

それを解決する方法として、ゲームオブジェクトに対して「シーン切り替え時に自動で破棄されない」という設定を付加する事がスクリプトから可能です。
全シーンに存在し続けるオブジェクト(のスクリプト)が情報を管理すれば、シーン間での情報の受け渡しも可能ですね。

今回はこの方法を利用して、「ボス撃破の最速タイム」という情報をバトルシーンからステージセレクトシーンに渡して表示させてみたいと思います。

データ管理オブジェクト&スクリプトの作成

まずはデータを管理するオブジェクトを用意しましょう。

これはタイトルシーンで作成する必要があります。
今後「シーン切り替え時に破棄されない」設定を付与しますが、そのオブジェクトを持つシーンが繰り返し読み込まれると同じオブジェクトも繰り返し生まれ続ける事になります。
よって一度しか読み込まれないシーンであるタイトルシーンでこのオブジェクトの作成を行いましょう。

まずは空のオブジェクトを作成し、名前は「DataManager」とします。
続いて「Data」スクリプトを作成してオブジェクトにアタッチしましょう。

そしてDataスクリプトに処理を書き込みます。
「シーン切り替え時に破棄されない」設定を行うのと、保存したいデータを変数で宣言するだけでOKです。
データの利用は各シーンのManagerに任せるので、ここはシンプルに纏まります。

Data.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Data : MonoBehaviour {

	// メンバ変数宣言
	public float BestTime_01; // ステージ1最速クリアタイム

	// 起動時に1回だけ呼び出されるメソッド
	void Start () {
		// オブジェクトに「シーン切り替え時に破棄されない」設定を付与
		DontDestroyOnLoad (gameObject);

		// 初期化処理
		BestTime_01 = 99.99f;
	}
}

これでDataManagerオブジェクトはシーンをまたいでも破棄されなくなりました。
メンバ変数で受け渡すデータを管理していきます。

後はデータの利用側のスクリプトを書き、動作を確認していきましょう。

注意点

タイトルシーンでのみデータ管理オブジェクトの作成が行われるようになっている為、それ以外のシーンでテストプレイをすると正常な動作確認が難しくなります。

しかし「常に自身が1つのみ存在する」という設定を付与できれば、プレハブ化したそのオブジェクトを全てのシーンに配置しても誤作動は起こらなくなります。
その設定はやや複雑な処理になる為、ここでは割愛しますが気になる方は「シングルトンオブジェクト」という語句で調べてみましょう。

戦闘時間の計測と表示

次はバトルシーンにて戦闘時間の計測を行います。
計測だけでなく表示も行うので、まずはTextUIを用意しましょう。以下の設定例を参考に作ってみてください。

GameManagerが計測した戦闘時間をこのUIに表示させます。
ToString()メソッドは引数での指示によって文字にする小数部分等を指定できます。

GameManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // UI機能の利用に必要なusing文

// ゲーム管理スクリプト
public class GameManager : MonoBehaviour {

	// メンバ変数宣言
	public GameObject imageObj_Health_1; // プレイヤー残り体力1を示すUI
	public GameObject imageObj_Health_2; // プレイヤー残り体力2を示すUI
	public GameObject imageObj_Health_3; // プレイヤー残り体力3を示すUI
	public Image      image_BossHealth;  // ボス残り体力表示UI

	public Image      image_EnergyGage;  // プレイヤー残りエネルギー表示UI

	public GameObject playerObj;		// プレイヤーオブジェクト
	public GameObject bossObj;			// ボスオブジェクト
	private bool afterFinish;			// 戦闘が終了しているかのフラグ
	private float afterFinishTime;      // 戦闘終了後の経過時間(秒)
	public Text text_Result;            // 戦闘結果表示UI

	public Text text_battleTime;		// 戦闘時間表示UI
	private float battleTime;           // 戦闘時間(秒)

	public AudioSource se_Victory;	// (効果音)勝利
	public AudioSource se_Lose;		// (効果音)敗北

	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		// 戦闘時間をカウントして表示
		if (!afterFinish)
		{ // 戦闘が終了していなければ
			// 経過時間をカウント
			battleTime += Time.deltaTime;
			text_battleTime.text = "Time : " + battleTime.ToString ("F2");
		}

		//-----戦闘終了後処理-----
		if (!afterFinish)
		{ // 戦闘が終了していなければ
			if (playerObj == null)
			{ // プレイヤーか消滅していたら
				// 戦闘結果をUIに表示する
				text_Result.text = "Player Lose...";
				// 戦闘終了のフラグを立てる
				afterFinish = true;
				afterFinishTime = 0.0f;
				// 敗北効果音を再生
				se_Lose.Play ();
			}
			else if (bossObj == null)
			{ // ボスが消滅していたら
				// 戦闘結果をUIに表示する
				text_Result.text = "Player Win!!";
				// 戦闘終了のフラグを立てる
				afterFinish = true;
				afterFinishTime = 0.0f;
				// 勝利効果音を再生
				se_Victory.Play ();
				// 撃破時間処理
				Data data = GameObject.Find ("DataManager").GetComponent<Data> (); // データスクリプトを取得
				if (battleTime < data.BestTime_01)
				{ // 戦闘時間がステージの最速タイムより短ければ
				  // 最速タイムを更新
					data.BestTime_01 = battleTime;
				}
			}
		}
		else
		{ // 戦闘が終了していたら
		  // 経過時間をカウント
			afterFinishTime += Time.deltaTime;
			if (afterFinishTime > 2.0f)
			{ // 戦闘終了から2秒が経過したら
				// ステージセレクト画面に遷移
				GameObject.Find ("FadeManager").GetComponent<Fade> ().TransitionScene ("StageSelectScene");
			}
		}
	}

	// プレイヤーの残り体力をUIに適用(PlayerControllerから呼び出される)
	// 引数health : 残り体力
	public void SetPlayerHealthUI (int health)
	{
		(省略)
	}

	// ボスの残り体力をUIに適用(BossControllerから呼び出される)
	// 引数health    : 残り体力
	// 引数maxHealth : 最大体力
	public void SetBossHealthUI (int health, int maxHealth)
	{
		(省略)
	}

	// プレイヤーの残りエネルギーをUIに適用(PlayerControllerから呼び出される)
	// 引数energy    : 残りエネルギー
	// 引数maxEnergy : 最大エネルギー
	public void SetEnergyUI (float energy, float maxEnergy)
	{
		(省略)
	}
}

別シーンで最速タイムを表示

これで戦闘時間の記録と表示はOKです。後はステージセレクトシーンでこの情報を利用する所まで試してみましょう。

こちらも同じく、まずは最速記録の表示UIを用意する所からです。
以下の設定を参考にTextUIを作ってみましょう。

スクリプトも似たようなものです。慣れてきたでしょうか?

StageSelectManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // UI機能の利用に必要なusing文

public class StageSelectManager : MonoBehaviour {

	// メンバ変数宣言
	public Text text_BestTime_1; // ステージ1最速クリアタイム表示UI
	
	// 起動時に1回だけ呼び出されるメソッド
	void Start ()
	{
		// 最速クリアタイムをUIに表示
		Data data = GameObject.Find ("DataManager").GetComponent<Data> (); // データスクリプトを取得
		text_BestTime_1.text = "BestTime : \n" + data.BestTime_01.ToString ("F2");
	}

	// SampleScene(バトル画面)へのシーン遷移を行うメソッド
	// ボタン[StageButton_1]の入力時に呼び出される
	public void TransitionScene_Stage1 ()
	{
		// SampleSceneをロードする
		GameObject.Find ("FadeManager").GetComponent<Fade> ().TransitionScene ("SampleScene");
		// ゲームオブジェクトに付いている効果音を再生する
		GetComponent<AudioSource> ().Play ();
	}
}

これでシーン間の情報の受け渡し方法を学びつつ、戦闘時間の記録が可能になりました。
ゲームに手軽に競技性を持たせる手段としてタイムアタックやスコアアタックはなかなか効果的です。

 

新しいボスの追加

応用編での実装内容も最後となりました。このゲームに複数のボスを追加し、色々なバリエーションの攻撃を楽しめるようにしましょう!
ボスの画像は使いまわしとしますが、十分慣れてきた方は各自別の画像を用意してセットしてみてください。その際、当たり判定の調整は必要になります。

シーンの複製

新規ボスを追加するにあたって、ここではボス1体ごとに1つのシーン(ステージ)を用意する事にします。
バトルシーンのシステム自体はこれでほぼ完成として、このシーンを使いまわしてボスのパラメータだけを変更させるようにします。
ボスのスクリプトも今までと同じものを用いて拡張する形にします。
つまり、ボスは自身が何番目のボスであるかという情報を得て、それによって処理を分岐させる方向性です。

それではシーンの複製を行うのでバトルシーンを開きましょう。
Assetsフォルダでのコピーペーストでも問題はありませんが、もっと簡単な方法があります。
メニュー[File]→[Save Scene As…]で別の名前を付けてシーンを保存するだけです。

今回はボスを2体追加するので、新しいシーンを「Stage2」と「Stage3」にしました。
ついでに今まで使っていたバトルシーンの名前も「Stage1」に変えてしまいます。

シーンを追加したらBuildSettingsウィンドウでの設定も忘れずに行っておきましょう。

別のやり方

今回はシーンを複製してボスだけ変えるという手法を取りましたが、別の手法として全ステージを1つのシーンでカバーする事も可能です。
その手法のメリットは、全ステージ共通で使用するオブジェクト等をプレハブ化の必要なく1つにまとめられる点です。
余力があったらチャレンジしてみましょう。

ステージセレクト画面の設定

ボスの変更を行ってからでも良いですが、先にステージセレクトシーンの設定も変更しておきます。
選択可能ステージが3つに増えたのと、シーン名の変化があるためそれに対応します。

以下を参考に、まずはUIを配置してみましょう。

スクリプトでは各ボタン押下時に呼び出されるメソッドをそれぞれ用意します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // UI機能の利用に必要なusing文

public class StageSelectManager : MonoBehaviour {

	// メンバ変数宣言
	public Text text_BestTime_1; // ステージ1最速クリアタイム表示UI
	public Text text_BestTime_2; // ステージ2最速クリアタイム表示UI
	public Text text_BestTime_3; // ステージ3最速クリアタイム表示UI

	// 起動時に1回だけ呼び出されるメソッド
	void Start ()
	{
		// 最速クリアタイムをUIに表示
		Data data = GameObject.Find ("DataManager").GetComponent<Data> (); // データスクリプトを取得
		text_BestTime_1.text = "BestTime : \n" + data.BestTime_01.ToString ("F2");
		text_BestTime_2.text = "BestTime : \n" + data.BestTime_02.ToString ("F2");
		text_BestTime_3.text = "BestTime : \n" + data.BestTime_03.ToString ("F2");
	}

	// SampleScene(バトル画面)へのシーン遷移を行うメソッド
	// ボタン[StageButton_1]の入力時に呼び出される
	public void TransitionScene_Stage1 ()
	{
		// SampleSceneをロードする
		GameObject.Find ("FadeManager").GetComponent<Fade> ().TransitionScene ("Stage1");
		GetComponent<AudioSource> ().Play (); // ゲームオブジェクトに付いている効果音を再生する
	}
	// ボタン[StageButton_2]の入力時に呼び出される
	public void TransitionScene_Stage2 ()
	{
		// SampleSceneをロードする
		GameObject.Find ("FadeManager").GetComponent<Fade> ().TransitionScene ("Stage2");
		GetComponent<AudioSource> ().Play (); // ゲームオブジェクトに付いている効果音を再生する
	}
	// ボタン[StageButton_3]の入力時に呼び出される
	public void TransitionScene_Stage3 ()
	{
		// SampleSceneをロードする
		GameObject.Find ("FadeManager").GetComponent<Fade> ().TransitionScene ("Stage3");
		GetComponent<AudioSource> ().Play (); // ゲームオブジェクトに付いている効果音を再生する
	}
}

データスクリプト(Data.cs)に新ステージ用のタイム記録変数が足りなくてエラーが出ますので以下の2つを宣言しましょう。

public float BestTime_02; // ステージ2最速クリアタイム
public float BestTime_03; // ステージ3最速クリアタイム

まだオブジェクト側の設定が残っていますね。ステージ選択ButtonのOnClick()パラメータにシーン遷移メソッドを設定してあげます。

もう少し下準備は続きます。

GameManagerでの対応

シーンが増えた影響で、GameManagerスクリプトでも「最速タイムをデータスクリプトにセット」という処理が複雑になります。
この処理はメソッド化して作成しましょう。

GameManager.cs内新規メソッド

	// 戦闘勝利時に戦闘時間をベストタイムと比較し、より短ければ記録を更新するメソッド
	void CheckBestTime ()
	{
		// データオブジェクトを取得
		GameObject dataObj = GameObject.Find ("DataManager");
		if (dataObj == null)
			return;
		// データスクリプトを取得
		Data data = dataObj.GetComponent<Data> ();

		// 現在のステージ番号を取得
		// (アクティブシーンの名前の6文字目を取得し、それを数値に変換してセット)
		int num = SceneManager.GetActiveScene ().name[5] - '0';

		// 戦闘時間がステージの最速タイムより短ければ最速タイムを更新する処理
		if (num == 1)
		{ // ステージ1の場合
			if (battleTime < data.BestTime_01)
				data.BestTime_01 = battleTime;
		}
		else if (num == 2)
		{ // ステージ2の場合
			if (battleTime < data.BestTime_02)
				data.BestTime_02 = battleTime;
		}
		else if (num == 3)
		{ // ステージ3の場合
			if (battleTime < data.BestTime_03)
				data.BestTime_03 = battleTime;
		}
	}

メソッドを追加したら、これを戦闘終了後処理の「撃破時間処理」で呼び出します。今まで書いてあった処理は消しておきましょう。

このメソッドを見ると分かりますが、シーン名の6文字目を見て何番目のステージかを判断するという強引な処理を行っています。
よって「using UnityEngine.SceneManagement;」の分も必要です。
これから書くボススクリプトの方ではちゃんとpublic変数で何番目のボスかをセットしてあげる事にします。

ここまで出来たら動作確認を行い、それぞれのステージを選択してクリアした時ベストタイムが更新される事を確認してください。
うまくいかない場合はシーンの切り替え処理のミスかpublic変数への参照の渡し方のミス、あるいはButtonコンポーネントのOnClick()の設定にミスがあるかもしれません。

現在開いているシーンが何ステージかが分かりづらい場合、先に下記のボス設定を行ってしまいましょう。

ボスのスクリプト調整

まだボスがステージごとに全く同じ行動を行う為、まずは差別化を図らせてみたいと思います。
本格的な行動パターンの構築は後回しにし、とりあえず「違うボスである」事が分かるようになれば良いでしょう。

publicメンバ変数で何ステージのボスであるかを指定できるようにしつつ、その値によって行動パターンを変化させます。

BossController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BossController : MonoBehaviour {

	// メンバ変数宣言
	public GameManager gameManager; // ゲームマネージャー
	public GameObject bulletPrefab; // 弾のプレハブ
	public GameObject playerObj;    // プレイヤーオブジェクト
	public int stage;		// ステージ番号
	public int health;		// 体力
	private int maxHealth;	// 最大体力
	private float time; // 経過時間(秒)

	public GameObject VanishParticlePrefab;	// 被弾パーティクルプレハブ

	// 起動時に1回だけ呼び出されるメソッド
	void Start ()
	{
		// 初期化処理
		maxHealth = health; // 初期体力を最大体力とする
	}

	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		// 経過時間をカウント
		time += Time.deltaTime;

		// -----弾発射処理-----
		if (playerObj != null)
		{ // プレイヤーオブジェクトが存在する時
			// ステージ番号ごとに違うパターンで弾を発射する
			// ----ステージ1----
			if (stage == 1)
			{
				// 0.4秒ごとに弾を発射
				if (time > 0.4f)
				{ // 経過時間が0.4秒より大きければ
				  // 経過時間をリセット
					time -= 0.4f;
					
					// 自機狙い弾を発射
					Shot (5.0f, GetAngleToPlayer (), 5.0f);
				}
			}
			// ----ステージ2----
			if (stage == 2)
			{
				// 0.4秒ごとに弾を発射
				if (time > 0.4f)
				{ // 経過時間が0.4秒より大きければ
				  // 経過時間をリセット
					time -= 0.4f;
					
					// 3way弾を発射
					Shot (5.0f, GetAngleToPlayer () - 20.0f, 5.0f);
					Shot (5.0f, GetAngleToPlayer (), 5.0f);
					Shot (5.0f, GetAngleToPlayer () + 20.0f, 5.0f);
				}
			}
			// ----ステージ3----
			if (stage == 3)
			{
				// 0.4秒ごとに弾を発射
				if (time > 0.4f)
				{ // 経過時間が0.4秒より大きければ
				  // 経過時間をリセット
					time -= 0.4f;

					// 高速弾を発射
					Shot (8.0f, GetAngleToPlayer (), 5.0f);
					// 3way弾を発射
					Shot (5.0f, GetAngleToPlayer () - 20.0f, 5.0f);
					Shot (5.0f, GetAngleToPlayer (), 5.0f);
					Shot (5.0f, GetAngleToPlayer () + 20.0f, 5.0f);
				}
			}
		}
	}

	// プレイヤーへの角度計算メソッド
	float GetAngleToPlayer ()
	{
		(省略)
	}

	// 弾を発射するメソッド
	// 第1引数 speed : 速度
	// 第2引数 angle : 角度
	// 第3引数 limitTime : 生存時間(秒)
	void Shot (float speed, float angle, float limitTime)
	{
		(省略)
	}

	// 当たり判定内に他オブジェクトが侵入した際呼び出されるメソッド
	// 引数:接触オブジェクトしたオブジェクトのCollider情報
	void OnTriggerEnter2D (Collider2D collider)
	{
		(省略)
	}
}

スクリプトが用意できたら各ステージのシーンを開き、それぞれのボスオブジェクトにステージ番号と体力をセットします。
後半のボス程難しくしたいので、体力にも変化を与えると良いでしょう。

これでステージごとに違うボスが違う行動パターン・体力を持っている仕様が出来ました。
このままでも弾の発射数を変えたりする事で十分なバリエーションを持たせる事が可能です。

しかしここは応用編ですので、更に一歩踏み込んで特殊弾を作ってみましょう。

追尾弾の追加

折角新しいボスを追加したので、新しい形の攻撃も行ってきて欲しいですね。
ここではアクションゲームの定番である「自機誘導弾」を作ってみます。
自機狙い弾と違うのは、発射後も弾自体がプレイヤーに向かって進行角度を変え続ける点です。

Bulletスクリプトの改良

新種の弾を追加する場合、本当はオブジェクトもスクリプトも新しい物を用意するのが望ましいですが、
今回はシンプルにBulletスクリプトをそのまま拡張して利用します。

ボスの弾発射処理時に、Bulletスクリプトに対して「これは誘導弾である」という情報を渡せば後は通常弾と違う処理をさせられますね。
その方向性でスクリプトを組んでみましょう。

Bullet.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour {

	// メンバ変数宣言
	private float time = 0.0f;  // 経過時間
	// 生成時指定パラメータ
	public float speed;		// 速度
	public float angle;     // 角度(Degree)
	public float limitTime; // 生存時間(秒)

	public bool mode_Homing;		// 誘導弾モード
	public GameObject homingTarget;	// 誘導対象オブジェクト

	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		// ローカル変数宣言
		float rad = angle * Mathf.Deg2Rad; // 角度(Radian)
		Vector2 vec; // 移動量ベクトル(x方向, y方向)
		float move_x, move_y; // x方向、y方向それぞれの移動量格納用

		// -----特殊弾処理-----
		if (mode_Homing)
		{
			if (homingTarget != null)
			{ // 誘導対象オブジェクトが存在していれば
				// 誘導対象の座標を取得
				Vector2 target = homingTarget.transform.position;
				// 誘導対象への角度を計算
				rad = Mathf.Atan2 (target.y - transform.position.y, target.x - transform.position.x);
			}
		}

		// -----移動処理-----
		// 速度と角度から移動量を算出
		move_x = Mathf.Cos (rad) * speed * Time.deltaTime; // x方向の移動量
		move_y = Mathf.Sin (rad) * speed * Time.deltaTime; // y方向の移動量
		vec = new Vector2 (move_x, move_y); // 変数vecを初期化しつつ、xとyの移動量をセット

		// 変数vec分だけ自オブジェクトを移動
		transform.Translate (vec);

		// -----寿命処理-----
		// 前回のUpdate実行から経過した時間をtimeに加算
		time += Time.deltaTime;
		// 消滅処理
		if (time > 5.0f)
		{ // 弾の経過時間が5秒より大きければ
			Destroy (gameObject);
		}
	}
}

ボス側の発射処理

通常弾の処理と共存させつつ誘導弾の実装が出来ました。
後はボススクリプトでこの弾の発射メソッドを作成します。

BossController.cs内新規メソッド

	// 誘導弾を発射するメソッド
	void Shot_Homing (float speed, float angle, float limitTime)
	{
		// GameObject型ローカル変数を宣言
		GameObject obj;
		// 弾プレハブのインスタンスを生成し、変数objに格納
		obj = Instantiate (bulletPrefab);
		// 弾インスタンスの座標にボスの座標をセット
		obj.transform.position = transform.position;
		// インスタンスのオブジェクト名を変更(自弾と区別するため)
		obj.name = "EnemyBullet";
		// 弾のパラメータをセット
		obj.GetComponent<Bullet> ().speed = speed;          // 速度
		obj.GetComponent<Bullet> ().angle = angle;          // 角度
		obj.GetComponent<Bullet> ().limitTime = limitTime;  // 生存時間
		obj.GetComponent<Bullet> ().mode_Homing = true;			// 誘導弾モードをON
		obj.GetComponent<Bullet> ().homingTarget = playerObj;	// 誘導対象を指定
		// 弾の色を変更
		obj.GetComponent<SpriteRenderer> ().color = Color.blue; // 青
	}

このメソッドの使い方は通常弾発射であるShot()メソッドと全く同じです。
ステージ2辺りのボスに発射させて挙動を確認してみましょう。

反射弾の追加

最後にもう一種類だけ特殊弾を追加してみましょう。
次に追加する弾は、普通に直進して画面端にたどり着いた時一度だけ反射をするという「反射弾」にします。
プレイヤーは真後ろにも気を付ける必要が出てくるので、新鮮な感覚が味わえます。

画面端で反射する処理

先ほどの弾スクリプトを更に拡張しましょう。

Bullet.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour {

	// メンバ変数宣言
	private float time = 0.0f;  // 経過時間
	// 生成時指定パラメータ
	public float speed;		// 速度
	public float angle;     // 角度(Degree)
	public float limitTime; // 生存時間(秒)

	public bool mode_Homing;        // 誘導弾モード
	public GameObject homingTarget; // 誘導対象オブジェクト

	public bool mode_Bounding;	// 反射弾モード
	public bool bounded;		// 反射済みフラグ

	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		// ローカル変数宣言
		float rad = angle * Mathf.Deg2Rad; // 角度(Radian)
		Vector2 vec; // 移動量ベクトル(x方向, y方向)
		float move_x, move_y; // x方向、y方向それぞれの移動量格納用

		// -----特殊弾処理-----
		if (mode_Homing)
		{ // 誘導弾処理
			(省略)
		}
		else if (mode_Bounding)
		{ // 反射弾処理
			if (!bounded)
			{ // 一度も反射していなければ
				// 変数宣言
				Vector2 screenPos;	// 画面のサイズ
				bool check = false; // 画面端に接触した判定

				// 画面のサイズを取得(スクリーン座標)
				screenPos = Camera.main.WorldToScreenPoint (transform.position);

				// 画面端の接触を検出
				if (screenPos.x <= 0.0f ||
					screenPos.x >= Screen.width)
				{ // 左右端に接触
					check = true;
					angle = 180.0f - angle;	// 反射した角度を計算してセット
				}
				if (screenPos.y <= 0.0f ||
					screenPos.y >= Screen.height)
				{ // 上下端に接触
					check = true;
					angle = 360.0f - angle; // 反射した角度を計算してセット
				}
				
				// 反射時判定
				if (check)
				{
					bounded = true; // 反射済みをセット
					// 反射済みである事が分かりやすくなるよう色を変更
					GetComponent<SpriteRenderer> ().color = Color.yellow;
				}
			}
		}

		// -----移動処理-----
		(省略)

		// -----寿命処理-----
		(省略)
	}
}

これで反射弾に設定した弾は画面端で1回だけ反射するようになります。
反射した後は色を変化させる事で、どの弾が反射済みかを分かりやすくします。

最後にボスで反射弾発射を用意しましょう。

BossController.cs内新規メソッド

	// 反射弾を発射するメソッド
	void Shot_Bounding (float speed, float angle, float limitTime)
	{
		// GameObject型ローカル変数を宣言
		GameObject obj;
		// 弾プレハブのインスタンスを生成し、変数objに格納
		obj = Instantiate (bulletPrefab);
		// 弾インスタンスの座標にボスの座標をセット
		obj.transform.position = transform.position;
		// インスタンスのオブジェクト名を変更(自弾と区別するため)
		obj.name = "EnemyBullet";
		// 弾のパラメータをセット
		obj.GetComponent<Bullet> ().speed = speed;          // 速度
		obj.GetComponent<Bullet> ().angle = angle;          // 角度
		obj.GetComponent<Bullet> ().limitTime = limitTime;  // 生存時間
		obj.GetComponent<Bullet> ().mode_Bounding = true;     // 反射弾モードをON
		// 弾の色を変更
		obj.GetComponent<SpriteRenderer> ().color = Color.green; // 緑
	}

これで反射弾の処理も完了です。
これと誘導弾、通常弾を組み合わせ、自由にボスの攻撃パターンを組んでいってください!

(おまけ)サンプルゲームを超えてみよう!

ここまでの仕様を全て実装し、ボスの攻撃パターン等に少しの調整を加えたのがこの講座で公開しているサンプルゲームになっております。
これと近いものは実装できたでしょうか?

ここまでUnityの学習を続けてきた皆さんなら既に応用力が備わっています。
ぜひこのゲームにプラスアルファの要素を追加し続けてみてください。そして是非、サンプルゲームの出来を大きく超える作品に仕上げてみてください!

改良点のヒント

筆者がパッと思い付いた、このゲームに欲しい改良点の一覧です。

  • ボスをもっと増やしたい
    ボスとの1体1がメインのゲームですので、更にボスの種類が欲しい。
  • プレイヤーをもっと強く
    例えばプレイヤーにチャージショットの能力があれば、よりアクション性が増す。
  • 画面をもっと綺麗に
    テクスチャ等をより優れたものにしたい。
    また、[Trail Renderer]コンポーネントをうまく使用すると弾の軌跡を表示できる。
  • 敵弾にかする(グレイズ)判定
    例えば敵弾にかすった時エネルギーが回復するようにすると、ゲーム性が増す。
  • 育成機能の実装
    ボスを倒すごとにプレイヤーの体力やショット威力が上がるとリプレイ性が増す。
    Dataスクリプトを利用すれば実装できる。

改良点をさらに考える

プレイヤーがゲームに求める要素は千差万別です。それと同じように、開発者が作りたいゲームも様々です。
ぜひあなたの感性で求める機能を追加していってみましょう!

きっとUnityのスクリプトリファレンスは少しずつ読めるようになっているはずです。
新しいロジックを考えて新要素を実装していくのも効果的な学習になります。

おわりに

お疲れ様でした。以上で講座「0から2Dアクションバトルゲームを作ろう!」は終了となります。ここまで読んでいただきありがとうございました。
ここで学んだゲーム開発のプロセスはほんの一例です。ぜひ色々なジャンルのゲーム開発にチャレンジして、経験を積んでいきましょう!

 

今回のチュートリアルの感想やこのようなゲームのチュートリアルを掲載してほしいという場合には、
Twitterにて「#アズでゲーム制作,#unity」という2つのハッシュタグを付けてツイートしていただければ運営が定期的に確認いたします。

特に、感想をツイートしていただけると運営のモチベーションになります!(Twitterののいいねやリツイートもとても嬉しいです)

また、質問やわかりにくい箇所についても同様のハッシュタグでツイートしていただければ、質問に回答したり記事の内容を改善したりするかもしれません

↓フォローはこちらから

 

(最後にunityroomにて公開中の完成版のデータをこちらで公開いたします!) *現在準備中です
よき、「ゲーム制作ライフ」を!