CODEX

Intro to Multiplayer in Unity with Photon

Multiplayer programming is tricky, so let’s start with the fundamentals of working with networked game objects.

Evan Martinez
CodeX

--

My most treasured memories have been from playing multiplayer games with my friends, whether online or on the couch. Pulling all-nighters playing Halo and drinking soda in the basement was our definition of an awesome weekend. Now that I’m on my own game development journey, I can’t wait to be able to build my own multiplayer games so that others can make their own memories.

Unfortunately, it turns out that it’s very hard to jump right into networked multiplayer programming, even when tools like Unity and Photon have made huge strides in ease of use. Online multiplayer requires you to look at your game in a completely different way, and that conceptual shift was a huge stumbling block for me. In this article, I’ll explain how to shift your thinking to get into creating multiplayer games with Unity and Photon.

Introducing Photon

Photon is a great package that provides you with all sorts of code we’ll need to multiplayer programming. In an effort to focus on concepts, we’ll breeze through the setup steps. It’s super easy to get started with Photon, and the instructions on their website should be more than adequate to get set up (https://doc.photonengine.com/en-us/pun/current/getting-started/pun-intro). Essentially you need to:

  • Create a Photon account.
  • Create an application through the Photon site to get an application ID.
  • Add the Photon package to your Unity project via the Package Manager.
  • Inside Unity, run the Photon Setup Wizard to enter your application ID.

Since Photon provides you with some basic free usage, you’ll be able to handle up to 20 concurrent users without having to do additional configuration!

Multiplayer Multiverse

I originally thought that when I was playing an online multiplayer game, it was like every player had a window into a single game instance somewhere. I thought that my commands were sent to that game instance and I was merely seeing the result played out, kind of like everybody was watching the same TV show. Technically, you can make your multiplayer game like that, but we’ll be using a different model. Instead, what happens is each player (also called a client) runs a separate instance of the game on their own computer.

Photo by Nick Fewings on Unsplash

Let’s ignore player input for a minute and imagine we have a game that starts by creating an environment. If you and I join the same game, then each of our computers will run the game’s code independently. The code might place some keys, lock some doors, and initialize some explosive barrels, for instance. The result is that you and I will be looking at the exact same game level, which makes sense considering the exact same code was run.

For the most part, we don’t need to change how we code our game logic. The same rules should apply no matter how many clients are connected. Even if 128 people are playing, there should be code that says a barrel should explode if shot. The tricky stuff starts when we have to consider how we’ll keep all the clients in sync. If I shoot a barrel in my game for example, we need to make sure that your game shows my character shooting and the barrel exploding.

The Glorious PhotonView

The Photon package gives us access to a MonoBehaviour component called PhotonView, and boy is it going to do some work! When you attach a PhotonView to a game object, you’re saying that the game object is the same across all clients. That means that if a given barrel has photonViewID = 1, anything we do to that barrel in my instance of the game can by synced with the same barrel in your instance of the game. If my barrel explodes, it can tell all of the other versions of itself in other clients to also explode.

Photo by Stephen Radford on Unsplash

PhotonViews communicate using functions called RPC’s. If a function is designated as an RPC, then it can be called by other versions of itself in other clients. Here’s what an RPC looks like:

public class LaserShot: MonoBehaviourPun
{
public GameObject laserBeam;
private void Update()
{
if (Input.GetKeyDown(KeyCode.A))
{
ShootLaser();
}
}
public void ShootLaser()
{
laserBeam.SetActive(true);
this.photonView.RPC("ShootLaser_RPC", RpcTarget.Others);
}
[PunRPC]
private void ShootLaser_RPC()
{
laserBeam.SetActive(true);
}
}

(Quick aside: MonoBehaviourPun is the same as MonoBehaviour, it just allows you to access a PhotonView component on the same game object automatically without having to use a GetComponent call.)

In this example, each client has a Character game object with this LaserShot component and a PhotonView component.

  • The Character in my instance of the game calls its ShootLaser function, which activates its laserBeam in my game.
  • Then, this.photonView.RPC sends a message to all other versions of this Character game object (by virtue of the RpcTarget.Others parameter) to tell them to call their ShootLaser_RPC function.

So what’s happening here is that my version of the game object activates its laser beam, then all other versions of that same game object get a call to activate their laser beams as well. ShootLaser_RPC, could have been called anything, all that matters is that the function has [PunRPC] before it.

What’s Mine is Mine

There’s a huge problem with the example above. Remember that every client is running its own copy of the same game. That means that if you and I are in a deathmatch, we’ll each have a game that has two Characters in it. Since my version of the game has two Characters, each with a LaserShot component, then hitting the ‘A’ key will call the ShootLaser function on each Character. There’s no code to determine which Character I can control!

Photo by Maximalfocus on Unsplash

Not to worry, because a photonView has a concept of ownership, and you can use that ownership to determine whether or not your key commands should work on a particular game object.

Every client in a Photon-networked game is called a Player, and a photonView's owner can be set to a given Player. During the game, you can query if a photonView belongs to the local client with isMine:

private void Update() {
if (this.photonView.isMine && Input.GetKeyDown(KeyCode.A))
{
ShootLaser();
}
}

Or an even better pattern:

private void Update() {
if (!this.photonView.isMine)
{
return;
}
if (Input.GetKeyDown(KeyCode.A)
{
ShootLaser();
}
}

Now, even though both Players are running their Update functions, only the Player with the photonView belonging to you will actually respond to input!

Out of the Box

Keeping things in sync is a lot of work, so Photon provides you with a couple more components to make your life a little easier.

If you also add the PhotonTransformView component, then the position, rotation, and scale of the game object can be kept in sync automatically across clients. That means that you can focus on letting each player control the movement of their own character, and those properties will update in other versions of the game!

Similarly, the PhotonAnimatorView component will keep animator parameters in sync. As long as each player controls their own animator parameters, they’ll be updated across every version of the game automatically!

Cool Down

Whew, that was a lot to cover! There are a lot of new patterns we’ll need to learn for networked programming, but these foundational concepts are how you’ll be approaching problems. It’s complex, but very doable. I hope this helped clear up some confusion around Photon and RPC’s, feel free to ask any questions you might have!

--

--

Evan Martinez
CodeX
Writer for

Hey I'm Evan. I'm a software engineer who loves coding, games, and coding games! I hope to write more about philosophy and coding.