Just Unity Multiplayer with Photon
JUMP is a library that facilitates writing simple multiplayer games using Unity 3D and Photon Unity Networking.
I was working on a multiplayer game and found myself in trouble trying to handle events from Unity and Photon at the same time; ending up with fragile code - so I worked to streamline and simplify the scenario and build a reusable library.
Out of the box, JUMP Provides:
[0.3.0] - 2016-11-13 - Adding offline bots support!
For details, see Changelog
You can get the source code here or download JUMP from the Unity Asset Store.
JUMP works with Unity 5 and requires Photon PUN to function properly:
For more information about configuring PUN, see PUN Setup.
Note: JUMP was tested with Unity version 5.3.4 and Photon PUN Free version 1.67
The Unity Asset Store package includes the DiceRoller sample project.
DiceRollerSample/
to the Unity buildDiceRollerConnection
scene and start Unity.Note: To test multiplayer games in Unity, you will need to run two copies of the game, so that they can connect to each other. The best way, is to build a copy of the game (File/Build & Run) for one player and start Unity debugging for the second player. See this Unity forum post for more details.
In alternative to use the Asset store, you can create a folder in the unity project (under Assets) and copy all the files from the JUMP Multiplayer\
folder.
Then you can use the Object Model and the prefabs.
The scenario supported by JUMP is very simple and designed with mobile multiplayer games in mind:
JUMP automates building Photon game rooms, easy.
Note: While it supports more than two players, we tested JUMP with the two players scenario
JUMP uses Photon Random Matchmaking without the use of the Photon Lobby making the UI and the object model much simpler to use compared to the use of the Lobby.
JUMP supports up to five different Unity scenes:
Note: support for multiple levels can be added to the Playing the game scene.
JUMP cannot provide a dedicated remote server using Photon Unity Networking or Photon Cloud, so it uses the Host model, where one of the clients also hosts the Game Server (sometimes called Local Server)
Photon provides support for this with the concept of Master Client.
JUMP supports the concept of an Authoritative or Semi-Authoritative server using a Command/Snapshot Client-Server communication:
The model is similar to the one explained by Fast Paced Multiplayer, but we use the term Command instead of Actions and Snapshot instad of New State
The slanted lines indicate that there is lag between the client and server communication, see Fighting Latency on Call of Duty III
While it is possible to optimize the client/server communication in terms of reliability and space utilized with delta compression of snapshots and queues of commands, JUMP simply uses Photon Reliable UDP to communicate, it is a good balance of reliability and ease-of-use.
JUMP Multiplayer with (Semi)Authoritative Server model is based on lots of literature on multiplayer games:
JUMP Uses the following classes:
JUMPMultiplayer
JUMPGameClient
JUMPGameServer
IJUMPGameServerEngine
JUMPCommand
JUMPCommand_Snapshot
JUMPPlayer
JUMPOptions
JUMPMultiplayer
is the main class in JUMP, handling the connection to the server, the matchmaking, setting up the game room and managing the game client and server.
JUMPMultiplayer
inherits from Photon.PunBehaviour
, which in turn extends UnityEngine.MonoBehaviour
to interact both with Photon Networking and Unity Scenes.
To use JUMPMultiplayer
, you place it on a Unity Scene, set the Stage
property and handle the UnityEvents that are raised by loading other scenes and setting a different Stage or issuing commands. See DiceRoller Sample for an example.
To facilitate development, JUMP provides a series of JUMPMultiplayer Prefabs see details here
JUMPMultiplayer.Stage
JUMPMultiplayer
works in stages, one for each of the Steamlined UI flow
public enum Stages
{
Connection,
Master,
GameRoom,
Play,
OfflinePlay
}
At high level the Stages
state diagram is the following:
In more detail, based on the current stage, JUMPMultiplayer
provides the following events and operations:
Stages.Connection
In the Connection
stage, JUMP will try to connect to Photon Server, using the Photon connection settings:
When the connection is established, the OnMasterConnnect
event is raised:
public UnityEvent OnMasterConnect;
You respond to the event by loading a different Unity Scene with a JUMPMultiplayer
object set in to the Master
stage.
Note that if the connection fails, then the OnMasterConnect
event is raised, but the JUMPMultiplayer.IsOffline
property is set to true.
Stages.Master
The Master
stage is the main screen one, the user is connected to Photon, but not yet into a game room.
While connected to the Photon Master server (but not in a Room), the JUMPMultiplayer.IsConnectedToMaster
property will be set to true
.
To start the matchmaking process, you call the Matchmake
operation:
public void Matchmake()
This will make JUMP try to matchmake and connect to a Photon Game Room using Randon matchmaking.
When a game room is found then the OnGameRoomConnect
event is fired:
public UnityEvent OnGameRoomConnect;
If no room is not found, then one is created and the user is joined to it waiting for other players. The same OnGameRoomConnect
event is fired.
You want to handle this even by loading a scene that tells the users they are waiting for the other player to connect.
When the client connects to the Game Room, an instance of the JUMPGameServer
is created - this will invoke your custom Server Engine. Just implement the IJUMPGameServerEngine
interface in your class and provide the name of the class (including the namespace) to the GameServerEngineTypeName
property:
public string GameServerEngineTypeName;
If the connection fails or we lose connection to the PhotonServer, then the OnMasterDisconnect
event is fired. You want to handle this event by going back to the 'Connection' scene to try and reconnect once - if reconnection fails, you will be navigate again to the main scree with the IsOffline
property set to true - in which case, you want to tell your users that they are offline.
public UnityEvent OnMasterDisconnect;
For offline play with bots, use:
public void OfflinePlay()
This will set up offline server and client as well as creating Bots (implementing the IJUMPBots interface) and then trigger the `OnOfflinePlayConnect' event.
Stages.GameRoom
In this stage, the player is connected to a Photon Game Room and waiting for the room to be full with two players.
While connected to the Game Room, the JUMPMultiplayer.IsConnectedToGameRoom
property will be set to true
.
You can cancel the action and get out of the Game Room, by calling the CancelGameRoom
operation:
public void CancelGameRoom()
This will cancel the Game Room request and raise the OnGameRoomDisconnect
event:
public UnityEvent OnGameRoomDisconnect;
The OnGameRoomDisconnect
event is also triggered if you lose connection to Photon. You want to handle this event by going back to the Main screen (Stages.Master
).
When the second player connects, then the OnPlayConnect
event is fired:
public UnityEvent OnPlayConnect;
The OnPlayConnect
event is also fired if the room is already present and you are joining as the second player; in this case you will not have the time to cancel the game room request.
You want to handle the OnPlayConnect
event by going to the Game Play scene.
Stages.Play
This is the stage where the play happens, both players are joined to a Photon Game Room and exchanging commands and snapshots with the server to play the game. While in the Play stage, the JUMPMultiplayer.IsPlayingGame
variable is set to true; note that this is a combination of both the IsConnectedToGameRoom
and IsRoomFull
properties.
You can cancel the game and get out of the room by calling QuitPlay
:
public void QuitPlay()
This will exit the game room and trigger the OnPlayDisconnected
event:
public UnityEvent OnPlayDisconnected;
The OnPlayDisconnected
event is also fired if the other player leaves the room or if you lose connection to Photon.
You want to handle the OnPlayDisconnected
event by telling the user the reason for the disconnection (using the QuitGameReason
property) and then moving back to the Main screen (Stages.Master
).
When JUMPMulyiplayer
enters in the Play stage, then the JUMPGameClient
is initialized. At this point the client connects to the JUMPGameServer
that in turn starts sending Snapshots to the client.
The JUMPMultiplayer
will raise an OnSnapshotReceived
event every time an snapshot is sent from the server to the client.
For more information on how to handle the Snapshots, see the JUMPGameServer
section.
public JUMPSnapshotReceivedUnityEvent OnSnapshotReceived;
Stages.OfflinePlay
In this stage, the IsOfflinePlay
property is set to true.
You will receive regular events like with online play OnSnapshotReceived
and you can send command as well GameClient.SendCommand(Command)
.
You can quit calling QuitPlay
, same as in the online play scenario.
JUMPMultiplayer.PlayerID
Provides the ID of the Photon player object (or -1 if you are not connected to Photon).
The /JUMP/Multiplayer folder contains five prefabs, one for each of the Stages. The prefabs are:
The prefabs are simply a Game Object with a JUMPMultiplayer
component set to the relative Stage
. The idea is to place these in each of the five scenes that will compose the UI Flow
JUMPGameClient
uses the singleton pattern, to access it, use the JUMPMultiplayer.GameClient
property (which refers to Singleton<JUMPGameClient>.Instance
.
You use the JUMPGameClient
to send commands to the server; to do so, just use the SendCommandToServer
operation. To define your own commands, see JUMPCommand
.
JUMPMultiplayer.GameClient.SendCommandToServer(new myCommand());
The ConnectToServer
operation and OnSnapshotReceived
event are used internally by JUMPMultiplayer
, you don't need to worry about them :)
The JUMPGameServer
is managed by the JUMPMultiplayer
class, you don't interact with it directly.
JUMPMultiplayer
uses a singleton JUMPGameServer
insance to start the game, process client commands and send snapshots to the client.
The JUMPGameServer
will send a numbe of snapshots to the client per second that can be customized setting the JUMPOptions.SnapshotsPerSec
property, the default is 3 snapshots per second.
The JUMPGameServer
is designed to interact with your custom Server Engine - just implement the IJUMPGameServerEngine
interface and set the GameServerEngineTypeName
property of a JUMPMultiplayer
instance with Stage Stages.Master
(or a JUMPMultiplayerMaster prefab).
The IJUMPGameServerEngine
interface allows you to customize the Server Engine for your multiplayer game.
An instance of your Server Engine will be hosted by the Master Client, all the communication between client and server is being taken care of by JUMP, you can focus on implementing your game logic.
Here is the IJUMPGameServerEngine
interface:
public interface IJUMPGameServerEngine
{
void StartGame(List<JUMPPlayer> Players);
void Tick(double ElapsedSeconds);
void ProcessCommand(JUMPCommand command);
JUMPCommand CommandFromEvent(byte eventCode, object content);
JUMPCommand_Snapshot TakeSnapshot(int ForPlayerID);
}
void StartGame(List<JUMPPlayer> Players)
JUMP will call StartGame when the JUMPMultiplayer
is in the Master
stage and the player joins (or creates) a Room, right before calling OnGameRoomConnect
.
In this operation, you want to initialize your game state, using the information on the Players
list to save the list of players that are in the game.
For example, the DiceRoller Custom Server intializes its own GameState and saves the players using a custom DiceRollerPlayer class.
void Tick(double ElapsedSeconds)
On the Master Server, JUMPGameServer
calls Tick
every frame update to make your game progress forward.
Do anything time related in this operation; don't bother sending Snapshots, this is automated for you with the TakeSnapshot
operation.
For example, the DiceRoller Custom Server counts down its 30 seconds timeout for the game, after that the game is over.
void ProcessCommand(JUMPCommand command)
This is where you process your custom commands that the client sends.
See JUMPCommand for how to define your own commands and the DiceRoller Custom Server for an example of definition and use of a custom command.
JUMPCommand CommandFromEvent(byte eventCode, object content)
JUMPGameServer
needs a way to find out if the Photon Event that it just received from the client comes from your Game Client and carries your custom command, in order to do so, you can implement the CommandFromEvent
function, checking if the eventCode
is one of your custom operations' one.
See, the DiceRoller Sample for an example on how to write this function.
JUMPCommand_Snapshot TakeSnapshot(int ForPlayerID)
JUMPGameServer
will send Snapshots automatically to your clients; the TakeSnapshot
function is where you can customize the Snapshot.
See, the DiceRoller Custom Server for an example on how to write this function.
JUMPCommand
is a base class that allows JUMP to define commands and to send them and receive them using the Photon Events system. You can define your own custom commands with little coding.
JUMPCommand
uses the CommandEventCode
property as Photon Event Code, this is a byte variable, in your game you can use any value from 0
to 189
.
The CommandData
is an object array used to store and retrieve the data for your command, and that can be easily serialized with Photon messages. Only basic types are allowed as Command properties to store in CommandData
, for more background information, see Serialization In Photon
Custom JUMPCommand
s typically need two constructors, one used for reconstruction of the Command when received from Photon, and one used to initialize the CommandData before sending it to the server.
JUMPMultiplayer
uses JUMPCommand_Connect
as a custom command used to connect the GameClient
:
public class JUMPCommand_Connect : JUMPCommand
{
public const byte JUMPCommand_Connect_EventCode = 191;
public int PlayerID { get { return (int)CommandData[0]; } set { CommandData[0] = value; } }
public JUMPCommand_Connect(int playerID) : base(new object[1], JUMPCommand_Connect_EventCode)
{
PlayerID = playerID;
}
public JUMPCommand_Connect(object[] data) : base(data, JUMPCommand_Connect_EventCode)
{
}
}
See, the [DiceRoller Custom Server] for other examples on wiritng your own custom Commands.
Then the JUMPGameServer
sends JUMPCommand_Snapshot
periodically to the client.
JUMPCommand_Snapshot
is a JUMPCommand
that has two property already set: the JUMPSnapshot_EventCode
and the ForPlayerID
property.
You can define your own Snapshot, by inheriting from the JUMPCommand_Snapshot
class -
See, the [DiceRoller Custom Server] for other examples on wiritng your own custom Snapshots.
JUMPPlayer
is a simple class used to store basic information like PlayerID
and IsConnected
.
You can extend the JUMPPlayer
with your own properties, like Score for example and keep them in your server state.
See, the [DiceRoller Custom Server] for an example on extendig the JUMPPlayer
class.
You can set a few options with the JUMPOptions
class - here are the options with their defaults:
public static class JUMPOptions
{
public static string GameVersion = "0.1";
public static byte NumPlayers = 2;
public static int DisconnectTimeout = 10 * 1000;
public static int SnapshotsPerSec = 3;
}
Note that DisconnectTimeout
is set to 60 seconds if the build is in Debug mode.
Offline Bots ara available in the Offline Play state.
To create a Bot, you need to have a class that implements the IJUMPBot
interface.
Note, that a bot is also a player, so the IJUMPBot
implements also the 'IJUMPPlayer' interface.
JUMP will create automatically as many bots as the number of players in the game (JUMPOptions.NumPlayers
) minus one for your human player.
Note: a mix of online players and bots is not supported, you can only have one human player and the rest bots.
To start the offline play with bots, set the BotTypeName
property with the class name of the bot you want to use and then call the OfflinePlay
method, when you are in the Master stage.
This will create a list of Bots and trigger the OnOfflinePlayConnect
event.
At this point you want to load a scene with a JUMPMultiplayer object in OfflinePlay stage.
The client will receive snapshot and you can send commands, like in the online Play
stage, but in this case, your bots will be invoked and will be sending commands to the server.
You can very easily have a single play scene that handles online and offline play, all you need to do is have a JUMPMultiplayerPlay
and a JUMPMultiplayerOfflinePlay
prefabs (disabled) in the play scene, then in the Activate
function, check the JUMPMultiplayer.IsOfflinePlay property and just set active the relevant one - see the DiceRoller sample for an example.
To facilitate writing engaging Bots, they have full access to the state of the game. Bots don't use the snapshot, but get a pointer to your Engine, where you usually store your game state, and a Tick from the server:
public interface IJUMPBot : IJUMPPlayer
{
void Tick(double ElapsedSeconds);
IJUMPGameServerEngine Engine { get; set; }
}
In the Tick event, you want to access the state on your Engine and have your bots behave accordingly.
In the DiceRoller sample, for example, the Bot simply rolls a 3 every three seconds. Note that you can access (DiceRollerEngine) Engine
and have full access to your state, for example you want your bot to try and catch up with the player and have a higher chance to roll a high number if the bot is behind.
DiceRoller is a simple example of how to use JUMP. It is made of five scenes, a custom Server Engine and a Game Manager for the play scene.
The Connection Scene has one instance of the JUMPMultiplayerConnection prefab (as a reminder, this is a behaviour that has a JUMPMultplayer
component, with the Stage
set as Connection
).
The only event handled by the scene the OnMasterDisconnect
, in which we load the Master Scene.
The Scene also uses the UI Prefab JUMPStatusConnection that displays the status of the connection with Photon.
The Master Scene has has one instance of the JUMPMultiplayerMaster prefab and can handle online and offline play with bots.
The scene handles the OnMasterDisconnect
event, that goes back to the Connection Scene to try and reconnect once.
The scene sets the GameServerEngineTypeName
variable to "DiceRollerSample.DiceRollerEngine"
to create a custom server engine - for details see the [DiceRoller Custom Server].
The scene sets the BotTypeName
variable to "DiceRollerSample.DiceRollerBot"
to create a custom bot - for details see the [DiceRoller Bot].
For the online play:
Matchmake
operation on the JUMPMultiplayerMaster* prefab.OnGameRoomConnect
and loads the Game Room SceneFor the offline play:
OfflinePlay
operation on the JUMPMultiplayerMaster* prefab.OnOfflinePlayConnect
and loads the Play SceneThe scene uses a few more UI prefabs:
The Game Room Scene has one instance of the JUMPMultiplayerGameRoom prefab.
The scene handles two events:
OnGameRoomDisconnect
that goes back to the Master Scene.OnPlayConnect
that loads the Play SceneThe scene uses a few more UI prefabs:
CancelGameRoom
operation on the JUMPMultiplayerGameRoom prefab.The Game Room Scene has two instance of the JUMPMultiplayerPlay prefab:
JUMPMultiplayerPlay
is the one that handles the online play JUMPMultiplayerOfflinePlay
is the one that handles the offline play:The play scene manager (DiceRollerGameManager
) activates one of the prefabs, based on the OfflinePlayMode:
void Start()
{
...
OfflinePlayManager.gameObject.SetActive(JUMPMultiplayer.IsOfflinePlayMode);
OnlinePlayManager.gameObject.SetActive(!JUMPMultiplayer.IsOfflinePlayMode);
}
For both online and offlinePlay, the scene handles the same two events (fired by the active JUMPMultiplayer prefab):
OnPlayDisconnect
that goes back to the Master Scene.OnSnapshotReceived
that handles the Snapshots form the server via the DiceRollerGameManager - see [DiceRoller Custom Server] for detailsThe scene uses a few more UI prefabs:
QuitPlay
operation on the JUMPMultiplayerPlay prefab.DiceRoller is a simple game, two players roll a dice and try to score the most points in 30 seconds. To function, DiceRoller needs the Scenes and two additional components:
Note: for simplicity of implementation, the action of rolling a dice is non-authoritative: the clients decide the outcome of rolling the dice autonomously; this helps keeping the DiceRoller sample code very straightforward and simple to understand. In your game you might want to have the server take the decisions on the outcome of player actions like this.
To create a custom server, DiceRoller implements the IJUMPGameServerEngine
interface:
DiceRoller defines a custom command for rolling the dice, this inherits from JUMPCommand
and extends it:
public class DiceRollerCommand_RollDice : JUMPCommand
{
public const byte RollDice_EventCode = 100;
public int PlayerID { get { return (int)CommandData[0]; } set { CommandData[0] = value; } }
public int RolledDiceValue { get { return (int)CommandData[1]; } set { CommandData[1] = value; } }
// Create a command to send with this initializer
public DiceRollerCommand_RollDice(int playerID, int rolledDiceValue) : base(new object[2], RollDice_EventCode)
{
PlayerID = playerID;
RolledDiceValue = rolledDiceValue;
}
// Create a command when receiving it from Photon
public DiceRollerCommand_RollDice(object[] data) : base(data, RollDice_EventCode)
{
}
}
Note the definition of the event code: RollDice_EventCode
, the use of the standard PlayerID
property as the first value in the CommanData array and the definition of two constructors: DiceRollerCommand_RollDice(object[] data)
used to create the command from the Photon message and DiceRollerCommand_RollDice(int playerID, int rolledDiceValue)
used to send the message.
DiceRoller defines a custom Player to store the game state in DiceRollerGameState
.
public class DiceRollerPlayer : JUMPPlayer
{
public int Score = 0;
}
Note the inheritance from JUMPPlayer
and the addition of the only property that matters in this case, the Score.
DiceRoller custom server defines theits own game state of course - this contains a list of DiceRollerPlayers
, the time remaining in the game, the stage of the game (can be waiting for the players, playing or complete) and the time remaining when the game is being played:
public enum DiceRollerGameStages
{
WaitingForPlayers,
Playing,
Complete
}
public class DiceRollerGameState
{
public Dictionary<int, DiceRollerPlayer> Players = new Dictionary<int, DiceRollerPlayer>();
public float SecondsRemaining;
public DiceRollerGameStages Stage;
public int WinnerPlayerID;
}
To communicate the state of the game with the clients, the DiceRoller custom server must define a Snapshot.
The DiceRoller_Snapshot
class inherits from JUMPCommand_Snapshot
and so can make use of the array of data CommandData
, the JUMPSnapshot_EventCode
and the ForPlayerID
properties already defined in the base class.
All it needs to do is to define two constructors and use the CommandData
array to store the Snapshot data.
Note that a Snapshot is different from the GameState because it is aimed to only one of the two players: players should not see each other's data (for example if they have playing cards).
public class DiceRoller_Snapshot : JUMPCommand_Snapshot
{
// ForPlayerID is at CommandData[0]
public int MyScore { get { return (int)CommandData[1]; } set { CommandData[1] = value; } }
public int OpponentScore { get { return (int)CommandData[2]; } set { CommandData[2] = value; } }
public float SecondsRemaining { get { return (float)CommandData[3]; } set { CommandData[3] = value; } }
public DiceRollerGameStages Stage { get { return (DiceRollerGameStages)CommandData[4]; } set { CommandData[4] = value; } }
public int WinnerPlayerID { get { return (int)CommandData[5]; } set { CommandData[5] = value; } }
// Create a command to send with this initializer
public DiceRoller_Snapshot() : base(new object[6])
{
}
// Create a command when receiving it from Photon
public DiceRoller_Snapshot(object[] data) : base(data)
{
}
}
Note how the DiceRoller_Snapshot()
constructor creates a new array with 6 elements: one for the ForPlayerID
property used by the JUMPCommand_Snapshot
base class and five for the custom properties.
Also note how the properties are stored from the second element in the array on, because ForPlayerID
is stored at element 0.
The DiceRollerEngine
class implements the IJUMPGameServerEngine
interface.
It uses an internal variable to hold the state:
private DiceRollerGameState GameState;
The constructor simply initializes the state in the waiting for players mode:
public DiceRollerEngine()
{
GameState = new DiceRollerGameState();
GameState.Stage = DiceRollerGameStages.WaitingForPlayers;
}
The CommandFromEvent
function handles the RollDice custom command:
public JUMPCommand CommandFromEvent(byte eventCode, object content)
{
if (eventCode == DiceRollerCommand_RollDice.RollDice_EventCode)
{
return new DiceRollerCommand_RollDice((object[]) content);
}
return null;
}
The StartGame
operation gets the information about the players and sets the state of the game:
public void StartGame(List<JUMPPlayer> Players)
{
GameState = new DiceRollerGameState();
GameState.SecondsRemaining = 30;
GameState.Stage = DiceRollerGameStages.Playing;
foreach (var pl in Players)
{
DiceRollerPlayer player = new DiceRollerPlayer();
player.PlayerID = pl.PlayerID;
player.IsConnected = pl.IsConnected;
player.Score = 0;
GameState.Players.Add(player.PlayerID, player);
}
}
The ProcessCommand
handles the server state when a client sends a RollDice command (the connect commands are manager automatically by the JUMPGameServer
class).
public void ProcessCommand(JUMPCommand command)
{
if (command.CommandEventCode == DiceRollerCommand_RollDice.RollDice_EventCode)
{
DiceRollerCommand_RollDice rollDiceCommand = command as DiceRollerCommand_RollDice;
DiceRollerPlayer player;
if (GameState.Stage == DiceRollerGameStages.Playing)
{
if (GameState.Players.TryGetValue(rollDiceCommand.PlayerID, out player))
{
player.Score += rollDiceCommand.RolledDiceValue;
}
}
}
}
The Tick
operation handles the game clock, when the clock expires, the game state is changed to Coplete and a winner is determined.
public void Tick(double ElapsedSeconds)
{
if (GameState.Stage == DiceRollerGameStages.Playing)
{
GameState.SecondsRemaining -= (float) ElapsedSeconds;
if (GameState.SecondsRemaining <= 0)
{
int maxscore = 0;
int winner = -1;
foreach (var item in GameState.Players)
{
if (item.Value.Score > maxscore)
{
maxscore = item.Value.Score;
winner = item.Key;
}
}
GameState.Stage = DiceRollerGameStages.Complete;
GameState.WinnerPlayerID = winner;
}
}
}
The TakeSnapshot
operation is called by JUMPGameServer
to send the snapshot to a player, so the snapshot is created for that specific player:
public JUMPCommand_Snapshot TakeSnapshot(int FofrPlayerID)
{
DiceRoller_Snapshot snap = new DiceRoller_Snapshot();
snap.ForPlayerID = ForPlayerID;
snap.MyScore = 0;
snap.OpponentScore = 0;
foreach (var item in GameState.Players)
{
if (item.Value.PlayerID == ForPlayerID)
{
snap.MyScore = item.Value.Score;
}
else
{
snap.OpponentScore = item.Value.Score;
}
}
snap.SecondsRemaining = GameState.SecondsRemaining;
snap.Stage = GameState.Stage;
snap.WinnerPlayerID = GameState.WinnerPlayerID;
return (JUMPCommand_Snapshot) snap;
}
The final piece of DiceRoller is the DiceRollerGameManager
, this is the Controller part in the MVC pattern: displaying the information in the user interface (View) and working with the Snapshot
(Model) received by the JUMPMultiplayer
class (see Play Scene).
'DiceRollerGameManager' is a 'MonoBehaviour' and uses multiple Text
and Button
controls to display and control the game:
public Text MyScore;
public Text TheirScore;
public Text GameStatus;
public Text TimeLeft;
public Text Result;
public Button RollDice;
When a snapshot is received, the controls are updated:
DiceRollerGameStages UIStage = DiceRollerGameStages.WaitingForPlayers;
public void OnSnapshotReceived(JUMPCommand_Snapshot data)
{
DiceRoller_Snapshot snap = new DiceRoller_Snapshot(data.CommandData);
GameStatus.text = snap.Stage.ToString();
MyScore.text = snap.MyScore.ToString();
TheirScore.text = snap.OpponentScore.ToString();
TimeLeft.text = snap.SecondsRemaining.ToString("0.");
UIStage = snap.Stage;
if (UIStage == DiceRollerGameStages.Complete)
{
Result.text = (snap.MyScore > snap.OpponentScore) ? "You Won :)" : ((snap.MyScore == snap.OpponentScore) ? "Tied!" : "You Lost :(");
}
}
The user can roll a dice, this operation will send the custom command to the server using the JUMPGameClient
singleton:
public void RollADice()
{
int score = UnityEngine.Random.Range(1, 6);
if (RollDice != null)
{
RollDice.GetComponent<Text>().text = "Rolled a " + score + " \nroll again..";
}
Singleton<JUMPGameClient>.Instance.SendCommandToServer(new DiceRollerCommand_RollDice(JUMPMultiplayer.PlayerID, score));
}
Finally, the Unity Start
function is used to initialize the random seed and in the Update
function we enable the RollDice button only if we are playing:
// Use this for initialization
void Start () {
UnityEngine.Random.seed = System.DateTime.Now.Millisecond;
}
// Update is called once per frame
void Update () {
RollDice.interactable = (UIStage == DiceRollerGameStages.Playing);
}
The DiceRollerBot
is very simple, just rolls a 3 every three seconds!
You can develop deeper strategies having full access to the DiceRollerEngine
and the full DiceRollerGameState
(the Bot can distinguish his score from his opponent using the Bot's PlayerID
, set bv the engine.
Here is the full bot implementation:
public class DiceRollerBot : JUMPPlayer, IJUMPBot
{
public int Score = 0;
private TimeSpan TickTimer = TimeSpan.Zero;
private TimeSpan CommandsFrequency = TimeSpan.FromMilliseconds(1000 / 0.3);
public IJUMPGameServerEngine Engine { get; set; }
public void Tick(double ElapsedSeconds)
{
TickTimer += TimeSpan.FromSeconds(ElapsedSeconds);
if (TickTimer > CommandsFrequency)
{
TickTimer = TimeSpan.Zero;
// Every 3 seconds, roll a 3
DiceRollerEngine engine = Engine as DiceRollerEngine;
engine.ProcessCommand(new DiceRollerCommand_RollDice(PlayerID, 3));
}
}
}
Given the fact that Unity does not allow the same project to be opened in two different instances of the Unity Editor and that it can only run one scene at a time, it is very hard to create test automation and to test JUMP. We have done manual testing, but finding a way to automate some of this test would be ideal. We thought about mocking, but then it would require to mock the behaviour of Photon, making assumptions that might not be matched in the real case and invalidating the tests.
So we worked with manual tests so far, here are the test cases we tried:
Unless stated otherwise all works are Copyright © 2016 Juiced Team LLC.
And licensed under the MIT License
If you want to donate, you can simply purchase the JUMP package in the Unity Asset Store.
We are not ready to accept pull requests at the time, we are considering that for the future. If you are interested in contributing, file a bug and we will consider your request. All contributions will be voluntary and will grant no rights, compensation or license, you will retain the rights to reuse your code.
Photon, Photon Engine, PUN: ©2016 Exit Games®
Unity 3D, Unity Engine: ©2016 Unity Technologies