[Unity] ベジェ曲線で矢を飛ばす(非エンジニア)

unityバージョン:2019.4.25f1

矢を物理演算を使わずに飛ばしたかったので、Vector3.Lerpを使ってベジェ曲線で飛ばすことにした

矢を放物線状に飛ばす

以下のScriptを矢のオブジェクトにAdd Componentすると
矢の出現と同時に飛んでいく
※ターゲットと発射元のオブジェクトが必要

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ArrowScript : MonoBehaviour // クラス名はファイル名と同じにする
{  
    void Start()
    {
        // ThrowingArrow というメソッド内に記述されている処理を実行する
        ThrowingArrow();
    }
   
    private void ThrowingArrow()
    {
        // 矢を発射するオブジェクトを取得
        GameObject _player = GameObject.Find("DummyObject/Player");
        // 矢のターゲットのオブジェクトを取得
        GameObject _target = GameObject.Find("DummyObject/Enemy");
        
        // ベジェ曲線の制御点を取得
        // 開始地点
        Vector3 arrow_start = _player.transform.position;
        // 終着地点
        Vector3 arrow_end = _target.transform.position;
        // 中間地点
        Vector3 arrow_middle = Vector3.Lerp(arrow_start, arrow_end, 0.5f);
        arrow_middle.y += 3; //放物線を描くために中間地点のY軸を上に移動
       
        // 矢のスピード
        float divide_distance = 10 / Vector3.Distance(arrow_start, arrow_end);
        
        // ShootArrow というコルーチンを開始
        StartCoroutine(ShootArrow(arrow_start, arrow_end, arrow_middle, divide_distance));
    }
    private IEnumerator ShootArrow (Vector3 start, Vector3 end, Vector3 middle, float arrow_speed)
    {
        // ベジェ曲線用の変数の宣言
        float t = 0.0f;
        
        // ループを抜けるまで以下を繰り返す
        while(true) {
            if ( t > 1 )
            {
                // 終着点でこのオブジェクトを削除
                Destroy(gameObject);
                yield break; // ループを抜ける
            }
            
            // ベジェ曲線の処理
            t += arrow_speed * Time.deltaTime;
            Vector3 a = Vector3.Lerp(start, middle, t);
            Vector3 b = Vector3.Lerp(middle, end, t);
            // 座標を代入
            this.transform.position = Vector3.Lerp(a, b, t);
            
            // 矢の向き
            this.transform.LookAt(b, Vector3.up);
            
            yield return null;
        }
    }
}

ベジェ曲線について

制御点となる複数の座標をもとに曲線を描く

詳しい説明は以下を参考にしてみてください
中学生でもわかるベジェ曲線
http://blog.sigbus.info/2011/10/bezier.html

Lerpについて

 Vector3.Lerp(a, b, t);
a == 座標a
b == 座標b
t == 座標ab間の割合(0~1)
aとbの2点の座標の間の位置をt(0~1)の値で指定してその座標を取得する
tを0.5で指定すれば2点間の中心地点の座標が取得できる

これを利用して3つのVector3.Lerpを使うことでベジェ曲線を作ることができる tを毎フレーム加算していくことで各制御点の間にある座標が移動して矢が放物線を描く
以下の図でtの加算によって赤と青と紫の点が同時に移動すると紫の点が黄緑の線のような軌道を描く

画像1

詳しい説明は以下を参考にしてみてください
[Unity] Vector3.Lerpの使い方
https://qiita.com/aimy-07/items/ad0d99191da21c0adbc3

中心地点の取得
ここではVector3.Lerpを使って取得しているが下記のような計算で取得することも可能

画像2

上記から以下のように記述しても中心地点を取得できる

Vector3 arrow_middle = new Vector3(( arrow_start.x + arrow_end.x ) / 2, ( arrow_start.y + arrow_end.y ) / 2, ( arrow_start.z + arrow_end.z ) / 2);

詳しくは以下を参考にしてみてください
中点の座標を求める公式と証明
https://mathwords.net/tyutenzahyo

2点の座標と角度の中間を求める
https://qiita.com/nenjiru/items/ba6ee630b23f0cdd2136

Vector3.Distance

float divide_distance = 10 / Vector3.Distance(arrow_start, arrow_end);

Vector3.Distance(開始点, 終着点)で2点間の距離をとる
ここでは10を距離で割った値をtの加算値として使っている
2点間の距離が遠かったり近かったりした場合にtの加算値が同じままでは距離によって矢のスピードが変わってしまいます(距離が遠いほど速くなる)なので距離で10の値を割ることでスピードを一定に保つようにしています
※10を大きい値にすれば矢のスピードが上がる

GameObject.Find

GameObject.Find("DummyObject/Player");
Hierarchyウィンドウ内のオブジェクトを見つけて取得する
そのオブジェクトの座標や回転の情報、コンポーネントの情報を取得したい時には先にこれでオブジェクトを取得してからその情報にアクセスする

""にHierarchyウィンドウ内の場所を記述する
オブジェクト名が一意ではない場合は親オブジェクトを含めて記述しなければならない
上記の記述ではDummyObjectが親で取得したいオブジェクトがPlayerになる

コルーチン

