The Marco Polo Tutorial is a Unity3D based tutorial. It will show you how to develop your own multiplayer enabled application powered by Photon Cloud.
- Walking Vikings
- Photon Cloud
- Starting From Scratch
- Reception: Getting a Room
- Marco Polo: Syncing Positions
- Shout and Respond
- Switch “It”
The first part of this tutorial is a walkthrough for one of the Photon Unity Networking demos (short: PUN). You will import the PUN package, set it up to use the Photon Cloud and give it a test run.
The second part teaches you to develop multiplayer features with the Photon Unity Networking package and the Photon Cloud service.
What you need
This tutorial assumes you know the basics of using the Unity Editor and programming. The sample code is written in C# but works similarly in Unity Script.
Let’s first try the “Viking Demo”, one of the Photon Unity Networking samples from the Asset Store.
Everything needed is in the package, so we create a new, empty project in Unity. Search for “Photon Viking Demo” in the Asset Store (or click the link). Download and import the package in Unity.
Importing the package opens a “PUN Setup Wizard” window in the editor. More about that in a moment.
We now have three folders in our project: “DemoVikings”, “Photon Unity Networking” and "Plugins". The PUN and Plugins directories wrap up all the networking code you would need in a project. The “Vikings” folder contains the sample.
The PUN Setup Wizard is there to help you with the network settings and offers a convenient way to get started with our multiplayer game: The Photon Cloud!
Cloud? Yes, Cloud. It’s a bunch of Photon servers which we can use for our game. We will explain in a bit.
Using the Cloud with the "Free Plan" is free and without obligation, so for now we just enter our mail address and the Wizard does its magic.
New accounts get an “AppId” right away. If your mail address is already registered, you are asked to open the Dashboard. Login and fetch the “AppId” to paste it into the input field.
When the AppId is saved, we are done with this step.
Walking the Viking
After all that work, it’s time for some action. Multiplayer action!
Open the scene "DemoVikings\Scenes\VikingScene" and build and run the demo as standalone.
A “main menu” pops up and we can enter a player name and room names and there is a (empty) “Room Listing”. When we “Create” a room the scene switches and finally a Viking shows up. It’s only one but that’s a start.
Start the demo in the editor as well and the “Room Listing” contains a single entry. We “join” that room and finally there are Vikings (as in plural).
We can start a few more, running around with each. The camera always follows our “own” Viking per client.
This already concludes the first part of this tutorial. You learned how to get the PUN package, import and set it up. Not surprising but also important: You have learned that you will need to run multiple clients to test your game.
So, what exactly does this “Photon Cloud” do?!
Basically, it’s a bunch of PCs with the Photon Server running on them. This “cloud” of servers is maintained by Exit Games and offered as hassle-free service for your multiplayer games. Servers are added on demand, so any number of players can be dealt with.
Even though Photon Cloud is not completely free, the costs are low, especially compared to regular hosting. Read more about the pricing here.
Photon Unity Networking will handle the Photon Cloud for you but this is what’s going on internally in a nutshell:
Everyone connects to a "Name Server" first. It checks which app your client (with the AppId) and which region the client wants to use. Then it forwards the client to a Master Server.
The Master Server is the hub for a bunch of regional servers. It knows all all existing games. Any time a game (room) gets created or joined, the client gets forwarded to one of the other machines - called “Game Server”.
The setup in PUN is ridiculously simple and you don’t have to care about hosting costs, performance or maintenance. Not once.
The Photon Cloud is built with “room-based games” in mind, meaning there is a limited number of players (let’s say: less than 10) per match, separated from anyone else. In a room (usually) everyone receives whatever the others send. Outside of a room, players are not able to communicate, so we always want them in rooms as soon as possible.
The best way to get into a room is to use Random Matchmaking. We just ask the server for any room or a room with certain properties.
All rooms have a name as identifier. Unless the room is full or closed, we can join it by name. Conveniently, the Master Server can provide is a list of rooms for our app.
The lobby for your application exists on the Master Server to list rooms for your game. PUN automatically joins the default lobby and gets the room list. That’s not strictly a must-have: you could join rooms directly if you know their name or join a random game.
In newer PUN versions, the "Auto-Join Lobby" setting is done in the PhotonServerSettings. The default is to not join a lobby.
Application IDs & Game Version
If everyone connects to the same servers, there must be a way to separate your players from everyone else’s.
Each game (as in application) gets its own “AppId” in the Cloud. Players will always meet other players with the same “AppId” in their client only.
There is also a “game version” you can use to separate players with older clients from those with newer ones.
Starting From Scratch
Let’s start something new. For the sake of a simple tutorial, we will not create the next MMO but the popular children's game Marco Polo – with Monsters. Just in case, Wikipedia explains what "Marco Polo game" means. It's enough to show the basics and you should be able to build something on top.
Start over by creating a new, empty project. This time, during import of "Photon Viking Demo", we uncheck the “DemoVikings” folder. When the wizard opens, just enter your AppId. Select the PhotonServerSettings and check "Auto-Join Lobby" (it's needed in this tutorial).
Reception: Getting a Room
Before we do anything else, we need to get our players into a room. In a room we can move around and let others see that.
Create a folder “Marco Polo” and a new C# script: “RandomMatchmaker”. It contains Unity’s Start method - the perfect place to connect as soon as possible.
The most important class in the PUN package is called
PhotonNetwork. It’s similar to Unity’s Network class and contains almost all methods we’re going to use.
As we already saved our AppId in the
PhotonServerSettings file, we can now connect by calling
PhotonNetwork.ConnectUsingSettings. The gameVersion parameter should be any short string to identify this version. Let's use "0.1".
In play mode, this should get us connected and into the lobby. However, we won't notice any of that - there is no output at all. Let’s show some state! With a minimum of GUI, the code looks like this:
The script is not in the scene yet. We create a new, empty GameObject and name it “Scripts”. This makes it easier to find the scripts later on and we don’t rely on the camera.
Add the RandomMatchmaker to Scripts (the GameObject), save the scene and run it. Our label cycles through some states and finishes in the lobby. If it stops in state OnConnectedToMaster, you should enable "Auto Join Lobby" in the PhotonServerSettings.
PUN is Calling
Like Unity, PUN will call certain methods in our code when something interesting is happening. Currently, we are interested in something like “arrived in the lobby” or “found a room” and “didn’t find a room”.
PUN defines its callbacks in an enum named PhotonNetworkingMessage. Nice.
However, to implement callbacks easily in our code, we better change our class a bit: Find "MonoBehaviour" in RandomMatchmaker and replace it with
Photon.PunBehaviour. At the beginning of the file, add
using Photon;, which is the NameSpace of PunBehaviour.
Now, the class is a PunBehaviour and we can simply override each callback individually. Type
override and the IDE should list them, so they are easy to find.
If the IDE adds a call to the base method, simply delete that. We don't have to call the empty base-implementation for PUN callbacks.
Note: Callbacks don't have to be an override of PunBehaviour! The methods just need to fit the name and parameter signature. You can also implement them without parameters.
One of the callback methods in PUN is OnJoinedLobby. It’s called when PUN got us into a lobby.
For the time being, we ignore the list of Rooms and instead try to get into a room quickly. The PhotonNetwork class has a JoinRandomRoom method. This tries to get us into any room. Let's try.
When running this iteration, the connectionStateDetailed still ends up as JoinedLobby. Obviously, JoinRandomRoom does not work yet for some reason.
In case of errors, PUN makes use of the log and the console (Ctrl+Shift+C). If something doesn't work as expected and there is no output in console, you can set "higher" log level with PhotonNetwork.logLevel:
Here is an example output:
In our tutorial, lets keep the default log level and just add OnPhotonRandomJoinFailed() callback. It will output info to the log and console if errors occur. Don’t mix it up with the similar OnPhotonJoinRoomFailed (which is used when we failed to create a room).
This proves: PUN Callbacks work even if they are not an
Creating a Room
Joining a random room fails if no one else is playing or if all rooms are maxed out with players. We could show this to our player (GUI work left as exercise) and let her retry in a while. We skip this and simply create a room.
We lookout for a “create room” method in PhotonNetwork. There is a perfect match: CreateRoom. The tooltip explains that there are two overloads and if we pass null as room name it will get a generated one (a GUID). As we don’t show room names yet, we don’t care and pass null.
Try out this code by running it. The detailed state is changing more often than before and ends on “Joined”. We’re in our very own room!
Marco Polo: Syncing Positions
We’ve seen the Vikings running around, so let’s try to do the same. First, download the “Monster” character. It is in the Asset Store too but the linked version is exactly the one we are looking for.
We will instantiate this prefab by name and this means it must be in a Resources folder. Select the asset “monsterprefab” and add a PhotonView component. The Components menu will have a category “Photon Networking” where you find it.
If you know Unity’s Networking: A PhotonView is PUN’s equivalent of a NetworkView. PUN needs a PhotonView per instantiated prefab to keep the networking reference (known as ViewId), the owner of the object and a reference to “observed” components.
PUN keeps track of the PhotonViews it instantiated locally and the observed components can send updates to the other clients at runtime. More observed objects mean more work and network traffic.
To setup the new PhotonView to observe the translation of its “monsterprefab”, drag & drop the Transform component to the “observed” field. We don’t need to change the other settings of the PhotonView.
To instantiate a monster that everyone in the same room can see, we have to use
PhotonNetwork.Instantiate. This will assign a ViewID for this instance and let the other players know where to place the monster. For this to work, you need a PhotonView on the Resource prefab to instantiate.
Don’t mix this up with Unity’s Instantiate, which doesn't update the other players in the room.
Let's do this when we joined the room. In OnJoinedRoom, we call PhotonNetwork.Instantiate like this:
Running this will show a monster pop up and start falling. Poor monster. Let’s create some ground.
The next steps are common "Unity work”: Create a directional light in the scene and rotate it to point down in some angle - imitating the sun.
Add a plane, scale it to 10, 1, 10 and move it to 0, 0, 0. Let’s try some substances from the Asset Store. Download the “Eighteen Free Substances” package and import it. The “Pavement_01” looks nice, so we open its folder and apply the material to the plane. In the inspector, change the texture tiling from 1 to 10 for x and y.
Fight for Control
Run two clients to check progress again. Obviously, all monsters are moved by our key input and the camera won't behave. We cloned the monster including all components that handle input and camera.
There are countless ways to solve this. One simple way is to disable the CharacterControl and CharacterCamera components of the “monsterprefab” (!) and enable them only for “our” monster instance. PhotonNetwork.Instantiate returns the GameObject it created, so modifying our own monster is kind of easy. Aside from one handicap: Both scripts are written in UnityScript and our C# code does not know them yet.
To make any script of one language available to the scripts of another language, you can move it to a “Plugins” folder in your project. So rename the "Scripts" folder from the Monster package to "Plugins" and move it to the root of the project (drag & drop in the editor).
After instantiating our monster, we can now grab the components CharacterControl and CharacterCamera and activate both (for our monster only).
So far, monsters of other players are moved but don’t walk. The animation is missing. Also, the position updates by teleporting the monster around. This can be fixed by a bit of code.
We need another script. Create a C# script “NetworkCharacter” in the Marco Polo folder. Add it to the “monsterprefab” and make it the observed component of the PhotonView (drag & drop).
While a script is observed, the PhotonView regularly calls the method OnPhotonSerializeView. The task of this is to create the info we want to pass to others and to handle such incoming info – depending on who created the PhotonView.
A PhotonStream is passed to OnPhotonSerializeView and the value of isWriting tells us if we need to write or read remote data from it. First, we send and receive Position and Rotation. This has the same effect as the PhotonView without script:
A simple way to smoothly “push” a monster to its correct position is to lerp it there over time.
We only want to smooth the position of remote monsters (we can move our own just fine). If we make the script a Photon.MonoBehaviour (note the Photon namespace), it can access the GameObject's PhotonView, which in turn provides a "isMine" property.
We add Vector3 correctPlayerPos and Quaternion correctPlayerRot to the NetworkCharacter. In OnPhotonSerializeView we store the new values and apply them (bit by bit) in Update.
Monsters of other players won’t be using the exact same speed this way but they will reach the same point in a short time. For some games it might be more important to have the characters at the correct position than how exactly they get there.
Now we run into trouble: We can’t grab the animation state easily: Multiple ones are running, blended, etc, so we have to find another way to sync this.
Looking around for a solution, the myThirdPersonController class (short: controller) comes to our rescue. It uses a _characterState variable to trigger almost all animations and is based on the CharacterController.velocity of our monster. This works locally but our remote copies don't have a velocity. They are just re-positioned.
If each monster would sends its state, the copies could apply the incoming value. This should work nicely, if we didn’t disable the controller in the first place.
If we want to use the myThirdPersonController it must be active even on remote instances and we need another way to ignore our input for “remote monsters”. The variable isControllable looks good: It’s never set yet and not always checked where it should but we can fix both. Let's go.
Again, some "standard" Unity work: Add the myThirdPersonController to the "monsterprefab". The animations will be missing, so add them by dropping each on the respective field. Drag and drop the prefab's Transform (top most prefab component) to the "Buik" field.
As we don't use CharacterControl and CharacterCamera anymore, remove their enabling-code from RandomMatchmaker.OnJoinedRoom. Both components stay inactive now (they could be removed, too). Instead, we set isControllable when we create our monster in RandomMatchmaker.
Open the myThirdPersonController code and make _characterState public and isControllable public and false. We need to find all places where input is used to check isControllable first and ignore input when it's not our monster. Read on.
We read input axes only if controllable (in myThirdPersonController):
We suppress “firing” unless isControllable:
And no one will jump, unless controlled:
Note: The original script was resetting Input at the beginning of Update. Remove that! We have multiple scripts running and we don’t mind which one is executed first. If we keep resetting Input, we will miss it elsewhere.
Now, take a look how animations are applied. Except for “idle”, the current animation is set by the state and faded in. Sounds ok.
In the original script, the idle animation never sets a state but we want to use it for synchronization. To add that, find "controller.velocity.sqrMagnitude < 0.5" in Update() and set the state. We should check isControllable, as remote copies will get their state from the controlling user.
If the char is not controllable, we will apply speed and animations. Below is a modified Update method:
While the original script modifies the animation speed by velocity, we can’t do that for other players’ monsters. Just skip this part and apply the desired animation speed. This could be done better but looks ok for this case.
What did we forget? We planned to send the characterState but didn't do it yet. Modify NetworkCharacter accordingly.
Starting this version finally syncs animations and has smoothed movement:
Notice how the editor (left) shows the same animation as the standalone on top (right).
Shout and Respond
It would be cool if our monsters could actually talk. We could record something or just look around the web for a text to speech synthesizer. No matter how: Get two audio clips for this (e.g. from fromtexttospeech.com).
A PhotonView has a method called “RPC” which is short for “Remote Procedure Call”. So the point of this is to call another method? Boring!
The good thing about an RPC is: It will call a method on other clients in the same room, on the same GameObject. For convenience, it can also call the method on “this” client.
The PhotonView.RPC() method can’t just call any method but only those with a special attribute:
PunRPC. In code, this is [PunRPC] (and @PunRPC in UnityScript).
Nice. We can use RPC methods easily to make our monster shout – locally
and on other player’s clients. In the folder “Marco Polo”, create another
script “AudioRpc”. Add the methods “Marco” and “Polo” with the
attribute. Also add public AudioClip fields, so you can reference the
sound files to play them.
Add “AudioRpc” to the “monsterprefab” and apply the two sound files to the references (both times: drag & drop). We also need to add an Audio Source on the prefab (Main Menu, Component, Audio, Audio Source).
For convenience, we could just add buttons to the RandomMatchmaker GUI and call the RPC by those. However, we don't have access to our monster's PhotonView yet. We can grab and store that when the monster is created. Modify RandomMatchmaker like so:
Note that there are different PhotonTargets options. We’re using “All” now, to play the sound locally, too.
If you run two clients now and click either button, the voice might be too faint to hear. Move to the cam or adjust the loudness and you will notice an echo: Both clients play the sound. The “remote” player will have a short delay (depending on the networking delay).
This only makes a difference while testing but let’s disable the RPC audio in background. Implement OnApplicationFocus in AudioRPC and disable the script when the app loses focus. Then test again. The echo is still there.
RPCs are called on disabled scripts, too. This could be annoying but most likely it’s a good thing. If you want to skip RPCs on disabled scripts, check: .enabled. For the "Polo" part, this looks like
So far, we can call out and answer. What’s missing is a basic game logic to know who is “it” and to tag others.
For our first piece of game logic, we create a C# script “GameLogic” in the “Marco Polo” folder and add it to the “Scripts” object.
The first player who gets into a room is “it”. We know we just got into a room when the method OnJoinedRoom is called and by checking PhotonNetwork.playerList we can easily find out if we’re the first one.
Photon uses a player “ID” or “playerNumber” to mark each of the players in a room. It’s an integer, which makes it relatively small in terms of data. As it’s not re-assigned ever, we can “talk” about players in a room by their ID.
The ID of our local client is stored in PhotonNetwork.player.ID. If we’re alone, we’ll save this into “playerWhoIsIt”, a static field which is easy to access it in different scripts.
When more players are joining we need to let them know who is currently “it”. PUN calls OnPhotonPlayerConnected when someone new arrives so reacting to that is easy.
We will create a method “TaggedPlayer” to set “playerWhoIsIt” and make it a RPC, so players who are in the game already might call it.
We already prepared an RPC method but unless the script is added to some GameObject with PhotonView, we can't call it. Logically, it's not the task of our monsters to remember who is “it”, so let's add another PhotonView.
This time we add a PhotonView to the “Scripts” object in the scene. Being part of the scene hierarchy, it's owner will be "Scene". It's easy to find, always available and usable by any player. The
GameLogic and the PhotonView are on the same GameObject, so we can
GetComponent the scene's PhotonView and call RPCs on that.
It's enough, if just one player updates newcomers. There is no Host as in Unity Networking but PUN has a replacement: The “Master Client” is always the player with the lowest ID in a room. Any client can check if it is currently the master by:
OnPhotonPlayerConnected() and a static method "TagPlayer" to the GameLogic script. The latter is extremely easy to call from anywhere. We will need that later on. This part looks like that:
Players can leave anytime and maybe we lose our “seeking” player this way.
PUN will call OnPhotonPlayerDisconnected when anyone leaves. We should find out if the player who left is “it” and assign a new one. Again, this work is only done by one player, the master.
We still need a way to tag others. To be independent of colliders, we will simply check if the player clicks on another monster. This clicked monster is going to be "it".
We create a new C# script “ClickDetector” in the “Marco Polo” folder and drag it to the “monsterprefab”. In it, we use the Update method to check mouse clicks if it's this player's turn. We get the clicked object (if any) and make sure it's not the plane or our monster.
Once we know what was clicked, we get it's PhotonView. This in turn knows which player the monster belongs to, so we can use that player ID to tag.
Using the static TagPlayer method, it’s very easy to tag another player.
The GUI buttons for “Marco” and “Polo” are still both shown for any player. This can be changed easily by checking “playerWhoIsIt”, too. We show “Marco” only we are “it”. RandomMatchmaker should be modified like so:
Now we have a small prototype of a simple game. Sure, a lot is missing, but we definitely have the basics covered. Our game finds players in some random room, syncs positions, smoothes out movement and has a basic game logic on top of that. You learned to use player IDs, the “Master Client”, implement PUN Networking Messages and a lot more.
It doesn’t sound that scary anymore to hide players who are not “it”, to sync the “attack” animation with a RPC or to delay the next tagging, after someone was just tagged. Also, as we figured out how to control our character, controlling the camera in a similar way couldn’t be that hard anymore.
Unleash your monsters! :)