Roba Memo - 素人のUnity覚書と奮闘記

素人のUnity覚書と奮闘記

矢を放物線状に飛ばしたい(2D 重力無視)

矢を放物線状に飛ばしたい!ということで四苦八苦した模様をメモ。

完成動画

矢にコライダーつけて当たり判定云々は致しません。
重力云々、考慮致しません。
視覚エフェクトとして、矢が放たれて的に当たるだけの処理です。
かなり適当です。笑

ベジェ曲線の方程式

こちらのサイト様を参考にさせて貰いました。
devmag.org.za ベジェ曲線上にある座標(x, y)を求める数式が紹介されています。
有り難く、その数式を使わせてもらいました。

利用したのは、3点から求める方法。
(画像お借りしました)

f:id:nico-taniku:20170830164010p:plain
[x,y]=(1–t)2 P0+2(1–t)tP1+t2 P2

方程式の t って何?

[x,y]=(1–t)2P0+2(1–t)tP1+t2P2

方程式に出てくる記号のようなものを整理してみよう。
P0 = 始点
P2 = 終点
P1 = ハンドル
t = ????

t って何だろう?

方程式に、
t = 0 を当てはめると、P0になる。
t = 1を当てはめると、P2になる。
つまり、
t=0のとき、始点になる。
t=1のとき、終点になる

ということは、tを 0〜1 へと変化させると 始点〜終点 へと移動することになる。

(原文)
The function takes a parameter t. The value of the function is a point on the curve; it depends on the parameter t, and on a set of points, called the control points. The first and last control points are the endpoints of the curve. Generally, the curve does not pass through the other control points. The value t can range from 0 to 1. The value 0 corresponds to the start point of the curve; the value 1 corresponds to the endpoint of the curve. Values in between correspond to other points on the curve.

英語わからんので、Weblio先生に助けて貰いながらやったけど、頭の中で矢が飛ぶイメージが出来た気がする〜!

P1xを求める

P1=ハンドル と書きましたが、このP1座標で、放物線の山の形が決まる。

f:id:nico-taniku:20170830183408p:plain
P1xは、P2xからP0xの間となるので、P2xを1としたときの割合(ratio)を指定することで求められるはず。

P1x = P2x * ratio  

例えば、ratio=0.5fならば、上の図のような感じになるはず。
さらに、P2x=0のとき。つまり真上か真下に敵がいるとき、P1(0,0)となり、矢は直線的に飛んで行くことになる。
うんうん、2Dゲームだからこれでいいんだ。矢らしい動きになりそうだ。

って、なんか適当だな。これでいいんだろうか?と不安になりつつ、次はy座標。

P1yを角度から求める

角度から求める理由

単純にパラメーターで高度(P1y)を数値で設定してもいいのかな?
いや、P1yがP2yより低い位置になると放物線が逆向きになってしまうような?
さらに、P2x=0のときにP2yより高いと、行き過ぎてから戻ってきちゃったりする?
高度を固定すると変になるが、変化させる条件を考えるのは、ちょっと難しそう。

方法を変えて発射角度を設定するのはどうだろうか?
f:id:nico-taniku:20170830183408p:plain
P0から指定された角度で伸びる直線と、P1xから垂直に伸びる直線の交点がP1になる。
それによって出来た三角形からP1yが求められるんじゃないだろうか?
P2xが0に近づくほど、三角形は小さくなり、P2x=0でなくなる。
ということは、真上か真下にいるときに、矢が行き過ぎて戻ってくることはない。
さらに、発射角度を45度以上90度未満に設定しておけば、放物線が逆に反り返ることも防げそうだ。

P1yの求めかた

ではでは、三角比の定義を思いだ・・・せないので、グーグル先生にHelp!!
f:id:nico-taniku:20170830185653p:plain

1・サイン・コサイン三角形より引用
斜辺の長さを1とすると辺の比が1:sinθ:cosθ となる.θ の値が変化してもこの比を表す式は変わらない.

