Unityではしごの昇り降りを実装する

この記事はカバー株式会社 Advent Calendar 2023 17日目の記事になります。
カバー株式会社エンジニアのOです。よろしくお願いいたします。

前回の記事は @mk-cover による [「笑い声」を分析・検証してみる](https://qiita.com/mk-cover/items/e67c4f13b23edce603ea) です。こちらも是非ご覧ください。

## はじめに

この記事では、Unityでのキャラ制御におけるはしごの昇り降りを実装する際の話をします。

キャラがはしごに近づいたら、はしごを掴み、前後移動入力等ではしごを登ったり降りたりするというのがよくあるはしごまわりの挙動だと思います。

この記事では主に「前後移動入力等ではしごを登ったり降りたりする」あたりについてフォーカスし、簡単に実装してみてからブラッシュアップするまでの流れを紹介したいと思います。

ブラッシュアップでは結構面倒くさいことを色々やるのですが、なぜそれが必要なのかの説明のため、簡単に実装をしてからブラッシュアップする流れで書こうとなりました。少し長い内容になってしまったのですがご容赦ください。

## 簡単に実装する

まずはパッと思いつきそうな方法でさっと実装してみます。

### 環境

- Unity 2022.3.15f1

### 前準備

はしごの処理とあまり関係ないけど必要な前準備をさっと済ませます。
本当に最小構成でいいなら適当なHumanoidのAvatarが設定されたAnimatorが1個あれば大丈夫です。

- 3D (URP)のテンプレートを使ってUnityプロジェクトを作成
- キャラと景観が欲しいので [Starter Assets - ThirdPerson](https://assetstore.unity.com/packages/essentials/starter-assets-thirdperson-updates-in-new-charactercontroller-pa-196526) をインポート
- SampleScene に以下の Prefab を配置
  - Assets/StarterAssets/Environment/Prefabs/Environment_Prefab.prefab
  - Assets/ThirdPersonController/Prefabs/PlayerArmature.prefab
  - Assets/ThirdPersonController/Prefabs/PlayerFollowCamera.prefab
- PlayerFollowCamera の CinemachineVirtualCamera.Follow に PlayerArmature/PlayerCameraRoot を設定
- Main Camera に CinemachineBrain をアタッチ
- PlayerArmature から Animator 以外のコンポートをすべて削除
- 適当な AnimatorController を作成し PlayerArmature の Animator に設定
- Box を組み合わせて適当にはしごっぽいオブジェクトを作って PlayerArmature の前に配置する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/c64e1e9b-c5c7-cf7f-7473-6f036b0cb2f3.png)

これにて前準備は完了です。

### AnimationClip の準備

とりあえずはしごを登るモーションが欲しいので mixamo で ladder と検索して適当に拝借します。
今回は「Climbing Ladder」というモーションを使わせてもらいます。

![スクリーンショット 2023-12-14 185243.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/0933df89-2717-dfc5-ccb8-940027c534e9.jpeg)

ダウンロードした fbx を unity にインポートします。
インスペクタの Rig タブから AnimationType を Humanoid に変更します。
Animation タブで以下の画像のような2つの clip を作成します

![スクリーンショット 2023-12-14 193229.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/4276564c-44c2-12eb-0000-135bbedad085.png)

これにて AnimationClip の準備は完了です。

### AnimatorController を組む

前準備で作った AnimatorController で以下のようなステートを組みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/8318761a-318e-19d9-a6bc-e2dc57d43c15.png)

各ステートには以下のように AnimationClip を設定します。

- idle ステート: idle
- up ステート: climbing
- down ステート: climbing、Speed は -1

Transition はとりあえずすべて以下のように設定しておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/c51a3e4e-46fe-c8f1-e05e-4890c851703a.png)

各 Transition の Condition は以下のように設定します。

- idle -> up : InputY Greater 0.1
- idle -> down : InputY Less -0.1
- up -> idle : InputY Less 0.1
- up -> down : InputY Less -0.1
- down -> idle : InputY Greater -0.1
- down -> up : InputY Greater 0.1

これにて AnimatorController の準備は完了です。

### Animator制御スクリプトを書く

制御といいつつただ入力を Animator Parameter に反映するだけのスクリプトです。
以下のようなスクリプトを作って PlayerArmature にアタッチします。

```cs
using UnityEngine;

namespace Ladder
{
    public class LadderMovement : MonoBehaviour
    {
        [SerializeField] private Animator _animator;

        private static readonly int InputY = Animator.StringToHash("InputY");

        private void Update()
        {
            _animator.SetFloat(InputY, Input.GetAxis("Vertical"));
        }
    }
}
```

移動はアニメーションにまかせたいので Animator の Apply Root Motion を有効にします。

これにて制御まわりの準備は完了です。

### とりあえず動いた

![ladder_0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/185cddd5-503f-d88c-1ba5-3f7355315b58.gif)

### なんとかしたいポイント

- 待機状態になると必ず右手が上にきちゃうのをなんとかしたい
    ![ladder_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/75125810-7cc1-c4e8-a90e-dd80c39d8c4e.gif)
- 入力を断続的に連打すると変な動きになるのをなんとかしたい
    ![ladder_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/114d90d0-f5cd-9451-8734-43aca264fce6.gif)
- 踏ざんを踏んでない・掴んでないのをなんとかしたい


## なんとかする

なんとかしたいポイントをなんとかします。
主な工夫ポイントとしては以下の通りです。

