Best Practices - Designing Your Data (Unreal Engine)
Cavrnus lets you synchronize whatever data you want via Properties, however you want to do it.
However, there are some important ways that a synchronized application differs from a local (single-user) one. As such, there are some best practices for planning how to synchronize your Application. You obviously don’t need to follow any of them, but they may be helpful.
“Explain” Your State With Properties
Ultimately, you can think of Properties as an “explanation” of the current state of your Application. This means they may not actually need to correlate 1:1 with data fields inside your Editor.
Let’s take the example of a training simulation where users need to put tires on a car. This simulation contains a checklist showing which tires are attached.
At its core, this can be done with just eight properties:
Tire 1 Position <Transform>
Tire 2 Position <Transform>
Tire 3 Position <Transform>
Tire 4 Position <Transform>
Left Front Wheel Attached <Bool>
Right Front Wheel Attached <Bool>
Left Back Wheel Attached <Bool>
Right Back Wheel Attached <Bool>
The four Tire transform properties exist to show the tires moving as users pick them up and move them. The four Attached bools exist to confirm that the tire has been properly attached and to update the checklist.
Since Properties are provided from the journal, any script or menu in the scene can access them. So, to monitor the Left From Wheel state the checklist menu need only call:
spaceConn.BindBoolPropertyValue("SimulationState", "LeftFrontWheelAttached", v => toggle.SetIsOnWithoutNotify(v));
Have the UI Represent the Properties, not Vice Versa
Let’s once again consider the example of a Color Picker:
There are a lot of different UI options there for setting the color. In the end though, they all represent the same core value.
Therefore, you should avoid having Properties to represent each UI option. Instead, you should have a single Color property representing your data, and have all of the different UI options Post to it.
Don’t Rely on “Events”
One important aspect of Cavrnus is that users can join a Space at any time. This could be at the “beginning” of a sequence of events, or it could be midway through it. If your state is entirely “described” by Properties then this isn’t a problem.
However, you should be wary about what goes in your Bind() calls, because you can only rely on users having hit the “Latest” one.
To give an example, let’s consider an application that cycles through multiple sets of Primitive Shapes (Enterprise Primitive Shapes). They have a single String Property called “Shapes”, which cycles through “Set 1”, “Set 2”, and “Set 3”.
Set 1 has the Cube and Sphere visible. Set 2 has just the Cube, Set 3 has nothing visible.
The Space is guaranteed to follow this order, so when the developer writes their Bind() function they do this:
void UObjectManager::ShowShapes(FString ShapeSet)
{
if(ShapeSet == "Set 1")
{
Cube->SetActorHiddenInGame(false);
Sphere->SetActorHiddenInGame(false);
}
else if(ShapeSet == "Set 2")
{
Sphere->SetActorHiddenInGame(true);
}
else if(ShapeSet == "Set 3")
{
Cube->SetActorHiddenInGame(true);
}
}
For a User who joins while the Property is set to “Set 1” this all works fine:
However, what happens if User 2 arrives late, and when they show up the Properties are already in “Set 3”? Since they never passed through “Set 2” the Sphere was never hidden and they would see this:
To fix this, you need to make sure that, every time you get a property update, the Application fully establishes it’s state. So in this case, the code will need to set both Visibilities in “Set 2” and “Set 3”, even if it is sometimes redundant.
Corrected Code:
void UObjectManager::ShowShapes(FString ShapeSet)
{
if(ShapeSet == "Set 1")
{
Cube->SetActorHiddenInGame(false);
Sphere->SetActorHiddenInGame(false);
}
else if(ShapeSet == "Set 2")
{
Cube->SetActorHiddenInGame(false);
Sphere->SetActorHiddenInGame(true);
}
else if(ShapeSet == "Set 3")
{
Cube->SetActorHiddenInGame(true);
Sphere->SetActorHiddenInGame(true);
}
}