前の記事でプレイヤーをルートに沿うように移動させ、一人称視点のシューティングゲームを作ることができました。
前回の記事:
この記事ではシューティングゲームのメイン部分を開発していきます。
敵の生成処理、プレイヤーとの当たり判定&HPゲージの減少処理、そして弾を生成し、敵に向かって飛ばし、敵HPを減らす処理をゲームに組み込んでいきましょう!
シューティングゲームの敵を作ろう!
まずは敵ゲームオブジェクトを作成しましょう。
この講座では次の手順で敵を管理します。
- プレイヤーが指定した範囲に入ってくる→敵を生成する
- 敵をプレイヤーの方に移動させる。
敵を生成するエリアとなるゲームオブジェクトを作る
まずはプレイヤーが指定した範囲に入ってきたら、敵を生成するようにしましょう!
範囲はルートのポイントと同じように空のGameObjectにColliderをアタッチしたもので表現します。
Colliderには必ず「Is Trigger」にチェックを入れてください。
次の手順を行なってください。
- メニューからGameObject > 空のGameObjectをクリック。
- 名前はわかるようなものならなんでもOK(ここではEnemyAreaRoot)。
- 子ゲームオブジェクトを新しく生成。ここではEnemyAreaと名付けておく。
- 生成したGameObjectにCollider系のコンポーネントをアタッチする。
- アタッチしたCollider系のIs Triggerにチェックを入れる。
- シーン上の好きな位置に配置する。
- 使いまわせるようにプレハブ化する。
「EnemyArea」プレハブはシーン上のプレイヤーの進行ルートとかぶるように配置してください。
敵ゲームオブジェクトの生成 colliderをアタッチしてisTriggerをON
次に敵を表すプレハブを作成します。
- 適当な形状のGameObjectを作成(CylinderオブジェクトやCubeオブジェクトなどでOK)
- プレハブ化する時は名前を「Enemy」にする
- 当たり判定用のColliderコンポーネントをアタッチし、「Is Trigger」にチェックを入れる
「Enemy」には後々専用のコンポーネントをアタッチします。
見やすくするためにマテリアルをアタッチするのもいいでしょう。
また、「Enemy」も「Player」プレハブと同じようにルートGameObjectの下に実際に表示するモデルを追加していく方が汎用性が高まります。
特定エリアに入ってきたら敵を生成するスクリプトを作る
ここまでで敵を生成する際に使用するGameObjectができました。
「EnemyArea」と「Enemy」の二つのGameObjectで敵を管理します。
次は実際にゲーム進行時に敵を生成できるようにしていきましょう!
そのためにスクリプトを作成する必要があります。
次のサンプルコードの内容で「EnemyArea」スクリプトを新しく作成してください。
作成した「EnemyArea」は必ず「EnemyArea」プレハブにアタッチしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyArea : MonoBehaviour { public GameObject[] EnemyList; void Start() { foreach(var enemy in EnemyList) { enemy.SetActive(false); } } private void OnTriggerEnter(Collider other) { if(other.gameObject.name == "Player") { foreach (var enemy in EnemyList) { enemy.SetActive(true); } //一度敵を有効化したら、当たらないようにする var collider = GetComponent<Collider>(); collider.enabled = false; } } } |
「EnemyArea」コンポーネントには次のパラメータを設定できます。
- EnemyList: アクティブ化するシーン上の敵のリスト
無事コンパイルに成功したら、次の作業を行なってください。
- 敵をシーン上に配置する
- シーンに配置した「EnemyArea」コンポーネントに関連付けたいシーン上の敵を設定する。
「EnemyArea」の子GameObjectとして「Enemy」プレハブを配置することで、EnemyAreaごとに敵の位置やバリエーションを含めて設定できます。子オブジェクトの相対位置を変えることでEnemyAreaに付いてくる形で敵の生成場所を指定できます。
ただし、その際は親となる「EnemyArea」を非アクティブ化しないように注意しましょう!
子GameObjectは親GameObjectのアクティブ状況の影響を受け一緒に非アクティブになるからです。
作業が終わったら、再生してみましょう。
問題なければシーンに配置した「EnemyArea」の中にプレイヤーが入るとコンポーネントに設定した敵がアクティブ化して出現するようになります。
敵の生成エリアにプレイヤーが侵入→敵を生成の処理ができましたね。
敵をプレイヤーに向かって移動させる
次は生成した敵をプレイヤーに向かって移動させましょう!
「Enemy」スクリプトを新しく作成し、「Enemy」プレハブにアタッチしてください(複数種類あるときはそれぞれにアタッチ)。
「Enemy」スクリプトの内容は次のものになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy : MonoBehaviour { [Range(0, 100)] public float Speed = 10; public float DeadSecond = 10f; float _time; Player _player; void Start() { _time = 0f; _player = Object.FindObjectOfType<Player>(); } void Update() { _time += Time.deltaTime; if (_time >= DeadSecond) { Object.Destroy(gameObject); } else { var vec = _player.transform.position - transform.position; transform.position += vec.normalized * Speed * Time.deltaTime; } } } |
「Enemy」コンポーネントには次のパラメータが設定できます。
- Speed: 移動スピード
- DeadSecond:指定した秒数後に削除するようにするためのもの
コンパイルが成功しましたら、敵がプレイヤーに向かって移動するようになります。
プレイヤーの体力を減らせるようにしよう HPゲージの作り方
敵ができたので、プレイヤーに敵が当たったら体力ケージを減らすようにしましょう!
そのために「Player」クラスを次のように変更しましょう!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class Player : MonoBehaviour { public Transform[] RoutePoints; [Range(0, 200)] public float Speed = 10f; [Range(0, 50)] public float MoveSpeed = 10f; public float MoveRange = 40f; public float _initialLife = 100; public float Life = 100; public Image LifeGage; bool _isHitRoutePoint; IEnumerator Move() { var prevPointPos = transform.position; var basePosition = transform.position; var movedPos = Vector2.zero; foreach (var nextPoint in RoutePoints) { _isHitRoutePoint = false; //必ずfalseにする while (!_isHitRoutePoint) { //進行方向の計算 var vec = nextPoint.position - prevPointPos; vec.Normalize(); // プレイヤーの移動 basePosition += vec * Speed * Time.deltaTime; //上下左右に移動する処理 // 行列によるベクトルの変換に関係する知識を利用しています。 movedPos.x += Input.GetAxis("Horizontal") * MoveSpeed * Time.deltaTime; movedPos.y += Input.GetAxis("Vertical") * MoveSpeed * Time.deltaTime; movedPos = Vector2.ClampMagnitude(movedPos, MoveRange); var worldMovedPos = Matrix4x4.Rotate(transform.rotation).MultiplyVector(movedPos); //ルート上の位置に上下左右の移動量を加えている transform.position = basePosition + worldMovedPos; //次の処理は進行方向を向くように計算している transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(vec, Vector3.up), 0.5f); yield return null; } prevPointPos = nextPoint.position; } } private void OnTriggerEnter(Collider other) { if (other.gameObject.tag == "RoutePoint") { other.gameObject.SetActive(false); _isHitRoutePoint = true; } else if (other.gameObject.tag == "Enemy") { Life -= 10f; LifeGage.fillAmount = Life / _initialLife; other.gameObject.SetActive(false); Object.Destroy(other.gameObject); //当たった敵は削除する } } void Start() { StartCoroutine(Move()); } } |
コンパイルできたら、「Player」スクリプトに追加したものを「Player」オブジェクトに忘れずに設定しましょう!
- LifeGage:体力ケージ「LifeGage」をアタッチ
- Life:100 (好きな体力でもOK)
また当たり判定で必要になるので、「Enemy」プレハブのタグにも忘れずに「Enemy」を設定しましょう!
シューティングゲームの弾を打てるようにしよう
最後にプレイヤーが弾を撃てるようにしましょう!
この講座では敵をクリックしたらプレイヤーが敵を目掛けて弾を打つようにします。
弾用のプレハブを作成する
先に弾を表すプレハブを作成します。
次の手順で「Bullet」プレハブを作成してください。
- メニューのGameObject > 3D Object > Sphereをクリックし、作成したGameObjectをプレハブにする
- プレハブのタグに「Bullet」を指定する(のちの処理で当たり判定に利用)
- isTriggerにチェックを付ける(のちの処理で当たり判定に利用)
作成した「Bullet」プレハブには見分けやすくするためにマテリアルをアタッチするのもいいでしょう。
弾を打つ処理を追加する
新しく「Bullet」スクリプトを作成し、「Bullet」プレハブにアタッチしてください。
「Bullet」スクリプトは単純に最初に設定した方向に移動するものになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Bullet : MonoBehaviour { [Range(0, 10)] public float DeadSecond = 3f; [Range(0, 200)] public float Speed = 100f; public Vector3 StartPos { get; set; } public Vector3 TargetPos { get; set; } public void Init(Vector3 startPos, Vector3 targetPos) { StartPos = startPos; TargetPos = targetPos; StartCoroutine(Move()); } IEnumerator Move() { float time = 0f; transform.position = StartPos; var vec = (TargetPos - StartPos).normalized; while(time < DeadSecond) { time += Time.deltaTime; transform.position += vec * Speed * Time.deltaTime; yield return null; } Object.Destroy(gameObject); } } |
ここまでできましたら仕上げとして、弾を打つ処理を作成しましょう!
弾を打つ際には「Enemy」スクリプトと「Player」スクリプトに次の内容を追加しましょう!
「Player」スクリプトには次のコードを追加しましょう。
1 2 3 4 5 6 7 8 9 |
//Playerスクリプト // 次のフィールドを追加 public Bullet BulletPrefab; // 次のメソッドを追加 public void ShotBullet(Vector3 targetPos) { var bullet = Object.Instantiate(BulletPrefab, transform.position, Quaternion.identity); bullet.Init(transform.position, targetPos); } |
次は「Enemy」スクリプトには次のコードを追加しましょう。
1 2 3 4 5 6 |
//Enemyスクリプト // 次のメソッドを追加 private void OnMouseUpAsButton() { _player.ShotBullet(transform.position); } |
コンパイルできたら再生し、実際に弾を打ってみましょう!
その際に「Player」コンポーネントに追加したフィールドを忘れずにアタッチしましょう!
- BulletPrefab: 「Bullet」プレハブ
敵をクリックしても弾が出ない場合は「Player」をクリックすると判定されている可能性がありますので、次の対策を行うといいでしょう。
基本的にコンポーネントで定義したOnMouseDownAsButtonメソッドは最も画面手前にあるGameObjectのみに呼ばれるようなので注意しましょう。
- 「Player」プレハブのルートGameObjectの当たり判定を小さくする。
- 「Player」プレハブにあるコンポーネントにアタッチされているColliderを外す
敵にも体力を設定してみよう
弾を撃てるようになったので最後に弾が当たったら敵の体力を減るようにしましょう!
体力が0になったら、その敵を削除するようにします。
Enemyスクリプトに次の内容を追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//Enemyスクリプト // 次のフィールドを追加 public float Life = 10; // 次のメソッドを追加 private void OnTriggerEnter(Collider other) { if(other.gameObject.tag == "Bullet") { Life -= 10; Object.Destroy(other.gameObject); if(Life <= 0) { Object.Destroy(gameObject); } } } |
コンパイルに成功したら、「Enemy」プレハブに次の設定を追加してください。
- Rigidbodyコンポーネントをアタッチし、Is Kinematicにチェックする
- Use Gravityのチェックを外す
それでは再生してみて、敵を倒してみましょう
もし弾が当たらない場合は次の原因が考えられるので、それらを修正しましょう!
- 「Enemy」プレハブのサイズが小さすぎる
- 「Enemy」コンポーネントのLifeがまだ0以下になっていない
- 「Bullet」コンポーネントのSpeedの値が大きすぎる
- 「Bullet」プレハブのサイズが小さすぎる
- 「Bullet」プレハブのタグが「Bullet」ではない
- 「Bullet」プレハブにアタッチしているColliderのIs Triggerにチェックが入っていない
まとめ
今回の記事でシューティングゲームに必須の敵との戦闘システムを作りました。
今回特に多く出てきた処理としては当たり判定ですね。
- ColliderをTriggerにして当たり判定などのイベント的な処理を組み込む
この手法・手順には慣れ親しんでおくとよいでしょう。
また、講座ではゲームに要素を組み込む際に基本的に次の流れに沿っています。
- ゲームを完成させるための処理を考える
- 必要なGameObjectを作成し、それ用のコンポーネントを作成しアタッチする
- 実際の処理はスクリプトで制御する
取り分けてこう作るという決まりはないのですが、先にGameObjectの方を作り画面に見えるようにすると作りやすいかなと思いこうしています。
作り方によっては先にコンポーネントを作ってから、そのコンポーネントの内容に合うGameObjectを作成するなどの方法もありますが、その辺りは好みになります。
また、記事では必要最低限のGameObjectしか追加していませんが、読者の方は自由に作っていってOKです。ガンガン改造していきましょう。
またサンプルコードには数学関係のメソッドを使用しています。理論背景がわからない方には呪文のように思えますが、Unityではそういった方でも簡単に使用できるようにメソッドが提供されています。
もちろん、数学ができるとより自由にGameObjectを操ることができるので勉強するのもいいでしょう。
それでは次の記事に行ってみましょう。
次の記事:
参考用:スクリプトの完成図
ちなみに今回の記事で作成したスクリプトの完成図は次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class Player : MonoBehaviour { public Transform[] RoutePoints; [Range(0, 200)] public float Speed = 10f; [Range(0, 50)] public float MoveSpeed = 10f; public float MoveRange = 40f; public float _initialLife = 100; public float Life = 100; public Image LifeGage; public Bullet BulletPrefab; bool _isHitRoutePoint; IEnumerator Move() { var prevPointPos = transform.position; var basePosition = transform.position; var movedPos = Vector2.zero; foreach (var nextPoint in RoutePoints) { _isHitRoutePoint = false; //必ずfalseにする while (!_isHitRoutePoint) { //進行方向の計算 var vec = nextPoint.position - prevPointPos; vec.Normalize(); // プレイヤーの移動 basePosition += vec * Speed * Time.deltaTime; //上下左右に移動する処理 // 行列によるベクトルの変換に関係する知識を利用しています。 movedPos.x += Input.GetAxis("Horizontal") * MoveSpeed * Time.deltaTime; movedPos.y += Input.GetAxis("Vertical") * MoveSpeed * Time.deltaTime; movedPos = Vector2.ClampMagnitude(movedPos, MoveRange); var worldMovedPos = Matrix4x4.Rotate(transform.rotation).MultiplyVector(movedPos); //ルート上の位置に上下左右の移動量を加えている transform.position = basePosition + worldMovedPos; //次の処理は進行方向を向くように計算している transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(vec, Vector3.up), 0.5f); yield return null; } prevPointPos = nextPoint.position; } } private void OnTriggerEnter(Collider other) { if (other.gameObject.tag == "RoutePoint") { other.gameObject.SetActive(false); _isHitRoutePoint = true; } else if (other.gameObject.tag == "Enemy") { Life -= 10f; LifeGage.fillAmount = Life / _initialLife; other.gameObject.SetActive(false); Object.Destroy(other.gameObject); //当たった敵は削除する } } void Start() { StartCoroutine(Move()); } public void ShotBullet(Vector3 targetPos) { var bullet = Object.Instantiate(BulletPrefab, transform.position, Quaternion.identity); bullet.Init(transform.position, targetPos); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyArea : MonoBehaviour { public GameObject[] EnemyList; void Start() { foreach (var enemy in EnemyList) { enemy.SetActive(false); } } private void OnTriggerEnter(Collider other) { if (other.gameObject.name == "Player") { foreach (var enemy in EnemyList) { enemy.SetActive(true); } //一度敵を有効化したら、当たらないようにする var collider = GetComponent<Collider>(); collider.enabled = false; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy : MonoBehaviour { [Range(0, 100)] public float Speed = 10; public float DeadSecond = 10f; public float Life = 10; float _time; Player _player; void Start() { _time = 0f; _player = Object.FindObjectOfType<Player>(); } void Update() { _time += Time.deltaTime; if (_time >= DeadSecond) { Object.Destroy(gameObject); } else { var vec = _player.transform.position - transform.position; transform.position += vec.normalized * Speed * Time.deltaTime; } } private void OnMouseUpAsButton() { _player.ShotBullet(transform.position); } private void OnTriggerEnter(Collider other) { if (other.gameObject.tag == "Bullet") { Life -= 10; Object.Destroy(other.gameObject); if (Life <= 0) { Object.Destroy(gameObject); } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Bullet : MonoBehaviour { [Range(0, 10)] public float DeadSecond = 3f; [Range(0, 200)] public float Speed = 100f; public Vector3 StartPos { get; set; } public Vector3 TargetPos { get; set; } public void Init(Vector3 startPos, Vector3 targetPos) { StartPos = startPos; TargetPos = targetPos; StartCoroutine(Move()); } IEnumerator Move() { float time = 0f; transform.position = StartPos; var vec = (TargetPos - StartPos).normalized; while (time < DeadSecond) { time += Time.deltaTime; transform.position += vec * Speed * Time.deltaTime; yield return null; } Object.Destroy(gameObject); } } |
次の記事:
コメント
はじめまして。いつも参考にさせていただいております。
質問がありまして、コメントをさせていただいております。
OnMouseUpAsButton メソッドを利用した弾の生成と発射の処理なのですが、上手く反応しなかったため、プレイヤー側のすべてのコライダーを外して試したのですが、その場合でも、このメソッドは反応しませんでした。
そこで試しに、プレイヤーのゲームオブジェクトの前面(Z軸が手前になるように)にエネミーを配置した所、こちらは正常に OnMouseUpAsButton メソッドが実行されました。この時、プレイヤーのコライダーの有無は関係ありませんでした。(コライダーの有り無しの両方で試してみました)
結論としてなのですが、OnMouseUpAsButton メソッドはコライダーの有無に関係なく、あくまでもゲームオブジェクトのZ軸の座標で判定されてしまうのでしょうか。そうなると、想定している弾の生成処理にならないのですが、他に回避する方法がありますでしょうか。それとも、Enemyから ShotBullet メソッドを実行せず、Ray などを使ってプレイヤー側でEnemyの座標を取って弾を生成する、とかでしょうか。
お手数ではございますが、ご教示いただけますと幸いです。
よろしくお願いいたします。
先ほどコメントさせていただいたものです。
申し訳ございません。自己解決いたしました。
エネミーのゲームオブジェクトを階層構造で作成しており、親オブジェクトはフォルダ代わり、子オブジェクトにゲームオブジェクトの実体を置いて、子オブジェクトにコライダーがアタッチさせていたのが、OnMouseUpAsButton メソッドが動かない原因でした。
親オブジェクトにコライダーがアタッチしたら、正常に判定を行うようになりました。
ありがとうございました。