Fusion 2.1 and Meta Core XR SDK

当面、Fusion 2.1 と Meta Core XR SDK パッケージ (com.meta.xr.sdk.core)間には互換性の問題が生じています。Meta SDKがFusion2.1との互換性を持つようアップデートされるまで、一時的な回避策として本ページでご紹介する指示に従ってください。

本件につきまして、ヘルプが必要な場合はご遠慮なくお問い合わせください。

互換性の問題

Meta Core XR SDKにはFusion SDKへの参照が含まれており、ネットワーク同期に対応したシーンを構築するためのコンポーネント群を提供しています。

Fusion 2.1バージョンはAPIに破壊的な変更を導入しており、Meta側でそのような変更をSDKに反映することができていません。

Meta統合に影響の出ているAPI変更は以下の通りです。

  • インターフェイスメソッドである INetworkRunnerCallbacks.OnReliableDataReceived への型の変更
  • NetworkTransform.DisableSharedModeInterpolation プロパティの除去(本機能はコンフィグフラグに統合されました)
  • Fusionで完全な基盤となるPhoton Realtime APIの使用が可能に。それに伴い、いくつかの機能がPhoton.Realtimeに依拠するようになり、これを使用しているモジュールはasmdefファイル内の参照を追加しなければならなくなりました。.

Meta XR Core SDKの次のバージョンでは、Fusion 2.1に対応するよう現在Metaチームと調整中です。

調整が完了するまでの間、Fusion 2.1でMeta Core XR SDKを使用するには、 com.meta.xr.sdk.coreパッケージでいくつか変更を行う必要があります。

変更の適用方法

com.meta.xr.sdk.coreは、Library\PackageCacheフォルダ内にデフォルトでインストールされています。

この場所でパッケージの編集は完全には行えません(特に、インスペクタ内でのasmdefファイル編集)。そこで、必要な修正を適用しやすくするための解決策がパッケージの埋め込みです。

パッケージの埋め込みについて詳細は以下のリンクを参照してください。

パッケージを埋め込むためのスクリプト

パッケージの埋め込みを簡単に行うため、「PackageEmbedding.cs」スクリプトを追加できます(本ドキュメントの最後でソースコードをご確認ください。)

次に、プロジェクトビューで「Meta XR Core SDK」パッケージを右クリックします。最後に「Move as local embedded package(ローカルで埋め込まれたパッケージとして移動)」を選択します。パッケージがLibrary\PackageCacheフォルダからPackagesフォルダへ移動し、変更が適用しやすくなります。

これを元に戻すには、エディタを閉じてPackages\com.meta.xr.sdk.coreフォルダを削除します。

備考:代わりに、パッケージマネジャービューで「Meta XR Core SDK」パッケージを選択し、「Remove(消去)」を選択することもできます。しかし、OVRPlugin.dllが既に読み込まれているため、Unityは稼働しながらフォルダを完全にクリーンアップすることができません。まずエディタを閉じることがより良いアプローチです。

適用する変更

Meta SDK 78.0.0 または 81.0.0では、Meta XR Core SDKと共にFusion 2.1を問題なく実行するために、稼働前に4つのファイルを編集する必要があります。

推奨するアプローチは以下の通りです。

  • Meta XR Core SDKパッケージを埋め込む
  • Fusion 2.1をインストールする(エラーが表示されます)
  • 編集する2つのasmdefファイルに Photon.realtimeリファレンスを追加する
  • 2つのファイルを編集する
  • Unityを再起動する。初回起動時にエディタでエラーが表示されても驚かないでください。メッセージは無視してください。Unityがプロジェクトの正常なコンパイルと、エラーがこれ以上ないことを検証するのに起動を完了する必要があるためです。
  • 必要に応じて、適切なバージョンをIf needed, install the appropriate version of Photon Voice

Meta.XR.MultiplayerBlocks.Fusion.asmdef

編集するファイルの場所

Packages/com.meta.xr.sdk.core/Scripts/BuildingBlocks/MultiplayerBlocks/PhotonFusion/Meta.XR.MultiplayerBlocks.Fusion.asmdef

編集するセクションの初期コンテンツ

    "references": [
        "Oculus.Interaction",
        "Oculus.Platform",
        "Oculus.VR",
        "Oculus.AvatarSDK2",
        "PhotonVoice.Fusion",
        "PhotonRealtime",
        "PhotonVoice",
        "PhotonVoice.API",
        "Meta.XR.BuildingBlocks",
        "Meta.XR.MultiplayerBlocks.Shared",
        "Fusion.Unity",
        "Fusion.Addons.Physics"
    ],

