Sponsored By

UNET Unity 5 Networking Tutorial Part 1 of 3 - Introducing the HLAPI

In this article series, we will be building a small networked multiplayer demo using Unity 5's Networking High-Level API, or HLAPI.

Christian Arellano, Blogger

September 22, 2015

13 Min Read
Game Developer logo in a gray background | Game Developer

Introduction

In this article series, we will be building a small networked multiplayer demo using Unity 5's Networking High-Level API, or HLAPI. Even if what we will build is simple, our example will try to cover the following key concepts which should help you build larger game projects using the HLAPI:

By keeping our project's scope as small as possible, hopefully it would be easier to extract and understand the networking principles demonstrated so that you can transfer and apply the concepts to your own games.

Prerequisites

Basic familiarity with Unity is assumed. In particular, you should know what MonoBehaviours are, what their relationship with GameObjects is, and how/when their event functions such as Awake, Start, Update, etc. are called. No prior experience with or knowledge about Unity Networking is needed.

Our implementation is based on the architecture described by Gabriel Gambetta in his article series Fast-Paced Multiplayer. In particular, refer to Part 2 of that series to understand how we will implement client-side prediction and server reconciliation in our project.

It should be stated that this article is basically an annotated excerpt of the document Converting a Single Player Game to Multiplayer from the Unity documentation. I would recommend this document to anyone looking to get up and running with the HLAPI as quickly as possible, and is a great resource if you're looking for a checklist-style guide to using the HLAPI.

Download The Project

The project folder is available for download. Most of the project is driven by only one custom MonoBehaviour, and it is only 115 lines long with whitespace. Downloading the sample project folder is recommended so that you can go over the code and actually run the project on your system which, together with this article, will hopefully help you better understand how it all works.

Gee, this article is too long, I do not feel like reading all of this right now...

I know that some developers out there learn better by reading code directly instead of starting with an article - the project folder download is a good jump-off point for you if you are one of them.

I also plan to write a few more articles about Unity Networking after this series and release some more sample multiplayer projects, so if you are interested in those, let us stay in touch and I will let you know when those become available.

Our Starting Point

We will start with a cube that can be moved from one grid position to another using the arrow keys.

Let us store the current grid position of the cube using a struct:


struct CubeState {
    public int x;
    public int y;
}

CubeState state;

We set the cube's initial position in the Awake function:


void Awake () {
    InitState();
}

void InitState () {
    state = new CubeState {
        x = 0,
        y = 0
    };
}

Most of the work is done in the Update function, where we simply check for any arrow keys pressed and act accordingly:


void Update () {
    KeyCode[] arrowKeys = {KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.RightArrow, KeyCode.LeftArrow};
    foreach (KeyCode arrowKey in arrowKeys) {
        if (!Input.GetKeyDown(arrowKey)) continue;
        state = Move(state, arrowKey);
    }
    SyncState();
}

void SyncState () {
    transform.position = new Vector2(state.x, state.y);
}

If an arrow key is indeed pressed, we compute the cube's new grid position using the Move function, which takes the cube's previous grid position and the arrow key pressed as arguments, and gives us back the cube's new grid position:


CubeState Move(CubeState previous, KeyCode arrowKey) {
    int dx = 0;
    int dy = 0;
    switch (arrowKey) {
        case KeyCode.UpArrow:
            dy = 1;
            break;
        case KeyCode.DownArrow:
            dy = -1;
            break;
        case KeyCode.RightArrow:
            dx = 1;
            break;
        case KeyCode.LeftArrow:
            dx = -1;
            break;
    }
    return new CubeState {
        x = dx + previous.x,
        y = dy + previous.y
    };
}

Right now, we are just making use of Unity's basic, non-multiplayer features, so hopefully everything seems familiar to you at this point. If not, I suggest that you familiarize yourself with the basics of Unity first, then try going back to this article to learn about how to get started making multiplayer games with Unity.

We will be working with this example and adding multiplayer functionality as we go along.

Let us begin!

The Big Picture

One way of visualizing how our networked simulation is going to work is by thinking of our scene as one being shared by multiple players. In this setup, each player is assigned a GameObject which that player owns. In the case of our project, we will assign a cube to each connected player. Given this scenario, we will try to answer the following questions:

  1. How are connections among players established?

  2. How are multiple cubes instantiated for the connected players?

  3. How does the server keep everything in sync across all connected clients?

  4. How do players tell the server how the cubes should move?

Note that once we have the answers to each of these questions, we will have the basic ingredients needed for building many different types of multiplayer games. Let us try to answer each of these one by one.

Establishing Connections

The HLAPI follows a client-server architecture, which means that machines do not connect to each other directly, but instead one of the machines is designated as the server and all the other machines, the clients, connect to the server. The server can also act as a client at the same time, so there is no need to have a dedicated server (although this is also possible). We will build our project such that it should be possible to run it either in server-only mode or as a server and as a client at the same time (also known as host mode).

Remember that in Unity, functionality is determined by GameObjects and the combinations of Components that they contain. Keeping this in mind, we will be following this recipe to allow us to act as a server or as a client:

  1. Create an empty GameObject. You can name it "NetworkManager".

  2. Add a NetworkManager component to it. This component provides an easy way to control the HLAPI.

  3. Add a NetworkManagerHUD component to it. This component provides a basic UI to control the NetworkManager.

And we are done! With the HLAPI, it is this easy to give a project the ability to start a server or connect to one as a client.

Assigning Player GameObjects to Clients

