Lasers and Mirrors: Let’s make it a multiplayer game.

Ok, the game has matured a lot now. But playing alone a puzzle game can be a bit boring. Why don’t we make it an online game where friends can play together? Sounds great right?

When we are creating a multiplayer game we need understand one simple concept. That is each game works as a client and it needs to communicate with a server. That server can hold the logic of the game, tracks the game’s progress and sends information to other clients such as health of other players, bullets shot and other things. You can also make a certain player host a game like Among Us where players create rooms and other players join the room and play together. You can either create this whole server/client logic on your own or you can just use one of the tools available to make things easy.

This is where Photon comes in handy. Photon is a plugin for unity that you can use for free to create multiplayer games. In this article I will explain how I turned this game, from a single player game to a multiplayer game.

First you need to go to Photon’s Website and create an account. Once you do that you will be taken to your Dashboard. In the mean time, head to PUN 2 — Free page in Unity’s Asset Store. Purchase it, and then import it into your project. When the import is done it will ask you to enter the app ID of your game. This is where you need to head to the dashboard of your photon account.

Figure 1: Photon Dashboard

Click on the Create A New App button. This will take you a another page where you can create your app.

Figure 2: The Create a New Application form

All you need to do in that form is to make sure that Photon PUN is selected for Photon Type and enter the name of your game. When you are done, click on Create. This will create an App ID for your game and you can see it in your dashboard.

Figure 3: The App ID of the created game

Now, copy that app ID from the dashboard and paste it into PUN’s welcome window and click on Link App and now your game is linked to Photon. 

With all the logistics out of our way, lets start coding.

Before we do that, lets see what we need to do:

  1. First we need to connect to the server.
  2. When the game is connected to a server, we need to move the player into a lobby so they can create of join games.
  3. Whether the player creates of joins a game, the player needs them to be directed towards a newly created room or an existing room.

I started by creating a loading scene that starts with the game. This scene has a GameObject that is responsible for connecting to the Photon server.

Figure 4: The Loading scene

The Connect To Server GameObject contains a script that connects to the Photon server:

Figure 5: The ConnectToServer Class

As you can see we are using a new namespace for PUN so we can use its functionalities. The beauty of this library is that it has a class called MonoBehaviorPunCallbacks; this class inherets from MonoBehavior so you can use it with GameObjects and at the same time leverage the callbacks of PUN. When the object starts we connect to the PUN server using the default settings. Then we have two call backs:

  1. OnConnectedToMaster: this is called when the game connects to the server and as you can see, when the connection is successful we then join a lobby.
  2. OnJoinedLobby: this is called when the game successfully joins a lobby. At this point we want the game to transition to the lobby scene so the player can create or join rooms.

The Lobby is another scene that contains more UI. In the lobby the player can create a nickname for themselves, create a new room or join an existing room.

Figure 6: The game’s lobby

In that scene we have a GameObject that manages the creation or joining of rooms. The script of that class looks like this:

Figure 7: The CreateAndJoinRooms Class

As you can see in figure 7, I am referencing the input fields where the player input resides. Then I have two public functions: CreateRoom() and JoinRoom() these two functions are linked to the Create Room button and Join Room button’s OnClickEvent. These two functions simply make sure that the player has a nickname added and then The CreateRoom() function create a new room with the room name given; and JoinRoom() function joins a room by its name. Finally there is a PUN callback called OnJoinedRoom() that gets called when the game joins the room and at this point you want the game to move to the game scene. If you notice, we are not using the SceneManager to transition to the game scene instead we are using PhotonNetwork to do the transition. The reason behind that is that we want to move to the scene with the room’s session. If we use SceneManager we will only transition to a local version of the scene and not the scene corresponding to the room created.

The way I designed the game is that when a player joins the room, the game assigns either a Mirror or a Lens to the player. Only the assigned player should be able to control their own object and no other object. Doing this is pretty straight forward. First, you need to move you player prefabs from their existing folders to a folder called Resources. If the folder does not exist, you have to create it yourself and add the spawnable prefabs in it. Otherwise PUN will not be able to identify which prefabs to respawn.