変更したコンテンツ

エディター版での代替案:

  • パッケージが埋め込まれている場合、手動でファイルを編集する代わりに、Unityインスペクタウィンドウの「Assembly definition references(アセンブリ定義参照)」で直接asmdefファイルを編集し、Photon.Realtime asmdef参照を追加できます。
  • インスペクタでPhoton.Realtime asmdefを問題なく表示するには、Fusionのインストールが必要です。

    "references": [
        "Oculus.Interaction",
        "Oculus.Platform",
        "Oculus.VR",
        "Oculus.AvatarSDK2",
        "PhotonVoice.Fusion",
        "PhotonRealtime",
        "PhotonVoice",
        "PhotonVoice.API",
        "Meta.XR.BuildingBlocks",
        "Meta.XR.MultiplayerBlocks.Shared",
        "Fusion.Unity",
        "Fusion.Addons.Physics",
        "Photon.Realtime"
    ],

Meta.XR.MultiplayerBlocks.Fusion.Editor.asmdef

編集するファイルの場所

Packages/com.meta.xr.sdk.core/Editor/BuildingBlocks/BlockData/MultiplayerBlocks/PhotonFusion/Meta.XR.MultiplayerBlocks.Fusion.Editor.asmdef

編集するセクションの初期コンテンツ

    "references": [
        "Meta.XR.BuildingBlocks",
        "Meta.XR.BuildingBlocks.Editor",
        "Oculus.Interaction",
        "Oculus.Interaction.OVR.Editor",
        "Oculus.VR",
        "Oculus.VR.Editor",
        "PhotonVoice",
        "PhotonRealtime",
        "PhotonVoice.Fusion",
        "Meta.XR.MultiplayerBlocks.Fusion",
        "Fusion.Unity.Editor",
        "Fusion.Unity",
        "Fusion.Addons.Physics",
        "Meta.XR.MultiplayerBlocks.Shared.Editor",
        "Meta.XR.MultiplayerBlocks.Shared"
    ],

変更したコンテンツ

エディター版での代替案:

  • パッケージが埋め込まれている場合、手動でファイルを編集する代わりに、Unityインスペクタウィンドウの「Assembly definition references(アセンブリ定義参照)」で直接asmdefファイルを編集し、Photon.Realtime asmdef参照を追加できます。
  • インスペクタでPhoton.Realtime asmdefを問題なく表示するには、Fusionのインストールが必要です。

    "references": [
        "Meta.XR.BuildingBlocks",
        "Meta.XR.BuildingBlocks.Editor",
        "Oculus.Interaction",
        "Oculus.Interaction.OVR.Editor",
        "Oculus.VR",
        "Oculus.VR.Editor",
        "PhotonVoice",
        "PhotonRealtime",
        "PhotonVoice.Fusion",
        "Meta.XR.MultiplayerBlocks.Fusion",
        "Fusion.Unity.Editor",
        "Fusion.Unity",
        "Fusion.Addons.Physics",
        "Meta.XR.MultiplayerBlocks.Shared.Editor",
        "Meta.XR.MultiplayerBlocks.Shared",
        "Photon.Realtime"
    ],

FusionBBEvents.cs

編集するファイルの場所

Packages/com.meta.xr.sdk.core/Scripts/BuildingBlocks/MultiplayerBlocks/PhotonFusion/NetworkRunner/Scripts/FusionBBEvents.cs

編集するセクションの初期コード

void INetworkRunnerCallbacks.OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key,
      ArraySegment<byte> data)
  {
    OnReliableDataReceived?.Invoke(runner, player, key, data);
  }

変更したコード

#if FUSION_2_1
  void INetworkRunnerCallbacks.OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key,
      ReadOnlySpan<byte> data)
  {
    OnReliableDataReceived?.Invoke(runner, player, key, data.ToArray());
  }
#else
  void INetworkRunnerCallbacks.OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key,
      ArraySegment<byte> data)
  {
    OnReliableDataReceived?.Invoke(runner, player, key, data);
  }
#endif

NetworkedGrabbableObjectFusionInstallationRoutine.cs

編集するファイルの場所

Packages/com.meta.xr.sdk.core/Editor/BuildingBlocks/BlockData/MultiplayerBlocks/PhotonFusion/NetworkedGrabbableObject/Scripts/NetworkedGrabbableObjectFusionInstallationRoutine.cs

編集するセクションの初期コード

  networkTransform.DisableSharedModeInterpolation = true; // 変更していないとオブジェクトがISDKにグラビングされません

変更したコード

