simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Unity:オンラインVRMチャットサンプルの作り方2

チャット入力画面とチャットテキストの表示

f:id:simplestar_tech:20190526084537p:plain
最後まで読むとできるようになる絵

プラス、バルーンはこんな動きをする予定

こちらの記事の続きです。
simplestar-tech.hatenablog.com
今回はチャット入力画面を作ってみましょう

UI を新たに作るので Canvas 以下を次のように配置

f:id:simplestar_tech:20190526082741p:plain
チャットテキスト入力欄

UI はこんな感じの見た目にします

f:id:simplestar_tech:20190526082845p:plain
チャットテキスト入力欄の見た目

この 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 と接続します

f:id:simplestar_tech:20190526083136p:plain
TextChatGUIのインスペクタービュー

prefab はなんの変哲もない Text です。

f:id:simplestar_tech:20190526083356p:plain
ChatBalloonPrefab
強いて言うならアンカーが左下という点
f:id:simplestar_tech:20190526083457p:plain
Anchorタイプ

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);
    }
}

コレを動かすと次の通り

次の記事では、オンライン通信部分を作ります。
simplestar-tech.hatenablog.com