考え方
アナフィラキシーショックを起こしそうですが。

斜辺1のとき sinθ
斜辺xなら sinθ * x
つまり、xを求めればP1yが算出できる。

xを求める
斜辺1のとき cosθ
斜辺xのとき P1x
これを比率計算して、xを求める。

1 : cosθ = x : P1x
cosθ * x = P1x * 1
x = P1x / cosθ

式にxを当てはめる

P1y = sinθ * P1x / cosθ

え〜、ほんまに、これであってんの〜?
ちょっとシミュレーションしてみた。
そしたら、常に同じ角度で放つので、敵が目の前にいるのに上方向へ放つようになってしまった。

距離に応じて角度を変化させる

f:id:nico-taniku:20170831091516p:plain
こうなってくると、物理演算とか使った方がいいんだろうな。と思いつつもゴリ押しする!笑

P2x=5fのとき45度で放つなど、距離と角度の基準を決めておく。
敵の距離が近づくにつれて、角度を小さくする。
敵の距離が離れるにつれて、角度を大きくする。
角度が50度を超えたら、50度に固定する。

比率計算で実際の角度を求める。
基準の角度 : 実際の角度 = 基準の距離:実際の距離
実際の角度 * 基準の距離 = 基準の角度 * 実際の距離
実際の角度 = 基準の角度 * 実際の距離 / 基準の距離

これを式にすると・・・

angle = 基準の角度;
angle = angle * distance / 基準の距離;
if (angle > 最大角度) {
    angle = 最大角度;
}

これで、矢っぽくなるはず〜♪

1秒あたりの t の変化量

少し話を遡って、おさらい。
tを 0〜1 へと変化させると 始点〜終点 へと移動することになる。

t += 1秒あたりのtの変化量 * Time.deltaTime;

これを使えば、毎フレームtを加算されて0〜1へと変化するはず。

しかし、一定の変化量だと、敵が1m先だろうと5m先だろうと、同じ秒数で到達してしまう。
これを防ぐには、距離に応じて変化量を調整すればいい。

distance = 総移動距離
speed = 秒速

時間=距離/速さ(はじきの公式より)
totalTime = distance / speed;

これはtが0から1へ移動するのにかかる時間になるので
1秒あたりのtの変化量 = 1 / distance / speed;

この値に Time*deltaTimeをかけてやれば、1フレームあたりの t の変化量が割り出される。
これを毎フレーム加算すれば、いけるはず。
ということで、いざ、コーディング!!

コーディング

コルーチンを作る

とりあえず、コルーチンで処理をループさせる。

public float speed;
GameObject target;

public void OnStart (GameObject target)
{
    this.target = target;
    StartCoroutine (Throw ());
}

IEnumerator Throw ()
{
    float t = 0f;
    float distance = Vector3.Distance (transform.position, target.transform.position);

    while (t <= 1 && target) {
        t += 1 / distance / speed * Time.deltaTime;

        yield return null;
    }
}

distanceについて

//点aと点bの距離を返す(ベクトル長さ)
public static float Distance(Vector3 a, Vector3 b);

ベクトルの長さなので、正の値になる。
たぶん、tの値って道のりだと思うんだよね。
このコードだと、直線距離になっちゃうから、ぴったり!ってわけじゃないけど、距離に応じて滞空時間は変わるようになったので良しとしておく。笑

whiteの条件式について
target==tureの条件を入れたのは、敵が死亡したらオブジェクトを破棄(Destroy)しちゃうから、そうなるとnullっちゃうので、それの対応処置。

P0

P0は始点(0.0)なので変数にする必要は無いんだけど、実際は矢のワールド座標が始点になるので、オフセットする必要がある。

関連部分のみ。

IEnumerator Throw ()
{

    Vector3 offset = transform.position;

}

これを、算出した矢の座標に足す。

P2

