Syncing Visual Effects of Fake Items with Real Items on Grab in Unity Netcode

Hello everyone,

I’m working on a multiplayer project using Unity’s Netcode where I’m facing an issue with syncing visual effects between real and fake items when a player grabs an item. My goal is to ensure that the visual state (like on/off for a lantern) of a fake item matches the real item’s state immediately when grabbed.

Here’s a simplified version of my PlayerInventory script where I handle the item grabbing and attempt to synchronize the state:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;

public class PlayerInventory : NetworkBehaviour {
    [SerializeField] private List<GrabbableObject> _grabbableObjects = new List<GrabbableObject>();
    private int _currentIndex = 0;

    public void AddGrabbableObject(GrabbableObject grabbableObject) {
        if (!IsOwner) return;

        _grabbableObjects.Add(grabbableObject);
        grabbableObject.SetInventoryIndex(_grabbableObjects.Count - 1);
        _currentIndex = grabbableObject.GetInventoryIndex();
        ShowObject(_currentIndex);

        if (grabbableObject is IVisualEffectItem visualEffectItem) {
            SyncFakeItemWithRealItem(visualEffectItem);
        }
    }

    private void SyncFakeItemWithRealItem(IVisualEffectItem visualEffectItem) {
        bool isActive = visualEffectItem.IsEffectActive();
        foreach (var visualItem in GetComponentsInChildren<VisualItem>()) {
            if (visualItem.playerFakeItemTypeID == visualEffectItem.GetInteractableObjectSO().itemTypeID) {
                visualItem.SetEffectActive(isActive);
            }
        }
    }

    private void ShowObject(int index) {
        // Logic to activate/deactivate game objects in inventory based on index
    }
}

Issue: When a player grabs an item (like a lantern), I want the fake item’s visual effect (light on/off) to immediately reflect the state of the real item. However, the synchronization doesn’t always happen as expected. Sometimes the state is not updated instantly or accurately reflects the real item’s state when grabbed.

Could anyone suggest how I might improve the synchronization between the real item’s state and the fake item’s visual effect when an item is picked up? I’m looking for a reliable way to ensure that when an item is grabbed, its visual state is instantly synced across all clients correctly.

Thank you in advance for your suggestions and help!

This is because Netcode for GameObjects is server authoritative and you’ll always need a round trip to confirm actions and thus latency can easily create desynced visuals. So behaviors you want to be synced as described need to be client authoritative or use client prediction. Since these objects seem to be globally available for interaction by any player and you’re also not using distributed authority in Unity 6, you’ll need to go the client prediction route.

Netcode for GameObjects doesn’t provide client prediction, but does provide an anticipation API.

As a general technique you can also consider hiding some latency by creating larger trigger areas for the action you’re taking and masking things with animations that “ramp up” to the final state, to give the network latency a chance to resolve.

1 Like

I’m not 100 sure about if i need prediction or anticipation(I think I knew this was something entirely differnt sorry:)), you’re right I’m not in Unity6 and i can’t use the distribution magic but currently what i do is in my original lantern object is;

a player triggers Use from it’s inventory →
RequestLightToggleServerRPC {
SameThing’sClientRPC}

SameThing’sClientRPC{
setNetworkVariabletoTrue}

This works very well for my original network prefabs actually. The game is not competitive though I’ll have to mention.

The main problem might be my experience in coding as well but I’ll try to make it work with when you hold an object, find it’s networkobjectID reference, read it, find out if it has networkvariable nad it’s state, set yourself accordingly…

I was allured by chatGPT magic and tried few things with it but i realized it f’ed up part of the project by now. So I try to stay away from it

What is the meaning and difference of “fake” and “real” items? Those are not established concepts. Unless you mean local vs remote items.

In any case when a local player picks up a torch, you do both things:

  • add a torch to player’s hands with the expected status (light on) - this happens only locally, the torch held in the player’s hand is a non-networked object
  • send a ServerRpc to confirm the pickup

If the server responds with a “can’t do that” you’ll remove the local torch from the player’s hands. Otherwise the server will despawn the world item and update its internal state that this player is holding a lit torch. Whether the torch is burned off and should not have a flame is a state of the torch that’s already synchronized because it can either be set upon spawning the world torch or uses the last state of the last usage of the torch.

You’re absolutely right, sorry about that! :blush:

To clarify:

  • Fake objects: These are TPS and FPS child objects that are not networked. For example, I have a TorchFakeItemVısual(FakeItem.cs) under the player’s camera (for the local player) and body (for others to see).
  • Real object: This is the networked torch, which has a NetworkVariable called isON.
  • Player inventory: It also has a NetworkVariable, something like isFakeItemActive, to manage state locally.

I started realizing that my RPCs might not be set up correctly now. I’ve been experimenting a bit (maybe relying on AI assistance too much :sweat_smile:). Currently, I’m working on cleaning up the code because the naming and use cases are a bit too specific. Until I address this, I’m holding off on modularizing anything further.

Here is fake item

PlayerInventory _playerInventory;

