Search Unity

Resolved How to pass a (network) prefab to a ServerRPC to spawn it

Discussion in 'Netcode for GameObjects' started by Ralfolas, Apr 17, 2023.

  1. Ralfolas

    Ralfolas

    Joined:
    Mar 12, 2023
    Posts:
    5
    Hi!

    I'm working on an ability/spellcasting system where abilities are mainly defined via ScriptableObjects. Each AbilityScriptableObject has a reference to a prefab which is the actual spell/projectile. The used prefabs are NetworkObjects which are registered as NetworkPrefabs in the NetworkManager/NetworkConfig.

    When a spell is cast, I want to spawn it on the server, but I can't find an effective way to resolve the prefab on the server.

    I tried using NetworkObjectReference, but this can only be used for already spawned objects (see code below - it causes the error: ArgumentException: NetworkObjectReference can only be created from spawned NetworkObjects).

    My intuition is to obtain some sort of identifier for the registered NetworkPrefab, send it via ServerRPC and then resolve it on the server, but I haven't been able to access the registered NetworkPrefabs.

    I also posted this question on Reddit, but people there suggested to maintain a list of my spell prefabs somewhere and send my own identifiers to the server. I really don't like this approach, as I don't want to manage an additional list of prefabs that I need to keep in sync with my ScriptableObjects.

    Any better ideas on how to solve this?

    Code (CSharp):
    1. public virtual bool Cast(AbilityScriptableObject abilitySo, Vector3 spawn, Vector3 target)
    2.     {
    3.         NetworkObjectReference prefabRef = new(abilitySo.AbilityPrefab);
    4.  
    5.         CastServerRpc(prefabRef, spawn, target);
    6.         return true;
    7.     }
    8.  
    9.     [ServerRpc]
    10.     public void CastServerRpc(NetworkObjectReference prefabRef, Vector3 spawnPos, Vector3 target, ServerRpcParams serverRpcParams = default)
    11.     {
    12.         if(!prefabRef.TryGet(out NetworkObject prefab))
    13.         {
    14.             Debug.LogError("Failed to obtain network object from reference: " +prefabRef);
    15.         }
    16.  
    17.         GameObject projectile = Instantiate(prefab.gameObject);
    18.         projectile.transform.position = spawnPos;
    19.  
    20.         /*Set direction of projectile.*/
    21.         projectile.GetComponent<TargetableProjectile>().SetTarget(target);
    22.  
    23.         /*Spawn the projectile*/
    24.         projectile.GetComponent<NetworkObject>().SpawnWithOwnership(serverRpcParams.Receive.SenderClientId, true);
    25.     }
     
  2. Nyphur

    Nyphur

    Joined:
    Jan 29, 2016
    Posts:
    98
    The step you're stuck on seems to be a way for the client to tell the server which spell effect to pick. In general you want to be sending the minimum data possible, so the approach suggested to you on Reddit is a good one. Instead of sending an object you just want to send an ID number or name or hash. The server script that has CastServerRpc needs to have a list of all spell prefabs and look up the one with the provided ID or hash. When the client casts, it sends abilitySo.AbilityPrefab.ID instead of trying to send the entire object.

    I know you said you don't want to maintain a separate list of prefabs synced up with your scriptableobjects but you need to do that anyway. All prefabs to be spawned must be registered with the NetworkManager, so you need to either pre-add all possible spell effects to your NetworkManager's NetworkPrefab list in the editor or if you want to do it at runtime there's also the NetworkManager.AddNetworkPrefab method.

    I've never used AddNetworkPrefab but it sounds like what you want. A script that iterates all the SOs and adds their prefabs to the networkprefab list on startup would do the trick. There are a few caveats to the method listed here: https://docs.unity3d.com/Packages/c...ager_AddNetworkPrefab_UnityEngine_GameObject_ . The main ones are that the server and client both need to add all the same prefabs to the list and that if you're using NetworkConfig.ForceSamePrefabs then the loading has to happen before starting the server or the client connecting.
     
    Ralfolas likes this.
  3. Ralfolas

    Ralfolas

    Joined:
    Mar 12, 2023
    Posts:
    5
    Thanks Nyphur, I haven't thought about adding the NetworkPrefabs at runtime, but it is an interesting option! Will definitely consider it!

    Just to clarify: I am totally fine with maintaining the list of NetworkPrefabs in the NetworkManager, as I can see why it is required by the framework. What I don't want is writing my own (redundant) directory with all the spell prefabs.

    Ideally I want to access the NetworkManager's list of NetworkPrefabs to
    1. Client: Obtain the desired NetworkPrefab identifier (primitive) and send it to the server
    2. Server: Use the identifier to resolve the NetworkPrefab and spawn it
    Do you know if this is possible?
     
  4. Nyphur

    Nyphur

    Joined:
    Jan 29, 2016
    Posts:
    98
    That would be nice but I don't believe the list of network prefabs is readable at runtime.

    Since you're going to be making a script to collate all the spell effect prefabs at runtime and add them, might as well just leave the list as a public variable in that script to be accessed later. Make the script a singleton for ease of access or stick it on the NetworkManager object or something to make it easy to fetch a reference.
     
  5. Ralfolas

    Ralfolas

    Joined:
    Mar 12, 2023
    Posts:
    5
    Thanks, I'll try to solve this later this week and may post my solution here if I have the feeling it's good enough for sharing. :)

    Any other ideas are still welcome!
     
  6. Nyphur

    Nyphur

    Joined:
    Jan 29, 2016
    Posts:
    98
    If you really need to avoid having a list of the prefabs on a script somewhere, the only other option would be to look it up on the server every time an ability is cast. You could send some kind of info on which SO was used (an ID or filename or index) instead of which prefab and then the server finds and read that specific SO and fetches the prefab from it.

    Ultimately you do need a list of those SOs or prefabs somewhere on some script to access them, because the alternative would be to load them from disk every time an ability is cast and that's adding slow disk I/O to a frequent action that should be instant.

    Both the client and server will always have the exact same set of prefabs anyway, and you do need to add all the spell effect prefabs to the NetworkManager in order to spawn them so you are making a list somewhere already. I'd just leave the list in a component on the networkmanager to be looked up when needed tbh. Let me know what solution you end up going with, I'm invested now XD
     
    Ralfolas likes this.
  7. Ralfolas

    Ralfolas

    Joined:
    Mar 12, 2023
    Posts:
    5
    I will :)

    This is roughly what I'm planning:
    1. On game initialization, load all AbilityScritpableObjects from the Resources folder and:
      1. Add them to a Singleton directory script for easy access
      2. Add their prefabs to the NetworkManager's NetworkPrefabs list
    2. Define a serializable type (like "AbilityReference") with:
      1. A serializable ID field of some sort (I'll probably add a string field to my SOs for that)
      2. A constructor accepting an AbilityScriptableObject but only storing its serializable ID
      3. A method for resolving and returning the SO by looking it up in the custom directory script
    This way I should be able to easily pass and resolve references to my Abilities via RPCs.

    Edit: Done :)
    I just left out the NetworkPrefabs bit, as I would need to change my initialization order for that.

    Directory class to obtain Scriptable Objects, which also automatically loads them from my Resources folder:
    Code (CSharp):
    1. public class SoDirectory : MonoBehaviour
    2. {
    3.     private const string FOLDER_SCRIPTABLE_OBJECTS = "ScriptableObjects";
    4.  
    5.     private readonly Dictionary<string, AbilityScriptableObject> _dictionary = new();
    6.  
    7.     // Start is called before the first frame update
    8.     void Start()
    9.     {
    10.         AbilityScriptableObject[] abilitySos = Resources.LoadAll<AbilityScriptableObject>(FOLDER_SCRIPTABLE_OBJECTS);
    11.         Debug.LogError($"Initializing {abilitySos.Length} AbilityScriptableObjects...");
    12.  
    13.         foreach (AbilityScriptableObject abilitySo in abilitySos)
    14.         {
    15.             string abilityId = abilitySo.Id;
    16.  
    17.             /*Note: We could add the ability prefab to the network manager here,
    18.              but this would require the manager to be spawned but networking not
    19.              started.*/
    20.  
    21.             /*Add SO to the directory*/
    22.             if (_dictionary.ContainsKey(abilityId))
    23.             {
    24.                 Debug.LogError("Duplicate Ability ID: " + abilityId);
    25.             }
    26.             else
    27.             {
    28.                 _dictionary.Add(abilityId, abilitySo);
    29.             }
    30.         }
    31.     }
    32.  
    33.     public AbilityScriptableObject Get(string abilitySoId)
    34.     {
    35.         if (_dictionary.TryGetValue(abilitySoId, out AbilityScriptableObject abilitySo))
    36.         {
    37.             return abilitySo;
    38.         }
    39.         Debug.LogError($"Ability {abilitySoId} not found in directory.");
    40.         return null;
    41.     }
    42. }
    The serializable class that is passed in RCPs:
    Code (CSharp):
    1. public class AbilitySoReference : INetworkSerializable
    2. {
    3.     public string AbilitySoId;
    4.  
    5.     public AbilitySoReference()
    6.     {
    7.         /*Used by serializer*/
    8.     }
    9.  
    10.     public AbilitySoReference(AbilityScriptableObject abilitySo)
    11.     {
    12.         AbilitySoId = abilitySo.Id;
    13.     }
    14.  
    15.     public bool TryResolve(out AbilityScriptableObject abilitySo)
    16.     {
    17.         abilitySo = Singleton.Instance.SoDirectory.Get(AbilitySoId);
    18.         return abilitySo != null;
    19.     }
    20.  
    21.     public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    22.     {
    23.         serializer.SerializeValue(ref AbilitySoId);
    24.     }
    25.     public override string ToString()
    26.     {
    27.         return $"AbilitySoReference [AbilitySoId={AbilitySoId}]";
    28.     }
    29. }
    And finally the updated cast script. Notice that I added an AsReference() convenience method to my ScriptableObject. It just returns a new instance of AbilitySoReference.
    Code (CSharp):
    1. public class AimedCast : Ability
    2. {
    3.     public virtual bool Cast(AbilityScriptableObject abilitySo, Vector3 spawn, Vector3 target)
    4.     {
    5.         CastServerRpc(abilitySo.AsReference(), spawn, target);
    6.         return true;
    7.     }
    8.  
    9.     [ServerRpc]
    10.     public void CastServerRpc(AbilitySoReference abilityRef, Vector3 spawnPos, Vector3 target, ServerRpcParams serverRpcParams = default)
    11.     {
    12.         if(!abilityRef.TryResolve(out AbilityScriptableObject abilitySo))
    13.         {
    14.             Debug.LogError("Failed to resolve ability to cast: " +abilityRef);
    15.             return;
    16.         }
    17.  
    18.         GameObject projectile = Instantiate(abilitySo.AbilityPrefab);
    19.         projectile.transform.position = spawnPos;
    20.  
    21.         /*Set direction of projectile.*/
    22.         projectile.GetComponent<TargetableProjectile>().SetTarget(target);
    23.  
    24.         /*Spawn the projectile*/
    25.         projectile.GetComponent<NetworkObject>().SpawnWithOwnership(serverRpcParams.Receive.SenderClientId, true);
    26.     }
    27. }
     
    Last edited: Apr 18, 2023
    Riccardoric, TimurKut and Nyphur like this.