Weed.nagoya - 挑戦&物欲プログラマー

HoloLensの出張授業をする会社で、教材を開発しています

PrefabSpawnManagerを使ったHoloLensのシェアリング(Sharing)のしかた - 磁界学習アプリをシェアリングに対応させました

f:id:weed_7777:20180510224212p:plain


磁界学習アプリを操作している様子

かねてからHoloLensを使った出張授業で使ってきた磁界学習アプリを、シェアリングに対応させました。以下では、その過程でわかったことや学んだことを説明します。

磁界学習アプリのシェアリングの概要

上の動画にあるように、シェアリングに参加しているプレイヤーは各自1つの棒磁石を自由に動かすことができます。それとは別に多数の方位磁針が格子状に並んでおり、すべての棒磁石の影響を反映して方向や明るさが変わります。

方位磁針1つ1つはシェアリングしていません。方位磁針を生成する基準点だけをシェアリングしています。具体的には、

  1. シェアリングに新たにプレイヤーが参加した時点で、方位磁針を生成する基準点だけを新しいプレイヤーの空間に生成します
  2. 基準点のStart()メソッドが多数の方位磁針を格子状に生成します
  3. 各方位磁針には、複数の棒磁石の影響を計算して向きや明るさを決定するUpdate()メソッドが組み込まれています
  4. プレイヤーが棒磁石を動かすと、それぞれのプレイヤーの別個のHoloLens空間の中で方位磁針がUpdate()されます

つまり、実際にシェアリングしているのは棒磁石と基準点だけで、しかも基準点は生成するだけなので、動的にシェアリングしているのは棒磁石だけです。

まず、PrefabSpawnManagerを設置する

HoloLensのシェアリングでプレハブを生成したいときは、MRTKにそのためのスクリプトが入っています。PrefabSpawnManagerスクリプトです(使用はあまり勧めないというアドバイスd_yama(@dy_karous)さんからいただきましたので、使用にはご注意ください)。このスクリプトを使うと、シェアリングをしている各HoloLensの世界に同時にプレハブを生成することができます。位置・向きは自動的に同期されます。

1. PrefabSpawnManagerスクリプトをアタッチ

まずSharingStageオブジェクトをつくり、PrefabSpawnManagerスクリプトをにアタッチします。SharingStageオブジェクトにアタッチするのは、シーンが遷移したときにPrefabSpawnManagerへの参照を取れなくなるからです。

なお、何も指定しないとプレハブから生成されたオブジェクトはSharingオブジェクトの子オブジェクトになりますが、DontDestoryOnLoadに入るため、シーン内のオブジェクトからGameObject.Find()などができなくなってしまいます。今回は各方位磁針が棒磁石のN極、S極をすべて取得する必要があったため、棒磁石をDontDestroyOnLoadに入れるわけにはいきませんでした。

2. SyncSpawnedMagnetスクリプトの作成

「Magnet」の部分は適宜読み替えて下さい。SyncSpawnedObjectクラスを継承して作成します。正直なところ今回は中身はどうでもいいです。プレハブを生成するときにクラス名が必要になるため、作成しています。

//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
//

using HoloToolkit.Sharing.Spawning;
using HoloToolkit.Sharing.SyncModel;

namespace HoloToolkit.Sharing.Tests
{
    /// <summary>
    /// Class that demonstrates a custom class using sync model attributes.
    /// </summary>
    [SyncDataClass]
    public class SyncSpawnedMagnet : SyncSpawnedObject
    {
        [SyncData]
        public SyncBool showsMagneticForceLines;  // 適当
    }
}

3. SyncSpawnedMagnetクラスの登録

PrefabSpawnManagerコンポーネントに、先ほど作成したSyncSpawnedMagnetクラスを登録します。

f:id:weed_7777:20180502133125p:plain

4. MagnetSpawnerスクリプトの作成

コードを書きます。

using System;
using HoloToolkit.Sharing;
using HoloToolkit.Sharing.Spawning;
using UnityEngine;