P2は終点(target.x , targety)から、先ほどのオフセットとの差になる。
計算で使うので変数にいれとく。

関連部分のみ。

IEnumerator Throw ()
{

    Vector3 P2 = target.transform.position - offset;

}

P1

P1座標を求める式のおさらい。

P1x = P2x * ratio  
P1y = sinθ * P1x / cosθ

ratio = 割合。X座標のどの位置に放物線の山の頂点を持ってくるかを決める。
sinθ・cosθを求めるには、発射角度が必要。
その設定方法は、

P2x=5fのとき45度で放つなど、基準を決めておく。
敵の距離が近づくにつれて、角度を小さくする。
敵の距離が離れるにつれて、角度を大きくする。
角度が50度を超えたら、50度に固定する。

angle = 基準の角度;
angle = angle * distance / 基準の距離;
if (angle > 最大角度) {
    angle = 最大角度;
}

これをコードにする。
ここでは、5mで45度・上限50度になるように設定してある。

関連部分のみ。

public float ratio;

IEnumerator Throw ()
{
    Vector3 P2 = target.transform.position - offset;
    float distance = Vector3.Distance (transform.position, target.transform.position);
    float angle = 45f;
    float base_range = 5f;
    float max_angle = 50f;

    angle = angle * distance / base_range;
    if (angle > max_angle) {
        angle = max_angle;
    }

    float P1x = P2.x * ratio;
    float P1y = Mathf.Sin (angle * Mathf.Deg2Rad) * Mathf.Abs (P1x) / Mathf.Cos (angle * Mathf.Deg2Rad);
    Vector3 P1 = new Vector3 (P1x, P1y, 0);

}

sinθ・cosθについて
sinθ・cosθはMathf.SIn(ラジアン) , Mathf.Cos(ラジアン) で求めることができる。
ただしangleは角度なので、角度からラジアンに変換しないといけない。
それが、 angle * Mathf.Deg2Rad になる。

Mathf.Abs ()について
Mathf.Abs()は、絶対値を求めるメソッドになる。
例えば、始点よりも終点がマイナス方向にあった場合、放物線が逆向きになってしまうのを防ぐために絶対値で正の数値になるようにしてある。

矢の座標(Vx , Vy, 0)を求める

[x,y]=(1–t)2P0+2(1–t)tP1+t2P2

この式に当てはめる。
P0は(0,0)なので、2(1–t)tP1+t2P2になり、これにoffsetを足す。

2(1–t)tP1+t2P2+offset

GameObject target;
public float speed, ratio;

public void OnStart (GameObject target)
{
    this.target = target;
    StartCoroutine (Throw ());
}

IEnumerator Throw ()
{       
    float t = 0f;
    float distance = Vector3.Distance (transform.position, target.transform.position);
    Vector3 offset = transform.position;
    Vector3 P2 = target.transform.position - offset;

    float angle = 45f;
    float base_range = 5f;
    float max_angle = 50f;

    angle = angle * distance / base_range;
    if (angle > max_angle) {
        angle = max_angle;
    }

    float P1x = P2.x * ratio;
    //angle * Mathf.Deg2Rad 角度からラジアンへ変換
    float P1y = Mathf.Sin (angle * Mathf.Deg2Rad) * Mathf.Abs (P1x) / Mathf.Cos (angle * Mathf.Deg2Rad);
    Vector3 P1 = new Vector3 (P1x, P1y, 0);

    while (t <= 1 && target) {
        float Vx = 2 * (1f - t) * t * P1.x + Mathf.Pow (t, 2) * P2.x + offset.x;
        float Vy = 2 * (1f - t) * t * P1.y + Mathf.Pow (t, 2) * P2.y + offset.y;
        transform.position = new Vector3 (Vx, Vy, 0);

        t += 1 / distance / speed * Time.deltaTime;

        yield return null;
    }

    Destroy (this.gameObject);
}

xのn乗を求めるメソッド

