mindgeek
mindgeek2mo ago

Per-platform avatar prefabs?

My game is cross platform between VR and WebGL, so currently when a VR avatar prefab is spawned through the RealtimeAvatarManager on WebGL side, it has a bunch of unnecessary VR scripts on it. My current solution is to delete those scripts on WebGL side when the avatar is spawned. This works, but it still requires those scripts to be included in the WebGL build (Using OVR and Autohand, it adds up to quite a bit). Is there a better way to have a VR avatar prefab that is unique to each platform? My initial thought is to use asset bundles, but not sure how that would work with RealtimeAvatarManager.
13 Replies
kapa.ai
kapa.ai2mo ago
Hi @mindgeek. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- To address your question about using per-platform avatar prefabs with NormalVR, there are a couple of approaches you can consider: 1. Using Addressables: NormalVR provides support for using the Unity Addressables system to load prefabs. This can allow you to have different prefabs for different platforms. You can implement a custom IRealtimePrefabLoadDelegate to load the appropriate prefab for each platform using Addressables. Here's a basic example of how you could implement this:
using UnityEngine;
using UnityEngine.AddressableAssets;
using Normal.Realtime;

public class PlatformSpecificPrefabLoadDelegate : MonoBehaviour, IRealtimePrefabLoadDelegate {
public GameObject LoadRealtimePrefab(RealtimePrefabMetadata prefabMetadata) {
string platformSpecificKey = prefabMetadata.prefabName + "_" + GetPlatformSuffix();
var loadOperation = Addressables.LoadAssetAsync<GameObject>(platformSpecificKey);
return loadOperation.WaitForCompletion();
}

private string GetPlatformSuffix() {
#if UNITY_WEBGL
return "WebGL";
#elif UNITY_ANDROID
return "VR";
#else
return "Default";
#endif
}
}
using UnityEngine;
using UnityEngine.AddressableAssets;
using Normal.Realtime;

