プレイ中のゲームや気になるゲームとか

Blenderでブログの女の子を5.5頭身で作る #33 NavMeshで巡回してモーションする

 
前回は、UnityのTerrain機能と各種アセットを使って「キャラを動かすための世界」を作りました。
 
これで「Fallout4の拠点の人みたいに、キャラが自動で移動して、移動先で何かのモーションをする」ための準備ができました。
 
既にモーションは購入済みですので、あとは「キャラが自動で移動」してくれればOK!
なんですが、AI的に動くプログラムをイチから作るのは大変そう。。。
 
ネットで調べて見ると、Unityには「ナビゲーションシステム(NavMesh)」という便利な機能があるらしい。これを使ってキャラクターを動かしてみます。



目次

(1) 開発環境と作成したスクリプトについて

動作環境

・Windows 10
・Unity 2018.4.1f1
・Intel Corei7 3770
・ASUS ROG-STRIX-RTX2060S-O8G-GAMING
・Oculus Quest
・Oculus Link
 

作成したスクリプトについて

今回、プログラムを作成するにあたり、Unity公式をはじめ、たくさんのネット記事を参考にさせて頂きました。ありがとうございました!
 
今回作成したプログラムは、記事の後半にまとめて掲載してます。
 
とりあえず動くことを目標にしたので、例外処理も無く、ベタな作りになっていますが、よろしければご参考くださいね。
 

(2) NavMesh(ナビメッシュ)で巡回させる

NavMeshを使うと、あらかじめ設定しておいた「メッシュ状のエリア」の上を「NavMesh Agentをアタッチしたキャラ」が自動的に動いてくれるようになります。
 
しかも、「NavMesh上の障害物」や「他のAgentキャラ」を良い感じに避けつつ、目的地へは最短ルートで移動してくれます。目的地を到着した時に、次の目的地を設定すれば、各ポイントを巡回する動きになりますね。
 
とても便利です!
 

(3) 目的地とモーションを決める

まずは、「目的地(移動先)」と「そこで実行するモーション」を決めておきましょう。
 

目的地とモーション

 
  1. ベンチ:座る
  2. はしご:登って降りる
  3. 家の前:しゃがむ
  4. 家の前:回転ジャンプする
  5. 家の前:ブレイクダンスする
  6. リビング:ヨガをする(5種類からランダム)
  7. 寝室:ベッドで寝る
行動範囲が狭い方が動作を確認しやすいので、今回は家の前と中だけにしてみました。
 
この7つを「NavMeshの目的地」に設定しつつ、目的地に着いた時は「決まったモーションを実行する」ようにプログラムすれば良さそうです。
 


(4) 「NavMesh」で動けるエリアを設定する

次に、キャラが動ける範囲を「NavMesh」で設定します。
 
と言っても、基本的には「Navigation」タブで「Bake」を選択し、パラメータを確認した後で「Bake」ボタンを押すだけ。
これだけで、キャラクターが動ける箇所が「青いエリア」として設定されます。
 
おかしなところに移動するようであれば、パラメータの高さや角度を調整して、ベイクし直しましょう。
 
寝室の中もベイクされてますね。
 

(5) キャラに「NavMesh Agent」をアタッチする

自動的に動かしたいキャラに「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つ

作成時のポイントです。
 
  1. Parameters」タブで、Int型の「motion」パラメータを作成する。
  2. オレンジ色のState「Walk(歩くモーション)」の周りに「他のState」を配置する。
    これ以上、Stateの数が増えると見にくくなりそうなので、その時は別の方法を探す必要がありそう。
     
  3. motionパラメータの値が変わったら他のStateに遷移する」ように、Transitionを設定する。
    例)Walk -> Jumpの場合、「Conditions」の条件で「motion、Equals、10」を設定する
  4. Walkに戻るTransition」に「ToWalk」タグを設定しておく。

    使い道はあとで説明します。
     
  5. モーションをゆっくりにしたい場合、「Speed」の値を小さくする。
    例)「しゃがむ(POSE17)」モーションは、ポーズだけなので一瞬で終わってしまう。Speedを「0.005」などにすることで、長時間ポーズを維持できる。

    やや強引な方法ですが。
     
  6. モーション時の高さ(Y軸)」は、「Root Transform Position(Y)」の「Offset」で調整する。

    「State」をダブルクリックすると、この設定画面に行けます。なお、「オブジェクトのtransform値」でも変更することが可能なんですが、ワープしたみたいな感じになるので止めました。
     
  7. 必要に応じて「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);
 
コルーチンは、このサイトが分かりやすかったです。
 

コルーチンのメソッドを定義する

ここでは、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) 動画で確認してみる

今回作成したスクリプトを動かした時の動画です。
 
・目的地ごとの動画7本(モーションの確率は変えてます)
・Oculus Linkで実際に移動しながら目的地を見てまわるロングバージョンの動画1本
 
これまでの動画は「Unityの再生モード」を撮っていましたが、最後の長い動画は「ビルドしてexe実行」してみました。ビルドに時間がかかるので、毎回はできませんが、やはりビルドした方がFPSが上がるので、動作が滑らかですね。
 

ベンチに座るモーション

たまに座る場所がずれます。あと、ベンチの正面を向く速度をもう少し早くしたい。
 

はしごを登るモーション

この動画を見ていると、アクションゲームで「はしごの部位に手と足がリンクしている」ことがスゴイことだと気づきました。
 

しゃがむモーション

しゃがみポーズは「UnityちゃんのPOSE17」を使ってるんですが、足のめり込みがひどかったので、「Very Animation」というアセットで修正しています。
 
今回少し使っただけですが、とても使いやすいツールでした。いろんなポーズが簡単に作れるので、また試してみよう。
 

ジャンプするモーション

何気にカッコ良いジャンプです。
 

ブレイクダンスするモーション

これだけの人数でダンスすると面白い。
 

リビングでヨガをするモーション

5種類のヨガをランダムで実行させてます。
 

ベッドで寝るモーション

ベンチと同じく、位置調整が難しいですね。1キャラ限定にした方が良いかも。
 

巡回して7つの目的地でモーションする

モーションしているところを見て回りました。Oculus Linkを使うと、実際にモーションしている場に行けるのが良いですね。いろんな角度から見ていると飽きません。
 

まとめ

キャラクターを「NavMesh」で巡回して、モーションしてみました。
 
ランダムで動かしているだけなんですが、「Fallout4の拠点の人」のような動きになったような気がします!
 
今回は家の中と前だけでしたが、次回は池の周りを散歩したりしてみたいですね。
 
スポンサーリンク

コメント

  • トラックバックは利用できません。

  • コメント (0)

  1. この記事へのコメントはありません。

Blenderでブログの女の子を5.5頭身で作る #32 UnityのTerrainでキャラを動かす世界を作る

Blenderでブログの女の子を5.5頭身で作る #34 NavMeshで巡回してモーションする2(外を歩く)


最近のコメント

だーしゅ
IT関係のお仕事してます。
Oculus Quest+Unityが楽しい!

[当ブログについて]