Mathf.Pow(x, n);  

よし!これで矢が飛ぶはず〜♪
って、飛ばしてみたら、今度は矢の向きが気になった!!!

矢の向きを調整する

矢の向きをスムーズに向きを変えたいので下記のメソッドを使う。

//aからbへ t秒かけて向きを変えるメソッド  
public static Quaternion Slerp(Quaternion a, Quaternion b, float t);

ただし、開始地点では、瞬時にP1に向けて飛んでいくので、即時に回転させるメソッドを使う。

//aからbへ 向きを変えるメソッド
public static Quaternion FromToRotation(Vector3 a, Vector3 b);

切り替えるタイミングは・・・実は適当w
f:id:nico-taniku:20170901111138p:plain
P1に到達してしまっては、遅いんですよね。
それより手前から、徐々に向きを変えていきたい。
なので、 t > ratio * n になったらSlerpを実行する。

関連部分のみ。

IEnumerator Throw ()
{
    Vector3 look = P1;
    transform.rotation = Quaternion.FromToRotation (Vector3.up, look);
    float slerp_start_point = ratio * 0.5f;

    while (t <= 1 && target) {


        if (t > slerp_start_point) {
            look = target.transform.position - transform.position;
            Quaternion to = Quaternion.FromToRotation (Vector3.up, look);
            transform.rotation = Quaternion.Slerp (transform.rotation, to, speed * 0.5f * Time.deltaTime);
        }


        t += 1 / distance / speed * Time.deltaTime;
        yield return null;
    }
}

Slerpの第三引数( speed * 0.5f * Time.deltaTime )について
これも、適当です。笑
再生しながら調整しました。

参考にしたサイト様 karaagedigital.hatenablog.jp

完成コード

using UnityEngine;
using System.Collections;

public class ThrowArrow : MonoBehaviour
{
    GameObject target;
    public float speed, ratio;

    void Start ()
    {          
        OnStart (GameObject.Find ("tree").gameObject);
    }

    public void OnStart (GameObject target)
    {
        this.target = target;
        StartCoroutine (Throw ());
    }

    IEnumerator Throw ()
    {       
        float t = 0f;
        float distance = Vector3.Distance (transform.position, target.transform.position);

        Vector3 offset = transform.position;
            Vector3 P2 = target.transform.position - offset;

        //高度設定
        float angle = 45f;
        float base_range = 5f;
        float max_angle = 50f;

        angle = angle * distance / base_range;
        if (angle > max_angle) {
            angle = max_angle;
        }


        float P1x = P2.x * ratio;
        //angle * Mathf.Deg2Rad 角度からラジアンへ変換
        float P1y = Mathf.Sin (angle * Mathf.Deg2Rad) * Mathf.Abs (P1x) / Mathf.Cos (angle * Mathf.Deg2Rad);
        Vector3 P1 = new Vector3 (P1x, P1y, 0);

        Vector3 look = P1;
        transform.rotation = Quaternion.FromToRotation (Vector3.up, look);
        float slerp_start_point = ratio * 0.5f;

        while (t <= 1 && target) {
            float Vx = 2 * (1f - t) * t * P1.x + Mathf.Pow (t, 2) * P2.x + offset.x;
            float Vy = 2 * (1f - t) * t * P1.y + Mathf.Pow (t, 2) * P2.y + offset.y;
            transform.position = new Vector3 (Vx, Vy, 0);

            if (t > slerp_start_point) {
                look = target.transform.position - transform.position;
                Quaternion to = Quaternion.FromToRotation (Vector3.up, look);
                transform.rotation = Quaternion.Slerp (transform.rotation, to, speed * 0.5f * Time.deltaTime);
            }

            t += 1 / distance / speed * Time.deltaTime;
            yield return null;
        }

        //Destroy (this.gameObject);
    }


}

パラメーター設定

f:id:nico-taniku:20170901115702p:plain:w300

以上。