Community-driven PUN integration for UIS/UCC

ChristianWiele

Active member
Hi,

as you might know, there is currently no officical integration for UIS and UCC with the PUN multiplayer add-on. Still I wanted to use UIS for my project so I started building my own integration. It's not done yet but I thought it might be a good idea to continue it as a community-driven solution from here. This way more of you can benefit from / test the solution, and we might help @Justin to quicker get to an official solution. Unfortunately, the current solution requires the modification of Opsive code, so anytime the PUN add-on is updated you have to manually add the required changes.

These are the areas I have worked on / identified so far:

* basic equip / uneqip
* sync items on player entering room (open)
* pickup / drop (mostly open)
* UI
* crafting

Enjoy, and let's drive this jointly forward.

BTW, don't try this at home, if you know what I mean :)

--- EQUIP/UNEQUIP ---
This part gave me the biggest headaches as UIS and UCC use fundamentally different mechanisms to add / remove items to the character.
UCC with its simple inventory use the following mechanism to add / equip items across the network:
* all equippable items need to be added to the RuntimePickup component of the PunGame gameobject
* as the character spawns all these items are added as children to the Items gameobject of the character
* each item has a static / fixed itemIdentifierID
* upon equip/unequip the PunCharacter component sends this itemIdentifierID to the remote machines
* on the remote machine the item is resolved using the ItemIdentifierTracker which provides a static mapping between itemIdentifierID and the corresponding item
* the corresponding gameobjects of the equipped / unequipped item are then activated / deactivated

The UIS uses a different approach:
* first there are different stages in the inventory (item collections). If you want to equip an item it first has to be moved into the Equippable item collections
* only if an item is in the Equippable collection the corresponding gameobjects are added dynamically to the character
* each item has a random itemIdentifierID which might change upon respawn (this is an instance ID while in UCC this is a class ID)
* in addition each item has a fixed itemIdentifierDefinitionID (class ID)

The main issue is the dynamic itemIdentifierID in UIS. I tried to find the least invasive solution, but the only way was to modify the PunCharacter and ItemIdentifierTracker scripts
* the PunCharacter resolves the itemIdentifierDefinitionID, and sends both itemIdentifierID and itemIdentifierDefinitionID to the remote client
* the ItemIdentifierTracker is modified to allow for registration of the dynamic itemIdentifierID
* each time the equipped/unequipped item is changed, a corresponding item is created/destroyed on the remote side.

In the following I only list the added/modified methods in the two scripts.

I added these methods to the ItemIdentifierTracker.cs. These methods allow to dynamically add and remove itemIdentifier IDs. These modifications have to be done in the original file as the PUN add-on accesses the ItemIdentifierTracker through static class methods.

Code:
        public static void AddItemIdentifier(uint id, IItemIdentifier itemIdentifier)
        {
            Instance.AddItemIdentifierInternal(id, itemIdentifier);
        }

        public void AddItemIdentifierInternal(uint id, IItemIdentifier itemIdentifier)
        {
            if(m_IDItemIdentifierMap.TryGetValue(id, out var existingItemIdentifier))
            {
                return;
            }
            m_IDItemIdentifierMap.Add(id, itemIdentifier);
        }

        public static void RemoveItemIdentifier(uint id)
        {
            Instance.RemoveItemIdentifierInternal(id);
        }

        public void RemoveItemIdentifierInternal(uint id)
        {
            if(!m_IDItemIdentifierMap.TryGetValue(id, out var existingItemIdentifier))
            {
                return;
            }
            m_IDItemIdentifierMap.Remove(id);
        }

And here are the modifications for the PunCharacter script. You can copy the PunCharacter script to something like PunCharacter4UIS and do the modifications in the copy. Then the modifications are not overridden when importing an update to the PUN add-on. Still you have to manually check for code changes in the PunCharacter then.

