In this video we're going to complete the work we started
in part three of this series by synchronizing the player's
movement across multiple clients.
And, as a result, you'll get a glimpse at a workflow that
you can use to add features to your own SpatialOS
multiplayer based game.
My name is Charles and this is Infallible Code a channel
designed to help you become a better game developer.
If you'd like to learn more about Unity, programming, and
game development, then be sure to subscribe and hit
that Bell icon so you'll be notified whenever a new video
is made available.
Let's begin by taking a quick look at the example project.
It was created using the blank starter project and uses the
GameObject Creation feature module in order to spawn a game
object for the player when they connect..
We've also written some custom logic that spawns a
different prefab based on whether or not the worker is
authoritative. Authoritative workers will be given a player
prefab that has a first person player controller attached
to it. And non-authoritative workers will be given a prefab
that consists of nothing more than a model.
The problem with this project, the one that we're going to
solve in this video, is that the player's position and
rotation is not synchronized across clients.
We're gonna have to add some logic so the authoritative
worker sends the position and rotation to all other
workers that are connected to the SpatialOS server.
Why don't we that now? When it comes down to it, we're
going to need to be able to read and write the transforme
data directly on the Unity game object.
So we're gonna want to write a couple of MonoBehaviours to
do this: one for the authoritative client that has the
ability to write the position and rotational data to a
SpatialOS component and the other for the non-authoritative
clients that can read that data and apply it to their
version of the player GameObjects in the scene.
Let's go ahead and add those the scripts folder.
I'm going to call the authoritative one
WritePlayerTransform.
And the non-authoritative one ReadPlayerTransform.
Now the idea here is that the authoritative worker is going
to write the Unity transform information
and then all of the non-authoritative workers are going to
read the data off of that component and apply it to their
version of that player's GameObject.
But first we'll need to define a custom SpatialOS component
that we can use to hold the positional and rotational data.
Let's navigate to the root folder of our project, the one
that contains the GDK for Unity, and then open
up the project folder.
From here we'll go to schema and then create a folder
that'll hold all of our future schema files.
I'll call mine "spatial_craft".
In here we'll create a schema file that will hold the
definition for our custom component.
I'm going to call it
"player_transform.schema". Schema files are files written
in SpatialOS schema language, that SpatialOS can compile
into code we'll use in our project.
SpatialOS uses the schema files because it's language
agnostic which means these schema files can be compiled
into any language that wants to leverage SpatialOS.
Let's open this up in our code editor.
Now if you're not familiar with schema lang then I
definitely recommend either reading the documentation or
checking out my video, but I'll do my best to explain each
step as we go along.
First each schema file requires a package so I'll create
one and call it "com.infalliblecode".
The package is used if we need to import this type into
another file later on, which we'll see in a second.
To define our component we need to use the component
keyword.
And then Pascal case to name it again.
I'm going to call it "PlayerTransform".
Every component requires a unique ID.
I'm gonna use the ID 4000 and then increment from there
for every component that I define later on.
So the next one would be 4001.
Next we want to add two properties: one for position and
one for rotation.
And I'd like them to be represented by vectors just like
they are in Unity, so we can go ahead and import a vector
type that's available in the GDK for Unity.
Perfect. Now we can use the Vector3 type for our
properties. I'll call them:
position
and rotation.
You'll notice that I gave each of these properties IDs and
that's just a requirement of the schema language.
And that's it. We've defined our custom component and now
we can switch over to Unity and compile it.
now because this project is based off of the blank starter
project, I have a SpatialOS menu here that provides a
couple of helpful tools.
The one we're interested in right now is the generate code
option. And when we click this it's going to go through all
of the scheme of files and compile them into C sharp code
when it's done it'll place them in this folder called
"Generated", into the source folder.
And we can see that we have a "com" and then
"infalliblecode" folder that now contains all of the files
generated for the PlayerTransform component, which includes
the component itself, a snapshot a class, reader and writer
classes which we'll use in the model behavior, and a couple
of other things that are used behind the scenes.
So now that this has been generated we can go ahead and add
this component to the player entity template.
That way whenever an entity is generated it'll have this
component as well and we can write and read to it in our
MonoBehaviours. So let's switch to the code editor.
If you're following along with a series or using the blank
starter project, then you'll find the code that's
responsible for creating the player entity template inside
of the UnityGameLogicConnector class.
And adding our component is as easily as calling
template.AddComponent and then
passing in a new
PlayerTransform Snapshot.
And we're going to give write access to the client via
this "clientattribute" variable.
And the reason for that is because we want the client to
determine the position and rotation of its own player
GameObject.
Now this implementation works out for this tutorial but you
may want a different implementation based on the needs of
your game. For now we're good to go and we can switch over
to the classes that will read and write to this component.
So let's go ahead and open up our WritePlayerTransform
MonoBehaviour and add our logic.
Now we'll need access to the writer that was generated from
our schema file, and we can do that by adding a private
member variable called PlayerTransformWriter.
And I'll just call it "writer".
Now it's not enough to just add the member variable
we need to add a special attribute called
"Require". And this will tell SpatialOS to populate this
field when the model behavior runs.
So we don't have to do anything else to gain access to this
writer class. So we can quickly implement the
Update method and call the writer's SendUpdate
method, which will allow us to pass in a new Update
object which should contain all the data we want to send.
And that data is the Transform's current position and
rotation. So I'm going to go ahead and move this into its
own variable.
And We'll initialize those two variables.
So the first one is position.
Now you may think you could just call transform.position
but the Vector3 type that we defined in our scheme of file
is not the same as the one that's used in Unity.
It's a special class that's part of the GDK for Unity and
we can access it by calling Vector3, and
make sure that we're referencing the one from Improbable,
and then call FromUnityVector and we can pass in
our Unity Vector3.
And that's going to convert it to Improbable's version of a
vector. We can do the same for rotation.
So I'll say Vector3F.FromUnityVector and
we can just call the transform, rotation.eulerAngles
to get that. And, believe it or not, that's all that's
required for writing the Transform's position and rotation
to our custom component so we can move on to reading it.
Let's open up the ReadPlayerTransform class.
We're gonna go ahead and use the Require attribute to
get a reference to the PlayerTransformReader.
And I'll just go ahead and call that "reader".
And just like before we will implement the Update method.
And this time we'll update the Transform's position using
the readers Data object.
And since now we're converting the Improbable version of
the vector to Unity's version of a vector, We'll be able to
call "ToUnityVector".
And then we'll do the same thing for rotation: I'll call
transform.rotation and then I'll use Quaternion's
Euler method and pass in
reader.Data.Rotation.ToUnityVector. And
that's it.
Now we've read the position of rotation values
from the SpatialOS entity into the GameObjet's transform.
So that's all well and good in code but now we need to
switch back to Unity and added to the player prefabs.
Back in Unity, we're going to locate the prefabs for the
player in the Resources folder, and then Prefabs, and
then UnityClient.
And as you'll recall, we have one player prefab to
represent the authoritative workers and another
to represent the non-authoritative workers.
For the authoritative workers, we're gonna open up the
prefab and we want to add the writer.
So we'll open up the scripts folder, locate the
WritePlayerTransform MonoBehaviour, and add it to the prefab.
And that's because the authoritative worker is going to be
in charge of keeping track where the players being moved.
Now for the non authoritative workers, we'll want to open
up their prefab, which is nothing more than a simple model,
and then we're gonna go ahead and grab the
ReadPlayerTransform MonoBehaviour and drag it onto that.
That way the non-authoritative representation
will be able to read the values that come from the
non-authoritative representation and apply it to its
version of the GameObject, thus keeping their positions and
rotations in sync.
So now we're ready to test this.
Why don't we switch back to the scene and then build a
client. To do that we can click on SpatialOS
and then build for local and then UnityClient.
This is just going to build a client for us so we can run a
standalone version.
When that's done and I'll click control-L or command-L on
Mac, to run a local instance of SpatialOS.
And when that's done we can go ahead and run the
development scene.
All right. So far so good.
Looks like I can move around freely.
Now let's launch a standalone client and see if the player
that connects moves around our development scene.
I'll click on SpatialOS and then launch
standalone client.
When that's done we can go ahead and click on play.
And then I'll just move out of the way real quick.
And then position my camera on the development scene.
And now we can move around and look good that
looks like the GameObject is synchronizing its expected.
Beautiful.
So now our player game objects are synchronized across all
clients and we got a sneak peek at the workflow
that we'll be using to implement our own game mechanics.
Which we'll be doing in the next video using everything
we've learned so far.
If you enjoyed this video then please leave a like and a
comment letting me know what you thought.
Thanks for watching. And as always I'll catch you
in the next video.
Special shout out to Trond, LootPigeon, Dark Rush
Photography, Justin Hurst, NZ, Sean Carey, Thomas, and WayneGlows.