Figure 8: The Resources folder with the player objects

Then I created a GameObject to manage the spawning of the player prefabs into the level. This script should have a reference to the player prefabs and I have created bounds so I spawn the prefabs in random locations.

Figure 9: The SpawnPlayers references

The script of SpawnPlayers is this:

Figure 10: SpawnPlayers Script

As you can see in figure 10, in the start function, we pick a random position to insatiate the player, then I pick a random player prefab (Mirror or Lens) and insatiate the player prefab in the level.

One important step is that you need to update the player prefabs and add the following components to it: Photon View and Photon Transform View Classis. Photon View basically tells the system that that certain prefab belongs to which user. Photon Transform View Classic simply syncs any change in position or rotation to the prefab with the other players. If you do not add that component, you will be moving and rotating the prefab in your screen but it will remain stationary in all the other screens. I did the same with both the Mirror Prefab and the Lens Prefab

Figure 11: The Photon components added to the player prefab

To make sure that the player is only able to move their prefab I modified the MoveObject script a bit. Now, it looks like this:

Figure 12: The updated MoveObject functions

All I did is that in the OnMouseDown and OnMouseDrag I added a condition so that the drag logic executes if and only if the view is the player’s.

I also wanted to do is to display the player’s nickname on top of the player’s prefab, just like other Multiplayer games. All I did is that I added a child canvas and marked its Render Mode to be World Space so it displays on the player and not as a static interface item. In that canvas, I added a Text object to hold the player’s nickname.

Figure 13: The added Canvas for player nickname

Then I created a script on the player prefab to handle the assignation of the nickname to the Text object. The script looks like this:

Figure 14: The PlayerNameTag Script

As you can see the script has a reference to the Text object so it can update it. In the start function we check whether the photon view is the player’s or not. If it is, we don’t want to display the nickname of the player on their own object. Otherwise, we simple grab the nickname from Photon View and assign it to the Text Object.

When the game is won, the GameManager then calls a function from the NetworkManager Object to disconnect from the current room and return to the lobby. The NetworkManager Script looks like this:

Figure 15: The NetworkManager Script

The DisconnectPlayer() function is called from the Game Manager. That function starts a coroutine that leaves the room and once the game leaves the room it loads the lobby scene so the player can join or create another room.

One of the things that I noticed when testing is that every player gets a different level than the other. If you remember from my previous post, the game automatically places the laser and the target at random positions. The problem now is that, each game manager in every client creates its own level by placing the laser and target in different positions at each client. What needs to happen is that I want the host to somehow communicate with the clients and tell them that this is where they should put the laser, and this is where they should put the target; so we need to sync the levels in the clients to match the one at the host. After a lot of research, I found that this is very wasy to implement.

First, Since we need to check who’s game manager is this (A host or a client) we have to add a Photon View component to the Game Manager Object.

Figure 16: The Photon View Component added to the Game Manager

I also did slight modifications to the GameManager script where I declared global variables to define the selected indices of both the laser and target positions. I have also modified the start function of the script to look like this:

Figure 17: The updated Start function in the GameManager script

As you can see in figure 17, I encapsulated the script to select a random position to be only done by the master client which is the host. The question now is, how to we send the selected indices to the clients so they position the laser and target at the exact same place? There is a function called OnPhotonSerializeView() which can be implemented by implementing an interface called IPunObservable. This function is called by PUN several times per second so that the script can write and read synchronization data for the photon view. The function looks like this:

Figure 18: The OnPhotonSerializeView() function

What happens here is that the function checks whether the current view is writing or not and since this is the host (for the purpose of this game), the function will be sending data and not reading. Basically, I put both indices as a Vector2 and serialize it for the stream to write it. If the client is not writing, then, I read the data from the steam as Vector2 and grab the correct transforms from the array and place both the laser and target at the correct positions as the host. With that done, I created a system where the client level are in sync with the host’s level.

This is how you create a very simple multiplayer game!

Figure 19: The Multiplayer system in action

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s