Client development

Project configuration

Some basic project configuration is required to avoid some common errors.

You should go to Player Settings and make sure you have 'Run in Background' checked to allow the networking system work even in background.

Also, allow the Dispatcher and NetManager run before the rest of the code. Under project settings, go to Script Execution Order and place them like in the image.

Basic example

The first step is to add a NetManager, which will manage all the basics of the server connection.

The steps to add a NetManager are:

  1. Add a new empty GameObject by right-clicking on hierarchy

  2. Name it as NetManager

  3. Click on 'Add Component' and look for NetManager

Now, lets fill the Host field with the address of our server. If you are running it on your local machine with the default configuration, just type "ws://localhost:7537". We will just leave the NetEntities empty for now.

It's time to check if everything is running fine, lets connect to the server and print a message.

  1. Create a new C# script by right-clicking on the file inspector

  2. Name it as you want (i.e. ConnectionTest)

  3. Remove the Update block as we won't use it this time

Use the Connect() method from the NetManager instance to connect:

    void Start()
    {
        // Lets try to connect to the server
        NetManager.Instance.Connect();
    }

If you attach the ConnectionTest script to a GameObject and run the project, you won't see anything. Lets add some juice to our script.

You can subscribe to different events over the networking system, so modify your code with the OnOpen event subscription:

    void Start()
    {
        // Lets try to connect to the server
        NetManager.Instance.Connect();

        NetManager.Instance.OnOpen += () => {
            Debug.Log("We are connected!");
        };
    }

If everything is correct, when you run the project, you should see this message on the debug console.

Sending data to the server and receiving a response

Now we are going to send a message to the server asking for the version, and then wait for an answer.

The communication in Socks works with JSON messages, so you need to construct an appropiate message that the server can recognise and process. Lucky you, Socks provides a NetMessage class for that!

Lets create a new object with NetMessage type, and use the correct Net Type (check Net Types section) to let the server know that you are asking for the version. You can extend the script used in the previous example.

    void Start()
    {
        // Lets try to connect to the server
        NetManager.Instance.Connect();

        NetManager.Instance.OnOpen += () => {
            Debug.Log("We are connected!");
            
            // Lets check the server version
            NetMessage message = new NetMessage(100);
        };
    }

The type 100 is the net type that says to the server 'hey, send me the version you are running". Now, just send the message.

    void Start()
    {
        // Lets try to connect to the server
        NetManager.Instance.Connect();

        NetManager.Instance.OnOpen += () => {
            Debug.Log("We are connected!");
            
            // Lets check the server version
            NetMessage message = new NetMessage(100);
            // Then send it to the server
            message.Send();
        };
    }

We are close to the goal. If you subscribe to the OnMessage event, you will be able to read the bunch of bytes sent by the server. So, parse them by creating a new NetMessage with those bytes, and do some basic stuff to read the version.

    void Start()
    {
        // Lets try to connect to the server
        NetManager.Instance.Connect();

        NetManager.Instance.OnOpen += () => {
            Debug.Log("We are connected!");

            // Lets check the server version
            NetMessage message = new NetMessage(100);
            // Then send it to the server
            message.Send();
        };

        NetManager.Instance.OnMessage += (byte[] msg) =>
        {
            NetMessage received = new NetMessage(msg);
            if (received.GetNetType() == 100) {
                Debug.Log(received.GetData()["version"].str);
            }
        };
    }

Once again, if everything is running fine, you should see the message on console.

Getting auth access

Some functions in Socks (those related with inventories, roles, etc) requires user authentication. To get the auth access, you just have to provide a correct user/password combination, then the server will keep in the session our authentication state.

The approach is simple, we just need to build a NetMessage with the appropiate type (300 in this case) and then assign the message data to push it to the server.

void Start()
{
    // Lets try to connect to the server
    NetManager.Instance.Connect();

    NetManager.Instance.OnOpen += () => {
        // Lets try to construct a login message
        NetMessage message = new NetMessage(300);
        message.SetData("user", "our-user");
        message.SetData("password", "our-password");
        // Then send it to the server
        message.Send();
    };
}

