9. Select Player Avatar (Write Better Netcode)


Now that we can spawn players, we want to have a selectable Avatar. The Avatar is the visual representation of the player.

Typically, commonly, mistakenly developers will create multiple networked player prefabs, one for each Avatar.

Then they wonder: how to make a selectable Avatar when there’s only one Player Prefab entry in NetworkManager‘s Inspector?

The solution to select player Avatar is to instantiate an Avatar object inside the universal ModularPlayer prefab.

Over time, we’ll use the same pattern for Camera, Character Controller, Collider, and so on. None of these are network objects! All of them are replaceable!

The ModularPlayer Thus Far

On purpose, there’s nothing in the ModularPlayer prefab:

ModularPlayer enables to Select Player Avatar
The ModularPlayer prefab and components at this stage.

Well, the player currently does have this capsule collider:

This collider is the uniform collider for all Avatars.

The collider may also become a customizable option. But we can go a long way assuming that all Avatar colliders are alike. In fact, for many games a uniform collider enhances fairness and predictability.

The Avatar Prefabs

I made some inclusive Avatar prefabs for demonstration purposes:

They are gender neutral, too.

A quick look at the Project view reveals the prefabs thus far, and a bit of the AudioVisual folder structure that I extend as needed:

Somehow this reminds me of Day of the Tentacle

Each player has a custom skin color material. Peeking inside our visually challenged player you see it’s just a bunch of primitives:

Capsules. Capsules everywhere.

I do take care of removing all default colliders since our visual representation should be separate from collisions.

Given this setup, it should be easy to imagine this being an animated character with an Animator Controller on it.

Planned Script Interaction

Looking ahead, the visualized Avatar may need to react to player events or the networked state of the player.

For such scripting needs, an instantiated Avatar object can either call transform.parent.GetComponent<Player>() or simply GetComponentInParent<Player>() to get the Player script.

Vice versa the Player script can get the current Avatar object via the PlayerAvatar component. More on that later.

Having obtained the reference you need on either side, it’s just a matter of either polling the player’s state and/or registering for player events to drive the Avatar animations – including walk, run, idle but also fire, reload and switch weapon, and everything else really (VFX, Audio).

Fire and switch weapon? Wait, shouldn’t this be networked?

Sure, this gets routed through the Player script. Then it’s the same thing as for the Avatar itself: destroy weapon object, instantiate new weapon prefab, instantiate projectile(s) prefabs.

Generally, it’s the Player script that pushes data and tasks down to the non-networked child objects, which will do their thing completely oblivious to any networking concerns.

PlayerAvatars ScriptableObject

On the ModularPlayer prefab is a PlayerAvatar script that references the Player Avatars ScriptableObject:

I store this ScriptableObject, and most others, under the Settings folder. In this case: Settings/Netcode.

The Inspector reveals that it contains just a simple list of objects:

You can almost imagine the code, right? Well, you don’t have to:

I clamp the index to always return a valid Avatar object.

This provides PlayerAvatar with a list of Avatar prefabs. Notice how an Avatar is now uniquely identifiable by index. We’ll only ever need to send that Avatar index across the network.

PlayerAvatar Script

The PlayerAvatar script has the following tasks:

  • Destroy the previous Avatar child object
  • Instantiate the new Avatar child object
  • In the future: customizable Avatar

As of now, it will spawn the desired Avatar by index and if necessary, destroy the previously active Avatar object.

Pooling is of no concern since Avatar selection is a rare/menu operation

Do note that PlayerAvatar is a MonoBehaviour. It has no networked functionality.

A Byte Is Three Bytes Less

You may also wonder about the Byte avatarIndex. I assume that having more than 256 Avatars is not likely to ever occur.

For this level of configurability you’d create a customizable Avatar with separately selectable head, torso, legs, arms, shoulders, belly button jewelry and what not.

In which case you’d typically have two Avatars: male and female. And maybe races, so you’re still down to 8 races times two or more genders – all well below 256, including the body parts themselves.

Why is that important? Traffic! A byte is a byte, an int is four bytes.

Anywhere I can cut down on how much data I’m sending, I’ll do it. It’s just best practice because every extraneous byte can cost you real money for Relay, Server Hosting, Cloud Save and other services.

Spawn And Change Avatar

If you recall, spawning a player sent an avatarIndex along which gets applied in the SpawnPlayerServerRpc:

It’s just assigning the index to the Player‘s AvatarIndex property. Note how the RPC parameters are bytes, not int, to reduce the network traffic. Couch players we also only have four at most.

The same assignment is done when the test code every so often changes a player’s Avatar:

So how does this magically synchronize the AvatarIndex with all other clients?

The AvatarIndex Property

In the Player script the AvatarIndex property merely forwards to the same property in the PlayerVars component:

PlayerVars contains all NetworkVariable instances and handles a couple related tasks. Namely OnValueChanged events and sending RPCs to write a NetworkVariable.

Here I use a NetworkVariable<Byte> and expose it as the AvatarIndex so that for outside code it’s merely a property:

Since this NetworkVariable has the default permissions it can only be written to by the server. Hence the setter calls SetAvatarIndexServerRpc:

This RPC then performs the assignment. Netcode takes care of synchronizing this value change to all clients.

This way our code only ever deals with a property and needn’t concern itself whether to send an RPC or change the value directly.

Oh and if we are the Server, the SetAvatarIndexServerRpc method is just a normal method call – it’s not sent over the network!

Owner NetworkVariables

The same pattern also works if the NetworkVariable had Owner write permissions:

Only writable by Owner, not writable by Server

Then just change to SendTo.Owner in the Rpc attribute:

And rename the method to SetAvatarIndexOwnerRpc for consistency’s sake. Now the RPC is a direct method call for the owner object and all other participants will send a net RPC.

This makes changing permissions of a NetworkVariable really, really simple. It’s completely transparent to outside code.

Disallow Writes

Lastly, and in fact specifically in our case we don’t want to allow remote clients to change our Avatar. In that case we only need to make a check in the property setter:

For any non-Owner who wants to set this property this just logs a warning.

Strictly speaking, we could also get rid of the Rpc in this case. And we’d have to update the server-side spawn RPC to not change the variable, instead relaying this to the client’s DidSpawn Rpc.

Applying AvatarIndex Changes

The PlayerVars script also handles the OnValueChanged event propagation.

In fact it only handles the event assignment. The actual implementation is found in the Player script. This is done because the PlayerVars script may not be aware of all the references that require updating – the Player script is our go-to controller class that orchestrates such changes.

Although currently it doesn’t and needn’t do anything other than calling SetAvatar (see above) on the PlayerAvatar script:

But it’s easy enough to imagine that an Avatar change may also need to invoke an outside event, perhaps for the GUI. Or it may record and defer the change if the player is currently dead.

Summary

And that concludes the Avatar selection. It’s pretty neat and simple, and the code now only requires get/set the AvatarIndex property.

The property will magically send an RPC if we aren’t authorized to make the change, or handle permissions any other way as we see fit.

Next …

Read on with 10. Assign Input Devices

Return to the Write Better Netcode Overview

Source Code on GitHub (GPL3 License)

Join my Patreon – it’s free! Get the latest updates by email.

Leave a comment below if you have any questions or feedback!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

WordPress Cookie Notice by Real Cookie Banner