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

0からアクションゲームを作ろう講座その9です。
講座も応用編に突入しました。様々な追加要素を実装していきましょう!

この章でやること

前章まででゲームを基礎的な要素のみで完成させ、ブラウザで遊べるようにしました。
しかしゲームの楽しさ・面白さという点では大きな不足がありました。

今章では発展的な学習をしつつ、ゲームを楽しくする試みを取り入れていきましょう!

今回実装する要素一覧

この章で実装する内容、及びそこで学ぶ技術は次のようになります。

  • ボスの体力を表示をゲージ形式に変更
    Fillを利用してテクスチャの一部のみを表示する方法など
  • プレイヤーにエネルギーの概念を追加し、行動(ショット・バリア)で消費させる
  • プレイヤーに無敵時間を実装
    新機能追加時に行うアプローチやバランス調整など
  • ステージセレクト画面の作成
    シーンの作成や遷移に関する復習など
  • シーン遷移をスムーズに行うよう改良
    フェードインとフェードアウトの実装
  • バトル終了後に勝利キャラを表示
    Textコンポーネントの応用的な利用

 

全て終える頃にはよりゲームらしくなっていますよ!

 

ボスの体力をゲージで表示

まず手を付けていきたい改善点はボスの体力表示です。
UIの機能で表示させる事は同じですが、文字ではなく画像を使った表現に変えたいですね。

今回はFillの機能を利用して「体力が減ったら画像が小さくなる」、つまりゲージ形式での表現に変更してみましょう。
以下がゲージ本体のサンプル画像です。

前章の最後で行ったフォルダ整理を思い出し、Texturesフォルダにインポートしましょう。

Imageオブジェクトの追加と設定

まず今まで使っていたBossHealthTextオブジェクトは不要になるので削除しておきましょう。
そして新たにゲージ表示を行うImageUIオブジェクトを作成します。オブジェクト名は「BossHealthGage」にしました。

テクスチャのセットまで出来たら次は「ゲージが最大まで伸びている状態」でUIの位置・サイズ調整をします。
今回は画面最下部に横幅いっぱいに伸ばしてみました。一緒に色も付けておきます。

この後はFillの設定を行い、ボスの体力に応じてこのゲージが短くなるようにします。

fillとは?

テクスチャの一部のみを表示し、その表示量(割合)をスクリプトで制御するにはImageコンポーネントでFillの設定が必要となります。
最初に[Image Type]パラメータでFilledを選択し、そこで出現するFill関係パラメータに適切な値を入力します。
各パラメータは以下の内容を表しています。

  • Fill Method
    テクスチャの非表示・表示をどの方向で行うかを指定します。詳細は下記です。
  • Fill Origin
    非表示・表示を開始する時の始点となる位置や方向です。
    Fill Methodによって選択項目も変化します。
  • Fill Amount
    テクスチャの表示量(割合)です。これをスクリプトで制御する事でゲージ表示が実現します。
    0~1の小数で指定します。
  • Clockwise
    表示・非表示の方向の時計周り・反時計周りを指定します。Fill Methodの設定によっては選択不要です。

FillMethodにHorizontalを指定すると、FillAmountの数値を減少させた時テクスチャが右側から非表示になっていきます。今回はこの設定を使用します。
他にも選択できるFillMethodの項目を確認しましょう。

  • Horizontal
    FillAmountの数値によって水平方向に表示・非表示をします。
  • Vertical
    垂直方向に表示・非表示を行います。
  • Radial 90
    いずれかの角を起点として放射状に行います。
  • Radial 180
    いずれかの縁の一部を起点として放射状に行います。
  • Radial 360
    テクスチャの中心を起点として放射状に行います。

これでオブジェクト側の設定は終わりです。

スクリプト側の設定

ボスの体力表示を行ってきたGameManagerスクリプトの変更を行いましょう。
オブジェクトが変わっているので、インスペクターウィンドウからの再セットも忘れずに。
GameManager.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 GameObject playerObj;		// プレイヤーオブジェクト
	public GameObject bossObj;			// ボスオブジェクト
	private bool afterFinish;			// 戦闘が終了しているかのフラグ
	private float afterFinishTime;      // 戦闘終了後の経過時間(秒)

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

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

	// ボスの残り体力をUIに適用(BossControllerから呼び出される)
	// 引数health    : 残り体力
	// 引数maxHealth : 最大体力
	public void SetBossHealthUI (int health, int maxHealth)
	{
		// ボスの残り体力の比率を計算
		float ratio; // ボス残り体力の割合(0~1)
		ratio = (float)health / maxHealth; // float型へのキャストが必要

		// 画像のfillAmountに比率をセット
		image_BossHealth.fillAmount = ratio;
	}
}

