ブログの女の子を作る #33 NavMeshで巡回してモーションする【Unity】
前回は、UnityのTerrain機能と各種アセットを使って「キャラを動かすための世界」を作りました。
これで「Fallout4の拠点の人みたいに、キャラが自動で移動して、移動先で何かのモーションをする」ための準備ができました。
既にモーションは購入済みですので、あとは「キャラが自動で移動」してくれればOK!
なんですが、AI的に動くプログラムをイチから作るのは大変そう。。。
ネットで調べて見ると、Unityには「ナビゲーションシステム(NavMesh)」という便利な機能があるらしい。これを使ってキャラクターを動かしてみます。
目次
- (1) 開発環境と作成したスクリプトについて
- (2) NavMesh(ナビメッシュ)で巡回させる
- (3) 目的地とモーションを決める
- (4) 「NavMesh」で動けるエリアを設定する
- (5) キャラに「NavMesh Agent」をアタッチする
- (6) キャラクターを10体つくる
- (7) パンツの色をランダムで
- (8) モーションをまとめた「Animator Controller」を作る
- (9) 「AgentMotion.cs」を作成する
- (10) 目的地の準備
- (11) 目的地をランダムに設定する
- (12) 目的地に着いたらモーションする
- (13) 次の目的地に向かって歩き出す
- (14) 次の目的地を向いてから歩き始める
- (15) 作成したスクリプト
- (16) 動画で確認してみる
- まとめ
(1) 開発環境と作成したスクリプトについて
動作環境
・Windows 10
・Unity 2018.4.1f1
・Intel Corei7 3770
・ASUS ROG-STRIX-RTX2060S-O8G-GAMING
・Meta Quest
・Oculus Link
作成したスクリプトについて
今回、プログラムを作成するにあたり、Unity公式をはじめ、たくさんのネット記事を参考にさせて頂きました。ありがとうございました!
今回作成したプログラムは、記事の後半にまとめて掲載してます。
とりあえず動くことを目標にしたので、例外処理も無く、ベタな作りになっていますが、よろしければご参考くださいね。
NavMeshを使うと、あらかじめ設定しておいた「メッシュ状のエリア」の上を「NavMesh Agentをアタッチしたキャラ」が自動的に動いてくれるようになります。
しかも、「NavMesh上の障害物」や「他のAgentキャラ」を良い感じに避けつつ、目的地へは最短ルートで移動してくれます。目的地を到着した時に、次の目的地を設定すれば、各ポイントを巡回する動きになりますね。
とても便利です!
(3) 目的地とモーションを決める
まずは、「目的地(移動先)」と「そこで実行するモーション」を決めておきましょう。
目的地とモーション
- ベンチ:座る
- はしご:登って降りる
- 家の前:しゃがむ
- 家の前:回転ジャンプする
- 家の前:ブレイクダンスする
- リビング:ヨガをする(5種類からランダム)
- 寝室:ベッドで寝る
行動範囲が狭い方が動作を確認しやすいので、今回は家の前と中だけにしてみました。
この7つを「NavMeshの目的地」に設定しつつ、目的地に着いた時は「決まったモーションを実行する」ようにプログラムすれば良さそうです。
次に、キャラが動ける範囲を「NavMesh」で設定します。
と言っても、基本的には「Navigation」タブで「Bake」を選択し、パラメータを確認した後で「Bake」ボタンを押すだけ。
これだけで、キャラクターが動ける箇所が「青いエリア」として設定されます。
おかしなところに移動するようであれば、パラメータの高さや角度を調整して、ベイクし直しましょう。
寝室の中もベイクされてますね。
自動的に動かしたいキャラに「NavMesh Agent」をアタッチします。
Inspectorの「Nav Mesh Agent」欄で Speed を設定できますが、今回はスクリプト内で制御してますので、基本的にはデフォルトのままです。
Unityのサンプルプログラムを見てみる
Unityのサンプルプログラムを見てみると、スクリプト内で「NavMeshAgentコンポーネント」を取得して、目的地の位置を goal.position から取得して、「destination」に設定していました。
これだけで、キャラクターが目的地に移動してくれるようになります。Unityのサンプルではこの部分ですね。
[MoveTo.cs]
// MoveTo.cs using UnityEngine; using System.Collections; public class MoveTo : MonoBehaviour { public Transform goal; void Start () { NavMeshAgent agent = GetComponent<NavMeshAgent>(); agent.destination = goal.position; } }
今回は、記事の中盤で「AgentMotion.cs」というスクリプトを作成し、キャラクターにアタッチしています。そのスクリプトの中で、目的地を決めたり、モーションを実行するなどの処理を実装していますよ。
(6) キャラクターを10体つくる
巡回させるキャラは1体では寂しいですので、10体くらい同時に動かしてみたいです。
どうやって10体にするか
これまでの記事で「6種類の服」を作りましたので、これで6体は確保です。
残り4体は、「靴と靴下の組み合わせを変える」と「ワイシャツとリボンを青にする」ことで行けそうです。
この10体のキャラをBlenderで作成して、「別々のオブジェクト」としてUnityにインポートしても良いのですが、かなり面倒そうですし、後から3Dモデルを修正したくなった場合の手間も増えそうです。同じキャラを一部流用しているだけですので、別々で作るのはやめておきます。
ベースを作って Instantiate() で増やす
今回は、「体、服、アクセサリーなどのオブジェクト」と「NavMeshAgentやMagica Cloth、その他必要なコンポーネント」をすべてセットした「ベースとなるキャラ」を1体だけ作成する方法にします。
これを管理しているのが「InitCharacter.cs」です。
このベースキャラを Instantiate() で複数のインスタンスを作る事でコピーし、InitChara() で初期設定した後、それぞれのオブジェクトを SetActive() で「個別にオン」することで、10体分のキャラクターを作成します。
オブジェクトをSetActive()でオン/オフする
SetActive() でオン/オフを切り替える際、一つ一つ指定するとロジックが長くなるので、メソッド引数で params を使い、オブジェクト名を複数渡せるメソッド SetObject() を作成しました。
[InitCharacter.cs]
// オブジェクトのactive設定 void SetObject(Transform obj, bool isActive, params string[] names) { for (int i = 0; i < names.Length; i++) { obj.Find(names[i]).gameObject.SetActive(isActive); } }
一人目のキャラの場合は、こんな感じで指定してます。
[InitCharacter.cs]
// キャラ設定1 private Transform SetChara1() { Transform chara = Instantiate(baseChara); //chara.GetComponent<IKAnimator>().enabled = true; // カメラを向く InitChara(chara, "chara1"); SetObject(chara, true, Define.HUKU_SK, Define.HUKU_YS, Define.HUKU_RB, Define.HUKU_KT, Define.HUKU_KTS, Define.MVD_SK, Define.MMC_SK); return chara; }
カンマ区切りで、複数の定数を引き渡しています。
Define.cs で定数値を管理する
定数値として、「HUKU_xx」で服のオブジェクト、「MVD_xx」や「MMC_xx」はMagica Clothの「Magica Virtual Deformer」や「Magica Mesh Cloth」コンポーネントを指定しています。
これらの定数値を管理するのが「Define.cs」です。こんな感じですね。
[Define.cs]
// オブジェクト名 public static readonly string HUKU_SK = "スカート"; public static readonly string HUKU_SK_C1 = "スカートC1"; public static readonly string HUKU_SK_C2 = "スカートC2"; public static readonly string HUKU_SK_C3 = "スカートC3"; public static readonly string HUKU_SK_L = "スカートL"; public static readonly string HUKU_PANTS = "パンツ";
別の方法として、オブジェクトやコンポーネントを「リソースファイルから追加する」こともできそうでしたが、オブジェクトの数が少ないのでこの方法を採用してます。
10人できました!
(7) パンツの色をランダムで
各キャラクターの服装は固定なので、何かランダムな要素が欲しいところです。
いまのキャラで変更できそうな箇所と言えばパンツ。パンツの色をランダムに設定することにしましょう。いまは白色ですので、あと3色くらい作ってみます。
マテリアルを作る
「Project -> Create -> Material」でマテリアルを3つ作成し、それぞれに「ピンク・青・黒のテクスチャ画像」を設定していきます。
「テクスチャの画像ファイル」は、Unityプロジェクトの「textureフォルダ」などにD&Dして、インポートしておきましょう。
テクスチャ設定は、「Inspector -> Color -> Lit Color, Alpha」でしたね。
マテリアルの「シェーダー設定」は、この記事の設定と同じにしてますよ。
スクリプトからマテリアルを使えるようにする
これで、元の白色と合わせると「4種類のマテリアル」が準備できました。
スクリプトで、
[InitCharacter.cs]
public class InitCharacter : MonoBehaviour { [SerializeField] private Transform baseChara; [SerializeField] private Material[] pantsMaterials; [SerializeField] private Material[] ribbonMaterials;
のように書くと、Inspector内で「どのオブジェクトを使うか」を指定するための「入力欄」が表示されるようになります。
Size に数値を入れてフォーカスアウト(別の入力欄に移動)すると、「Element 0 から始まる入力欄」が自動的に作られます。
スクリプトでは pantsMaterials変数 に Materialクラス を指定しています。「Project -> Assets」から先ほど作成したマテリアルをD&Dすることで、スクリプト内で使えるようになります。ちなみにですが、変数定義時に Material[] のように “[]”を付けることで、「複数のMaterialデータを持てる配列」として指定していますよ。
マテリアルを抽選リストを作る
Random.Range(int min, int max) を使うと、「min以上、max未満のint値」をランダムに取得できます。
このロジックだけでも「0が出たら白、1ならピンク」のようにできますが、このままでは「すべての色が同じ確率(25%)」になってしまいます。色によって「発生確率(重み)」を変えたいので、抽選テーブルを作ることにしました。
パンツの番号を「0:白、1:ピンク、2:青、3:黒」、それぞれの重みを「5、2、2、1」とした場合、抽選テーブルは「0、0、0、0、0、1、1、2、2、3」になります。
[InitCharacter.cs]
// パンツマテリアルの抽選リスト作成 void CreateLotPantsList() { // マテリアルの番号 int[] index = { 0, 1, 2, 3 }; // マテリアルの重み int[] omomi = { 5, // 白 2, // ピンク 2, // 青 1 // 黒 }; for (int i = 0; i < index.Length; i++) { for (int j = 0; j < omomi[i]; j++) { lotPantsList.Add(index[i]); } } }
2重ループで、パンツの番号を lotPantsList変数 に追加してますね。これで抽選テーブルができました。
マテリアルをランダムで決定する
あとは、「int index = lotPantsList[Random.Range(0, lotPantsList.Count)];」でマテリアルの番号を取得できます。これで、白色は50%、黒は10%の確率になりましたね。
[InitCharacter.cs]
// パンツ設定 int index = lotPantsList[Random.Range(0, lotPantsList.Count)]; chara.Find(Define.HUKU_PANTS).gameObject.GetComponent<Renderer>().sharedMaterial = pantsMaterials[index]; Material parbnMate = chara.Find(Define.HUKU_PANTS).gameObject.GetComponent<Renderer>().materials[1];
この方法はお手軽ですが、超レア(0.01%)とかを設定したい場合は、抽選テーブルの要素数が膨大になってしまうので、別の仕組みが良さそうです。
(8) モーションをまとめた「Animator Controller」を作る
次は、キャラクターが「モーションするための下準備」をしておきます。
今回は、UnityChanの「UnityChanActionCheck」を参考にさせて頂きつつ、「Junkai」という名前の「Animator Controller」を作りました。
作り方や各パラメータの意味は、このサイトを参考にさせて頂きました。
「Animator Controller」を作るポイント7つ
作成時のポイントです。
- 「Parameters」タブで、Int型の「motion」パラメータを作成する。
- オレンジ色のState「Walk(歩くモーション)」の周りに「他のState」を配置する。
これ以上、Stateの数が増えると見にくくなりそうなので、その時は別の方法を探す必要がありそう。
- 「motionパラメータの値が変わったら他のStateに遷移する」ように、Transitionを設定する。
例)Walk -> Jumpの場合、「Conditions」の条件で「motion、Equals、10」を設定する
- 「Walkに戻るTransition」に「ToWalk」タグを設定しておく。
使い道はあとで説明します。
- モーションをゆっくりにしたい場合、「Speed」の値を小さくする。
例)「しゃがむ(POSE17)」モーションは、ポーズだけなので一瞬で終わってしまう。Speedを「0.005」などにすることで、長時間ポーズを維持できる。
やや強引な方法ですが。
- 「モーション時の高さ(Y軸)」は、「Root Transform Position(Y)」の「Offset」で調整する。
「State」をダブルクリックすると、この設定画面に行けます。なお、「オブジェクトのtransform値」でも変更することが可能なんですが、ワープしたみたいな感じになるので止めました。
- 必要に応じて「Bake Into Pose」のチェックも入れてみる。
正直なところ、正しい使い方になってるかは分かりませんが、なんとかイメージした挙動になりました。
キャラに「Animator Controller」を設定する
最後に、キャラの「Inspector -> Animator -> Controller」に作成した Animator Controller「Junkai」をD&Dします。
(9) 「AgentMotion.cs」を作成する
ここまでの作業で、
・NavMeshのエリア設定
・Agentへのアタッチ
・キャラクター10体の確保
・Animator Controllerの作成
ができました。
次は、これらを使って、キャラクターの目的地やモーションを管理するメインのスクリプト「AgentMotion.cs」を作っていきます。このスクリプトは、ベースキャラにアタッチします。
(10) 目的地の準備
Unityのサンプルにもあった「agent.destination = goal.position;」を実行すると、キャラの目的地を設定できます。ここの「goal.position」では、Unityの世界に存在するオブジェクトの「位置」をVector3という型で指定しています。
Vector3について、この記事が分かりやすかった。
目的地の位置情報を設定する
目的地のオブジェクトを指定する方法は、パンツのマテリアルと同じです。
[AgentMotion.cs]
public class AgentMotion : MonoBehaviour { [SerializeField] private Transform[] motionPoints;
今回はマテリアルでは無いので、座標情報などを保持している Transform を使います。このデータの中に「position(Vector3の位置情報)」が入っている、という感じですね。
Inspectorでは、こんな感じに「目的地のオブジェクト」を指定しています。
指定する時は、HierarchyからD&Dです。
このオブジェクトは、ベンチやハシゴなど「既に存在する物(場所)」で良ければ、そのオブジェクトをそのまま指定します。
もし、「何もない場所」を目的地にしたい場合は、「Cubeなどの適当なオブジェクト」を配置します。
「非表示(Mesh Renderをオフ)」にすれば見えなくなるので、邪魔になることもありません。
(11) 目的地をランダムに設定する
目的地をランダムに設定するのも、パンツと同じ仕組みです。
目的地の抽選テーブルを作る
まず、CreateLotMotionsList() で「目的地の抽選テーブル(lotPointsList)」を作成します。
[AgentMotion.cs]
// 場所の抽選リスト作成 void CreateLotMotionsList() { // 場所の番号 int[] basho = { (int)PointIndex.Jump, (int)PointIndex.Shagamu, (int)PointIndex.Yoga, (int)PointIndex.Ladder, (int)PointIndex.Breakdance, (int)PointIndex.Bench, (int)PointIndex.Bed }; // 場所の重み int[] omomi = { 10, // ジャンプ 15, // しゃがむ 15, // ヨガ 5, // はしご 10, // ブレイクダンス 5, // ベンチ 3 // ベッド }; for (int i = 0; i < basho.Length; i++) { for (int j = 0; j < omomi[i]; j++) { lotPointsList.Add(basho[i]); } } }
lotPointsListは一回だけ作れば良いので、Update()ではなく「Start()」で CreateLotMotionsList() を実行して作ってますよ。
[AgentMotion.cs]
void Start() { agent = GetComponent<NavMeshAgent>(); animator = GetComponent<Animator>(); agent.speed = Define.INIT_SPEED; CreateLotMotionsList(); // 抽選リストを作成 SetDestByLot(); // 最初の目的地を設定 }
目的地の位置をランダムに決定する
次に200行目で、乱数を使って「目的地の番号」を取得します。
[AgentMotion.cs]
// 目的地をランダムに設定 void SetDestByLot() { destPoint = lotPointsList[Random.Range(0, lotPointsList.Count)]; Vector3 basho = motionPoints[destPoint].position; // 目的地付近で位置をずらす switch (destPoint) { // ジャンプ case (int)PointIndex.Jump: basho += OffsetPosition(2.0f); break; // しゃがむ case (int)PointIndex.Shagamu: basho += OffsetPosition(2.0f); break; // ヨガ case (int)PointIndex.Yoga: basho += OffsetPosition(2.0f); break; // ブレイクダンス case (int)PointIndex.Breakdance: basho += OffsetPosition(2.0f); break; } agent.destination = basho; }
更に、複数のキャラが「同じ位置」に移動してくると混雑しますので、208行目などで OffsetPosition() を実行し、少しだけ位置をずらしています。
[AgentMotion.cs]
// 位置をずらす Vector3 OffsetPosition(float offset) { return new Vector3(Random.Range(-offset, offset), 0.0f, Random.Range(-offset, offset)); }
最後に、219行目の「agent.destination = basho;」で、次の目的地を設定しています。
(12) 目的地に着いたらモーションする
ここまでの処理で、キャラが目的地に向かって歩くようになりましたので、次は、目的地に近づいたときにモーションをさせます。
目的地に近づいたかチェックする
Update() に書いた処理は「1フレームごとに実行」されます。目的地までの距離を1フレームごとにチェックすることで、目的地に近づいたかを判断できます。
[AgentMotion.cs]
void Update() { // 目的地に近づいた場合 if (!agent.pathPending && agent.remainingDistance < 0.5f) { // モーション実行と次の目的地を設定 SetMotionAndNextDest(); }
近づいた時に SetMotionAndNextDest() を実行することで、モーションを実行させます。
目的地で実行するモーションを定義する
「目的地の番号」は、常に「destPoint」変数に保存しています。この値でswitch文を分岐させることで、どんなモーションを実行するかを定義しています。
例えば、ジャンプの場合はこんな感じになります。
[AgentMotion.cs]
// モーション実行と次の目的地を設定 void SetMotionAndNextDest() { switch (destPoint) { // ジャンプ case (int)PointIndex.Jump: SetAnimNavOff(MotionIndex.Jump); SetDestByLot(); break; // しゃがむ case (int)PointIndex.Shagamu: SetAnimNavOff(MotionIndex.Shagamu); SetDestByLot(); break;
キャラの重なりを軽減する
ここで、モーション実行用メソッドが「SetAnimNavOn」と「SetAnimNavOff」の2種類あるのは、「agent.updatePosition」と「agent.updateRotation」の値を切り替えるためです。
[AgentMotion.cs]
// モーション実行(NavMeshオフ) void SetAnimNavOff(MotionIndex motion) { agent.speed = 0f; agent.updatePosition = false; agent.updateRotation = false; animator.SetInteger(Define.MOTION_PARAM, (int)motion); } // モーション実行(NavMeshオン) void SetAnimNavOn(MotionIndex motion) { agent.speed = 0f; agent.updatePosition = true; // キャラの重なり軽減 agent.updateRotation = true; animator.SetInteger(Define.MOTION_PARAM, (int)motion); }
「agent.updatePosition = false;」とすると、NavMeshによる位置制御が停止し、スクリプトで直接制御できるようになるんですが、キャラクターが複数いる場合、重なって表示されることが多かったので、それを軽減したい目的地では「agent.updatePosition = true;」を使うようにしています。
trueにすると、モーション中でも「他のキャラが近づいたら」押し出すような感じで、移動してくれるんですね。
いまの設定では、「ベンチ、ヨガ、ブレイクダンス、ベッド」は true、「ジャンプ、しゃがむ、はしご」は falseにしてます。重なる可能性が低いモーションは false にしてる感じですが、「true」で統一しても良かったのかも知れんません。
ただ、「1つの場所で、モーションするのは一人」の制御ができるのであれば、false にしてスクリプトで位置制御した方が、ずれる心配は無くなると思います。状況によって、使い分けするのが良さそうですね。
モーションを実行する
モーション実行は、先ほどのメソッドの「animator.SetInteger(Define.MOTION_PARAM, (int)motion);」の部分です。
「animator」変数には、Animator Controllerの「Junkai」が入ってます。ここの「motion」パラメータに 10 などの数値をセットすることで、それぞれのモーションの「State」に遷移することで、モーションが実行されます。10はジャンプでしたね。
モーションを実行した後は、112行目で「SetDestByLot();」を実行し、次の目的地を設定しています。
(13) 次の目的地に向かって歩き出す
モーションが終わったら、次の目的地に移動したいですね。
「ToWalk」タグでモーション終了を検知する
「モーションが終わったかどうか?」は、「いま実行しているTransitionが何か」で判断できます。
[AgentMotion.cs]
// モーションが終わった場合 AnimatorTransitionInfo aniTraInfo = animator.GetAnimatorTransitionInfo(0); if (aniTraInfo.IsUserName("ToWalk")) { agent.updateRotation = true; agent.speed = Define.INIT_SPEED; // 次の目的地をゆっくり向く IEnumerator mukuTugi = MukuTugi(); StartCoroutine(mukuTugi); if (aniTraInfo.nameHash == Animator.StringToHash("Ladder -> Walk")) { // はしごの場合 IEnumerator oriruHashigo = OriruHashigo(); StartCoroutine(oriruHashigo); } else { agent.updatePosition = true; } // 歩くモーションを設定 animator.SetInteger(Define.MOTION_PARAM, (int)Define.MotionIndex.Walk); }
ここで、45行目では「Animator Controller」のTransitionに設定しておいた「ToWalk」タグを使って判断しています。
それぞれのモーションから「歩く」のモーションに戻る場合、「Transitionの名前(”Jump -> Walk”など)」を直接使っても良いのですが、すべての名前を羅列するのが面倒だったので、タグを使いました。この方法なら「if (aniTraInfo.IsUserName(“ToWalk”))」だけで済むので楽ですね。
nameHashで個別に判断する
モーションが終わったときに、モーションごとに「個別な処理」をしたい場合は、nameHash を使います。
例えば、「はしごのモーションが終わった時」は、54行目のように「if (aniTraInfo.nameHash == Animator.StringToHash(“Ladder -> Walk”))」と書くことで、判断できますよ。
再び歩き出す
最後に「animator.SetInteger(Define.MOTION_PARAM, (int)Define.MotionIndex.Walk);」で歩くモーションを設定すれば完了です。
(14) 次の目的地を向いてから歩き始める
ここまでの処理で、次の目的地に向かって歩き出すんですが、モーションが終わった直後に「回転しながら歩く」ような不自然な挙動になってました。方向と速度によっては、後ろ向きに歩くように見える場合も。。。
これを回避するために、コルーチンという仕組みを使い、「次の目的地を向いてから」歩き始めるようにしてます。
[AgentMotion.cs]
// 次の目的地をゆっくり向く IEnumerator mukuTugi = MukuTugi(); StartCoroutine(mukuTugi);
コルーチンは、このサイトが分かりやすかったです。
DoRuby
1 Post
5 Users
26 Pockets
【C#/Unity】コルーチン(Coroutine)とは何なのか
コルーチンについて理解を深めるためにざっくりと調べてまとめてみました。
コルーチンのメソッドを定義する
ここでは、MukuTugi() という「IEnumeratorで定義されたメソッド」を呼んでます。これがコルーチンです。
[AgentMotion.cs]
//次の目的地を向く IEnumerator MukuTugi() { for (int i = 0; i < 30; i++) { // 次の目的地を向くQuaternionを取得 Quaternion targetRotation = Quaternion.LookRotation(agent.destination - transform.position, Vector3.up); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime); yield return null; } }
このコルーチンを実行すると、「MukuTugi()というメソッド内の処理を、1フレームごとに、”yield return null;”の行まで、少しずつ実行する」ようになります。実際には、for文の処理を「30フレームかけて」実行することになります。
別の言い方をすれば、「非同期的にメソッドを実行する」感じでしょうか。
同じ仕組みを使って、MukuShomen() でオブジェクトの正面を向いたり、OriruHashigo() でゆっくり下に移動したりしてます。便利ですね。
Quaternionが難しい
回転させる時の処理で、特に「Quaternion(クォータニオン)」が難しかった。。。
この辺りのサイトを参考にさせて頂きました。
とりあえず、乗算すると「角度を足せる」のは分かりましたよ。
(15) 作成したスクリプト
今回作成した3本のスクリプトです。
スクロールが長いので、「(16) 動画で確認してみる」をご覧になられる方はこちらからジャンプしてくださいね。
Define.cs
定数値を定義します。
namespace Amaotolog { /// <summary> /// 定義値クラス /// </summary> public static class Define { public static readonly float INIT_SPEED = 0.7f; // 初期速度 public static readonly string MOTION_PARAM = "motion"; // モーションパラメータ名 // 目的地の値 public enum PointIndex { Jump, Shagamu, Yoga, Ladder, Breakdance, Bench, Bed } // animation切り替え用のパラメータ値 public enum MotionIndex { Walk = 0, Jump = 10, Shagamu = 20, Yoga1 = 30, Yoga2 = 31, Yoga3 = 32, Yoga4 = 33, Yoga5 = 34, Yoga6 = 35, Ladder = 40, Breakdance = 50, Bench = 60, Bed = 70 } // オブジェクト名 public static readonly string HUKU_SK = "スカート"; public static readonly string HUKU_SK_C1 = "スカートC1"; public static readonly string HUKU_SK_C2 = "スカートC2"; public static readonly string HUKU_SK_C3 = "スカートC3"; public static readonly string HUKU_SK_L = "スカートL"; public static readonly string HUKU_PANTS = "パンツ"; public static readonly string HUKU_YS = "ワイシャツ"; public static readonly string HUKU_YS2 = "ワイシャツ2"; public static readonly string HUKU_RB = "リボン"; public static readonly string HUKU_RB2 = "リボン2"; public static readonly string HUKU_WP_U = "ワンピース上"; public static readonly string HUKU_WP_S = "ワンピース下"; public static readonly string HUKU_RB3 = "リボン3"; public static readonly string HUKU_WP_RB = "ワンピースリボン"; public static readonly string HUKU_KT = "靴"; public static readonly string HUKU_KT_B = "靴_B"; public static readonly string HUKU_KTS = "靴下"; public static readonly string HUKU_KTS_B = "靴下S_B"; // Magica Clothのコンポーネント名 public static readonly string MVD_SK = "Magica Virtual Deformer (スカート)"; public static readonly string MMC_SK = "Magica Mesh Cloth (スカート)"; public static readonly string MVD_SK_C1 = "Magica Virtual Deformer (スカートC1)"; public static readonly string MMC_SK_C1 = "Magica Mesh Cloth (スカートC1)"; public static readonly string MVD_SK_C2 = "Magica Virtual Deformer (スカートC2)"; public static readonly string MMC_SK_C2 = "Magica Mesh Cloth (スカートC2)"; public static readonly string MVD_SK_C3 = "Magica Virtual Deformer (スカートC3)"; public static readonly string MMC_SK_C3 = "Magica Mesh Cloth (スカートC3)"; public static readonly string MVD_SK_L = "Magica Virtual Deformer (スカートL)"; public static readonly string MMC_SK_L = "Magica Mesh Cloth (スカートL)"; public static readonly string MVD_WP_S = "Magica Virtual Deformer (ワンピース下)"; public static readonly string MMC_WP_S = "Magica Mesh Cloth (ワンピース下)"; } }
InitCharacter.cs
キャラクターの初期設定を行います。
using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; namespace Amaotolog { /// <summary> /// キャラクターの初期設定クラス /// </summary> public class InitCharacter : MonoBehaviour { [SerializeField] private Transform baseChara; [SerializeField] private Material[] pantsMaterials; [SerializeField] private Material[] ribbonMaterials; [SerializeField] private enum AgentFlg { on, off } [SerializeField] private AgentFlg agentFlg; List<int> lotPantsList = new List<int>(); void Start() { if (agentFlg == AgentFlg.on) { CreateLotPantsList(); // 抽選リストを作成 // キャラクター生成 SetChara1(); SetChara2(); SetChara3(); SetChara4(); SetChara5(); SetChara6(); SetChara7(); SetChara8(); SetChara9(); SetChara10(); baseChara.gameObject.SetActive(false); //ベースキャラは非表示 } else { // テストモード(キャラ単体) baseChara.GetComponent<UnityChan.IdleChanger>().enabled = true; // IdleChangerでポーズ変更 baseChara.GetComponent<IKAnimator>().enabled = true; // AKAnimatorをオン // NavMeshオフ baseChara.GetComponent<NavMeshAgent>().enabled = false; baseChara.GetComponent<AgentMotion>().enabled = false; baseChara.GetComponent<BoxCollider>().enabled = false; } } // キャラ設定の初期化 void InitChara(Transform chara, string charaName) { chara.name = charaName; // 名前 // 服設定 SetObject(chara, false, Define.HUKU_SK, Define.HUKU_SK_C1, Define.HUKU_SK_C2, Define.HUKU_SK_C3, Define.HUKU_SK_L, Define.HUKU_YS, Define.HUKU_YS2, Define.HUKU_RB, Define.HUKU_RB2, Define.HUKU_WP_U, Define.HUKU_WP_S, Define.HUKU_RB3, Define.HUKU_WP_RB, Define.HUKU_KT, Define.HUKU_KT_B, Define.HUKU_KTS, Define.HUKU_KTS_B); // Magica Cloth設定 SetObject(chara, false, Define.MVD_SK, Define.MMC_SK, Define.MVD_SK_C1, Define.MMC_SK_C1, Define.MVD_SK_C2, Define.MMC_SK_C2, Define.MVD_SK_C3, Define.MMC_SK_C3, Define.MVD_SK_L, Define.MMC_SK_L, Define.MVD_WP_S, Define.MMC_WP_S); // パンツ設定 int index = lotPantsList[Random.Range(0, lotPantsList.Count)]; chara.Find(Define.HUKU_PANTS).gameObject.GetComponent<Renderer>().sharedMaterial = pantsMaterials[index]; Material parbnMate = chara.Find(Define.HUKU_PANTS).gameObject.GetComponent<Renderer>().materials[1]; switch (index) { case 2: parbnMate.color = Color.blue; break; case 3: parbnMate.color = Color.black; break; } } // キャラ設定1 private Transform SetChara1() { Transform chara = Instantiate(baseChara); //chara.GetComponent<IKAnimator>().enabled = true; // カメラを向く InitChara(chara, "chara1"); SetObject(chara, true, Define.HUKU_SK, Define.HUKU_YS, Define.HUKU_RB, Define.HUKU_KT, Define.HUKU_KTS, Define.MVD_SK, Define.MMC_SK); return chara; } // キャラ設定2 private Transform SetChara2() { Transform chara = Instantiate(baseChara); InitChara(chara, "chara2"); SetObject(chara, true, Define.HUKU_SK_C1, Define.HUKU_YS, Define.HUKU_RB2, Define.HUKU_KT_B, Define.HUKU_KTS_B, Define.MVD_SK_C1, Define.MMC_SK_C1); return chara; } // キャラ設定3 private Transform SetChara3() { Transform chara = Instantiate(baseChara); InitChara(chara, "chara3"); SetObject(chara, true, Define.HUKU_SK_C2, Define.HUKU_YS, Define.HUKU_RB2, Define.HUKU_KT_B, Define.HUKU_KTS_B, Define.MVD_SK_C2, Define.MMC_SK_C2); return chara; } // キャラ設定4 private Transform SetChara4() { Transform chara = Instantiate(baseChara); InitChara(chara, "chara4"); SetObject(chara, true, Define.HUKU_SK_C3, Define.HUKU_YS, Define.HUKU_RB2, Define.HUKU_KT_B, Define.HUKU_KTS_B, Define.MVD_SK_C3, Define.MMC_SK_C3); return chara; } // キャラ設定5 private Transform SetChara5() { Transform chara = Instantiate(baseChara); InitChara(chara, "chara5"); SetObject(chara, true, Define.HUKU_SK_L, Define.HUKU_YS, Define.HUKU_RB2, Define.HUKU_KT_B, Define.HUKU_KTS_B, Define.MVD_SK_L, Define.MMC_SK_L); return chara; } // キャラ設定6 private Transform SetChara6() { Transform chara = Instantiate(baseChara); InitChara(chara, "chara6"); SetObject(chara, true, Define.HUKU_WP_U, Define.HUKU_WP_S, Define.HUKU_RB3, Define.HUKU_WP_RB, Define.HUKU_KT, Define.HUKU_KTS, Define.MVD_WP_S, Define.MMC_WP_S); return chara; } // キャラ設定7 private Transform SetChara7() { Transform chara = Instantiate(baseChara); InitChara(chara, "chara7"); SetObject(chara, true, Define.HUKU_SK_C3, Define.HUKU_YS2, Define.HUKU_RB, Define.HUKU_KT, Define.HUKU_KTS, Define.MVD_SK_C3, Define.MMC_SK_C3); chara.Find(Define.HUKU_RB).gameObject.GetComponent<Renderer>().sharedMaterial = ribbonMaterials[0]; return chara; } // キャラ設定8 private Transform SetChara8() { Transform chara = Instantiate(baseChara); InitChara(chara, "chara8"); SetObject(chara, true, Define.HUKU_SK_C1, Define.HUKU_YS2, Define.HUKU_RB, Define.HUKU_KT, Define.HUKU_KTS, Define.MVD_SK_C1, Define.MMC_SK_C1); chara.Find(Define.HUKU_RB).gameObject.GetComponent<Renderer>().sharedMaterial = ribbonMaterials[0]; return chara; } // キャラ設定9 private Transform SetChara9() { Transform chara = Instantiate(baseChara); InitChara(chara, "chara9"); SetObject(chara, true, Define.HUKU_SK_L, Define.HUKU_YS2, Define.HUKU_RB, Define.HUKU_KT, Define.HUKU_KTS, Define.MVD_SK_L, Define.MMC_SK_L); chara.Find(Define.HUKU_RB).gameObject.GetComponent<Renderer>().sharedMaterial = ribbonMaterials[0]; chara.Find(Define.HUKU_SK_L).gameObject.GetComponent<Renderer>().materials[1].color = Color.blue; return chara; } // キャラ設定10 private Transform SetChara10() { Transform chara = Instantiate(baseChara); InitChara(chara, "chara10"); SetObject(chara, true, Define.HUKU_WP_U, Define.HUKU_WP_S, Define.HUKU_RB3, Define.HUKU_WP_RB, Define.HUKU_KT_B, Define.HUKU_KTS_B, Define.MVD_WP_S, Define.MMC_WP_S); return chara; } // パンツマテリアルの抽選リスト作成 void CreateLotPantsList() { // マテリアルの番号 int[] index = { 0, 1, 2, 3 }; // マテリアルの重み int[] omomi = { 5, // 白 2, // ピンク 2, // 青 1 // 黒 }; for (int i = 0; i < index.Length; i++) { for (int j = 0; j < omomi[i]; j++) { lotPantsList.Add(index[i]); } } } // オブジェクトのactive設定 void SetObject(Transform obj, bool isActive, params string[] names) { for (int i = 0; i < names.Length; i++) { obj.Find(names[i]).gameObject.SetActive(isActive); } } } }
AgentMotion.cs
キャラの移動とモーションを実行します。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using static Amaotolog.Define; namespace Amaotolog { /// <summary> /// キャラの移動とモーションクラス /// </summary> [RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(Animator))] public class AgentMotion : MonoBehaviour { [SerializeField] private Transform[] motionPoints; private int destPoint = 0; List<int> lotPointsList = new List<int>(); private NavMeshAgent agent; private Animator animator; void Start() { agent = GetComponent<NavMeshAgent>(); animator = GetComponent<Animator>(); agent.speed = Define.INIT_SPEED; CreateLotMotionsList(); // 抽選リストを作成 SetDestByLot(); // 最初の目的地を設定 } void Update() { // 目的地に近づいた場合 if (!agent.pathPending && agent.remainingDistance < 0.5f) { // モーション実行と次の目的地を設定 SetMotionAndNextDest(); } // モーションが終わった場合 AnimatorTransitionInfo aniTraInfo = animator.GetAnimatorTransitionInfo(0); if (aniTraInfo.IsUserName("ToWalk")) { agent.updateRotation = true; agent.speed = Define.INIT_SPEED; // 次の目的地をゆっくり向く IEnumerator mukuTugi = MukuTugi(); StartCoroutine(mukuTugi); if (aniTraInfo.nameHash == Animator.StringToHash("Ladder -> Walk")) { // はしごの場合 IEnumerator oriruHashigo = OriruHashigo(); StartCoroutine(oriruHashigo); } else { agent.updatePosition = true; } // 歩くモーションを設定 animator.SetInteger(Define.MOTION_PARAM, (int)Define.MotionIndex.Walk); } } // 場所の抽選リスト作成 void CreateLotMotionsList() { // 場所の番号 int[] basho = { (int)PointIndex.Jump, (int)PointIndex.Shagamu, (int)PointIndex.Yoga, (int)PointIndex.Ladder, (int)PointIndex.Breakdance, (int)PointIndex.Bench, (int)PointIndex.Bed }; // 場所の重み int[] omomi = { 10, // ジャンプ 15, // しゃがむ 15, // ヨガ 5, // はしご 10, // ブレイクダンス 5, // ベンチ 3 // ベッド }; for (int i = 0; i < basho.Length; i++) { for (int j = 0; j < omomi[i]; j++) { lotPointsList.Add(basho[i]); } } } // モーション実行と次の目的地を設定 void SetMotionAndNextDest() { switch (destPoint) { // ジャンプ case (int)PointIndex.Jump: SetAnimNavOff(MotionIndex.Jump); SetDestByLot(); break; // しゃがむ case (int)PointIndex.Shagamu: SetAnimNavOff(MotionIndex.Shagamu); SetDestByLot(); break; // ヨガ case (int)PointIndex.Yoga: MotionYoga(); SetDestByLot(); break; // はしご case (int)PointIndex.Ladder: SetAnimNavOff(MotionIndex.Ladder); SetDestByLot(); break; // ブレイクダンス case (int)PointIndex.Breakdance: SetAnimNavOn(MotionIndex.Breakdance); SetDestByLot(); break; // ベンチに座る case (int)PointIndex.Bench: MotionBench(); SetDestByLot(); break; // ベッドで寝る case (int)PointIndex.Bed: // 正面をゆっくり向く IEnumerator mukuShomen = MukuShomen(); StartCoroutine(mukuShomen); SetAnimNavOn(MotionIndex.Bed); SetDestByLot(); break; } } // ベンチのモーション実行 void MotionBench() { // ベンチの正面をゆっくり向く IEnumerator mukuShomen = MukuShomen(); StartCoroutine(mukuShomen); SetAnimNavOn(MotionIndex.Bench); } // ヨガのモーション void MotionYoga() { MotionIndex[] motionList = { MotionIndex.Yoga2, MotionIndex.Yoga3, MotionIndex.Yoga4, MotionIndex.Yoga5, MotionIndex.Yoga6 }; SetAnimNavOn(motionList[Random.Range(0, motionList.Length)]); } // モーション実行(NavMeshオフ) void SetAnimNavOff(MotionIndex motion) { agent.speed = 0f; agent.updatePosition = false; agent.updateRotation = false; animator.SetInteger(Define.MOTION_PARAM, (int)motion); } // モーション実行(NavMeshオン) void SetAnimNavOn(MotionIndex motion) { agent.speed = 0f; agent.updatePosition = true; // キャラの重なり軽減 agent.updateRotation = true; animator.SetInteger(Define.MOTION_PARAM, (int)motion); } // 目的地をランダムに設定 void SetDestByLot() { destPoint = lotPointsList[Random.Range(0, lotPointsList.Count)]; Vector3 basho = motionPoints[destPoint].position; // 目的地付近で位置をずらす switch (destPoint) { // ジャンプ case (int)PointIndex.Jump: basho += OffsetPosition(2.0f); break; // しゃがむ case (int)PointIndex.Shagamu: basho += OffsetPosition(2.0f); break; // ヨガ case (int)PointIndex.Yoga: basho += OffsetPosition(2.0f); break; // ブレイクダンス case (int)PointIndex.Breakdance: basho += OffsetPosition(2.0f); break; } agent.destination = basho; } // 位置をずらす Vector3 OffsetPosition(float offset) { return new Vector3(Random.Range(-offset, offset), 0.0f, Random.Range(-offset, offset)); } // オブジェクトの正面を向く IEnumerator MukuShomen() { // オブジェクトの正面を向くQuaternionを取得 Quaternion targetRotation = transform.rotation * Quaternion.FromToRotation(transform.forward, motionPoints[destPoint].forward); for (int i = 0; i < 120; i++) { transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime); yield return null; } } //次の目的地を向く IEnumerator MukuTugi() { for (int i = 0; i < 30; i++) { // 次の目的地を向くQuaternionを取得 Quaternion targetRotation = Quaternion.LookRotation(agent.destination - transform.position, Vector3.up); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime); yield return null; } } // はしごを降りる IEnumerator OriruHashigo() { for (int i = 0; i < 20; i++) { // 下にゆっくり移動 transform.Translate(0, -0.01f, 0); yield return null; } // 移動してからNavMeshをオン agent.updatePosition = true; } } }
(16) 動画で確認してみる
今回作成したスクリプトを動かした時の動画です。
これまでの動画は「Unityの再生モード」を撮っていましたが、最後の長い動画は「ビルドしてexe実行」してみました。ビルドに時間がかかるので、毎回はできませんが、やはりビルドした方がFPSが上がるので、動作が滑らかですね。
ベンチに座るモーション
たまに座る場所がずれます。あと、ベンチの正面を向く速度をもう少し早くしたい。
しゃがむモーション
しゃがみポーズは「UnityちゃんのPOSE17」を使ってるんですが、足のめり込みがひどかったので、「Very Animation」というアセットで修正しています。
今回少し使っただけですが、とても使いやすいツールでした。いろんなポーズが簡単に作れるので、また試してみよう。
巡回して7つの目的地でモーションする
モーションしているところを見て回りました。Oculus Linkを使うと、実際にモーションしている場に行けるのが良いですね。いろんな角度から見ていると飽きません。
まとめ
キャラクターを「NavMesh」で巡回して、モーションしてみました。
ランダムで動かしているだけなんですが、「Fallout4の拠点の人」のような動きになったような気がします!
今回は家の中と前だけでしたが、次回は池の周りを散歩したりしてみたいですね。
コメント
トラックバックは利用できません。
コメント (0)
この記事へのコメントはありません。