如何在不使用 Photon View Id 的情况下 spawn/instantiate 多个游戏对象?

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

游戏定义:

我正在创建一个涉及在随机位置生成多个对象(食物)的游戏。当玩家接触食物时,食物会被摧毁。食物的数量将超过2000。

问题:

我想让这些食物出现在所有玩家的游戏环境中。我正在从 Master 实例化它,所有食物都使用 Photon View ID;但是ViewID的限制只有999,我试过调大最大值,但是担心会出现带宽问题

有什么方法可以在不使用大量 ViewID 的情况下将食物同步给所有玩家?

创建您自己的网络 ID 和管理员!

根据您的需要,最简单的事情是让中央管理器 (MasterClient) 生成食物实例并为它们分配一个唯一的 ID。然后告诉所有其他客户端也生成此项目并分配相同的 ID(例如,使用具有所有必需参数的 RPC)。此外,为了处理 MasterClient 的切换,请保留所有现有 ID 的列表,例如在 Room 属性中,因此在发生切换时,新的 masterclient 可以接管工作以分配唯一 ID => 没有限制 ;)

当然,这可能会变得非常“hacky”,您必须尝试一下并真正测试好它!

注意:以下代码未经测试,是在智能手机上输入的!但我希望它能给你一个好的起点。

这个 class 将进入 Food 预制件,因此每个食物都有这个自定义网络标识

// 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);
    }
}

这将进入您的播放器或场景,以便您的其他脚本可以与其通信,并且它有权发送 RPCs ;)

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