namespace FeelPhysics.HoloMagnet36
{
    /// <summary>
    /// 自分がシェアリングサーバに参加したときに、磁石を生成する
    /// </summary>
    public class BarMagnetSpawner : MonoBehaviour
    {
        /// <summary>
        /// PrefabSpawnManagerへの参照を取るためのフィールド
        /// </summary>
        [SerializeField]
        private PrefabSpawnManager spawnManager;

        [SerializeField]
        [Tooltip("どのオブジェクトにぶら下がるか")]
        private Transform spawnParentTransform;

        /// <summary>
        /// ユーザーID
        /// </summary>
        private long myUserId;

        private void Awake()
        {
            // spawnParentTransformが設定されていなければ、spawnされたオブジェクトは自身にぶらさがる
            if (this.spawnParentTransform == null)
            {
                this.spawnParentTransform = transform;
            }
        }

        /// <summary>
        /// シェアリングサーバーに接続するのを待つ
        /// </summary>
        private void Start()
        {
            SharingStage.Instance.SharingManagerConnected += this.Connected;
        }

        /// <summary>
        /// シェアリングサーバに接続すると呼ばれる
        /// ここからさらにユーザが参加するのを待つ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Connected(object sender, System.EventArgs e)
        {
            SharingStage.Instance.SharingManagerConnected -= this.Connected;

            SharingStage.Instance.SessionUsersTracker.UserJoined += this.UserJoinedSession;
            SharingStage.Instance.SessionUsersTracker.UserLeft += this.UserLeftSession;
        }

        /// <summary>
        /// 新しいユーザが、現在のセッションから退出すると呼ばれる
        /// </summary>
        /// <param name="user">現在のセッションから退出したユーザ</param>
        private void UserLeftSession(User user)
        {
            this.DeleteMyMagnets();  // 他のユーザが出たときにも対応するように要変更
        }

        /// <summary>
        /// 新しいユーザが、現在のセッションに参加すると呼ばれる
        /// </summary>
        /// <param name="joinedUser">現在のセッションに参加したユーザ</param>
        private void UserJoinedSession(User joinedUser)
        {
            // 他のユーザが参加したときは磁石の更新は行わない
            if (joinedUser.GetID() == this.myUserId)
            {
                this.DeleteMyMagnets();

                this.CreateMagnet(this.myUserId);
            }
        }

        /// <summary>
        /// 新たに参加したユーザのユーザIDの磁石がない場合、PrefabSpawnManagerを使ってSpawnする
        /// </summary>
        /// <param name="userId"></param>
        private void CreateMagnet(long userId)
        {
            Vector3 position = new Vector3(0, 0, 1.5f);
            Quaternion rotation = Quaternion.Euler(new Vector3(0, 0, -90));
            var spawnedObject = new SyncSpawnedBarMagnet();
            this.spawnManager.Spawn(
                spawnedObject, position, rotation, spawnParentTransform.gameObject, 
                "SpawnedBarMagnet", true);
        }

        /// <summary>
        /// セッション参加が複数回起きて自分が生成した磁石が複数になった場合のために、
        /// 自分が生成した磁石をすべて削除する
        /// </summary>
        public void DeleteMyMagnets()
        {
            var magnets = GameObject.FindGameObjectsWithTag("Bar Magnet");
            foreach (var magnet in magnets)
            {
                var syncModelAccessor = magnet.GetComponent<DefaultSyncModelAccessor>();
                if (syncModelAccessor != null)
                {
                    var syncSpawnObject = (SyncSpawnedObject)syncModelAccessor.SyncModel;
                    if (syncSpawnObject.OwnerId == myUserId)
                    {
                        // 磁石のOnDestroyを走らせる
                        UnityEngine.Object.DestroyImmediate(magnet);

                        this.spawnManager.Delete(syncSpawnObject);
                    }
                }
            }
        }
    }
}