#if FUSION_2_1
  // 変更していないとオブジェクトがISDKにグラビングされません
  networkTransform.ConfigFlags = NetworkTransform.NetworkTransformFlags.DisableSharedModeInterpolation; 
#else
  // 変更していないとオブジェクトがISDKにグラビングされません
  networkTransform.DisableSharedModeInterpolation = true;
#endif

Photon Voice

Photon Voiceが必要な場合、アセットストアに出ているばじょんではなくPhoton Voice Realtime 5バージョンを使用するようにしてください。

Fusion 2.1ダウンロードページの「Complementary download」セクションにあります。
https://doc.photonengine.com/fusion/current/getting-started/preview-2-1/download-2-1#matching-downloads

PackageEmbedding.cs ソース

#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;

namespace Photon.PackageTooling
{
    /*
     * Allows to move an existing package as an embedded package
     * See https://docs.unity3d.com/Manual/upm-embed.html, https://docs.unity3d.com/Manual/upm-api.html#Embed and https://docs.unity3d.com/ScriptReference/PackageManager.Client.Embed.html
     * 
     * Inspired by https://github.com/liortal53/upm-embed/blob/master/Assets/EmbedPackage/Scripts/EmbedPackage.cs
     */
    public static class PackageEmbedding
    {
        static EmbedRequest EmbeddingRequest;
        static RemoveRequest RemoveEmbeddingRequest;

        public static bool IsAnyRequestPending()
        {
            return EditorApplication.isCompiling || EmbeddingRequest != null || RemoveEmbeddingRequest != null;
        }
        public static void EmbedPackage(string packageName)
        {
            if (IsAnyRequestPending())
            {
                Debug.LogError("Editor busy, or an embedding-related request is already in progress: please ait and retry");
                return;
            }
            Debug.Log($"Installed package '{packageName}' is being moved to the Packages folder of the project as an embedded package ...");

            EmbeddingRequest = UnityEditor.PackageManager.Client.Embed(packageName);
            EditorApplication.update += EmbedProgress;
        }

        private static void EmbedProgress()
        {
            if (EmbeddingRequest.IsCompleted)
            {
                switch (EmbeddingRequest.Status)
                {
                    case StatusCode.Success:
                        Debug.Log($"Package successfully embedded (id: {EmbeddingRequest.Result.packageId}. " +
                            $" Uninstalling it from the package manager will remove this custom version and return to the original one." +
                            $" Alternatively, to un-embed it, simply delete the embedded version version folder in your project Packages folder ({Path.GetFullPath(EmbeddingRequest.Result.assetPath)}):" +
                            $" the repository package will re reinstalled in its unedited version");
                        AssetDatabase.Refresh();
                        Debug.LogError("Database refreshed");
                        break;
                    case StatusCode.Failure:
                        Debug.Log($"Unable to embed package: {EmbeddingRequest.Error.message}");
                        break;
                }
                
                EditorApplication.update -= EmbedProgress;
                EmbeddingRequest = null;

            }
        }

        public static bool TryGetFirstSelectedAssetPath(out string selectedItemPath, out string selectedItemFullPath)
        {
            // Source: https://docs.unity3d.com/ScriptReference/Selection.GetFiltered.html and https://discussions.unity.com/t/selection-activeobject-on-items-on-left-side-of-project-window/664382/6
            selectedItemPath = null;
            selectedItemFullPath = null;
            UnityEngine.Object[] selectedAssets = Selection.GetFiltered(typeof(object), SelectionMode.Assets);
            foreach (var selectedAsset in selectedAssets)
            {
                selectedItemPath = AssetDatabase.GetAssetPath(selectedAsset);
                selectedItemFullPath = Path.GetFullPath(selectedItemPath);
                return true;
            }
            return false;

        }

        public static void UnEmbedPackage(string packageName, string absolutePath)
        {
            if (IsAnyRequestPending())
            {
                Debug.LogError("Editor busy, or an embedding-related request is already in progress: please ait and retry");
                return;
            }
            Debug.Log($"Embedded package '{packageName}' will be deleted and replaced by the original unembedded package version");

            Debug.LogError("Will erase " + absolutePath);
            if(TrySafeDeleteFolder(absolutePath) == false)
            {
                Debug.LogError("Failed to erase embedded version");
            }
            else
            {
                // Deletion succeeded
                Debug.LogError("Deletion ok");
                AssetDatabase.Refresh();
                Debug.LogError("Database refreshed");
            }
        }

        public static bool TrySafeDeleteFolder(string path)
        {
            var folderInfo = new DirectoryInfo(path);
            if (TrySafeDeleteFolder(folderInfo, dryRun: true) == false)
            {
                Debug.LogError($"Deletion of {path} aborted: no files were edited");
                return false;
            } else
            {
                Debug.LogError("Dry run of deletion ok");
            }
            return TrySafeDeleteFolder(folderInfo);
        }

