ミニゲームを作ってUnityを学ぶ![3Dマインスイーパー編]
###第5回目: ゲームの進行
フィールドの生成やブロックを操作する機能が完成したことでようやくマインスイーパーとしての形が見えてきました。
今回は実際にゲームをプレイできるよう、ゲーム起動直後から終了までの一連の流れを制御していきます。
#UIの追加
まず最初にゲームの進行に必要なUIを準備していきます。
###スタートボタン
プレイ開始のタイミングを図るためのボタンを作成します。
- シーンにCanvasを配置
- Canvas内に「ButtonStart」という名前でButtonを配置
- ButtonStartのポジションを以下のように設定
- ButtonStartの子要素であるTextの文字列を「LOADING」に設定
###スタートボタンにメソッドを登録
SceneMain.csに新しいメソッドOnStart()を追加して、ボタンを押した際に呼び出されるよう登録します。
- SceneMainにOnStart()というメソッドを追加
public void OnStart()
{
Debug.Log("OnStart!");
}
- 下の画像を参考に、ButtonStartのインスペクタから項目On Click()の右下にある+マークをクリック
- 続けて赤枠部分にシーンに配置されているSceneMainを設定
- No Functionと表記された部分をクリックしてSceneMain#OnStart()を設定
###結果表示用のテキスト
ゲームクリアまたはゲームオーバーになった際にその旨を表示するテキストを作成します。
- Canvas内に「TextResult」という名前でTextを配置
- Colorを(R=255, G=255, B=255, A=255)に変更
- インスペクタ上のオブジェクト名の横にあるチェックを外して非表示の状態にしておく
- 赤枠部分とポジションを以下のように設定
- 文字を縁取りで装飾するためにAddComponentからOutlineをアタッチ
↓中央部分のテキストは非表示のため、ゲーム起動直後には表示されていません
UIマネージャー
出来上がったUIを制御するためのマネージャーを実装します。
- 空オブジェクト「UiManager」をゼロポジションに配置
- スクリプト「UiManager」を作成して上記の空オブジェクトにアタッチ
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using UnityEngine.UI;
public class UiManager : MonoBehaviour
{
//---------------------------
// スタートボタンのテキスト //
//---------------------------------------------------------------------------------
[SerializeField]
1: private Text mStartText;
public void RenewStartText(string str, string color)
{
mStartText.text = "<color=" + color + ">" + str + "</color>";
}
//-------------------
// 結果表示・非表示 //
//---------------------------------------------------------------------------------
[SerializeField]
2: private Text mResultText;
public void ShowResultText(string str)
{
mResultText.text = str;
mResultText.gameObject.SetActive(true);
}
public void HideResultText()
{
mResultText.gameObject.SetActive(false);
}
}
1: 「ButtonStart/Text」をインスペクタから設定
2: 「TextResult」をインスペクタから設定
#ゲームクリアとゲームオーバー
これでUIの準備できましたので、続けてゲームの終了条件になる部分を実装していきます。
BlockManagerのOnLeftClick()でコメントアウトしていたゲームクリアとゲームオーバーの判定を有効にして、それに対応するコードを追記します。
/// <summary>
/// 左クリック
/// 対象ブロックを開く
/// </summary>
private void OnLeftClick()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray, out hit))
{
GameObject go = hit.collider.gameObject;
if(go.tag == GameController.TAG_BLOCK)
{
// 対象が爆弾ブロックか判定
BlockModel target = go.GetComponent<BlockModel>();
if (target.HasBomb)
{
// チェック済ならば何もしない
if (target.IsCheck) return;
// チェックしていないなら開いてゲームオーバー
コメントアウト解除 GameOver(target);
}else
{
// 爆弾でないならば一連の開く処理
OpenBlock(target);
// ゲームクリアの判定
コメントアウト解除 JudgeGameClear();
}
}
}
}
以下のコードを追加
//------------------------------
// ゲームクリアとゲームオーバー //
//---------------------------------------------------------------------------------
// ゲームクリアのフラグ
public bool IsGameClear { get; private set; }
// ゲームオーバーのフラグ
public bool IsGameOver { get; private set; }
/// <summary>
/// ゲームオーバー
/// 全ての爆弾を表示してゲームオーバーフラグを立てる
/// </summary>
private void GameOver(BlockModel target)
{
// 全ての爆弾ブロックに通常の爆弾を表示
var bombBlocks = mBlockList.Where(block => block.HasBomb);
foreach(BlockModel model in bombBlocks)
{
model.ShowBomb(false);
}
// ターゲットのみ特別な爆弾に表示を変更
target.ShowBomb(true);
// フラグを立てる
IsGameOver = true;
}
/// <summary>
/// 爆弾がなく開かれていないブロックが存在するか確認し、存在しないならばクリアフラグを立てる
/// </summary>
private void JudgeGameClear()
{
if (mBlockList.Any(block => !block.HasBomb && !block.IsOpen) == false)
{
IsGameClear = true;
}
}
いずれかの通常ブロックを開いた際に実行されるJudgeGameClear()では開かれていない状態の通常ブロックが存在するかの判定を行い、結果が0だった場合にゲームクリアのフラグを立てています。
また爆弾ブロックを開いてしまった場合に実行されるGameOver()では、開いてしまったブロックに赤い爆弾マークを表示し、それ以外の爆弾ブロックには通常の黒い爆弾マークを表示した後、ゲームオーバーのフラグを立てています。
#ゲーム起動から終了まで
終了条件の判定が実装できましたので、次はゲームを起動したときの処理からゲーム終了までの流れを制御していきます。
- SceneMainを修正
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class SceneMain : MonoBehaviour
{
private GameController mGame;
[SerializeField]
private BlockManager mBlock;
[SerializeField]
1: private UiManager mUi;
void Awake()
{
mGame = GameController.Instance;
mGame.Init();
}
//-------------
// 状態と更新 //
//---------------------------------------------------------------------------------
private enum STATE
{
LOADING = 0,
WAIT_START,
PLAY,
RESULT
}
private STATE mState = STATE.LOADING;
void Update()
{
switch (mState)
{
// ステージ生成
case STATE.LOADING:
LoadStage();
break;
// スタートボタンが押されるまで何もしないで待機(取り外してOK)
case STATE.WAIT_START:
break;
// プレイ中:ゲームの終了条件を監視し、終了でない場合はプレイヤーの入力を受け付ける
case STATE.PLAY:
if (mBlock.IsGameClear)
{
EndGame(true);
return;
}
if (mBlock.IsGameOver)
{
EndGame(false);
return;
}
mBlock.CheckMouseInput();
break;
// 結果表示中:
case STATE.RESULT:
break;
}
}
//---------------------
// ゲームの開始と終了 //
//---------------------------------------------------------------------------------
private void StartGame()
{
mUi.RenewStartText("RESET", "red");
mState = STATE.PLAY;
}
private void EndGame(bool clearFlg)
{
if (clearFlg)
{
mUi.ShowResultText("GAME CLEAR!");
}
else
{
mUi.ShowResultText("GAME OVER");
}
mState = STATE.RESULT;
}
//-----------------
// ステージの生成 //
//---------------------------------------------------------------------------------
private void LoadStage()
{
int gameLevel = GameController.LEVEL_EASY;
mBlock.CreateField(gameLevel);
mUi.RenewStartText("START", "blue");
mState = STATE.WAIT_START;
}
//-----------
// 入力待機 //
//---------------------------------------------------------------------------------
public void OnStart()
{
switch (mState)
{
case STATE.WAIT_START:
StartGame();
break;
case STATE.PLAY:
case STATE.RESULT:
break;
}
}
}
1: 空オブジェクト「UiManager」をインスペクタから設定
まず最初に、ゲームを起動してから1回目のUpdate()が呼ばれたタイミングでLoadStage()が実行され、難易度イージーのフィールドが生成されます。
その後は先ほどボタンに登録したOnStart()が実行されるまで何もしない待機状態に入り、プレイヤーがスタートボタンを押したタイミングでゲームを開始します。
以後はゲームクリアまたはゲームオーバーのフラグが立つまでプレイヤーの操作を受け付けるループ処理を行い、フラグが立ったタイミングで結果を表示するとともにループを抜け出します。
また、ゲームの進行状況にあわせてスタートボタンの表記をそれぞれ「LOADING, START, RESET」に変更しています。
#ゲームをやり直す
この段階でプロジェクトを実行するとマインスイーパーを楽しむことができますが、いちどゲーム終了のフラグが立ってしまうと画面が結果表示で止まり、戻すにはプロジェクトを再実行する以外に方法がありません。
この状態を解消するために、SceneMainを修正してゲームをやり直す機能を実装していきます。
- OnStart()を修正し、新しいメソッドResetGame()を追加
private void ResetGame()
{
mUi.RenewStartText("LOADING", "black");
mUi.HideResultText();
mState = STATE.LOADING;
}
public void OnStart()
{
switch (mState)
{
case STATE.WAIT_START:
StartGame();
break;
case STATE.PLAY:
case STATE.RESULT:
追加 ResetGame();
break;
}
}
- BlockManagerのCreateFieldにフラグを解除する処理を追加
/// <summary>
/// フィールドを生成する
/// </summary>
public void CreateField(int gameLevel)
{
// 前のブロックが存在する場合は全て破棄
foreach(BlockModel model in mBlockList)
{
Destroy(model.gameObject);
}
mBlockList.Clear();
追加 // フラグの解除
IsGameClear = false;
IsGameOver = false;
// ゲームレベルによってサイズと爆弾の数を決定
int xLength;
int yLength;
int bombCount;
switch (gameLevel)
{
case GameController.LEVEL_EASY:
xLength = 9;
yLength = 9;
bombCount = 10;
break;
case GameController.LEVEL_NORMAL:
xLength = 16;
yLength = 16;
bombCount = 40;
break;
default:
xLength = 30;
yLength = 16;
bombCount = 99;
break;
}
// ブロックを並べる
InstantiateBlocks(xLength, yLength);
// ブロックに爆弾を設置
SetBombs(bombCount);
// カメラを中心に設定
float cameraX = xLength * BLOCK_SIZE / 2.0f;
float cameraZ = yLength * BLOCK_SIZE / 2.0f - CAMERA_OFFSET_Z;
float cameraY = 6.5f;
Transform cameraTrans = Camera.main.transform;
cameraTrans.position = new Vector3(cameraX, cameraY, cameraZ);
cameraTrans.rotation = Quaternion.Euler(new Vector3(50.0f, 0.0f, 0.0f));
}
これでスタートボタンの表記が「RESET」になっている状態でボタンを押すとステージが新しく生成され、何度でもゲームを楽しめるようになりました。