Search code examples
c#unity-game-enginephoton

How to spawn/instantiate multiple game object without using Photon View Id?


Game Definition:

I am creating a game involving spawning multiple objects (foods) at random places. The food will be destroyed when the player touches it. The number of foods will be more than 2000.

Problem:

I want these foods to show in all of the players' game environments. I am instantiating it from the Master, and all foods are using Photon View ID; however, the limit of ViewID is only 999. I tried increasing the maximum, but I am worried that it will cause problems like bandwidth issues.

Is there any way where I can synchronize the foods to all the players without using a lot of ViewID?


Solution

  • Create your own network ID and manager!

    Depending on your needs the simplest thing would be to have a central manager (MasterClient) spawning food instances and assign them a unique ID. Then tell all other clients to also spawn this item and assign the same ID (e.g. using RPCs with all required parameters). Additionally for handling switching of MasterClient keep a list of all existing IDs e.g. in the Room properties so in case of a switch the new masterclient can take over the job to assign unique IDs => No limits ;)

    Of course this can get quite "hacky" and you have to play around a bit and test it really well!

    Note: The following code is untested and typed on a smartphone! But I hope it gives you a good starting point.

    This class would go onto the Food prefab so every food has this custom network identity

    // Put this on your food prefab(s)
    public class FoodID : MonoBehaviour
    {
        // The assigned ID
        public uint ID;
    
        // An event to handle any kind of destroyed food no matter for what reason
        // in general though rather go via the FoodManagement.DestroyFood method instead
        public static event Action<FoodID> onDestroyed;
    
        private void OnDestroy()
        {
            onDestroyed?.Invoke(this);
        }
    }
    

    and this would go onto your player or into the scene so your other scripts can communicate with it and it has the authority to send RPCs around ;)

    public class FoodManagement : MonoBehaviourPunCallbacks
    {
        [FormerlySerializedAs("foodPrefab")]
        public FoodID foodIDPrefab;
    
        // keep track of already ued IDs
        private readonly HashSet<uint> _usedIDs = new HashSet<uint>
        {
            // by default I always block the 0 because it means invalid/unassigned ID ;)
            0
        };
    
        // keep references from ID to food LOCAL
        private readonly Dictionary<uint, FoodID> _foodInstances = new Dictionary<uint, FoodID>();
    
        // instance for random number generation used in GetRandomUInt
        private readonly Random _random = new Random();
    
        private void Awake()
        {
            // Register a callback just to be sure that all kind of Destroy on a Food object is handled forwarded correctly
            FoodID.onDestroyed += DestroyFood;
        }
    
        private void OnDestroy()
        {
            // In general make sure to remove callbacks once not needed anymore to avoid exceptions
            FoodID.onDestroyed -= DestroyFood;
        }
    
        // Register a food instance and according ID to the dictionary and hashset
        private void AddFoodInstance(FoodID foodID)
        {
            _usedIDs.Add(foodID.ID);
            _foodInstances.Add(foodID.ID, foodID);
        }
    
        // Unregister a foo instance and according ID from the dictionary and hashset
        private void RemoveFoodInstance(uint id)
        {
            _usedIDs.Remove(id);
            _foodInstances.Remove(id);
        }
    
        // Get a unique random uint ID that is not already in use
        private uint GetFreeID()
        {
            uint id;
            do
            {
                id = GetRandomUInt();
            } while (id == 0 || _usedIDs.Contains(id));
    
            return id;
        }
    
        // Generates a random uint
        private uint GetRandomUInt()
        {
            var thirtyBits = (uint)_random.Next(1 << 30);
            var twoBits = (uint)_random.Next(1 << 2);
            var fullRange = (thirtyBits << 2) | twoBits;
    
            return fullRange;
        }
    
        // Create a new Food instance network wide on the given location
        public void SpawnFood(Vector3 position)
        {
            // Make sure only the current Master client creates unique IDs in order to get no conflicts
    
            if (PhotonNetwork.IsMasterClient)
            {
                SpawnFoodOnMaster(position);
            }
            else
            {
                photonView.RPC(nameof(SpawnFoodOnMaster), RpcTarget.MasterClient, position);
            }
        }
    
        // Only the master client creates IDs and forwards th spawning to all clients
        private void SpawnFoodOnMaster(Vector3 position)
        {
            if (!PhotonNetwork.IsMasterClient)
            {
                Debug.LogError($"{nameof(SpawnFoodOnMaster)} invoked on Non-Master client!");
                return;
            }
    
            var id = GetFreeID();
    
            photonView.RPC(nameof(RPCSpawnFood), RpcTarget.All, id, position);
        }
    
        // Finally all clients will spawn the food at given location and register it in their local ID registry
        private void RPCSpawnFood(uint id, Vector3 position)
        {
            var newFood = Instantiate(foodIDPrefab, position, Quaternion.identity);
            newFood.ID = id;
    
            AddFoodInstance(newFood);
        }
    
        // Destroy the given Food network wide
        public void DestroyFood(FoodID foodID)
        {
            DestroyFood(foodID.ID);
        }
    
        // Destroy the Food with given ID network wide
        public void DestroyFood(uint id)
        {
            if (PhotonNetwork.IsMasterClient)
            {
                DestroyFoodOnMaster(id);
            }
            else
            {
                photonView.RPC(nameof(DestroyFoodOnMaster), RpcTarget.MasterClient, id);
            }
        }
    
        // The same as for the spawning: Only the master client forwards this call
        // Reason: This prevents conflicts if at the same time food is destroyed and created or
        // if two clients try to destroy the same food at the same time
        void DestroyFoodOnMaster(uint id)
        {
            if (!_usedIDs.Contains(id))
            {
                Debug.LogError($"Trying to destroy food with non-registered ID {id}");
                return;
            }
    
            photonView.RPC(nameof(RPCDestroyFood), RpcTarget.All, id);
        }
    
        // Destroy Food ith given id network wide and remove it from the registries
        void RPCDestroyFood(uint id)
        {
            if (_foodInstances.TryGetValue(id, out var food))
            {
                if (food) Destroy(food.gameObject);
            }
    
            RemoveFoodInstance(id);
        }
    
        // Once you join a new room make sure you receive the current state
        // since our custom ID system is not automatically handled by Photon anymore
        public override void OnJoinedRoom()
        {
            base.OnJoinedRoom();
    
            if (PhotonNetwork.IsMasterClient) return;
    
            photonView.RPC(nameof(RequestInitialStateFromMaster), RpcTarget.MasterClient, PhotonNetwork.LocalPlayer);
        }
    
        // When a new joined clients requests the current state as the master client answer with he current state
        private void RequestInitialStateFromMaster(Player requester)
        {
            if (!PhotonNetwork.IsMasterClient)
            {
                Debug.LogError($"{nameof(RequestInitialStateFromMaster)} invoked on Non-Master client!");
                return;
            }
    
            var state = _foodInstances.Values.ToDictionary(food => food.ID, food => food.transform.position);
    
            photonView.RPC(nameof(AnswerInitialState), requester, state);
        }
    
        // When the master sends us the current state instantiate and register all Food instances
        private void AnswerInitialState(Dictionary<uint, Vector3> state)
        {
            foreach (var kvp in state)
            {
                RPCSpawnFood(kvp.Key, kvp.Value);
            }
        }
    }