int型の変数同士で除算を行っても整数でしか商を得られないので、float型数値で得る為には変数名の頭に(float)という文言を入れる必要があります。
これを変数のキャストと言います。これでこの演算を行っている間だけ、変数healthはfloat型のように扱えます。

またメソッドの引数の変えたので、これを呼び出しているボスのスクリプトにも微修正が必要です。
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; // 経過時間(秒)

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

	// 毎フレーム呼び出されるメソッド
	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; // メソッド終了
		}

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

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

自身の初期体力を体力の最大値とし、この2つの数値から残り体力を割合として算出しています。

また、GameManagerに「BossHealthGage」をセットするのを忘れないでください。

変更が完了すれば、ボスの体力が減少した時ゲージも減少するようになっています。

ボスの体力を調整

ゲームの難易度調整はこれから行っていきますが、ひとまずゲージ機能の確認も兼ねてボスの体力をもっと増やしてみましょう。
一度行った事なので大丈夫だと思いますが、ボスの初期体力はpublic型変数で決めている為インスペクターウィンドウから変更できます。

エネルギー要素の実装

7章でプレイヤーに与えたバリア能力が強力すぎてバランスを崩しています。続いては、これの改善を行いますが、どのようなアプローチで行えば良いのでしょうか?

プレイヤーの能力にも何かしら制限を与える事で、ユーザーにもどの能力をいつ使うかという選択肢が発生します。
全ての行動にリスクとリターンが存在すればプレイスタイルを1つに限定せずに済みますね。

ただ1つの要素のみを難しくしてもバランス調整は成功しません。ゲームの方向性を考えながら、どうすれば全体がより良くなるかを考えると良いでしょう。
今回はエネルギーの概念をプレイヤーに持たせ、バリア展開時は大きく、ショット発射時は小さくエネルギーを消費させるようにします。
エネルギーが不足すると行動は失敗し、逆に何も行動をしなければエネルギーは回復するようにしてみましょう。

エネルギーゲージを用意

エネルギー残量を表示するために、復習も兼ねて先ほど行ったゲージ形式を使用しましょう。
UIの表示位置としてはプレイヤーの体力アイコンの下が適切でしょうか。サイズも大きくなくて良いでしょう。

ここにプレイヤーや弾が隠れると邪魔に感じてしまうので、画像のアルファ値を下げて半透明にする配慮もあるとなお良いです。

ゲージの設定をGameManagerスクリプトから行えるよう準備

エネルギーゲージは今オブジェクトを準備しただけであり、続いてスクリプトからそれを変更できるようにする準備をする必要があります。
それをPlayerControllerスクリプトで行っても問題はないのですが、今回はUI制御をGameManagerスクリプトで行うように決めたので、少々面倒に感じてもしっかり役割分担はさせましょう。

以下のようなメソッドを追加すれば良いでしょう。
変数image_EnergyGageは先ほどのエネルギーゲージUIを指しています。宣言とインスペクターウィンドウからのセットを忘れずに行ってください。

宣言は上の一行を追加すれば大丈夫です。

GameManager.cs内新規メソッド

	// プレイヤーの残りエネルギーをUIに適用(PlayerControllerから呼び出される)
	// 引数energy    : 残りエネルギー
	// 引数maxEnergy : 最大エネルギー
	public void SetEnergyUI (float energy, float maxEnergy)
	{
		// 残りエネルギーの比率を計算
		float ratio; // ボス残り体力の割合(0~1)
		ratio = energy / maxEnergy;

		// 画像のfillAmountに比率をセット
		image_EnergyGage.fillAmount = ratio;
	}

エネルギーの消費と回復

あとはプレイヤーのスクリプトを変更し、エネルギーのシステムを実装しつつそれをUIに反映させていきます。
PlayerController.cs

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

public class PlayerController : MonoBehaviour
{
	// メンバ変数宣言
	public GameManager gameManager;		// ゲームマネージャー
	public GameObject bulletPrefab;		// 弾のプレハブ
	public GameObject barrierPrefab;    // バリアのプレハブ
	private GameObject barrierInstance;	// バリアのインスタンス(実体)
	private int health; // 体力

