今回はUnityで基本的な数学・物理学を使った処理を行ってみたいと思います。
わたくし自身は数学・物理学は得意ではないので理解を助ける為にUnityで実際に計算して試してみるというただのメモ書きになります。
距離、速さ、時間の計算と等加速度運動の式、三角関数を使ってみようと思います。
わたくしみたいな数学、物理が苦手な人の助けになる記事かも?しれません。
今回の記事を見て頂くと、
↑のような砲台から弾を発射し放物線上に移動して到達点に達するような機能を作成する事が出来ます。
距離、速さ、時間を使った計算
距離、速さ、時間を使った計算は小学生の時にやりましたね。
距離 = 速さ × 時間
という式です。
距離、速さ、時間は単なる数値(スカラー値)なので方向を持っていません。
2Dや3Dのゲームではゲームオブジェクトは二次元ベクトルと3次元ベクトルで位置を表示しますので、距離を計算する場合は2点間のベクトル(大きさと方向を持つ)の長さ(スカラー値)を計算する必要があります。
3次元ベクトルpos1とpos2があったとしたら、その距離を計算するには
1 2 3 | Vector3.Distance(pos1, pos2); |
とVector3.Distanceを使うと2点間の距離を計算出来ます。
個々の要素の引き算の2乗をして足して、全体の平方根を取っても距離が計算が出来ます。
1 2 3 | Mathf.Sqrt(Mathf.Pow(pos1.x - pos2.x, 2) + Mathf.Pow(pos1.y - pos2.y, 2) + Mathf.Pow(pos1.z - pos2.z, 2)) |
Mathf.Sqrtが平方根、Mathf.Powで累乗を計算出来ます。
試しにサンプルを作成してみます。
CalculateDistanceスクリプトを作成しMain Cameraに取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class CalculateDistance : MonoBehaviour { public Vector3 pos1; public Vector3 pos2; // Use this for initialization void Start () { Debug.Log("Vector3.Distanceで計算:" + Vector3.Distance(pos1, pos2)); Debug.Log("自力で計算:" + Mathf.Sqrt(Mathf.Pow(pos1.x - pos2.x, 2) + Mathf.Pow(pos1.y - pos2.y, 2) + Mathf.Pow(pos1.z - pos2.z, 2))); } } |
pos1とpos2はインスペクタで設定出来るので、インスペクタでpos2のXを2とします。
2だけX軸で移動した点なのでコンソールには2が表示されます。
インスペクタでpos1とpos2の位置を他の値に変えて確認してみてください。
弾を横に一定速度で飛ばす
2点間の距離が計算出来たので、ある物体(球)をpos1からpos2に移動させるスクリプトを組んでみたいと思います。
球は一定の速さでpos1からpos2に移動させるとし、あらかじめ速さを与えてpos2に到達した時の時間を求めてみます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class CalculateDistance : MonoBehaviour { public Vector3 pos1; public Vector3 pos2; // 移動させるゲームオブジェクト [SerializeField] private Transform bullet; // ゲームオブジェクトの速さ [SerializeField] private float speed = 10f; // 経過時間 private float elapsedTime = 0f; // 到達したかどうか private bool isArrive = false; private void Start() { bullet.position = pos1; } private void Update() { if(isArrive) { return; } bullet.position = new Vector3(bullet.position.x + speed * Time.deltaTime, bullet.position.y, bullet.position.z); if (bullet.position.x >= pos2.x) { Debug.Log(elapsedTime); isArrive = true; } elapsedTime += Time.deltaTime; } } |
速さを10としました。
Updateメソッドでゲームオブジェクトの位置を直接変更しています。
Unityでは1が1mで計算するとして速さが10だと1秒間で10m移動なので10(m/s)になります。
距離(100m) = 速さ(10m/s) × 時間(t)
で計算出来るので、pos1の位置を(0, 0, 0)としpos2を(100, 0, 0)にするとゲームオブジェクトがpos2に到達する時間は10秒程度になります。
Time.deltaTimeについて
位置を更新する際にspeedの値にTime.deltaTimeをかけたものを元のX軸の値に足しています。
これはspeedだけだとUpdateメソッドが呼ばれる度にspeedの値分X軸の値が変更されてしまうので、
距離 = 速さ × 時間
の式の通りに速さに時間をかける必要があります。
Updateメソッドは1秒間に何十回も呼ばれるので、1秒当たりの時間を計算する為にTime.deltaTimeをかけます。
Time.deltaTimeをUnityのスクリプトリファレンスで確認すると
最後のフレームを完了するのに要した時間(秒)
となっています。
つまり前回Updateメソッドが呼ばれてから今回のUpdateメソッドが呼ばれるまでの時間が求められます。
その為、
1回のUpdateでのX軸の移動距離 = speed × Time.deltaTime
になり、それを元のX軸の値に足すことでX軸の移動先の値になります。
ヒエラルキー上に右クリック→3D Object→Sphereを選択し、名前をBulletとしてSphere ColliderのIs Triggerのチェックを一応外しておきます。
CalculateDistanceスクリプトのインスペクタのbulletに設定します。
このように距離、速さ、時間の中の2点がわかればもう一つの値が求まりますね。
Rigidbodyを使って同じように飛ばす
自身で弾の位置を計算せずRigidbodyの機能を使って弾を飛ばし、先ほどと同じような速さで移動させたい事もあります。
そこでRigidbodyに力を加えて同じような速さで飛ぶようにスクリプトを組んでみます。
ヒエラルキー上にSphereを新しく作り名前をBullet2としてRigidbodyコンポーネントを取り付けます。
ShotBulletスクリプトを作り取り付けます。
先ほどはMain Cameraに取り付けたスクリプトでBulletゲームオブジェクトを操作していましたが、今度はBullet2ゲームオブジェクト自体にスクリプトを取り付け操作する事にします。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; namespace TrigonometricFunction { public class ShotBullet : MonoBehaviour { public Vector3 pos1 = new Vector3(0f, 2f, 0f); public Vector3 pos2 = new Vector3(100f, 2f, 0f); private Rigidbody rigid; [SerializeField] private float speed = 10f; // 経過時間 private float elapsedTime = 0f; // 到達したかどうか private bool isArrive = false; // Use this for initialization void Start () { transform.position = pos1; rigid = GetComponent<Rigidbody>(); rigid.AddForce((pos2 - pos1).normalized * speed, ForceMode.Impulse); } private void Update() { if (isArrive) { return; } if (transform.position.x >= pos2.x) { Debug.Log("弾2の到達時間:" + elapsedTime); isArrive = true; rigid.velocity = Vector3.zero; } elapsedTime += Time.deltaTime; } } } |
pos1から見たpos2の方向は
pos2 – pos1
で計算出来ます。
RigidbodyのAddForceメソッドを使ってBullet2オブジェクトに力を加えます。
力を加える時に力を加える方向のベクトルが必要ですが、pos2 – pos1は大きさを持ったベクトルとなっている為、方向のみのベクトルではありません。
そのまま速さをかけてしまうとちゃんとした計算が出来ません。
そこでpos2 – pos1で得られた方向のベクトルを.normalizedで正規化し大きさが1の方向のベクトルを計算し、それにspeedをかけます。
力を加えるモードはForceMode.Impulseで即座にゲームオブジェクトに力を加えていますが、これは単位が
質量 × 距離 / 時間
なのでRigidbodyのMassをデフォルト値の1としていた場合、距離 / 時間が加える力になります。
距離 / 時間は速さなのでこの速さをAddForceの第1引数に与えればいいことになります。
ですが3次元ベクトルの場合は
方向 × 速さ = 速度
を計算する必要があるので、先ほどの方向のベクトルを求めてそれに速さをかけ速度を求め、それをAddForceの第1引数に与える必要があります。
今回は弾の到達点に弾が到着するという機能なので、ForceMode.Impulseのように質量を考慮する必要がないので、
1 2 3 | bulletInsRigidbody.AddForce(bulletIns.transform.forward * hypotenuse, ForceMode.VelocityChange); |
のように質量をかけず、ForceMode.VelocityChangeを使用しても同じです。
自前で計算して移動させたものとRigidbodyのAddForceで移動させたものは
↑のような感じで同じような動きをします。
実際にBulletとBullet2の到達時間を比較するとだいたい同じ時間になると思います。
等加速度運動
次に等加速度運動の式を使った処理をやってみます。
重力を働かせる
距離、速さ、時間を使って横に一定速度でゲームオブジェクトを移動させる事が出来ました。
次はゲームオブジェクトに重力を働かせてみます。
重力に関しては
の記事で行っていますが、CharacterControllerのMoveメソッドの引数にはキャラクターの速度を指定する必要があるので、キャラクターのY軸の速度を
速度 = 重力加速度 × 時間
という式で求める為、
速度 = Physics.gravity.y × Time.deltatime
という式を当てはめます。
Physics.gravity.yはUnityメニューのEdit→Project Settings→Physicsで設定されている定数でY軸の数値で-9.81で定義されています。
なのでオブジェクトに自由に重力加速度を設定したい場合は定数を使わず
速度 = -5.5f * Time.deltatime
としても大丈夫です。
ゲームオブジェクトの落下速度はこれで計算出来ますが、Main Cameraに取り付けたCalculateDistanceスクリプトのように直接ゲームオブジェクトの位置を移動させる場合は移動した変位(距離)を計算したいところです。
そこで等加速度運動の式を使います。
距離 = 初速度 × 時間 + 1/2 × 加速度 × 時間²
となります。
自由落下すると仮定して初速度を0にすれば
1 2 3 | y = 0.5f * acceleration * Mathf.Pow(Time.deltaTime, 2); |
のようにスクリプトで書けます。
1/2を0.5fとしてかけているのは、プログラムでは割り算より掛け算の方が計算が早いみたいだからです。
CalculateDistanceスクリプトを少し改良しAcceleratingBulletスクリプトを作成しBulletに取り付けます(Main Cameraに取り付けたTrigonometrixFunctionTestスクリプトは外しておきます)。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; namespace TrigonometricFunction { public class AcceleratingBullet : MonoBehaviour { // スタート位置 public Vector3 pos1 = new Vector3(0f, 50f, 0f); // ゲームオブジェクトの速さ [SerializeField] private float speed = 10f; // 落下変位 private float gravityY; // 落下時間 private float totalTime = 0f; // Y軸の元の位置 private float basePosY; private void Start() { transform.position = pos1; basePosY = transform.position.y; } private void Update() { // 経過時間を計算 totalTime += Time.deltaTime; // 重力で落下する距離を計算 gravityY = 0.5f * Physics.gravity.y * Mathf.Pow(totalTime, 2); transform.position = new Vector3(transform.position.x + speed * Time.deltaTime, basePosY + gravityY, transform.position.z); } } } |
落下する距離は
距離 = 0.5f × 重力加速度 × 時間
なので落下している時間をUpdateが呼ばれる度に計算します。
重力で落下する距離を計算した時間を使って求めます。
gravityYは重力で落下した距離を計算したものなので、元々の位置にこの落下距離を足すことで移動先のY軸の値を計算します。
これで重力による落下の処理が出来ました。
Rigidbodyで重力を働かせる
Bullet2ゲームオブジェクトに取り付けたShotBulletスクリプトではRigidbodyのAddForceメソッドを使ってX軸に力を加えていました。
重力を働かせたい場合は先ほどのように自分で計算式を作ることも出来ますが、インスペクタのRigidbodyの設定を変えるだけで重力を働かせることが出来るのでそちらを使ってみます。
↑のようにBullet2ゲームオブジェクトのRigidbodyのUse Gravityにチェックを入れます。
ShotBulletスクリプトを取り外しAcceleratingBullet2スクリプトを作成し取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using System.Collections; using System.Collections.Generic; using UnityEngine; namespace TrigonometricFunction { public class AccelerationgBullet2 : MonoBehaviour { // 初期位置 public Vector3 pos1 = new Vector3(0f, 50f, 0f); private Rigidbody rigid; [SerializeField] private float speed = 10f; // Use this for initialization void Start() { transform.position = pos1; rigid = GetComponent<Rigidbody>(); rigid.AddForce(Vector3.right * speed, ForceMode.Impulse); } } } |
力を加える方向はX軸の方向にしました。
ShotBulletスクリプトと同じように2点間からその移動先の方向を求めてそちらに力を加える方がゲームで使う時に便利ですが、今回はX軸に力を加えればいいだけなので書き換えました。
このスクリプトでは横に移動するだけですが、RigidbodyのUse Gravityにチェックを入れたのでそちらで重力の計算が勝手に行われます。
自身で計算した重力を働かせた物体とRigidbodyの設定で重力を働かせた物体の動きを確認するとほとんど同じ動きをするのがわかります。
Bullet2ゲームオブジェクトを少し横にずらして確認してみます。
↑のようになりました。
同じ位置からスタートさせるとほぼ同じ動きになっているのがよりわかりやすくなります。
三角関数を使う
次は三角関数を使ってみようと思います。
三角関数は辺と角度の関係を表した関数です。
sinθ = b / c
cosθ = a / c
tanθ = b / a
となります。
詳しい事は数学の本やサイトをご覧ください。
弾を放物線上に飛ばす
ここまでで弾を横に等速度運動させ、重力を使って下に加速度運動をさせることが出来ました。
しかし弾を砲台から発射し上向きに弾を飛ばしてから自然に落下するような処理を作りたい時もあります。
UnityではRigidbodyのAddForceメソッドを使って砲台の向いている向きに力を加え放物線上に飛ばすということも出来ます。
ですが弾が狙いを定めた位置で丁度落下するように砲台の角度を設定したいこともあります。
そういった時は三角関数を用いて砲台をどの程度上向きにするかという計算をする必要があります。
直角三角形で計算出来る位置に到達点がある
砲台から弾を飛ばす場合に砲台と飛ばす先のY軸の値が同じであれば直角三角形を用いて砲台の角度をどれだけ上向きにすればいいかがわかります。
まずは砲台をCubeを使って作成します。
ヒエラルキー上にCubeを作成し名前をButteryにし、Transformの値を変更します。
Butteryの子要素に空のゲームオブジェクトを作成し名前をBulletHoleとし、位置をButteryのローカル軸の青の方向の先に移動させます。
BulletHoleは↑のように弾を発射する位置に移動させます。
自分で式を計算して弾を飛ばす
砲台の角度を計算し、自分で弾の次の位置を計算し到達点に飛ばす機能を作成してみます。
新しいスクリプトTrigonometricFunction1スクリプトを作成し、Butteryに取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class TrigonometricFunction1 : MonoBehaviour { // 弾の到達点 [SerializeField] private Transform arrivalPoint; // 弾の横軸の速さ [SerializeField] private float speed = 10f; // 弾を消すまでの時間 [SerializeField] private float deleteTime = 10f; // 弾が移動している時間 private float totalTime; // 弾のプレハブ [SerializeField] private GameObject bullet; // 弾のインスタンス private GameObject bulletIns; // 弾を飛ばす場所 private Transform bulletHole; // 弾の到達時間 private float arrivalTime; // 弾の角度 private float theta; // 弾の斜辺の長さ private float hypotenuse; // 重力で移動する距離 private float gravityY; private void Start() { bulletHole = transform.Find("BulletHole"); Shot(); } private void Update() { if (bulletIns != null) { // 経過時間計測 totalTime += Time.deltaTime; // 重力で移動する距離を計算 gravityY = 0.5f * Physics.gravity.y * totalTime * totalTime; // 弾の位置を斜線方向の距離+重力で移動する距離を足して求める bulletIns.transform.position = bulletIns.transform.forward * hypotenuse * totalTime + new Vector3(0f, gravityY, 0f); } } private void Shot() { // X軸の長さを計算 var x = Vector3.Distance(bulletHole.transform.position, arrivalPoint.position); // 到着時間の計算 arrivalTime = x / speed; // 落下距離を計算 var fallingDistance = 0.5f * Physics.gravity.y * arrivalTime * arrivalTime; // 角度を計算 theta = Mathf.Atan2(fallingDistance, x); // 一旦攻撃対象の方を見る transform.LookAt(arrivalPoint.position); // 砲台の向きを変える transform.Rotate(Vector3.right, theta * Mathf.Rad2Deg, Space.Self); // 弾のインスタンス化 bulletIns = Instantiate(bullet, bulletHole.position, bulletHole.rotation); // 時間が経ったら削除する Destroy(bulletIns, deleteTime); // 横軸のspeedから斜め方向の速さを計算 hypotenuse = speed / Mathf.Cos(theta); // 砲台から到達点に線を引く Debug.DrawLine(transform.position, arrivalPoint.position, Color.red, deleteTime); // 砲台から到達点の上(重力が働いていない時の到達場所)に線を引く Debug.DrawLine(transform.position, arrivalPoint.position - new Vector3(0f, fallingDistance, 0f), Color.yellow, deleteTime); // 砲台と到達点の上の半分の位置から到達点に線を引く Debug.DrawLine((arrivalPoint.position - new Vector3(0f, fallingDistance, 0f) - transform.position) / 2f, arrivalPoint.position, Color.gray, deleteTime); } } |
ゲームオブジェクトが登場した時にShotメソッドを呼び出します。
最初にShotメソッドを見ていきます。
弾の放射位置と到達点の距離を計算し変数xに入れます。
その距離xをspeedで割って弾が到達点に到達する時間を計算しarrivalTimeに入れます。
等加速度運動の式を使ってarrivalTimeで落下する距離を計算しfallingDistanceに入れます。
アークタンジェントのメソッドを使って落下距離とX軸の距離から角度thetaを計算します。
ここで砲台の向きを到達点の方向に向けます。
これは上向き以外の向きを到達点に向けさせるためです。
transform.Rotateを使って砲台を計算した角度分上向きにします。
弾のプレハブをインスタンス化します。
speedは弾がX軸を進む速さなので、斜めに進む速さを求めます。
斜辺 × cosθ = 底辺
なのでこれを使って斜辺の長さを求めます。
1 2 3 | hypotenuse = speed / Mathf.Cos(theta); |
となります。
次にUpdateメソッドを見ていきます。
Updateメソッドでは弾のインスタンスが存在していれば弾の位置を計算し移動させます。
経過時間はtotalTimeに足していきます。
弾に働く重力で移動する距離は自由落下運動の式にあてはめ移動値を求めます。
1 2 3 | gravityY = 0.5f * Physics.gravity.y * totalTime * totalTime; |
斜辺方向の距離は
1 2 3 | bulletIns.transform.forward * hypotenuse * totalTime |
で弾が向いている方向(弾のインスタンス化時にBulletHoleの向きに合わせた)に斜辺の速さをかけ、それに経過時間をかけることで移動先の位置が求められます。
弾の位置はこの位置に重力での落下距離を足した位置になるのでそれをbulletIns.transform.positionに入れます。
これで弾が到達点に放物線上に発射されます。
ヒエラルキー上にCubeを作成し名前をArrivalPointとしTransformのPositionのXに50を設定してみます。
TrigonometricFunction1のインスペクタにArrivalPointを設定します。
bulletには弾のプレハブを設定します。
それでは実行してみましょう。
↑のように到達点に向けて放物線上に弾が移動しています。
Rigidbodyで放物線上に弾を飛ばす
自分で弾の位置を計算する事が出来ましたが、Rigidbodyを使った処理でもやってみましょう。
TrigonometricFunction2スクリプトを作成し砲台(Buttery)に取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class TrigonometricFunction2 : MonoBehaviour { // 弾の到達点 [SerializeField] private Transform arrivalPoint; // 弾のX軸の速さ [SerializeField] private float speed = 10f; // 弾を消すまでの時間 [SerializeField] private float deleteTime = 10f; // 弾のプレハブ [SerializeField] private GameObject bullet; // 弾のインスタンス private GameObject bulletIns; // 弾を発射する場所 private Transform bulletHole; // 弾が到達点に達する時間 private float arrivalTime; // 砲台の角度 private float theta; // 経過時間 private float elapsedTime = 0f; // 弾を発射するまでの時間間隔 [SerializeField] private float durationTime = 0.5f; private void Start() { bulletHole = transform.Find("BulletHole"); } private void Update() { elapsedTime += Time.deltaTime; if (elapsedTime >= durationTime) { elapsedTime = 0f; Shot(); } } private void Shot() { // X軸の長さを計算 var x = Vector3.Distance(bulletHole.transform.position, arrivalPoint.position); Debug.Log("X軸の長さ:" + x); // 到着時間の計算 arrivalTime = x / speed; // 落下距離を計算 var fallingDistance = 0.5f * Physics.gravity.y * arrivalTime * arrivalTime; // 角度を計算 theta = Mathf.Atan2(fallingDistance, x); // 一旦攻撃対象の方を見る transform.LookAt(arrivalPoint.position); // 砲台の向きを変える transform.Rotate(Vector3.right, theta * Mathf.Rad2Deg, Space.Self); // 弾のインスタンス化 bulletIns = Instantiate(bullet, bulletHole.position, bulletHole.rotation); // 時間が経ったら削除する Destroy(bulletIns, deleteTime); // 横軸のspeedから斜め方向の力を計算 var hypotenuse = speed / Mathf.Cos(theta); var bulletInsRigidbody = bulletIns.GetComponent<Rigidbody>(); bulletInsRigidbody.AddForce(bulletInsRigidbody.mass * bulletIns.transform.forward * hypotenuse, ForceMode.Impulse); // 砲台から到達点に線を引く Debug.DrawLine(transform.position, arrivalPoint.position, Color.red, deleteTime); // 砲台から到達点の上(重力が働いていない時の到達場所)に線を引く Debug.DrawLine(transform.position, arrivalPoint.position - new Vector3(0f, fallingDistance, 0f), Color.yellow, deleteTime); // 砲台と到達点の上の半分の位置から到達点に線を引く Debug.DrawLine((arrivalPoint.position - new Vector3(0f, fallingDistance, 0f) - transform.position) / 2f, arrivalPoint.position, Color.gray, deleteTime); } } |
こちらは一定時間が経過したら弾をインスタンス化しどんどん飛ばすようにしています。
Shotメソッド内はTrigonometricFunction1スクリプトのものとほとんど同じですが、この中で弾のRigidbodyコンポーネントを取得しAddForceを使って力を加えています。
力を加える方向は弾の向きに斜辺の速さをかけたものです。
重力の計算は弾のプレハブに取り付けたRigidbodyコンポーネントのUse Gravityにチェックを入れて働かせます。
これでRigidbodyを使った処理が出来ました。
砲台と到達点で直角三角形にならない場合
さて、先ほど砲台と到達点と落下距離から直角三角形になる場合の処理が出来ました。
しかし到達点のY軸の値を砲台と違う値にした場合は弾がうまく到達点に達しません。
そこで余弦定理を使って直角三角形にならない場合でも弾がうまく飛ぶように処理を変更していきます。
下のような点A、B、C点とその角に対する辺をa、b、cとした時に
$$a^2=b^2+c^2-2bc\cos{A}$$となります。
これを余弦定理と呼びます。
cosAについて解くと
$$\cos{A}=\frac{b^2+c^2-a^2}{2bc}$$
↑のようになり、
角度を求めるにはアークコサインを計算すればいいので、
$$A = \arccos{\frac{b^2+c^2-a^2}{2bc}}$$
を計算します。
今回はこの余弦定理を使って砲台の角度を計算したいと思います。
自分で式を計算して弾の位置を計算する
まずは弾の位置を自分で計算するバージョンを作成していきます。
TrigonometricFunction3スクリプトを作成しButteryに取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class TrigonometricFunction3 : MonoBehaviour { // 弾の到達点 [SerializeField] private Transform arrivalPoint; // 弾の横軸の速さ [SerializeField] private float speed = 10f; // 弾を消すまでの時間 [SerializeField] private float deleteTime = 10f; // 弾が移動している時間 private float totalTime; // 弾のプレハブ [SerializeField] private GameObject bullet; // 弾のインスタンス private GameObject bulletIns; // 弾を飛ばす場所 private Transform bulletHole; // 弾の到達時間 private float arrivalTime; // 弾の角度 private float theta; // 弾の斜辺の長さ private float hypotenuse; // 斜辺の速さ private float hypotenuseSpeed; // 重力で移動する距離 private float gravityY; private void Start() { bulletHole = transform.Find("BulletHole"); Shot(); } private void Update() { if (bulletIns != null) { // 経過時間計測 totalTime += Time.deltaTime; // 重力で移動する距離を計算 gravityY = 0.5f * Physics.gravity.y * totalTime * totalTime; // 弾の位置を斜線方向の距離+重力で移動する距離を足して求める bulletIns.transform.position = bulletIns.transform.forward * hypotenuseSpeed * totalTime + new Vector3(0f, gravityY, 0f); } } private void Shot() { // 底辺の長さを計算 var adjacent = Vector3.Distance(transform.position, arrivalPoint.position); Debug.Log(adjacent); // 到着時間の計算 arrivalTime = adjacent / speed; // 落下距離を計算 var opposite = Mathf.Abs(0.5f * Physics.gravity.y * arrivalTime * arrivalTime); Debug.Log(opposite); // 到達点+上向きに落下距離 var upPos = arrivalPoint.position + Vector3.up * opposite; // 斜辺の長さを計算 var hypotenuse = Vector3.Distance(transform.position, upPos); Debug.Log(hypotenuse); // 角度を計算 theta = -Mathf.Acos((Mathf.Pow(hypotenuse, 2) + Mathf.Pow(adjacent, 2) - Mathf.Pow(opposite, 2)) / (2 * hypotenuse * adjacent)); // 一旦攻撃対象の方を見る transform.LookAt(arrivalPoint.position); // 砲台の向きを変える transform.Rotate(Vector3.right, theta * Mathf.Rad2Deg, Space.Self); // 弾のインスタンス化 bulletIns = Instantiate(bullet, bulletHole.position, bulletHole.rotation); // 時間が経ったら削除する Destroy(bulletIns, deleteTime); // 横軸のspeedから斜め方向の速さを計算 hypotenuseSpeed = hypotenuse / arrivalTime; Debug.Log(hypotenuseSpeed); // 砲台から到達点に線を引く Debug.DrawLine(transform.position, arrivalPoint.position, Color.red, deleteTime); // 砲台から到達点の上(重力が働いていない時の到達場所)に線を引く Debug.DrawLine(transform.position, arrivalPoint.position + new Vector3(0f, opposite, 0f), Color.yellow, deleteTime); // 砲台と到達点の上の半分の位置から到達点に線を引く Debug.DrawLine((arrivalPoint.position + new Vector3(0f, opposite, 0f) - transform.position) / 2f, arrivalPoint.position, Color.gray, deleteTime); } } |
Shotメソッドでやっていることは砲台の位置と到達点の距離でadjacentを求めます。
さらに、重力での落下距離を計算しoppositeを求め、到達点の上側に落下距離分を足した位置を求め、砲台の位置と到達点の上の位置との距離でhypotenuseを求めます。
これで3辺が求まりました。
3辺の長さが求められたので余弦定理でcosθを求め、そこからθの値を求めます。
あとは弾の位置を計算する時に必要な斜辺方向の速さを求める必要があるので、斜辺の長さ / 時間で速さを求めます。
それを行っているのが、
1 2 3 | hypotenuseSpeed = hypotenuse / arrivalTime; |
の部分です。
これで機能が出来上がりました。
サンプルはRigidbody版の方で試した方がわかりやすいのでRigidbody版を作ってから紹介します。
Rigidbodyで弾の動きを作成する
次はRigidbodyの機能で動かすバージョンを作成していきます。
TrigonometricFunction4スクリプトを作成しButteryに取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class TrigonometricFunction4 : MonoBehaviour { // 弾の到達点 [SerializeField] private Transform arrivalPoint; // 弾の横軸の速さ [SerializeField] private float speed = 10f; // 弾を消すまでの時間 [SerializeField] private float deleteTime = 10f; // 弾のプレハブ [SerializeField] private GameObject bullet; // 弾のインスタンス private GameObject bulletIns; // 弾を飛ばす場所 private Transform bulletHole; // 弾の到達時間 private float arrivalTime; // 弾の角度 private float theta; // 斜辺の速さ private float hypotenuseSpeed; // 経過時間 private float elapsedTime = 0f; // 弾を発射するまでの時間間隔 [SerializeField] private float durationTime = 0.5f; private void Start() { bulletHole = transform.Find("BulletHole"); } private void Update() { elapsedTime += Time.deltaTime; if (elapsedTime >= durationTime) { elapsedTime = 0f; Shot(); } } private void Shot() { // 底辺の長さを計算 var adjacent = Vector3.Distance(bulletHole.transform.position, arrivalPoint.position); Debug.Log(adjacent); // 到着時間の計算 arrivalTime = adjacent / speed; // 落下距離を計算 var opposite = Mathf.Abs(0.5f * Physics.gravity.y * arrivalTime * arrivalTime); Debug.Log(opposite); // 到達点+上向きに落下距離 var upPos = arrivalPoint.position + Vector3.up * opposite; // 斜辺の長さを計算 var hypotenuse = Vector3.Distance(bulletHole.transform.position, upPos); Debug.Log(hypotenuse); // 角度を計算 theta = -Mathf.Acos((Mathf.Pow(hypotenuse, 2) + Mathf.Pow(adjacent, 2) - Mathf.Pow(opposite, 2)) / (2 * hypotenuse * adjacent)); // 一旦攻撃対象の方を見る transform.LookAt(arrivalPoint.position); // 砲台の向きを変える transform.Rotate(Vector3.right, theta * Mathf.Rad2Deg, Space.Self); // 弾のインスタンス化 bulletIns = Instantiate(bullet, bulletHole.position, bulletHole.rotation); // 横軸のspeedから斜め方向の速さを計算 hypotenuseSpeed = hypotenuse / arrivalTime; // Rigidbodyに力を加える var bulletInsRigidbody = bulletIns.GetComponent<Rigidbody>(); bulletInsRigidbody.AddForce(bulletInsRigidbody.mass * bulletIns.transform.forward * hypotenuseSpeed, ForceMode.Impulse); // 時間が経ったら削除する Destroy(bulletIns, deleteTime); // 砲台から到達点に線を引く Debug.DrawLine(transform.position, arrivalPoint.position, Color.red, deleteTime); // 砲台から到達点の上(重力が働いていない時の到達場所)に線を引く Debug.DrawLine(transform.position, arrivalPoint.position + new Vector3(0f, opposite, 0f), Color.yellow, deleteTime); // 砲台と到達点の上の半分の位置から到達点に線を引く Debug.DrawLine((arrivalPoint.position + new Vector3(0f, opposite, 0f) - transform.position) / 2f, arrivalPoint.position, Color.gray, deleteTime); } } |
やっていることはTrigonometricFunction3スクリプトとほぼ同じでShotメソッド内で斜辺方向に力を加えているだけです。
インスペクタのbulletにRigidbodyのUse Gravityにチェックを入れた弾のプレハブを設定します。
実行すると、
↑のように到達点を移動させても弾が丁度当たるように飛んでいるのがわかります。
記事の最初に示したサンプルと同じです。
基本的な数学、物理を使った計算で弾を飛ばしてみましたがなかなか使えそうですね(^^)v