[Unity] パーティクルで追尾弾を作る

# 経緯
Unity のパーティクルシステムで自動追尾（ホーミング）弾を作ろうと思いました。ググるとそれなりに解説が出てくるのですが、パーティクルではなく普通の GameObject を弾に見立てた物が多くて、パーティクルシステムに応用するのに少し改変が必要だったので、まとめました。

![homing1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/97cae946-7296-fe7d-c0f8-5751b298d8fd.gif) ![homing2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/9e07a507-a05f-9078-2de2-4a29b818abfe.gif) ![homing3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/d82548f9-ffe7-c409-991c-a304f7581aea.gif)


参考にしたのは主にこちらの二つの記事です

[UKONNの巻き方] [Unity]ホーミングレーザーの作り方（3D）
https://ukonn.net/unity-how-to-make-homing-laser
[HEXA DRIVE] ホーミングミサイル
http://hexadrive.sblo.jp/article/60164980.html

# パーティクル操作の基本
参考記事で GameObject で追尾させる方法はわかった。ではパーティクルに適用するにはどうするか。

基本はこんな感じになります。

```csharp:ParticleOperation.cs
using UnityEngine;

public class ParticleOperation : MonoBehaviour
{
    ParticleSystem ps;
    ParticleSystem.Particle[] m_Particles;
 
    void Start()
    {
        ps = GetComponent<ParticleSystem>();
    }
 
    void Update()
    {
        m_Particles = new ParticleSystem.Particle[ps.main.maxParticles];
        int numParticlesAlive = ps.GetParticles(m_Particles);
        for (int i = 0; i < numParticlesAlive; i++)
        {
            // ここでパーティクル毎に計算結果を適用する
            m_Particles[i].velocity = ...;
        }
        ps.SetParticles(m_Particles, numParticlesAlive);
    }
}
```

このクラスは ParticleSystem と同じ GameObject に AddComponent する想定ですが、 ps を public または SerializeField にしてインスペクタからアタッチしてもいいと思います。
**その場合は Start メソッドの記述は要りません**。

Update では対象の ParticleSystem のパーティクル情報を取得して for 文で回して個々のパーティクルの情報を更新、最後に SetParticles で上書きします。

# 目標を追尾する
調べてみると追尾弾と言っても、実装方法に色々な考え方があることがわかって来ました。そこで、とりあえず参考記事をもとに３種類試してみました。

## 目標に向けて加速度をつける
こちらの記事 https://ukonn.net/unity-how-to-make-homing-laser で紹介されてる方法です。さらに元は Unity の中の人の[講演動画](https://youtu.be/t_4MbV2zIwg)から。
この手法の特徴は、終盤になるほど加速度的に目標に近づき、**最終的には必中させることもできる**点です。

### コード
**※最初に貼った ParticleOperation.cs と共通部分は一部省略してます**

```cs:HomingByAcceleration.cs
    ParticleSystem ps;
    ParticleSystem.Particle[] m_Particles;

    public float threshold = 100f;
    public float intensity = 1f;

    // ターゲットをセットする
    public Transform target;

    void Update()
    {
        m_Particles = new ParticleSystem.Particle[ps.main.maxParticles];
        int numParticlesAlive = ps.GetParticles(m_Particles);
        for (int i = 0; i < numParticlesAlive; i++)
        {
            var velocity = ps.transform.TransformPoint(m_Particles[i].velocity);
            var position = ps.transform.TransformPoint(m_Particles[i].position);

            var period = m_Particles[i].remainingLifetime * 0.9f;

            //ターゲットと自分自身の差
            var diff = target.TransformPoint(target.position) - position;
            Vector3 accel = (diff - velocity * period) * 2f / (period * period);

            //加速度が一定以上だと追尾を弱くする
            if (accel.magnitude > threshold)
            {
                accel = accel.normalized * threshold;
            }

            // 速度の計算
            velocity += accel * Time.deltaTime * intensity;
            m_Particles[i].velocity = ps.transform.InverseTransformPoint(velocity);
        }
        ps.SetParticles(m_Particles, numParticlesAlive);
    }

```

**命中までの時間はパーティクルの寿命**を使用しました。

他のサンプルにも言えることですが、TransformPoint/InverseTransformPoint を使ってローカル座標とワールド座標の変換を行う必要があります。もしパーティクルとtargetが同じ座標空間なら変換はなくても大丈夫です。 

### 結果
![homing1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/97cae946-7296-fe7d-c0f8-5751b298d8fd.gif)
上のコードだと threshold を大きくすれば必中になる・・・はずなんですが、あんまりそう見えないですね。おそらく position ではなく velocity を操作してるので、計算結果が適用されるタイミングでは目標がさらに動いてしまっているのだと思います。
**position を直接設定する方法も試してみたんですが、上手くいかなくて諦めました。**