	private float energy;		// エネルギー
	private float maxEnergy;	// 最大エネルギー

	// 起動時に1回だけ呼び出されるメソッド
	void Start ()
	{
		// 初期化処理
		health = 3; // 初期・最大体力
		energy = maxEnergy = 5; // 初期・最大エネルギー

		// 初期体力をUIに表示
		gameManager.SetPlayerHealthUI (health);
	}

	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		// -----移動処理-----
		// マウスカーソルの座標をVector2型変数cursorPosに取得
		Vector2 cursorPos = Input.mousePosition;
		// cursorPos内の座標データをスクリーン座標からワールド座標に変換する
		cursorPos = Camera.main.ScreenToWorldPoint (cursorPos);
		// Transformコンポーネントのpositionに計算したcursorPosを代入
		transform.position = cursorPos;

		// -----弾発射処理-----
		if (Input.GetMouseButtonDown (0) && barrierInstance == null)
		{ // 左クリックを押された瞬間かつ、バリアの実体が存在しない時
			if (energy > 0.3f)
			{ // エネルギー残量が0.3f以上ある時
				// エネルギーを消費
				energy -= 0.3f;
				// GameObject型ローカル変数を宣言 (生成したインスタンスを格納する)
				GameObject obj;
				// 弾プレハブのインスタンスを生成し、変数objに格納
				obj = Instantiate (bulletPrefab);
				// 弾インスタンスの座標にプレイヤーの座標をセット
				obj.transform.position = transform.position;
				// インスタンスのオブジェクト名を変更(敵弾と区別するため)
				obj.name = "PlayerBullet";
				// 弾のパラメータをセット
				obj.GetComponent<Bullet> ().speed = 6.0f;       // 速度
				obj.GetComponent<Bullet> ().angle = 0.0f;       // 角度
				obj.GetComponent<Bullet> ().limitTime = 5.0f;   // 生存時間
				// 弾の色を変更
				obj.GetComponent<SpriteRenderer> ().color = Color.cyan; // シアン
			}
		}

		// -----バリア処理-----
		// バリア展開
		if (Input.GetMouseButton (1) && barrierInstance == null)
		{ // 右クリックを押されている状態かつ、バリアの実体が存在しない時
			if (energy > 1.0f)
			{ // エネルギー残量が1.0f以上ある時
				// バリアの実体を生成し、親オブジェクトを自身のオブジェクトに指定する
				// 変数barrierInstanceに実体の参照をセット
				barrierInstance = Instantiate (barrierPrefab, transform);
				// バリア実体のローカル座標(親との相対距離)を0で初期化
				barrierInstance.transform.localPosition = new Vector3 (0.0f, 0.0f, 0.0f);
			}
		}
		// バリア消去
		if (Input.GetMouseButtonUp (1) && barrierInstance != null)
		{ // 右クリックが離された瞬間かつ、バリアの実体が存在する時
			// バリアの実体を消去
			Destroy (barrierInstance);
			// 変数barrierInstanceを初期化
			barrierInstance = null;
		}

		// -----エネルギー処理-----
		// エネルギー回復処理
		// バリア展開中は代わりに減少し、0を下回ったらバリア強制解除
		if (barrierInstance == null)
		{ // バリア展開中でない
			energy += Time.deltaTime; // 時間経過で回復
		}
		else
		{ // バリア展開中である
			energy -= 2.0f * Time.deltaTime; // 時間経過で大きく減少
			// エネルギーが不足したらバリアを消滅させる
			if (energy < 0.0f)
			{
				// バリアの実体を消去
				Destroy (barrierInstance);
				// 変数barrierInstanceを初期化
				barrierInstance = null;
			}
		}
		// エネルギー残量が下限値・上限値を超えないよう調整
		energy = Mathf.Clamp (energy, 0.0f, maxEnergy);
		// エネルギー残量をUIに表示
		gameManager.SetEnergyUI (energy, maxEnergy);
	}

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

エネルギーが不足するとバリアは強制解除となりますが、展開と解除だけをすぐ繰り返されても泥沼なので、展開時に一定量のエネルギーを必要とする調整にしています。

これでエネルギー機能の実装は完了です。テストプレイを行い、お好みでエネルギーの消費量や回復量を調整してください。