StartCoroutine(ShootArrow(arrow_start, arrow_end, arrow_middle, divide_distance));
StartCoroutine()でコルーチンを開始してIEnumerator ShootArrow(){}内の処理を行う
ShootArrowがコルーチンの名前
()内に記述されているのは引数と呼ばれるもの、ここでは変数を使って渡しているが、値を直接記述しても良い、値や変数の型は引数として指定した型と同じにしなければならない

private IEnumerator ShootArrow (Vector3 start, Vector3 end, Vector3 middle, float arrow_speed){}
これはコルーチン用のメソッドで{}内に行いたい処理を記述する
()内では引数を指定している、渡す値がなければ記述する必要はない
記述する場合は変数の型と変数の名前を記述する
変数が複数ある場合は , をつけて追記する
ここで指定した変数の名前はこのコルーチンの中の処理を記述する際に使う

ループ処理
コルーチン内で以下のように記述することで毎フレーム処理を繰り返す
yield break;でループを止めることができる

while(true) {
    // 処理
    yield return null;
}

毎フレームではなく一定時間ごとに繰り返す場合は以下のように記述する
以下は3秒毎に繰り返す 秒数は引数で渡すこともできる
yield returnの前に処理を記述すると処理を行なってから待つ
後ろに記述すると待ってから処理を行う

while(true) {
    yield return new WaitForSeconds (3.0f);
    // 処理
}

WaitForSecondsを大量に使う場合はキャッシュした方が良いらしい
キャッシュの方法は以下を参考にしてみてください

コルーチンの中断
コルーチンを途中で中断したい場合はコルーチンを変数に代入する必要がある

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SampleScript : MonoBehaviour // クラス名はファイル名と同じにする
{
    // コルーチン型の変数を宣言
    private Coroutine aaa;
    
    void Start()
    {
        // aaa という変数にコルーチンを代入しつつ BBBコルーチンを開始
        aaa = StartCoroutine(BBB());
    }
    void Update()
    {
        // Aキーを押した時
        if(Input.GetKeyDown(KeyCode.A))
        {
            // aaa という変数に入っているコルーチンを止める
            StopCoroutine(aaa);
        }
    }
    // コルーチン
    private IEnumerator BBB ()
    {
        while(true) {
            // 処理
            yield return null;
        }
    }
}

コルーチンが変数に代入されていない状態でStopCoroutine()が実行されるとNullReferenceExceptionというエラーが起きる
コルーチンが開始する前にStopCoroutine()の実行の条件を満たしてしまう場合はエラーを回避したいので以下のように記述する

try{
    StopCoroutine(aaa);
}
catch (System.NullReferenceException)
{
    // 処理なし
}

コルーチンについて詳しくは以下を参考にしてみてください
【C#/Unity】コルーチン(Coroutine)とは何なのか
https://spirits.appirits.com/doruby/8712/?cn-reloaded=1

Time.deltaTime

フレーム毎の経過時間を取得する
処理落ち対策に入れるっぽい

詳しい説明は以下を参考にしてみてください
【Unity】Time.deltaTimeの正しい使い方わかってる?適当に掛ければいいてもんじゃない!
https://qiita.com/toRisouP/items/930100e25e666494fcd6

メインループ マニアック解説〜これからのTime.deltaTime〜 - Unityステーション
https://www.youtube.com/watch?v=TP7N57r5Tqw

LookAt

this.transform.LookAt(b, Vector3.up);
座標bにこのオブジェクトを向ける
Vector3.upはどの面を上方向として扱うかを指定している
これは省略可能で省略した場合はVector3.upが指定される
Vector3.forward 正面
Vector3.back 背面
Vector3.up 上面
Vector3.down 下面
Vector3.right 右面
Vector3.left 左面
どれを使っても矢が正しい向きにならない時は3Dモデルの方を直した方が早い、ボーンの向きも変更する場合はそれに付随するアニメーションをエクスポートし直さなければならない

詳しい説明は以下を参考にしてみてください
【Unity】オブジェクトをターゲットの方向に回転させる
https://nekojara.city/unity-look-at

メソッド内での変数の宣言

今回のScriptではほとんどの変数をメソッド内で宣言している
メソッド内で宣言した場合はその変数はメソッド内でのみしか使えない
メソッドを跨いで変数を使用したいときはメソッドの外で宣言しなければならない
ただ変数が増えてくるとメソッド内でしか使わないものはメソッド内で宣言しておいた方が見やすくなると思う

他のScriptで使用する際は変数の前にpublicをつけて宣言する
基本はprivateで良い、何もつけなければprivateで宣言したことになる
publicか[SerializeField] privateで宣言するとInspectorウィンドウに変数が表示されてそこで値の変更が可能になる

詳しい説明は以下を参考にしてみてください
Unityの[SerializeField]について色々な疑問に答えてみる

追記

矢の動きにイージングをかけたい場合は以下を参考にしてみてください

たぶん以下のように書き換えるとイージングがかかると思う

// ベジェ曲線の処理
t += arrow_speed * Time.deltaTime;
float c = -1 * t * (t - 2.0f);
Vector3 a = Vector3.Lerp(start, middle, t);
Vector3 b = Vector3.Lerp(middle, end, t);
// 座標を代入
this.transform.position = Vector3.Lerp(a, b, c);

勉強中なので説明が間違っている可能性があります自分でも調べて確認してみてください


この記事が気に入ったらサポートをしてみませんか?