Colyseus的room.onStateChange重复触发问题
一个简单的Colyseus应用的例子,服务端程序如下:
SceneRoom.ts:
import { Room, Client } from "colyseus";
import { Schema, type } from "@colyseus/schema";
// 定义状态
class SceneState extends Schema {
@type("string")
sceneId: string = "001";
}
export class SceneRoom extends Room<SceneState> {
onCreate() {
this.setState(new SceneState()); // 设置初始状态
// 监听来自客户端的消息
this.onMessage("changeScene", (client, newSceneId: string) => {
if (this.state.sceneId !== newSceneId) {
// 避免重复更新
this.state.sceneId = newSceneId;
console.log(
`Client ${client.sessionId} changed sceneId to: ${newSceneId}`
);
}
});
}
onJoin(client: Client) {
console.log(`Client joined: ${client.sessionId}`);
}
onLeave(client: Client) {
console.log(`Client left: ${client.sessionId}`);
}
onDispose() {
console.log("Room disposed");
}
}
设了一个状态变量sceneId: string = "001";
客户端程序ColyseusTest.vue:
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as Colyseus from 'colyseus.js';
const client = ref<Colyseus.Client | null>(null);
const room = ref<Colyseus.Room | null>(null);
const currentSceneId = ref<string>('未连接');
const newSceneId = ref<string>('');
const connectionStatus = ref<string>('未连接');
const connectToRoom = async () => {
connectionStatus.value = '正在连接...';
client.value = new Colyseus.Client('ws://localhost:5173/ws');
try {
room.value = await client.value.joinOrCreate('scene_room');
connectionStatus.value = '已连接';
// 初始状态同步
currentSceneId.value = room.value.state.sceneId;
// 使用 onStateChange 监听整个状态变化
let lastState = JSON.stringify({});
room.value.onStateChange((state) => {
const currentState = JSON.stringify(state);
if (currentState === lastState) return; // 忽略重复状态
lastState = currentState;
console.log('State updated:', state);
console.log('Raw state:', JSON.stringify(state));
});
console.log('Connected to room:', room.value);
} catch (error) {
console.error('Failed to connect to room:', error);
connectionStatus.value = '连接失败';
}
};
const changeScene = () => {
if (room.value) {
room.value.send('changeScene', newSceneId.value);
console.log('Sent new Scene ID:', newSceneId.value);
} else {
console.error('Room not connected');
}
};
onMounted(() => {
connectToRoom();
});
</script>
<template>
<div class="colyseus-client">
<h1>Colyseus Vue Client</h1>
<p>
当前场景 ID: <strong>{{ currentSceneId }}</strong>
</p>
<div>
<input v-model="newSceneId" placeholder="输入新的场景 ID" type="text" />
<button @click="changeScene">修改场景 ID</button>
</div>
<p v-if="connectionStatus">状态: {{ connectionStatus }}</p>
</div>
</template>
用两个不同的浏览器分别打开了这个测试页面。 其中一个浏览器页面进行了场景ID的修改,其控制台输出如下:
Connected to room: Proxy(_Room) {hasJoined: true, onStateChange: ƒ, onError: ƒ, onLeave: ƒ, onJoin: ƒ, …}
ColyseusTest.vue:29 State updated: _ {$changes: ChangeTree2, $callbacks: undefined, _sceneId: '001', onChange: ƒ}
ColyseusTest.vue:30 Raw state: {"sceneId":"001"}
ColyseusTest.vue:53 Sent new Scene ID: 002
ColyseusTest.vue:29 State updated: _ {$changes: ChangeTree2, $callbacks: undefined, _sceneId: '002', onChange: ƒ}
ColyseusTest.vue:30 Raw state: {"sceneId":"002"}
ColyseusTest.vue:29 State updated: _ {$changes: ChangeTree2, $callbacks: undefined, _sceneId: '002', onChange: ƒ}
ColyseusTest.vue:30 Raw state: {"sceneId":"002"}
ColyseusTest.vue:53 Sent new Scene ID: 003
ColyseusTest.vue:29 State updated: _ {$changes: ChangeTree2, $callbacks: undefined, _sceneId: '003', onChange: ƒ}
ColyseusTest.vue:30 Raw state: {"sceneId":"003"}
ColyseusTest.vue:29 State updated: _ {$changes: ChangeTree2, $callbacks: undefined, _sceneId: '003', onChange: ƒ}
ColyseusTest.vue:30 Raw state: {"sceneId":"003"}
ColyseusTest.vue:53 Sent new Scene ID: 004
ColyseusTest.vue:29 State updated: _ {$changes: ChangeTree2, $callbacks: undefined, _sceneId: '004', onChange: ƒ}
ColyseusTest.vue:30 Raw state: {"sceneId":"004"}
ColyseusTest.vue:29 State updated: _ {$changes: ChangeTree2, $callbacks: undefined, _sceneId: '004', onChange: ƒ}
ColyseusTest.vue:30 Raw state: {"sceneId":"004"}
服务器输出如下:
node-1 | Client joined: 9N5h-irJg
node-1 | Client joined: soEPwRQay
node-1 | Client soEPwRQay changed sceneId to: 002
node-1 | Client soEPwRQay changed sceneId to: 003
node-1 | Client soEPwRQay changed sceneId to: 004
另外一个客户端控制台输出如下:
Connected to room:
Proxy { <target>: {…}, <handler>: {…} }
ColyseusTest.vue:43:12
State updated:
Object { sceneId: Getter & Setter, onChange: onChange(changes)
, … }
ColyseusTest.vue:29:14
Raw state: {"sceneId":"001"} ColyseusTest.vue:30:14
State updated:
Object { sceneId: Getter & Setter, onChange: onChange(changes)
, … }
ColyseusTest.vue:29:14
Raw state: {"sceneId":"002"} ColyseusTest.vue:30:14
State updated:
Object { sceneId: Getter & Setter, onChange: onChange(changes)
, … }
ColyseusTest.vue:29:14
Raw state: {"sceneId":"002"} ColyseusTest.vue:30:14
State updated:
Object { sceneId: Getter & Setter, onChange: onChange(changes)
, … }
ColyseusTest.vue:29:14
Raw state: {"sceneId":"003"} ColyseusTest.vue:30:14
State updated:
Object { sceneId: Getter & Setter, onChange: onChange(changes)
, … }
ColyseusTest.vue:29:14
Raw state: {"sceneId":"003"} ColyseusTest.vue:30:14
State updated:
Object { sceneId: Getter & Setter, onChange: onChange(changes)
, … }
ColyseusTest.vue:29:14
Raw state: {"sceneId":"004"} ColyseusTest.vue:30:14
State updated:
Object { sceneId: Getter & Setter, onChange: onChange(changes)
, … }
ColyseusTest.vue:29:14
Raw state: {"sceneId":"004"}
由上述输出可见,每次修改sceneId, 函数onStateChange都被触发了两次。
我猜测这可能是Colyseus 的“增量补丁”多次触发 onStateChange。
解决方案是采用字段级的更新监听。 通常情况下,Colyseus 的 onChange / listen API 可以实现更轻量级、局部性的更新监听(只在真正有变更的字段时处理),也就不会出现多次全量触发的问题。
具体到这个例子里,就是直接监听sceneId,就不会重复触发了:
// 使用字段级别的监听
room.value.state.listen('sceneId', (newValue, oldValue) => {
currentSceneId.value = newValue
console.log('Scene ID changed from', oldValue, 'to', newValue)
})
总之,序列化整个 State 对象在 State 比较庞大时可能会有性能开销。使用更“精确监听”方式,能够提高性能且不会出现重复触发的情况。