Unity 公式チュートリアル「Space Shooter tutorial」をコードから理解してみる という投稿をしましたが、今回はこれをベースに拡張し、簡易的なゲームエンジン的なものとして汎用化し、それを用いて独自のシューティングゲームを作成してみます。
簡単なサンプルをもとに、自分の欲しい機能を考え、なるべく汎用化してコードを拡張して、実装していく感じ。完成したゲームは完全にオリジナルとは言えませんが、各コードは今後、自身のオリジナルゲームを作成していくうえでのコアパーツとして活躍してくれる、んじゃないかと期待しています。
今回のゲーム
元となる Unity 公式チュートリアル Space Shooter tutorial は以下でした。
これを元に、今回作成したゲームは以下になります。
コチラ で実際にプレイ可能です。
システム改善
さて、サンプルゲームに対して好きに拡張していきましょう。
敵の耐久力を設定したい
今のサンプルだと、敵も岩石も自機の弾一発で破壊できてしまいます。オブジェクトごとに耐久力を設定して、硬い敵は何発も撃たないと撃破できないように拡張してみましょう。
敵の破壊などを司っている 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 を見ると、以下のように耐久力の設定欄が追加されていることがわかります。
初期値は0ですので、Done_Asteroid 01
と Done_Asteroid 02
には 2 の値を設定します。これは自機の弾が2発で破壊できる、ということを意味しています。
Done_Asteroid 03
には 4 の耐久力を設定し、ついでに表示サイズを1.5倍に拡大し、スコア値を倍の 20 に変更してみましょう。この岩石は他よりちょっと大きく、4発撃たないと破壊できないが、スコアが高い、という変化が生まれました。
設定された耐久値(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 という無料アセットを発見しました。このアセットを早速、ダウンロードしてプロジェクトに追加します。
追加すると Missiles というフォルダが追加されますので、PreFabs にある Missile Cruise を使ってみましょう。
まずは Game Controller のインスペクターにある、Hazards のリストを1つ増やし、最後の要素としてこの PreFab を追加します。
そして PreFab である Missile Cruise に必要なコンポーネントを追加していきます。まず追加するのは、動作と衝突検出のためのリジットボディとボックスコライダー。リジットボディは「重力の使用」を外しておきます。あと少し大きい気がするので、0.8倍に縮小しておきましょう。あと、タグも Enemy にセットしておきます。
さて、他の敵と同じように、スクリプトも追加しておきましょう。このへんはお好みですが、速度は岩石の倍、耐久度は最低の1としておきました。
さて、テストプレイをしてみましょう。問題なく動作はしている!のですが… ミサイルが逆向きのまま落ちてきますね… かなり間抜けな感じです。
向きを直すにはどうしたら良いか
一番簡単なのは、このミサイルの PreFab 自体を修正して、向きを変えてあげることですね。でも今回は、コードでなんとか対応してみましょう。
今回の向きの原因と考えられるのは、Done_GameController.cs にある SpawnWaves 関数で、敵オブジェクトを生成している部分です。向きを固定しちゃってますね。
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行を追加するだけです。
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 です。
さてこれで、岩石の種類も増えて、敵ミサイルも追加されましたね。真っすぐ進んでくる敵は硬いのあり、速いのありで、もうお腹いっぱい。必要十分な感じでしょうか。
ミサイルにバックファイアを追加してみる
PreFab は基本、単なるモデルですから自由に改変可能です。なので既存の敵から Enemy Engines をコピペして、ミサイルの後ろに配置してみました。ちょっと短いですが、同じエンジンを積んでいる感が出るかな?などと。
うん、良い感じになりました。
寄せ集めの素材でも、何か共通点をもたせると意外としっくりきたりします。色とか、共通パーツなどなど。お手軽でわりと効果的な修正だったりしますので、いろいろ試してみてください。
更に別の敵キャラを追加してみる
Asset Store をまた検索して、無料アセット Low poly combat drone がありましたので、これも強めの敵キャラとして追加してみましょう。
こちらのアセットはモデルファイルのみのようなので、いったんヒエラルキーに追加し、サイズなど調整した後、PreFab として保存しておきます。
例によってエンジンを今度は二つ、コピペしておきましょう。
そして Game Controller の Hazards
のサイズを1増やして、この Drone の PreFab を追加してあげます。もう慣れたもの、って感じですよね。
そしてメッシュコライダー(複雑な形状なので)のコンポーネントを追加し、トリガーに設定しておきます。またリッジボディのコンポーネントを追加し、重力の使用をOFFしておきます。
またミサイルと同様に基本的な2つのスクリプトも追加しておきましょう。速度はかなり遅く、そして耐久力も10と高くしてみました。スコアは高めの50。中ボス的なイメージです。
これだけでも、硬い大きな敵がゆっくりと向かってくることになり、かなり迫力があります。後は動作や攻撃のロジック設定だけ、ですね。
敵に弾を撃たせる
ここで、元からある Done_Enemy_Ship
PreFab に設定された、他のスクリプトを再確認しておきましょう。
弾を発射する Done_WeaponController
スクリプトと、横移動する Done_EvasiveManeuver
スクリプトが利用されています。後者は設定できる項目が多くて、汎用性が高そうですね。
さて、まずは弾を発射させてみましょう。今回の敵は大きいので、同時に2発の弾を発射するように拡張したいところです。
というわけで、Done_WeaponController
を拡張してみました。元々の弾を1発撃つ部分はそのままに、サブショットを配列で登録できるようにしてあります。今回はメインと同じ弾をサブショットとして1つ登録することにより、2発の弾を同時に発射させてみます。
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
の設定欄が追加され、設定されているのがわかります。また発射間隔は既存の敵より間をあけています。
そして弾の発射位置と方向を示すオブジェクトは Shot Spawn
と Sub Shot Spawns
に指定されていますが、Pre Fab 上では以下のような位置に配置してあります。
この弾の発射位置を示すオブジェクトですが、Y軸方向に 10度 ずつ回転させ、わずかに外向きに開かせているのがポイントです。これにより、発射される2つの弾が少し広がって発射される感じになります。こんな感じ。
硬い敵ですので、連射で倒せるよう正面が安全地帯にしてある、という感じでしょうか。
元のサンプルの設計が優れていて、こういった弾を発射する位置は、専用のオブジェクトを用意してコードから参照したほうが、わかりやすいですし、修正も容易です。今回の改造も、そういった長所を生かしつつ進めています。
敵の動作ロジックを設定してみる
さて、引き続き敵の動きを設定していきましょう。幸いなことに Done_EvasiveManeuver
スクリプトがわりと汎用的なので、こちらは設定値の変更で対応したいとおもいます。
実際に動かしつつ、以下のような値を設定してみました。ちょっとモッサリとした動きになっています。
と、ここで問題が発生しました。横移動したこの敵キャラが、上下逆に戻ってしまうのです。向きを直すにはどうしたら良いか でお手軽に修正したモデルの向きの問題が、横移動のロジック(の傾け)でリセットされてしまうのが原因のようです。
というわけで、Done_EvasiveManeuver
を以下のように修正し、オブジェクトの向きを維持しつつ移動(傾け)を実行するようにしてみました。
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 のなかを眺めていたら、標準の敵キャラの赤いバージョンのテクスチャが用意されていることを発見!さっそく利用してみましょう。
Materials にある vehicle_enemyShip_metal_mat
を複製し(編集メニューから「複製」を選ぶか、Ctrl+D キー)、vehicle_enemyShip_metal_mat 1
というマテリアルが作成されるので、vehicle_enemyShip_metal_mat Red
とリネームします。
そしてインスペクター上で Main Maps にあるアルベド(の左にある小さな丸)をクリックして、先ほど発見した赤い敵機のテクスチャを選択します。
次に、_Complete-Game/PreFabs にある Drone_EnemyShip
を複製して、Drone_EnemyShip Red
PreFab を作成します。
そしてこの Drone_EnemyShip Red
PreFab を開き、vehicle_enemyShip
オブジェクトを選択し、右側のインスペクターに、さきほどの vehicle_enemyShip_metal_mat Red
マテリアルをドラッグ&ドロップして設定します。中央の Scene に表示された敵キャラが、紫色から赤色に変われば成功です。
赤いライバル機を登録しよう
さて、これだけだと単に「赤い敵機のモデル(PreFab)を作成した」だけですから、この赤い敵機をゲームに登場させましょう。
はい、これだけで登録は完了です。何故なら、必要なスクリプトやその設定は既に元の敵機に設定済みで、今回の赤い敵機はそれを複製したものなので、それら設定も引き継いでいるのです。
試しにゲームをプレイしてみると、赤い敵機がちゃんと登場して、もともとの紫の敵機と同様に飛行し、弾を撃つ様子を見ることができます。ただ、これでは色違いの敵機が追加されただけで、ライバル機っぽくは無いですよね。ガノタ(ガンダム好き)としてはこれでは終われない!
というわけで、まず射撃を2連射にしたいとおもいます。Drone_EnemyShip Red
PreFab を開き、Shot Spawn
オブジェクトを複製して、z軸を -1 ぐらいズラして配置します。
そしてインスペクターで Drone_Weapon_Controller
スクリプトの設定で、Sub Shot には通常の Enemy の弾を、Sub Shot Spawns には、今回追加した発射位置を指定すればokです。
実際にプレイして試してみると、赤い敵機が2連で弾を撃ってくるのが確認できます。
もう少し個性を出してみましょう。耐久力を3に変更して倒しにくくし、またマニューバの Time を伸ばし、Wait を短くすることで、より活発に動くようにしてみます。
うん、これで赤い敵機は通常の敵機より、3倍ぐらい強くなった!気がしますね。
現時点でのゲームはこんな感じ
サンプルゲームと基本的には変わらないのですが、現時点では以下のような感じで、かなりシューティングゲームっぽくなってきました。
- 岩石に大きさと耐久度の差がついた
- 真っすぐ高速に進んでくるミサイルが追加された
- 硬くてゆっくり進み、弾を斜めに発射する中ボス的な敵キャラが追加された
- 標準的な敵の強化版である赤い敵キャラが追加された
というわけで
以上、敵機の種類をいろいろ増やしてみました。動き、耐久力、弾の出し方などなど、敵キャラの個性の出し方っていろいろ考えられます。またこれ、シューティングゲームを作っていくなかでも、楽しい作業だったりします。
が、長くなってきたので、いったん【前編】として終わりたいとおもいます。引き続き【後編】では、タイトル画面を作成したり、ステージごとに難易度が上昇するなど、ゲームとして最低限の仕上げをします。
それではまた!