2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Unity 公式チュートリアル「Space Shooter tutorial」をベースに自作ゲームを作る【前編】

Last updated at Posted at 2019-05-05

Unity 公式チュートリアル「Space Shooter tutorial」をコードから理解してみる という投稿をしましたが、今回はこれをベースに拡張し、簡易的なゲームエンジン的なものとして汎用化し、それを用いて独自のシューティングゲームを作成してみます。

簡単なサンプルをもとに、自分の欲しい機能を考え、なるべく汎用化してコードを拡張して、実装していく感じ。完成したゲームは完全にオリジナルとは言えませんが、各コードは今後、自身のオリジナルゲームを作成していくうえでのコアパーツとして活躍してくれる、んじゃないかと期待しています。

今回のゲーム

元となる Unity 公式チュートリアル Space Shooter tutorial は以下でした。
qiita.gif
これを元に、今回作成したゲームは以下になります。
qiita.gif
コチラ で実際にプレイ可能です。

システム改善

さて、サンプルゲームに対して好きに拡張していきましょう。

敵の耐久力を設定したい

今のサンプルだと、敵も岩石も自機の弾一発で破壊できてしまいます。オブジェクトごとに耐久力を設定して、硬い敵は何発も撃たないと撃破できないように拡張してみましょう。

敵の破壊などを司っている Done_DestroyByContact.cs は今回の拡張における最重要スクリプトでもあるので、以下に拡張したソースをそのまま掲載しますね。

Done_DestroyByContact.cs
public class Done_DestroyByContact : MonoBehaviour
{
	public GameObject explosion;
	public GameObject playerExplosion;
	public int scoreValue;
	public int objectHp;               // 追加
	private Done_GameController gameController;
	private int hp;                    // 追加 (1)
	private Collider lastTriggerEnter; // 追加 (4)

	void Start ()
	{
		GameObject gameControllerObject = GameObject.FindGameObjectWithTag ("GameController");
		hp = objectHp;                 // 追加 (1)
		if (gameControllerObject != null)
		{
			gameController = gameControllerObject.GetComponent <Done_GameController>();
        }
		if (gameController == null)
		{
			Debug.Log ("Cannot find 'GameController' script");
		}
	}

	void OnTriggerEnter (Collider other)
	{
		if (other.tag == "Enemy" || other.tag == "Boundary")
		{
			return;
		}

		if (other.tag == "Player")
		{
			Instantiate(playerExplosion, other.transform.position, other.transform.rotation);
			gameController.GameOver();
		}

        if (lastTriggerEnter != other)     // 追加 (4)
        {                                  // 追加 (4)
            hp--;                          // 追加 (2)
            lastTriggerEnter = other;      // 追加 (4)
        }                                  // 追加 (4)
        if (hp <= 0)                       // 追加 (3)
        {                                  // 追加 (3)
            if (explosion != null)
            {
                Instantiate(explosion, transform.position, transform.rotation);
            }
            gameController.AddScore(scoreValue);
            Destroy(gameObject);
        }                                  // 追加 (3)
        Destroy(other.gameObject);
    }
}

まず public int objectHp の部分で、対象の耐久力を設定する入力欄を用意します。このスクリプトをアタッチした岩石の PreFab を見ると、以下のように耐久力の設定欄が追加されていることがわかります。
image.png
初期値は0ですので、Done_Asteroid 01Done_Asteroid 02 には 2 の値を設定します。これは自機の弾が2発で破壊できる、ということを意味しています。

Done_Asteroid 03 には 4 の耐久力を設定し、ついでに表示サイズを1.5倍に拡大し、スコア値を倍の 20 に変更してみましょう。この岩石は他よりちょっと大きく、4発撃たないと破壊できないが、スコアが高い、という変化が生まれました。
image.png

設定された耐久値(Object Hp)は、Start() 関数でインスタンス変数 hp にコピーされ、これがこの対象の残り耐久力を管理する変数です。(1)の部分ですね。

これまでは「弾に当たったら破壊」だったのが、「弾に当たったら hp から1を引いて、0 以下になったら破壊」するように処理を書き換えます。(2) の部分で hp の値を減らしていて、(3) の部分の if 文が 0 以下になったかどうかを判断するための条件式です。

追加のコード説明

この状態で実際にゲームをプレイしたところ、なぜか「耐久力が2ある岩石が、弾1発で破壊されることがある」という問題に遭遇しました。

調べてみると「弾が岩石に当たった瞬間に、OnTriggerEnter 関数が複数回呼ばれてしまう」というのがこの問題の原因でした。理由はよくわかりません。