Code:
    using Opsive.UltimateInventorySystem.Core.DataStructures;
    using Opsive.UltimateInventorySystem.Core;
    using Opsive.UltimateCharacterController.Integrations.UltimateInventorySystem;

        private void Awake()
        {
            m_GameObject = gameObject;
            m_CharacterLocomotion = m_GameObject.GetCachedComponent<UltimateCharacterLocomotion>();
            m_Inventory = m_GameObject.GetCachedComponent<InventoryBase>();

            // Added for the UIS integration
            characterInventoryBridge = m_GameObject.GetCachedComponent<CharacterInventoryBridge>();
        }

        public void EquipUnequipItem(uint itemIdentifierID, int slotID, bool equip)
        {
            var inventoryItemDefinitionID = InventorySystemManager.GetItem(itemIdentifierID).ItemDefinition.ID;
            photonView.RPC("EquipUnequipItemRPC", RpcTarget.Others, itemIdentifierID, inventoryItemDefinitionID, slotID, equip);
        }

        [PunRPC]
        private void EquipUnequipItemRPC(uint itemIdentifierID, uint inventoryItemDefinitionID, int slotID, bool equip)
        {
            if (equip) {
                // The character has to be alive to equip.
                if (!m_CharacterLocomotion.Alive) 
                {
                    return;
                }
                PickupItems();

                // Register Item if it is not already registered
                if(ItemIdentifierTracker.GetItemIdentifier(itemIdentifierID) == null ) 
                {
                    var addedItem = AddItemToEquippable(itemIdentifierID, inventoryItemDefinitionID);
                    RegisterItem(addedItem);
                }
            }

            var itemIdentifier = ItemIdentifierTracker.GetItemIdentifier(itemIdentifierID);
            if (itemIdentifier == null) {
                return;
            }

            var item = m_Inventory.GetItem(itemIdentifier, slotID);
            if (item == null) {
                return;
            }

            if (equip) {
                if (m_Inventory.GetActiveItem(slotID) != item) {
                    EventHandler.ExecuteEvent<Opsive.UltimateCharacterController.Items.Item, int>(m_GameObject, "OnAbilityWillEquipItem", item, slotID);
                    m_Inventory.EquipItem(itemIdentifier, slotID, true);
                }
            } else {
                EventHandler.ExecuteEvent<Opsive.UltimateCharacterController.Items.Item, int>(m_GameObject, "OnAbilityUnequipItemComplete", item, slotID);
                m_Inventory.UnequipItem(itemIdentifier, slotID);

                m_Inventory.RemoveItem(itemIdentifier, slotID, 0, false);

                InventorySystemManager.ItemRegister.Unregister(itemIdentifierID);

                UnRegisterItem(itemIdentifierID);

            }
        }

        public void ItemIdentifierPickup(uint itemIdentifierID, int amount, int slotID, bool immediatePickup, bool forceEquip)
        {

        }
    

        private ItemInfo AddItemToEquippable(uint characterItemID, uint inventoryItemDefinitionID)
        {
            
            var itemInfo = GetItemInfo(characterItemID, inventoryItemDefinitionID);
            var addedItem = characterInventoryBridge.EquippableItemCollections.AddItem(itemInfo);
            
            return addedItem;
        }

        private ItemInfo GetItemInfo(uint characterItemID, uint inventoryItemDefinitionID)
        {
            var itemDefinition = InventorySystemManager.GetItemDefinition(inventoryItemDefinitionID);
            var inventoryItem = InventorySystemManager.CreateItem(itemDefinition, characterItemID);
            
            var itemAmount = new ItemAmount(inventoryItem, 1);
    
            return new ItemInfo(itemAmount);
        }

        private void RegisterItem(ItemInfo itemInfo)
        {
            ItemIdentifierTracker.AddItemIdentifier(itemInfo.Item.ID, itemInfo.Item);
        }

        private void UnRegisterItem(uint itemID)
        {
            ItemIdentifierTracker.RemoveItemIdentifier(itemID);
        }


--- SYNC STATE (open) ---
I have not yet adapted the OnPlayerEnteredRoom method and the corresponding RPCs of the PunCharacter script. This method syncs the items when a new player enters the room.

--- PICKUP / DROP ---
Pickups work if you don't want to don't need the ability to also drop them.
* use the InventorItemPickup script (UCC method for pickups)
* add PhotonView, PunRespawnMonitor, PunNetworkInfo, PunLocationMonitor
* disable "Pickup On Trigger Enter", I ran into issues with this enabled

For pickups to be droppable a new script is required that corresponds to the PunItemPickup script for UCC. This script sets up the pickup on remote machines. Should not a big deal, but had no time and need so far to do it.


(Holy cow, exceeding 10.000 characters so I have to split the post ...)
 
--- UI ---
To get the UIS UI working in multiplayer mode you need to do the following:
* each player needs to have a unique ID for the InventoryIdentifier script. I use different prefabs, but you can also assign it dynamically at spawn time.
* add the PunInventoryUIHandler script to an empty gameobject. This script assigns the correct owner to the UI and panels.
* assign the Inventory Canvas to the inventoryUI parameter
* assign the InventoryMonitor and CurrencyOwnerMonitor accordingly