public class PlatformSpecificPrefabLoadDelegate : MonoBehaviour, IRealtimePrefabLoadDelegate {
public GameObject LoadRealtimePrefab(RealtimePrefabMetadata prefabMetadata) {
string platformSpecificKey = prefabMetadata.prefabName + "_" + GetPlatformSuffix();
var loadOperation = Addressables.LoadAssetAsync<GameObject>(platformSpecificKey);
return loadOperation.WaitForCompletion();
}

private string GetPlatformSuffix() {
#if UNITY_WEBGL
return "WebGL";
#elif UNITY_ANDROID
return "VR";
#else
return "Default";
#endif
}
}
You would then need to set up your Addressables to include platform-specific prefabs with keys like "AvatarPrefab_WebGL" and "AvatarPrefab_VR". 2. Custom RealtimeAvatarManager: Another approach would be to create a custom RealtimeAvatarManager that selects the appropriate prefab based on the current platform. This would involve copying the RealtimeAvatarManager script into your project and modifying it to use different prefabs for different platforms. Both of these approaches are mentioned in the NormalVR documentation: Avatars documentation (https://normcore.io/documentation/realtime/xr/avatars) states: "The source code to RealtimeAvatarManager and RealtimeAvatar are included with Normcore and serve as examples of how to implement an avatar management system in Normcore. While they cover the common case, larger XR apps may want to introduce extra functionality." Using Addressables documentation (https://normcore.io/documentation/guides/recipes/using-addressables) provides detailed information on how to implement Addressables support in NormalVR. These approaches would allow you to have different avatar prefabs for VR and WebGL platforms without including unnecessary scripts in your WebGL build. The Addressables approach might be more flexible and easier to manage, especially if you plan to support more platforms in the future. Remember to thoroughly test your implementation to ensure it works correctly across all your target platforms. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2mo ago
<@188493341468393473> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
mindgeek
mindgeekOP2mo ago
@kapa.ai Can you elaborate more on approach 2 using a custom RealtimeAvatarManager and include an example script showing how to load a realtime prefab that is not in the resources folder
kapa.ai
kapa.ai2mo ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Certainly! I'd be happy to elaborate on the approach of using a custom RealtimeAvatarManager to load platform-specific avatar prefabs that are not in the Resources folder. This approach gives you more control over how prefabs are loaded and instantiated. To implement this, we'll create a custom RealtimeAvatarManager that overrides the default behavior. Here's an example of how you might implement this:
using UnityEngine;
using Normal.Realtime;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class CustomRealtimeAvatarManager : RealtimeAvatarManager
{
[SerializeField] private string webGLAvatarAddressableKey = "WebGLAvatarPrefab";
[SerializeField] private string vrAvatarAddressableKey = "VRAvatarPrefab";

protected override GameObject InstantiateAvatar(RealtimeModel model)
{
string avatarKey = GetPlatformSpecificAvatarKey();
AsyncOperationHandle<GameObject> loadOperation = Addressables.LoadAssetAsync<GameObject>(avatarKey);
GameObject avatarPrefab = loadOperation.WaitForCompletion();

if (avatarPrefab == null)
{
Debug.LogError($"Failed to load avatar prefab for key: {avatarKey}");
return null;
}

GameObject avatarGameObject = Realtime.Instantiate(avatarPrefab.name, ownedByClient: false, preventOwnershipTakeover: false, useInstance: realtime);

// Set up the avatar...
SetUpAvatar(avatarGameObject, model);

return avatarGameObject;
}

private string GetPlatformSpecificAvatarKey()
{
#if UNITY_WEBGL
return webGLAvatarAddressableKey;
#else
return vrAvatarAddressableKey;
#endif
}
}
using UnityEngine;
using Normal.Realtime;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class CustomRealtimeAvatarManager : RealtimeAvatarManager
{
[SerializeField] private string webGLAvatarAddressableKey = "WebGLAvatarPrefab";
[SerializeField] private string vrAvatarAddressableKey = "VRAvatarPrefab";

protected override GameObject InstantiateAvatar(RealtimeModel model)
{
string avatarKey = GetPlatformSpecificAvatarKey();
AsyncOperationHandle<GameObject> loadOperation = Addressables.LoadAssetAsync<GameObject>(avatarKey);
GameObject avatarPrefab = loadOperation.WaitForCompletion();

if (avatarPrefab == null)
{
Debug.LogError($"Failed to load avatar prefab for key: {avatarKey}");
return null;
}

GameObject avatarGameObject = Realtime.Instantiate(avatarPrefab.name, ownedByClient: false, preventOwnershipTakeover: false, useInstance: realtime);

// Set up the avatar...
SetUpAvatar(avatarGameObject, model);

return avatarGameObject;
}

private string GetPlatformSpecificAvatarKey()
{
#if UNITY_WEBGL
return webGLAvatarAddressableKey;
#else
return vrAvatarAddressableKey;
#endif
}
}
This custom RealtimeAvatarManager does the following: 1. It uses the Addressables system to load avatar prefabs that are not in the Resources folder. 2. It defines separate Addressable keys for WebGL and VR avatar prefabs. 3. It overrides the InstantiateAvatar method to load the appropriate prefab based on the current platform. 4. It uses Realtime.Instantiate to create the avatar instance, ensuring it's properly set up for networking. To use this custom manager: 1. Replace the default RealtimeAvatarManager with this CustomRealtimeAvatarManager in your scene. 2. Set up your avatar prefabs as Addressable assets with the appropriate keys. 3. Configure the Addressable keys in the inspector for the CustomRealtimeAvatarManager component. This approach allows you to: - Use different avatar prefabs for different platforms without including unnecessary scripts in builds. - Load avatar prefabs from anywhere, not just the Resources folder. - Potentially implement more complex logic for avatar selection and instantiation. Remember that this is just an example, and you may need to adjust it based on your specific project structure and requirements. Also, make sure to properly manage the Addressable assets to avoid memory leaks. For more information on customizing RealtimeAvatarManager, you can refer to the Avatars documentation (https://normcore.io/documentation/realtime/xr/avatars), which mentions that the source code for RealtimeAvatarManager is included with Normcore and can be modified for larger XR apps. For details on using Addressables with Normcore, the Using Addressables documentation (https://normcore.io/documentation/guides/recipes/using-addressables) provides comprehensive guidance. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2mo ago
<@188493341468393473> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
mindgeek
mindgeekOP2mo ago
@kapa.ai Can you show an example script that does this with prefab names not in the resources folder, and not using addressables?
kapa.ai
kapa.ai2mo ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Certainly! I can provide an example of a custom RealtimeAvatarManager that loads prefabs not from the Resources folder and without using Addressables. This approach will use Unity's AssetBundle system instead. Here's an example script:
using UnityEngine;
using Normal.Realtime;
using System.Collections;

public class CustomRealtimeAvatarManager : RealtimeAvatarManager
{
[SerializeField] private string webGLAvatarBundleName = "webgl_avatars";
[SerializeField] private string vrAvatarBundleName = "vr_avatars";
[SerializeField] private string webGLAvatarPrefabName = "WebGLAvatarPrefab";
[SerializeField] private string vrAvatarPrefabName = "VRAvatarPrefab";

private AssetBundle loadedBundle;

protected override GameObject InstantiateAvatar(RealtimeModel model)
{
string bundleName = GetPlatformSpecificBundleName();
string prefabName = GetPlatformSpecificPrefabName();

StartCoroutine(LoadAvatarBundle(bundleName, prefabName, (avatarPrefab) =>
{
if (avatarPrefab == null)
{
Debug.LogError($"Failed to load avatar prefab: {prefabName} from bundle: {bundleName}");
return;
}

GameObject avatarGameObject = Realtime.Instantiate(prefabName, ownedByClient: false, preventOwnershipTakeover: false, useInstance: realtime);

// Set up the avatar...
SetUpAvatar(avatarGameObject, model);
}));

return null; // The actual instantiation happens in the coroutine callback
}

private IEnumerator LoadAvatarBundle(string bundleName, string prefabName, System.Action<GameObject> callback)
{
AssetBundleCreateRequest bundleRequest = AssetBundle.LoadFromFileAsync(System.IO.Path.Combine(Application.streamingAssetsPath, bundleName));
yield return bundleRequest;
if (bundleRequest.assetBundle == null)
{
Debug.LogError($"Failed to load AssetBundle: {bundleName}");
callback(null);
yield break;
}

loadedBundle = bundleRequest.assetBundle;

AssetBundleRequest prefabRequest = loadedBundle.LoadAssetAsync<GameObject>(prefabName);
yield return prefabRequest;

GameObject avatarPrefab = prefabRequest.asset as GameObject;
callback(avatarPrefab);
}

private string GetPlatformSpecificBundleName()
{
#if UNITY_WEBGL
return webGLAvatarBundleName;
#else
return vrAvatarBundleName;
#endif
}

private string GetPlatformSpecificPrefabName()
{
#if UNITY_WEBGL
return webGLAvatarPrefabName;
#else
return vrAvatarPrefabName;
#endif
}

private void OnDestroy()
{
if (loadedBundle != null)
{
loadedBundle.Unload(true);
}
}
}
using UnityEngine;
using Normal.Realtime;
using System.Collections;

public class CustomRealtimeAvatarManager : RealtimeAvatarManager
{
[SerializeField] private string webGLAvatarBundleName = "webgl_avatars";
[SerializeField] private string vrAvatarBundleName = "vr_avatars";
[SerializeField] private string webGLAvatarPrefabName = "WebGLAvatarPrefab";
[SerializeField] private string vrAvatarPrefabName = "VRAvatarPrefab";

private AssetBundle loadedBundle;

protected override GameObject InstantiateAvatar(RealtimeModel model)
{
string bundleName = GetPlatformSpecificBundleName();
string prefabName = GetPlatformSpecificPrefabName();

StartCoroutine(LoadAvatarBundle(bundleName, prefabName, (avatarPrefab) =>
{
if (avatarPrefab == null)
{
Debug.LogError($"Failed to load avatar prefab: {prefabName} from bundle: {bundleName}");
return;
}

GameObject avatarGameObject = Realtime.Instantiate(prefabName, ownedByClient: false, preventOwnershipTakeover: false, useInstance: realtime);

// Set up the avatar...
SetUpAvatar(avatarGameObject, model);
}));

return null; // The actual instantiation happens in the coroutine callback
}

private IEnumerator LoadAvatarBundle(string bundleName, string prefabName, System.Action<GameObject> callback)
{
AssetBundleCreateRequest bundleRequest = AssetBundle.LoadFromFileAsync(System.IO.Path.Combine(Application.streamingAssetsPath, bundleName));
yield return bundleRequest;
if (bundleRequest.assetBundle == null)
{
Debug.LogError($"Failed to load AssetBundle: {bundleName}");
callback(null);
yield break;
}

loadedBundle = bundleRequest.assetBundle;

AssetBundleRequest prefabRequest = loadedBundle.LoadAssetAsync<GameObject>(prefabName);
yield return prefabRequest;

GameObject avatarPrefab = prefabRequest.asset as GameObject;
callback(avatarPrefab);
}

private string GetPlatformSpecificBundleName()
{
#if UNITY_WEBGL
return webGLAvatarBundleName;
#else
return vrAvatarBundleName;
#endif
}

private string GetPlatformSpecificPrefabName()
{
#if UNITY_WEBGL
return webGLAvatarPrefabName;
#else
return vrAvatarPrefabName;
#endif
}

private void OnDestroy()
{
if (loadedBundle != null)
{
loadedBundle.Unload(true);
}
}
}
This script does the following: 1. It uses AssetBundles to load avatar prefabs that are not in the Resources folder. 2. It defines separate bundle names and prefab names for WebGL and VR avatars. 3. It overrides the InstantiateAvatar method to load the appropriate prefab based on the current platform. 4. It uses a coroutine to asynchronously load the AssetBundle and the prefab from it. 5. It uses Realtime.Instantiate to create the avatar instance, ensuring it's properly set up for networking. To use this custom manager: 1. Replace the default RealtimeAvatarManager with this CustomRealtimeAvatarManager in your scene. 2. Create AssetBundles for your avatar prefabs and place them in the StreamingAssets folder. 3. Configure the bundle names and prefab names in the inspector for the CustomRealtimeAvatarManager component. This approach allows you to: - Use different avatar prefabs for different platforms without including unnecessary scripts in builds. - Load avatar prefabs from AssetBundles, not from the Resources folder. - Potentially implement more complex logic for avatar selection and instantiation. Remember that this is an example and you may need to adjust it based on your specific project structure and requirements. Also, make sure to properly manage the AssetBundles to avoid memory leaks. For more information on customizing RealtimeAvatarManager, you can refer to the Avatars documentation (https://normcore.io/documentation/realtime/xr/avatars), which mentions that the source code for RealtimeAvatarManager is included with Normcore and can be modified for larger XR apps. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2mo ago
<@188493341468393473> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
mindgeek
mindgeekOP2mo ago
@kapa.ai @Normcore Assistant Can you show an example script that does this with prefab names not in the resources folder, and not using addressables or assetbundles?
kapa.ai
kapa.ai2mo ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Certainly! I understand you're looking for an example that doesn't use Resources, Addressables, or AssetBundles. In this case, we can create a custom RealtimeAvatarManager that loads prefabs directly from a specific folder in your project. Here's an example of how you might implement this:
using UnityEngine;
using Normal.Realtime;
using System.IO;

public class CustomRealtimeAvatarManager : RealtimeAvatarManager, IRealtimePrefabLoadDelegate
{
[SerializeField] private string webGLAvatarPrefabName = "WebGLAvatarPrefab";
[SerializeField] private string vrAvatarPrefabName = "VRAvatarPrefab";
[SerializeField] private string avatarPrefabPath = "Assets/Prefabs/Avatars/";

private void Awake()
{
// Set this instance as the prefab load delegate
GetComponent<Realtime>().prefabLoadDelegate = this;
}

protected override GameObject InstantiateAvatar(RealtimeModel model)
{
string prefabName = GetPlatformSpecificPrefabName();

GameObject avatarGameObject = Realtime.Instantiate(prefabName, ownedByClient: false, preventOwnershipTakeover: false, useInstance: realtime);

if (avatarGameObject == null)
{
Debug.LogError($"Failed to instantiate avatar prefab: {prefabName}");
return null;
}

// Set up the avatar...
SetUpAvatar(avatarGameObject, model);

return avatarGameObject;
}

private string GetPlatformSpecificPrefabName()
{
#if UNITY_WEBGL
return webGLAvatarPrefabName;
#else
return vrAvatarPrefabName;
#endif
}
// IRealtimePrefabLoadDelegate implementation
public GameObject LoadRealtimePrefab(RealtimePrefabMetadata prefabMetadata)
{
string fullPath = Path.Combine(avatarPrefabPath, prefabMetadata.prefabName + ".prefab");

#if UNITY_EDITOR
return UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(fullPath);
#else
Debug.LogError("This method of loading prefabs is only supported in the Unity Editor.");
return null;
#endif
}
}
using UnityEngine;
using Normal.Realtime;
using System.IO;

public class CustomRealtimeAvatarManager : RealtimeAvatarManager, IRealtimePrefabLoadDelegate
{
[SerializeField] private string webGLAvatarPrefabName = "WebGLAvatarPrefab";
[SerializeField] private string vrAvatarPrefabName = "VRAvatarPrefab";
[SerializeField] private string avatarPrefabPath = "Assets/Prefabs/Avatars/";

private void Awake()
{
// Set this instance as the prefab load delegate
GetComponent<Realtime>().prefabLoadDelegate = this;
}

protected override GameObject InstantiateAvatar(RealtimeModel model)
{
string prefabName = GetPlatformSpecificPrefabName();

GameObject avatarGameObject = Realtime.Instantiate(prefabName, ownedByClient: false, preventOwnershipTakeover: false, useInstance: realtime);

if (avatarGameObject == null)
{
Debug.LogError($"Failed to instantiate avatar prefab: {prefabName}");
return null;
}

// Set up the avatar...
SetUpAvatar(avatarGameObject, model);

return avatarGameObject;
}

private string GetPlatformSpecificPrefabName()
{
#if UNITY_WEBGL
return webGLAvatarPrefabName;
#else
return vrAvatarPrefabName;
#endif
}
// IRealtimePrefabLoadDelegate implementation
public GameObject LoadRealtimePrefab(RealtimePrefabMetadata prefabMetadata)
{
string fullPath = Path.Combine(avatarPrefabPath, prefabMetadata.prefabName + ".prefab");

#if UNITY_EDITOR
return UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(fullPath);
#else
Debug.LogError("This method of loading prefabs is only supported in the Unity Editor.");
return null;
#endif
}
}
This script does the following: 1. It defines separate prefab names for WebGL and VR avatars. 2. It specifies a path where the avatar prefabs are stored in your project. 3. It overrides the InstantiateAvatar method to use the appropriate prefab name based on the current platform. 4. It implements the IRealtimePrefabLoadDelegate interface to provide custom prefab loading logic. 5. The LoadRealtimePrefab method loads the prefab directly from the specified path in your project. To use this custom manager: 1. Replace the default RealtimeAvatarManager with this CustomRealtimeAvatarManager in your scene. 2. Place your avatar prefabs in the specified folder (e.g., "Assets/Prefabs/Avatars/"). 3. Configure the prefab names and path in the inspector for the CustomRealtimeAvatarManager component. Please note that this approach has some limitations: 1. It only works in the Unity Editor, not in builds. For builds, you would need to use one of the other methods (Resources, Addressables, or AssetBundles). 2. It requires your prefabs to be in a specific folder structure in your project. For more information on customizing RealtimeAvatarManager and implementing custom prefab loading, you can refer to the Avatars documentation (https://normcore.io/documentation/realtime/xr/avatars) and the IRealtimePrefabLoadDelegate documentation (https://normcore.io/documentation/reference/classes/Normal.Realtime.IRealtimePrefabLoadDelegate). Remember that for a production build, you'll need to use one of the other methods mentioned earlier (Resources, Addressables, or AssetBundles) to ensure your prefabs can be loaded at runtime. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2mo ago
<@188493341468393473> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Mechabit
Mechabit2mo ago
Using Addressables | Normcore Documenation
By default, Normcore loads all realtime prefabs using Resources.Load(). For larger projects, this can cause performance issues when loading prefabs from disk. It can also be a pain run a large project with multiple Resources folders.
Mechabit
Mechabit2mo ago
you can use that to selectively load prefabs

Did you find this page helpful?