そこで同じ弾が2度ヒットしないよう、lastTriggerEnter 変数を導入してみました。これは最後に衝突した対象を、保持しておくための内部変数です。(4)の部分ですね。

同じ衝突相手からOnTriggerEnter 関数が2度呼ばれた場合、1度目は普通に対応しますが、その相手を lastTriggerEnter 変数で覚えておきます。2度目の呼び出しでは、同じ相手と分かりますので、処理をスキップすれば良いのです。

敵の種類を増やしたい

今度は敵の種類を増やしてみます。まずは動きが単純な、高速で真っすぐに進むミサイルを追加してみましょう。

Asset Store を探して、Space Missiles という無料アセットを発見しました。このアセットを早速、ダウンロードしてプロジェクトに追加します。
image.png
追加すると Missiles というフォルダが追加されますので、PreFabs にある Missile Cruise を使ってみましょう。
image.png
まずは Game Controller のインスペクターにある、Hazards のリストを1つ増やし、最後の要素としてこの PreFab を追加します。
image.png
そして PreFab である Missile Cruise に必要なコンポーネントを追加していきます。まず追加するのは、動作と衝突検出のためのリジットボディとボックスコライダー。リジットボディは「重力の使用」を外しておきます。あと少し大きい気がするので、0.8倍に縮小しておきましょう。あと、タグも Enemy にセットしておきます。
image.png
さて、他の敵と同じように、スクリプトも追加しておきましょう。このへんはお好みですが、速度は岩石の倍、耐久度は最低の1としておきました。
image.png
さて、テストプレイをしてみましょう。問題なく動作はしている!のですが… ミサイルが逆向きのまま落ちてきますね… かなり間抜けな感じです。
image.png

向きを直すにはどうしたら良いか

一番簡単なのは、このミサイルの PreFab 自体を修正して、向きを変えてあげることですね。でも今回は、コードでなんとか対応してみましょう。

今回の向きの原因と考えられるのは、Done_GameController.cs にある SpawnWaves 関数で、敵オブジェクトを生成している部分です。向きを固定しちゃってますね。