Code:
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using Photon.Pun;
    using Photon.Realtime;
    using Opsive.Shared.Events;
    using Opsive.UltimateInventorySystem.UI.Panels;
    using Opsive.UltimateInventorySystem.Core.InventoryCollections;
    using Opsive.UltimateInventorySystem.UI.Monitors;
    using Opsive.UltimateInventorySystem.Exchange;

    public class PunInventoryUIHandler : MonoBehaviourPunCallbacks
    {
        [SerializeField] GameObject inventoryUI;
        [SerializeField] InventoryMonitor inventoryMonitor;
        [SerializeField] CurrencyOwnerMonitor currencyOwnerMonitor;

        void Awake()
        {
            EventHandler.RegisterEvent<Player, GameObject>("OnPlayerEnteredRoom", OnPlayerEnteredRoom);
        }

        private void OnPlayerEnteredRoom(Player player, GameObject playerObject)
        {
            if(player.IsLocal)
            {
                var inventoryID = playerObject.GetComponent<InventoryIdentifier>().ID;

                var displayManager = inventoryUI.GetComponent<DisplayPanelManager>();
                displayManager.SetPanelOwner(playerObject);

                if(currencyOwnerMonitor != null)
                {
                    currencyOwnerMonitor.SetCurrencyOwner(playerObject.GetComponent<CurrencyOwner>());
                }

                if(inventoryMonitor != null)
                {
                    inventoryMonitor.SetMonitoredInventory(playerObject.GetComponent<Inventory>());
                }

                EventHandler.ExecuteEvent("LateStartObjects");
            }   
        }

        private void OnDestroy()
        {
            EventHandler.UnregisterEvent<Player, GameObject>("OnPlayerEnteredRoom", OnPlayerEnteredRoom);
        }
    }

--- CRAFTING ---
If you want to use crafting you need to do the following:
* Create your CraftStable, put it into your scene and disable it.
* add the PunLateStartObjects to an empty gameobject
* add your CraftStable to the list of late start LateStartObjects

The CraftStable requires that the character, inventory, and UI are correctly setup first. The late start objects are triggered by the PunInventoryUIHandler.

Code:
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using Opsive.Shared.Events;

    public class PunLateStartObjects : MonoBehaviour
    {
        [SerializeField] GameObject[] lateStartObjects;
       
        private void Awake() 
        {
            EventHandler.RegisterEvent("LateStartObjects", OnLateStartObjects);
        }

        private void OnLateStartObjects()
        {
            foreach (GameObject go in lateStartObjects)
            {
                go.SetActive(true);
            }
        }


        private void OnDestroy()
        {
            EventHandler.UnregisterEvent("LateStartObjects", OnLateStartObjects);
        }

    }
 
Good afternoon/night. Tell me, have you synchronized the inventory IDs over the network? Or do they not participate in your test implementation in any way? It's just that there are interactive corpses in my scene, and it turns out you can loot the inventory of the killed character, plus loot boxes on the stage. I am at the beginning of the implementation path, and I asked myself this question.
 
Last edited:
Hi, I'm getting issues with the PUNCharacter script, I'm getting the following error: The type or namespace name 'UltimateInventorySystem' does not exist in the namespace 'Opsive'.
I've tried reimporting everything, but I can't seem to fix it. I'm using UCC 2.4.5 and UIS 1.2.6.

Do you have any ideas?
 
The lines where the error is occurring are the three using statements:
using Opsive.UltimateInventorySystem.Core.DataStructures;
using Opsive.UltimateInventorySystem.Core;
using Opsive.UltimateCharacterController.Integrations.UltimateInventorySystem;
 
I'm not sure how to go about adding the definition, I've followed unitys tutorials on creating and editing assemblies but it seems to cause more errors and doesn't fix the problem. Is there a specific assembly that needs to be edited?
 
I'm almost there with my project, I've got the inventory working, and the equip/unequip working as well. The only issue I have is that a remote players equipped items are not displaying on the local players screen, though the remote player does look like they are holding items. For what I can gather, the UIS instantiates the prefabs into the UCC character and what needs to happen is that it should instantiate using photon. Could you point me to the right script that instantiates the prefabs?
 
So the RPC should equip and unequip items on the remote player? If thats true then where would I run the RPC when I'm on the inventory screen? The above example, to my understanding, only updates when a player enters the room and not when a player equips from the inventory
 
This is huge, thank you so much! I was about to start doing this myself, without knowing what to do! This is definitely a good starting point, you're a life saver.
 
Hey, so I was wondering if anyone here could help me with the syncing item drops over the network? My current solution works a little, but it isn't great. I made a PUN version of the InventoryPickup script that I'll attach below. I also added:

NetworkObjectPool.NetworkSpawn(InventoryItemPickupPrefab, spawnedInventoryPickupObject, false);

to the BridgeDropProcessing script in the SpawnDropItem method, right before it returns the inventory pickup. The object pool is giving me "ArgumentException: An item with the same key has already been added. Key: InventoryItemPickup(Clone) (UnityEngine.GameObject)" after a pickup is dropped the second time. Also, the active state sometimes fails to be updated by the PunLocationMonitor. I'm using the code from Christian's post, so everything is pretty much working except for item drops.

I'm new to using PUN, so I'm super confused at this point. If anyone could provide any info, that would be awesome. Thank you!
 

Attachments

  • PunInventoryPickup.cs
    3.6 KB · Views: 11
Top