Skip to main content

Real-time Multiplayer with Photon

info

Click on the floor to move.

The complete project including matchmaking can be found here.

Photon (also known as PUN) is used in many games and has a JavaScript SDK available for HTML5 games.

Photon is for free for projects with up to 20 online players (CCU).

You will learn​

  • How to add Photon SDK to PlayCanvas
  • Multiplayer implementation with Photon

Setup​

PlayCanvas Project​

We start by forking the tutorial project here.

Empty Project

Photon account​

Account registration is required to use the SDK and view documentation.

Create your Photon account here - (Photon Engine).

Create a new app​

Click CREATE NEW APP from the dashboard

Create New Application

Select Photon Type and Application name​

Enter the following

  • Photon Type: RealTime
  • Name: PlayCanvas-Photon etc.

Create Real Time Project

Copy of AppID​

Please make a note of this AppId, as you will need it in the future.

App Id

Download SDK​

Download the SDK from the dashboard.

Click SDK from the dashboard​

SDK

Select RealTime JavaScript​

JavaScript SDK

Click Download SDK​

Download SDK

Unzip the SDK​

Unzip SDK

The SDK will be downloaded in ZIP format, unzip it: photon-javascript-sdk_vX-X-X-X → lib → Photon-Javascript_SDK.min.js.

Importing SDK​

Import the SDK you have just downloaded into the PlayCanvas editor.

Upload the SDK on the editor​

Upload SDK

Drag and drop the SDK to the assets in the editor.

Change Loading Type "Asset" to "Before Engine"​

Change Loading Type

Multiplayer implementation​

The multiplayer implementation will do the following:

  1. Use Photon class for real-time communication and Load Balancing
  2. Connect to Photon master server
  3. Create or Join a room
  4. Synchronize other players' actions and movement

The API reference and glossary are available on Photon's site.

Using Photon with PlayCanvas​

Instantiate classes from PlayCanvas to use Photon​

Create a script asset named photon-loadbalancing-playcanvas.js to the project to initialize Photon.

const PhotonLoadBalancingPlayCanvas = pc.createScript("photonLoadBalancingPlayCanvas");
PhotonLoadBalancingPlayCanvas.attributes.add("appId", { type: "string" });
PhotonLoadBalancingPlayCanvas.attributes.add("appVersion", { type: "string", default: "1.0" });
PhotonLoadBalancingPlayCanvas.attributes.add("wss", { type: "boolean", default: true });
PhotonLoadBalancingPlayCanvas.attributes.add("region", {
type: "string", default: "jp",
description: "Photon Cloud has servers in several regions, distributed across multiple hosting centers over the world.You can choose optimized region for you.",
enum: [
{ "Select Region": "default" },
{ "Asia, Singapore": "asia" },
{ "Australia, Melbourne": "au" },
{ "Canada, East Montreal": "cae" },
{ "Chinese Mainland (See Instructions) Shanghai": "cn" },
{ "Europe, Amsterdam": "eu" },
{ "India, Chennai": "in" },
{ "Japan, Tokyo": "jp" },
{ "Russia Moscow": "ru" },
{ "Russia, East Khabarovsk": "rue" },
{ "South Africa Johannesburg": "za" },
{ "South America, Sao Paulo": "sa" },
{ "South Korea, Seoul": "kr" },
{ "Turkey Istanbul": "tr" },
{ "USA, East Washington": "us" },
{ "USA, West San José": "usw" },
],
});

PhotonLoadBalancingPlayCanvas.prototype.initialize = function () {

// Photon Settings
this.loadBalancingClient = new Photon.LoadBalancing.LoadBalancingClient(this.wss ? 1 : 0, this.appId, this.appVersion);

// pc.Application
this.loadBalancingClient.app = this.app;
};
  • Photon.LoadBalancing.LoadBalancingClient This class contains many of the features of the Photon SDK for real-time communication.

Set Script for Root entity​

Create a new script asset photon-loadbalancing-playcanvas.js and attach it to the Root entity in the Editor.

Root Entity - Inspector

Paste AppId into the script attribute​

Enter AppId as a script attribute.

Script Attributes

this.loadBalancingClient = new Photon.LoadBalancing.LoadBalancingClient( this.wss ? 1 : 0, this.appId, this.appVersion );
  • wss Secure connection via WebSocket.
  • appId The application identifier value.
  • appVersion Used for versioning. Different versions cannot be connected to each other.

Connect to the Photon master server​

Connect to the master server using connectToRegionMaster​

PhotonLoadBalancingPlayCanvas.prototype.initialize = function () {

// Photon Settings
this.loadBalancingClient = new Photon.LoadBalancing.LoadBalancingClient(this.wss ? 1 : 0, this.appId, this.appVersion);

// pc.Application
this.loadBalancingClient.app = this.app;

// Connect to the master server
if (!this.loadBalancingClient.isInLobby()) {
this.loadBalancingClient.connectToRegionMaster(this.region);
}
};
  • connectToRegionMaster Connects to the master server in the specified region.
  • this.region Used to configure the region.

