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 比较庞大时可能会有性能开销。使用更“精确监听”方式,能够提高性能且不会出现重复触发的情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值