2020/11/09
今回のテーマ、タイトルだけでは伝わりにくいですが、2Dアクションでたまに見かける「フリーエイミング」的なものです。
これをUnityで実現する方法が分かりそうで分からなかったので挑戦してみました!
いくつか方法はありそうですが、本記事ではAnimatorControllerのBlendTreeを使いました。
実装時の前提として以下を想定しました。
- 2D方向(XY軸)のみ移動可能なゲームで使用する
 - ボタンを押している間は発射し続け、その最中に移動入力した方向に振り向く
 - 正面から下方向は考慮しない
 
実際は上記に加えて移動の制限やキャラクターの振り向きも考慮する必要がありますが、量が膨大になってしまうので今回は割愛させていただきます。
目次
射撃モーションの作成
モーションがないと始まらないので、「前方に射撃」「上方向に射撃」の2パターンを作っておきます。
作成したらUnityにインポートし、個別のアニメーションとして再生できる状態にしておきましょう。
発射する弾のプレハブ作成
発射する弾のプレハブを作成し、以下のスクリプトを設定します。クラス名は適当なのでお好みでどうぞ。
using UnityEngine;
public class ShotTestObject : MonoBehaviour
{
    [SerializeField]
    protected float shotSpeed = 10f;
    [SerializeField]
    protected float lifeTime = 2f;
    protected Vector3 forward;
    protected Quaternion forwardAxis;
    protected Rigidbody rb;
    protected GameObject characterObject;
    void Start()
    {
        rb = this.GetComponent<Rigidbody>();
        forward = characterObject.transform.forward;
        Destroy(gameObject, lifeTime);
    }
    void Update()
    {
        rb.velocity = (forwardAxis * forward) * shotSpeed;
    }
    public void SetCharacterObject(GameObject characterObject)
    {
        this.characterObject = characterObject;
    }
    public void SetForwordAxis(Quaternion axis)
    {
        this.forwardAxis = axis;
    }
}
後述するスクリプトで発射方向を変えられるようにするため、角度をQuaternion型で受け取れるようにしておきます。
また、移動はRigidbody.velocityで行っているので、Rigidbodyコンポーネントも付けておきます。
弾のエフェクトや当たり判定等の説明もすると長くなるので、そちらも割愛します。
BlendTreeの作成と遷移の設定
モデルで使用しているAnimatorControllerを開きます。ない場合は新規に作成してください。
まずは必要なパラメータを追加します。本記事ではBlendTreeへのステート切り替えフラグとして「AutoAttack」、BlendTree内のモーション切り替えの閾値として「AutoAttackAngle」を定義しました。
次にBlendTreeの作成です。ステートのないところで右クリック→BlendTreeでBlendTreeを作成します。
生成されたステートをダブルクリック→BlendTreeを選択し、インスペクターのMotionで先ほど作成したモーションを設定します。加えてParameterに先ほど追加したAutoAttackAngleを指定します。
設定後、ウィンドウ上部の階層表示からBaseLayerに切り替えます。
私のモデルの場合、すでにIdle(待機)とMove(移動)のステートがあったので、BlendTreeのステートとの遷移を設定しておきます。
「HasExitTime」のチェックを外し、「TransitionDuration」を0にします。遷移条件には先ほど追加したパラメータ「AutoAttack」を使用しましょう。
プレイヤー用スクリプトの作成
最後にプレイヤーキャラクター用のスクリプトを作成します。
using UnityEngine;
using System;
using System.Collections;
using DG.Tweening;
public class Player : MonoBehaviour
{
    public GameObject shotObject;
    public Transform shotOrigin;
    protected Animator animator;
    /// <summary>
    /// 遠距離攻撃の発射間隔
    /// </summary>
    protected int bulletInterval = 5;
    protected int bulletIntervalCount = 0;
    protected float currentAttackAngle;
    protected override void Start()
    {
        animator = this.GetComponent<Animator>();
    }
    protected override void Update()
    {
        if (animator) {
            if (Input.GetMouseButtonDown(0)) {
                animator.SetBool("AutoAttack", true);
            }
            if (Input.GetMouseButtonUp(0)) {
                animator.SetBool("AutoAttack", false);
            }
            if (animator.GetBool("AutoAttack")) {
                // 入力方向の角度取得
                float axis_x = Input.GetAxis("Horizontal");
                float axis_y = Input.GetAxis("Vertical");
                float lerpTime = 0.8f;
                float lookAngle = Vector2.Angle(transform.forward, new Vector2(axis_x, axis_y));
                // アニメーション用の線形補完
                float normalizedLookAngle = Mathf.Lerp(
                    animator.GetFloat("AutoAttackAngle"),
                    lookAngle / 90,
                    lerpTime
                );
                animator.SetFloat("AutoAttackAngle", normalizedLookAngle);
                // 発射方向用の線形補完
                currentAttackAngle = Mathf.Lerp(
                    currentAttackAngle,
                    lookAngle,
                    lerpTime
                );
                if (bulletInterval <= bulletIntervalCount) {
                    CreateShotObject(currentAttackAngle);
                    bulletIntervalCount = 0;
                }
            }
            bulletIntervalCount++;
        }
    }
    private void CreateShotObject(float axis)
    {
        GameObject shot = Instantiate(shotObject, shotOrigin.position, Quaternion.identity);
        var shotTestObject = shot.GetComponent<ShotTestObject>();
        shotTestObject.SetCharacterObject(gameObject);
        shotTestObject.SetForwordAxis(Quaternion.AngleAxis(axis, Vector3.forward * transform.rotation.y));
    }
}
パラメータのshotObjectは弾のプレハブを設定します。
shotOriginには弾の発射位置を設定します。私のモデルの場合は杖のboneの子として空のオブジェクトを作り、それを設定しました。
ステート切り替えフラグの制御
Input.GetMouseButtonDown()とInput.GetMouseButtonUp()でフラグのON/OFFを行います。その後Animator.GetBool()でフラグを確認し、trueであればBlendTreeと弾発射の処理に入ります。
もしキーボード入力やボタン入力の方が良い場合はGetKeyDown()やGetButtonDown()を使いましょう。
BlendTree用パラメータと発射方向の制御
移動入力の方向をInput.GetAxis()で取得し、Vector3.Angle()で正面からの角度を求めます。その角度を90で割った値をAnimatorControllerに設定することで、BlendTreeのモーションを変化させます。
また発射された弾の進む方向も変化させる必要があります。そのために弾のスクリプトのSetForwordAxis()でQuaternion.AngleAxis()から取得した角度を渡します。transform.rotation.yを掛けているのは向きに応じて角度を逆転させるためです。
この2つを作ってあげると、本記事冒頭のような挙動が実現できるかと思います。
弾の連射速度
フラグがONの間は弾を連射しますが、連射する間隔は「bulletIntervalで設定された値のフレーム」になります。
より具体的に言えば、(Update()内なので)毎フレーム自動でインクリメントされるbulletIntervalCountとbulletIntervalを比較します。bulletIntervalCountは発射後0にリセットするので、次に弾を生成するタイミングはbulletInterval以上の値まで溜まった時になります。
このスクリプトではbulletIntervalの値が5になっていますが、これより小さくすればもっと高速で連射できるようになります。逆に値を大きくすると連射速度は下がります。
あとがき
そんなわけで、BlendTreeを使ってフリーエイミング的な機能を実装してみました。
初めてBlendTreeを使いましたが、動的なモーションの補間にはかなり良さそうな印象です。今後の開発でも選択肢として覚えておくといろいろと使えそうですね。












					
					
					
					
						
						
						
						
						
						
						
						
						
						
						
						
						
						
						
						
						
						
						
						
						
                        
                        
                        