If you successfully connect to the lobby by running connectToRegionMaster, JoinedLobby will be displayed in the log.

Console Log

Create or Join a room​

onRoomList function is called when a connection is made to the lobby.

JoinRandomOrCreateRoom to join a room if it exists, or randomly join a room if it does not exist.

PhotonLoadBalancingPlayCanvas.prototype.initialize = function () {

// Photon Settings
this.loadBalancingClient = new Photon.LoadBalancing.LoadBalancingClient(this.wss ? 1 : 0, this.appId, this.appVersion);

// pc.Application
this.loadBalancingClient.app = this.app;

// Connect to the master server
if (!this.loadBalancingClient.isInLobby()) {
this.loadBalancingClient.connectToRegionMaster(this.region);
}

// Added
this.loadBalancingClient.onRoomList = this.onRoomList;
this.loadBalancingClient.onJoinRoom = this.onJoinRoom;
};

PhotonLoadBalancingPlayCanvas.prototype.onRoomList = function () {
this.joinRandomOrCreateRoom();
};

PhotonLoadBalancingPlayCanvas.prototype.onJoinRoom = function (createdByMe) {
console.log("Joined the room.");
};
  • onRoomList(rooms) List of rooms in the lobby.
  • joinRandomOrCreateRoom(options, createRoomName, createOptions) Join to a random room. If the room does not exist, a new room will be created.
  • onJoinRoom When you join a room, this is called.

Join and Leave​

When a player joins a room, it is synchronized with other players. Use onActorJoin and onActorLeave.

PhotonLoadBalancingPlayCanvas.prototype.initialize = function () {

// Photon Settings
this.loadBalancingClient = new Photon.LoadBalancing.LoadBalancingClient(this.wss ? 1 : 0, this.appId, this.appVersion);

// pc.Application
this.loadBalancingClient.app = this.app;

// Connect to the master server
if (!this.loadBalancingClient.isInLobby()) {
this.loadBalancingClient.connectToRegionMaster(this.region);
}

this.loadBalancingClient.onRoomList = this.onRoomList;
this.loadBalancingClient.onJoinRoom = this.onJoinRoom;

// Added
this.loadBalancingClient.onActorJoin = this.onActorJoin;
this.loadBalancingClient.onActorLeave = this.onActorLeave;
};

PhotonLoadBalancingPlayCanvas.prototype.onRoomList = function () {
this.joinRandomOrCreateRoom();
};

PhotonLoadBalancingPlayCanvas.prototype.onJoinRoom = function (createdByMe) {
console.log("Joined the room.");
};

PhotonLoadBalancingPlayCanvas.prototype.onActorJoin = function (actor) {
const { actorNr } = actor;
if (actor.isLocal) return;
const otherPlayer = new pc.Entity();
otherPlayer.addComponent("render", { type: "capsule" });
otherPlayer.setLocalPosition(0, 1, 0);
otherPlayer.name = actorNr;
this.app.root.children[0].addChild(otherPlayer);
};

PhotonLoadBalancingPlayCanvas.prototype.onActorLeave = function (actor) {
const { actorNr } = actor;
const otherPlayer = this.app.root.children[0].findByName(actorNr);
if (actor.isLocal || !otherPlayer) return;
otherPlayer.destroy();
};

Actor

  • actor contains name, actorNr, isLocal, and userId.
  • onActorJoin when a new user connects, you can get the joined actor.
  • onActorLeave when a user is disconnected, you can get the disconnected actor.

If successful, the entity is added when the player joins.

Console log - Actors

Player Movement​

Create a new player.js for character movement.

const Player = pc.createScript("player");

Player.prototype.update = function (dt) {
const pos = new pc.Vec3(0, 0, 0);
if (this.app.keyboard.isPressed(pc.KEY_LEFT)) {
pos.x = -dt;
}
if (this.app.keyboard.isPressed(pc.KEY_RIGHT)) {
pos.x = dt;
}
if (this.app.keyboard.isPressed(pc.KEY_UP)) {
pos.z = -dt;
}
if (this.app.keyboard.isPressed(pc.KEY_DOWN)) {
pos.z = dt;
}
if (!pos.equals(new pc.Vec3(0, 0, 0))) {
this.entity.translate(pos);
}
};
  • this.app.keyboard.isPressed: check if the keyboard is pressed

Synchronize other players​

Use raiseEvent and onEvent to synchronize the player's location.

Position synchronization using raiseEvent​

