来週に再び、1週間ゲームジャムが開かれるみたいですねー。
次回は 05/22(Mon) 開催 | Unity 1週間ゲームジャム #unity1week https://t.co/XuECBWX89b
— unityroom (@uni_rm) 2017年5月1日
皆様お疲れ様でした!多くの方に好評いただけたようなので、しばらくは月1で開催していきたいと思います。
先週はUniteがあったり、今週にはビットサミットがあったりと最近はイベントが目白押しですね。
ただ、新しい技術や知識を習得するのも良いのですが、振り返ることも大事です。
前回の1週間ゲームジャムで組み込みたかったものがあったんですよ。
それを今回振り返っていこうと思います。
最終的には、学生時代のことを振り返るはめになるのですが……。
もっと学生時代に勉強しておけばよかったなぁー……。
この記事にはUnity5.6.1f1を使用しています。
- あの日を振り返る
- とりあえず公式を見るよ。見るだけだよ。
- sinやらcosやらは食べ物でしたっけ
- sinもcosも食べ物ではなかった
- 発射する角度を俺が決める
- 滞空時間を俺が決める
- 最大高度を俺が決める
- x方向の速度を俺が決める
- さいごに
あの日を振り返る
前回の1週間ゲームジャムでは油がはねるゲームを作りました。
このゲームには、油に見立てた球オブジェクトがフィールド上に放物線を描いて飛んでいく場面があります。
ただこの球オブジェクトの動きなのですが、Rigidbody.AddForce()
を使って、
適当な方向に、適当な力を、適当なタイミングで飛ばしています。
そのため、どこに着地するかを僕自身にも分かっていないのです。
フィールド全体に均等になるように、球オブジェクトが着地させるようにするのが理想です。
特にフィールド外まで飛んでしまうような事態は本来避けねばならないはずです。
そのためには、指定した場所にオブジェクトを飛ばすための力を計算し、Rigidbody.AddForce()
に渡してあげねばなりません。
……。
……。
ということはアレだろ。大層な計算式を用意せねばならないんだろ。
一般の人が高校数学や物理で習ったと思われる、公式と呼ばれるやつを駆使してさ!
ふっ、高校の授業をほとんど眠りこけ、留年ギリギリで卒業した俺には少々荷が重過ぎる内容だぜ。
ただいい機会なので、勉強してみましょう。
あの学生のころと違って、インターネットが盛んになった今なら多くの情報が取得できるはずだ!
俺はネットの力を信じてる!
とりあえず公式を見るよ。見るだけだよ。
スクリプトの基本型としてはこんな感じでしょうか。
TestController.cs
using UnityEngine; public class TestController : MonoBehaviour { [SerializeField] private Transform m_shootPoint = null; [SerializeField] private Transform m_target = null; [SerializeField] private GameObject m_shootObject = null; private void Update() { if( Input.GetMouseButtonDown( 0 ) && m_target != null ) { Shoot( m_target.position ); } } private void Shoot( Vector3 i_targetPosition ) { // Todo: 目標地点にいい感じで山なりを描いて飛んでいく力を計算して渡すのだ! Vector3 force = Vector3.zero; if( m_shootObject == null ) { throw new System.NullReferenceException( "m_shootObject" ); } if( m_shootPoint == null ) { throw new System.NullReferenceException( "m_shootPoint" ); } var obj = Instantiate<GameObject>( m_shootObject, m_shootPoint.position, Quaternion.identity ); var rigidbody = obj.AddComponent<Rigidbody>(); rigidbody.AddForce( force, ForceMode.Impulse ); } } // class TestController
このスクリプトに、計算式を書いていけばいいんですね。
ああ、気が重い……。
ちなみに、空気抵抗は考えませんよ。
考えるわけがないよね。
とりあえず公式を見てみましょうか。
えーと、こういう放物線を描く動くをする運動をなんていうんだっけ……。
んー、斜方投射かなぁ。
まずはwikiを開いてみます。
斜方投射 - Wikipedia
お前は何を言っているんだ!
これは何が書いてあるんだ!
早くも今書いているこの記事を、ヴェルタースオリジナルの美味しさについて語る記事に変更したいのですが、もう少し見ていきましょうか。
とりあえず、公式に書いてある変数を一つずつ確認していきましょう。
- : x方向の速さベクトル
- : y方向の速さベクトル
- : x座標
- : y座標
- : 最初の速さベクトル
- : 最初のy座標
- : 時間
- : 重力
- : 角度
- : 何か
- : 何か
あってんのかね、これ……。
cosやらsinやらよく分かってないんだけど。
一点気になることといえば、重力加速度かな。
今回のスクリプトで重力加速度は、Physics.gravity
を使うつもりなんだよね。
Physics.gravity.y
には-9.81
と負数付き数値が入っているもんだから、って書く必要はないんじゃないかしら。
そのため、上記の公式はこんな感じになる気がするよ。
いや、多分ね。多分そうだと思うよ……。
そして、今この変数群の中で既に数値として分かっているものはなんだろうか。
少なくとも全て現時点で分からないことは無いはずだ。
今回の目的は特定の場所から目標地点に山なりを描いて着地することだ。
ということは、目標地点である,は分かっているはずだ。
そして、重力加速度は固定値なのでも同じく分かっている。
発射位置が分かっているので、もOKだ。
- : x方向の速さベクトル
- : y方向の速さベクトル
- : x座標(確定)
- : y座標(確定)
- : 最初の速さベクトル
- : 最初のy座標(確定)
- : 時間
- : 重力(確定)
- : 角度
- : 何か
- : 何か
うーん、まだまだ分からんことが多いなぁ。
そもそもsingとcosって何だっけなぁ。
sinやらcosやらは食べ物でしたっけ
うーん、何だっけな。sin、cos。
うろ覚えで見たことあるようなないような。
あー、三角比かぁ。
確か三角形のSの筆記体を描くようにしてsin、Cの文字を書くようにcos、と教わった気がするよ。
あー、なるほどなるほど。
で、はなんでcos掛けとるのかがわからないんだけど。
何故がx方向の速さとなりえるというのか。
---ネット検索中---
まずこんな三角形を用意して見ました。
うん、各辺がカラフルですね。
この各辺を先ほどの斜方投射の式に置き換えてみましょう。
- 黄色の辺 = 速さベクトル =
- 赤色の辺 = x方向の速さベクトル =
- 青色の辺 = y方向の速さベクトル =
さて、ではを求める式は、こんな感じになるみたいですよ。
ああうん、辛うじて記憶があるね。
では、赤色の辺の式にしてみましょう。両辺に黄色の辺を掛けて、
赤色の辺 = 黄色の辺 ×
そして、赤色の辺をに、黄色の辺をに置き換えると、
あっ、例の公式の計算式になりましたよ。
もう一ついきます。を求める式は、
青色の辺の式にしてみましょう。両辺に黄色の辺を掛けて、
青色の辺 = 黄色の辺 ×
そして、青色の辺をに、黄色の辺をに置き換えると、
公式に近づいてきました。
ただ、y方向には毎時重力が掛かっているはずだ。
ということは、この式にが加えられ、
これで公式どおりだ。
以下のページを参考にしましたー。
sinもcosも食べ物ではなかった
との存在が分かりました。
もう一度、公式で使っている変数を見直します。
- : x方向の速さベクトル(v_0から計算で求める)
- : y方向の速さベクトル(v_0から計算で求める)
- : x座標(確定)
- : y座標(確定)
- : 最初の速さベクトル
- : 最初のy座標(確定)
- : 時間
- : 重力(確定)
- : 角度
あと不明なものは, , の三つですねー。
最終的に、とが分かれば、Rigidbody.AddForce()
に渡す力を算出することが出来ると思うのですが。
ここからは、どれかしらの値を決めてしまうことで、残り変数の数値を計算で求めてみましょう。
発射する角度を俺が決める
まず、角度をこちらで決める場合について調べていきましょう。
角度を決めるということは、の数値は確定しそうです。
ということは、あとはを求めてしまえばいいわけですね。
おっ、なんかいけそうだぞ!
となると、不明なが式としては邪魔だなぁ。
どれかの公式をの式にして、それを他の公式に代入して消してしまおう。
をの式にしよう。
この式をに代入してみる。
あぁ……、恐れていた複雑な式になってしまった。
ちゃんと式を書けているかが不安だ。
もう少し整理したいなぁ。
三角比のを使うことでもう少し整頓できるみたいだ。
sin・cos ■わかりやすい高校物理の部屋■
はと同様に、辺による公式で求めることができる。
それとは別にこのような公式があるそうだ。
この辺は詳しくは追わないけど、なんとなーく理解できた。
これを先ほどの式に使ってみようかな。
ちょ、ちょっとは見やすくなったかなぁ。
ではこれをの式にしてみよう。
式的にはこれで完了だろうか。
こ、これをスクリプトに記述するわけか。
わー……、面倒くさーい。
TestController.cs
private void Shoot( Vector3 i_targetPosition ) { // とりあえず適当に60度でかっ飛ばすとするよ! ShootFixedAngle( i_targetPosition, 60.0f ); } private void ShootFixedAngle( Vector3 i_targetPosition, float i_angle ) { float speedVec = ComputeVectorFromAngle( i_targetPosition, i_angle ); if( speedVec <= 0.0f ) { // その位置に着地させることは不可能のようだ! Debug.LogWarning( "!!" ); return; } Vector3 vec = ConvertVectorToVector3( speedVec, i_angle, i_targetPosition ); InstantiateShootObject( vec ); } private float ComputeVectorFromAngle( Vector3 i_targetPosition, float i_angle ) { // xz平面の距離を計算。 Vector2 startPos = new Vector2( m_shootPoint.transform.position.x, m_shootPoint.transform.position.z ); Vector2 targetPos = new Vector2( i_targetPosition.x, i_targetPosition.z ); float distance = Vector2.Distance( targetPos, startPos ); float x = distance; float g = Physics.gravity.y; float y0 = m_shootPoint.transform.position.y; float y = i_targetPosition.y; // Mathf.Cos()、Mathf.Tan()に渡す値の単位はラジアンだ。角度のまま渡してはいけないぞ! float rad = i_angle * Mathf.Deg2Rad; float cos = Mathf.Cos( rad ); float tan = Mathf.Tan( rad ); float v0Square = g * x * x / ( 2 * cos * cos * ( y - y0 - x * tan ) ); // 負数を平方根計算すると虚数になってしまう。 // 虚数はfloatでは表現できない。 // こういう場合はこれ以上の計算は打ち切ろう。 if( v0Square <= 0.0f ) { return 0.0f; } float v0 = Mathf.Sqrt( v0Square ); return v0; } private Vector3 ConvertVectorToVector3( float i_v0, float i_angle, Vector3 i_targetPosition ) { Vector3 startPos = m_shootPoint.transform.position; Vector3 targetPos = i_targetPosition; startPos.y = 0.0f; targetPos.y = 0.0f; Vector3 dir = ( targetPos - startPos ).normalized; Quaternion yawRot = Quaternion.FromToRotation( Vector3.right, dir ); Vector3 vec = i_v0 * Vector3.right; vec = yawRot * Quaternion.AngleAxis( i_angle, Vector3.forward ) * vec; return vec; } private void InstantiateShootObject( Vector3 i_shootVector ) { if( m_shootObject == null ) { throw new System.NullReferenceException( "m_shootObject" ); } if( m_shootPoint == null ) { throw new System.NullReferenceException( "m_shootPoint" ); } var obj = Instantiate<GameObject>( m_shootObject, m_shootPoint.position, Quaternion.identity ); var rigidbody = obj.AddComponent<Rigidbody>(); // 速さベクトルのままAddForce()を渡してはいけないぞ。力(速さ×重さ)に変換するんだ Vector3 force = i_shootVector * rigidbody.mass; rigidbody.AddForce( force, ForceMode.Impulse ); }
やったー、いい感じに動いてるー。
苦労した甲斐があったぜ。
発射位置と目標位置の高さの変化にも対応してますぜ。
ちなみに、すんなりスクリプトが書けたように進めていますが、本当はミスしまくりながらトライアンドエラーを繰り返しました。
まず、コメントにも書いているとおり、Mathf.Cos()
やMathf.Tan()
に角度をそのまま渡してしまったんですね。
ドキュメントにも書いてありますが、渡すのはラジアンなのですよ。
ラジアンに変換する必要があったんです。
ラジアンは漢字の元ににているπを使う円の表現法でしたね確か。3.14のやつですか。
そして、Rigidbody.AddForce()
に速度ベクトルをそのまま渡してしまった。
Force()言うだけあって、力を渡す必要があったのです。
力はこんな感じに求めました。
力 = 速さ × 重さ
まあ最初はRigidbody
のmass
が常に1だったので、特に問題なく動いていたのですが、mass
を変えた途端に挙動がおかしくなったからなあ。
ちなみに角度を指定して目標地点に向かって飛ばす方法の場合には、以下のような欠点があります。
- 90度未満でないと平面方向に進めないので、指定する角度は0度以上90度未満でなければならない。
- ただし、真上に飛ばさないといけない場合は、上記の角度指定では目的地に到達できない。
指定した角度では目標地点に到達できない場合がちらほら。
90度未満指定なので、発射地点と目標地点が同じ場合は、絶対にたどり着けないんですよねー。
その場合は、どこかで変な数値なって、非数になり、最終的にエラーになってしまいます。
いくつかif()
で対応してはいますけど。
とりあえず、やった!飛んだ!
滞空時間を俺が決める
次は、滞空している時間を指定する場合です。
この場合は、との両方を求めねばならないので面倒くさそうです。
まずはを求めてみましょう。
うーん、どうやって求めたものか……。
ここはピタゴラスイッチでおなじみの三平方の定理を使って見ましょう。
【三平方の定理】 特別な直角三角形の3辺の比|中学生からの勉強質問(数学)|進研ゼミ中学講座
あったあった。
なんか特定の数値を語呂合わせで覚えるようなのもなかったけな。
まず、また例の三角形を見てみます。
- 黄色の辺 = 速さベクトル =
- 赤色の辺 = x方向の速さベクトル =
- 青色の辺 = y方向の速さベクトル =
三平方の定理によると、各辺の長さから以下の文が作れます。
すなわち、とが分ければが分かるわけだな!
まずは、から求めよう。
この式をを代入します。
そして両辺をで割って、の式に。
これではOK。
次はだ。
この式をの式にする。
この式をに代入。
この式をの式にする。
よし、とが分かったぞ!
これでこの式が解けるはずだ。
に代入しよう。
あとはが分かればいけるはずだ。
が分かったので、それを使ってもいいけど、とが分かっているのでそちらから求めてみます。
の式から、
先ほど算出したとの式を代入して、
随分複雑になってしまったけど、スクリプトに正しく書けるだろうか。
とりあえず、やってみよう。
TestController.cs
private void Shoot( Vector3 i_targetPosition ) { // とりあえず適当に2秒ぐらいで到着するようにするよ! ShootFixedTime( i_targetPosition, 2.0f ); } private void ShootFixedTime( Vector3 i_targetPosition, float i_time ) { float speedVec = ComputeVectorFromTime( i_targetPosition, i_time ); float angle = ComputeAngleFromTime( i_targetPosition, i_time ); if( speedVec <= 0.0f ) { // その位置に着地させることは不可能のようだ! Debug.LogWarning( "!!" ); return; } Vector3 vec = ConvertVectorToVector3( speedVec, angle, i_targetPosition ); InstantiateShootObject( vec ); } private float ComputeVectorFromTime( Vector3 i_targetPosition, float i_time ) { Vector2 vec = ComputeVectorXYFromTime( i_targetPosition, i_time ); float v_x = vec.x; float v_y = vec.y; float v0Square = v_x * v_x + v_y * v_y; // 負数を平方根計算すると虚数になってしまう。 // 虚数はfloatでは表現できない。 // こういう場合はこれ以上の計算は打ち切ろう。 if( v0Square <= 0.0f ) { return 0.0f; } float v0 = Mathf.Sqrt( v0Square ); return v0; } private float ComputeAngleFromTime( Vector3 i_targetPosition, float i_time ) { Vector2 vec = ComputeVectorXYFromTime( i_targetPosition, i_time ); float v_x = vec.x; float v_y = vec.y; float rad = Mathf.Atan2( v_y, v_x ); float angle = rad * Mathf.Rad2Deg; return angle; } private Vector2 ComputeVectorXYFromTime( Vector3 i_targetPosition, float i_time ) { // 瞬間移動はちょっと……。 if( i_time <= 0.0f ) { return Vector2.zero; } // xz平面の距離を計算。 Vector2 startPos = new Vector2( m_shootPoint.transform.position.x, m_shootPoint.transform.position.z ); Vector2 targetPos = new Vector2( i_targetPosition.x, i_targetPosition.z ); float distance = Vector2.Distance( targetPos, startPos ); float x = distance; // な、なぜ重力を反転せねばならないのだ... float g = -Physics.gravity.y; float y0 = m_shootPoint.transform.position.y; float y = i_targetPosition.y; float t = i_time; float v_x = x / t; float v_y = ( y - y0 ) / t + ( g * t ) / 2; return new Vector2( v_x, v_y ); }
やったー、上手に出来ましたー。
明らかに妙なコードの部分があるけどねー。
重力加速度を反転しなくてはいけない!?
// な、なぜ重力を反転せねばならないのだ...
float g = -Physics.gravity.y;
うん、そうなんだ。
なんか変なんだ。
g
変数に渡している値は-Physics.gravity.y
。
Physics.gravity.y
は最初から負数であるにもかかわらず、何故か負数を掛けている箇所が見えるはずだ。
そして、そうしないとこのスクリプトは正しく動作していないのだ!
ぐっ、何故だ。
確かにg
が負数の場合には、大抵の場合v_y
が負数になってしまう。
そうなると打ち出す角度も負数になってしまう。
打ち出す角度も負数、すなわち下向きに発射する場合は、すごい高いところから、短い時間で着地せねばならないときぐらいなものだ。
どこかで式を間違えたのだろうか。
なんということだ……。
最大高度を俺が決める
とりえあえず先ほどのスクリプトが妙なのは一旦おいといて、次に行きましょう次に。
今度は、放物線を描く最大の高さ(y座標)を指定する場合です。
一見、公式には当てはめるものが無さそうですが、できるのかな。
まず、放物線の最大の高さにあるということは、一瞬止まった状態。
すなわち、y方向の速度が重力によって0になったときだ。
式にするとこんな感じかな。
これをの式にすると、
この式をに代入する。
ただ今回のは、最高点の高さを表すので、に変更しよう。
この式をの式に変えて、最大高度にいるときの時間を求めよう。
これで、最大高度に到着した際の時間がわかったぞ。
全体の時間と混乱しないように、と名づけよう。
あとは最大高度から目的の高さまでに落下する時間を計算しよう。
が0になってから落下しているので、以下の計算式になるはずだ。
これも[の式に変えよう。
この最大高度から目標地点に到着するまでの時間をと名づけよう。
すなわち、全体で掛かる時間としては、
になるわけだ。
これで時間が算出できた。
時間が分かったのなら、あとは時間を決めうちしたときの計算式がそのまま使えるはずだ!
TestController.cs
private void Shoot( Vector3 i_targetPosition ) { // とりあえず適当に3mぐらいで高さまで飛ぶよ! ShootFixedHeight( i_targetPosition, 3.0f ); } private void ShootFixedHeight( Vector3 i_targetPosition, float i_height ) { float t1 = CalculateTimeFromStartToMaxHeight( i_targetPosition, i_height ); float t2 = CalculateTimeFromMaxHeightToEnd( i_targetPosition, i_height ); if( t1 <= 0.0f && t2 <= 0.0f ) { // その位置に着地させることは不可能のようだ! Debug.LogWarning( "!!" ); return; } float time = t1 + t2; ShootFixedTime( i_targetPosition, time ); } private float CalculateTimeFromStartToMaxHeight( Vector3 i_targetPosition, float i_height ) { float g = Physics.gravity.y; float y0 = m_shootPoint.transform.position.y; float timeSquare = 2 * ( y0 - i_height ) / g; if( timeSquare <= 0.0f ) { return 0.0f; } float time = Mathf.Sqrt( timeSquare ); return time; } private float CalculateTimeFromMaxHeightToEnd( Vector3 i_targetPosition, float i_height ) { float g = Physics.gravity.y; float y = i_targetPosition.y; float timeSquare = 2 * ( y - i_height ) / g; if( timeSquare <= 0.0f ) { return 0.0f; } float time = Mathf.Sqrt( timeSquare ); return time; }
いい感じです!
ただ、指定した高さより高い位置から発射したり、高い位置を目標にしたりするときの対応が必要になりますな。
x方向の速度を俺が決める
最後にはx方向の速度を指定する場合をやりましょう。
まあ、ゲーム的にはxz平面上の速度になるんだけど。
x方向の速度である、が分かってるということは、が決まっているということになるはずだ。
ということは、をの式に変えて、
これで時間が算出できた。
ということは、また時間を決めうちしたときの計算式が使えるわけだ。
TestController.cs
private void Shoot( Vector3 i_targetPosition ) { // とりあえず適当に3m/sぐらいで高さまで飛ぶよ! ShootFixedSpeedInPlaneDirection( i_targetPosition, 3.0f ); } private void ShootFixedSpeedInPlaneDirection( Vector3 i_targetPosition, float i_speed ) { if( i_speed <= 0.0f ) { // その位置に着地させることは不可能のようだ! Debug.LogWarning( "!!" ); return; } // xz平面の距離を計算。 Vector2 startPos = new Vector2( m_shootPoint.transform.position.x, m_shootPoint.transform.position.z ); Vector2 targetPos = new Vector2( i_targetPosition.x, i_targetPosition.z ); float distance = Vector2.Distance( targetPos, startPos ); float time = distance / i_speed; ShootFixedTime( i_targetPosition, time ); }
これはすごい簡単にできたなぁ。
さいごに
うーむ、結局時間から角度などを求めるときの、なぜか重力加速度の符号を反転せねば正常に動かない理由がわからなかった。
どこで計算式を間違えたというのだ。
もう眠いからこれ以上の考察は難しそうだ。
式の間違い等に気づかれましたら、是非ともご連絡を……。