In the second part of our pong series, you’ll be creating a fully synchronized multiplayer game using Elympics. Let’s dive in!

Ball synchronization

The first thing you’ll work with is the main element of the Unity 2D pong game: the ball. Let’s try to modify it so that its position is synchronized for every player. To do that, open Ball.prefab located in Assets/Prefabs.

Ball

In this prefab, we can see some standard components like Sprite Renderer and Box Collider and scripts responsible for moving the ball. To synchronize something with Elympics, we need all objects to use the basic component of Elympics Behaviour. Add it by clicking Add Component at the bottom and select Elympics Behaviour.

EB

Awesome! Let’s take a look at what you’ve just added to your Unity 2D pong.

Elympics Behaviour allows you to set synchronization settings like prediction, refresh rate, etc.

Let’s leave these settings at default for now. As we need to add position synchronization, let’s focus on the three buttons at the bottom: Add Game Object Active Synchronization, Add Transform Synchronization and Add Rigidbody 2D Synchronization.

As the entire ball movement is done using Rigidbody 2D, all you need to add is Rigidbody 2D Synchronization.

Add RB Sync

Rigidbody 2D synchronization lets you synchronize physics-based objects. To keep your network bandwidth low, add as few synchronizers as possible.

You’ve just added Elympics Rigidbody 2D Synchronizer to the ball object. It allows you to tweak synchronization settings and parameters such as the required precision. For now, let’s leave these at default.

That’s it! The ball movement is fully synchronized between the players of your Unity 2D pong. Every player connected to the game will be able to see the current ball position and velocity, in the same way as on the server instance.

Half remote mode

To test what you’ve just created, you’ll need to use the half remote mode. It’ll allow you to host both server and client games between multiple editor instances and/or game builds. 

In this case, one of the editors will act as a server, and all others will join as players (clients). Here comes a very convenient package bundled with Elympics: ParrelSync.

To create another editor, launch ParrelSyncClones Manager and add a new clone of your game by clicking Create a new clone. Mind that it may take a while.

Add new clone

Once it’s done, you should see two clones of launchable Unity instances.

Two Clones

To test our ball synchronization, you’ll need only one additional clone (for player 1). Click Open in New Editor in Clone 0 to launch another Unity editor with the same exact project that will be the player during our tests.

Remember that all changes made in the main editor will be automatically applied in all clones. It'll make game testing and debugging much easier.

As you'll have 2 editors open (root and clone), you'll need to specify which of them is the server and the client. To do that, launch Elympics Config in ToolsElympicsSelect or create Config.

Now, you should see one ElympicsConfig with all its settings in the inspector window. Scroll down and make sure that Mode is set to Half Remote, and that Player 1 has the Is Bot option checked.

Change to half remote

Now, set the main editor as Server in the half remote mode.

In the second editor (Player 0), edit the same settings and set it as Player. Connect the localhost IP to your loopback (the same as in the screenshot below).

Static options set for half remote

Player 1 should be set to bot so that you can start the game with one player only. Alternatively, you can create another editor clone and launch it as the second player.

That’s it! You should be able to test your game. 🎉

First, start Play mode in the editor that was configured as the server. Once it starts, do the same in the clone editor (player). At first, nothing exciting will happen, but when you try “playing” in the server editor, you’ll see that the ball is bouncing in the client editor as well despite not hitting the pad. It means that the ball is now fully synchronized and streamed to the client from the server.

Pong working
Anakin working

Wow! The position and velocity (so the entire simulation) of the ball are now fully synchronized between instances! 😎 It’s time to move to input handling.

Inputs: moving player pads

Ball synchronization is ready! Now, you need to assign specific pads to players and allow them to control them.

The input handling in Elympics is handles using the IInputHandler interface. Let’s implement it in our PlayerInputController.cs.