[SerializeField] private Transform parent;
[SerializeField] private GameObject visualEffect; // Light or other effect



private void OnEnable() {
    _playerInventory.OnPlayerEquipped += OnItemEquipped;
}

private void OnDisable() {
    _playerInventory.OnPlayerEquipped -= OnItemEquipped;
}

private void Awake() {
    InitReferences();
    if (visualEffect == null) {
        Debug.Log("Visual Effect not assigned.");
    }
}

private void InitReferences() {
    ikController = GetComponentInParent<IKController>();

    if (ikController == null)
        return;
    InitializeIKTarget();
    _playerInventory = GetComponentInParent<PlayerInventory>();
}


public void OnItemEquipped(int equippedID) {
    bool isActive = playerFakeItemTypeID == equippedID;
    //SetEffectActive(isActive);
    parent.gameObject.SetActive(isActive);
}

private void Update() {
    if (Input.GetKeyDown(KeyCode.V)) {
        var playerInventory = GetComponentInParent<PlayerInventory>();
        if (playerInventory != null) {
            var equippedItem = playerInventory.GetCurrentlyHeldItem();
            if (equippedItem != null && equippedItem is IVisualEffectItem effectItem) {
                bool isActive = effectItem.IsEffectActive();
                Debug.Log($"[FakeItem] V Pressed - ItemEquipID: {equippedItem.GetInteractableObjectSO().itemTypeID}, NetworkObjectID: {equippedItem.GetNetworkObject().NetworkObjectId}, State: {isActive}");
            } else {
                Debug.Log("[FakeItem] V Pressed - No item is currently equipped or does not implement IVisualEffectItem.");
            }
        }
    }
}




public void SetEffectActive(bool isActive) {
    if (visualEffect != null) {
        visualEffect.SetActive(isActive);
    }
}

here is the real data coming from playerinventory/grabbable object.


    private void OnEquippedItemChanged(int previousValue, int newValue) {
        Debug.Log($"Equipped item changed from {previousValue} to {newValue}");

        if (newValue == -1) {
            Debug.Log("No item equipped.");
        } else {
            Debug.Log($"Equipped item ID: {newValue}");
        }

        OnPlayerEquipped?.Invoke(newValue);
        UpdatePlayerHoldingFakeItemVisual(); // Update visuals or player state
        UpdatePlayerHoldingFakeItemVisualStateServerRPC();
    }
    public void ActivateItem(int itemID) {
        var currentlyHeldItem = GetCurrentlyHeldItem();
        if (currentlyHeldItem != null && currentlyHeldItem.GetInteractableObjectSO().itemTypeID == itemID) {
            // Use method will toggle the item's active state
            currentlyHeldItem.Use();
            // After Use, check the new active state and update visuals accordingly
            bool newItemState = ((IVisualEffectItem)currentlyHeldItem).IsEffectActive();
            ToggleFakeItemsEffect(itemID, newItemState);
        }
    }

    private void ToggleFakeItemsEffect(int itemID, bool newState) {
        if (IsServer) {
            ActivateVisualItemClientRpc(itemID, newState);
        } else {
            ActivateVisualItemServerRpc(itemID, newState);
        }
    }

    [ServerRpc(RequireOwnership = false)]
    private void ActivateVisualItemServerRpc(int itemID, bool newState) {
        ActivateVisualItemClientRpc(itemID, newState);
    }

    [ClientRpc]
    private void ActivateVisualItemClientRpc(int itemID, bool newState) {
        foreach (var visualItem in GetComponentsInChildren<FakeItem>()) {
            if (visualItem.playerFakeItemTypeID == itemID) {
                visualItem.SetEffectActive(newState);
                break;
            }
        }
    }
    public void EquipVisualItem(int id) {
        if (IsOwner) // Eğer bu client'ın sahibi isen
        {
            RequestEquipItemServerRpc(id); // Sunucuya bildir
        }
    }

    [ServerRpc(RequireOwnership = false)]
    private void RequestEquipItemServerRpc(int id, ServerRpcParams rpcParams = default) {
        equippedItemId.Value = id;
    }


    [ServerRpc(RequireOwnership = false)]
    private void RequestUnequipVisualEquippedItemServerRpc() {

        equippedItemId.Value = -1; // Unequip for visuals only

    }

    public void UpdatePlayerHoldingFakeItemVisual() {
        var heldItem = GetCurrentlyHeldItem();
        if (heldItem != null) {
            // Access the itemTypeID from the held item's ScriptableObject
            int equipmentID = heldItem.GetInteractableObjectSO().itemTypeID;

            Debug.Log($"Currently held item has Equipment ID: {equipmentID}");

            // Update player's situation based on the equipment ID
            EquipVisualItem(equipmentID); // Trigger visual/functional updates
        } else {
            Debug.Log("No item currently held.");
        }
    }

Ok I’m equipping the items correctly… (BECAUSE IT WORKS)
But I might have forget to do same for updating the state of those fake items…
on each action via RPC OR tying that variable to act on something …