プレイヤーの無敵時間の実装

もう一つ難易度関連で必要な要素はプレイヤーの無敵時間です。
現在プレイヤーの体力は3になっていますが、3発の敵弾を連続して受けると一瞬で負けてしまいます。

それでは少し不親切なので、一度敵弾を受けた後は暫くの間当たり判定を無視するようにします。
これは俗に無敵時間と呼ばれ、大抵のアクションゲームでは必要とされる要素だと言えます。

無敵時間の処理

再びプレイヤースクリプトを変更していきましょう。
float型変数invisibleTimeをまず追加します。ここには残り無敵時間を入れる事にします。
この変数に0より大きい値が入っていれば無敵状態として扱い、その間はこの変数を減らし続けるようにすれば良いです。

まずは被弾時の無敵関連処理です。
PlayerController.cs内OnTriggerEnter2D()メソッド

	// 当たり判定内に他オブジェクトが侵入した際呼び出されるメソッド
	void OnTriggerEnter2D (Collider2D collider)
	{
		// ボスが発射した弾でなければ処理を終了
		if (collider.gameObject.name != "EnemyBullet")
		{ // 接触オブジェクト名がEnemyBulletで無ければ
			return; // メソッド終了
		}
		// バリア展開中なら処理を終了
		if (barrierInstance != null)
			return;
		// 無敵時間が残っていたら処理を終了
		if (invisibleTime > 0.0f)
			return;

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

		// 自身の体力を1減らす
		health--;
		// 現在体力をUIに表示
		gameManager.SetPlayerHealthUI (health);
		// プレイヤー消滅処理
		if (health <= 0)
		{// プレイヤーの体力が0以下
			Destroy (gameObject); // 自オブジェクト消去
		}
		// 無敵時間を設定
		invisibleTime = 1.5f; // 1.5秒
	}

後はUpdate()メソッドの最後に残り無敵時間を減らす処理を入れれば良いでしょう。

		// -----無敵時間処理-----
		if (invisibleTime > 0.0f)
			invisibleTime -= Time.deltaTime;

無敵時間の表示を分かりやすくする

これで被弾後の少しの間は無敵となり、敵弾がすり抜けていくようになりました。
しかし見た目には変化がなく、いつ無敵が終わるのかが初見では分かりません。

無敵中はその事を分かりやすくする為、例えばキャラクターの描画に変化を与えると良いでしょう。
今回は単純に「無敵中は半透明」という処理に変更してみます。
PlayerController.cs内Update()メソッド内無敵時間処理

		// -----無敵時間処理-----
		// 現在のスプライトカラーを取得
		Color col = GetComponent<SpriteRenderer> ().color;
		if (invisibleTime > 0.0f)
		{ // 無敵時間である
			// 残り無敵時間を減少
			invisibleTime -= Time.deltaTime;
			// アルファ値に半透明を指定
			col.a = 0.5f;
		}
		else
		{ // 無敵時間でない
			// アルファ値に不透明を指定
			col.a = 1.0f;
		}
		// スプライトカラーをセット
		GetComponent<SpriteRenderer> ().color = col;

ここまで実装できれば、すぐにバトルが終わってしまうという事も減ったでしょう。
この調子で少しずつゲームの品質を上げ続けましょう。

ステージセレクト画面の作成

タイトル画面でゲーム開始のボタンを押した後、すぐにバトルが始まってしまうようではユーザーは心の準備ができません。
タイトルとバトルの間に1クッションが欲しいですね。

今回はステージセレクト画面を作成し、そこに差し込みます。
これによって、今後複数ボスのバリエーションを増やした時も選んでバトルを開始する事が出来ます。

参考レイアウト

最低限必要なUIは、この画面がステージセレクトである事を示すロゴとボス選択ボタンです。
サンプルゲームではこのような配置になっていました。

(現在はまだボスが1体のみですので、ステージ2以降のUIは不要です。)

ステージセレクトシーンの作成

タイトルシーンを作成した時の事を思い出しながらステージセレクトシーンを開発していきましょう。
新規シーンの作成はメニューバーの[File]→[New Scene]で出来ましたね。
まずは[Save Scene As]で名前を付けて保存しましょう。シーン名は「StageSelectScene」としておきます。