        public static bool TrySafeDeleteFolder(DirectoryInfo folderInfo, bool dryRun = false)
        { 
            if (folderInfo.Exists == false)
            {
                return false;
            }
            FileInfo[] fileInfos = folderInfo.GetFiles();
            foreach (var fileInfo in fileInfos)
            {
                if (fileInfo.IsReadOnly)
                {
                    Debug.LogError($"{fileInfo} is read-only");
                    return false;
                }
                if (dryRun == false)
                {
                    try
                    {
                        fileInfo.Delete();
                    }
                    catch (Exception e)
                    {
                        Debug.LogError($"Unable to delete file {fileInfo}: {e.GetType()} {e.Message}");
                        return false;
                    }
                }
                else
                {
                    // Dry run
                    try
                    {
                        // Check if the file is used. See https://stackoverflow.com/questions/876473/is-there-a-way-to-check-if-a-file-is-in-use
                        using (FileStream stream = fileInfo.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None))
                        {
                            stream.Close();
                        }
                    }
                    catch (Exception e)
                    {
                        Debug.LogError($"[Preliminary check fail] File {fileInfo} deletion won't be possible: [{e.GetType()}] {e.Message} (dry-run only: no files were edited)");
                        return false;
                    }
                }

            }
            DirectoryInfo[] subFolderInfos = folderInfo.GetDirectories();
            foreach(var subFolderInfo in subFolderInfos)
            {
                if (TrySafeDeleteFolder(subFolderInfo, dryRun: dryRun) == false)
                {
                    return false;
                }
            }
            if(dryRun == false)
            {
                try
                {
                    folderInfo.Delete();
                }
                catch (Exception e)
                {
                    Debug.LogError($"Unable to delete folder {folderInfo}: {e.GetType()} {e.Message}");
                }
            }

            return true;
        }

        public static void EmbedPackageFolder(string folderLocalPath, string folderGlobalPath, bool backupOriginalPath = false)
        {
            if (folderLocalPath.StartsWith("Assets") == false) return;
            string packageFile = Path.Join(folderLocalPath, "package.json");
            if (File.Exists(packageFile) == false) return;

            string packageName = Path.GetFileName(folderLocalPath);
            string originalPathFilePath = Path.Join(folderLocalPath, "OriginalFolderPath.log");
            string projectFolder = Path.GetDirectoryName(Application.dataPath); 
            string newLocationPath = Path.Join(projectFolder, "Packages", packageName);

            Debug.LogError($"Will move {folderGlobalPath} to {newLocationPath}");

            var folderInfo = new DirectoryInfo(folderLocalPath);
            if (TrySafeDeleteFolder(folderInfo, dryRun: true) == false)
            {
                Debug.LogError($"Moving of {folderLocalPath} aborted: no files were edited");
                return;
            }

            if (Directory.Exists(newLocationPath))
            {
                Debug.LogError($"Moving of {folderLocalPath} aborted: folder {newLocationPath} already exists");
                return;
            }

            if (backupOriginalPath && File.Exists(originalPathFilePath) == false)
            {
                // Store original path in case of revert needs
                var originalPathFile = File.CreateText(originalPathFilePath);
                originalPathFile.WriteLine(folderLocalPath);
                originalPathFile.Close();
            }

            try
            {
                Directory.Move(folderGlobalPath, newLocationPath);
                AssetDatabase.Refresh();
                Debug.LogError("Database refreshed");
            }
            catch (Exception e)
            {
                Debug.LogError("Unable to convert folder as an embedded package (close your editor an any app that could have access to it)) " + e.Message);
                throw e;
            }

        }

        #region Embed menu item
        [MenuItem("Assets/Move as local embedded package", false, 10_000)]
        public static void PackageEmbeddingAction()
        {
            if (TryGetFirstSelectedAssetPath(out string selectedItemPath, out string selectedItemFullPath))
            {
                string packageName = Path.GetFileName(selectedItemPath);

                PackagePresenceCheck.LookForPackage(packageName, (packageInfo) => {
                    if (packageInfo == null)
                    {
                        Debug.LogError($"Package {packageName} not found");
                    }
                    else if (packageInfo.isDirectDependency == false)
                    {
                        Debug.LogError($"Error: Client.Embed cannot embed indirect dependency. Install {packageInfo.name} directly, not automatically as a dependency, to be able to embed it");
                    }
                    else
                    {
                        switch (packageInfo.source)
                        {
                            case PackageSource.BuiltIn:
                                Debug.LogError($"Error: built-in package embedding not supported ({packageInfo.name})");
                                break;
                            case PackageSource.Embedded:
                                Debug.LogError($"Package {packageInfo.name} is already embedded. Uninstalling it from the package manager will remove this custom version and return to the original one");
                                break;
                        }
                    }
                    EmbedPackage(packageName);
                });
            }
        }