- モーションは右手左手それぞれ用意
- AnimatorController で右手左手のどっちが上か管理
- 昇り降りの Transition は HasExitTime を有効化

### モーションは右手左手それぞれ用意

なんとかしたいポイントの上1つ目の対策として、左手を上に持ってこれるようにします。そのため、Unity の ModelImporter の機能を活用します。idle と climbing それぞれ右手左手用の AnimationClip を用意します。

- idle left : 左手が上の待機ポーズ
- idle right : 左手が上の待機ポーズ
- climbing left : 左手が上で始まる登りモーション（右手が上になるところまで）
- climbing right : 右手が上で始まる登りモーション（左手が上になるところまで）

![タイトルなし.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/1ea5c5ec-91e2-60e4-4894-843dca5ea0d0.jpeg)

これらを使ってAnimatorControllerを組みなおします。

### AnimatorController で右手左手のどっちが上か管理

以下のように AnimatorController を組みなおします。

![スクリーンショット 2023-12-14 222059.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/dcf13e7f-f98d-bd5c-f6fe-3ced7e851071.png)

どっちの手が上側か記録するための「LadderHand」という float パラメーターを追加します。
float なのは BlendTree でもこのパラメーターを使うためです。
1 が右手、-1 が左手が上にくる状態であるということにします。

各ステートは以下のようにパラメーター・AnimationClip を設定します。

- idle ステート: LadderHand を Parameter としたBlendTree
    - idle left : Threshold は -1
    - idle right : Threshold は 1
- up サブSM: 登りモーション格納用サブステートマシン
    - left ステート : climbing left
    - right ステート : climbing right
- down サブSM: 降りモーション格納用サブステートマシン
    - left ステート : climbing left、Speed は -1
    - right ステート : climbing right、Speed は -1

Transition は以下のように設定します。

- ルート SM 内の Transition : 
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/e449bd39-5857-8ba6-382c-69a887d50659.png)
- up サブ SM 内の Transition : 
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/e4db9073-016c-33c9-cb42-ee4eb9d8774b.png)
- down サブ SM 内の Transition : 
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/2c067bd8-1b6d-8e52-ce9e-a4ed138aa492.png)

また、up サブ SM, down サブ SM 内の left -> right, right -> left の Transition については Interruption Source を Next State にしておくとよりいい感じになります。

Transition Condition は以下のように設定します。

- ルートSM
    - idle -> up サブSM : InputY Greater 0.1
    - idle -> down サブSM : InputY Less -0.1
    - up サブSM ->  down サブSM : InputY Less -0.1
    - down サブSM -> up サブSM : InputY Greater 0.1
- up サブSM
    - Entry -> left : LadderHand Less -0.1
    - Entry -> right : LadderHand Greater 0.1
    - left -> right : InputY Greater 0.1
    - right -> left : InputY Greater 0.1
    - left -> Exit : InputY Less 0.1
    - right -> Exit : InputY Less 0.1
- down サブSM
    - Entry -> left : LadderHand Less -0.1
    - Entry -> right : LadderHand Greater 0.1
    - left -> right : InputY Less -0.1
    - right -> left : InputY Less -0.1
    - left -> Exit : InputY Greater -0.1
    - right -> Exit : InputY Greater -0.1

LadderHand を制御する StateMachineBehaviour も用意します。
特定のステートに入ったら LadderHand を指定した値に更新できるようにします。

```cs
using UnityEngine;

namespace Ladder
{
    public class LadderHandUpdater : StateMachineBehaviour
    {
        [SerializeField] private float _handValue = 1f;

        private static readonly int LadderHand = Animator.StringToHash("LadderHand");

        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            animator.SetFloat(LadderHand, _handValue);
        }
    }
}

```

LadderHandUpdater を以下のステートに設定します。

- up サブSM
    - left : HandValue 1
    - right : HandValue -1
- down サブSM
    - left : HandValue 1
    - right : HandValue -1


### 昇り降りの Transition は HasExitTime を有効化

これについては Transition の設定時で作業はすでに終わってます。
なぜ HasExitTime を有効化するといいのかについて簡単に説明します。
HasExitTime は有効化すると、そのステートのモーションを再生しきってから次のステートに遷移するようになります。HasExitTime が無効化されていると、モーションを再生しきる前に次のステートに遷移します（モーションは中断される）。
登る、降るモーションは中途半端なタイミングで中断されてしまうと中途半端な体勢から無理やり待機モーションに遷移してしまうのでモーションに違和感が生じてしまいます。
そのため、HasExitTime を有効化することで違和感が起きづらくしています。

### なんとかした結果

![ladder_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/f56c75c0-e9bc-3e2b-386c-7a1ff317059d.gif)

まぁまぁいい感じになったのではないでしょうか。

ただ「踏ざんを踏んでない・掴んでないのをなんとかしたい」については未対応です🥲
IKをつかえばどうとでもなりそうなので、今後の課題としてまたの機会に取り扱えたいなと思います。

## おわりに

Unity ではしごの昇り降りをいい感じに実装する話でした。
実はこちら先日リリースされましたホロアースのサンドボックス内のはしごで実際に使われている実装だったりします。

移動制御とかはしご検知とかも含めたりするとこんな感じの動きになります。

![ladder_he1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629143/a4f1403e-3b3a-9767-93d7-076ecf7556dd.gif)

ホロアースぜひ触ってみてください🖖

次回は @yt-cover さんの「UnityでC++クラスを扱いたい」です。お楽しみに。