跳到主要内容

使用 Photon 实现实时多人联机

信息

点击地面移动。

包含匹配功能的完整项目见此处。

许多游戏使用 Photon,其生态中也有 PUN 等方案,并提供适用于 HTML5 游戏的 JavaScript SDK。

原教程介绍的免费额度为最多 20 名同时在线玩家(CCU)。

学习内容​

  • 如何向 PlayCanvas 添加 Photon SDK。
  • 使用 Photon 实现多人联机。

配置​

PlayCanvas 项目​

首先派生教程项目。

空项目

Photon 账户​

使用 SDK 和查看文档需要注册账户。

在 Photon Engine 网站创建账户。

创建新应用​

在控制台点击 CREATE NEW APP(创建新应用)。

创建新应用

选择 Photon 类型与应用名称​

填写以下内容:

  • Photon Type:RealTime。
  • Name:例如 PlayCanvas-Photon。

创建 RealTime 项目

复制 AppID​

记下 AppId,稍后将会用到。

应用 ID

下载 SDK​

从控制台下载 SDK。

在控制台点击 SDK​

SDK

选择 RealTime JavaScript​

JavaScript SDK

点击 Download SDK​

下载 SDK

解压 SDK​

解压 SDK

SDK 为 ZIP 文件,解压后打开 photon-javascript-sdk_vX-X-X-X → lib → Photon-Javascript_SDK.min.js。

导入 SDK​

将刚下载的 SDK 导入 PlayCanvas 编辑器。

在编辑器中上传 SDK​

上传 SDK

将 SDK 拖到编辑器的资源面板。

将加载类型从 Asset 改为 Before Engine​

修改加载类型

实现多人联机​

实现包括以下步骤:

  1. 使用 Photon 类进行实时通信与负载均衡。
  2. 连接 Photon 主服务器。
  3. 创建或加入房间。
  4. 同步其他玩家的操作与移动。

Photon 网站提供 API 参考与术语表。

在 PlayCanvas 中使用 Photon​

在 PlayCanvas 中实例化 Photon 类​

创建名为 photon-loadbalancing-playcanvas.js 的脚本资源,用于初始化 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:包含 Photon SDK 实时通信的许多功能。

为 Root 实体设置脚本​

在编辑器中将新建的 photon-loadbalancing-playcanvas.js 脚本挂到 Root 实体。

Root 实体检查器

将 AppId 粘贴到脚本属性​

在脚本属性中输入 AppId。

脚本属性

this.loadBalancingClient = new Photon.LoadBalancing.LoadBalancingClient( this.wss ? 1 : 0, this.appId, this.appVersion );
  • wss:通过 WebSocket 建立安全连接。
  • appId:应用标识符。
  • appVersion:用于版本区分,不同版本不能互相连接。

连接 Photon 主服务器​

使用 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:连接指定区域的主服务器。
  • this.region:配置区域。

调用 connectToRegionMaster 成功连接大厅后,日志会显示 JoinedLobby。

控制台日志

创建或加入房间​

连接大厅时调用 onRoomList。

使用 joinRandomOrCreateRoom 随机加入可用房间,没有合适房间时创建新房间。

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):接收大厅中的房间列表。
  • joinRandomOrCreateRoom(options, createRoomName, createOptions):随机加入房间,如果不存在则创建新房间。
  • onJoinRoom:加入房间后调用。

加入与离开​

玩家加入房间时,需要与其他玩家同步。 使用 onActorJoin 和 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 包含 name、actorNr、isLocal 和 userId。
  • onActorJoin:新用户连接时,获取加入的参与者。
  • onActorLeave:用户断开时,获取离开的参与者。

成功后,玩家加入时会添加对应实体。

参与者控制台日志

玩家移动​

创建 player.js,处理角色移动。

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:检查按键是否按下。

同步其他玩家​

通过 raiseEvent 和 onEvent 同步玩家位置。

使用 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):发送 eventCode 与 data。
  • onEvent(code, content, actorNr):接收数据,包括 actorNr 和事件代码。

改为玩家移动时触发事件​

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 });
}
};

完成​

现在可以通过 Photon 进行多人游戏!

项目

你已能使用 Photon 创建房间,并在玩家之间同步位置。

虽然本项目仅演示玩家间的基本实时通信,但也可以添加匹配功能。包含房间创建与房间列表的完整项目见此处。