服務器 API » 房間¶
Room 類的作用是實現遊戲會話, 也可作為一組客戶端之間的交流通道.
- 默認情況下系統在做房間匹配時, 房間根據客戶端請求 隨求隨建.
- Room 類必須使用
.define()
公開定義.
import http from "http";
import { Room, Client } from "colyseus";
export class MyRoom extends Room {
// 房間初始化時
onCreate (options: any) { }
// 在 WebSocket 握手完成前, 客戶端基於其提供的 options 進行驗證
onAuth (client: Client, options: any, request: http.IncomingMessage) { }
// 當客戶端成功加入房間時
onJoin (client: Client, options: any, auth: any) { }
// 當客戶端離開房間時
onLeave (client: Client, consented: boolean) { }
// 析構函數, 當房間裏沒有客戶端時被調用. (參考 `autoDispose`)
onDispose () { }
}
const colyseus = require('colyseus');
export class MyRoom extends colyseus.Room {
// 房間初始化時
onCreate (options) { }
// 在 WebSocket 握手完成前, 客戶端基於其提供的 options 進行驗證
onAuth (client, options, request) { }
// 當客戶端成功加入房間時
onJoin (client, options, auth) { }
// 當客戶端離開房間時
onLeave (client, consented) { }
// 析構函數, 當房間裏沒有客戶端時被調用. (參考 `autoDispose`)
onDispose () { }
}
房間生命周期事件¶
- 房間生命周期函數會自動被調用.
- 生命周期事件都可支持
async
/await
.
onCreate (options)
¶
房間由 matchmaker 創建後, 調用一次.
options
參數在房間創建時由客戶端提供:
// 客戶端 - JavaScript SDK
client.joinOrCreate("my_room", {
name: "Jake",
map: "de_dust2"
})
// onCreate() - options 為:
// {
// name: "Jake",
// map: "de_dust2"
// }
服務器的 .define()
時設置的 options 可以被覆蓋以便進行用戶認證等操作:
// 服務器端
gameServer.define("my_room", MyRoom, {
map: "cs_assault"
})
// onCreate() - options are:
// {
// name: "Jake",
// map: "cs_assault"
// }
上例中, 在 onCreate()
時, options 的 map
為 "cs_assault"
, 但是在 onJoin()
時變成了 "de_dust2"
.
onAuth (client, options, request)
¶
在 onJoin()
之前, 將執行 onAuth()
方法. 在客戶進入房間時, 可以使用此方法驗證身份.
- 如果
onAuth()
返回一個 truthy 值, 將調用onJoin()
, 並將返回值作為第三個參數. - 如果
onAuth()
返回 falsy 值, 將立即拒絕客戶端登入, 並客戶端報告匹配失敗. - 也可以拋出一個
ServerError
, 以便在客戶端進行處理.
如果沒有實現 onAuth 方法, 則默認返回 true
, 從而允許任何客戶連接.
獲取玩家的 IP 地址
可以利用 request
變量取得用戶的 IP 地址, http 標頭和更多信息. 例如: request.headers['x-forwarded-for'] || request.connection.remoteAddress
舉例
import { Room, ServerError } from "colyseus";
class MyRoom extends Room {
async onAuth (client, options, request) {
/**
* 可以使用 `async` / `await`,
* 異步底層基於 `Promise`.
*/
const userData = await validateToken(options.accessToken);
if (userData) {
return userData;
} else {
throw new ServerError(400, "bad access token");
}
}
}
import { Room } from "colyseus";
class MyRoom extends Room {
onAuth (client, options, request): boolean {
/**
* 也可以立即返回 `boolean` 值.
*/
if (options.password === "secret") {
return true;
} else {
throw new ServerError(400, "bad access token");
}
}
}
import { Room } from "colyseus";
class MyRoom extends Room {
onAuth (client, options, request): Promise<any> {
/**
* 還可以返回一個 `Promise`, 然後利用它來異步地驗證用戶合法性.
*/
return new Promise((resolve, reject) => {
validateToken(options.accessToken, (err, userData) => {
if (!err) {
resolve(userData);
} else {
reject(new ServerError(400, "bad access token"));
}
});
});
}
}
客戶端舉例
在客戶端, 可以在 matchmaking 函數 (join
, joinOrCreate
等函數) 中使用自定義的身份驗證服務 (例如 Facebook):
client.joinOrCreate("world", {
accessToken: yourFacebookAccessToken
}).then((room) => {
// 成功
}).catch((err) => {
// 處理報錯...
err.code // 400
err.message // "bad access token"
});
try {
var room = await client.JoinOrCreate<YourStateClass>("world", new {
accessToken = yourFacebookAccessToken
});
// 成功
} catch (err) {
// 處理報錯...
err.code // 400
err.message // "bad access token"
}
client:join_or_create("world", {
accessToken = yourFacebookAccessToken
}, function(err, room)
if err then
-- 處理報錯...
err.code -- 400
err.message -- "bad access token"
return
end
-- 成功
end)
client.joinOrCreate("world", {
accessToken: yourFacebookAccessToken
}, YourStateClass, function (err, room) {
if (err != null) {
// 處理報錯...
err.code // 400
err.message // "bad access token"
return;
}
// 成功
})
client.joinOrCreate("world", {
{ "accessToken", yourFacebookAccessToken }
}, [=](MatchMakeError *err, Room<YourStateClass>* room) {
if (err != "") {
// 處理報錯...
err.code // 400
err.message // "bad access token"
return;
}
// 成功
});
onJoin (client, options, auth?)
¶
參數:
client
:client
實例.options
: 把 Server#define() 中指定的值, 與客戶端client.join()
時提供的選項值進行合並.auth
: (可選) 由onAuth
返回的身份驗證數據
在 requestJoin
和 onAuth
完成後, 客戶端成功進入房間時調用.
onLeave (client, consented)
¶
當客戶端離開房間時會調用此函數. 如果是由 客戶端主動離開, 則 consented
參數是 true
, 否則是 false
.
可以將此函數定義為 async
. 參見 優雅關閉.
onLeave(client, consented) {
if (this.state.players.has(client.sessionId)) {
this.state.players.delete(client.sessionId);
}
}
async onLeave(client, consented) {
const player = this.state.players.get(client.sessionId);
await persistUserOnDatabase(player);
}
onDispose ()
¶
在銷毀房間之前會調用 onDispose()
方法, 條件可以是:
- 房間裏沒有客戶端, 而且
autoDispose
被設置為true
(默認值) - 手動調用了
.disconnect()
.
可以寫成 async onDispose()
將它定義為異步方法, 以便在數據庫中保留一些數據. 事實上此方法很適合在遊戲結束時把玩家數據存進數據庫裏.
參見 優雅關閉.
房間示例¶
此示例演示了房間 onCreate
, onJoin
和 onMessage
的用法.
import { Room, Client } from "colyseus";
import { Schema, MapSchema, type } from "@colyseus/schema";
// 一個抽象玩家對象, 表達其在2D世界的位置
export class Player extends Schema {
@type("number")
x: number = 0.11;
@type("number")
y: number = 2.22;
}
// 自定義遊戲狀態, 當前只有以 Player 為元素的一個 ArraySchema
export class State extends Schema {
@type({ map: Player })
players = new MapSchema<Player>();
}
export class GameRoom extends Room<State> {
// 在 room 實例化時 Colyseus 會自動調用此函數
onCreate(options: any) {
// 初始化房間狀態
this.setState(new State());
// 房間接到 "move" 消息時調用
this.onMessage("move", (client, data) => {
const player = this.state.players.get(client.sessionId);
player.x += data.x;
player.y += data.y;
console.log(client.sessionId + " at, x: " + player.x, "y: " + player.y);
});
}
// 客戶端進入房間時自動調用此函數
onJoin(client: Client, options: any) {
this.state.players.set(client.sessionId, new Player());
}
}
const colyseus = require('colyseus');
const schema = require('@colyseus/schema');
// 一個抽象玩家對象, 表達其在2D世界的位置
exports.Player = class Player extends schema.Schema {
constructor() {
super();
this.x = 0.11;
this.y = 2.22;
}
}
schema.defineTypes(Player, {
x: "number",
y: "number",
});
// 自定義遊戲狀態, 當前只有以 Player 為元素的一個 ArraySchema
exports.State = class State extends schema.Schema {
constructor() {
super();
this.players = new schema.MapSchema();
}
}
defineTypes(State, {
players: { map: Player }
});
exports.GameRoom = class GameRoom extends colyseus.Room {
// 在 room 實例化時 Colyseus 會自動調用此函數
onCreate(options) {
// 初始化房間狀態
this.setState(new State());
// 房間接到 "move" 消息時調用
this.onMessage("move", (client, data) => {
const player = this.state.players.get(client.sessionId);
player.x += data.x;
player.y += data.y;
console.log(client.sessionId + " at, x: " + player.x, "y: " + player.y);
});
}
// 客戶端進入房間時自動調用此函數
onJoin(client, options) {
this.state.players.set(client.sessionId, new Player());
}
}
onBeforePatch ()
¶
按照補丁頻率, 每次在 state 同步之前都會觸發 onBeforePatch
生命周期函數. (參見 setPatchRate())
onBeforePatch() {
//
// 這裏可以對 state 做出某些處理,
// 然後 state 就會被序列化並傳送給所有客戶端
//
}
公開方法¶
房間公開了以下方法.
onMessage (type, callback)
¶
註冊一個回調, 以處理客戶端發送的各類型的消息.
type
參數可以是 string
或 number
類型
某一類型消息回調
onCreate () {
this.onMessage("action", (client, message) => {
console.log(client.sessionId, "sent 'action' message: ", message);
});
}
通用類型消息回調
可以註冊一個通用回調以處理所有類型的消息.
onCreate () {
this.onMessage("action", (client, message) => {
//
// 當收到 'action' 消息時觸發回調.
//
});
this.onMessage("*", (client, type, message) => {
//
// 當收到其他各種消息時觸發回調,
// 不包括 "action", 因為已經提前對該類型消息進行了註冊.
//
console.log(client.sessionId, "sent", type, message);
});
}
客戶端通過使用 room.send()
來發送消息
詳見 room.send()
} 章節.
setState (object)
¶
設置房間同步狀態. 參見 State Synchronization 和 Schema 以了解更多信息.
Tip
設置同步狀態通常只需在 onCreate()
時調用一次即可
Warning
房間狀態更新時不需要調用 .setState()
. 因為每次調用都會重置二叉樹路徑算法.
setSimulationInterval (callback[, milliseconds=16.6])
¶
(可選) 設置一個可以更改遊戲狀態的模擬時間間隔. 代表遊戲更新循環. 默認模擬間隔: 16.6ms (60fps)
onCreate () {
this.setSimulationInterval((deltaTime) => this.update(deltaTime));
}
update (deltaTime) {
// 此處實現遊戲物理或者視覺更新!
// 同時也是房間狀態更新的地方
}
setPatchRate (milliseconds)
¶
設置 state 補丁發送給所有客戶端的頻率. 默認值為 50
ms (20fps)
setPrivate (bool)
¶
將該房間設置為私人房間(參數傳入 false
則表示設置為公共房間).
私人房間不會出現在 >getAvailableRooms()
方法返回的房間列表中.
setMetadata (metadata)
¶
設置該房間的元數據. 每個房間實例都可附加元數據 - 附加元數據的唯一目的在於客戶端使用 client.getAvailableRooms()
獲取房間和通過 roomId
連接房間時能區分同名但不同屬性的房間.
// 服務端
this.setMetadata({ friendlyFire: true });
此時房間已經附加了元數據, 舉例來說, 客戶端可以檢查哪個房間有 friendlyFire
, 然後通過其 roomId
連接到想要進入的房間:
// 客戶端
client.getAvailableRooms("battle").then(rooms => {
for (var i=0; i<rooms.length; i++) {
if (room.metadata?.friendlyFire) {
//
// 查找具有 `friendlyFire` 元數據的房間 id:
//
var room = client.join(room.roomId);
return;
}
}
});
setSeatReservationTime (seconds)
¶
設置該房間等待客戶端加入的秒數. 應該考慮 onAuth()
需要等待多長時間, 以設置不同的座位預訂時間. 默認值為 15 秒.
如果想要全局設置房間等待時間, 可以設置 COLYSEUS_SEAT_RESERVATION_TIME
環境變量.
send (client, message)
¶
已棄用
this.send()
已被棄用. 請使用 client.send()
代替.
broadcast (type, message, options?)
¶
向已連接的所有客戶端發送一條消息廣播.
options 參數可以包含:
except
: 排除發送消息至這些Client
afterNextPatch
: 等到下一個狀態補丁再發送廣播消息
廣播示例¶
向所有客戶端廣播一條消息:
onCreate() {
this.onMessage("action", (client, message) => {
// 廣播至所有客戶端
this.broadcast("action-taken", "an action has been taken!");
});
}
向所有客戶端廣播一條消息, 發送者除外:
onCreate() {
this.onMessage("fire", (client, message) => {
// 發送 "fire" 事件到所有客戶端, 除了發送者自己.
this.broadcast("fire", message, { except: client });
});
}
在應用狀態變更之後, 向所有客戶端廣播一條消息:
onCreate() {
this.onMessage("destroy", (client, message) => {
// 改變 state
this.state.destroySomething();
// 此消息會在 state 改變應用之後再到達客戶端
this.broadcast("destroy", "something has been destroyed", { afterNextPatch: true });
});
}
廣播一條 schema 消息:
class MyMessage extends Schema {
@type("string") message: string;
}
// ...
onCreate() {
this.onMessage("action", (client, message) => {
const data = new MyMessage();
data.message = "an action has been taken!";
this.broadcast(data);
});
}
lock ()
¶
鎖定房間會從供新客戶端連接的房間池中移除該房間.
unlock ()
¶
解鎖房會將房間重新添加至供新客戶連接的房間池中.
allowReconnection (client, seconds)
¶
允許指定的客戶 reconnect
房間. 必須在 onLeave()
方法中使用.
client
: 掉線的Client
實例seconds
: 等待客戶端實施.reconnect()
的秒數, 或者傳入參數值"manual"
, 來實現手動拒絕重連 (見下面第二個示例)
返回類型:
allowReconnection()
返回一個Deferred<Client>
實例.Deferred
是一個類似於 pormise 的類型Deferred
類型可以通過調用.reject()
強製拒絕 promise (參見第二個示例)
示例 在 20 秒超時後拒絕重新連接.
async onLeave (client: Client, consented: boolean) {
// 標註客戶端離線
this.state.players.get(client.sessionId).connected = false;
try {
if (consented) {
throw new Error("consented leave");
}
// 允許離線客戶端在 20 秒內重新連接
await this.allowReconnection(client, 20);
// 客戶端回連, 標註其已連接.
this.state.players.get(client.sessionId).connected = true;
} catch (e) {
// 20 秒超時. 移除離線客戶端.
this.state.players.delete(client.sessionId);
}
}
示例 使用自定義邏輯手動拒絕重連.
async onLeave (client: Client, consented: boolean) {
// 標註客戶端離線
this.state.players.get(client.sessionId).connected = false;
try {
if (consented) {
throw new Error("consented leave");
}
//
// 獲取重連令牌
// 註意: 這裏不要使用 `await`
//
const reconnection = this.allowReconnection(client, "manual");
//
// 這裏展示了自定義邏輯拒絕重連
// 的 API 用法, 如果用戶 2 輪失敗
// 則設置超時禁止重連,
// (假設遊戲是回合製的)
//
// 實際操作中, 應該把 `reconnection` 保存在
// 你的 Player 實例中, 然後在自定義邏輯中
// 進行檢測
//
const currentRound = this.state.currentRound;
const interval = setInterval(() => {
if ((this.state.currentRound - currentRound) > 2) {
// 手動禁止客戶端重連
reconnection.reject();
clearInterval(interval);
}
}, 1000);
// 允許離線重連
await reconnection;
// 客戶端回連, 標註其已連接.
this.state.players.get(client.sessionId).connected = true;
} catch (e) {
// 20 秒超時. 移除離線客戶端.
this.state.players.delete(client.sessionId);
}
}
disconnect ()
¶
斷開所有客戶斷, 然後銷毀房間.
broadcastPatch ()
¶
一般不需要這樣做!
框架系統會自動調用此方法.
此方法會檢查 state
是否發生變化, 並將變化廣播給所有已連接的客戶端.
如果想要控製何時廣播補丁, 可以禁用默認補丁間隔時間來實現:
onCreate() {
// 關閉自動補丁廣播
this.setPatchRate(null);
// 確保計時有效
this.setSimulationInterval(() => {/* */});
this.clock.setInterval(() => {
// 達到自定義條件, 廣播補丁.
if (yourCondition) {
this.broadcastPatch();
}
}, 2000);
}
公開屬性¶
roomId: string
¶
自動生成的 9 字符長的唯一房間 id.
在 onCreate()
期間, 可以修改 this.roomId
.
使用自定義 roomId
roomName: string
¶
房間名稱會作為 gameServer.define()
的第一個參數.
state: T
¶
提供給 setState()
的狀態實例.
clients: Client[]
¶
已連接客戶端的數組. 參見 Client instance.
maxClients: number
¶
允許連接進入房間的最大客戶端數量. 當數量達到此限製時, 房間將自動鎖定. 房間除非通過 lock() 方法手動鎖定, 否則都會在客戶端斷開房間時立即解鎖.
patchRate: number
¶
將房間狀態發送至客戶端的頻率, 單位為毫秒. 默認值為 50
ms (20fps)
autoDispose: boolean
¶
最後一個客戶端斷開連接後, 自動銷毀房間. 默認值是 true
locked: boolean
(只讀)¶
以下情況會影響此屬性:
clock: ClockTimer
¶
一個 ClockTimer
實例,
用於 timing events.
Presence Presence
¶
presence
實例. 查閱 Presence API 了解更多詳細信息.
客戶端¶
服務器端的 client
實例負責服務器與客戶端之間的 transport 層. 不應該與 客戶端 SDK 裏的 Client
相混淆, 因為它們的意義完全不同!
可以通過 this.clients
, 在 Room#onJoin()
, Room#onLeave()
和 Room#onMessage()
中操作 client
實例.
Note
這是來自 ws
包的原始 WebSocket 連接. 還有更多的方法可用, 但是不建議用於 Colyseus.
屬性¶
sessionId: string
¶
每個會話的唯一 id.
Note
在客戶端, 可以在 room
實例中找到 sessionId
.
userData: any
¶
可用於存儲關於客戶端連接的自定義數據. userData
不會 與客戶端同步, 僅用於保存指定用戶的連接.
onJoin(client, options) {
client.userData = { playerNumber: this.clients.length };
}
onLeave(client) {
console.log(client.userData.playerNumber);
}
auth: any
¶
onAuth()
期間返回的自定義數據.
方法¶
send(type, message)
¶
發送某類型消息至客戶端. 消息使用 MsgPack 編碼, 可用於任何可序列化的 JSON 數據結構.
type
可以是 string
或 number
.
發送消息:
//
// 發送字符串類型消息 ("powerup")
//
client.send("powerup", { kind: "ammo" });
//
// 發送數字類型消息 (1)
//
client.send(1, { kind: "ammo"});
Tip
sendBytes(type, bytes)
¶
向客戶端發送字節數組.
參數 type
可以是一個 string
或者是一個 number
.
當需要使用自定義編碼, 而不使用默認編碼器 (MsgPack) 時會很有用.
發送消息:
//
// 發送字符串類型 ("powerup") 消息
//
client.sendBytes("powerup", [ 172, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 ]);
//
// 發送數字類型 (1) 消息
//
client.sendBytes(1, [ 172, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 ]);
leave(code?: number)
¶
把 client
與房間強行斷開. 您可以在關閉連接時發送一個介於 4000
和 4999
之間的自定義 code
(參見 WebSocket 斷線狀態代碼表)
Tip
這將在客戶端觸發 room.onLeave
事件.
WebSocket 斷線狀態代碼表¶
斷線代碼 (uint16) | 代碼名稱 | 內部使用 | 可自定義 | 說明 |
---|---|---|---|---|
0 - 999 |
Yes | No | 未使用 | |
1000 |
CLOSE_NORMAL |
No | No | 成功斷開 / 套接字斷開 |
1001 |
CLOSE_GOING_AWAY |
No | No | 客戶端離開 (瀏覽器頁面關閉) |
1002 |
CLOSE_PROTOCOL_ERROR |
Yes | No | 入口接到錯誤幀 |
1003 |
CLOSE_UNSUPPORTED |
Yes | No | 入口接到不支持幀 (例如二進製入口接到文本幀) |
1004 |
Yes | No | 保留 | |
1005 |
CLOSED_NO_STATUS |
Yes | No | 未收到狀態代碼的斷開 |
1006 |
CLOSE_ABNORMAL |
Yes | No | 收到無斷開代碼的幀 |
1007 |
Unsupported payload | Yes | No | 入口接到錯誤消息 (例如非法 UTF-8) |
1008 |
Policy violation | No | No | 1003 與 1009 之外的一般狀態代碼 |
1009 |
CLOSE_TOO_LARGE |
No | No | 入口接到無法處理的大數據幀 |
1010 |
Mandatory extension | No | No | 客戶端發送了未協商的擴展數據 |
1011 |
Server error | No | No | 運行中的服務器內部錯誤 |
1012 |
Service restart | No | No | 服務器/服務正在重啟 |
1013 |
Try again later | No | No | 服務器臨時狀況導致客戶端請求受阻 |
1014 |
Bad gateway | No | No | 用於網關的服務器收到非法響應 |
1015 |
TLS handshake fail | Yes | No | 傳輸層安全相關錯誤 |
1016 - 1999 |
Yes | No | 為未來的 WebSocket 標準保留. | |
2000 - 2999 |
Yes | Yes | 為 WebSocket 擴展數據保留 | |
3000 - 3999 |
No | Yes | 用於支持其他庫或框架使用. 服務器可能不會用到. 可以通過 IANA 先到先得途徑註冊. | |
4000 - 4999 |
No | Yes | 用於應用服務器 |
error(code, message)
¶
將錯誤代碼與消息一並發送給客戶端. 客戶端可以在 onError
中對其進行處理.