To handle the local hot-seat gameplay, you'll need to check, using the Update method, which player played the input to assign it to the correct pad. With an online multiplayer, you won’t have to handle that case anymore, because each player will have their own keyboard and will send their own input to the server.

Now, let's implement IInputHandler in PlayerInputController (remember to add the using Elympics; clause).

You'll need to implement two methods from IInputHandler: OnInputForClient and OnInputForBot. Before you do that, take a look at each of them:

  • OnInputForClient: used to collect inputs (from devices such as keyboards) for the current client. This input will be collected by Elympics and sent to the server as well as executed locally. This method is the place where all input devices should be queried, and all input data should be saved to inputSerializer so that they can be deserialized and applied in the next step.
  • OnInputForBot: if your game uses bots (just like it should), this is the place to get the same inputs for our bots. The rules for this method are exactly the same as for OnInputForClient.

In most single-player games written in Unity, it’s easier to instantly apply effects of players’ inputs to the game world in the Update method. However, when creating a game with Elympics, it’s crucial to separate those two distinct steps.

Input should be understood as the player’s intention of doing something with the game world. That's what should be saved and serialized in OnInputForClient and OnInputForBot respectively. Later, this intention should be transferred to the game world in ElympicsUpdate using ElympicsBehaviour.TryGetInput, which is a separate step (it’s a necessary distinction for technical reasons and parallel gameplay synchronization. Read more about reconciliation to dive deeper).

You won’t be creating any bots in your game for now, so let's focus on OnInputForClient.

OnInputForClient: implementation

Let’s start by gathering player input and using it with the previously described interface. First, let’s move the whole Update method to OnInputForClient. You don't need the entire Update now, so you can get rid of it. The same applies to FixedUpdate: you can delete it safely too.

Your OnInputForClient should look like this:

public void OnInputForClient(IInputWriter inputSerializer)
{
 if (playerId == 0)
   movementY = Input.GetAxis("Vertical Player 0");
 else if (playerId == 1)
   movementY = Input.GetAxis("Vertical Player 1");
}

When creating a multiplayer game, you don’t need to distinguish which player is responsible for a certain input. Elympics will handle and abstract it for you. You can change the implementation to:

float movementY = Input.GetAxis("Vertical");

Remember to modify Input Manager for Unity. You won't be using “Vertical Player 0” and “Vertical Player 1” – just “Vertical”.

You can modify it easily in Unity’s input settings: EditProject SettingsInput Manager.

Project settings
Input manager

Now, you’ll need to do something with the input from the system. Remember not to use this input right away. In OnInputForClient, you just need to gather and serialize all inputs (intents). Let’s do that!

To save this input for Elympics to send and use, you need to use inputSerializer provided to your OnInputForClient method. Just add this line:

inputSerializer.Write(movementY);

That’s it! 🎉 But we’re not finished yet.

You have two pads on scene which means you’ll be sending two inputs each time: one for the out pad and one for the other (enemy). You can add the necessary checks when applying these inputs, but why not fix it now? Just add these two lines at the start of the OnInputForClient method:

if (Elympics.Player != PredictableFor)
  return;

You can easily identify each player by using PredictableFor, so you no longer need the playerId variable. Just remember to set predictability in every character’s ElympicsBehaviour to the chosen player.

Awesome! You’ve simplified a few components in our implementation:

  • you don’t have to handle player identification with inputs gathering,
  • movementY variable can be a local variable; it won’t be used to modify the game world directly, but serialized to inputSerializer and delivered to the server and clients by Elympics,
  • you need only one local input manager in project settings. Elympics will determine the correct input<>player assignment.

Congratulations! Player’s input was saved, serialized, and automatically sent to the server!

The entire PlayerInputController should look as follows:

public class PlayerInputController : MonoBehaviour, IInputHandler
{
  private PlayerMovement playerMovement = null;

  private void Awake()
  {
    playerMovement = GetComponent<PlayerMovement>();
  }

