In this guide I’m going to show you how to make a simple multiplayer drawing app with Normcore.
To start, download the project template here. This project includes the implementation for a basic VR brush in Unity.
First, we’ll walk through how the singleplayer brush example works, and then we’ll walk through how to make it multiplayer using Normcore.
Once you have the project open, open up the Brush scene. You should see a Brush object in the scene. First, let’s try it out. Make a note of which hand the Brush script is set to, hit Play, and use the trigger to draw:
Make sure the Brush component on the Brush object is set to the hand you want to draw with.
The brush is made up of three scripts, so let’s walk through what those are and how they work.
First up is the Brush component. The Brush component tracks the position of your hand. When the trigger is pressed, it creates a BrushStroke object to represent the brush stroke in the scene.
Each frame that you continue to hold the trigger, the Brush tells the BrushStroke component where the tip of the brush is as you move your hand around.
Next up is the BrushStroke component. If you drag the BrushStroke prefab from the Project into the scene, you’ll see it on the root of the prefab
Take a look at the BrushStroke prefab. You'll notice it has a BrushStroke component on it.
The BrushStroke component takes the current position of the brush tip each frame, and adds a new segment to the ribbon as it moves. It waits for the brush tip to move a specific distance before adding a new segment. This threshold is designed to keep the ribbon smooth while also keeping the mesh simple.
Every time it adds a new ribbon point, it updates the BrushStrokeMesh which creates the geometry needed to render the ribbon.
Last up is the BrushStrokeMesh component. This component is located on the Mesh game object of the BrushStroke prefab.
As the BrushStrokeMesh component is given new ribbon points by the BrushStroke component, it will create new vertices that correspond to each ribbon point’s position and rotation.
BrushStrokeMesh also includes one bonus feature: It allows you to update the position of the last brush stroke in the ribbon. The BrushStroke component uses this to connect the end of the ribbon to the brush tip position to make the brush stroke look like it’s drawing continuously
Here you can see the last point on the brush stroke being updated to match the latest hand position, with new points being created when you pass a certain distance from the previous one.
Making it multiplayer
Now that we have a singleplayer brush prototype working nicely. Let’s walk through how to make it multiplayer.
Before we begin, import the latest version of Normcore from either the assets store, or our website, and make sure you’ve got an App Key.
Whenever I add multiplayer support to a prototype, I like to think about the state that needs to be shared between clients. In our case we have the following:
How many BrushStroke objects exist in the scene
The ribbon points in each brush stroke
For any brush strokes that are actively being drawn, the position of the brush tip so we can connect the ribbon to their brush tip in real time
If you’ve used Normcore before, you know that Normcore can keep track of which prefabs you’ve instantiated in the scene. Luckily, that takes care of #1. That means that for each brush stroke, we’ll need a model that contains the array of ribbon points (#2), the current brush tip position, as well as a boolean to signal whether the brush stroke is being actively drawn or not (#3)
That means our BrushStrokeModel will look something like this:
Let’s go ahead and make our models. To start, make two new scripts: BrushStrokeModel, and RibbonPointModel.
We’ll start with RibbonPointModel. First off, remove the methods that Unity adds, and remove the MonoBehaviour super class. Next we’ll add a variable for position, and one for rotation:
Now, let’s add a [RealtimeModel] attribute, and [RealtimeProperty] attributes for both properties. In this case we’ll want both properties to be reliable, and we don’t need any change events.
Now let’s compile this model. Head back to Unity, wait for it to recompile, and then click “Compile Model” in the inspector for the RibbonPointModel.cs script.
Click the RibbonPointModel script in your project hierarchy, and then click 'Compile Model' in the script inspector.
The compiled model should look something like this:
For the BrushStrokeModel we’re going to create a RealtimeArray property to hold our ribbon points. This will also ensure that only the changes we make to the array are sent to the server each time. We’ll also add a property for the brush tip position and rotation, as well as a bool to signal when we’ve finished drawing this brush stroke so remote clients can ignore the brush tip position.
Once you’ve got this in, go back to Unity and hit “Compile Model”. You should end up with something that looks like this:
Wonderful! Now we have two models that we can use to synchronize our brush strokes.
We need to modify the Brush component to use Realtime to instantiate all instances of the BrushStroke prefab so that way they’re instantiated on all clients.
Let’s start by making a Realtime instance in the scene. I generally recommend using the “Realtime + Avatars” prefab because it will give us avatars + voice chat out of the box. Drag that prefab into the scene like so:
Adding a 'Realtime + Avatars' prefab to the scene.
Now, let’s add a property on Brush so we can get a reference to this. Make sure to add using Normal.Realtime; to the top of your file:
Before we forget, let’s wire up our Realtime instance in the Unity editor:
Drag the instance of Realtime into the Realtime field on the Brush inspector.
Swap the Instantiate() call for Realtime.Instantiate(). This will ensure that the prefab is instantiated on everyone’s client.
We’ll add a quick check at the beginning of Update() to bail early if we’re not connected to the room yet:
That’s everything we need to do to the Brush.
Let’s turn our focus to the BrushStroke prefab. First, move it to a folder named Resources. This will allow Unity to instantiate it by name which is required by Normcore.
Adding the BrushStroke prefab to a 'Resources' folder.
Open the BrushStroke.cs file, the first thing we’re going to do is make it a RealtimeComponent subclass instead of MonoBehaviour. This will give us some extra properties we can use, it will also allow RealtimeView to auto-detect this component. Make sure to add using Normal.Realtime; at the top of the file too.
Drag the BrushStroke prefab into the scene so we can modify it. All that we need to do is add a RealtimeView component. Now that BrushStroke inherits from RealtimeComponent, it will be auto-detected and added to RealtimeView’s component list:
Adding a Realtime View to the BrushStroke prefab.
Wonderful. Apply the prefab, and then remove it from the scene.
At this point, we can test it out. Hit Play and draw a brush stroke. Our prefab will be instantiated, and if you had another client connected, they would see the BrushStroke prefab show up in the scene. However, we haven’t wired up the model to BrushStroke, so the actual ribbons won’t be synchronized just yet. Realtime will also complain that BrushStroke doesn’t have a model property. Let’s fix that.
If you look at the top of BrushStroke.cs, there’s a section called RibbonState. Let’s replace that with our BrushStrokeModel. We’ll also add a private property that Realtime will use to give us a copy of the model:
Now, let’s go through the brush stroke class and replace each piece of state with a reference to the model.
We’ll start with the public interface. Overall this one is pretty easy, we can replace each reference to _brushTipPosition, _brushTipRotation, and _brushStrokeFinalized with the variables we have on BrushStrokeModel. In many cases, you can treat your model the same way you could local variables that store state for your application.
Now things get a little more interesting. AddRibbonPointsIfNeeded is used to measure the distance between the previous ribbon point, and the tip of the brush. If the distance is large enough, it adds a ribbon point. This block of code is currently going to run on everyone’s client that joins the room, and we don’t want everyone adding ribbon points to every brush stroke, we only want the person who is drawing this brush stroke to add ribbon points.
A quick fix for this is to check if realtimeView.isOwnedLocally is true. We’re subclassing RealtimeComponent, which means we inherit a reference to the RealtimeView on our prefab. In addition to this, earlier in Brush.cs, when we called Realtime.Instantiate() we passed ownedByClient: true which tells Realtime to set the RealtimeView at the root to be owned by the client that called Instantiate(). Therefore, we can use realtimeView.isOwnedLocally to determine if this brush stroke is owned by this client or not. We’ll add a quick check at the beginning of AddRibbonPointsIfNeeded:
Ok, next up, let’s look at AddRibbonPoint:
This does two things right now, it adds a ribbon point to our array of ribbon points, and it tells the mesh that we’ve added a ribbon point so it can render it. Updating the first part to use our models is straightforward:
This will compile and it will even work for the local client, however, on a remote client, their _model.ribbonPoints array will update, but their BrushStrokeMesh won’t be notified of the new ribbon points.
RealtimeArray has an event called modelAdded that fires whenever a model is added to the array. That event will fire if another client adds a ribbon point, it’s also going to fire when we add one locally. We’re going to use that event to know when to update our mesh.
First, let’s make an event handler and move the _mesh.InsertRibbonPoint() line over to it. Make sure to add using Normal.Realtime.Serialization; to the top of the file:
Now, inside of the setter for our model, we’re going to register for the modelAdded event:
Once both pieces are in, your class should look something like this:
We’re getting close, let’s update AnimateLastRibbonPointTowardsBrushTipPosition, to reference the model too:
Ok, now there’s one last thing. If a player joins the room late, their BrushStroke is going to be given a model that already includes ribbon points. In order to render those ribbon points, we’ll add a little bit of code to update the BrushStrokeMesh when the model is set:
And we’re done! Let’s test it out!
Works like a charm! Export a build and send it to a friend. You’ll both be able to join the same space and draw with each other :)
Make a copy of the Brush game object and set it to the other hand so you can draw with two hands at once
If you add “destroyWhenOwnerOrLastClientLeaves: false” to the Realtime.Instantiate call, the brush strokes will stick around after you leave and come back to the room! Try making a tool that can erase brush strokes by calling Realtime.Destroy() on them