Photon is a networking system that’s compatible with Unity (and many other clients including Flash). Here’s some documentation I wrote for using Photon with Unity earlier this year. I’ve put it here as it may be useful to other people. Corrections are, of course, welcome!
Photon is a client-server networking system, so it is based around having a dedicated server through which all the data is sent and which can control the game. The server-side code can be controlled by what are essentially plugins, written in C# (Microsoft’s), and can be debugged with Visual Studio.
Getting started
A free limited simultaneous connections version of Photon can be downloaded from their website http://www.exitgames.com.
A separate Unity SDK is downloadable from their website as well.
Documentation
Documentation seems fairly weak. Their best introductory documentation that I’ve found is the Photon-DotNet-Client-Documentation PDF file included with the Unity SDK. They also have a CHM that covers the code, but this has nothing that’s not in the code itself as comments.
Applications
Photon is built around Applications, which are C# libraries that provide server-side logic.
It comes with an application called Lite which they suggest deriving from. It provides some basics that they expect you to want, mainly the concept of Rooms that players can join. It adds ‘properties’ (see the bottom of this page) which is data that is automatically synced between client and server and either can update. It also comes with a sample called LiteLobby that extends this to allow you to join Lobbies, then join rooms from those lobbies.
LiteLobby gives you a good idea of how to extend Lite to add extra features.
There are some other applications I’ve not looked at, such as MMO and NetSyncObjects. The later is designed for use with Unity, but is higher level than you may want. This document looks at the lower level systems
Principles
Photon has a two core concepts – Events and Operations.
Photon provides some things to make these easy(ish) to deal with.
Events
Events are sent to clients by the server. Clients can’t send events to the server.
The Lite application blurs this a bit by providing an operation (see below) called RaiseEvent that lets a client instruct the server to send an event to all other clients (but not itself). It would seem this provides convenience (as it allows you to avoid writing server-side logic) but may not actually be a Good Thing (security-wise etc).
Each event has an event code to identify it, which is a byte, along with a Hashtable that stores the data that goes with it. Events can be sent reliably or not.
Operations
Operations are essentially the reverse of an event. The client sends them to the server.
Photon considers these to be RPCs, but they’re not as automatic as you might expect RPCs to be. An Operation is a message that the client can send to the the server, and again the type of operation is represented as a byte, called the opCode.
A key difference between events and operations are that operations are able to return a result to the client.
When you trigger an operation from the client, you’re given an id that is returned to you in the result, to allow you to link up your request with the response from the server.
Code
Unity
Implementing Photon in Unity is pretty easy. Put the PhotonUntiy3D dll that came with the Photon Untiy SDK in your Assets directory somewhere, and Unity should load it and add it as a reference in the Visual Studio project.
You’ll notice the reference to LitePeer below. This is because the Unity Photon library includes support for the Lite server-side library. I believe it’s optional whether you use this. Functions like joining rooms (below) depend on this though (but of course, you could write your own system for doing this instead of relying on Lite).
You then need a class that implements IPhotonPeerListener, this is a reasonable outline I think:
using ExitGames.Client.Photon; |
public class PhotonListener : IPhotonPeerListener |
peer = new LitePeer(this); |
peer.Connect("localhost:5055", "PhotonUnityTest"); |
// this is called when an error occurs in photon |
public void DebugReturn(DebugLevel level, string message) |
// this gets disconnection messages etc |
public void PeerStatusCallback(StatusCode returnCode) |
// this gets given events when the server sends them |
public void EventAction(byte eventCode, Hashtable photonEvent) |
// this gets given the result of operations when the server returns them |
public void OperationResult(byte opCode, int returnCode, Hashtable returnValues, short invocID) |
This is the way you interact with Photon.
You should create an instance of this class from a component. They recommend attaching this component to something like the camera that’s always around, though this likely doesn’t really make sense in a real project. You should also set Application.runInBackground = true somewhere otherwise Photon will get disconnected when the application isn’t focused. You should also call the Update function of the PhotonListener class periodically, i.e. from a MonoBehaviour’s Update function.
This code should be enough to get you connected to a server. If you’re using Lite as the base, you’ll want to join a room once you connect.
You can do this by adding this to the PeerStatusCallback:
if (returnCode == StatusCode.Connect) |
peer.OpJoin("GameNameGoesHere"); |
This joins a room named ‘GameNameGoesHere’ when the Peer successfully connects to the server,
OpJoin is an operation, and results of operations are (unsurprisingly) reported to the OperationResult function. You’ll receive a call to this if the join is successful or unsuccessful. You could add something like:
public void OperationResult(byte opCode, int returnCode, Hashtable returnValues, short invocID) |
if (opCode == (byte)LiteOpCode.Join) |
If you just run the server at this point, clients should be able to connect. You can now use OpRaiseEvent to send messages to other clients, and receive events in the EventAction callback. You can use OpCustom to send arbitrary Operations to the server.
Server
Running the Photon Server
Photon comes with the Lite application, which allows clients to communicate without the server doing much more than forwarding messages between them. Lite also has other functionality that’s explored later.
You can now run the Photon server and make Unity connect to it.
- Open the deploy directory in the Photon server directory
- In the bin_Win64 directory run PhotonControl.exe
- An icon should appear in the system tray
- Right click on this and select Photon -> Start as application
- Now you can run your unity project and it should be able to connect and exchange data using OpRaiseEvent.
Server-side applications
Of course, part of the point of using a server rather than peer-to-peer is being able to write server-side code.
The server-side code is in C#, though ‘real’ Microsoft C# rather than Mono. You write what Photon calls Applications that are essentially plugins that the server runs.
Server-side there’s the Lite and LiteLobby applications that extends the basic system.
The basic principle is that we create a class library that contains a class that derives from Photon.SocketServer.Application and override it’s methods as necessary. We then place that in the server’s deploy directory and add it to it’s config so that it loads it.
Once you’ve derived from Application, you can do whatever you want.
Starting from scratch
- Create a new Visual Studio project, selecting a C# Class Library as the type. I called mine PhotonTest.
- Add the Lite project to your solution
- Right click your project and add a reference to the Lite project
- Add references to the following DLLs in the server’s lib directory: ExitGames.Logging.Log4Net, ExitGamesLibs, log4net, Photon.SocketServer and PhotonHostRuntimeInterface.
- Create a class that derives from LiteApplication. I’ve called it PhotonTestApplication:
using Photon.SocketServer; |
class PhotonTestApplication : LiteApplication |
- This should be enough to get your application running inside Photon.
Running it
We’re going to get this very simple application running on Photon. This involves setting up the directories for it and adding a reference to it to Photon’s config file.
- Open the deploy directory of the server
- Inside you’ll see a directory for each build of the server – bin_Win32, bin_Win64 etc.
- You’ll also see directories for Lite and LiteLobby (and Mmo). These are the applications that the server can run. We want our newly created application to build to here.
- Create the directories for it, following the example of how the Lite and LiteLobby directories are arranged. So, this should be PhotonTest\PhotonTest\bin. I’m not sure the double PhotonTest is required, but that what’sLite and LiteLobby have.
- In visual studio, set your project to build to this directory. Go to the project properties, select Build and then set the Output Path.
- Build the project
We need to tell Photon about our application by adding it to its config.
- In the deploy directory, open the the relevant ‘bin_’ directory for your platform and open PhotonServer.config in an editor
- Scroll down to the bottom and you’ll see an <Applications …> section. We need to modify this to point to our PhotonTest application. This should be fairly self-explanatory.
- Remove the sections for MMODemo and LiteLobby.
- Modify the Lite section changing all the Lites to PhotonTest. Make sure the Type field refers to the class you’ve made that derives from LiteApplication correctly, so if you’ve followed my names, this would be PhotonTest.PhotonTestApplication.
- Modify the Default application field to PhotonTest.
- The Applications section should look like this:
<Applications Default="PhotonTest"> |
<!-- PhotonTest Application --> |
BaseDirectory="PhotonTest\PhotonTest" |
Type="PhotonTest.PhotonTestApplication" |
ExcludeFiles="log4net.config"> |
- I’m not sure if the server automatically reloads the config when you change it, if it doesn’t, restart it using the PhotonControl system tray icon.
Watching it
Photon writes logs out to a file, and comes with a program called BareTail for reading them.
You can right click on the PhotonControl icon and select Open Logs to see them.
All the logs live in the log directory in the deploy directory.
You won’t see messages you log out from your program in the logs that PhotoControl opens by default. These are written out to a file you have to specify in the log4net.config file in your bin directory. You should open this file, then modify the File parameter under for the LogFileAppender to be something like “log/PhotonTest.log”. You can then open this log to see the output.
You can change how much is logged to this log by changing the logging levels from INFO to DEBUG at the bottom of the log4net.config file.
The way to log to this file is to use LogManager.GetCurrentClassLogger() which provides various functions such as Debug, Fatal and Info etc. You’ll need to add a using ExitGames.Logging. There are examples of this in the Lite and LiteLobby projects.
Adding some server-side logic
This is where it gets a bit complicated, and depends on whether you’re using Lite or not.
At it’s most basic, we need to add a new class that derives either directly from Photon’s IPeer or with Lite’s derived version LitePeer.
This class needs to handle incoming operations from clients. LitePeer handles a number already for us – the join operation for example, so if you derive from it, you need to work within in it’s framework.
I’m going to assume we’re using LitePeer - deriving directly from IPeer is simpler though you lose the useful features it has.
Creating the OperationRequestDispatcher
Lite has a class called OperationRequestDispatcher which handles all the operation requests each peer makes. To add our own operations, we need to derive from this and override its DispatchOperationRequest method.
- Implement a class called OperationRequestDispatcher
- Override DispatchOperationRequest
using Photon.SocketServer; |
class OperationRequestDispatcher : Lite.OperationRequestDispatcher |
public override bool DispatchOperationRequest(LitePeer peer, OperationRequest operationRequest) |
return base.DispatchOperationRequest(peer, operationRequest); |
Creating our own Peer class
We need our own peer class to handle the operations received from each peer.
- Create a new class called PhotonTestPeer which derives from LitePeer.
- Override the OnOperationRequest method. Leave this blank for now.
- Create a static instance of OperationRequestDispatcher called operationDispatcher
Your class should look something like:
using Photon.SocketServer; |
class PhotonTestPeer : LitePeer |
private static readonly OperationRequestDispatcher operationDispatcher = new OperationRequestDispatcher(); |
public PhotonTestPeer(PhotonPeer photonPeer) |
public override void OnOperationRequest(OperationRequest operationRequest) |
Using the Peer class
We’ve got the basic outline of the PhotonTestPeer class, now we need to use it to represent each client that joins the server.
- Open the PhotonTestApplication class
- Override the CreatePeer function from the base class
- Make this return an instance of our PhotonTestPeer class.
You should have:
class PhotonTestApplication : LiteApplication |
protected override IPeer CreatePeer(PhotonPeer photonPeer, InitRequest initRequest) |
return new PhotonTestPeer(photonPeer); |
Fleshing out the PhotonTestPeer class
Your OnOperationRequest function will be called every time a peer requests an operation is performed.
You could just handle the operations here, but you’ll break Lite’s built-in operations. We need to use the OperationRequestDispatcher here.
Add the following to your OnOperationRequest function:
if (!operationDispatcher.DispatchOperationRequest(this, operationRequest)) |
string message = string.Format("Unknown operation code {0}", operationRequest.OperationCode); |
this.PublishOperationResponse(new OperationResponse(operationRequest, -1, message, false)); |
This will use the OperationDispatcher instance we have to try to handle the operation. Because this is derived from Lite’s OperationDispatcher, the basic Lite operations should now work.
Notice the call to PublishOperationResponse. This method is provided by Lite and is used to return the result of an operation to the client that requested it. OperationResponses can be semi-automatically generated as we’ll see later.
Testing it
You should now be able to test your application.
- Build the project
- Photon detects when an application changes automatically and reloads it – this happens 10 seconds after you build.
- Watch the PhotonInstance(…) log file to see when it’s done it and see any errors
- Try connecting with Unity – everything should still work
The Game
This gets a bit complicated…
Lite has one area we’ve not explored – the Room class. This is basically a named group of peers that can communicate with each other. Lite handles the joining and leaving for us, and provides us with some convenient methods for getting at this group of peers. It wraps these peers in a class called Actor - an Actor is a LitePeer that’s in a Room.
Room allows operations that a peer receives to be queued up and handled by using the EnqueueOperation function.
Lite has a class called LiteGame which is a derived version of Room that uses receives operations delivered with EnqueueOperation by the OperationRqeuestDispatcher and uses them to add Actors to the Room.
LiteGame is the class we’re interested in – I’m not sure there’s a good reason to derive from Room directly.
The LiteGame class’s Actors list is protected, so we need to derive from it.
- Create a new class called PhotonTestGame
- This should derive from LiteGame
- For now, just implement a constructor that logs out that it has started so we can see if it works.
Here’s the code you should have:
class PhotonTestGame : LiteGame |
private static readonly ILogger log = LogManager.GetCurrentClassLogger(); |
public PhotonTestGame(string gameName) |
log.Debug("PhotonTestGame created (" + gameName + ")!"); |
PhotonTestGameCache
Lite has a class called RoomCacheBase. This class is used to create rooms by name – if a room of that name already exists, the existing room is returned. It also handles reference counting rooms – removing them when they’re empty.
We need to create a derived version of RoomCacheBase which will create PhotonTestGame instances. If we didn’t do this, Lite’s LiteGameCache would be used instead which would create LiteGame instances.
- Create a new class called PhotonTestGameCache.
- Implement CreateRoom
- Make this return a new PhotonTestGame instance
- Add a public static Instance variable initialized to a new instance of PhotonTestGameCache.
class PhotonTestGameCache : RoomCacheBase |
public static readonly PhotonTestGameCache Instance = new PhotonTestGameCache(); |
protected override Room CreateRoom(string roomId, params object[] args) |
return new PhotonTestGame(roomId); |
CreateRoom is called by the RoomCacheBase code when it hasn’t found a room with the matching roomId in it’s cache.
Joining these bits up
Finally, we need to join these bits up – so that we’re using the newly created PhotonTestGameCache when the player wants to join a room.
To do this, just override GetRoomReference in OperationRequestDispatcher and call GetRoomReference on the PhotonTestGameCache the Instance variable returns.
protected override RoomReference GetRoomReference(LitePeer peer, JoinOperation joinOperation) |
return PhotonTestGameCache.Instance.GetRoomReference(joinOperation.GameId); |
This works because the code for handling joining rooms etc is in the base class of our OperationRequestDispatcher. It uses this GetRoomReference function each time it needs a Room.
You should now be able to test this works. You should see “PhotonTestGame created” output in the log when the player joins the room if it does.
Events
We’ve not actually made the client and server interact yet!
Events let you tell clients to do things.
Client-side
Create a prefab in Unity and put it in your Resources directory. I’ve used a Cube called CubePrefab in this example.
Let’s handle an event in the Unity code:
public void EventAction(byte eventCode, Hashtable photonEvent) |
float x = (float)photonEvent[(byte)100]; |
float y = (float)photonEvent[(byte)101]; |
float z = (float)photonEvent[(byte)102]; |
Instantiate(Resources.Load("CubePrefab"), new Vector3(x, y, z), Quaternion.identity); |
An event is a code plus a Hashtable. The Hashtable contains three values – the x, y and z position of the cube to spawn.
Notice that each item in the hash table has a number – 100, 101 and 102. These are the IDs and are bytes (despite some things suggesting they’re shorts, this was a change that was made in a recent Photon version). You can use strings in hashtables, but Photon strongly recommend that you use bytes for performance reasons.
Photon has some predefined IDs for things, as does Lite. It seems numbers greater than 100 probably don’t have pre-existing meanings.
Server-side
Server-side, Photon has some fairly clever support for Events (and Operations) that’s missing in the client-side Unity code.
The event class
- Create a new directory in your project called Events
- In this directory, create a new class called CreateBlockEvent
using Photon.SocketServer.Rpc; |
namespace PhotonTest.Events |
public class CreateBlockEvent : LiteEventBase |
public CreateBlockEvent(float x, float y, float z) |
: base(-1) // -1 because we don't have an actor that owns this |
this.Code = 1; // the event code of the event (see 'case 1' above) |
[EventParameter(Code = 100)] |
public float X { get; set; } |
[EventParameter(Code = 101)] |
public float Y { get; set; } |
[EventParameter(Code = 102)] |
public float Z { get; set; } |
We use this class to construct events to send to the clients.
Each of the X, Y and Z are stored separately. Photon claims to support any data type, but I’ve not looked into this yet.
Note how the Code for each parameter matches the codes used client-side.
Using the event class
Now let’s hook this up server-side by creating a thread that repeatedly spawns these cubes on all the clients:
- In PhotonTestGame create a new thread with a function called CreateBlocks, and start it.
- Create a CreateBlocks functions
// create a block at a random position |
float x = (float)(random.NextDouble() * 20.0f - 10.0f); |
CreateBlockEvent createBlockEvent = new CreateBlockEvent(x, y, z); |
// send the event to all actors |
PublishEvent(createBlockEvent, Actors, Reliability.Reliable); |
This could should be fairly self-explanatory. Of interest is the use of the Actors property to specify who to send the event to. This class provides a number of useful ways to get at the actors in the room, and subsets of them. We’ll see that later.
Give it a go
That should work. Make sure your camera is near to and looking at the origin, then run it and see what happens.
Operations
Operations are the reverse of events – clients can send them to the server.
Client-side
Let’s make it so that clicking on the screen spawns a block there.
In your Update function, add:
if(Input.GetMouseButtonDown(0)) |
Hashtable hashtable = new Hashtable(); |
Vector3 mousePos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 5); // 5 = the distance away from the camera to create the cube |
Vector3 mousePosition = Camera.main.ScreenToWorldPoint(mousePos); |
Instantiate(Resources.Load("CubePrefab"), mousePosition, Quaternion.identity); |
hashtable[(byte)100] = mousePosition.x; |
hashtable[(byte)101] = mousePosition.y; |
hashtable[(byte)102] = mousePosition.z; |
peer.OpCustom((byte)150, hashtable, true); |
This looks similar to the event code. Again, we have an ID and a Hashtable.
We use peer.OpCustom to send the event. A better approach would be to override the peer class and add a method for each operation that wraps this code up.
Server-side
Server-side it’s again a bit more complicated.
The operation class
- Create a new directory in your project called Operations
- Create a class in this directory called CreateBlockOperation
As with the event, we need to create a class that matches up with what we’ve got in the Unity code:
using Photon.SocketServer.Rpc; |
using Photon.SocketServer; |
namespace PhotonTest.Operations |
class CreateBlockOperation : Operation |
public CreateBlockOperation(OperationRequest operationRequest) |
[RequestParameter(Code = 100, IsOptional = false)] |
public float X { get; set; } |
[RequestParameter(Code = 101, IsOptional = false)] |
public float Y { get; set; } |
[RequestParameter(Code = 102, IsOptional = false)] |
public float Z { get; set; } |
| As mentioned before, operations have responses to indicate if they’ve succeeded or failed or return data. These are handled through the same class as the request. You can tag parameters with ResponseParameter and/orRequestParameter. Those tagged with ResponseParameter will be sent back to the client as part of your response to their operation. |
Using the OperationRequestDispatcher
We’ve finally got a real use for the OperationRequestDispatcher!
- Open OperationRequestDispatcher
- Add the following to your DispatchOperationRequest method
switch (operationRequest.OperationCode) |
PhotonTestGame game = (PhotonTestGame)peer.State.Room; |
CreateBlockOperation request = new CreateBlockOperation(operationRequest); |
if (!peer.ValidateOperation(request)) |
game.CreateBlock(request.X, request.Y, request.Z, peer); |
In real code, you’d replace that 150 with an enum, but it’s left as a number for clarity here.
Notice that we’re checking the validity of the request – this checks if all the required data is there (i.e. everything marked as ‘IsOptional = false’ in the Operation class).
Then let’s implement the CreateBlocks function:
public void CreateBlock(float x, float y, float z, LitePeer owner) |
IEnumerable<Actor> actors = Actors.GetExcludedList(Actors.GetActorByPeer(owner)); |
CreateBlockEvent createBlockEvent = new CreateBlockEvent(x, y, z); |
PublishEvent(createBlockEvent, actors, Reliability.Reliable); |
This sends the event to all the players except one (the owner). We specify the player that triggered the operation as the owner, so the player won’t get told about the block they created (they’ve created it themselves locally).
Notice the use of GetExcludedList and GetActorByPeer.
An alternative to calling game.CreateBlock directly from the DispatchOperationRequest method is to use EnqueueOperation as the Lite code does.
- Remove the PhotonTestGame game = (PhotonTestGame)peer.State.Room; line from DispatchOperationRequest
- Replace the game.CreateBlock line of DispatchOperationRequest with this:
peer.State.Room.EnqueueOperation(peer, operationRequest); |
Now you can implement ExecuteOperation in PhotonTestGame to handle this operation there.
protected override void ExecuteOperation(LitePeer peer, OperationRequest operationRequest) |
base.ExecuteOperation(peer, operationRequest); |
switch (operationRequest.OperationCode) |
CreateBlockOperation createBlockOperation = new CreateBlockOperation(operationRequest); |
if (!createBlockOperation.IsValid) |
CreateBlock(createBlockOperation.X, createBlockOperation.Y, createBlockOperation.Z, peer); |
Whether this is preferable is unclear! |
Other things
Debugging
You can debug with Visual Studio by attaching it to the ProtonSocketServer.exe process. If you record a macro of this, you can add it to the visual studio toolbar which makes it very easy to do.
Channels
Photon supports channels so that you can prioritise operations and events separately. I’ve not covered them here, but the event and operation sending functions have overriden versions that allow you to specify a channel number.
Data is always sequenced in the same channel – so sending reliable and unreliable data in the same channel seems to be a Bad Thing. If reliable data is lost (and resent) then the unreliable data that was sent after the reliable data won’t be received until after the reliable data has arrived.
As such, if the reliable and unreliable data aren’t required to be processed in-order, then they should sent on separate channels (e.g. chat and position updates).
Lite’s Properties
Properties are added by Lite to games and peers/actors. Any peer can change any actor’s properties. Only the peer who created the room can change that room’s properties (though it’s not obvious if there’s a way to know if you created the room.)
When you join a room, you’re sent the properties of all the actors in that room.
When a property is changed, an event is raised to tell all the players (though only if the ‘Broadcast’ flag is set on the operation from the client).
This obviously means that players can have different views of the properties depending on when they joined, as they’ll have whatever was set on the actors in the room when they joined + what properties were set with the Broadcast flag set. The result of this can vary depending when you joined. Presumably you’d be ignoring properties that are set without the Broadcast flag though.
Because anyone can set properties, they seem not a good thing to use in the real world. They could be developed further to improve them though.
Retlang
Photon doesn’t really mention it, but it includes a renamed version of a library called retlang. This is a threading library that lets you pass events between objects. It’s used heavily in Lite to link up the different components.
Well…
That’s it. Assuming I’ve not made any silly mistakes, that should all work and you’ve got a great networked game working.