見出し画像

Now in REALITY Tech #33 REALITYの配信中に視聴者にメッセージを常に表示できる機能を支える技術

こんにちは。Unityチームのizmです。
今回は最近のアップデートで新機能として追加された「配信中に視聴者にメッセージを常に表示できる機能」(以降、長いので配信ボードと呼びます)のUnityにおける実装の様子をお届けします。

配信ボードって何

こんな感じに、配信者が視聴者に対してテロップのように文字が表示できる機能です。
入室直後にどんな話をしているかを伝えることができるので、お題募集や告知などに使ってもらえたら嬉しいです。

このエントリでは配信ボードを作るときのちょっとした知見をまとめておきます。

開発編

こういった「相手側にも情報が同期する」機能を開発するときは念入りなマルチプレイテストをする必要があります。

Unityの場合は開発環境(UnityEditor上)の定番のアセットとしてParrelSyncがあります。個人的にはリアルタイムネットワークが絡む開発なら必須だと思っているくらいに愛用しているアセットです。

上の説明動画にあるように、同一のプロジェクトに対して複数個のUnityEditorを立ち上げて配信者と視聴者、あるいはコラボが有効か無効かどうかなど、いろいろな役割で情報同期が動いているかテストすることができます。

このParrelSyncはとても便利ですが、例えばプロジェクトの設定(特にユーザ認証情報)を配置する場所を気をつけておきましょう。

ParrelSyncの挙動は同一のローカル内のAssetsやProjectSettingsに対してSymbolicLinkを生成して実現しています。そのため

  • UserAccount情報をPlayerPrefsに書き込んでいる

  • UserAccount情報をPersistantDataPathに書き込んでいる

みたいなよくある実装のままだと、複数エディタで確認しようとしても同じアカウントでログインすることになってうまく動きません。

例えばifdefでUnityEditorだったらPersistantDataPath以下にプロジェクトの親フォルダ名(HogeとHoge_0、Hoge_1など)を流用した専用の保存場所を用意しておくと良いと思います。

/// <summary>
/// $Home/something/MyProject/のパスから"MyProject"を抜き出す
/// 例えばParrelSyncでクローンしたプロジェクトの場合は"MyProject_0"や"MyProject_1"が返ってくる
/// </summary>
/// <returns></returns>
private static string PickProjectDirectoryName()
{
     var directoryInfo = new DirectoryInfo(Application.dataPath).Parent;
     return directoryInfo != null ? directoryInfo.Name : string.Empty;
}

ParrelSyncを使う場合、同一PCから複数アカウントとしてAPIサーバにアクセスすることになるので、サーバエンジニアの人に「こういう感じに同一PCから何個も別アカウントを使ってアクセスするけどAPIサーバ側で検知して弾いたりしてます?」という確認をしておくと良いです。

UX編

ドラッグできるuGUIコンポーネント

配信ボードは配信者が位置を決められます。つまりドラッグ移動が可能です。これはUnityのInputField挙動と思い切り相性が悪いので、InputField左側にドラッグ移動用ボタンを取り付けて対応しました。
以下のようなスクリプトをuGUIのImageやButtonなどに貼り付けることで、ドラッグ移動が可能になります。

特に注意する点としてIDragHandlerのPointerの移動量はCanvasのScalerFactorを参照する必要があります。
そうしないと指でドラッグしてても移動量が指の移動と一致しなくなるためです。(素朴なCanvas構成では起きませんが、入れ子のCanvasが関係する場合、必要になります)

    [RequireComponent(typeof(RectTransform))]
    public class DraggableUiObject : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
    {
        private RectTransform _rectTransform;
        private Canvas _rootCanvas;
        public bool Dragging { get; private set; } = false;

        private void Awake()
        {
            _rectTransform = GetComponent<RectTransform>();
        }

        public void OnDrag(PointerEventData e)
        {
            if (_rootCanvas == null)
            {
                _rootCanvas = GetComponentInParent<Canvas>().rootCanvas;
            }

            var dragDelta = new Vector2(e.delta.x, e.delta.y);
            // rootCanvasのscalerの値を使う
            // そうしないと指ドラッグ時にImageの位置が一致しない(指から離れていってしまう)
            var scale = _rootCanvas.scaleFactor;
            var anchoredPosition = _rectTransform.anchoredPosition;
            anchoredPosition += dragDelta / scale;
            _rectTransform.anchoredPosition = anchoredPosition;
        }

        public void OnBeginDrag(PointerEventData data)
        {
            Dragging = true;
        }

        public void OnEndDrag(PointerEventData data)
        {
            Dragging = false;
        }
    }

InputFieldタップ時にキーボードが出て欲しい

InputFieldをタップしたら(中身が空なら)まずキーボードが出てほしいですね。これはInputFieldのtextを調べて、空であれば以下のようにActivateすることでiOS,Androidでキーボードを表示させることができます。

inputField.Select();
inputField.ActivateInputField();

InputFieldは伸び縮みしてほしい

Unity標準のInputFieldは幅が固定です、しかし体験としては入力した文字の横幅に合わせて伸び縮みしてほしいです。
いくつかの方法がありますが、今回はLayoutUtility.GetPreferredWidthを使うことで正しそうな横幅を再計算させる、という手法を採用しました。

もしかするともっとお手軽な方法があるかもしれませんが、LayoutUtilityは破壊的な変更ではなく、レイアウトの計算だけを行うメソッドが生えているため、使い勝手が良いシチュエーションがあります。

var rectTransform = inputField.GetComponent<RectTransform>();
// 例:横は最大250(rect width)
var preferredWidth = Mathf.Min(LayoutUtility.GetPreferredWidth(rectTransform), 250);
rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, preferredWidth);

終わりに

文字が入力できて動かせて他の人と同期するInputFieldを作る。というユーザ体験を作るためには、こういった実装や知識が必要になりました。

世の中のUIやUXというのは、意外と挙動に反して面倒くさい実装が必要だったりするなあ…としみじみしつつ今回のエントリを終えようと思います。

REALITYでは「ユーザの体験向上のために面倒くさい実装を喜んで行う」タイプのエンジニアを募集しています。