うら干物書き

ゲームを作っています。

【Unity】Rigidbodyを使い、放物線を描き、目標の地点へ

 来週に再び、1週間ゲームジャムが開かれるみたいですねー。

 先週はUniteがあったり、今週にはビットサミットがあったりと最近はイベントが目白押しですね。

 ただ、新しい技術や知識を習得するのも良いのですが、振り返ることも大事です。
 前回の1週間ゲームジャムで組み込みたかったものがあったんですよ。
 それを今回振り返っていこうと思います。
 最終的には、学生時代のことを振り返るはめになるのですが……。
 もっと学生時代に勉強しておけばよかったなぁー……。

f:id:urahimono:20170517005839p:plain


この記事にはUnity5.6.1f1を使用しています。

あの日を振り返る

 前回の1週間ゲームジャムでは油がはねるゲームを作りました。
 このゲームには、油に見立てたオブジェクトがフィールド上に放物線を描いて飛んでいく場面があります。

f:id:urahimono:20170430230623p:plain

 ただこの球オブジェクトの動きなのですが、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

  • { \displaystyle v_x = v_0\cos\theta }
  • { \displaystyle v_y = -gt + v_0\sin\theta }
  • { \displaystyle x = v_0t\cos\theta }
  • { \displaystyle y = -\frac{1}{2}gt^{2} + v_0t\sin\theta + y_0 }

 お前は何を言っているんだ!
 これは何が書いてあるんだ!

 早くも今書いているこの記事を、ヴェルタースオリジナルの美味しさについて語る記事に変更したいのですが、もう少し見ていきましょうか。
 とりあえず、公式に書いてある変数を一つずつ確認していきましょう。

  • { v_x } : x方向の速さベクトル
  • { v_y } : y方向の速さベクトル
  • { x } : x座標
  • { y } : y座標
  • { v_0 } : 最初の速さベクトル
  • { y_0 } : 最初のy座標
  • { t } : 時間
  • { g } : 重力
  • { \theta } : 角度
  • { \cos } : 何か
  • { \sin } : 何か

 あってんのかね、これ……。
 cosやらsinやらよく分かってないんだけど。

 一点気になることといえば、重力加速度かな。
 今回のスクリプトで重力加速度は、Physics.gravityを使うつもりなんだよね。
 Physics.gravity.yには-9.81と負数付き数値が入っているもんだから、{ -g }って書く必要はないんじゃないかしら。
 そのため、上記の公式はこんな感じになる気がするよ。

  • { \displaystyle v_x = v_0\cos\theta }
  • { \displaystyle v_y = gt + v_0\sin\theta }
  • { \displaystyle x = v_0t\cos\theta }
  • { \displaystyle y = \frac{1}{2}gt^{2} + v_0t\sin\theta + y_0 }

 いや、多分ね。多分そうだと思うよ……。

 そして、今この変数群の中で既に数値として分かっているものはなんだろうか。
 少なくとも全て現時点で分からないことは無いはずだ。

 今回の目的は特定の場所から目標地点に山なりを描いて着地することだ。
 ということは、目標地点である{ x },{ y }は分かっているはずだ。
 そして、重力加速度は固定値なので{ g }も同じく分かっている。
 発射位置が分かっているので、{ y_0 }もOKだ。

  • { v_x } : x方向の速さベクトル
  • { v_y } : y方向の速さベクトル
  • { x } : x座標(確定)
  • { y } : y座標(確定)
  • { v_0 } : 最初の速さベクトル
  • { y_0 } : 最初のy座標(確定)
  • { t } : 時間
  • { g } : 重力(確定)
  • { \theta } : 角度
  • { \cos } : 何か
  • { \sin } : 何か

 うーん、まだまだ分からんことが多いなぁ。
 そもそもsingcosって何だっけなぁ。

sinやらcosやらは食べ物でしたっけ

 うーん、何だっけな。sincos
 うろ覚えで見たことあるようなないような。

sin・cos ■わかりやすい高校物理の部屋■

 あー、三角比かぁ。
 確か三角形のSの筆記体を描くようにしてsin、Cの文字を書くようにcos、と教わった気がするよ。
 あー、なるほどなるほど。

 で、{ \displaystyle v_x = v_0\cos\theta }はなんでcos掛けとるのかがわからないんだけど。
 何故{ v_0\cos\theta }がx方向の速さとなりえるというのか。

—ネット検索中—

 まずこんな三角形を用意して見ました。

