チャット入力画面とチャットテキストの表示
プラス、バルーンはこんな動きをする予定
Unity で近くにいるキャラクターの発言だけを聞き取れるようにしつつ、画面外にいてもどこから声をかけられているかわかるような仕組みを模索中
— Simplestar (@lpcwstr) May 23, 2019
こういう言葉にならないイメージの動きをコードに書くの難易度高い
実装詳細のメモはこちら→https://t.co/qhXhjV3z3c pic.twitter.com/AnYsINKYtx
こちらの記事の続きです。
simplestar-tech.hatenablog.com
今回はチャット入力画面を作ってみましょう
UI を新たに作るので Canvas 以下を次のように配置
UI はこんな感じの見た目にします
この UI と接続するロジックを以下の通り記述します。
今回はコードの目的ごとにコメントを記入したよ。気になる処理は参考にどうぞ
using System.Collections.Generic; using Invector.vCharacterController; using Invector.vCharacterController.vActions; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; /// <summary> /// ゲーム内に InputField を出現させ、テキスト入力後に吹き出しGUIに配置 /// </summary> public class TextChatGUI : MonoBehaviour { #region UI Connection [SerializeField] GameObject panelTextChat; [SerializeField] InputField inputFieldTextChat; [SerializeField] Transform panelTextChatBalloons; #endregion #region Scene Components [SerializeField] VRMLoader vrmLoader; [SerializeField] Transform triggerActions; [SerializeField] Transform vrmCharacters; #endregion #region Assets [SerializeField] GameObject chatBalloonPrefab; #endregion UnityAction<string /*userUniqueId*/, string /*chatText*/> onEndEditMessage; void Start() { this.vrmLoader.onLoadVRM += this.OnLoadVRM; this.inputFieldTextChat.onEndEdit.AddListener(this.OnEndEditChatText); this.onEndEditMessage += this.OnSendMessage; // @debug 横着、オンライン通知イベントに OnSendMessage を接続予定 } void Update() { // panel が非表示のときにキーボードの T を押すと panel が出現 if (Input.GetKeyUp(KeyCode.T) && !this.panelTextChat.activeSelf) { var vInput = vrmCharacters.GetComponentInChildren<vThirdPersonInput>(); if (null != vInput) { SwitchTextChatMode(vInput, showInputFieldTextChat : true); } } // 重なりが自然になるよう z 座標の小さい順にレンダリングするための処理 if (this.panelTextChatBalloons.gameObject.activeSelf) { // 吹き出し UI の z 座標の大きい順に並べ替え var chatBalloons = new List<Transform>(); foreach (var chatBalloon in this.panelTextChatBalloons) { chatBalloons.Add(chatBalloon as Transform); } chatBalloons.Sort((a, b) => b.localPosition.z > a.localPosition.z ? 1 : -1); // 並べ替えた順序でヒエラルキーの順序を変更 int sibIndex = 0; foreach (var chatBallon in chatBalloons) { chatBallon.SetSiblingIndex(sibIndex++); } } } void OnLoadVRM(GameObject vrmRoot) { if (null == vrmRoot) { return; } // VRM インスタンスを指定 Transform 配下へ移動 vrmRoot.transform.SetParent(this.vrmCharacters); // Balloon の 3D 位置用の空オブジェクトを作成し VRM インスタンス配下へ移動 var textChatUIAnchor = new GameObject("TextChatUIAnchor", new System.Type[] { typeof(ChatBalloonLocater)}); textChatUIAnchor.transform.SetParent(vrmRoot.transform); // メッシュデータから身長(m)を計測 var heightMeasure = new MeshHeightMeasure(); var vrmHeight = heightMeasure.GetMeshHeight(vrmRoot); // 身長 + オフセット上に来るように配置 var heightOffset = 0.1f; textChatUIAnchor.transform.localPosition = new Vector3(0, vrmHeight + heightOffset, 0); // BalloonLocater が制御すべき Balloon を作成 + Locater に設定 var chatBalloonLocater = textChatUIAnchor.GetComponent<ChatBalloonLocater>(); var chatBalloon = Instantiate(this.chatBalloonPrefab, this.panelTextChatBalloons).GetComponent<RectTransform>(); chatBalloonLocater.balloonRectTransform = chatBalloon.GetComponent<RectTransform>(); // プレイヤー用の Balloon 名を 0 に設定 chatBalloon.name = "0"; this.playerTextChatBalloonName = chatBalloon.name; // 空文字列を初期テキストに設定してクリア UpdateBalloonText("", chatBalloon); } void OnEndEditChatText(string chatText) { var vInput = vrmCharacters.GetComponentInChildren<vThirdPersonInput>(); if (null != vInput) { SwitchTextChatMode(vInput, showInputFieldTextChat: false); } this.onEndEditMessage?.Invoke(this.playerTextChatBalloonName, chatText); } void SwitchTextChatMode(vThirdPersonInput vInput, bool showInputFieldTextChat) { panelTextChat.SetActive(showInputFieldTextChat); if (showInputFieldTextChat) { // テキスト入力欄にキャレットを配置 inputFieldTextChat.ActivateInputField(); } else { // 非表示後にテキストをクリア inputFieldTextChat.text = ""; } // プレイヤー入力のロック・ロック解除 vInput.lockInput = showInputFieldTextChat; foreach (var vTriggerActions in triggerActions.GetComponentsInChildren<vTriggerGenericAction>()) { vTriggerActions.actionInput.useInput = !vInput.lockInput; } // カーソル再表示とカーソルロック解除(なくてもチャットできる) // vInput.ShowCursor(vInput.lockInput); // vInput.LockCursor(vInput.lockInput); } void OnSendMessage(string userUniqueId, string chatText) { var chatBalloon = this.panelTextChatBalloons.Find($"{userUniqueId}")?.GetComponent<RectTransform>(); UpdateBalloonText(chatText, chatBalloon); } static void UpdateBalloonText(string chatText, RectTransform chatBalloon) { if (null == chatBalloon) { return; } // 空文字列の時だけ非表示 chatBalloon.GetComponent<Image>().enabled = 0 != chatText.Length; // 入力テキストが収まるよう表示幅を変更 var chatBallonText = chatBalloon.GetComponentInChildren<Text>(); chatBallonText.text = chatText; float margin = 20; chatBalloon.sizeDelta = new Vector2(chatBallonText.preferredWidth + margin, chatBalloon.sizeDelta.y); } string playerTextChatBalloonName = ""; }
インスペクタービューでは以下の通り UI と接続します
prefab はなんの変哲もない Text です。強いて言うならアンカーが左下という点
Balloon の配置は以下のクラスに担当させています。(ロジックの作成の様子は過去記事で既出)
using UnityEngine; /// <summary> /// キャラの頭の上の吹き出し位置を毎フレーム更新 /// </summary> public class ChatBalloonLocater : MonoBehaviour { #region UI Connection internal RectTransform balloonRectTransform; #endregion #region Scene Components new Camera camera; #endregion void Start() { this.camera = Camera.main; } void Update() { if (null == this.balloonRectTransform || !this.balloonRectTransform.gameObject.activeSelf) { return; } // 遠くなるほど小さく var distance = Vector3.Distance(this.transform.position, this.camera.transform.position); this.balloonRectTransform.localScale = Vector3.one * Mathf.Clamp01(3 / distance); // スクリーン座標が画面外に出る時は、画面内に納まるようにクランプ Vector3 screenPos = this.camera.WorldToScreenPoint(this.transform.position); var scaleOffset = 5.0f; var marginX = this.balloonRectTransform.rect.width / 2 * this.balloonRectTransform.localScale.x + scaleOffset; var marginY = this.balloonRectTransform.rect.height / 2 * this.balloonRectTransform.localScale.y + scaleOffset; var x = Mathf.Clamp(screenPos.x, marginX, Screen.width - marginX); var y = Mathf.Clamp(screenPos.y, marginY, Screen.height - marginY); var z = screenPos.z; this.balloonRectTransform.rotation = Quaternion.identity; // 3Dアンカーのカメラの前後判定 var dot = Vector3.Dot(this.camera.transform.forward, this.transform.position - this.camera.transform.position); var flag = Mathf.Sign(dot); // 3Dアンカーが背後にあるなら左右反転 if (0 > flag) { x = Screen.width - x; y = Screen.height - y; this.balloonRectTransform.rotation = Quaternion.Euler(0, 180, 0); } // ここまでの計算を RectTransform に反映 this.balloonRectTransform.position = new Vector3(x, y, z); } }
コレを動かすと次の通り
t キーで、テキストチャットウィンドウが下部に開き
— Simplestar (@lpcwstr) May 25, 2019
その間はまったく操作できなくなりつつ、日本語入力可能で、文字列長の幅調整入りつつ、身長に合わせて配置され、例のトラッキングするロジック乗せると、こんな絵になりました。#Unity #gamedev https://t.co/HOdw3NlE08 @YouTube pic.twitter.com/vqnQTJbOrL
次の記事では、オンライン通信部分を作ります。
simplestar-tech.hatenablog.com