  public void OnInputForClient(IInputWriter inputSerializer)
  {
    if (Elympics.Player != PredictableFor)
        return;
  
    float movementY = Input.GetAxis("Vertical");
    inputSerializer.Write(movementY);
  }

  public void OnInputForBot(IInputWriter inputSerializer)
  {  
  
  }
}

Applying input: reading and applying players’ inputs to the game world using ElympicsUpdate

The next key concept in Elympics is reading the previously saved input. You can do that in the ElympicsUpdate method by using ElympicsBehaviour.TryGetInput.

As inputs are tick-synchronized and guaranteed to be present only in that context, they should only be read inside ElympicsUpdate.

Remember that ElympicsUpdate needs to be implemented from IUpdatable!

Take a look at the reading input implementation:

public void ElympicsUpdate()
{
    var movementY = 0.0f;
 
    if (ElympicsBehaviour.TryGetInput(PredictableFor, out var inputReader))
    {
        inputReader.Read(out movementY);
    }

    playerMovement.ApplyMovement(movementY);
}

As you can see, ElympicsBehaviour.TryGetInput has two parameters:

  • ElympicsPlayer: the player that sent all the data regarding their input. Used mostly when applying input to the object controlled by a specific player.
  • out IInputReader: object storing input data saved previously to the IInputWriter.

In our pong game, you'll be using only one dimension of input (a single variable): float - movementY. That’s the only input sent to the server, and the only one that we’re going to read from inputReader.

inputReader.Read(out movementY);

And just like that, you’re reading the information sent by players. It’s important to read data in the same order as it was serialized in!

The next step is to check if the input read is meant for this particular object. To do that, you'll need to use the playerId variable that stores the id of the player responsible for controlling the specific pad (players are indexed starting at 0).

playerMovement.ApplyMovement(movementY);

From now on, you’ll be able to move the pad from the player’s editor in the half remote mode! Each player will have their own pad assigned and will only be able to move it using both arrow keys and w/s keys.

Before testing your work, you’ll also need to update the pad prefab (player).

Player pad synchronization

Players are now able to move their pads by sending movement data to the server. Next, you’ll need to synchronize the state of both pads. Fortunately, you already know how to do that!

Open Player.prefab (located in Assets/Prefabs).

Pad Prefab

The first step, just like before, is to add the ElympicsBehaviour component. Then, add Rigidbody2D Synchronization in order to synchronize the pad object.

Player add rigid body synchronization

Note that implementing the IInputHandler interface in PlayerInputController.cs changed the number of observed Mono Bahaviours in the Elympics Behaviour component.

Player observed objects

If any object with ElympicsBehaviour is controlled directly by any of the players, it’s always useful to add Prediction so that the player’s actions are instantly visible on the screen (with no roundtrip of the data to the server).

Let’s make the pad predictable for the player that controls it. Predictable for parameter is modified in ElympicsBehaviour and is set to None by default. Let’s change that to Player.

Predictable for

The changes in Predictable for were applied in player prefab, but we still need to set them in specific instances. Let’s make sure that the Id of the player in the scene is the same as the Id in PlayerInputController.cs and equal to the player that controls this particular pad.

Predictable for both players

Ready! 🎉

From now on, whenever a player connects, they will be assigned to their own pad. Only they will be able to control it (with arrow and w/s keys). The server is no longer able to move any of the pads directly in the half remote mode.

Don’t forget to test what you’ve done! You’ll need one editor for the server and two clones for both players (remember to uncheck Is Bot for player 1).

Synced Gameplay
Top: Player 0, Bottom: Server

Congratulations! 

You’ve just created a fully synchronized multiplayer Unity 2D pong game using Elympics! 🎉🎉

In the next Unity 2D tutorial from this series, we’ll use synchronized variables to save our match score in a deterministic and safe way. Stay tuned!

In the meantime, remember to visit our Discord where you'll find Elympics project updates and tech support from our developers.