## 目標へのベクトルを加算する
これはネタ元は某英語サイトで「目標へのベクトルを加算すればいい」と一言書いてあっただけですが、試してみました。考え方自体は一つ目の方法に似てますが、計算方法はより単純です。

### コード
**※最初に貼った ParticleOperation.cs と共通部分は一部省略してます**

```cs:HomingByAddVector.cs
    ParticleSystem ps;
    ParticleSystem.Particle[] m_Particles;

    // ターゲットをセットする
    public Transform target;
    public float _speed = 6.0f;    // 1秒間に進む距離
    public float _rotSpeed = 180.0f;  // 1秒間に回転する角度

    void Update()
    {
        m_Particles = new ParticleSystem.Particle[ps.main.maxParticles];
        int numParticlesAlive = ps.GetParticles(m_Particles);
        for (int i = 0; i < numParticlesAlive; i++)
        {
            var forward = ps.transform.TransformPoint(m_Particles[i].velocity);
            var position = ps.transform.TransformPoint(m_Particles[i].position);

            // ターゲットへのベクトル
            var direction = (target.TransformPoint(target.position) - position).normalized;

            var period = 1 - (m_Particles[i].remainingLifetime / m_Particles[i].startLifetime);

            m_Particles[i].velocity = ps.transform.InverseTransformPoint(forward + direction * period);
        }
        ps.SetParticles(m_Particles, numParticlesAlive);
    }
```

### 結果
![homing2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/9e07a507-a05f-9078-2de2-4a29b818abfe.gif)
結果も一つ目と似てますが、periodの計算が1の補数になるようにしたため、初期の方向転換が急激で終盤は直線的になる傾向があります。
ちなみに、一つ目と違って理屈上、必中ではありません。

## 目標に向けて進行方向を回転させる
こちらの記事 http://hexadrive.sblo.jp/article/60164980.html を参考にしました。
この手法の特徴は、速度の絶対値（magnitude）を変えずに向きだけを変えるので、**ParticleSystem 側の設定で速度のコントロールがし易い**点だと思います。角度制限があるので必中ではありません。

### コード
**※最初に貼った ParticleOperation.cs と共通部分は一部省略してます**

```cs:HomingByRotation.cs
    ParticleSystem ps;
    ParticleSystem.Particle[] m_Particles;

    // ターゲットをセットする
    public Transform target;
    public float _rotSpeed = 180.0f;  // 1秒間に回転する角度

    void Update()
    {
        m_Particles = new ParticleSystem.Particle[ps.main.maxParticles];
        int numParticlesAlive = ps.GetParticles(m_Particles);
        for (int i = 0; i < numParticlesAlive; i++)
        {
            var velocity = m_Particles[i].velocity;
            var position = m_Particles[i].position;

            // ターゲットへのベクトル
            var direction = ps.transform.InverseTransformPoint(target.TransformPoint(target.position)) - position;

            // ターゲットまでの角度
            float angleDiff = Vector3.Angle(velocity, direction);

            // 回転角
            float angleAdd = (_rotSpeed * Time.deltaTime);
            
            // ターゲットへ向けるクォータニオン
            Quaternion rotTarget = Quaternion.FromToRotation(velocity,direction);              
            if (angleDiff <= angleAdd)
            {
                // ターゲットが回転角以内なら完全にターゲットの方を向く
                m_Particles[i].velocity = (rotTarget * velocity);
            }
            else
            {
                // ターゲットが回転角の外なら、指定角度だけターゲットに向ける
                float t = (angleAdd / angleDiff);
                m_Particles[i].velocity = Quaternion.Slerp(Quaternion.identity, rotTarget, t) * velocity;
            }
        }
        ps.SetParticles(m_Particles, numParticlesAlive);
    }
```

回転計算は元記事のままでは上手く利用できなかったので少々修正してます。

### 結果
![homing3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/d82548f9-ffe7-c409-991c-a304f7581aea.gif) 
これまでの二つにくらべると、独特な軌道ですね。一定の旋回半径をもって回転しているのがわかります。最初の二つはレーザーっぽいですが、この動きはミサイルにいいんじゃないかと思いますね。

# まとめ

+ パーティクルをスクリプトから操作する一般的な方法がわかった
+ パーティクルは gameObject とはプロパティが少し違うことがわかった
+ パーティクルでは position, rotation を操作するより velocity を操作する方がいい感じに調整できた。    
+ 追尾の計算方法は色々あって、それぞれ特徴があることがわかった
    + 目標に向けて加速度を計算する方法を試した
        + 必中にはなってないので、まだ調整の余地がある
    + 目標へのベクトルを加算する方法を試した
    + 目標に向けて進行方向を回転させる方法を試した