上节实现了 ws 的服务端
这节我们在前端里连上,实现多人在线的功能。
进入前端项目,安装下依赖:
pnpm install socket.io-client
在 vite.config.ts 加一下接口转发逻辑:

'/socket.io': {
target: 'http://localhost:3000',
changeOrigin: true,
ws: true
}
创建 src/worldSync.js 做一下 ws 的连接和各种事件的处理:

import { io } from 'socket.io-client';
/** 与 open-world-server 一致:命名空间 /world,path /socket.io */
function getSocketOrigin() {
const base = import.meta.env.VITE_WS_BASE;
if (base) return String(base).replace(/\/$/, '');
if (import.meta.env.DEV) {
return typeof window !== 'undefined' ? window.location.origin : 'http://localhost:5173';
}
return typeof window !== 'undefined' ? window.location.origin : '';
}
let socket = null;
let emitAcc = 0;
const EMIT_INTERVAL = 1 / 12;
/** playerState 较频繁,仅节流打印摘要 */
let lastPlayerStateLog = 0;
const PLAYER_STATE_LOG_MS = 1000;
/**
* 连接多人房间(需已登录 JWT)。重复调用会先断开旧连接。
* @param {string} accessToken
*/
export function connectWorld(accessToken) {
if (!accessToken) return;
disconnectWorld();
const origin = getSocketOrigin();
socket = io(`${origin}/world`, {
path: '/socket.io',
auth: { token: accessToken },
transports: ['websocket', 'polling'],
});
socket.on('connect', () => {
console.log('[world] 已连接', { id: socket.id });
});
socket.on('disconnect', (reason) => {
console.log('[world] 已断开', reason);
});
socket.on('connect_error', (err) => {
console.warn('[world] 连接失败', err.message);
});
socket.on('worldRoster', (data) => {
console.log('[world] worldRoster', data);
});
socket.on('playerJoined', (data) => {
console.log('[world] playerJoined', data);
});
socket.on('playerLeft', (data) => {
console.log('[world] playerLeft', data);
});
socket.on('playerState', (payload) => {
const now = Date.now();
if (now - lastPlayerStateLog >= PLAYER_STATE_LOG_MS) {
lastPlayerStateLog = now;
console.log('[world] playerState (节流 1s 内汇总)', payload);
}
});
}
export function disconnectWorld() {
emitAcc = 0;
lastPlayerStateLog = 0;
if (socket) {
socket.removeAllListeners();
socket.disconnect();
socket = null;
}
}
/**
* 在 mesh 的物理循环里调用;仅在步行且角色已加载时上传状态。
* @param {number} dt
* @param {() => { position: { x: number; y: number; z: number }; rotationY: number }} getPayload
* @param {boolean} enabled
*/
export function worldSyncTick(dt, getPayload, enabled) {
if (!socket?.connected || !enabled) return;
emitAcc += dt;
if (emitAcc < EMIT_INTERVAL) return;
emitAcc = 0;
const p = getPayload();
socket.emit('state', {
position: p.position,
rotationY: p.rotationY,
});
}
这里我们只是打印了下
然后在 mesh.js 里每次动画都更新下:

import { worldSyncTick } from './worldSync.js';
const canWorldSync =
characterModel &&
!isCarView &&
!isPlaneView &&
!isComputerView;
worldSyncTick(
dt,
() => ({
position: {
x: playerBody.position.x,
y: playerBody.position.y,
z: playerBody.position.z,
},
rotationY: characterModel.rotation.y,
}),
canWorldSync
);
通过 state 事件来把当前的位置、角度等传到服务端,分发给 room 内其他客户端
在 main.js 里连接下:


if (data.accessToken) {
connectWorld(data.accessToken);
}

const token = localStorage.getItem('accessToken');
if (token) {
connectWorld(token);
}
这样,整体从建立连接,到 state 的状态更新,以及接收其他 client 的事件就都完成了。
测试下:
首先我注册了 guang、dogndong 两个账号。
分别打开一个浏览器、一个无痕浏览器来测试:


可以看到,当一个玩家在移动的时候,另一个玩家也能收到其他玩家的状态,包括位置、角度等。
那把这些渲染出来不就是多人在线了么?
总结
这节我们学了 ws 客户端的逻辑,连上 ws 服务端之后,和后端通过各种事件传递消息。
比如其他 player 的 state 的变更。
接下来把这些绘制出来,就是多人在线功能。