そしてオブジェクトの配置ですが、ひとまず動作確認が出来れば良いので単純なUIをいくつか置くだけでOKです。
後で気になり始めたら調整を行うのでも大丈夫でしょう。
背景画像もタイトル画面の使いまわしで試してみます。オブジェクトのコピー・ペーストはシーンをまたいでも可能です。

build settingsでの設定

続いてシーン間遷移処理の実装に入るので、Build Settingsウィンドウを開きます。
スクリプトでシーンを移動する時は[Scenes In Build]への登録が必要でしたね。

[Add Open Scenes]ボタンを押下してビルド対象シーンのリストにステージセレクトシーンを加えます。
そしてこちらは必須ではありませんが、順番をタイトルシーンとバトルシーンの間にしておくと良いでしょう。

ステージセレクト制御スクリプト

ビルドの設定も終わり、後はスクリプトでシーン制御を行うのみです。
まずは今まで作ってきたGameManager、TitleManagerと同じように「StageSelectManager」のオブジェクトとスクリプトを作成しましょう。

そしてバトルシーンへと遷移するメソッドを作成し…

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

public class StageSelectManager : MonoBehaviour {

	// SampleScene(バトル画面)へのシーン遷移を行うメソッド
	// ボタン[StageButton_1]の入力時に呼び出される
	public void TransitionScene ()
	{
		// SampleSceneをロードする
		SceneManager.LoadScene ("SampleScene");
	}
}

「Stage 1」のボタンが押された時にそのメソッドを呼び出す設定をします。

タイトル画面の微修正

ステージセレクトシーンでやる事はひとまず終了です。続いてタイトルシーンの修正も行います。
ボタンを押した時の遷移先シーンがステージセレクトシーンになるようにしましょう。
TitleManager.cs内TransitionScene()メソッド

	// StageSelectScene(ステージセレクト画面)へのシーン遷移を行うメソッド
	// ボタンUIの入力時に呼び出される
	public void TransitionScene ()
	{
		// StageSelectSceneをロードする
		SceneManager.LoadScene ("StageSelectScene");
	}

これでステージセレクト画面をゲームに組み込む作業が終わりました。

フェードイン・フェードアウト

シーンの数が増えたのは良いのですが、未だシーンの切り替えが瞬時に終わってしまう為、ユーザーにとっては唐突なものに感じられます。
切り替え時には何らかの演出があった方が望ましいでしょう。その形の1つが「フェード」です。

フェードは画面切り替えの開始時に画面を白く(または暗く)していく「フェードイン」から始まり、
完全に画面を隠しきったタイミングで次シーンのロードを行います。
ロードが終了したら、画面を覆っていたフェードを徐々に透明に戻し、「フェードアウト」が完了します。

実装方法について

フェードイン・フェードアウト処理は全てのシーン遷移時に実行します。
ただし、ゲームオブジェクトはある設定を行わない限りシーン遷移時に全て破棄されます。
(その設定は次章で解説いたします。)
なので今回は全てのシーンに全く同じ処理をするフェード制御オブジェクトを配置する事にします。

フェード具体的な処理内容は以下の段階によって分けられます。

  • シーン読み込み時(Start()メソッド実行時点)
    フェードオブジェクトを真っ白にし、その後徐々に透明に戻す。(フェードアウト)
  • シーン切り替え開始時
    フェードオブジェクトを徐々に透明から白にします。(フェードイン)
  • フェードイン完了時(画面が真っ白になった時)
    LoadScene()メソッドでシーン切り替えをする。

まずは画面を覆うフェードオブジェクトを作成し、それを制御スクリプトが透明にしたり白にする動作を組みます。

フェードオブジェクトの作成

フェード本体はUIのImageで作成しましょう。
オブジェクト名は「FadeManager」にします。UIですがここに後でフェード制御のスクリプトも組み込む予定です。

画像はセットしなければ白い四角が表示されるので、これをフェード画像として利用します。
サイズを変更して画面全体を覆わせてみましょう。

もう一つ行わなければならない設定として、Imageコンポーネントの[Raycast Target]パラメータをオフにする変更が必要です。
UGUIでは全てのUIはマウスクリック等の検出が可能なのですが、画面を覆っているFadeUIで検出を行ってしまうと後ろにあるUIをクリックできなくなってしまいます。
RaycastTargetを外せばこの設定は無効となるので、基本的にボタン等以外のUIではオフにしておくと無難です。