f:id:urahimono:20170517005318p:plain

 うん、各辺がカラフルですね。
 この各辺を先ほどの斜方投射の式に置き換えてみましょう。

  • 黄色の辺 = 速さベクトル = { v_0 }
  • 赤色の辺 = x方向の速さベクトル = { v_x }
  • 青色の辺 = y方向の速さベクトル = { v_y }

 さて、では{ \cos\theta }を求める式は、こんな感じになるみたいですよ。

{ \displaystyle \cos\theta = \frac{赤色の辺}{黄色の辺} }

 ああうん、辛うじて記憶があるね。

 では、赤色の辺の式にしてみましょう。両辺に黄色の辺を掛けて、
 
赤色の辺 = 黄色の辺 × { \cos\theta }

 そして、赤色の辺{ v_x }に、黄色の辺{ v_0 }に置き換えると、
 
{ \displaystyle v_x = v_0\cos\theta }

 あっ、例の公式の計算式になりましたよ。

 もう一ついきます。{ \sin\theta }を求める式は、

{ \displaystyle \sin\theta = \frac{青色の辺}{黄色の辺} }

青色の辺の式にしてみましょう。両辺に黄色の辺を掛けて、

青色の辺 = 黄色の辺 × { \sin\theta } 

 そして、青色の辺{ v_y }に、黄色の辺{ v_0 }に置き換えると、
 
{ \displaystyle v_y = v_0\sin\theta }

 公式に近づいてきました。
 ただ、y方向には毎時重力が掛かっているはずだ。
 ということは、この式に{ gt }が加えられ、
 
{ \displaystyle v_y = gt + v_0\sin\theta }

 これで公式どおりだ。
 以下のページを参考にしましたー。

等加速度直線運動 ■わかりやすい高校物理の部屋■

sinもcosも食べ物ではなかった

 { \cos }{ \sin }の存在が分かりました。
 もう一度、公式で使っている変数を見直します。

  • { v_x } : x方向の速さベクトル(v_0から計算で求める)
  • { v_y } : y方向の速さベクトル(v_0から計算で求める)
  • { x } : x座標(確定)
  • { y } : y座標(確定)
  • { v_0 } : 最初の速さベクトル
  • { y_0 } : 最初のy座標(確定)
  • { t } : 時間
  • { g } : 重力(確定)
  • { \theta } : 角度

 あと不明なものは{ v_0 }, { t }, { \theta }の三つですねー。
 最終的に、{ v_0 }{ \theta }が分かれば、Rigidbody.AddForce()に渡す力を算出することが出来ると思うのですが。

 ここからは、どれかしらの値を決めてしまうことで、残り変数の数値を計算で求めてみましょう。

発射する角度を俺が決める

 まず、角度をこちらで決める場合について調べていきましょう。
 角度を決めるということは、{ \cos\theta }の数値は確定しそうです。

 ということは、あとは{ v_0 }を求めてしまえばいいわけですね。
 おっ、なんかいけそうだぞ!

 となると、不明な{ t }が式としては邪魔だなぁ。
 どれかの公式を{ t }の式にして、それを他の公式に代入して消してしまおう。

 { \displaystyle x = v_0t\cos\theta }{ t }の式にしよう。
 
{ \displaystyle t = \frac{x}{v_0\cos\theta} }

 この式を{ \displaystyle y = \frac{1}{2}gt^{2} + v_0t\sin\theta + y_0 }に代入してみる。

{ \displaystyle y = \frac{1}{2}g\frac{x^{2}}{v_0^{2}\cos\theta^{2}} + \frac{\sin\theta x}{\cos\theta} + y_0 }

 あぁ……、恐れていた複雑な式になってしまった。
 ちゃんと式を書けているかが不安だ。

 もう少し整理したいなぁ。
 三角比の{ \tan\theta }を使うことでもう少し整頓できるみたいだ。
sin・cos ■わかりやすい高校物理の部屋■

 { \tan\theta }{ \cos\theta }と同様に、辺による公式で求めることができる。
 それとは別にこのような公式があるそうだ。
 
{ \displaystyle \tan\theta = \frac{\sin\theta}{\cos\theta} }

 この辺は詳しくは追わないけど、なんとなーく理解できた。
 これを先ほどの式に使ってみようかな。

{ \displaystyle y = \frac{1}{2}g\frac{x^{2}}{v_0^{2}\cos\theta^{2}} + x\tan\theta + y_0 }

 ちょ、ちょっとは見やすくなったかなぁ。
 ではこれを{ v_0 }の式にしてみよう。

{ \displaystyle v_0 = \sqrt{\frac{ gx^{2} }{ 2\cos\theta^{2}(y - y_0 - x\tan\theta) } } }

 式的にはこれで完了だろうか。
 こ、これをスクリプトに記述するわけか。
 わー……、面倒くさーい。

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 );
}