Then, just subscribe to the OnMessage event and check if the authentication process went fine.

void Start()
{
    // Lets try to connect to the server
    NetManager.Instance.Connect();

    NetManager.Instance.OnOpen += () => {
        // Lets try to construct a login message
        NetMessage message = new NetMessage(300);
        message.SetData("user", "our-user");
        message.SetData("password", "our-password");
        // Then send it to the server
        message.Send();
    };

    NetManager.Instance.OnMessage += (byte[] msg) =>
    {
        NetMessage received = new NetMessage(msg);
        if (received.GetNetType() == 300) {
            Debug.Log(received.Print());
            if(received.GetData()["status"].str == "OK") {
                Debug.Log("We are authenticated!");
            }
        }
    };
}

After that, you can just send message types that require authentication and will work on their own.

Introducing Net Entities

In the previous examples, you have learned to work with net messages. But, what about syncing GameObjects?

NetEntity is a base class that provides the basic networking functionality for GameObject syncing, and can be easily extended by overriding its methods.

Lets add some objects to the scene:

  1. Add a plane as a floor

  2. Add a Capsule that will act as our player

  3. Remove the Capsule collider

  4. Add a Character Controller to it

  5. To control it, you can just add the provided Basic Movement script

It's time to learn how to synchronize the position of the players.

Create a new script and call it 'CapsulePlayer'. It should inherit from NetEntityBase, not from MonoBehaviour, so change it.

Now, just override two methods:

  • NetOutput: add the data you want to send over the net for the GameObject its attached to

  • NetUpdate: will be called with the received data from other players GameObjects, so use it to update the position with that data

public class CapsulePlayer : NetEntityBase
{

    public override NetMessage NetOutput()
    {
        NetMessage message = new NetMessage(0);
        message.SetData("x", transform.position.x.Truncate(2));
        message.SetData("z", transform.position.z.Truncate(2));
        return message;
    }

    public override void NetUpdate(NetMessage message)
    {
        transform.position = new Vector3( message.GetData()["x"].f, 0, message.GetData()["z"].f);
    }

}

Attach the created NetEntity script to your capsule player.

Since it inherits from NetEntityBase, it will have a NetRate property. It specifies how many messages will be sent over the net per second. A value between 5 to 10 has a good balance between smoothness and network saturation.

Make a prefab by dragging the GameObject to the file explorer, then remove it from the scene. Add it to the Net Entities list of the Net Manager to allow it to be instantiated over the network.

Now, create a new script, call it Spawner, and attach it to an empty GameObject on the scene.

void Start()
{
    // Lets try to connect to the server
    NetManager.Instance.Connect();

    NetManager.Instance.OnConnect += () =>
    {
        // After connecting, lets instantiate our player
        NetManager.Instance.NetInstantiate(0);
    };
}

Maybe you have noticed that we are using OnConnect now instead of OnOpen. The difference between these two events is that in the case of OnConnect, we have received our client id, so we are ready to instantiate networked GameObjects.

Build the project and run two instances, if you didn't forgot anything, it will look similar to the video.

Voilà! You have realtime network communication. Well, maybe the players smoothness could be improved, but we will talk about it later...

Smooth Networked Entities

Online games are as good as their netcode is. While usually a game in your machine can generate 60 images (frames) per second, sending a packet to a server, broadcasting to all other clients and processing them 60 times per second is inviable. So, what can we do?

Go to your previously created player prefab, remove the NetEntity script (CapsulePlayer) and add a NetTransform.

NetTransform is a script that extends the NetEntityBase and takes care of synchronizing x, y and z axis of GameObject's position and the y axis of rotation, which usually will be enough for most use cases. Also let us decide how many decimals use to sync and if it should apply interpolation functions to make them smooth.

Set the net rate to 10, make sure smooth function is enabled and try to re-run the project by launching 2 instances. A value of 10 should be enough for most fast-paced games.