最後に注意点として、UIの表示が重なった時はヒエラルキーウィンドウで後ろに存在するUIが上に表示されるという仕様があります。
ですのでフェードを一番後ろに配置しないと、フェードインしてもボタン等他UIが隠れないという現象が発生します。

これでUI部分の準備は完了です。
この画像の透明度はスクリプトで制御される為、開発中はアルファ値を0にしておくと他のオブジェクトが隠れなくなり作業がしやすいです。

フェード制御スクリプトの作成

フェードの動作を行うスクリプトを作成し、先ほどのFadeManagerオブジェクトにアタッチします。

このスクリプトでは、単純なフェードの表示・非表示だけでなくシーン読み込みの処理まで行います。
シーン読み込みを今までのようにGameManagerやTitleManager等で行った場合、それぞれで毎フレーム「フェードの進行状況」を監視する必要があるからです。
同じような処理を複数スクリプトに行わせるよりは、「フェード完了時にシーン読み込み」の処理をFadeManagerに行わせれば簡潔に済みますね。

それではスクリプトを作成します。スクリプト名は「Fade」にします。
Fade.cs

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

// フェード制御スクリプト
public class Fade : MonoBehaviour {

	// メンバ変数宣言
	bool fadeIn;	// フェードインフラグ
	bool fadeOut;	// フェードアウトフラグ

	float alpha;		// 現在のアルファ値(不透明度)
	float feedSpeed;	// フェードの進行スピード

	string loadScene;	// シーン切り替えを行う時の行先シーン名

	// 起動時に1回だけ呼び出されるメソッド
	void Start ()
	{
		// 初期化処理
		fadeIn = true;      // シーン開始時はフェードインフラグをオン
		alpha = 1.0f;       // 〃フェード画像の不透明度を1に
		fadeOut = false;    // フェードアウトフラグをオフ
		loadScene = "";

		feedSpeed = 1.2f;	// フェード進行スピード
	}

	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		// フェードイン処理
		if (fadeIn)
		{
			// 時間経過でアルファ値を減少
			alpha -= feedSpeed * Time.deltaTime;
			// フェードイン終了処理
			if (alpha < 0.0f)
			{ // アルファ値が0.0より小さければ
				alpha = 0.0f;
				fadeIn = false; // フラグをオフ
			}
			// アルファ値を画像に適用
			GetComponent<Image> ().color = new Color (1.0f, 1.0f, 1.0f, alpha);
		}

		// フェードアウト処理
		if (fadeOut)
		{
			// 時間経過でアルファ値を増加
			alpha += feedSpeed * Time.deltaTime;
			// フェードアウト終了処理
			if (alpha > 1.0f)
			{ // アルファ値が1.0より大きければ
				alpha = 1.0f;
				fadeOut = false; // フラグをオフ
				// シーンの切り替えを実行
				SceneManager.LoadScene (loadScene);
			}
			// アルファ値を画像に適用
			GetComponent<Image> ().color = new Color (1.0f, 1.0f, 1.0f, alpha);
		}
	}

	// フェード処理と共にシーン切り替えを行う公開メソッド
	// 引数sceneName : 行先シーン名
	public void TransitionScene (string sceneName)
	{
		// 既にフェードインかフェードアウト中なら処理しない
		if (fadeIn || fadeOut)
			return;

		// フェードアウト開始処理
		fadeOut = true;			// フェードアウトフラグをオン
		loadScene = sceneName;	// 行先シーン名を記憶
		alpha = 0.0f;
	}
}

作成できたらオブジェクトにアタッチし、その後オブジェクトをプレハブ化しましょう。他シーンで使いまわす為です。

他シーンへのインスタンスの配置

ここまでの作業をステージセレクトシーンで行っていたのなら、タイトルシーンとバトルシーンにこのプレハブのインスタンスを配置します。
全てのシーンにFadeManagerオブジェクトが配置されているようにしてください。
その際、Canvas内のUIの順番は間違えないようにしましょう。フェードが一番後ろでしたね。

他スクリプトの修正

後は今まで行っていたシーン切り替えの処理を、LoadScene()からFadeスクリプト内のTransitionScene()メソッドに置き換えるだけです。
これを行先シーン名と共に呼び出せば、自動でフェードイン処理とシーン切り替えまでを行ってくれます。

TitleManager.cs、StageSelectManager.cs、GameManagerにあるSceneManager.LoadScene()呼び出しをそれぞれ下記に変更してください。