f:id:urahimono:20170515222206g:plain

 やったー、いい感じに動いてるー。
 苦労した甲斐があったぜ。

 発射位置と目標位置の高さの変化にも対応してますぜ。

f:id:urahimono:20170515222931g:plain
f:id:urahimono:20170515223256g:plain

 ちなみに、すんなりスクリプトが書けたように進めていますが、本当はミスしまくりながらトライアンドエラーを繰り返しました。

 まず、コメントにも書いているとおり、Mathf.Cos()Mathf.Tan()に角度をそのまま渡してしまったんですね。
 ドキュメントにも書いてありますが、渡すのはラジアンなのですよ。
 ラジアンに変換する必要があったんです。
 ラジアンは漢字のににているπを使う円の表現法でしたね確か。3.14のやつですか。

docs.unity3d.com

 そして、Rigidbody.AddForce()に速度ベクトルをそのまま渡してしまった。
 Force()言うだけあって、を渡す必要があったのです。
 はこんな感じに求めました。
 
力 = 速さ × 重さ

 まあ最初はRigidbodymassが常に1だったので、特に問題なく動いていたのですが、massを変えた途端に挙動がおかしくなったからなあ。

 ちなみに角度を指定して目標地点に向かって飛ばす方法の場合には、以下のような欠点があります。
 
- 90度未満でないと平面方向に進めないので、指定する角度は0度以上90度未満でなければならない。
- ただし、真上に飛ばさないといけない場合は、上記の角度指定では目的地に到達できない。

 指定した角度では目標地点に到達できない場合がちらほら。
 90度未満指定なので、発射地点と目標地点が同じ場合は、絶対にたどり着けないんですよねー。
 その場合は、どこかで変な数値なって、非数になり、最終的にエラーになってしまいます。
 いくつかif()で対応してはいますけど。

 とりあえず、やった!飛んだ!

滞空時間を俺が決める

 次は、滞空している時間を指定する場合です。
 この場合は、{ v_0 }{ \theta }の両方を求めねばならないので面倒くさそうです。

 まずは{ v_0 }を求めてみましょう。
 うーん、どうやって求めたものか……。
 ここはピタゴラスイッチでおなじみの三平方の定理を使って見ましょう。

【三平方の定理】 特別な直角三角形の3辺の比|中学生からの質問(数学)|進研ゼミ中学講座|ベネッセコーポレーション

 あったあった。
 なんか特定の数値を語呂合わせで覚えるようなのもなかったけな。

 まず、また例の三角形を見てみます。

f:id:urahimono:20170517005318p:plain

  • 黄色の辺 = 速さベクトル = { v_0 }
  • 赤色の辺 = x方向の速さベクトル = { v_x }
  • 青色の辺 = y方向の速さベクトル = { v_y }

 三平方の定理によると、各辺の長さから以下の文が作れます。
{ \displaystyle v_0 = \sqrt{ v_x^{2} + v_y^{2} } }

 すなわち、{ v_x }{ v_y }が分ければ{ v_0 }が分かるわけだな!
 
 まずは、{ v_x }から求めよう。

{ \displaystyle x = v_0t\cos\theta }

 この式を{ \displaystyle v_x = v_0\cos\theta }を代入します。
 
{ \displaystyle x = v_x t }

 そして両辺を{ t }で割って、{ v_x }の式に。
 
{ \displaystyle v_x = \frac{x}{t} }

 これで{ v_x }はOK。

 次は{ v_y }だ。

{ \displaystyle v_y = gt + v_0\sin\theta }

 この式を{ v_0\sin\theta }の式にする。

{ \displaystyle v_0\sin\theta = v_y - gt }

 この式を{ \displaystyle y = \frac{1}{2}gt^{2} + v_0t\sin\theta + y_0 }に代入。

{ \displaystyle y = \frac{1}{2}gt^{2} + (v_y - gt )t + y_0 }

 この式を{ v_y }の式にする。
{ \displaystyle v_y = \frac{ y - y_0 }{t} + \frac{gt}{2} }

 よし、{ v_x }{ v_y }が分かったぞ!

 これでこの式が解けるはずだ。

{ \displaystyle v_0 = \sqrt{ v_x^{2} + v_y^{2} } }に代入しよう。
{ \displaystyle v_0 = \sqrt{ (\frac{x}{t})^{2} + (\frac{ y - y_0 }{t} + \frac{gt}{2})^{2} } }

 あとは{ \theta }が分かればいけるはずだ。
 
 { v_0 }が分かったので、それを使ってもいいけど、{ v_x }{ v_y }が分かっているのでそちらから求めてみます。

 { \tan\theta }の式から、
 