const PhotonLoadBalancingPlayCanvas = pc.createScript("photonLoadBalancingPlayCanvas");
PhotonLoadBalancingPlayCanvas.attributes.add("appId", { type: "string" });
PhotonLoadBalancingPlayCanvas.attributes.add("appVersion", {
type: "string",
default: "1.0",
});
PhotonLoadBalancingPlayCanvas.attributes.add("wss", {
type: "boolean",
default: true,
});
PhotonLoadBalancingPlayCanvas.attributes.add("region", {
type: "string",
default: "jp",
description:
"Photon Cloud has servers in several regions, distributed across multiple hosting centers over the world.You can choose optimized region for you.",
enum: [
{ "Select Region": "default" },
{ "Asia, Singapore": "asia" },
{ "Australia, Melbourne": "au" },
{ "Canada, East Montreal": "cae" },
{ "Chinese Mainland (See Instructions) Shanghai": "cn" },
{ "Europe, Amsterdam": "eu" },
{ "India, Chennai": "in" },
{ "Japan, Tokyo": "jp" },
{ "Russia Moscow": "ru" },
{ "Russia, East Khabarovsk": "rue" },
{ "South Africa Johannesburg": "za" },
{ "South America, Sao Paulo": "sa" },
{ "South Korea, Seoul": "kr" },
{ "Turkey Istanbul": "tr" },
{ "USA, East Washington": "us" },
{ "USA, West San José": "usw" },
],
});

PhotonLoadBalancingPlayCanvas.prototype.initialize = function () {

// Photon Settings
this.loadBalancingClient = new Photon.LoadBalancing.LoadBalancingClient(this.wss ? 1 : 0, this.appId, this.appVersion);

// pc.Application
this.loadBalancingClient.app = this.app;

// Connect to the master server
if (!this.loadBalancingClient.isInLobby()) {
this.loadBalancingClient.connectToRegionMaster(this.region);
}
this.loadBalancingClient.onRoomList = this.onRoomList;
this.loadBalancingClient.onJoinRoom = this.onJoinRoom;
this.loadBalancingClient.onActorJoin = this.onActorJoin;
this.loadBalancingClient.onActorLeave = this.onActorLeave;

// Added
this.loadBalancingClient.onEvent = this.onEvent;
this.app.on("createOtherPlayerEntity", this.createOtherPlayerEntity, this);
this.app.on("loadbalancing:sendPlayerPosition", this.sendPlayerPosition, this);
};

PhotonLoadBalancingPlayCanvas.prototype.onRoomList = function () {
this.joinRandomOrCreateRoom();
};

PhotonLoadBalancingPlayCanvas.prototype.onJoinRoom = function (createdByMe) {
this.myRoomActorsArray().forEach((actor) => {
if (actor.isLocal) return;
this.app.fire("createOtherPlayerEntity", actor);
});
};

PhotonLoadBalancingPlayCanvas.prototype.onActorJoin = function (actor) {
if (actor.isLocal) return;
this.app.fire("createOtherPlayerEntity", actor);
const { x, y, z } = this.app.root.findByName("Player").getLocalPosition();
this.app.fire("loadbalancing:sendPlayerPosition", { x, y, z });
};

PhotonLoadBalancingPlayCanvas.prototype.onActorLeave = function (actor) {
const { actorNr } = actor;
const otherPlayer = this.app.root.findByName(actorNr);
if (actor.isLocal || !otherPlayer) return;
otherPlayer.destroy();
};

PhotonLoadBalancingPlayCanvas.prototype.createOtherPlayerEntity = function (actor) {
const { actorNr } = actor;
const entity = new pc.Entity();
entity.addComponent("render", { type: "capsule" });
entity.setLocalPosition(0, 1, 0);
entity.name = actorNr;
this.app.root.children[0].addChild(entity);
};

PhotonLoadBalancingPlayCanvas.prototype.sendPlayerPosition = function (position) {
this.loadBalancingClient.raiseEvent(1, { position });
};

PhotonLoadBalancingPlayCanvas.prototype.onEvent = function (code, content, actorNr) {
switch (code) {
case 1: {
const otherPlayer = this.app.root.findByName(actorNr);
if (otherPlayer) {
const { x, y, z } = content.position;
otherPlayer.setLocalPosition(x, y, z);
}
break;
}
default:
}
};
  • raiseEvent(eventCode,data, options) send eventCode and data.
  • onEvent(code, content, actorNr) receive data. Includes actorNr and eventCode.

Changed to fire events when player moves​

const Player = pc.createScript("player");

Player.prototype.update = function (dt) {
const pos = new pc.Vec3(0, 0, 0);
if (this.app.keyboard.isPressed(pc.KEY_LEFT)) {
pos.x = -dt;
}
if (this.app.keyboard.isPressed(pc.KEY_RIGHT)) {
pos.x = dt;
}
if (this.app.keyboard.isPressed(pc.KEY_UP)) {
pos.z = -dt;
}
if (this.app.keyboard.isPressed(pc.KEY_DOWN)) {
pos.z = dt;
}
if (!pos.equals(new pc.Vec3(0, 0, 0))) {
this.entity.translate(pos);

// Added
const { x, y, z } = this.entity.getPosition();
this.app.fire("loadbalancing:sendPlayerPosition", { x, y, z });
}
};

Done​

You can now play multiplayer in Photon!

Project

You can create a room using Photon and synchronize the positions of players with each other.

Although this project was only a simple real-time communication between players, you can also create a project that includes matchmaking. For the full project, including room creation and room listings, please click here .