どうしてもUserJoinedSessionが複数回起きてしまうため、そのたびに棒磁石を生成すると棒磁石が複数本できてしまいます。そこで、UserJoinedSessionが起きるたびに既存の棒磁石はすべて削除し、新しい棒磁石を1つだけ生成しています。このあたりについては、littlewing(@keshin_sky)さんにアドバイスを頂きました。

PrefabSpawnManager.Spawn()の最後の引数はtrueを指定します。こうすることで、生成したオブジェクトにOwner情報が付与されます。ある棒磁石が、自分の棒磁石なのか、他の人の棒磁石なのか、知ることができます。

作成したスクリプトは適当なオブジェクト(私はBarMagnetSpawnerと名付けたEmptyObject)にアタッチします。

5. Spawn ManagerフィールドにSharingStageオブジェクトをドラッグする

Unity上で、先ほど作成したMagnetSpawnerスクリプトのSpawn Managerフィールドに、PrefabSpawnManagerスクリプトをアタッチしたオブジェクト(SharingStageなど)をドラッグします。

これで終了です。

PrefabSpawnManagerスクリプトの中の処理

PrefabSpawnManagerスクリプトの中では、どのように処理されているのでしょう?これを、MRTK-ExamplesのSharingSpawnTestシーンを読み解くことで理解しましょう。

このシーンは、「One」と発声すると(なぜか15m先に)プレハブが生成されます。その流れをアクティビティ図で追ってみました。

f:id:weed_7777:20180430124054p:plain

PrefabSpawnManager.Spawn()とSyncSourceが重要なポイントです。前者は、プレハブの生成とモデルのSharingStageへの登録を同時にしてくれます。生成したプレハブにはDefaultSyncModelAccessorスクリプトとTransformSynchronizerスクリプトが自動的にアタッチされます。

シェアリングのSyncObjectのクラス図

PrefabSpawnManagerが扱う最も基本的なクラスがSyncObjectです。PrefabSpawnManager、SyncSpawnedObjectなどのクラスから使用します。そのクラス図も確認しておきましょう。

f:id:weed_7777:20180506173732p:plain

重要なポイントはSyncObjectにOwnerIdというフィールドがあることです。これを使って自分が生成したオブジェクトかどうかを確認することができます。

SyncPrimitiveクラスにはUpdateFromRemoteメソッドなどが用意され、他のプレイヤーの世界での変更が伝わるようになっています。

SpawnedSyncObjectにはTransform情報があり、シェアリングに参加しているプレイヤー間で自動的に同期されます。

PrefabSpawnManagerで生成したオブジェクトのUserIdを取り出す

UserIdでなくても良いのですが、オブジェクトに付与された同期されている情報を取り出しましょう。まず、PrefabSpawnManagerでSpawnするときの最後の引数をtrueにします。

this.spawnManager.Spawn(spawnedObject, position, rotation, null, "SpawnedMagnet", true);

すると、生成したオブジェクトにOwner情報が含められるようになります。これを取り出すには、DefaultSyncModelAccessorコンポーネントを使います。DefaultSyncModelAccessorコンポーネントは、PrefabSpawnManagerで生成したオブジェクトに自動的にアタッチされるスクリプトです。自動的にアタッチされるコンポーネントには、他にもTransform Synchronizerがあります。

f:id:weed_7777:20180507094550p:plain

このDefaultSyncModelAccessorコンポーネントからオブジェクトの様々なプロパティにアクセスすることができます。

int userId = GetComponent<DefaultSyncModelAccessor>().SyncModel.OwnerId;

シェアリングで生成したオブジェクトをGameObject.Find()できない

f:id:weed_7777:20180509182015p:plain

シェアリングで生成したオブジェクトはデフォルトではSharingオブジェクトの子になります。Sharingオブジェクトは実行時にDontDestroyOnLoadに入ってしまうので、シーンからGameObject.Find()などでシェアリングで生成したオブジェクトを取得できません。

解決策としては、「SharingStage」のようなオブジェクトを作って、シェアリングでオブジェクトを生成する際はそれ(「SharingStage」)の子として生成するのが良いでしょう。


以上、ご参考になれば幸いです。