Skip to content

Instantly share code, notes, and snippets.

@d12
Last active December 7, 2022 21:49
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save d12/f577bb50b0fdeeb17084819c78fe5c43 to your computer and use it in GitHub Desktop.
Save d12/f577bb50b0fdeeb17084819c78fe5c43 to your computer and use it in GitHub Desktop.
How to use RealtimeDictionary

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.

What is a RealtimeDictionary

RealtimeDictionary isn't a generic C# dictionary that syncs across the network. It specifically maps from uints (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.

Building a player scoreboard

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.

UserScoreModel

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.

ScoreboardModel

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 UserScoreModels. Now hit "Compile model" on this script just like the last one, and then that's it for the Normcore stuff.

ScoreboardSync

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.

Using ScoreboardSync

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.

@JackaPacka
Copy link

Thanks for the help :)

@nuehado
Copy link

nuehado commented Jul 4, 2021

This is helpful, thanks!

@MHillier98
Copy link

MHillier98 commented Sep 5, 2021

Doesn't seem to work anymore? It gives me this error when calling IncrementScore:

SerializationException: WriteStream attempted to write model (ScoreboardModel) but actual length (19) didn't match the precalculated length (23).
Normal.Realtime.Serialization.WriteStream.WriteRawModel (Normal.Realtime.Serialization.IStreamWriter model, System.Int32 modelWriteLength, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Serialization.WriteStream.WriteModel (System.UInt32 propertyID, Normal.Realtime.Serialization.IStreamWriter value, Normal.Realtime.Serialization.StreamContext context, System.Boolean forceWriteFullModel) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.ImmutableModelCollection.Write (Normal.Realtime.Serialization.WriteStream stream, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.RealtimeModel.Normal.Realtime.Serialization.IStreamWriter.Write (Normal.Realtime.Serialization.WriteStream stream, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Serialization.WriteStream.WriteRawModel (Normal.Realtime.Serialization.IStreamWriter model, System.Int32 modelWriteLength, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Serialization.WriteStream.WriteModel (System.UInt32 propertyID, Normal.Realtime.Serialization.IStreamWriter value, Normal.Realtime.Serialization.StreamContext context, System.Boolean forceWriteFullModel) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.RealtimeViewModel.Write (Normal.Realtime.Serialization.WriteStream stream, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.RealtimeModel.Normal.Realtime.Serialization.IStreamWriter.Write (Normal.Realtime.Serialization.WriteStream stream, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Serialization.WriteStream.WriteRawModel (Normal.Realtime.Serialization.IStreamWriter model, System.Int32 modelWriteLength, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Serialization.WriteStream.WriteModel (System.UInt32 propertyID, Normal.Realtime.Serialization.IStreamWriter value, Normal.Realtime.Serialization.StreamContext context, System.Boolean forceWriteFullModel) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Collections.StringKeyDictionary`1[TValue].Write (Normal.Realtime.Serialization.WriteStream stream, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.RealtimeModel.Normal.Realtime.Serialization.IStreamWriter.Write (Normal.Realtime.Serialization.WriteStream stream, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Serialization.WriteStream.WriteRawModel (Normal.Realtime.Serialization.IStreamWriter model, System.Int32 modelWriteLength, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Serialization.WriteStream.WriteRawCollection (Normal.Realtime.Serialization.ICollection collection, System.Int32 collectionWriteLength, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Serialization.WriteStream.WriteCollection (System.UInt32 propertyID, Normal.Realtime.Serialization.ICollection value, Normal.Realtime.Serialization.StreamContext context, System.Boolean forceWriteFullModel) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Datastore.Write (Normal.Realtime.Serialization.WriteStream stream, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Serialization.WriteStream.WriteRawModel (Normal.Realtime.Serialization.IStreamWriter model, System.Int32 modelWriteLength, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Serialization.WriteStream.WriteRawModel (Normal.Realtime.Serialization.IStreamWriter model, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Serialization.WriteStream.SerializeModel (Normal.Realtime.Serialization.IStreamWriter model, Normal.Realtime.Serialization.StreamContext context) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Datastore.SerializeDeltaUpdates (System.Boolean reliable, System.UInt32 updateID, System.Double roomTime) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
Normal.Realtime.Room.PersistenceTick (System.Double deltaTime) (at <d17e381c8fa446b8a7c6cfeeea910134>:0)
UnityEngine.Debug:LogException(Exception)
Normal.Realtime.Room:PersistenceTick(Double)
Normal.Realtime.Room:RoomTick(Double)
Normal.Realtime.Room:Tick(Double)
Normal.Realtime.Realtime:Update()

For some reason, it seems to work when you make IncrementScore look like this:

    public void IncrementScore(uint playerId)
    {
        if (!PlayerExistsInDict(playerId))
            AddPlayerToDict(playerId);

        UserScoreModel scoreModel = model.players[playerId];
        UserScoreModel newUserScoreModel = new UserScoreModel();
        newUserScoreModel.score = scoreModel.score + 1;
        model.players[playerId] = newUserScoreModel;
    }

@d12
Copy link
Author

d12 commented Sep 6, 2021

Updated IncrementScore to use model.players[playerId].score += 1; which seems to fix that bug.

@cowolff
Copy link

cowolff commented May 25, 2022

I constently get this error while using your approach:

NullReferenceException: Object reference not set to an instance of an object
ScoreboardSync.AddPlayerToDict (System.String name) (at Assets/Scripts/Scoring/ScoreboardSync.cs:58)
ScoreboardSync.IncrementScore (System.UInt32 playerId, System.String name) (at Assets/Scripts/Scoring/ScoreboardSync.cs:25)
PlayerController.Update () (at Assets/Scripts/PlayerController.cs:72)

And some playing around with Debug statements show that somehow the model variable in ScoreboardSync remains null but I can't really figure out why. So it throws the NullReferenceException exception when

model.players.Add(playerId, newUserScoreModel);

is used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment