ミニゲームを作ってUnityを学ぶ![タンクウォーズ編]
###第6回目: ゲーム全体の管理とシーンの制御
例えばタイトル画面のシーン、実際にプレイするシーン、結果表示のシーンなど、一般的なUnityのプロジェクトは複数のシーンから構成されています。
今回のような小規模なゲームでは一つのシーンでプロジェクトが成り立ってしまいますが、ここではあえてプロジェクト(以後、ゲーム全体と称します)とシーンについてそれぞれ個別のスクリプトを使って管理・制御していきたいと思います。
#シーンを制御する
まずは現在のシーン(main.unity)を制御するスクリプトについて、土台を作っていきます。
- ゼロポジションに「SceneMain」という名前の空オブジェクトを配置
- 同じく「SceneMain」という名前のスクリプトを作成し、それを空オブジェクト「SceneMain」にアタッチ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SceneMain : MonoBehaviour
{
private enum STATE
{
INIT = 0,
PLAY,
GAME_OVER
};
private STATE mState = STATE.INIT;
void Update()
{
switch (mState)
{
case STATE.INIT:
// 初期化処理
break;
case STATE.PLAY:
// プレイ中
break;
case STATE.GAME_OVER:
// ゲームオーバー
break;
}
}
}
SceneMainクラスはINIT, PLAY, GAME_OVERの3つのうちいずれかの状態(mState)を持ち、その状態によってUpdate()の処理を分岐させています。
#ゲーム全体を管理する
続いてゲーム全体を管理するスクリプトの土台です。
- 「GameController」という名前のスクリプトを作成
※ GameControllerは他のスクリプトと違ってオブジェクトにアタッチしません
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameController : MonoBehaviour
{
// 初期化タイミングでインスタンスを生成
private static readonly GameController mInstance = new GameController();
// コンストラクタをprivateにすることによって他クラスからnewできないようにする
private GameController() { }
// 他クラスからこのマネージャーを参照する
public static GameController Instance
{
get
{
return mInstance;
}
}
}
少しわかりにくいかもしれませんが、このGameControllerはプロジェクト全体で1つしかインスタンスが存在せず、同時にその唯一のインスタンスはプロジェクト内の全てのスクリプトから呼び出すことができます。
この特性を利用することで本来はシーンが切り替わる際に破棄されてしまう値を残したり、複数のクラスから呼び出される定数などを一元管理することが主な役割です。
#GameControllerを取得する
では実際に、SceneMainからGameControllerのインスタンスを取得してゲーム全体に関係する設定をしてみます。
private GameController mGame;
void Awake()
{
mGame = GameController.Instance;
mGame.InitGame();
}
// FixedUpdateの間隔
private readonly float FIXED_TIME_STEP = 0.03f;
/// <summary>
/// ゲームシステムを初期化する
/// </summary>
public void InitGame()
{
// 更新間隔の設定
Time.fixedDeltaTime = FIXED_TIME_STEP;
}
SceneMainのAwake()でGameControllerのインスタンスを取得し、続いてGameControllerに定義されているInitGame()を実行しています。
プロジェクトの流れとしては
- ゲームの読み込みを開始
- SceneMainオブジェクトにアタッチされたSceneMainクラスのAwake()が実行される
- 初めてGameControllerのインスタンスを取得するタイミングでGameControllerのインスタンス化が行われる
- GameControllerのInitGame()によってFixedUpdateの更新間隔が設定される
- 以後、SceneMainクラスのUpdate()によってゲームが進行する
となります。
###SetLayerCollision()の移動
TankModelのAwake()で実行しているレイヤー毎の当たり判定を決めるSetLayerCollision()はゲーム全体に関わる処理ですので、このタイミングでGameControllerクラスに移動しておきます。
// FixedUpdateの間隔
private readonly float FIXED_TIME_STEP = 0.03f;
/// <summary>
/// ゲームシステムを初期化する
/// </summary>
public void InitGame()
{
// 更新間隔の設定
Time.fixedDeltaTime = FIXED_TIME_STEP;
// 当たり判定の設定
追加 SetLayerCollision();
}
追加↓ public static int LAYER_WHEEL = 9;
public static int LAYER_SUSPENSIONS = 10;
public static int LAYER_MAIN_BODY = 11;
private void SetLayerCollision()
{
// 初期化:車輪とボディについて、全ての接触を有効にする
for (int i = 0; i <= 11; i++)
{
Physics.IgnoreLayerCollision(LAYER_WHEEL, i, false);
Physics.IgnoreLayerCollision(LAYER_MAIN_BODY, i, false);
}
// 車輪同士の接触は無効
Physics.IgnoreLayerCollision(LAYER_WHEEL, LAYER_WHEEL, true);
// 車輪と本体の接触は無効
Physics.IgnoreLayerCollision(9, 11, true);
// サスペンションと全ての物体の接触は無効
for (int i = 0; i <= 11; i++)
{
Physics.IgnoreLayerCollision(10, i, true);
}
}
これでゲームの初期化処理としてFixedUpdateの更新頻度とレイヤー毎の当たり判定が設定できました。
TankModelの該当する部分のコードは不要となりましたので削除しておきます。
###定数を管理
これまでに登場した定数についてもGameCotrollerで定義しておきます。
// タグ
public static string TAG_TANK = "Tank";
public static string TAG_DESTROY_AREA = "DestroyArea";
どちらも現在はBulletModelでしか使用していない定数ですので移動する意味は薄いのですが、タグ名をGameControllerで管理しておくというルールを作っておけば、登録しているタグ名を途中で変更することになった場合などにコード側の修正が容易にできます。
そして実際にBulletModelで上記の定数を利用するコードがこちらです。
/// <summary>
/// 弾が領域外に出た際の処理
/// このオブジェクトを休眠状態に遷移する。
/// </summary>
/// <param name="collider"></param>
void OnTriggerExit(Collider collider)
{
if(collider.tag == GameController.TAG_DESTROY_AREA)
{
Sleep();
}
}
/// <summary>
/// 弾が戦車に当たった際の処理
/// </summary>
/// <param name="collider"></param>
void OnTriggerEnter(Collider collider)
{
if(collider.tag == GameController.TAG_TANK)
{
// 被弾した戦車にダメージを与える
bool IsDead = collider.transform.GetComponent<TankHealth>().AddDamage(mAttackValue);
if (!IsDead)
{
// おおまかな接触点を取得(OnCollisionEnterとは異なり、triggerでは正確な座標を取得できない:でも着弾点としては十分?)
Vector3 hitPos = collider.ClosestPointOnBounds(mTrans.position);
// 接触した場所に爆発エフェクトを生成
Instantiate(mExplosionSmallPrefab, hitPos, Quaternion.identity);
}else
{
// HPが無くなった場合は戦車中央のポイントに大きな爆発エフェクトを生成
Vector3 hitPos = collider.transform.position;
Instantiate(mExplosionLargePrefab, hitPos, Quaternion.identity);
// 対象を死亡状態へ
collider.transform.GetComponent<TankModel>().IsDead = true;
}
// この弾を休眠状態へ
Sleep();
}
}
これでゲーム全体の管理とシーンを制御する土台が完成しました。
ゲームの開始や終了など具体的な処理は次回以降に引き継ぎますので、今回は少し短いですがここで終了となります。