【Unity】Unityでストレイフ

## 概要
　いくつかのFPSゲームには「ストレイフ」と呼ばれるテクニックが存在します．一般的にストレイフとは，空中で移動キー入力と視点移動を組み合わせることで，空中で移動方向を変えることです．また，このときわずかな加速が得られます．ストレイフが存在するゲームでは，それはしばしば壁に素早く隠れる場合や，情報をとるためのピーク（壁から体を出してのぞくこと）をするときに必須のテクニックとして扱われます．しかし勝負を有利にするためだけでなく，その挙動自体が非常に面白く，かつプレイヤースキルを要求されるため，ストレイフと，ストレイフを連続で行うことで大きな加速を得る「バニーホップ」で遊んでいるプレイヤーは多く存在します（驚くべきことに，彼らは撃ち合いがメインのゲームにおいて，ただただタイルやブロックの上を跳びまわっているだけなのです）．今回は，そんなストレイフが可能なキャラクターコントローラはどのようにしたら実現できるかということをご紹介します．

<img width="600", alt="projection", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/02a784fb-8b4c-003a-a28a-0df82ad39445.gif">


【図1】完成したキャラクターコントローラによるストレイフとバニーホップ．地上での最大速度は 7.7 だが，15.5 程度まで加速していることがわかる．なお，オートジャンプ（着地後，自動的にすぐさまジャンプすること）はオンにしている（バニーホップを成功させるには，着地後すぐにジャンプすることが重要となる）．

※ 今回ご紹介するキャラクターコントローラは，ひと昔まえのゲームエンジンで使われていた仕組みに基づくものです．そのため，今日多くのプレイヤーに利用されているゲーム内で実現可能なストレイフとは，原理が異なるかもしれません．

## 基本的な仕組み
　はじめに，現在のフレームでのプレイヤーの速度（プレイヤーベクトル：$V_{\rm player}$）から，次のフレームでのプレイヤーの速度 $V_{\rm player, next}$ を計算する手順を示します．