At this point, although we are already able to establish connections, there really isn't much happening yet. When a player connects, what we would like to do is instantiate a cube and give the player ownership of this cube. We can do this using Player Objects:

  1. First, we take our existing cube GameObject (described in the "Our Starting Point" section above) and add a NetworkIdentity component to it. This tells the HLAPI to take care of the networking aspects of this GameObject.

  2. Assign this prefab to your NetworkManager's "Player Prefab" field.

  3. If the cube is still on the scene, delete it. A new instance of the cube will be created automatically every time a new player connects. Also, because of the cube's NetworkIdentity component, the HLAPI knows that this is a networked GameObject, which causes this very same cube to be instantiated on all connected clients.

Synchronizing MonoBehaviours Across Machines

Contrary to what you might expect if you have not worked on a networked game before, adding a NetworkIdentity component to a GameObject does not automatically synchronize its corresponding attached components with all other connected clients on a network. In my opinion, it is actually a good thing that the synchronization is not fully automatic, since different games have different needs and having some control over what is synchronized and what is not allows us to tune a game for better performance, and in some cases even allows us to be more flexible with our game designs.

Having said all that, synchronizing a MonoBehaviour's data across the network using the HLAPI is very simple:

  1. Subclass NetworkBehaviour instead of MonoBehaviour. A NetworkBehaviour is basically a MonoBehaviour plus the added ability to pass data between the client and the server. Making this change is very easy. We just turn this:

    
    public class CubePlayer : MonoBehaviour

    Into this:

    
    public class CubePlayer : NetworkBehaviour

    By using NetworkBehaviour, we will have the data-passing features of the HLAPI available for us to use in our code.

  2. Mark member variables to be synchronized with the SyncVar attribute. In practice, we just need to change this:

    
    CubeState state;

    Into this:

    
    [SyncVar] CubeState state;

    By marking our member variable as a SyncVar, any update to the value of the variable on the server is propagated across all connected clients. Just make sure that any changes to SyncVars are done from the server.

Restricting Input

If you try to run the project at this point, you will notice that when we press the arrow keys, all the cubes on the screen respond to the input, which is not what we expect. Instead, only the cube that we own should respond to the input. To correct this, we just need to make a slight tweak to our Update function as follows:


void Update () {
    if (isLocalPlayer) {
        // handle input here...
    }
    SyncState();
}

The isLocalPlayer variable tells us whether or not the player object we are dealing with is our own. If it isn't, then we ignore any input, and if it is, then we move the cube as we should.

Controlling Cubes

The last sentence from the "Synchronizing MonoBehaviours Across Machines" section gives us a very important clue on what we should do next. In order for our SyncVars to work, they should be updated from the server. However, this isn't what we are doing at the moment, which is preventing our SyncVars from doing their job. Currently, our code does this:

  1. Each client updates variables marked as SyncVar directly whenever an arrow key is pressed.

  2. Only the host's updates get propagated to all connected clients, while everyone else's updates are effectively ignored.

Remember that SyncVars should only be modified from the server, so this is what we should be doing instead:

  1. When an arrow key is pressed on a client, the client should tell the server which arrow key was pressed.

  2. The server, in turn, should update the SyncVars based on the received input.

  3. Since the SyncVars are updated from the server, any changes are propagated properly to all connected clients.

The HLAPI provides us with a way of calling functions on the server from the client using Commands. Existing functions can be turned into Commands by marking them as follows (similar to how we turn variables into SyncVars):

  1. Add the Cmd prefix to the function name.

For example, given this function:


void MoveOnServer (KeyCode arrowKey) {
    state = Move(state, arrowKey);
}

We just need to change this function like so to turn it into a Command:


[Command] void CmdMoveOnServer (KeyCode arrowKey) { /* ... */ }

Once we have a Command, we can call this function in the same way that we do any other function, but since it is marked as a command, calling it on a client causes the code it contains to run on the server. Continuing our example, to use the Command that we have made for our project, we just need to change the following line in our Update function:


state = Move(state, arrowKey);

To this:


CmdMoveOnServer(arrowKey);

By making this simple change, we are now able to execute functions on the server, which means that our SyncVars are now updated properly.

Note that Commands are only called on Player Objects that you own - this prevents a client from taking over control of another client. On the server side, this gives us guarantees on who made the function calls, which helps us make our networked multiplayer games secure.

Server-Only Functions

Before we wrap this up, let us revisit our Awake function, which consists of only one line: a call to InitState. To make our demo run as expected, we need to make sure that this initialization runs on the server only and not on the clients. One way of ensuring this is by marking InitState as a server-only function. Fortunately, this is easy with the HLAPI. All we need to do is mark our function with the Server attribute like this:


[Server] void InitState () { /* ... */ }

If we call InitState on a server/host, it runs as it should. Otherwise, the call gets ignored.

Next Steps

If you made it this far, congratulations! You now have a simple, yet working, networked multiplayer demo in your hands. If you haven't yet, you can grab a copy of the project folder and see all of this in action on your own machine.

When you run the demo, you might notice that when you try to control your cube, there is a delay between pressing the arrow key and the box moving on the screen. This is the effect of latency. Unfortunately, it takes some time for the keypresses that we send to travel from the client to the server, and some more time for our cube's state information to travel from the server to the client.

Fortunately, there is a way for us to hide latency, and we will be covering this in Part 2 of this series.

When will Parts 2 and 3 be available?

The not-so-helpful answer is this: as soon as I finish them. However, the code for parts 2 and 3 are already included in the project folder available for download. If you are interested in seeing how client-side prediction and server reconciliation can be implemented using the HLAPI, hopefully the project folder download will be a good resource for you.

Do you know anyone who might find this article helpful?

If you liked this article, I would really appreciate it if you share it with a friend or a colleague who you think might also be interested in building networked multiplayer games with Unity.

Thank you for reading, and until next time!

Read more about:

Featured Blogs
Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like