Unityのキャラクターをキーボード操作できるようになるまでにやったこと

# はじめに

本記事では、[VRM](https://vrm.dev/vrm/vrm_about/)フォーマットで作成されたキャラクターを、キーボード操作で動かせるようになるまでに必要なことなどをつらつらっとメモっています。

さらに、`NavMesh` 使って、自動巡回できるようにもしたい。

# 前提とする環境

* Windows 環境：Windows 10/11
* Linux 環境：Ubuntu 20.04 LTS
* Mac 環境
  * Intel
    * macOS Catalina ver.10.15.7 以降
  * Applesilicon

# キャラクタの3Dモデル

3Dモデル（VRM ファイル）は自作するか、入手しておく必要があります。

入手先としては、この辺りが参考になるかも。

https://hub.vroid.com/

※VRoid Hubは、3Dキャラクターのためのプラットフォームです。

# ステップ

1. Unityを起動する
1. [UniVRM](https://github.com/vrm-c/UniVRM/releases)をダウンロードする
1. [UnityにUniVRM unitypackageを Import する](https://vrm.dev/vrm/how_to_make_vrm/setup_unity/#unity-に-univrm-unitypackage-を-import)
1. [VRMファイルをインポートして、変換する](#VRMファイルをインポートして、変換する)
1. [キャラクタ操作スクリプトの設計をする](#キャラクタ操作スクリプトの設計をする)
1. [AnimationControllerを作成する](#AnimationControllerを作成する)
1. [Unityスクリプトを作成する](#Unityスクリプトを作成する)
1. [キャラクタに諸々アタッチして動作チェック](#キャラクタに諸々アタッチして動作チェック)

# VRMファイルをインポートして、変換する

Unity の Scenes 直下に、`Model` フォルダを作成して、VRMファイルを D&D します。

![スクリーンショット 2024-04-18 11.07.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/58450f5e-c7a1-e7fb-5842-7c73a0fda19a.png)

（上図はmitone.vrmというファイルを配置したもの）

`miton`をクリックして、Inspectorビューを開き、Migrate To Vm1 をチェックし、`Apply` をクリック。

![スクリーンショット 2024-04-18 11.08.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/cd5c61c0-1ea8-c0c0-f2c9-6721f5682031.png)

成功すると、キャラクタが見えるようになる！

![スクリーンショット 2024-04-18 11.09.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/24af5eb1-a0ce-80d1-d12c-38b1907d87ff.png)


# キャラクタ操作スクリプトの設計をする

キャラクタはキーボードで操作します。
操作方法は、以下です。

* 上下キー
  * 前進、後進する
* 左右キー
  * 方向を変更する
* スペースキー
  * ジャンプする

プログラムの処理フローは以下の流れです。

![スクリーンショット 2024-04-18 11.41.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/284a1c08-70e2-4cd3-0167-77090fe3e862.png)

キーボード入力は、Unityスクリプトがモニタリングします。そして、入力されたキー内容に応じて AnimationControllerの状態遷移とキャラクタの動き（Motion)を切り替えます。

speed_v、speed_h、jumpはそれぞれ、Animation Controllerへの入力パラメータです。

* speed_v(-1〜1の値)
  * 上下キー操作の結果：下は負の値、上は正の値
* speed_h(-1〜1の値)
  * 左右キー操作の結果：左は負の値、右は正の値
* jump(true/false)
  * スペースキー操作の結果：trueはジャンプし、falseはジャンプ解除する

# AnimationControllerを作成する

Unityの「Animation Controller」は、キャラクターやオブジェクトのアニメーション状態を管理し、遷移を制御するためのツールです。これを使うと、異なるアニメーションクリップ間で滑らかに遷移するための条件やルールを設定できます。

作り方は、プロジェクトビューで、コンテキストメニューから、`Create` -> `Animation Controller`をクリックです（下図）。

![スクリーンショット 2024-04-18 11.52.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/a509a89e-1b75-d20f-4f11-5c36d5d158ee.png)

## 状態設計と実装

ダブルクリックすると、下図のように状態が３つ見えます。

![スクリーンショット 2024-04-18 11.56.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/009f1ee5-809a-68fa-c522-ca8480acb37f.png)

今回は、下図のような状態遷移にします。

![スクリーンショット 2024-04-18 12.06.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/1900a0ec-3345-8f98-35d5-ac2611f726b6.png)

初期状態は、`Locomotion`で、ここで上下・前後移動の動きを実現する状態です。`Jump`は、キャラクタをジャンプさせる状態です。

Locomotionは、下図のように`From New Blend Tree`で作成します。

![スクリーンショット 2024-04-18 12.25.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/d27c5c64-1802-a5ae-fade-c80023c43e3f.png)

Jumpは、下図のように`Empty`で作成します。

![スクリーンショット 2024-04-18 12.26.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/fe77b948-676f-ea3c-87a6-f84fea4d5976.png)

次に、入力パラメータを設計内容に従って、それぞれ追加します。

そして、状態遷移トリガを追加します。

`Locomotion`：jumpがtrueの場合に、`Jump`に遷移
![スクリーンショット 2024-04-18 13.02.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/73babbb4-6151-291e-0a33-461aae937a84.png)

`Has Exit Time`のチェックを外す必要があります。（理由はまだわかっていません...）



`Jump`：jumpがfalseの場合に、`Locomotion`に遷移
![スクリーンショット 2024-04-18 13.04.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/8d1153a9-37dd-a378-3757-f09bc1fcc0c9.png)


## モーション設定

今回利用するモーションとしては、ユニティちゃんを利用します。

https://unity-chan.com/download/releaseNote.php?id=UnityChan

最新のものをダウンロードして、Import します。

### Jump のモーション設定

Jumpを選択して、`Motion`をクリックして、`JUMP0`を選択します。

![スクリーンショット 2024-04-18 12.28.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/7383c060-22c0-1424-c5e6-208009d134f8.png)

### Locomotion のモーション設定

Locomotionの状態では、上下・前後の移動のモーションを入力パラメータに応じて切り替えます。

そのための設定が、下図のものです。

![スクリーンショット 2024-04-18 12.35.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/2b30f50b-0839-f2d3-be12-78b9a7f59193.png)

設定方法の詳細はこちらが参考になります。

https://light11.hatenadiary.com/entry/2019/04/18/224048

# Unityスクリプトを作成する

今回作成したUnityスクリプトは以下のものです。


```C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CharacterOperation : MonoBehaviour
{

	// Animator コンポーネント
	private Animator animator;

	// 設定したフラグの名前
	private const string key_isJump = "jump";
	private Rigidbody my_body;

	// Start is called before the first frame update
	void Start()
	{
		this.animator = GetComponent<Animator>();
		my_body = GetComponent<Rigidbody>();

	}
	float speed = 2.0f;
	void Update()
	{
		var h = Input.GetAxis("Horizontal");
		var v = Input.GetAxis("Vertical");
		//Debug.Log("h=" + h + " v=" + v);
		if (Mathf.Abs(h) > 0.01f || Mathf.Abs(v) > 0.01f)
		{
			if (v < 0)
			{
				v = v / 1.5f;
			}
			var localVelocity = new Vector3(0, 0, v);
			var worldVelocity = transform.TransformDirection(localVelocity);
			my_body.velocity = worldVelocity * speed;

			//向き操作
			if (h > 0)
			{
				transform.Rotate(0, 0.1f, 0);  // 右に0.1度回転
			}
			else if (h < 0)
			{
				transform.Rotate(0, -0.1f, 0);  // 左に0.1度回転
			}
			//アニメーション操作
			//Debug.Log("x=" + my_body.velocity.x + " z=" + my_body.velocity.z);
			animator.SetFloat("speed_v", v);
			animator.SetFloat("speed_h", h);

		}
		if (Input.GetKey(KeyCode.Space))
		{
			this.animator.SetBool(key_isJump, true);
		}
		else
		{
			this.animator.SetBool(key_isJump, false);
		}
	}
}

```

# キャラクタに諸々アタッチして動作チェック

まずは、キャラクタをシーンビューに D&D します。

![スクリーンショット 2024-04-18 12.41.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/22f5e31b-de4e-c07b-52f6-b3de4789c7a5.png)

次に、物理オブジェクト RigidBody を割り当てます。この時、ContraintsのFreeze ROtation のXとZにチェックを入れます。（これやっとかないと移動する時に倒れちゃうので）


さらに、コライダー(Capsule Collider)を割り当てます。この時、キャラクタのサイズに合わせてパラメータ調整します。

![スクリーンショット 2024-04-18 12.45.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/6a728a4e-e5da-7c0d-c013-8625047f4cab.png)

![スクリーンショット 2024-04-18 12.45.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/514f297e-60a0-38e8-a525-97639db9ace8.png)

次に、先ほど作成したスクリプトをアタッチします。

![スクリーンショット 2024-04-18 12.45.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/dbe0ccde-ec00-1c70-152e-cc880c8fc90c.png)

最後に、AnimationControllerを割り当て終了です。

![スクリーンショット 2024-04-18 12.46.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/b8cf4ad2-fc25-5f59-2e8a-a442a74abfb7.png)

# デモ

こんな感じで動きます。

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

# 自動巡回させるためのコード

Unity の [NavMesh](https://docs.unity3d.com/ja/2019.4/Manual/nav-BuildingNavMesh.html) を使うと簡単に実現できます。

## インストール

Unityのパッケージマネージャで NavMesh をインストールする必要があります。

## 方法

* 移動領域に、NavMeshSurfaceアタッチして Bake する
* 障害物を置きたい場合は、NavMeshObstacleをアタッチして Curve にチェックをいれる
* 移動させたいオブジェクトに NavMeshAgentをアタッチ。Speedで移動速度をコントロールできる


## プログラム

```C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine.AI;
using UnityEngine;

public class CharacterOperation : MonoBehaviour
{

	// Animator コンポーネント
	private Animator animator;
	public Transform[] goals;
	private NavMeshAgent agent;

	// 設定したフラグの名前
	private const string key_isJump = "jump";
	private Rigidbody my_body;
	private int curr_goal_inx = 0;

	// Start is called before the first frame update
	void Start()
	{
		this.animator = GetComponent<Animator>();
		my_body = GetComponent<Rigidbody>();
		agent = this.GetComponent<NavMeshAgent>();
		if (agent == null)
        {
			throw new System.Exception("can not find Agent");
        }
		agent.destination = goals[0].position;

	}
	private void Update()
    {
		Vector3 velocity = agent.velocity;

		Vector3 localVelocity = transform.InverseTransformDirection(velocity);

		// ローカル速度のz成分とx成分をアニメーターに設定
		animator.SetFloat("speed_v", localVelocity.z);
		animator.SetFloat("speed_h", localVelocity.x);
		if (agent.remainingDistance < 0.5f)
        {
			curr_goal_inx++;
			if (curr_goal_inx >= this.goals.Length)
            {
				curr_goal_inx = 0;
            }
			agent.destination = goals[curr_goal_inx].position;
		}
	}
}

```

https://www.youtube.com/watch?v=S-poV6RVOOk

# 最後に

ここで利用させていただきましたVRMファイルは、[株式会社プラスプラス](https://www.plusplus.jp/)様からご提供いただいたものを使用しております。ご協力いただき誠にありがとうございます。

https://www.plusplus.jp/