1. 摩擦によるベクトル $V_{\rm fric.}$（摩擦ベクトルと呼ぶ．ただし，摩擦ベクトルは力ではなく速度を表す点に注意）とプレイヤーベクトル $V_{\rm player}$ の和 $V_{\rm player, fric.}$ を得る（摩擦ベクトルはプレイヤーベクトルと逆向きのため，$V_{\rm player, fric.}$ の大きさはプレイヤーベクトルの大きさよりも小さくなる．空中では摩擦ベクトルの大きさを 0 として計算するため，空中では $V_{\rm player, fric.} = V_{\rm player}$ となる）．
2. $V_{\rm player, fric.}$ の入力ベクトルへの射影ベクトル $V_{\rm player, fric. // V_{\rm input}}$ を得る（入力ベクトルとは，プレイヤーが移動したい方向を表す単位ベクトルである）．
2. 射影ベクトルの大きさと最大速度 $|V_{\rm max}|$ を比較し，加算ベクトル $V_{\rm add}$ を得る（最大速度は，地上と空中で異なる）．
5. $V_{\rm player, fric.} + V_{\rm add}$ を，次のフレームでのプレイヤーベクトル $V_{\rm player, next}$ とする．

※ 最大速度を $|V_{\rm max}|$ と表記するのは若干おかしい気がしますが，|V|_{\rm max} としてもうまく描画されなかったのでこちらの表記を使用します．とにかく，ベクトル量ではなくスカラー量であるということが重要です．

　上記のざっくりした説明だけではもはや説明にもなっていないと思うので，詳細な説明は以降で，具体例を示しながら行いたいと思います．
　説明に入るまえに前提知識として持っていただきたいのは，このキャラクターコントローラではプレイヤーの速度の大きさを直接制限するのではなく，図3のようにプレイヤーの速度の入力ベクトル（移動キーの入力によって生成されるベクトル．プレイヤーが移動したいと思っている方向を表す）への射影ベクトルの大きさを制限しています．このことについては後々詳しい説明をしますが，つまりプレイヤーの速度自体は設定された制限速度を超えうる，ということです．

<img width="350", alt="all vectors", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/bf9641c1-68f3-cf03-2a55-6dc2b47062e1.png">

【図2】各ベクトルの関係．図に示した通り，摩擦ベクトルはプレイヤーベクトルと逆向きのベクトルである．また，$V_{\rm player, fric.} = V_{\rm player} + V_{\rm fric.}$ である．加算ベクトルと射影ベクトル，$V_{\rm player, fric. // V_{\rm input}}$ の間には $|V_{\rm add}| = |V_{\rm max}| - |V_{\rm player, fric. // V_{\rm input}}|$ が（基本的には）成り立つ．なお，入力ベクトルが長く描かれているが，実際は入力ベクトルは単位ベクトルとして扱う．

<img width="350", alt="projection", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/ea777403-adcf-338e-2b94-398fe7a9e1fe.png">

【図3】プレイヤーの速度の入力ベクトルへの射影のようす．このキャラクターコントローラでは，図中の $|V_{\rm player, next}|\cos{\alpha}$ を制限する．なお入力ベクトルは，たとえば W（前進）のみを押した場合は (front, right) = (1, 0)， W と D（右方向）を押した場合は $(1/\sqrt{2}, 1/\sqrt{2})$，W と D と A（左）を押した場合は (1, 0) といったように定義する．

## 具体例と仕組みの詳細
　それでは，このキャラクターコントローラが実際にどのように動作するのかと，各ベクトルがどのように計算されるかなどをご説明します．

### 1. 地上で，静止状態から直進する
　まずは，静止状態から正面方向に移動する場合を考えます．
　静止状態ではプレイヤーベクトルの大きさは 0 であるため，摩擦ベクトルの大きさも 0 です（計算方法は後述）．射影ベクトルも当然大きさ 0 のベクトルとなります．
　次に，加算ベクトルを計算します．加算ベクトルの向きは入力ベクトル $V_{\rm input}$ と同じです．加算ベクトルの大きさは，地上での加速度の大きさを $a_{\rm ground}$ ，フレーム時間（次フレームとの時間差）を $\Delta t$ として，以下のように定められます．

```math
\begin{array}{l}
\Delta v = |V_{\rm max, ground}| - |V_{\rm player, fric. // V_{\rm input}}| \\
A = a_{\rm ground}  \Delta t 
\end{array} \\
としたとき，\\
|V_{\rm add}| = \left\{
\begin{array}{ll}
0 & (\Delta v \leq 0) \\
\Delta v & (0 < \Delta v \leq A) \\
A & (A < \Delta v)
\end{array}
\right.
```

　数式では若干わかりにくいかもしれませんが，要するに加算ベクトルの大きさは，次のフレームでのプレイヤーベクトルの入力ベクトル方向成分が地上での最大速度を超過しないように計算されているということです．これが，先述した「このキャラクターコントローラではプレイヤーの速度の大きさを直接制限するのではなく...」の部分に該当します．
　最後に，計算された加算ベクトルと $V_{\rm player, fric}$ の和を次のフレームでのプレイヤーベクトルとして処理は終了です．図で表すと，以下の図4のようになります．

<img width="350", alt="static_front", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/a7df1e20-8d95-96ce-8d23-d3b9b6258da3.png">

【図4】静止状態から正面方向への移動キーを押したときの，加算ベクトル $V_{\rm add}$ と次のフレームでのプレイヤーベクトル $V_{\rm player, next}$．地上での最大速度 $|V_{\rm max, ground}|$ が，地上での加速度の大きさ $a_{\rm ground}$ と比較して十分大きい場合，単に $V_{\rm player, next} = V_{\rm add}$ となる．なお，$Z, X$ は空間の座標を示し，$front, right$ はプレイヤーの向きを基準とした座標を表す．

### 2. 地上で，正面方向にある程度加速している状態から移動方向を変える
　次に，正面方向にある程度加速している状態（$|V_{\rm player}| \leq |V_{\rm max, ground}|$）から，W と D を押して斜め右方向に急に方向を変える場合を考えます．
　はじめに摩擦ベクトルを計算して，摩擦ベクトルとプレイヤーベクトルの和 $V_{\rm player, fric.}$ を得ます．ここで，摩擦ベクトルの大きさは，摩擦による加速度の大きさを $a_{\rm fric.}$，フレーム時間を $\Delta t$ として以下のように計算されます．

```math
D = a_{\rm fric.} \Delta t \\
としたとき，\\
|V_{\rm fric.}| = \left\{
\begin{array}{ll}
|V_{\rm player}| & (0 \leq |V_{\rm player}| \leq D) \\
D & (D < |V_{\rm player}|)
\end{array}
\right.
```

　次に，入力ベクトルへの射影ベクトル $V_{\rm player, fric. // V_{\rm input}}$ を得ます．そして，その射影ベクトルの大きさから加算ベクトルを計算するのですが，45度ずれたベクトルへの射影なので，当然射影ベクトルの大きさは最大速度よりも小さくなり，加算ベクトルの大きさは 0 よりも大きくなります．結果として次のフレームでのプレイヤーベクトルは図5の5枚目のようになります．
　面白いことにと言うべきか，奇妙なことにと言うべきか，このキャラクターコントローラでは入力ベクトルの方向に急に移動方向を合わせるわけではありません．$a_{\rm fric.}$ を $|V_{\rm max, ground}|$ に対して小さめに設定すると，曲がる際に若干横滑りするような挙動を示します．
　また，「これでは，プレイヤーベクトルの大きさが一時的に地上での最大速度を超過するのでは？」と思われるかもしれませんが，その通り，容易に超過します．先述したとおり，射影ベクトルの大きさが地上での最大速度を超過しないようにできているだけなのです．

<img width="350", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/b4d2445b-a99a-19a1-f82e-e0a194dc8faf.png"> <img width="350", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/c0a3fc2c-d12d-31c8-1166-3ea73805e5b2.png"> 
<img width="350", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/fdaa39b2-db16-59a3-01d8-56043f528142.png"> <img width="350", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/86894322-9ab6-3e72-bb36-754be39e2200.png"> <img width="350", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/4dcbfffd-cfaa-1722-ceaa-f03a419c5bc3.png">

【図5】正面方向に十分加速した状態で，斜め右方向に急に入力ベクトルの向きを変えた場合の計算の流れ．急に向きを変えた場合，その後の数フレームでは$|V_{\rm max}| - |V_{\rm player, fric. // V_{\rm input}}|$ が $a_{\rm ground} \Delta t$ よりも大きくなり，$|V_{\rm add}| = a_{\rm ground} \Delta t$ となる場合がある．$V_{\rm player, next}$ は数フレーム経過することで徐々に入力ベクトルと同じ方向に近づいていく．

### 3. 空中で，正面方向に十分加速している状態から横方向移動キーを押す
　それではようやく，空中でのベクトルの計算方法を説明します．といっても，まだ視点移動は加えません．例として，正面方向に十分加速している状態（$|V_{\rm player}| = |V_{\rm max, ground}|$）から，D キー（右方向移動キー）を押した場合を考えます．
　空中では，次のフレームでのプレイヤーベクトルを計算するときに，摩擦ベクトルの大きさを常に 0 ，最大速度を空中での最大速度 $|V_{\rm max, air}|$ として，地上での場合と同様に計算します．
　さて例として挙げた状況では，プレイヤーベクトルは正面方向への成分しかもっていないので，射影ベクトルの大きさは 0 です．よって，射影ベクトルの大きさが最大速度以下となるため，0 以上の大きさを持つ右方向への加算ベクトルが得られます．結果として，次のフレームのプレイヤーベクトルは図6のようになります．ここでも，次のフレームでのプレイヤーベクトルの大きさは $|V_{\rm max, ground}|$ を超えます．しかし，視点移動を加えない限り，プレイヤーベクトルの大きさは $|V_{\rm max, ground}|$ をわずかに上回った値で頭打ちになります．

<img width="350", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/e0cbf8e1-c321-bc7a-4470-d2a6636e0ed6.png">

【図6】正面方向に十分加速している状態から右方向移動キーを押した際のベクトルの状態．空中で移動方向と直交する向きの移動キーを押しただけでは，その方向にわずかに移動するだけで移動方向を大きく変えることはできない．ちなみに，空中での最大速度 $|V_{\rm max, air}|$ は，$|V_{\rm max, ground}|$ に対して非常に小さい値に設定しておくのがよい．

### 4. 空中で，正面方向に十分加速した状態で横方向移動キーを押しながら視点移動を行う
　それでは，空中で移動キーと視点移動を組み合わせた場合を考えます．例として，正面方向に十分加速した状態（$|V_{\rm player}| = |V_{\rm max, ground}|$）でジャンプし，D キー（右方向移動キー）を押し，その後 D キーを押しつつ，右に視点移動していく場合を考えます．
　まず，空中で D キーを押すと図7のようにベクトルがわずかに傾き，次のフレームでのプレイヤーベクトル $V_{\rm player, next}$ の大きさは $|V_{\rm max, ground}|$ をわずかに超過します．これは，前項で説明した通りです．
　次に，この状態でマウスを使って視点移動を行います．すると，射影ベクトルの大きさが $|V_{\rm max, air}|$ を下回るため，0 より大きな大きさを持つ加速ベクトルを得られます．これを続けると，少しずつ小さな加算ベクトルが加算されていき，やがて大きな加速を得られることになります．これが，ストレイフの原理となります．
　ちなみに，基本的には視点移動がはやいほうがより大きな加速を得られますが，視点移動がはやすぎると加算ベクトルの入力ベクトル方向成分と，プレイヤーベクトルの入力ベクトル方向成分の符号が逆となり，減速してしまいます（図8）．そのため，より速く移動するには現在のプレイヤーベクトルの大きさに応じて適切なはやさの視点移動を行う必要があり，ここにプレイヤースキルが要求されます．

<img width="350", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/8f537ab3-53b6-5725-6d10-06dd07ee6e7f.png"> <img width="350", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/25c025e0-7d4b-4437-e129-e8a9300cff32.png">
<img width="350", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/4eee3230-809b-16f7-97ef-b344bb96dac1.png">


【図7】右方向移動キーを押しながら，視点を右に移動させたときの例．適切なはやさで移動方向キーと同じ方向に視点を移動させ続ける限り $|V_{\rm player, fric. // V_{\rm input}}| < |V_{\rm max, air}|$ となるため，加速し続ける．なお，$front, right$ は $f, r$ として示している．

<img width="350", src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2343218/e45f7a2c-d47c-3bad-1806-6ad41fbb7382.png">

【図8】視点移動がはやすぎる場合．図の例のように，$f'$（視点の向きを示す）が $V_{\rm player}'$ 追い越すような状態となると，加算ベクトルによって次のフレームでのプレイヤーベクトルは小さくなる（減速する）．

## 実装
　ここまで長々と説明してきましたが，コード自体は単純です．なお，下記に示したクラス，関数は次のフレームでのプレイヤーベクトルを計算するためだけのものです．着地判定や，```Rigid Body```のパラメータの変更などは，キャラクターコントローラの核となる別のクラスで行っています．
　入力および出力には```Vector2```型を使用しています．平面内の速度成分しか考えないため，このようにしています．

```c#
static public class PlayerControllerSub
{
    static readonly float maxSpeedOnTheGround = 10.0f;
    static readonly float maxSpeedInTheAir = 0.5f;
    static readonly float accelOnTheGround = 120.0f;
    static readonly float accelInTheAir = 100.0f;
    static readonly float frictionAccel = 70.0f;

    static public Vector2 GetNextPlayerVector(Vector2 playerVector, Vector2 inputVector, float dt, bool onTheGround = true)
    {
        var normalizedInputVector = inputVector.normalized;

        // 地上でのパラメータ
        var _frictionAccel = frictionAccel;
        var maxSpeed = maxSpeedOnTheGround;
        var accel = accelOnTheGround;

        // 空中でのパラメータ
        if (!onTheGround)
        {
            _frictionAccel = 0.0f;
            maxSpeed = maxSpeedInTheAir;
            accel = accelInTheAir;
        }

        // 摩擦ベクトルの大きさを計算する
        var magnitudeOfFriction = Clip(playerVector.magnitude, 0.0f, _frictionAccel * dt);

        // 摩擦ベクトルを得る（プレイヤーベクトルと逆向き）
        var frictionVector = playerVector.normalized * (-magnitudeOfFriction);

        // プレイヤーベクトルと摩擦ベクトルの和を得る．
        var playerVector_fric = playerVector + frictionVector;

        // 射影ベクトルの大きさを得る．
        var magnitudeOfProjection = Vector2.Dot(playerVector_fric, normalizedInputVector);

        // 加算ベクトルの大きさを得る．
        var magnitudeOfAddVector = Clip(maxSpeed - magnitudeOfProjection, 0.0f, accel * dt);

        // 加算ベクトルを得る．
        var addVector = normalizedInputVector * magnitudeOfAddVector;

        // 加算ベクトルと，プレイヤーベクトルと摩擦ベクトルを足したベクトルとの和を，次のフレームでのプレイヤーベクトルとする．
        return playerVector_fric + addVector;
    }

    static float Clip(float val, float min, float max)
    {
        if (val <= min) { return min; }

        if (val <= max) { return val; }

        return max;
    } 
}
```

## 最後に
　タイルを跳びまわる目的でなくても，ストレイフはキャラコンに奥深さと大きな自由度を与えるでしょう．そのため，ここで紹介した仕組みを持つキャラクターコントローラをご自身のプロジェクトに組み込むのは面白いことかもしれません．ぜひ検討してみてください．

## 参考

はじめに参考としたサイト．概要をつかむのに大いに役立った．

https://adrianb.io/2015/02/14/bunnyhop.html

最も参考になった動画．図を駆使したわかりやすい説明だけでなく，動画の中で```python```によるコードも示されている．

https://www.youtube.com/watch?v=v3zT3Z5apaM

実験用のプロジェクト．操作性をぜひ体感してみてください．
【追記】キャラクターコントローラを大幅に修正し，より簡潔なコードにまとめました．

https://github.com/hkosuda/StrafeController