(TitleManager.csの場合)
GameObject.Find ("FadeManager").GetComponent<Fade> ().TransitionScene ("StageSelectScene");

(StageSelectManager.csの場合)
GameObject.Find ("FadeManager").GetComponent<Fade> ().TransitionScene ("SampleScene");

(GameManager.csの場合)
GameObject.Find ("FadeManager").GetComponent<Fade> ().TransitionScene ("StageSelectScene");

これでフェードに関する処理は完了です。
よく使われる演出ですので、一通りの流れは覚えておくと良いでしょう。

Find()メソッドについて

いま書き換えを行ったスクリプトにあったGameObject.Find()というメソッドですが、
これは引数内にオブジェクト名を入れると自動的にシーン中からその名前のオブジェクトを探しだし、参照を返してくれるという便利なメソッドです。

ただしこの処理は少しだけ動作が重たいという弱点もあります。毎フレーム連続で呼び出すなどしてしまうと、ゲームの実行速度が大きく落ちる場合があります。
頼りすぎに注意しましょう。
(よって現在のスクリプトも本当は悪い例になっています。どこに原因があるかを探してみましょう。)

バトル終了時の処理

ここはおまけ程度の内容ですが、バトル終了後にプレイヤーが勝ったのかボスが勝ったのかの表示があると分かりやすいですね。

UIのTextを使って簡単な文章を表示させてみましょう。
今回はTextやImageに対して付加できるEffectsも試してみます。

TextとEffect

まずはバトルシーンにて戦闘結果表示用のUIオブジェクトを作成します。
オブジェクト名は「ResultText」等にしましょう。Canvas内での表示順はFadeManagerより前ならどこでも大丈夫です。

その後はTextコンポーネントでフォントサイズ等の調整をします。
表示する文字列はスクリプトで制御しますが、最初は適当な文言を入れて見え方を確認し、調整が終わったら文字列を空白にします。

それが済んだらTextへのエフェクト付与を試してみましょう。
UGUIではTextまたはImageコンポーネントに対して効果を及ぼすエフェクトコンポーネントが存在します。
同じオブジェクトに追加で取り付けるだけでOKです。

エフェクトコンポーネントは現在以下の2種が存在します。

  • Shadowコンポーネント
    文字・画像に対して影のように見える部分を追加する。
    影の色や不透明度は変更可能。
  • Outlineコンポーネント
    文字・画像に対して輪郭線を付与できる。
    輪郭線の色や不透明度は変更可能。

ほか、「Position as UV1」というコンポーネントもありますが滅多に使われないので気にしなくて大丈夫です。

今回はShadowコンポーネントを追加し、文字に立体感を出してみましょう。
まずはResultTextに[Add Component]ボタンからコンポーネントの追加をします。
[UI]→[Effects]→[Shadow]と選択しましょう。

設定項目は影の色を指定する[Effect Color]と
文字に対する影の相対座標を指定する[Effect Distance]があります。

Game Managerスクリプトの修正

後はゲーム管理スクリプト上で、戦闘結果に応じてこのTextに文字列を設定する処理を追加しましょう。
先ほどのコンポーネントを指定するText型のpublic変数text_Resultを用意し、戦闘終了後処理を変更します。

		//-----戦闘終了後処理-----
		if (!afterFinish)
		{ // 戦闘が終了していなければ
			if (playerObj == null)
			{ // プレイヤーか消滅していたら
				// 戦闘結果をUIに表示する
				text_Result.text = "Player Lose...";
				// 戦闘終了のフラグを立てる
				afterFinish = true;
				afterFinishTime = 0.0f;
			}
			else if (bossObj == null)
			{ // ボスが消滅していたら
				// 戦闘結果をUIに表示する
				text_Result.text = "Player Win!!";
				// 戦闘終了のフラグを立てる
				afterFinish = true;
				afterFinishTime = 0.0f;
			}
		}
		else
		{ // 戦闘が終了していたら
			(省略)
		}

これで戦闘結果が画面に表示されるようになりました。

次章に続きます

応用編の前半はここまでです。後半にも重要知識の説明がありますので、ぜひ開発を続けてみてください。
この章の内容を実装しただけでもかなりゲームは遊びやすくなったと思います。この調子で頑張りましょう!

 

<前回>  <=   「⑧ビルドをしてゲームを遊んでみよう

<次回>  =>  「⑩アレンジ編:ゲームの完成度を高めよう(後半)