今回はキャラクターの移動先を予測して弾を発射する機能を作成してみたいと思います。
例えば物理的な弾をキャラクター目掛けて発射したとしてもキャラクターがずっと動いている状態だと前のキャラクターの位置に弾を飛ばすので弾は当たりません。
そこでキャラクターの次の移動先を予測し、弾がキャラクターになるべく当たるようにしようという試みです。
今回の機能を作成すると、
↑のような感じでキャラクターの移動先に弾が飛んでくるようになります。
ただ、常にキャラクターの移動先に速い弾が飛んでくるとキャラクターを操作するのにストレスがかかるかもしれません・・・・(^_^;)
状況に応じて変えた方がいいかもしれません。
キャラクターの移動先を予測して弾を飛ばす機能の作成
それでは操作キャラクターの移動先に弾を飛ばす機能を作成していきましょう。
キャラクターの移動先を予測する方法
今回の機能を作成する前にまずはどのようにキャラクターの移動先を予測し弾を当てるようにするかを考えてみます。
キャラクターの次の移動先はキャラクターの現在の位置、現在の向き、現在の移動速度がわかれば次の移動先はキャラクターの位置から現在の向きに現在の速度で移動したと仮定すればわかります。
ただし、その先に弾を発射しても弾がキャラクターに到達するまでには時間がかかります。
その為、キャラクターと弾の発射地点との距離を弾の速さで割って到達時間を計算し、その到達時間でどれだけキャラクターが移動するかも考慮する必要がありそうです。
またキャラクターの基点は足元になっている(3DCGでの設定によりますが)のでそこから少し上を到達点にする為のオフセット値を加味します。
これらの考えを元に計算式を作ると
キャラクターの移動予測値 = キャラクターの現在地 + オフセット値 + (キャラクターの前方 × キャラクターのXZ軸の速度 × 弾の到達時間) + (キャラクタの上方 × キャラクターのY軸の速度 × 弾の到達時間)
となります。
キャラクターの予測移動値の計算ではXZ軸とY軸とを別に計算しますが、これはキャラクターの向いている向きとキャラクターの上方の移動値をそれぞれ計算したい為です。
これでキャラクターの移動予測値の計算式が出来ました。
もっとちゃんとした計算が出来るのかもしれませんが、わたくしには無理のようです。((+_+))
キャラクターの操作スクリプト
キャラクターの移動先を予測する方法がわかったので、次はキャラクターの操作スクリプトを作成します。
キャラクターの設定やスクリプトに関しては
を参照してみてください。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class PredictionChara : MonoBehaviour { private CharacterController characterController; private Animator animator; private Vector3 velocity; [SerializeField] private float walkSpeed = 1.5f; // Use this for initialization void Start () { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); } // Update is called once per frame void Update () { if (characterController.isGrounded) { velocity = Vector3.zero; var input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); if(input.magnitude > 0f) { transform.LookAt(base.transform.position + input); velocity = transform.forward * walkSpeed; animator.SetFloat("Speed", input.magnitude); } else { animator.SetFloat("Speed", 0f); } } velocity.y += Physics.gravity.y * Time.deltaTime; characterController.Move(velocity * Time.deltaTime); } public Vector2 GetVelocityXZ() { return new Vector2(characterController.velocity.x, characterController.velocity.z); } public float GetVelocityY() { return characterController.velocity.y; } public Transform GetTransform() { return transform; } } |
基本的な移動処理以外にキャラクターのXZ軸のCharacterControllerの速度を返すGetVelocityXZメソッドとY軸の速度を返すGetVelocityYメソッド、キャラクターのTransformを返すGetTransformメソッドを作成しています。
CharacterControllerコンポーネントの参照をcharacterControllerに入れていて、そのvelocityでキャラクターの移動速度を取得出来ます。
キャラクターのゲームオブジェクトのインスペクタでTagにPlayerを設定しておきます。
弾のプレハブの作成
次に飛ばす弾のプレハブを作成していきます。
ヒエラルキー上で右クリック→3D Object→Sphereを選択し、名前をBulletとします。
Assetsフォルダ上で右クリックからCreate Materialを選択し、名前をBulletとし弾用のマテリアルを作成します。
Albedoの右のカラーマップで黒色にし、ヒエラルキー上のBulletゲームオブジェクトにドラッグ&ドロップし設定します。
今回は弾は物理的に当たらないようにする為Sphere ColliderのIs Triggerにチェックを入れます。
BulletゲームオブジェクトにはPredictionBulletスクリプトを作成し取り付けます。
フィールド宣言部とAwakeメソッド
フィールド宣言とAwakeメソッドの処理を作成します。
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 | using UnityEngine; using System.Collections; using UnityEngine.UI; public class PredictionBullet : MonoBehaviour { // 弾を消すまでの時間 [SerializeField] private float deleteTime = 10f; private Rigidbody rigid; // キャラクターの基点からのオフセット値 [SerializeField] private Vector3 offset = new Vector3(0f, 0.5f, 0f); // 弾がキャラクターに到達する時のキャラクターの移動予測値 private Vector3 predictionCharaPoint; [SerializeField] private float speed = 20f; // キャラクター操作スクリプト private PredictionChara predictionChara; // 弾を徐々に動かす時の次の移動値 private Vector3 nextPos; // 弾の飛ばす向き private Vector3 direction; void Awake() { // Rigidbodyを取得 rigid = GetComponent<Rigidbody>(); } |
コメントを付けてあるので説明はいらないと思います。
OnEnableメソッド
OnEnableメソッドはゲームオブジェクトがアクティブになった時に実行されるので、その時に初期化処理をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // 弾がアクティブになった時 void OnEnable() { // キャラクターの操作スクリプトを取得 predictionChara = GameObject.FindWithTag("Player").GetComponent<PredictionChara>(); // キャラクターの現在地と弾の出現位置の距離を弾のスピードで割って到達時間を計算 var arrivalTime = Vector3.Distance(predictionChara.GetTransform().position, transform.position) / speed; // キャラクターの横の移動予測値 var predictionPosXZ = predictionChara.GetTransform().forward * predictionChara.GetVelocityXZ().magnitude * arrivalTime; // キャラクターの縦の移動予測値 var predictionPosY = predictionChara.GetTransform().up * predictionChara.GetVelocityY() * arrivalTime; // キャラクターがそのまま移動した時に弾がキャラクターに当たる位置を計算 predictionCharaPoint = predictionChara.GetTransform().position + offset + predictionPosXZ + predictionPosY; // 弾の軌跡を表示 Debug.DrawLine(transform.position, predictionCharaPoint, Color.red, deleteTime); // 次のポイントの初期値に現在地を設定 nextPos = transform.position; // 弾の飛ばす向きを計算 direction = (predictionCharaPoint - nextPos).normalized; // 弾を発射してから指定した時間が経過したら自動で削除 Destroy(this.gameObject, deleteTime); } |
最初にキャラクターをPlayerタグで探してそこからキャラクター操作スクリプトであるPredictionCharaを取得します。
Vector3.Distanceを使ってキャラクターの位置と弾の現在の位置の距離を計算し、弾の速さで割って到達時間を求めます。
キャラクターの横方向の移動予測先はキャラクターの前方にXZ軸の速度(GetVelocityXZ())の長さ(magnitude)をかけてUpdate1回の移動値を計算し、それに弾の到達時間をかけることで弾が到達するまでのキャラクターの移動値を計算します。
Y軸の移動値はキャラクターの上方にY軸の速度と弾の到達時間をかけて求めます。
キャラクターの予測移動先(predictionCharaPoint)はキャラクターの現在位置に弾が到達するまでの時間でキャラクターが移動値を足したもので計算します。
キャラクターの予測移動先が計算出来たら、弾の現在地からキャラクターの予測移動先までの線を引いて弾の軌跡をシーンビューで確認出来るようにしておきます。
弾の移動は徐々に行う為、次の移動地であるnextPosには現在の弾の位置を設定しておきます。
キャラクターの予測移動先から現在の弾の位置を引いてキャラクターの予測移動先のベクトルを求め.normalizedで正規化して方向を求めます。
弾がずっと存在すると処理速度に影響するのでdeleteTimeで指定した時間が経過したら自動で削除されるようにしておきます。
弾の移動処理と衝突処理
次に弾の移動処理と衝突処理を作成します。
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 | private void Update() { // nextPos = Vector3.MoveTowards(transform.position, predictionCharaPoint, speed * Time.deltaTime); // 弾の次の移動先を計算 nextPos += direction * speed * Time.deltaTime; } private void FixedUpdate() { rigid.MovePosition(nextPos); } void OnCollisionEnter(Collision col) { // Enemyタグがついた敵に衝突したら自身と敵を削除 if (col.gameObject.tag == "Player") { Debug.Log("ヒット"); Destroy(gameObject); } } void OnTriggerEnter(Collider col) { // Enemyタグがついた敵に衝突したら自身と敵を削除 if (col.tag == "Player") { Debug.Log("ヒット"); Destroy(gameObject); } } |
Updateメソッドで
方向 × 速さ × Time.deltaTime
の処理を行い1秒間の移動値を計算しそれをnextPosに足していく事で弾を移動させます。
コメント化してある処理でも同じような処理が出来ますが、Vector3.MoveTowardsを使うと到達点に弾が到達するとその先にいかないのでこの処理はやめました。
Updateメソッドでは移動先を求めているだけで実際には移動させていません。
FixedUpdateメソッドでRigidbodyを使った弾の移動処理を行います。
Updateメソッドで求めた移動先にFixedUpdate内でRigidbodyのMovePositionメソッドを使って移動させます。
BulletゲームオブジェクトのSphere ColliderのIs Triggerにチェックを入れたので、弾は物理的に他のゲームオブジェクトと当たらない為OnTriggerEnterメソッドを使って他のゲームオブジェクトとの衝突を検知します。
Is Triggerのチェックを入れないで物理的に当てる場合はOnCollisionEnterを使ってください。
今回は両方記載しています。
両方ともPlayerタグを設定したキャラクターのコライダと接触した時にコンソールに「ヒット」と表示し、自身のゲームオブジェクトを削除しています。
実際に作成した弾は
↑のような大きさになりました。
BulletゲームオブジェクトをAssetsフォルダにドラッグ&ドロップしプレハブにし、ヒエラルキー上のBulletゲームオブジェクトを削除します。
弾をインスタンス化するスクリプトの作成
Bulletプレハブが出来たのでこのプレハブをインスタンス化するスクリプトを作成します。
Main CameraゲームオブジェクトにPredictionShotスクリプトを作成し取り付けます。
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 UnityEngine; using System.Collections; public class PredictionShot : MonoBehaviour { // 弾のプレハブ [SerializeField] private GameObject bullet; // レンズからのオフセット値 [SerializeField] private float offset; // 弾を飛ばす間隔時間 [SerializeField] private float waitTime = 0.1f; // 経過時間 private float elapsedTime = 0f; // Update is called once per frame void Update() { elapsedTime += Time.deltaTime; if (elapsedTime < waitTime) { return; } // カメラのレンズの中心を求める var centerOfLens = Camera.main.ViewportToWorldPoint(new Vector3(0.5f, 0.5f, Camera.main.nearClipPlane + offset)); // カメラのレンズの中心から弾を飛ばす Instantiate(bullet, centerOfLens, Quaternion.identity); elapsedTime = 0f; } } |
指定した時間が経過したらカメラの中心の位置に弾をインスタンス化します。
スクリプトの詳細に関しては
辺りを参照してください。
このスクリプトでは弾のプレハブをインスタンス化するだけで、弾を実際に移動させる処理はPredictionBulletスクリプトです。
これで機能が完成しました。
弾に重力を働かせたものに対応する
直線で弾を飛ばす場合の対応は出来ましたが重力を働かせた弾の場合はうまくいきません。
そこで、弾に重力を働かせたものにも対応していきます。
重力対応版を作る為の方法
まずはどのように重力を働かせた弾に対応するかを考えていきます。
今回は砲台(台は作りませんが・・・・)を作り、発射口から弾を飛ばすようにします。
その為、砲台をキャラクターの予測移動先の方に一旦向かせた後、弾が放物線を描いてキャラクターにぶつかるように砲台の筒を上向きに向かせるようにします。
砲台から飛び出した弾がキャラクターの予測移動先で丁度落下するように砲台を上向きにする必要があります。
筒をキャラクターの方向に向かせてから弾が放物線を描いてキャラクターにぶつかるように上向きに回転させますが、角度を計算する為に三角関数を使用します。
砲台の上向きの角度は三角関数を使って計算すると言いましたが、
↑のような直角三角形の場合は
Sinθ = b / c
Cosθ = a / c
Tanθ = b / a
となります。
しかしキャラクターは移動するので砲台とキャラクターの位置、キャラクターの上に弾の落下距離分足した位置で直角三角形を作ることが出来ません。
その為、3辺のそれぞれの長さを求めてから余弦定理を使って砲台の上向きの角度を求めることにします。
ここら辺の数学、物理の処理は
を参照してください。
辺の長さの求め方
a辺は「弾が出る位置からキャラクターの予測移動先との距離」
b辺は「キャラクターの予測移動先に弾が重力で落下する距離を上向きに足した位置とキャラクターの予測移動先の距離」
c辺は「砲台の位置とキャラクターの予測移動先に弾が重力で落下する距離を上向きに足した位置の距離」
で計算します。
b辺を求める時に必要な重力で落下する距離は
y = 1/2 × g × t²
で求めます。
gは重力加速度、tは時間です。
tは弾が落下する時間ですが、弾が落下する時間は弾がキャラクターに到達する時間と同じなので、a辺の長さを弾の速さで割って計算した時間を使用するようにします。
砲台を上に向け弾を発射する機能を実装する
砲台の向きを変えて重力が働いた弾をキャラクターの予測移動先に飛ばす機能を実装していきます。
重力を働かせない弾では弾自体に移動する機能を持たせていましたが今回は弾を打った時に力を加えるようにし、弾自体には衝突の処理だけさせるようにします。
弾のプレハブBulletの修正
まずは弾のプレハブであるBulletを選択し、インスペクタのRigidbodyのUse Gravityにチェックを入れます。
これで弾に重力が働くようになりました。
PredictionBulletスクリプトを取り外し、新しくProcessingBulletスクリプトを作り取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class ProcessingBullet : MonoBehaviour { void OnCollisionEnter(Collision col) { // Enemyタグがついた敵に衝突したら自身と敵を削除 if (col.gameObject.tag == "Player") { Debug.Log("ヒット"); Destroy(gameObject); } } void OnTriggerEnter(Collider col) { // Enemyタグがついた敵に衝突したら自身と敵を削除 if (col.tag == "Player") { Debug.Log("ヒット"); Destroy(gameObject); } } } |
これで弾自体では衝突判定をするだけになりました。
砲台の作成
次は砲台を作成します。
ヒエラルキー上で右クリックから3D Object→Cubeを選択し名前をButteryとします。
その子要素に空のゲームオブジェクトを作成し名前をBulletHoleとします。
BulletHoleを弾が出る位置に移動させZ軸(青色の軸)の向きが弾が飛んでいく方向になるようにしておきます。
ButteryにはShotBulletスクリプトを作成し取り付けます。
ShotBulletスクリプトの作成
ShotBulletスクリプトに先ほど考えた処理を書いていきます。
フィールド宣言部
まずはフィールド宣言部分です。
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 | using UnityEngine; using System.Collections; public class ShotBullet : MonoBehaviour { // 弾のプレハブ [SerializeField] private GameObject bullet; // 弾を飛ばす間隔時間 [SerializeField] private float waitTime = 0.1f; // 経過時間 private float elapsedTime = 0f; // 砲台の弾が出てくる場所 private Transform bulletHoleTra; // 弾を飛ばす力 [SerializeField] private float power = 10f; // 弾を消すまでの時間 [SerializeField] private float deleteTime = 10f; // 弾を飛ばす位置のオフセット値 [SerializeField] private Vector3 offset = new Vector3(0f, 1f, 0f); // キャラクター操作スクリプト private PredictionChara predictionChara; |
PredictionBulletに書いていたものをShotBulletに移しただけですね。
Start、Updateメソッド
次にStartメソッドとUpdateメソッドを見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | private void Start() { // 弾を飛ばす位置を取得 bulletHoleTra = transform.Find("BulletHole"); // キャラクターの操作スクリプトを取得 predictionChara = GameObject.FindWithTag("Player").GetComponent<PredictionChara>(); } // Update is called once per frame void Update() { elapsedTime += Time.deltaTime; if (elapsedTime < waitTime) { return; } Shot(); } |
StartメソッドではBulletHoleゲームオブジェクトのTransformを取得し、キャラクターの操作スクリプトの取得をしています。
Updateメソッドでは指定した時間が経過したらShotメソッドを呼び出して弾を飛ばします。
Shotメソッド
Shotメソッドが実際に弾を飛ばしている処理です。
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 | void Shot() { // キャラクターの位置+offset var charaPoint = predictionChara.GetTransform().position + offset; // 弾の到達時間を計算 var arrivalTime = Vector3.Distance(charaPoint, bulletHoleTra.position) / power; // キャラクターの横の移動予測値 var predictionPosXZ = predictionChara.GetTransform().forward * predictionChara.GetVelocityXZ().magnitude * arrivalTime; // キャラクターの縦の移動予測値 var predictionPosY = predictionChara.GetTransform().up * predictionChara.GetVelocityY() * arrivalTime; // キャラクターがそのまま移動した時に弾がキャラクターに当たる位置を計算 var predictionCharaPoint = charaPoint + predictionPosXZ + predictionPosY; // 砲台の向きを一旦キャラクターに向ける transform.LookAt(predictionCharaPoint); // 砲台とキャラクターの予測移動先の長さ var adjacent = Vector3.Distance(bulletHoleTra.position, predictionCharaPoint); // 落下距離を計算 var fallingDistance = 0.5f * Physics.gravity.y * Mathf.Pow(arrivalTime, 2); // キャラクターの予測移動先の上の位置 var upPoint = predictionCharaPoint + Vector3.up * fallingDistance; // キャラクターの予測移動先の上の位置とキャラクターの予測移動先の長さ var opposite = Vector3.Distance(upPoint, predictionCharaPoint); // 斜辺の長さ var hypotenuse = Vector3.Distance(bulletHoleTra.position, upPoint); // 上向きに向ける角度 var theta = -Mathf.Acos((Mathf.Pow(hypotenuse, 2) + Mathf.Pow(adjacent, 2) - Mathf.Pow(opposite, 2)) / (2 * hypotenuse * adjacent)); // 弾の落下に合わせて上向きにする transform.Rotate(Vector3.right, theta * Mathf.Rad2Deg, Space.Self); // 弾をインスタンス化 var bulletIns = Instantiate(bullet, bulletHoleTra.position, Quaternion.identity); var rigid = bulletIns.GetComponent<Rigidbody>(); // 弾のRigidbodyを使って砲台が向いている方向に力を加える rigid.AddForce(bulletHoleTra.forward * power, ForceMode.Impulse); // 弾を発射してから指定した時間が経過したら自動で削除 Destroy(bulletIns, deleteTime); // 弾の軌跡を表示 Debug.DrawLine(bulletHoleTra.position, predictionCharaPoint, Color.red, deleteTime); } |
charaPointは現在のキャラクターの位置+オフセット位置です。
arrivalTimeはキャラクターの位置とBulletHoleの位置の距離をpowerで割って弾の到達時間を計算したものを入れます。
キャラクターの予測移動先の計算は依然と同じです。
キャラクターの予測移動先がわかったら砲台をキャラクターの予測移動先に向けます。
fallingDistanceは重力での落下距離を計算しています。
Mathf.Powを使って累乗を計算していますが、
arrivalTime * arrivalTime
をしているだけです。
3辺の計算は先ほど書いたとおりの式をスクリプトにしただけです。
これで角度が計算出来たので、transform.Rotateを使ってVector3.right(X軸)を軸としてSpace.Self(ローカル軸)で計算した角度分を上向きに回転させます。
求めたtheta(角度)にMathf.Rad2Degをかけていますが、Mathf.Atan2で求めた角度はラジアンで得られるのでそれを度数に変換する為にMathf.Rad2Deg値をかけています。
弾のプレハブをインスタンス化したら弾に取り付けているRigidbodyコンポーネントを取得し、BulletHoleの前方に力を加えて飛ばします。
フォースモードはForceMode.Impulseを指定します。
ForceMode.Impulseは即座にRigidbodyの質量を考慮して力を加えるモードで力の単位は
質量 × 距離 / 時間
今回は質量は無視してますがRigid.massで質量が得られるのでこれをかける必要があります。
距離 / 時間 = 速さ
なので
弾の出る場所の向き × power
で速さをAddForceの第1引数に与えています。
向きにpowerの速さをかけることで速度を求めています。
これで機能が完成しました。
重力を働かせた弾の予測移動先への移動の確認
それでは確認してみましょう。
キャラクターにはスタンダードアセットのFollowTargetスクリプトを取り付けてキャラクターに弾が飛んでいく様子がわかりやすいようにします。
↑のようになりました。
弾の移動速度は決まっているので、キャラクターが遠くに移動すると砲台はどんどん上向きに回転しますが弾は届きません。
キャラクターに弾が届かなくなったら弾を飛ばすのをやめるようにすると処理速度が改善されますね。
ForceMode.Impulseは質量を考慮するので、弾のRigidbodyのmassを大きくすると弾が飛ばなくなります。
おわりに
弾の速さが遅くキャラクターが遠くにいる場合はキャラクターの予測移動先に弾が到達するのが早まったり遅くなったりします。
これは単純にキャラクターの予測移動先の計算が間違っている可能性もありますが・・・・(^_^;)
キャラクターの前回の移動速度を元に弾の到達時間でどれだけ進むかを計算している為、確実にヒットするというわけにはいきません。
また弾が速い場合は確実にキャラクターに当たります。
これはこれで逃げても必ず当たってしまうのでゲームとしては成り立たないですね・・・・。
キャラクターの予測移動先に少しランダムな値を入れて調整してみるのもいいかもしれません。