Unity Netcode for Gameobjects - Parenting picked-up objects

I like many others before me, encountered a problem while using Unity’s netcode for gameobject while attempting to parent objects picked up by a player.

What I usually do (non-multiplayer)

Usually I would let the player pick up an item, in this scenario lets say a weapon since its a very common usecase. I would proceed by setting the weapon as a child of the players right or left hand, under the player models bone hierachy.

This is an easy way to make the weapon or item follow the players movement, and more importantly the hand animations ie while aiming, moving and otherwise..

What happens in Netcode for Gameobjects

However, while using the netcode for gameobjects, it is not directly possible to parent an object that is a network object under another that is not. Furthermore it is not possible to have network object on the players hand, while also having having the root player object as a network object ie nested network objects are not allowed as a prefab.

Unity proposes a solution that you can spawn the player root object, and then spawn the hands seperatly and then parenting the hands and in turn parent whatever item the player should hold under the hands.

While this may work for some, its not viable for me. Considering I have several sockets for weapon and equipment, ie right hand, left hand, left shoulder, right shoulder etc. The list quickly grows very large. I found it overly complicated instancing and spawning individual body parts and then assembling them like another Dr. Frankenstein.

Solution - TL;DR

Enter Animation rigging. When using Netcode for gameobjects, you can parent the picked up item under the player root, by just utilizing the TrySetParent method. This will place the item directly as a child under the player root. Great, now it follows the player around, but not the hand animation.

We add a ParentConstraint to the weapon. When picking up the weapon, we simply allocate the appropriate object ie the right hand as constraint source on the weapon.

Thats it, the weapon now follows the right hand animation as it should.

Of course this requires some fine tuning, for many cases, the player model right hand bone is rotated in an angle that will make the player wield the weapon unappropriately. As such, set up an offset object below the hand object, that is rotated appropriately, and use the offset as constraint source.

Practical implementation - The details

As mentioned above, I have many sockets for equipment. The player can wield weapons and items in both hands, holster weapons on either side of the hips and on either shoulder, attach points on the lower arm for whatever birds may land and stick around for a bit.

Furthermore, I like to use animation rigging for actions such as player aiming, it just looks better when the player actually aims the weapon towards the cross hairs.

Because of this, I initially made an animation rig on the character. Each rig holds a relevant constraint ie MultiParentConstraint for the arms or Two-Bone IK constraint for legs. Each rig then has its own child offset, which will be the attach point and constraint source for the weapon we pick up.

Each socket rig has a constraint component that constraints it to the corresponding object in the character model armature. The Offset of each socketrig is it self, the constraint source for the item we want the character to use.

    //class for referencing individual SocketRig
    public class SocketRigs{
    
        public enum SocketRig{
            RightHand,
            LeftHand,
            RightShoulder,
            LeftShoulder,
        }
        //populate the dictionary with all the relevant socketRigs and their corresponding offsets as transforms
        Dictionary<SocketRIg,Transform> socketMap = new Dictionary<SocketRig,Transform>();
        
        public Transform GetSource(SocketRig _rig){
            return socketMap[_rig].Value;
        }
        
    }
    
    //class on the item object, could be abstract or an interface 
    public class Item{
        ParentConstraint itemConstraint;
        ConstraintSource constraintSource;
        SocketRig itemSocket;
        SocketRigs characterSockets;
        
        //Method to be called when equipping item 
        public void EnableSocketConstraint(NetworkObject parentNetworkObject)
        {
            var parentSockets = parentNetworkObject.GetComponent<SocketRigs>();
            constraintSource.sourceTransform = parentSockets.GetSource(itemSocket);
            constraintSource.weight = 1.0f;
            itemConstraint.AddSource(constraintSource);
            itemConstraint.constraintActive = true;
        }
        
        //Method to be called when storing, dropping or otherwise removing the constraint relation
        public void DisableSocketConstraint()
        {
            if (itemConstraint.sourceCount > 0)
            {
                itemConstraint.constraintActive = false;
                itemConstraint.RemoveSource(0);
            }
        }    
    }

These classes are obviously overly simplified in order to show the most basic working implementation. I initially call the EnableSocketConstraint through overriding OnNetworkObjectParentChanged(NetworkObject parentNetworkObject) chekcing if to DisableSocketConstraint() if parent is null, otherwise attempt to attach the constraint to the source.

Conclusion

Developing multi-player games are obviously more complicated than single-player. You have to take into consider how much data you have to send over the network. Keep that in mind when designing the game. In this example, the weapon or item the player has constrained in his hand, constantly updates its position and rotation over the network as a seperate network object. It could just as well be a local reference instead. In another project, I solved this by having each player know the complete list of items ahead of time. Equiping and holstering weapons by one player sent a serverRPC equip request, the server then sends a clientRPC to all the players EquipWeapon(int playerID, int weaponID) all clients then spawned the weapon locally, using the approach that I wrote about initially in this article. This way the weapon was not really a network object, it was just a cosmetic appendance to the player.

There are multiple ways to solve this problem, the more ways we know, the better we are at selecting the best approach in a given scenario.

I hope you found this article useful.

/Peter