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

Time:10-29

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?

CodePudding user response:

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);
        }
    }
}
  • Related