Unity4.6オープンβでチュートリアルのShoothing Game作ってみる


今回のチュートリアルは[Unity公式チュートリアルより](http://japan.unity3d.com/developer/document/tutorial/2d-shooting-game/ "2Dシューティングゲーム")

##やる事
次期バージョンとして発表されているv4.6のオープンβ版を使ってチュートリアルを完結させる

##環境
- Macbook Pro 2011 Custom (16G, SSD)

##準備
- [Unity 4.6.OB17](http://unity3d.com/unity/beta/4.6 "Unity")
- [公式チュートリアルの最初のプロジェクトファイル](http://japan.unity3d.com/developer/document/tutorial/2d-shooting-game/game/project/ShootingGame.zip "Project")
- 公開先は[github](https://github.com/MrSmart00/Unity-Shooting-Tutorial "github")

ライセンス的に素材なんかも上げて問題無さそうだったので上げちゃいます。

##作る
###第01 ~ 第12回までの内容
- 第01回　スプライト / スプライトアニメーションの作成
スプライト画像を切ってSpriteオブジェクトにしてPrefab化するみたいなゲーム作る上で必要なキャラクターの準備。ただスプライトを再生するだけでもなんか嬉しいです！
特に問題無し。チュートリアル通りに普通に終了。

- 第02回　プレイヤーの移動
早くもC#登場！やっぱり自分で動かせるようになるとテンション上がります！
C#初心者の人はとりあえずコピペで問題無いですが、そんなに難しくないし、各行にちゃんとコメント書いてあるので、読んでおくと良いかと。

- 第03回　プレイヤーから弾を撃つ
スクリプトからオブジェクト生成、コルーチン制御とソーティングレイヤーのお話。

- 第04回　敵を作成しよう
共有処理の別コンポーネント化のお話。
この辺はオブジェクト指向の考えが必要になってきますね。
- 第05回　当たり判定とアニメーションイベントとレイヤー
2Dコライダーとあたり判定のお話。
Animationとか出て来てちょっと複雑になってきました。

- 第06回　背景を作る
Quadを使って移動スピードの違う背景群を表示するお話。

- 第07回　Wave型の仕組み作り
登録したprefabをスクリプトで自動生成するお話。

- 第08回　音をつける
BGMとSEの設定のお話。

- 第09回　プレイヤーの移動制限と様々な修正
数値制限の掛け方のお話。
ここでちょっと高度な書き方として抽象クラス化にする例が書かれているので、後で抽象クラス化する方法でまとめます。
- 第10回　タイトルを付ける
GUI Text使ってタイトルを表示するお話なんですが...
4.6にはGUI Text無くなっちゃったので、下で解説します。

- 第11回　エネミーのHP、弾の攻撃力、アニメーションの追加
終盤なだけあってなかなか難しくなって来たお話。
ここはエディターの操作がメインになってますが、4.6でも問題無かったです。

- 第12回　Waveを5個にする、スコアの実装
スコアの所でまたもGUI Text表示が入って来るので、その辺りを第１０回の時と同じようにしてあげる必要があります。

###第10回　タイトルを付ける
####”10.1　タイトルの表示”	
GUI Textを選択するところですが、GUI Textがありません...
代わりに今回のバージョンで実装されたuGUIの中からTextを使うことでなんとかなりそうです。

```
create > UI > Text
```

Textを作成するとヒエラルキー上にCanvas/Textの構成で生成されるので、Canvasの名前をチュートリアルと同じ"Title"に変更

![スクリーンショット 2014-09-07 16.20.04.png](https://qiita-image-store.s3.amazonaws.com/0/45511/e1a00496-dde0-64cb-0e7d-e45b3ab26173.png "スクリーンショット 2014-09-07 16.20.04.png")
Press Xの方も同じ要領でPos Y(なんでPositionではないんでしょうか...?)の値を設定すれば問題無さそうです。

####10.2　タイトル -> ゲームスタート ( -> 死んだら ) -> タイトル のマネージャークラスを作る
Manager.csの記述をオリジナルのまま使っても問題無いんですが、なんか気持ち悪い感が拭えなかったので、こんな感じに書き換えてみました。

```c#:Manager.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class Manager : MonoBehaviour {

	// Playerプレハブ
	public GameObject player;
	
	// タイトル
	public Canvas title;

	void Update ()
	{
		// ゲーム中ではなく、Xキーが押されたらtrueを返す。
		if (IsPlaying () == false && Input.GetKeyDown (KeyCode.X)) {
			GameStart ();
		}
	}
	
	void GameStart ()
	{
		// ゲームスタート時に、タイトルを非表示にしてプレイヤーを作成する
		title.enabled = false;
		Instantiate (player, player.transform.position, player.transform.rotation);
	}
	
	public void GameOver ()
	{
		// ゲームオーバー時に、タイトルを表示する
		title.enabled = true;
	}

	public bool IsPlaying ()
	{
		// ゲーム中かどうかはタイトルの表示/非表示で判断する
		return title.enabled == false;
	}
}

```
この書き方をする場合、GameObjectのManagerのInspectorにこのスクリプトをコンポーネント登録した時に、PlayerオブジェクトとTitleオブジェクトの登録が必須になるので注意！

###第12回　Waveを5個にする、スコアの実装
####12.2　スコアの実装
ここでもGUI Textを使ってるので、少し修正が必要。
まず、GUI Textの代わりに

```
create > UI > Text
```
とやるんだけど、なぜか、何処で作成しても第１０回で作ったTitleのCanvasの方に作られてしまうので、

```
create > UI > Canvas
//作ったCanvasを選択しつつ
create > UI > Text
```
と一度ルートのCanvasを作ってその中に作る。
次にCanvasの名前を"Score GUI"にして、Text２つ作ってそれぞれを"Score"、"HighScore"とリネームする。
続いてScoreとHighScoreのInspectorでこんな感じで入力すればだいたい一緒になる。
![No12-2.jpg](https://qiita-image-store.s3.amazonaws.com/0/45511/3884b167-bb60-cccc-591a-eaebb76c5748.jpeg "No12-2.jpg")

あとはScore.csの書き換え作業。今回は第１０回の時と違って書き換え必須になるので、必ずリプレイスして下さい。（とは言え、変わった所はクラス名くらいですが...）

```c#:Score.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class Score : MonoBehaviour {

	// スコアを表示するGUIText
	public Text scoreGUIText;
	
	// ハイスコアを表示するGUIText
	public Text highScoreGUIText;
	
	// スコア
	private int score;
	
	// ハイスコア
	private int highScore;
	
	// PlayerPrefsで保存するためのキー
	private string highScoreKey = "highScore";
	
	void Start ()
	{
		Initialize ();
	}
	
	void Update ()
	{
		// スコアがハイスコアより大きければ
		if (highScore < score) {
			highScore = score;
		}
		
		// スコア・ハイスコアを表示する
		scoreGUIText.text = score.ToString ();
		highScoreGUIText.text = "HighScore : " + highScore.ToString ();
	}
	
	// ゲーム開始前の状態に戻す
	private void Initialize ()
	{
		// スコアを0に戻す
		score = 0;
		
		// ハイスコアを取得する。保存されてなければ0を取得する。
		highScore = PlayerPrefs.GetInt (highScoreKey, 0);
	}
	
	// ポイントの追加
	public void AddPoint (int point)
	{
		score = score + point;
	}
	
	// ハイスコアの保存
	public void Save ()
	{
		// ハイスコアを保存する
		PlayerPrefs.SetInt (highScoreKey, highScore);
		PlayerPrefs.Save ();
		
		// ゲーム開始前の状態に戻す
		Initialize ();
	}
}
```
ついでに、第１０回でManager.csをここと同じように書き換えた人は、今回のManager.csへの全コピペするとせっかく書いたのが全部消えちゃうので、

```c#:Manager.cs
	public void GameOver ()
	{
		// ハイスコアの保存
		FindObjectOfType<Score>().Save();

		// ゲームオーバー時に、タイトルを表示する
		title.enabled = true;
	}
```
だけ書き換えると幸せになれると思います。

###まとめ
やっぱり良く出来たチュートリアルですね！大きな機能変更があってもそんなに労する事無く完結出来ました。

##追記
###Spaceshipクラスの抽象化
Spaceshipクラスを抽象化、Player、Enemyクラスで実現化する事で、よりコードをシンプルに出来る。

```c#:Spaceship.cs
using UnityEngine;

// Rigidbody2Dコンポーネントを必須にする
[RequireComponent(typeof(Rigidbody2D))]
public abstract class Spaceship : MonoBehaviour
{
	// 移動スピード
	public float speed;
	
	// 弾を撃つ間隔
	public float shotDelay;
	
	// 弾のPrefab
	public GameObject bullet;
	
	// 爆発のPrefab
	public GameObject explosion;
	
	// 爆発の作成
	public void Explosion ()
	{
		Instantiate (explosion, transform.position, transform.rotation);
	}
	
	// 弾の作成
	public void Shot (Transform origin)
	{
		Instantiate (bullet, origin.position, origin.rotation);
	}

	protected abstract void Move (Vector2 direction);
}
```

```c#:Player.cs
using UnityEngine;
using System.Collections;

public class Player : Spaceship {
	
	IEnumerator Start ()
	{
		while (true) {
			
			// 弾をプレイヤーと同じ位置/角度で作成
			Shot (transform);
			
			// ショット音を鳴らす
			audio.Play();
			
			// shotDelay秒待つ
			yield return new WaitForSeconds (shotDelay);
		}
	}
	
	void Update ()
	{
		// 右・左
		float x = Input.GetAxisRaw ("Horizontal");
		
		// 上・下
		float y = Input.GetAxisRaw ("Vertical");
		
		// 移動する向きを求める
		Vector2 direction = new Vector2 (x, y).normalized;
		
		// 移動の制限
		Move (direction);
		
	}
	
	// 機体の移動
	protected override void Move (Vector2 direction)
	{
		// 画面左下のワールド座標をビューポートから取得
		Vector2 min = Camera.main.ViewportToWorldPoint(new Vector2(0, 0));
		
		// 画面右上のワールド座標をビューポートから取得
		Vector2 max = Camera.main.ViewportToWorldPoint(new Vector2(1, 1));
		
		// プレイヤーの座標を取得
		Vector2 pos = transform.position;
		
		// 移動量を加える
		pos += direction  * speed * Time.deltaTime;
		
		// プレイヤーの位置が画面内に収まるように制限をかける
		pos.x = Mathf.Clamp (pos.x, min.x, max.x);
		pos.y = Mathf.Clamp (pos.y, min.y, max.y);
		
		// 制限をかけた値をプレイヤーの位置とする
		transform.position = pos;
	}
	
	// ぶつかった瞬間に呼び出される
	void OnTriggerEnter2D (Collider2D c)
	{
		// レイヤー名を取得
		string layerName = LayerMask.LayerToName(c.gameObject.layer);
		
		// レイヤー名がBullet (Enemy)の時は弾を削除
		if( layerName == "Bullet (Enemy)")
		{
			// 弾の削除
			Destroy(c.gameObject);
		}
		
		// レイヤー名がBullet (Enemy)またはEnemyの場合は爆発
		if( layerName == "Bullet (Enemy)" || layerName == "Enemy")
		{
			// Managerコンポーネントをシーン内から探して取得し、GameOverメソッドを呼び出す
			FindObjectOfType<Manager>().GameOver();
			
			// 爆発する
			Explosion();
			
			// プレイヤーを削除
			Destroy (gameObject);
		}
	}
}
```

```c#:Enemy.cs
using UnityEngine;
using System.Collections;

public class Enemy : Spaceship {

	// ヒットポイント
	public int hp = 1;

	// 弾を撃つかどうか
	public bool canShot;

	// スコアのポイント
	public int point = 100;

	IEnumerator Start ()
	{
		// ローカル座標のY軸のマイナス方向に移動する
		Move (transform.up * -1);
		
		// canShotがfalseの場合、ここでコルーチンを終了させる
		if (canShot == false) {
			yield break;
		}
		
		while (true) {
			
			// 子要素を全て取得する
			for (int i = 0; i < transform.childCount; i++) {
				
				Transform shotPosition = transform.GetChild (i);
				
				// ShotPositionの位置/角度で弾を撃つ
				Shot (shotPosition);
			}
			
			// shotDelay秒待つ
			yield return new WaitForSeconds (shotDelay);
		}
	}
	
	// 機体の移動
	protected override void Move (Vector2 direction)
	{
		rigidbody2D.velocity = direction * speed;
	}
	
	void OnTriggerEnter2D (Collider2D c)
	{
		// レイヤー名を取得
		string layerName = LayerMask.LayerToName (c.gameObject.layer);
		
		// レイヤー名がBullet (Player)以外の時は何も行わない
		if (layerName != "Bullet (Player)") return;
		
		// PlayerBulletのTransformを取得
		Transform playerBulletTransform = c.transform.parent;
		
		// Bulletコンポーネントを取得
		Bullet bullet =  playerBulletTransform.GetComponent<Bullet>();
		
		// ヒットポイントを減らす
		hp = hp - bullet.power;
		
		// 弾の削除
		Destroy(c.gameObject);
		
		// ヒットポイントが0以下であれば
		if(hp <= 0 )
		{
			// スコアコンポーネントを取得してポイントを追加
			FindObjectOfType<Score>().AddPoint(point);
			
			// 爆発
			Explosion ();
			
			// エネミーの削除
			Destroy (gameObject);
			
		}else{
			
			GetComponent<Animator> ().SetTrigger("Damage");
			
		}
	}
}
```