読者です 読者をやめる 読者になる 読者になる

tanaka's Programming Memo

プログラミングについてのメモ。

(8)スコアと残機の実装

Unityでブロック崩し

下準備

ゲームらしくするために、スコアと残機を実装しよう。

これまで作ってきたスクリプトが各オブジェクトを制御するものだったのに対いて、スコアの計算や残機処理はゲーム全体を統括する処理である。そこで【階層(Hierarchy)】ビューにゲーム全体を統括するスクリプトを登録しておくための空のゲームオブジェクト「SceneScripts」を作成して、そこにスクリプトを登録することにしよう。

  • 【階層(Hierarchy)】ビューの余白をクリックして、何も選択していない状態にする。
  • メニューから【GameObject】→【Create Empty】を選択する。
  • 追加した「GameObject」を「SceneScripts」に名前変更する。
スクリプトを作成

ゲーム全体を統括するためのスクリプトを生成する。

  • 【プロジェクト(Project)】ビューの「Scripts」フォルダを右クリックして、【Create】→【C# Script】を選択。
  • 生成したスクリプトのファイル名を「CGame」にする。
  • スクリプトをメモ帳などで開いて、文字のエンコードを【Unicode big endian】に変更する。
  • クラス名が「CGame」になっていることを確認して、もし違うクラス名だったら「CGame」に修正する。
  • 上書き保存をしてエディタを閉じて【Unity】に戻る。
  • 【プロジェクト(Project)】ビューの「Scripts」フォルダ内の「CGame」をドラッグして、【階層(Hierarchy)】ビューに作成した「SceneScripts」にドロップする。

変数の定義

以下のような変数を作成する。

  • ゲームのスコアを記録しておくためのint型の変数「iScore」「CGame」クラスに作成。
  • 残機数を記録しておくためのint型の変数「iLeft」「CGame」クラスに作成。
  • ゲームスタート時の残機を表すためのint型の変数「INIT_LEFT」「CGame」クラスに作成。
  • ブロックを壊した時の点数を設定しておくint型の変数「ADD_SCORE」「CBlock」クラスに作成。
  • 「iScore」と「iLeft」と「INIT_LEFT」は公開する必要がないので「private」にする。また「iBlockCnt」と同様に、ゲーム全体で値を共有したいので「static」にする。
  • 「ADD_SCORE」はインスペクターから変更できるようにしたいので公開(public)する。

以上を実装していく。

