今回はUnityのRPG等で操作キャラクターの移動と村人等と会話する時の処理を切り分ける機能を作ってみたいと思います。
(´Д`)
2019年初のUnity関連記事なので、一応決めた通り今年の顔文字を使ってみました(謎)
キャラクターは村人と会話する時は礼儀正しく村人の話を聞き終わるまで動かないようにします。
以前作成した記事で人と会話する機能を作りましたが、そちらは人の範囲内に入ったら一言コメントのようなものを表示し、範囲外に移動したらそのコメントを消すというものでした。
今回の場合は主人公が村人の範囲内に入った時にアクションボタンを押したら会話が開始され、その会話が終わるまで主人公は動かないようにします。
さらに村人の範囲内にいる間はアクションボタンを押せばいつでも同じ会話を聞くことが出来るようにしていきます。
今回の機能と以前作成したRPGメッセージ機能を搭載すると、
上のようになりました。
村人の会話範囲がかぶっていてもどちらかが会話していれば他の人は会話する事が出来ないようになっています。
主人公キャラクターの行動処理を分ける
主人公キャラクターにはCharacterControllerコンポーネントを取り付けコライダのサイズを調整し、AnimatorにAnimatorControllerを設定しIdle状態とWalk状態を作っておきます。
ここら辺は
等を参考に作成してください。
Idle→WalkへはアニメーションパラメータWalkSpeedが0.1より大きく、Walk→IdleへはWalkSpeedが0.1より小さくなった時に遷移するように作っておきます。
主人公キャラを選択し、インスペクタのTagにPlayerを設定しておきます。
主人公キャラクターにMoveAndTalkCharaスクリプトを作成し、取り付けます。
キャラクターの状態を列挙型で作成
キャラクターが通常移動している状態と村人と会話している状態を列挙型で作成し、キャラクターがその状態を保持出来るようにします。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class MoveAndTalkChara : MonoBehaviour { public enum CharacterState { normal, talk } private CharacterController characterController; private Animator animator; [SerializeField] private float walkSpeed = 1.25f; private Vector3 velocity; private CharacterState characterState; // Start is called before the first frame update void Start() { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); characterState = CharacterState.normal; } } |
主人公キャラクターの状態をcharacterStateに保持するようにします。
Updateメソッドで行動処理を分ける
キャラクターの移動や会話等の動作はUpdateメソッドで処理するので、そこで行動処理を分けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // Update is called once per frame void Update() { // 通常動作 if (characterState == CharacterState.normal) { Move(); // 会話中 } else if(characterState == CharacterState.talk) { Talk(); } velocity.y += Physics.gravity.y * Time.deltaTime; characterController.Move(velocity * Time.deltaTime); } |
キャラクターの状態がノーマルの時はMoveメソッド、会話の時はTalkメソッドを呼び出すようにします。
移動時も会話時もキャラクターにかかる重力は同じなのでどちらの処理でも実行されるようにします(空を飛ぶ行動処理も分ける場合は個別MoveやTalk等の行動処理メソッド内で重力計算をした方がいいかも)。
MoveとTalkメソッド
UpdateメソッドでMoveとTalkメソッドを呼び出しているので、その中身を記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 通常動作 void Move() { if (characterController.isGrounded) { velocity = Vector3.zero; var input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); if (input.magnitude > 0f) { velocity = input.normalized * walkSpeed; animator.SetFloat("WalkSpeed", input.magnitude); transform.LookAt(transform.position + input.normalized); } else { animator.SetFloat("WalkSpeed", 0f); } } } // 会話中 void Talk() { } |
Moveは移動キーを押された時の移動処理を記述し、Talkメソッドは特に何もやる事がないので空にしています。
会話の終了は村人がいいと言うまで離さないという事で村人側から主人公の状態を変更します。
会話中に何らかの処理をしたい時はTalkメソッドに記述するようにします。
移動処理の詳細については、
を参照してください。
主人公の状態の取得と設定処理
主人公の状態を取得したり、状況に応じて切り替える必要があるので、その為の処理を記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 状態確認メソッド public CharacterState GetState() { return characterState; } // 状態変更メソッド public void SetState(CharacterState setState) { characterState = setState; if(characterState == CharacterState.talk) { velocity = Vector3.zero; animator.SetFloat("WalkSpeed", 0f); } } |
キャラクターの状態は列挙型のCharacterState型で取得出来るようにし、状態を設定する時もCharacterState型で設定します。
キャラクターの状態を設定する時に会話中に状態を変更したら移動速度であるvelocityを0、アニメーションパラメータ―のWalkSpeedを0にし、その場でIdleに設定したアニメーションが再生されるようにします。
これで主人公キャラクターのスクリプトが完成しました。
会話表示用UIを作成する
村人と会話中に表示するUIを作成します。
ヒエラルキー上で右クリック→UI→Panelを選択します。
ヒエラルキーでPanelを選択し、矢印をドラッグして会話を表示する領域の幅を調整します。
Panelを選択した状態で右クリック→UI→Textを選択し、Panelの子要素にTextを作成します。
実際の会話はこのTextに表示されるので、Panelより少し内側に文字列が表示されるようにTextを変更します。
TextのサイズはPanelのサイズに応じて変更し、上下左右に10のマージンを設定します。
会話は左上から表示したいので、Alignmentで左上を設定します。
会話ウインドウは初めは表示させない為、Canvasを選択しインスペクタで名前の横のチェックを外し非アクティブの状態にしておきます。
これで会話ウインドウが出来上がりました。
会話する村人の作成
主人公キャラクターが出来たので、会話相手である村人を作成します。
村人のモデルはEthanを使うことにします。
Ethanをヒエラルキー上にドラッグ&ドロップしたら、名前をVillagerに変更し、右クリック→Create Emptyを選択し、名前をTalkAreaとします。
TalkAreaを選択し、インスペクタのAdd ComponentからPhysics→SphereColliderを取り付けIs Triggerにチェックをして物理的に当たらないようにし、Radiusを調整して会話範囲を設定します。
村人の会話用スクリプトの作成
TalkAreaには新しくVillagerTalkスクリプトのを作成し取り付けます。
実際の会話表示機能は
で作成したものをVillagerTalkスクリプトに修正と追加をして使用します。
まずはVillagerTalkスクリプトに主人公と会話を開始する処理を記述します。
フィールド等の作成
まずはフィールド等の記述です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class VillagerTalk : MonoBehaviour { // 会話のUI [SerializeField] private GameObject talkUI; // 主人公キャラクター操作スクリプト [SerializeField] private MoveAndTalkChara moveAndTalkChara; // 現在会話中かどうか private bool isTalk; } |
talkUIには先ほど作成した会話表示UIのCanvasを指定します。
主人公キャラクターの状態を取得したりする為、インスペクタで主人公操作スクリプトを設定します。
現在村人と会話中かどうかをisTalkで判断します。
isTalkは主人公操作スクリプトのGetStateで主人公の状態がTalk状態か判断すればいらないかもしれません。
主人公が会話範囲にいる時の処理
次に主人公が村人の会話範囲内にいる時の処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | private void OnTriggerStay(Collider other) { if(other.tag == "Player") { // 他の人と会話している場合は何もしない if (moveAndTalkChara.GetState() == MoveAndTalkChara.CharacterState.talk) { return; } // Actionに割り当てたキーで会話開始と終了 if (Input.GetButtonDown("Action")) { if (!isTalk) { Debug.Log("会話"); // 会話リセットメソッドを読んで会話を初期化する InitializeTalk(); // 1フレームに何度もキー押し判定されないようにリセット Input.ResetInputAxes(); } } } } |
主人公が村人の会話範囲内にいる時はいつでも会話を開始出来るようにする為、OnTriggerEnterではなくOnTriggerStayを使ってマイフレーム主人公が範囲内にいることを検知出来るようにします。
範囲内にいるコライダのタグがPlayerだった時にその後の処理を行います。
主人公キャラクターが会話状態の時は新しく会話は出来ないものとし、returnでその後の処理は行いません。
主人公が会話状態でない時にInputManagerで設定したActionに対応するボタンを押した時に会話を開始します。
わたくしの場合はPS3コントローラーの〇ボタンをActionに割り当てました。
InputManagerの設定に関しては、
等を参照してください。
会話を開始する時はInitializeTalkメソッド(この後作成します)を呼び出し会話の初期化処理を実行します。
その後Input.ResetInputAxesメソッドを呼び次フレームではキーの判定を解除します。
なぜこの処理が必要かというと、OnTriggerStayはマイフレーム呼ばれる為、Input.GetButtonDown(“Action”)のボタンの判定が複数フレームに渡って判断されてしまう可能性がある為です。
その為、ボタンの判定をした場合は一旦Input.ResetInputAxesメソッドを呼んでボタンの判定をリセットしています(ボタンを押しっぱなしの場合は次フレームは軸の入力が0になり、その次のフレームでは軸の入力が判定されます)。
ただしPS3コントローラー等のジョイスティックの斜め判定は問題ないんですが、キーボードの矢印キーで上と右を同時押しして斜め移動していた場合はうまく動作しませんでした(わたくしの環境のせいかも?)。
キーボードの斜め押しでジャンプキーを押した時もこの問題(この場合はジャンプしない)が発生するので、今回とは別の所に問題があるのかも?
会話初期化処理
会話を開始する時に一旦状態をリセットする為のメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 | // 会話を初期化する void InitializeTalk() { isOneMessage = false; isEndMessage = false; moveAndTalkChara.SetState(MoveAndTalkChara.CharacterState.talk); talkUI.SetActive(true); isTalk = true; } |
isOneMessageとisEndMessageは先ほどのリンク先のRPGメッセージを表示する記事のフィールドです。
それ以外は会話用UIのアクティブ化、会話をする時の初期化処理をしているだけです。
以前のRPGのメッセージ機能を使う場合の改造点
以前作成した
で作成した記事を今回のVillagerTalkスクリプトに組み込む場合はいくつか改造する必要があります。
まずは村人を複数設置する場合に固定の会話では困るので、インスペクタで村人それぞれの会話を設定出来るようにします。
会話内容をtalkMessageフィールドとして保持し、[SerializeField]アトリビュートでインスペクタに表示し、[TextArea(1, 10)]アトリビュートを付け最低1行で最大10行のテキストエリアとします。
会話内容が多い場合は10の部分を変更して行数を多くしておきます。
1 2 3 4 5 6 7 8 9 10 | [SerializeField] [TextArea(1, 10)] private string talkMessage = "初期メッセージ"; void Start() { messageText.text = ""; SetMessage(talkMessage); } |
StartメソッドではSetMessageメソッドの呼び出し時の引数にtalkMessageを渡します。
次に、Updateメソッド内の修正部分です。
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 | void Update() { // メッセージが終わっていない、または設定されていない、または会話が開始されていない if (isEndMessage || message == null || !isTalk) { return; } // 1回に表示するメッセージを表示していない if (!isOneMessage) { // その他処理 // メッセージ表示中にマウスの左ボタンを押したら一括表示 if (Input.GetButtonDown("Action")) { //その他処理 Input.ResetInputAxes(); } // 1回に表示するメッセージを表示した } else { // その他処理 // マウスクリックされたら次の文字表示処理 if (Input.GetButtonDown("Action")) { // その他処理 // メッセージが全部表示されていたら通常 if (nowTextNum >= message.Length) { nowTextNum = 0; isEndMessage = true; Debug.Log("通常"); talkUI.SetActive(false); isTalk = false; moveAndTalkChara.SetState(MoveAndTalkChara.CharacterState.normal); } Input.ResetInputAxes(); } } } |
中身の元の機能のままでマウス操作だった部分をActionボタンの割り当てに変えたり、マイフレームの判断を避ける為にInput.ResetInputAxesメソッドを追記したりしています。
会話が終了した時は主人公の状態をノーマル状態に変更しています。
なので、村人は自分の会話が終了したら主人公の状態を変えるので、村人は会話が終了するまで主人公を逃さないという事になりますね。(´Д`)
Villagerの子要素のTalkAreaのインスペクタは
のようになります。
TalkUIには会話用UIの親であるCanvasを指定し、MoveAndTalkCharaには主人公のMoveAndTalkCharaスクリプト、MessageTextには会話用UIのTextを設定します。
MessageTextはTalkUIの階層から探しても良さそうですが、面倒なのでこれでいいかな・・・・(´Д`)
これで会話する村人が出来ました。
村人を増やす
村人であるVillagerが出来たので、Villagerを選択した状態でCtrl+Dキーを押して複製し、マテリアルを変更して別の村人を作成します。
村人のモデルを変更する場合は空のゲームオブジェクトを使って親を空のゲームオブジェクトにして村人を作成しておくと便利かもしれません。
別の村人を作成したら配置します。
上のように村人の会話範囲になっています。
これで機能が完成しました。
終わりに
久しぶりの記事という事で、機能の説明の仕方を忘れ、ますます分かり辛い説明になってきたような気がしますね。(^_^;)
記事を続けて書けば少しは解消されるとは思いますが、なかなか新しい記事が登場しないので・・・・(´Д`)