Done_GameController.cs
    GameObject hazard = hazards[Random.Range(0, hazards.Length)];
    Vector3 spawnPosition = new Vector3(Random.Range(-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
    Quaternion spawnRotation = Quaternion.identity; // ココ
    Instantiate(hazard, spawnPosition, spawnRotation);

が、今回は敵の動作を初期化する Done_Mover.cs のほうを修正して対応してみましょう。以下の2行を追加するだけです。

Done_Mover.cs
public class Done_Mover : MonoBehaviour
{
	public float speed;
	public Vector3 turn; // 追加

	void Start ()
	{
        GetComponent<Rigidbody>().velocity = transform.forward * speed;
        transform.Rotate(turn); // 追加
	}
}

敵の初期化の際に、向きが指定されていれば、その方向に向きを変えるだけ、で対応しています。リジットボディに移動方向を指定している 後に ロジックを追加したのがポイント。前にすると移動方向もその方向の影響を受けてしまい面倒です。

ミサイルの PreFab のインスペクターで Y軸 を 180 度回転するように指定して、ミサイルの向きを変えれば ok です。
image.png
さてこれで、岩石の種類も増えて、敵ミサイルも追加されましたね。真っすぐ進んでくる敵は硬いのあり、速いのありで、もうお腹いっぱい。必要十分な感じでしょうか。

ミサイルにバックファイアを追加してみる

PreFab は基本、単なるモデルですから自由に改変可能です。なので既存の敵から Enemy Engines をコピペして、ミサイルの後ろに配置してみました。ちょっと短いですが、同じエンジンを積んでいる感が出るかな?などと。
image.png
うん、良い感じになりました。
image.png
寄せ集めの素材でも、何か共通点をもたせると意外としっくりきたりします。色とか、共通パーツなどなど。お手軽でわりと効果的な修正だったりしますので、いろいろ試してみてください。

更に別の敵キャラを追加してみる

Asset Store をまた検索して、無料アセット Low poly combat drone がありましたので、これも強めの敵キャラとして追加してみましょう。
image.png
こちらのアセットはモデルファイルのみのようなので、いったんヒエラルキーに追加し、サイズなど調整した後、PreFab として保存しておきます。
image.png
例によってエンジンを今度は二つ、コピペしておきましょう。
image.png
そして Game Controller の Hazards のサイズを1増やして、この Drone の PreFab を追加してあげます。もう慣れたもの、って感じですよね。
image.png
そしてメッシュコライダー(複雑な形状なので)のコンポーネントを追加し、トリガーに設定しておきます。またリッジボディのコンポーネントを追加し、重力の使用をOFFしておきます。
image.png
またミサイルと同様に基本的な2つのスクリプトも追加しておきましょう。速度はかなり遅く、そして耐久力も10と高くしてみました。スコアは高めの50。中ボス的なイメージです。
image.png
これだけでも、硬い大きな敵がゆっくりと向かってくることになり、かなり迫力があります。後は動作や攻撃のロジック設定だけ、ですね。

敵に弾を撃たせる

ここで、元からある Done_Enemy_Ship PreFab に設定された、他のスクリプトを再確認しておきましょう。
image.png
弾を発射する Done_WeaponController スクリプトと、横移動する Done_EvasiveManeuver スクリプトが利用されています。後者は設定できる項目が多くて、汎用性が高そうですね。

さて、まずは弾を発射させてみましょう。今回の敵は大きいので、同時に2発の弾を発射するように拡張したいところです。

というわけで、Done_WeaponController を拡張してみました。元々の弾を1発撃つ部分はそのままに、サブショットを配列で登録できるようにしてあります。今回はメインと同じ弾をサブショットとして1つ登録することにより、2発の弾を同時に発射させてみます。

Done_WeaponController.cs
public class Done_WeaponController : MonoBehaviour
{
	public GameObject shot;
	public Transform shotSpawn;
	public GameObject subShot;       // 追加
	public Transform[] subShotSpawn; // 追加
	public float fireRate;
	public float delay;

	void Start ()
	{
		InvokeRepeating ("Fire", delay, fireRate);
	}

	void Fire ()
	{
		Instantiate(shot, shotSpawn.position, shotSpawn.rotation);
		for (int i = 0; i < subShotSpawn.Length; i++) // 追加
		{                                             // 追加
			Instantiate(subShot, subShotSpawn[i].position, subShotSpawn[i].rotation); // 追加
		}                                             // 追加
		GetComponent<AudioSource>().Play();
	}
}

以下が実際のインスペクター上の設定になります。Shot と同様に Sub shot の設定欄が追加され、設定されているのがわかります。また発射間隔は既存の敵より間をあけています。
image.png
そして弾の発射位置と方向を示すオブジェクトは Shot SpawnSub Shot Spawns に指定されていますが、Pre Fab 上では以下のような位置に配置してあります。
image.png
この弾の発射位置を示すオブジェクトですが、Y軸方向に 10度 ずつ回転させ、わずかに外向きに開かせているのがポイントです。これにより、発射される2つの弾が少し広がって発射される感じになります。こんな感じ。
image.png
硬い敵ですので、連射で倒せるよう正面が安全地帯にしてある、という感じでしょうか。

元のサンプルの設計が優れていて、こういった弾を発射する位置は、専用のオブジェクトを用意してコードから参照したほうが、わかりやすいですし、修正も容易です。今回の改造も、そういった長所を生かしつつ進めています。

敵の動作ロジックを設定してみる

さて、引き続き敵の動きを設定していきましょう。幸いなことに Done_EvasiveManeuver スクリプトがわりと汎用的なので、こちらは設定値の変更で対応したいとおもいます。

実際に動かしつつ、以下のような値を設定してみました。ちょっとモッサリとした動きになっています。
image.png

と、ここで問題が発生しました。横移動したこの敵キャラが、上下逆に戻ってしまうのです。向きを直すにはどうしたら良いか でお手軽に修正したモデルの向きの問題が、横移動のロジック(の傾け)でリセットされてしまうのが原因のようです。

というわけで、Done_EvasiveManeuver を以下のように修正し、オブジェクトの向きを維持しつつ移動(傾け)を実行するようにしてみました。

Done_EvasiveManeuver.cs
	private Vector3 turn; // 追加

	void Start ()
	{
		currentSpeed = GetComponent<Rigidbody>().velocity.z;
		turn = GetComponent<Done_Mover>().turn; // 追加
		StartCoroutine(Evade());
	}

	void FixedUpdate ()
	{
		float newManeuver = Mathf.MoveTowards (GetComponent<Rigidbody>().velocity.x, targetManeuver, smoothing * Time.deltaTime);
		GetComponent<Rigidbody>().velocity = new Vector3 (newManeuver, 0.0f, currentSpeed);
		GetComponent<Rigidbody>().position = new Vector3
		(
			Mathf.Clamp(GetComponent<Rigidbody>().position.x, boundary.xMin, boundary.xMax), 
			0.0f, 
			Mathf.Clamp(GetComponent<Rigidbody>().position.z, boundary.zMin, boundary.zMax)
		);
		float f = (90f < turn.x && turn.x < 270f ? 1f : -1f) * (90f < turn.y && turn.y < 270f ? 1f : -1f) * (90f < turn.z && turn.z < 270f ? 1f : -1f); // 追加
		GetComponent<Rigidbody>().rotation = Quaternion.Euler (turn.x, turn.y, turn.z + GetComponent<Rigidbody>().velocity.x * tilt * f); // 修正
	}

さて、これで硬くて遅い、弾を多く撃つキャラが追加できました。ゲームの雰囲気も、ゲーム性も、だいぶ変化があったと思います。

赤いライバル機も欲しい

今回の Assets のなかを眺めていたら、標準の敵キャラの赤いバージョンのテクスチャが用意されていることを発見!さっそく利用してみましょう。
image.png
Materials にある vehicle_enemyShip_metal_mat を複製し(編集メニューから「複製」を選ぶか、Ctrl+D キー)、vehicle_enemyShip_metal_mat 1 というマテリアルが作成されるので、vehicle_enemyShip_metal_mat Red とリネームします。
image.png
そしてインスペクター上で Main Maps にあるアルベド(の左にある小さな丸)をクリックして、先ほど発見した赤い敵機のテクスチャを選択します。
image.png
次に、_Complete-Game/PreFabs にある Drone_EnemyShip を複製して、Drone_EnemyShip Red PreFab を作成します。
image.png
そしてこの Drone_EnemyShip Red PreFab を開き、vehicle_enemyShip オブジェクトを選択し、右側のインスペクターに、さきほどの vehicle_enemyShip_metal_mat Red マテリアルをドラッグ&ドロップして設定します。中央の Scene に表示された敵キャラが、紫色から赤色に変われば成功です。
image.png

赤いライバル機を登録しよう

さて、これだけだと単に「赤い敵機のモデル(PreFab)を作成した」だけですから、この赤い敵機をゲームに登場させましょう。
image.png
はい、これだけで登録は完了です。何故なら、必要なスクリプトやその設定は既に元の敵機に設定済みで、今回の赤い敵機はそれを複製したものなので、それら設定も引き継いでいるのです。

試しにゲームをプレイしてみると、赤い敵機がちゃんと登場して、もともとの紫の敵機と同様に飛行し、弾を撃つ様子を見ることができます。ただ、これでは色違いの敵機が追加されただけで、ライバル機っぽくは無いですよね。ガノタ(ガンダム好き)としてはこれでは終われない!

というわけで、まず射撃を2連射にしたいとおもいます。Drone_EnemyShip Red PreFab を開き、Shot Spawn オブジェクトを複製して、z軸を -1 ぐらいズラして配置します。
image.png
そしてインスペクターで Drone_Weapon_Controller スクリプトの設定で、Sub Shot には通常の Enemy の弾を、Sub Shot Spawns には、今回追加した発射位置を指定すればokです。
image.png
実際にプレイして試してみると、赤い敵機が2連で弾を撃ってくるのが確認できます。
image.png
もう少し個性を出してみましょう。耐久力を3に変更して倒しにくくし、またマニューバの Time を伸ばし、Wait を短くすることで、より活発に動くようにしてみます。
image.png
うん、これで赤い敵機は通常の敵機より、3倍ぐらい強くなった!気がしますね。

現時点でのゲームはこんな感じ

サンプルゲームと基本的には変わらないのですが、現時点では以下のような感じで、かなりシューティングゲームっぽくなってきました。

  • 岩石に大きさと耐久度の差がついた
  • 真っすぐ高速に進んでくるミサイルが追加された
  • 硬くてゆっくり進み、弾を斜めに発射する中ボス的な敵キャラが追加された
  • 標準的な敵の強化版である赤い敵キャラが追加された

qiita.gif

というわけで

以上、敵機の種類をいろいろ増やしてみました。動き、耐久力、弾の出し方などなど、敵キャラの個性の出し方っていろいろ考えられます。またこれ、シューティングゲームを作っていくなかでも、楽しい作業だったりします。

が、長くなってきたので、いったん【前編】として終わりたいとおもいます。引き続き【後編】では、タイトル画面を作成したり、ステージごとに難易度が上昇するなど、ゲームとして最低限の仕上げをします。

それではまた!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?