「CGame」に変数を追加
  • 【プロジェクト(Project)】ビューの「Scripts」フォルダ内の「CGame」をダブルクリックして【MonoDevelop】で開く。
  • 「public class CGame : MonoBehaviour {」という行を探して、下の行に以下の変数定義を追加する。
	// 点数
	private static int iScore;
	// 現在の残機。マイナスになるとゲームオーバー。
	private static int iLeft;
	// 面が開始する時点での残機(初期設定では3回ミスでゲームオーバー)
	private static int INIT_LEFT = 2;
  • 上書き保存。
「CBlock」に変数を追加
  • MonoDevelop】で「CBlock.cs」を開く。
  • 「public class CBlock : MonoBehaviour {」という行を探して、下の行に以下の変数定義を追加する。
	// 壊したら入る点数(初期値は10点)
	public int ADD_SCORE = 10;
  • 上書き保存をする。

ゲーム開始時の初期化関数を作成する

ゲームが開始する際には以下の処理を行いたい。

  • スコアを0にする。
  • 残機を規定の数(INIT_LEFT)にする。

ゲームを開始する時に呼び出す関数「initGame()」を作成して、その中で処理することにしよう。また、この関数は「CGame」クラスから直接呼び出せるように公開(public)の「static」関数にする。

初期化関数「initGame」を作る
  • MonoDevelop】で「CGame.cs」を開く。
  • スクリプトファイルの一番最後の「}」の上の行に以下のプログラムを追加する。
	public static void initGame() {
		iScore = 0;
		iLeft = INIT_LEFT;
	}

作成した関数を、タイトルから開始する際に呼び出す。

  • 【プロジェクト(Project)】ビューの「Scripts」フォルダ内の「CTitle」をダブルクリックする。
  • 「Application.LoadLevel("Game");」という行を探して、1行上に以下のプログラムを追加する。
			CGame.initGame();

画面表示

データが出来たので、それらを使ってスコアと残機を画面上に表示する。文字描画にはタイトルやゲームオーバーのステータス表示にも利用したOnGUI()を使う。書体や色を設定する【GUI Skin】も利用してみよう。

GUI Skinの作成
  • 【プロジェクト(Project)】ビューで、何もないところをクリックして何も選択されていない状態にする。
  • 【Create】→【GUI Skin】を選択。
  • 作成される「New GUISkin」の名前を「Block GUISkin」に変えておく。
  • 「Block GUISkin」を選択してインスペクター(Inspector)に内容が表示されるようにする。
  • 【Custom Styles】の左の三角アイコンをクリックして開く。
  • 【Size】を4に変更する。
  • 【Element 0】を開き、以下のように設定する。
    • 【Name】を「HUD Label」にする。これをラベルの見出し用にする。
    • 【Normal】の【Text Color】を赤にする。
      • 【Normal】の左の三角アイコンをクリックして開く。
      • 【Text Color】を赤にする。
    • 【Font Size】を「16」にする。

  • 【Element 1】を開き、以下のように設定する。
    • 【Name】を「HUD LabelS」にする。これをラベルの見出しの影用にする。
    • 【Normal】の【Text Color】を暗い赤にする。
      • 【Normal】の左の三角アイコンをクリックして開く。
      • 【Text Color】を暗い赤にする。
    • 【Font Size】を「16」にする。

  • 【Element 2】を開き、以下のように設定する。
    • 【Name】を「HUD Num」にする。これを数値用にする。
    • 【Normal】の【Text Color】を白にする。
      • 【Normal】の左の三角アイコンをクリックして開く。
      • 【Text Color】を白にする。
    • 【Font Size】を「16」にする。

  • 【Element 3】を開き、以下のように設定する。
    • 【Name】を「HUD NumS」にする。これを数値の影用にする。
    • 【Normal】の【Text Color】を灰色にする。
      • 【Normal】の左の三角アイコンをクリックして開く。
      • 【Text Color】を灰色にする。
    • 【Font Size】を「16」にする。


設定は以上である。それでは描画処理を実装していく。

パラメータの描画

スコアと残機数を画面に描画する。見出しは「HUD Label」と「HUD LabelS」の2つのフォントを使って描画する。まずは「HUD LabelS」で影となる文字を描画して、その上に少しピクセル数をずらして「HUD Label」で明るい文字を描画する。パラメータも「HUD Num」と「HUD Nums」を使って同様に描画する。

  • 【プロジェクト(Project)】ビューの「Scripts」フォルダ内の「CGame」をダブルクリックして開く。

GUI Skinを設定するための変数を追加する。

  • 「public class NewBehaviourScript : MonoBehaviour {」という行を探して、下の行に以下のプログラムを追加する。
	// GUISkin
	public GUISkin skinBlock;

GUIの描画関数でパラメータの描画を行う。

  • 一番最後の「}」の上の行に、以下のプログラムを追加する。
	void OnGUI() {
		GUIStyle stLabel = skinBlock.FindStyle("HUD Label");
		GUIStyle stLabelS = skinBlock.FindStyle("HUD LabelS");
		GUIStyle stNum = skinBlock.FindStyle("HUD Num");
		GUIStyle stNumS = skinBlock.FindStyle("HUD NumS");
		float grid = Screen.height/60f;
		float cx = Screen.width/2f;
		// スコアラベル
		GUI.Label(new Rect(cx+grid*22+1,grid*4+1,100,20),"SCORE",stLabelS);
		GUI.Label(new Rect(cx+grid*22,grid*4,100,20),"SCORE",stLabel);
		// 残機ラベル
		GUI.Label(new Rect(cx+grid*22+1,grid*50+1,100,20),"LEFT",stLabelS);
		GUI.Label(new Rect(cx+grid*22,grid*50,100,20),"LEFT",stLabel);
		// スコア
		GUI.Label(new Rect(cx+grid*20+1,grid*7+1,grid*18,20),""+iScore,stNumS);
		GUI.Label(new Rect(cx+grid*20,grid*7,grid*18,20),""+iScore,stNum);
		// 残機
		GUI.Label(new Rect(cx+grid*20+1,grid*53+1,grid*18,20),""+iLeft,stNumS);
		GUI.Label(new Rect(cx+grid*20,grid*53,grid*18,20),""+iLeft,stNum);
	}

プログラムは以下のような意味である。

	void OnGUI() {
  • GUI(グラフィカル・ユーザ・インターフェース)を描画するための命令を書いておく関数。「OnGUI()」という関数を作っておくと【Unity】が自動的に呼び出してくれる。
		GUIStyle stLabel = skinBlock.FindStyle("HUD Label");
		GUIStyle stLabelS = skinBlock.FindStyle("HUD LabelS");
		GUIStyle stNum = skinBlock.FindStyle("HUD Num");
		GUIStyle stNumS = skinBlock.FindStyle("HUD NumS");
  • 「Block GUISkin」に設定した4つのGUIのスタイルを変数に検索して取得する。
		float grid = Screen.height/60f;
		float cx = Screen.width/2f;
  • 座標を指定するための基準となる数値を計算。

Window環境では画面サイズが決まらないので、直接座標を指定すると実行環境によってレイアウトが崩れてしまう。【Unity】では画面の高さが基準となるので、画面の高さを表す「Screen.height」を基準にして座標計算を行うことにする。

  • 今回のプロジェクトでは画面の高さを600としている。それを60で割ることで、設計上で10にあたるサイズを「grid」に求めている。
  • 画面の左右方向は、ウィンドウサイズによって自在に変化してしまう。そのため、画面の端から座標を指定するより、画面中心からの座標を指定した方が楽なので、画面中央の座標を「cx」に求めている。
		GUI.Label(new Rect(cx+grid*22+1,grid*4+1,100,20),"SCORE",stLabelS);
  • 上記は以下の通りに描画する。
    • 左座標を画面中心(cx)から「grid」22個分+1
    • 上座標を「grid」4個分+1
    • 幅100
    • 高さ20
    • 文字列「SCORE」
    • フォントを「HUD LabelS」
  • 以下、座標を1ずらしたり、フォントを変更したりして、スコアや残機の内容を描画している。
  • 上書き保存する。
GUI Skinの設定

スクリプトに作成した「Block GUISkin」を設定する。

  • 【階層(Hierarchy)】ビューから「SceneScripts」を選択する。
  • 【プロジェクト(Project)】ビューから「Block GUISkin」をドラッグして、インスペクター(Inspector)上の「CGame(Script)」グループ内の【Skin Block】項目上にドロップする。

以上で設定は完了である。実行してみよう。

点数を加算する

表示が出来たので、点数を入れてみよう。点数の加算は簡単で、ブロックが壊れる時にスコアの加算処理を追加すればよい。

  • 【プロジェクト(Project)】ビューの「Scripts」フォルダ内の「CBlock」をダブルクリックして開く。
  • 「 public void setDamage() {」という行を探して、下の行に以下のプログラムを追加する。「Destroy(gameObject);」の前に追加すること。
		CGame.addScore(ADD_SCORE);

「CGame」スクリプトにまだ「addScore」という命令を作成していないので追加する。

  • MonoDevelop】で「CGame.cs」を開く。
  • 最後の「}」の上の行に以下のプログラムを追加する。
	public static void addScore(int add) {
		iScore += add;
	}

上書き保存して実行してみよう。点数が入れば成功である。

ミスの実装

残機が残っている時は、ゲームオーバーにならずにリスタートするようにする。

デバッグ用の初期化

現状だとタイトルからゲームシーンに切り替える時に初期化を呼び出しているので、残機が設定されていない。そこで、開発時にはゲーム開始時にCGame.initGame()を呼び出すようにしておく。

  • 【プロジェクト(Project)】ビューの「Scripts」フォルダ内の「CGame」をダブルクリックする。
  • 「void Start () {」という行を探して、下の行に以下のプログラムを追加する。
		CGame.initGame();
ミスの処理を実装

「CBall」クラスのミス判定に残機チェックを追加する。「CGame」クラスに残機を減らして、ゲームオーバーかどうかを返す関数を追加して、それを呼び出すようにする。

  • 【プロジェクト(Project)】ビューの「Scripts」フォルダ内の「CGame」をダブルクリックする。
  • 最後の「}」の上の行に以下のプログラムを追加する。
	public static bool decLeft() {
		if (iLeft <= 0) {
			return true;
		}
		iLeft--;
		return false;
	}
  • 「iLeft」が0以下になっていた時はゲームオーバーなので「true」を返す。
  • 「iLeft」が1以上で残機があれば、残機を減らしてゲーム続行のために「false」を返す。
  • 上書き保存。
  • MonoDevelop】で「CBall.cs」を開く。
  • 「Application.LoadLevel("GameOver");」という行を見つけたら、その行を消して、以下のプログラムに置き換える。
			if (CGame.decLeft()) {
				Application.LoadLevel("GameOver");
			}
			else {
				Destroy(gameObject);
				GameObject.Find("Player").SendMessage("createHoldBall");
			}

上書き保存して実行してみよう。残機が残っている状態でボールを落としたらリスタート。残機が無い状態でボールを落としたらゲームオーバーになれば成功である。

  • 「CGame.cs」を【MonoDevelop】で開いて、「Start()」関数内に書いた「CGame.initGame();」を削除しておこう。

微調整

スコアや残機のような数値は、右揃えの方が見やすい。「GUI Skin」の修正で簡単に直せるのでやってみよう。

  • 【プロジェクト(Project)】ビューの「Block GUISkin」をクリックして選択する。
  • 【Custom Styles】内の「HUD Num」を開く。
  • 【Alignment】を「UpperRight」に変更する。

  • 同様に、「HUD NumS」の【Alignment】も「UpperRight」に変更する。

以上が出来たら実行して、スコアと残機の表示が右寄せになっていることを確認しよう。

ここまでの「CBall.cs」

using UnityEngine;
using System.Collections;

public class CBall : MonoBehaviour {
	public float INIT_DEGREE = 75f;
	public float INIT_SPEED = 250f;

	// Use this for initialization
	void Start () {
		//shotBall();
	}
	
	// Update is called once per frame
	void Update () {
	
	}
	
	/** ボールを発射する*/
	void shotBall() {
		Vector3 vel = Vector3.zero;
		vel.x = INIT_SPEED*Mathf.Cos(INIT_DEGREE*Mathf.PI/180f);
		vel.y = INIT_SPEED*Mathf.Sin(INIT_DEGREE*Mathf.PI/180f);
		rigidbody.velocity = vel;
	}
	
	/** 衝突コールバック*/
	void OnCollisionEnter(Collision col) {
		if (col.gameObject.CompareTag("Block")) {
			col.gameObject.SendMessage("setDamage");
		}
	}
	
	/** ミス判定*/
	void OnTriggerEnter(Collider other) {
		if (other.CompareTag("Miss")) {
			if (CGame.decLeft()) {
				Application.LoadLevel("GameOver");
			}
			else {
				Destroy(gameObject);
				GameObject.Find("Player").SendMessage("createHoldBall");
			}
		}
	}
}

ここまでの「CBlock.cs」

using UnityEngine;
using System.Collections;

public class CBlock : MonoBehaviour {
	// 壊したら入る点数(初期値は10点)
	public int ADD_SCORE = 10;
	
	static int iBlockCnt;
	
	// Use this for initialization
	void Start () {
		iBlockCnt = GameObject.FindGameObjectsWithTag("Block").Length;
	}
	
	// Update is called once per frame
	void Update () {
	
	}
	
	public void setDamage() {
		CGame.addScore(ADD_SCORE);
		Destroy(gameObject);
		iBlockCnt--;
		if (iBlockCnt <= 0) {
			Application.LoadLevel("Game");
		}
	}
}

ここまでの「CGame.cs」

using UnityEngine;
using System.Collections;

public class CGame : MonoBehaviour {
	// GUISkin
	public GUISkin skinBlock;
	// 点数
	private static int iScore;
	// 現在の残機。マイナスになるとゲームオーバー。
	private static int iLeft;
	// 面が開始する時点での残機(3回ミスが出来る)
	private static int INIT_LEFT = 2;

	// Use this for initialization
	void Start () {
		//CGame.initGame();
	}
	
	// Update is called once per frame
	void Update () {
	
	}
	
	public static void initGame() {
		iScore = 0;
		iLeft = INIT_LEFT;
	}
	
	void OnGUI() {
		GUIStyle stLabel = skinBlock.FindStyle("HUD Label");
		GUIStyle stLabelS = skinBlock.FindStyle("HUD LabelS");
		GUIStyle stNum = skinBlock.FindStyle("HUD Num");
		GUIStyle stNumS = skinBlock.FindStyle("HUD NumS");
		float grid = Screen.height/60f;
		float cx = Screen.width/2f;
		// スコアラベル
		GUI.Label(new Rect(cx+grid*22+1,grid*4+1,100,20),"SCORE",stLabelS);
		GUI.Label(new Rect(cx+grid*22,grid*4,100,20),"SCORE",stLabel);
		// 残機ラベル
		GUI.Label(new Rect(cx+grid*22+1,grid*50+1,100,20),"LEFT",stLabelS);
		GUI.Label(new Rect(cx+grid*22,grid*50,100,20),"LEFT",stLabel);
		// スコア
		GUI.Label(new Rect(cx+grid*20+1,grid*7+1,grid*18,20),""+iScore,stNumS);
		GUI.Label(new Rect(cx+grid*20,grid*7,grid*18,20),""+iScore,stNum);
		// 残機
		GUI.Label(new Rect(cx+grid*20+1,grid*53+1,grid*18,20),""+iLeft,stNumS);
		GUI.Label(new Rect(cx+grid*20,grid*53,grid*18,20),""+iLeft,stNum);
	}
	
	public static void addScore(int add) {
		iScore += add;
	}
	
	public static bool decLeft() {
		if (iLeft <= 0) {
			return true;
		}
		iLeft--;
		return false;
	}
}

ここまでの「CTitle.cs」

using UnityEngine;
using System.Collections;

public class CTitle : MonoBehaviour {

	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
		if(Input.GetButton("Jump")) {
			CGame.initGame();
			Application.LoadLevel("Game");
		}
	}
	
	// GUI描画
	void OnGUI() {
		GUI.Label(new Rect(0,0,100,20),"タイトル");
	}
}