今回は村の中に村人を配置し、村の中を移動をする機能を作成していきたいと思います。
前回は村を作成しました。
ユニティちゃんのRPGを作ってみようの他の記事は
から見ることが出来ます。
村人用のキャラクターを配置する
まずは村人に使用する3Dキャラクターを用意します。
Ctrl+9キーを押してAsset Storeウインドウを開きカテゴリの3D、キャラクター、ヒューマノイドにチェックを入れ、価格で無料アセットにチェックを入れます。
出てきたCharacter Elfを今回は村人の3Dモデルとして使用します。
Villageゲームオブジェクトを選択した状態で右クリックからCreate Emptyを選択し、名前をVillagersとします。
TransformのPosition、RotationのXYZは全て0にしておきます。
Villagersを選択した状態で右クリックからCreate Emptyを選択し、名前をVillagerとします。
VillagerのPosition、RotationのXYZも全て0にしておきます。
Villager以下が一人の村人を表していて、その子要素のモデルを変更するだけで他の村人を作成することが出来るようにしていきます。
Assets/Character_Elf/Mesh/Elf_MeshをVillagerにドラッグ&ドロップし子要素にします。
Elf_Meshを選択した状態でF2キーを押し名前をElfにします。
Elfを選択した状態で右クリックからUnpack Prefab Completelyを選択しプレハブとのリンクを解除します。
ヒエラルキーに配置したElfのマテリアルはデフォルトのマテリアルになっているので変更します。
Elfの子要素のBody_Meshを選択し、インスペクタのMaterialsのElement0にAssets/Character_Elf/MaterialsにあるCharacter_Elfマテリアルを設定します。
現在までの階層は
上のようになっています。
Animator Controllerの取り付け
Elfゲームオブジェクトを選択し、Animatorコンポーネントが取り付けられていなかったら、インスペクタのAdd ComponentからMiscellaneous→Animatorを選択し取り付け、Apply Root Motionのチェックを外します。
Assets/RPG/Animatorsフォルダに移動し、右クリックからCreate→Animator Controllerを選択し、名前をElfとします。
ElfのAnimator Controllerを選択した状態でAnimatorウインドウを見て、Idle状態、Walk状態を作成し、それぞれ遷移するようにします。
Idle状態にはスタンダードアセットのHumanoidIdle、Walk状態にはスタンダードアセットのHumanoidWalkのアニメーションを設定します。
作り方はユニティちゃんのAnimator Controllerを作った時と同じなので割愛します。(-_-)
アニメーションパラメータにFloat型のSpeedを作成します。
Idle→WalkはHas Exit Timeのチェックを外しSpeedがGreaterで0.1
Walk→IdleはHas Exit Timeのチェックを外しSpeedがLessで0.1
という条件にします。
村人の移動機能
村人を動かす場合は村の中のいくつかのポイントを自動巡回するような感じにしたいです。
その為、CharacterControllerの移動機能を使って移動させると歩いている途中に壁があっても目的地にただ移動するだけなので、壁を回避させるスクリプトを追加しない限り壁にぶつかってしまいます。
そこで今回はNavigation機能を使って村人が指定した巡回ポイントを周るようにしていきます。
Navigation機能については
や
も参照してみてください。
村人の移動出来る場所をBakeする
まずは村人が移動出来る場所を設定します。
UnityのメニューのWindowからAI→Navigationを選択します。
VillageGroundゲームオブジェクトのインスペクタで名前の横のStaticにチェックが入っているのを確認したらNavigationウインドウを見ます。
Bakeタブを押してエージェント(NavMesh上を動くキャラクター)の移動条件を設定します。
上のような感じで設定しました。
設定が出来たら下のBakeボタンを押してNavMeshをベイクします。
ベイクが終わると、VillageGroundの地形上でエージェントが移動できる場所が色分けされます。
村人にNavMeshAgentを取り付け巡回させる
村人にNavMeshAgentコンポーネントを取り付けてNavMesh上を動くようにします。
Villagerゲームオブジェクトを選択し、インスペクタのAdd ComponentからNavigation→NavMeshAgentを選択し取り付けます。
NavMeshAgentの最大移動速度であるSpeedを1.3にし、Radiusに0.3とHeightに1.8を指定して障害物を避ける半径と高さを指定します。
これでVillagerにはNavMesh上を移動出来る移動機能が取り付けられました。
巡回ポイントの作成
村人が巡回するポイントを作成していきます。
Villageゲームオブジェクトを選択した状態で右クリックからCreate Emptyを選択し名前をPatrolPointsParentとし、インスペクタでTransform、RotationのXYZを全て0にします。
PatrolPointsParentを選択した状態で右クリックからCreate Emptyを選択し、名前をPoint0とし、シーンビューで巡回する位置に移動させます。
Point0を選択しCtrl+Dキーを押して複製し、名前をPoint1にし次の巡回する位置に移動させます。
同じように複数の巡回ポイントを作ります。
Point0~Point5までをCtrlキーを押しながら全て選択し、インスペクタの名前の横のアイコンを変更します。
するとシーンビュー上でPoint0~Point5までのゲームオブジェクトの位置がアイコンで表示されてわかりやすくなります。
巡回ポイントを周るスクリプトの作成
Villagerには移動機能が取り付けられたので、後はその機能をスクリプトから操作し移動させます。
Assets/RPG/Scriptsフォルダ内にVillagerScriptを作成しVillagerゲームオブジェクトに取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class VillagerScript : MonoBehaviour { public enum State { Wait, Walk } // 目的地 private Vector3 destination; // 巡回する位置の親 [SerializeField] private Transform patrolPointsParent = null; // 巡回する位置 private Transform[] patrolPositions; // 次に巡回する位置 private int nowPatrolPosition = 0; // エージェント private NavMeshAgent navMeshAgent; // アニメーター private Animator animator; // 村人の状態 private State state; // 待機した時間 private float elapsedTime; // 待機する時間 [SerializeField] private float waitTime = 5f; void OnEnable() { } private void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); animator = GetComponentInChildren<Animator>(); // 巡回地点を設定 patrolPositions = new Transform[patrolPointsParent.transform.childCount]; for (int i = 0; i < patrolPointsParent.transform.childCount; i++) { patrolPositions[i] = patrolPointsParent.transform.GetChild(i); } SetState(State.Wait); } void Update() { // 見回り if (state == State.Walk) { // エージェントの潜在的な速さを設定 animator.SetFloat("Speed", navMeshAgent.desiredVelocity.magnitude); // 目的地に到着したかどうかの判定 if (navMeshAgent.remainingDistance < 0.1f) { SetState(State.Wait); } // 到着していたら一定時間待つ } else if (state == State.Wait) { elapsedTime += Time.deltaTime; // 待ち時間を越えたら次の目的地を設定 if (elapsedTime > waitTime) { SetState(State.Walk); } } } // 村人の状態変更 public void SetState(State state) { this.state = state; if (state == State.Wait) { elapsedTime = 0f; animator.SetFloat("Speed", 0f); } else if(state == State.Walk) { SetNextPosition(); navMeshAgent.SetDestination(GetDestination()); } } // 巡回地点を順に周る public void SetNextPosition() { SetDestination(patrolPositions[nowPatrolPosition].position); nowPatrolPosition++; if (nowPatrolPosition >= patrolPositions.Length) { nowPatrolPosition = 0; } } // 目的地を設定する public void SetDestination(Vector3 position) { destination = position; } // 目的地を取得する public Vector3 GetDestination() { return destination; } } |
インスペクタでpatrolPointsParentにヒエラルキーのPatrolPointsParentゲームオブジェクトをドラッグ&ドロップして設定すると、patrolPoints配列はPatrolPointsParentの子要素分の要素を確保して入れます。
Updateメソッドでは村人の状態に合わせて処理を分けていて、State.Walk状態の時はアニメーションパラメータSpeedにエージェントの潜在的なスピードを渡しています。
navMeshAgent.remainingDistanceで目的地とエージェントの距離を計算出来るので、距離が十分近くなったらSetStateメソッドを呼び出して村人の状態をState.Wait状態に変更します。
State.Wait状態だった時はその場で一定時間待たせる為に時間を計測し、時間がwaitTimeを越えたらState.Walk状態へと変更し次の巡回ポイントに移動させます。
時間の計測は
1 2 3 | elapsedTime += Time.deltaTime; |
で行っていてelapsedTimeには経過時間を足していきますが、Time.deltaTimeは前回のUpdateメソッドが呼ばれてからの経過時間なので、Updateメソッドが呼ばれるごとにTime.deltaTimeを足していくと今までの経過時間が計算されるという仕組みです。
SetStateメソッドでは引数で変更する状態を受け取り、その状態に応じて初期設定と村人の状態を変更する処理をしています。
NavMeshAgentの目的地を設定しているのは、
1 2 3 | navMeshAgent.SetDestination(GetDestination()); |
上の部分でGetDestinationで目的地であるVector3の値を得て、それをnavMeshAgent.SetDestinationメソッドの引数として渡します。
navMeshAgent.SetDestinationは引数で与えられた位置に目的地を設定し、移動を開始させます。
SetNextPositionメソッドは次の巡回ポイントを目的地に設定するメソッドで、SetDestinationは引数で受け取った位置を目的地にするメソッド、GetDestinationは現在の目的地を返すメソッドになります。
ユニティちゃんとの衝突
これでUnityを実行して動かすと村人であるエルフが巡回ポイントを順番に周っていきます。
しかしユニティちゃんをすり抜けてしまいます。
これはユニティちゃんは移動するのであらかじめベイクされたNavMesh上を動くだけの村人には無視されてしまうからです。
そこでユニティちゃんが村人に障害物として認識してもらえるようにします。
UnityChanゲームオブジェクトを選択しインスペクタのAdd CompoenntからNavigation→Nav Mesh Obstacleを取り付けます。
Nav Mesh Obstacleの設定で障害物として認識させる範囲をCharacterControllerのコライダのサイズと合わせます(合わせなくても避ける範囲を指定してください)。
NavMeshObstacleを取り付けるとNavMeshAgentを持つキャラクターがその部分を避けて通るようになります。
これで村人はユニティちゃんを避けて通るようになりましたが、ユニティちゃんが村人に衝突しようとすると村人をすり抜けてしまいます(村人が押されている感じになる)。
そこで村人にCapsule Colliderを取り付け衝突するようにします。
Villagerを選択し、インスペクタのAdd ComponentからPhysics→Capsule Colliderを選択し、コライダの範囲を調整します。
またAdd ComponentからPhysics→Rigidbodyを取り付けIs Kinematicにチェックを入れます。
これでユニティちゃんが村人と衝突するようになりました。
Unityを実行して確認してみましょう。
上のようになりました。
終わりに
今回は村人をナビゲーション機能を使って村を巡回させる機能を作成しました。
村人といってもまだ一人しかいませんが・・・・、次回の会話機能が完成したらあと一人追加してみようかと思います。
この作品はユニティちゃんライセンス条項の元に提供されています