As you can see, it is now way more smooth than before. The provided smooth function is simple and works well for a general use, but you can write your own smooth, interpolation or prediction algorithm that fits better in your project.

Remote Procedure Calls

Remote Procedure Calls or RPCs are the way to call functions in GameObjects over the network. Think in a player with a gun, if that player wants to shot a bullet, then he needs to tell all other players that is about to fire its gun. He will send a RPC with the required data and then, on all other players machines, the GameObject that represent that player will be calling the gun fire function.

RPCs on Socks are also easy to manage.

Start by adding the provided Bullet prefab (which basically includes a script that moves it forward and destroy it after few seconds) to the list of Local Entities on the NetManager. You can instantiate local entities from anywhere, NetManager includes a local entity list just for avoid you creating one.

Now, create a new script, call it CapsulePlayerWithGun, make it inherit from NetEntityBase and copy all the content from the NetTransform script to get a good starting point.

RPCs should be defined as a void with a name that starts with "Rpc". Define a method called RpcFire and fill it with code.

The resulting script should look like this:

public class CapsulePlayerWithGun : NetEntityBase
{
    Vector3 lastPos = Vector3.zero;
    Vector3 newPos = Vector3.zero;
    Quaternion lastRot;
    Quaternion newRot;
    private float tickTime = 0;
    private float tickMaxTime;
    public int decimalPrecission = 2;
    public bool smoothNetEntity = true;
    public float maxSmoothDistance = 5f;

    void Start()
    {
       tickMaxTime = 1/(float)netRate;
    }
    
    void Update()
    {
        if (!IsOwner() && newPos != Vector3.zero) {
            if (smoothNetEntity) {
                tickTime += Time.deltaTime;
                if (Vector3.Distance(lastPos, newPos) < maxSmoothDistance) {
                    transform.position = Vector3.Lerp(lastPos, newPos, tickTime/tickMaxTime);
                    transform.rotation = Quaternion.Lerp(lastRot, newRot, tickTime/tickMaxTime * 2f);
                } else {
                    transform.position = newPos;
                    transform.rotation = newRot;
                }
            }
        }
    }

    public override NetMessage NetOutput()
    {
        NetMessage message = new NetMessage(0);
        message.SetData("x", transform.position.x.Truncate(decimalPrecission));
        message.SetData("y", transform.position.y.Truncate(decimalPrecission));
        message.SetData("z", transform.position.z.Truncate(decimalPrecission));
        message.SetData("r", transform.rotation.eulerAngles.y.Truncate(decimalPrecission));
        return message;
    }

    public override void NetUpdate(NetMessage message)
    {
        tickTime = 0;
        lastPos = transform.position;
        lastRot = transform.rotation;
        if (newPos == Vector3.zero) {
            transform.position = new Vector3( message.GetData()["x"].f, message.GetData()["y"].f, message.GetData()["z"].f);
        }
        newPos = new Vector3( message.GetData()["x"].f, message.GetData()["y"].f, message.GetData()["z"].f);
        newRot = Quaternion.Euler(0, message.GetData()["r"].f, 0);
        if (!smoothNetEntity) {
                transform.position = newPos;
                transform.rotation = newRot;
        }
    }

    void RpcFire(NetMessage message)
    {
        // When a user shoots, create a bullet on its position
        GameObject bullet = Instantiate(NetManager.Instance.localEntities[0]);
        bullet.transform.position = gameObject.transform.position;
    }
}

Now, lets make a way to shoot. Get the BasicMovement script and edit it to send the fire RPC when pressing the space bar.

void RpcFire(NetMessage message)
{
    // When a user shoots, create a bullet on its position
    GameObject bullet = Instantiate(NetManager.Instance.localEntities[0]);
    bullet.transform.position = gameObject.transform.position;
}

And we are ready to go.

If you followed correctly all the steps, you should have learnt the basics of networking and will be ready to develop your own networked game!

Last updated