        [MenuItem("Assets/Move as local embedded package", true)]
        public static bool PackageEmbeddingValidation()
        {
            if (TryGetFirstSelectedAssetPath(out string selectedItemPath, out string selectedItemFullPath))
            {
                if (selectedItemPath.StartsWith("Packages"))
                {
                    string packageName = Path.GetFileName(selectedItemPath);
                    // It is a package: already embedded ? 
                    if (selectedItemFullPath.Contains("PackageCache") == false)
                    {
                        // Already an embedded package
                        //Debug.Log($"Already embedded package: {packageName}");
                        return false;
                    }
                    // Non-embedded package (not a garantee we can embed it: built-in and dependency packages are not embeddable
                    //Debug.Log($"Non embedded pacakge {packageName} at path {selectedItemPath}"); 
                    return true;
                }
                return false;

            }
            return false;
        }
        #endregion        
    }

    /*
     * Package Lookup methods
     */
    public class PackagePresenceCheck
    {
        string[] packageNames = null;
        UnityEditor.PackageManager.Requests.ListRequest request;
        public delegate void ResultDelegate(Dictionary<string, UnityEditor.PackageManager.PackageInfo> packageInfoByPackageName);
        ResultDelegate resultCallback;
        public delegate void SingleResultDelegate(UnityEditor.PackageManager.PackageInfo packageInfo);
        SingleResultDelegate singleResultCallback;

        Dictionary<string, UnityEditor.PackageManager.PackageInfo> results = new Dictionary<string, UnityEditor.PackageManager.PackageInfo>();

        public static void LookForPackage(string packageToSearch, SingleResultDelegate packageLookupCallback = null, string defineToAddIfDetected = null)
        {
            var packageCheck = new PackagePresenceCheck(packageToSearch, (packageInfo) => {
                var group = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
                var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);

                if (packageInfo != null)
                {
                    if (string.IsNullOrEmpty(defineToAddIfDetected) == false && defines.Contains(defineToAddIfDetected) == false) {
                        defines = $"{defines};{defineToAddIfDetected}";
                        PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines);
                    }
                    packageLookupCallback(packageInfo);
                }
                else
                {
                    if (string.IsNullOrEmpty(defineToAddIfDetected) == false && defines.Contains(defineToAddIfDetected) == true)
                    {
                        Debug.LogError($"Project define symbols include {defineToAddIfDetected}, while package {packageToSearch} is not installed anymore: it should be removed in the Player settings");
                    }
                    packageLookupCallback(null);
                }
            });
        }

        public PackagePresenceCheck(string[] packageNames, ResultDelegate resultCallback, bool useOfflineMode = true)
        {
            this.packageNames = packageNames;
            this.resultCallback = resultCallback;
            request = UnityEditor.PackageManager.Client.List(offlineMode: useOfflineMode, includeIndirectDependencies: true);
            EditorApplication.update += Progress;
        }

        public PackagePresenceCheck(string packageName, SingleResultDelegate resultCallback, bool useOfflineMode = true)
        {
            this.packageNames = new string[] { packageName };
            this.singleResultCallback = resultCallback;
            request = UnityEditor.PackageManager.Client.List(offlineMode: useOfflineMode, includeIndirectDependencies: true);
            EditorApplication.update += Progress;
        }

        void Progress()
        {
            if (request.IsCompleted)
            {
                bool singleResultCallbackReturned = false;
                results = new Dictionary<string, UnityEditor.PackageManager.PackageInfo>();
                if (request.Status == StatusCode.Success)
                {
                    foreach (var info in request.Result)
                    {
                        foreach(var checkedPackageName in packageNames)
                        {
                            if (info.name == checkedPackageName)
                            {
                                results[checkedPackageName] = info;
                                if (singleResultCallback != null)
                                {
                                    singleResultCallbackReturned = true;
                                    singleResultCallback(info);
                                }
                                break;
                            }
                        }
                    }
                }
                if (singleResultCallback != null && singleResultCallbackReturned == false)
                {
                    singleResultCallback(null);
                }
                if (resultCallback != null)
                    resultCallback(results);
                EditorApplication.update -= Progress;
            }
        }
    }
}

#endif
Back to top