{ \displaystyle \tan\theta = \frac{vy}{vx} }
{ \displaystyle \theta = \tan^{-1}\frac{vy}{vx} }

 先ほど算出した{ v_x }{ v_y }の式を代入して、
{ \displaystyle \theta = \tan^{-1} (\frac{ y - y_0 }{t} + \frac{gt}{2}) \frac{t}{x} }
{ \displaystyle \theta = \tan^{-1} \frac{ 2(y - y_0) + gt^{2} }{2x} }

 随分複雑になってしまったけど、スクリプトに正しく書けるだろうか。
 とりあえず、やってみよう。

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 );
}

f:id:urahimono:20170517000712g:plain

 やったー、上手に出来ましたー。
 明らかに妙なコードの部分があるけどねー。

重力加速度を反転しなくてはいけない!?

// な、なぜ重力を反転せねばならないのだ…
float g = -Physics.gravity.y;

 うん、そうなんだ。
 なんか変なんだ。
 
 g変数に渡している値は-Physics.gravity.y
 Physics.gravity.yは最初から負数であるにもかかわらず、何故か負数を掛けている箇所が見えるはずだ。
 そして、そうしないとこのスクリプトは正しく動作していないのだ!

 ぐっ、何故だ。
 確かにgが負数の場合には、大抵の場合v_yが負数になってしまう。
 そうなると打ち出す角度も負数になってしまう。

 打ち出す角度も負数、すなわち下向きに発射する場合は、すごい高いところから、短い時間で着地せねばならないときぐらいなものだ。
 どこかで式を間違えたのだろうか。
 なんということだ……。

最大高度を俺が決める

 とりえあえず先ほどのスクリプトが妙なのは一旦おいといて、次に行きましょう次に。

 今度は、放物線を描く最大の高さ(y座標)を指定する場合です。
 一見、公式には当てはめるものが無さそうですが、できるのかな。

 まず、放物線の最大の高さにあるということは、一瞬止まった状態。
 すなわち、y方向の速度が重力によって0になったときだ。
 式にするとこんな感じかな。

{ \displaystyle 0 = gt + v_0\sin\theta }

 これを{ v_0\sin\theta }の式にすると、

{ \displaystyle v_0\sin\theta = -gt }

 この式を{ \displaystyle y = \frac{1}{2}gt^{2} + v_0t\sin\theta + y_0 }に代入する。
 ただ今回の{ y }は、最高点の高さを表すので、{ h }に変更しよう。

{ \displaystyle h = \frac{1}{2}gt^{2} - gt^{2} + y_0 }

 この式を{ t }の式に変えて、最大高度にいるときの時間を求めよう。

{ \displaystyle t = \sqrt{ \frac{2(y_0 - h)}{g} } }

 これで、最大高度に到着した際の時間がわかったぞ。
 全体の時間と混乱しないように、{ t_1 }と名づけよう。
 あとは最大高度から目的の高さまでに落下する時間を計算しよう。

 { v_y }が0になってから落下しているので、以下の計算式になるはずだ。

{ \displaystyle y = \frac{1}{2}gt^{2} + h }

 これも[{ t }の式に変えよう。

{ \displaystyle t = \sqrt{ \frac{2(y - h)}{g} } }

 この最大高度から目標地点に到着するまでの時間を{ t_2 }と名づけよう。
 すなわち、全体で掛かる時間としては、
 
{ \displaystyle t = t_1 + t_2 }

 になるわけだ。
 
 これで時間が算出できた。
 時間が分かったのなら、あとは時間を決めうちしたときの計算式がそのまま使えるはずだ!

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;
}

f:id:urahimono:20170517003218g:plain

 いい感じです!
 ただ、指定した高さより高い位置から発射したり、高い位置を目標にしたりするときの対応が必要になりますな。

x方向の速度を俺が決める

 最後にはx方向の速度を指定する場合をやりましょう。
 まあ、ゲーム的にはxz平面上の速度になるんだけど。

 x方向の速度である、{ v_x }が分かってるということは、{ v_0\cos\theta }が決まっているということになるはずだ。
 ということは、{ \displaystyle x = v_0t\cos\theta }{ t }の式に変えて、
 
{ \displaystyle t = \frac{ x }{ v_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 );
}

f:id:urahimono:20170517004723g:plain

 これはすごい簡単にできたなぁ。

さいごに

 うーむ、結局時間から角度などを求めるときの、なぜか重力加速度の符号を反転せねば正常に動かない理由がわからなかった。
 どこで計算式を間違えたというのだ。
 もう眠いからこれ以上の考察は難しそうだ。

 式の間違い等に気づかれましたら、是非ともご連絡を……。