Custom Avatars & Advanced CoPresence (Unreal Engine)
In Cavrnus, CoPresence operates through Property values, which are communicated Transiently and do not get logged in the Journal.
Local vs Remote User Avatars
In Cavrnus, the Local and Remote Avatars are separate. When a remote user joins, we will UWorld::SpawnActor
the Avatar specified in the Cavrnus Spatial Connector, and provide it with relevant property paths (this will be explained below).
Local Users, by contrast, are left up to the Application to provide/configure. If you wish to display a local Avatar, use a first person camera, or include an XR Rig, it will need to be done by your project. Then, there are a handful of simply steps to have that object send CoPresence data to Cavrnus.
Sending Transform Data from Local User
Each user is responsible for sending their own CoPresence Data. To do this, the actor class that your project has been set up to use (set in the project properties or in the game mode class) just needs to have the Sync Transform Character component and a Cavrnus Properties Container component added to it.
And that’s it, you’re done!
At runtime, the local user’s avatar will automatically have the Cavrnus Local User component added to it. When a space is joined, it will go through all of the Cavrnus Properties Container components on the avatar and update them to have the correct root for the user’s ID, which in turn will allow remote clients to receive those property updates and apply them to the correct remote avatar instance.
Using a Custom (Remote) Avatar
To create a Custom Avatar for remote users, we’ll assume you already have a model you wish to use. Here we will be using a Robot mesh.
The first step is to add a SyncTransformCharacter component under the primary component of the Actor, as well as the CavrnusPropertiesContainer component. This will move the Avatar wherever the Remote User says they currently are.
Then, assign this actor class to the Remote Avatar Class
field in the CavrnusSpatialConnector
instance in your level.
Now, all users should spawn with this Avatar!
There are a couple things to note about how this works at runtime under-the-hood:
Avatars are special objects where the Transform properties are only allowed to transmit from local user to remote user on other clients. Remote user representations cannot push transform updates back to the cloud.
The Cavrnus Properties Container component’s “Container Name” field on local and remote avatars can be left blank, because they are automatically updated to have a value that is linked to the user’s connection ID at run time. When running in PIE mode, you will see the value updated to something like “users/0wu9vekvi24”, and the Sync component(s) that are siblings with the container will use that value to build a unique string in the journal - e.g. the Sync Name Tag Image component has “Property Name” set to “profilePicture”, so the journal entry for that value will be “users/0wu9vekvi24/profilePicture“.
Adding UI/Name Tags to Remote Avatar
In addition to the CoPresence Transform, which is constantly being sent by the Remote User, there are additional user Properties which are exposed through properties, but are set elsewhere. These include things like the User’s Name, weather they are Muted, what Volume they are Speaking at, etc. This data is actually sent over-the-wire using different channels, but is copied into Properties for your convenience under-the-hood. Since the data is coming in from elsewhere, these properties are read-only, and any attempt to Post() to them will fail. While a simple BindXProperty call will retrieve them, for your convenience we have added calls to get them directly using the UCavrnusFunctionLibrary
class.
You can, of course, also use the Sync Components.
As a basic example, we can add a UI class with a Text Block component to the custom Avatar we have just made. We will then add a Sync Name Tag Text component to it, which inherits from the class CavrnusValueSyncString and is designed to hook up that Text component to the appropriate property from the user’s profile.
For the Property Name, we will use “name” which can be looked up in the UCavrnusFunctionLibrary
as the user’s name property. Also note that Send Changes is unchecked since, as stated, these are read-only.
When SyncNameTagText receives an update event for the text property that is being watched, that value will be assigned to the Text Component in the widget.
Finally, for the name of the properties container in the CavrnusPropertiesContainer component, no value is entered manually. As stated earlier, at runtime these fields will be updated with the unique value to this user, like “/users/0wu9vekvi24”. When combined with the field selected by SyncNameTagText, the widget’s text component will be updated with whatever value is in “/users/0wu9vekvi24/name”.
With this done, the avatar will now show the user’s name above it:
The name tag we provide in the existing built-in avatar is built entirely using the above tools & principals.
Adding Hands/Rigging to Remote Avatar
Syncronizing the Avatar position only requires a SyncTransform component on the player pawn blueprint. To track additional data, you only need add the corresponding Sync Property component to the relevant parts. In most cases, a CavrnusPropertyContainer component will be added automatically. If the container component doesn’t appear, simply add that component and provide a container path to store the property in the Journal.
Let’s say the Remote User had an XR Rig and we wanted to synchronize their hands. To do this, simply select the relevant part, and add another SyncTransform component to it. The UniqueContainerName will automatically fill in using the name of the path (e.g. “BP_PlayerPawn/LeftHand”). As long as data is being sent to that Property at runtime, the hand will synchronize in real-time.
This same concept applies to Rigged Avatars & Bone Transforms. For those it may be worth writing your own script since attaching SyncComponents may be difficult. But here is an example of how you could implement the receiving end for a Remote Avatar:
public void BindAvatarBones(CavrnusSpaceConnection spaceConn)
{
foreach(var bone in skinnedMeshRenderer.bones)
{
string ContainerName = GetComponent<CavrnusPropertiesContainer>().UniqueContainerName;
string PropertyName = bone.name; //We assume the bones all have unique names. If they don't, ensure this is unique some other way.
spaceConn.BindTransformPropertyValue(ContainerName, PropertyName, boneTrns =>
{
bone.localPosition = boneTrns.LocalPosition;
bone.localEulerAngles = boneTrns.LocalEulerAngles;
bone.localScale = boneTrns.LocalScale;
});
}
}
Sending Hands/Rigging from Local Player Character
… TODO …