I had trouble finding information or examples showing how to use RealtimeDictionary, so I put together this doc that details the things I learned as I figured it out.
RealtimeDictionary isn't a generic C# dictionary that syncs across the network. It specifically maps from uint
s (unsigned ints, or integers from 0 to ~4 billion) to RealtimeModels. There are other guides which cover RealtimeModels, but they're essentially network-synced structs that can hold whatever data you like.
It's a little complicated if you were just looking to map between two primitive data types, but creating a small RealtimeModel that wraps a single attribute is pretty quick and easy.
Also note that your keys must be unsigned ints. RealtimeDictionary does not support keys of other types.
I'm going to show how to build a scoreboard that tracks the score for each user in a game. UserScoreModel
will represent a single users score (and could later be expanded to store more than just a score). ScoreboardModel
is the parent model that has a RealtimeDictionary
property mapping between user IDs and UserScoreModel
models.
We can't compile the ScoreboardModel
until we build the UserScoreModel
, so we'll start there. Create UserScoreModel.cs
with the following code:
using Normal.Realtime;
using Normal.Realtime.Serialization;
[RealtimeModel]
public partial class UserScoreModel
{
[RealtimeProperty(1, true, false)]
private int _score;
}
If anything here is super confusing, I'd suggest checking out the guide on RealtimeModels and RealtimeComponents. We're essentially creating a new custom RealtimeModel that will track a single property, the score as an integer. (1, true, false)
means this is property ID 1, we want reliable transportation, and we don't need a C# event for when _score
updates. If you do need an event, set that last one to true.
Now navigate to the script in the Unity project tab, then in the inspector tab click "Compile Model". Make sure your project is compiling or you won't be able to click this button.
After clicking "Compile Model", your script should fill with a bunch of new code. You can ignore it all, it's not important to understand. You're now ready to make the second and last RealtimeModel.
Now we'll do the same thing, but this time for the ScoreboardModel
that tracks the players. We'll create ScoreboardModel.cs
with the following code:
using Normal.Realtime;
using Normal.Realtime.Serialization;
[RealtimeModel]
public partial class ScoreboardModel
{
[RealtimeProperty(1, true, false)]
private RealtimeDictionary<UserScoreModel> _players;
}
This looks identical to the last model, except we have a _players
property which is a RealtimeDictionary
mapping integers to UserScoreModel
s. Now hit "Compile model" on this script just like the last one, and then that's it for the Normcore stuff.
Finally, we'll need to make our RealtimeComponent
class that we can actually add to a GameObject. This class acts as the interface to your synced datastructure. Create ScoreboardSync.cs
with the following code:
using Normal.Realtime;
public class ScoreboardSync : RealtimeComponent<ScoreboardModel>
{
public int GetScore(uint playerId)
{
if(!PlayerExistsInDict(playerId)) return 0;
return model.players[playerId].score;
}
public void IncrementScore(uint playerId)
{
if(!PlayerExistsInDict(playerId))
AddPlayerToDict(playerId);
model.players[playerId].score += 1;
}
private void AddPlayerToDict(uint playerId)
{
UserScoreModel newUserScoreModel = new UserScoreModel();
newUserScoreModel.score = 0;
model.players.Add(playerId, newUserScoreModel);
}
private bool PlayerExistsInDict(uint playerId)
{
try
{
UserScoreModel _ = model.players[playerId];
return true;
}
catch
{
return false;
}
}
}
This class has a couple of public methods. GetScore
takes a user ID and returns the score for the user. This user ID can be whatever you like, the Normcore client ID will do (just watch out for ID re-use). IncrementScore
takes a user ID and increments their score, which will sync between all clients.
Finally, you can add this component and a RealtimeView
to a GameObject to your scene and then access it the same way you'd access any other GameObject component.
Updated
IncrementScore
to usemodel.players[playerId].score += 1;
which seems to fix that bug.