Pixi.js 游戏开发教程 / Pixi.js 多人网络(WebSocket)
多人网络(WebSocket)
用 WebSocket 为你的 Pixi.js 游戏添加多人在线功能。
一、WebSocket 基础
1.1 原生 WebSocket
// 客户端连接
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
console.log('已连接到服务器');
ws.send(JSON.stringify({ type: 'join', name: 'Player1' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('收到消息:', data);
};
ws.onclose = () => {
console.log('连接已断开');
};
ws.onerror = (error) => {
console.error('连接错误:', error);
};
1.2 使用 Socket.io
npm install socket.io socket.io-client
服务端:
// server.ts
import { Server } from 'socket.io';
const io = new Server(8080, {
cors: { origin: '*' },
});
io.on('connection', (socket) => {
console.log(`玩家连接: ${socket.id}`);
socket.on('join', (data) => {
console.log(`${data.name} 加入游戏`);
});
socket.on('disconnect', () => {
console.log(`玩家断开: ${socket.id}`);
});
});
客户端:
import { io } from 'socket.io-client';
const socket = io('http://localhost:8080');
socket.on('connect', () => {
console.log('已连接:', socket.id);
socket.emit('join', { name: 'Player1' });
});
1.3 原生 WS vs Socket.io
| 特性 | 原生 WebSocket | Socket.io |
|---|---|---|
| 自动重连 | 需手动实现 | 内置 |
| 房间系统 | 需自己实现 | 内置 |
| 二进制支持 | 原生支持 | 需配置 |
| 包体大小 | 0 KB | ~50 KB |
| 消息协议 | 自定义 | 基于 JSON |
| 适用场景 | 简单连接、高性能需求 | 快速开发、复杂功能 |
二、Node.js 游戏服务器
2.1 基础服务器框架
npm install ws express
npm install -D @types/ws typescript
// server/index.ts
import express from 'express';
import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';
const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
// 玩家数据
interface Player {
id: string;
x: number;
y: number;
name: string;
ws: WebSocket;
}
const players = new Map<string, Player>();
wss.on('connection', (ws: WebSocket) => {
const playerId = Math.random().toString(36).slice(2, 9);
const player: Player = {
id: playerId,
x: 400,
y: 300,
name: `Player_${playerId}`,
ws,
};
players.set(playerId, player);
// 通知其他玩家
broadcast({
type: 'player_join',
player: { id: playerId, x: player.x, y: player.y, name: player.name },
}, playerId);
// 发送当前游戏状态给新玩家
ws.send(JSON.stringify({
type: 'init',
playerId,
players: Array.from(players.values()).map(p => ({
id: p.id, x: p.x, y: p.y, name: p.name,
})),
}));
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
handleMessage(playerId, msg);
});
ws.on('close', () => {
players.delete(playerId);
broadcast({ type: 'player_leave', playerId });
});
});
function handleMessage(playerId: string, msg: any) {
const player = players.get(playerId);
if (!player) return;
switch (msg.type) {
case 'move':
player.x = msg.x;
player.y = msg.y;
broadcast({
type: 'player_move',
playerId,
x: msg.x,
y: msg.y,
}, playerId);
break;
}
}
function broadcast(data: any, excludeId?: string) {
const message = JSON.stringify(data);
for (const [id, player] of players) {
if (id !== excludeId && player.ws.readyState === WebSocket.OPEN) {
player.ws.send(message);
}
}
}
server.listen(8080, () => {
console.log('游戏服务器运行在 http://localhost:8080');
});
三、状态同步
3.1 快照同步(Snapshot)
定期发送完整游戏状态:
// 服务端:每 100ms 广播一次完整快照
setInterval(() => {
const snapshot = {
type: 'snapshot',
timestamp: Date.now(),
players: Array.from(players.values()).map(p => ({
id: p.id,
x: p.x,
y: p.y,
})),
enemies: enemies.map(e => ({
id: e.id,
x: e.x,
y: e.y,
hp: e.hp,
})),
};
broadcast(snapshot);
}, 100);
3.2 增量同步(Delta)
仅发送变化部分:
interface DeltaUpdate {
type: 'delta';
timestamp: number;
changes: {
id: string;
field: string;
value: any;
}[];
}
// 服务端:追踪变化并发送增量
let lastSnapshot = new Map<string, any>();
function sendDelta() {
const changes: any[] = [];
for (const [id, player] of players) {
const last = lastSnapshot.get(id);
if (!last || last.x !== player.x || last.y !== player.y) {
changes.push({ id, field: 'x', value: player.x });
changes.push({ id, field: 'y', value: player.y });
lastSnapshot.set(id, { x: player.x, y: player.y });
}
}
if (changes.length > 0) {
broadcast({
type: 'delta',
timestamp: Date.now(),
changes,
});
}
}
setInterval(sendDelta, 50);
3.3 同步策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 快照同步 | 实现简单、一致性好 | 带宽大 | 小规模、低频更新 |
| 增量同步 | 带宽小 | 实现复杂、需追踪变化 | 大规模、高频更新 |
| 混合同步 | 兼顾两者 | 实现最复杂 | 商业游戏 |
⚠️ 注意:快照同步在 10+ 玩家时带宽消耗急剧增加,建议使用增量同步。
四、客户端预测与延迟补偿
4.1 客户端预测(Client-Side Prediction)
玩家操作先在本地执行,收到服务端确认后校正:
class NetworkPlayer {
public sprite: Sprite;
private pendingInputs: { input: any; seq: number }[] = [];
private lastProcessedSeq = 0;
processServerState(serverState: { x: number; y: number; lastProcessedSeq: number }) {
// 用服务端状态覆盖
this.sprite.x = serverState.x;
this.sprite.y = serverState.y;
// 移除已确认的输入
this.pendingInputs = this.pendingInputs.filter(
i => i.seq > serverState.lastProcessedSeq
);
// 重新应用未确认的输入(reconciliation)
for (const pending of this.pendingInputs) {
this.applyInput(pending.input);
}
}
applyInput(input: { dx: number; dy: number }) {
this.sprite.x += input.dx;
this.sprite.y += input.dy;
}
sendInput(input: { dx: number; dy: number }, seq: number) {
this.pendingInputs.push({ input, seq });
this.applyInput(input);
// 发送到服务端
socket.emit('input', { ...input, seq });
}
}
4.2 输入延迟补偿(Lag Compensation)
// 服务端:保存历史状态
const stateHistory: { timestamp: number; state: any }[] = [];
function saveState(timestamp: number) {
stateHistory.push({
timestamp,
state: Array.from(players.values()).map(p => ({
id: p.id, x: p.x, y: p.y,
})),
});
// 保留最近 1 秒的历史
const cutoff = Date.now() - 1000;
while (stateHistory.length > 0 && stateHistory[0].timestamp < cutoff) {
stateHistory.shift();
}
}
// 处理射击时,回溯到玩家发出指令时的世界状态
function handleShoot(playerId: string, timestamp: number) {
// 找到最接近的历史状态
const history = stateHistory.find(h => h.timestamp >= timestamp);
if (!history) return;
// 在历史状态下检测命中
const hit = checkHit(history.state, playerId, timestamp);
if (hit) {
broadcast({ type: 'hit', targetId: hit.targetId, shooterId: playerId });
}
}
五、房间系统
5.1 实现房间管理
interface Room {
id: string;
name: string;
players: Set<string>;
maxPlayers: number;
state: 'waiting' | 'playing';
}
const rooms = new Map<string, Room>();
function createRoom(name: string, maxPlayers: number): Room {
const room: Room = {
id: Math.random().toString(36).slice(2, 8),
name,
players: new Set(),
maxPlayers,
state: 'waiting',
};
rooms.set(room.id, room);
return room;
}
function joinRoom(roomId: string, playerId: string): boolean {
const room = rooms.get(roomId);
if (!room || room.players.size >= room.maxPlayers) return false;
room.players.add(playerId);
// 通知房间内所有玩家
for (const pid of room.players) {
const player = players.get(pid);
if (player) {
player.ws.send(JSON.stringify({
type: 'room_update',
roomId,
players: Array.from(room.players),
}));
}
}
return true;
}
// Socket.io 内置房间
// socket.join('room-1');
// io.to('room-1').emit('message', data);
六、聊天系统
// 客户端
function sendChat(message: string) {
socket.emit('chat', {
message,
timestamp: Date.now(),
});
}
socket.on('chat', (data) => {
addChatMessage(data.playerName, data.message, data.timestamp);
});
// 服务端
socket.on('chat', (data) => {
// 广播给房间内所有人
io.to(playerRoom).emit('chat', {
playerId: socket.id,
playerName: player.name,
message: sanitize(data.message), // 防 XSS
timestamp: Date.now(),
});
});
// 消息过滤
function sanitize(text: string): string {
return text.replace(/</g, '<').replace(/>/g, '>').slice(0, 200);
}
七、权威服务器模式
7.1 核心原则
客户端:只发送输入 → 服务端:计算结果 → 客户端:显示结果
| 角色 | 职责 |
|---|---|
| 客户端 | 渲染、收集输入、预测 |
| 服务端 | 权威状态、碰撞检测、游戏逻辑 |
7.2 服务端权威实现
// 服务端游戏循环
const TICK_RATE = 20; // 每秒 20 次逻辑更新
const TICK_INTERVAL = 1000 / TICK_RATE;
function gameLoop() {
// 1. 收集所有玩家输入
for (const [id, player] of players) {
const input = pendingInputs.get(id);
if (input) {
// 验证输入合法性
if (isValidInput(input, player)) {
applyInput(player, input);
}
pendingInputs.delete(id);
}
}
// 2. 更新游戏逻辑
updateEnemies();
checkCollisions();
checkVictory();
// 3. 广播状态
broadcastState();
}
setInterval(gameLoop, TICK_INTERVAL);
// 输入验证(防作弊)
function isValidInput(input: any, player: Player): boolean {
// 检查移动速度是否合理
const maxSpeed = 10;
const dx = Math.abs(input.dx);
const dy = Math.abs(input.dy);
return dx <= maxSpeed && dy <= maxSpeed;
}
八、网络延迟处理
8.1 延迟测量
// Ping 测量
let pingStart: number;
setInterval(() => {
pingStart = Date.now();
socket.emit('ping_check');
}, 2000);
socket.on('pong_check', () => {
const ping = Date.now() - pingStart;
console.log(`Ping: ${ping}ms`);
});
8.2 插值平滑
// 在两个服务端状态之间插值,使运动平滑
class InterpolationBuffer {
private buffer: { timestamp: number; state: any }[] = [];
private interpolationDelay = 100; // 100ms 延迟
addState(timestamp: number, state: any) {
this.buffer.push({ timestamp, state });
// 保留 1 秒数据
const cutoff = Date.now() - 1000;
this.buffer = this.buffer.filter(s => s.timestamp > cutoff);
}
getInterpolatedState(currentTime: number): any {
const renderTime = currentTime - this.interpolationDelay;
// 找到前后两个状态
let prev = this.buffer[0];
let next = this.buffer[1];
for (let i = 0; i < this.buffer.length - 1; i++) {
if (this.buffer[i].timestamp <= renderTime &&
this.buffer[i + 1].timestamp >= renderTime) {
prev = this.buffer[i];
next = this.buffer[i + 1];
break;
}
}
if (!prev || !next) return prev?.state ?? next?.state;
// 线性插值
const t = (renderTime - prev.timestamp) / (next.timestamp - prev.timestamp);
return {
x: prev.state.x + (next.state.x - prev.state.x) * t,
y: prev.state.y + (next.state.y - prev.state.y) * t,
};
}
}
九、安全基础(防作弊)
| 风险 | 防范措施 |
|---|---|
| 修改客户端代码 | 服务端验证所有关键逻辑 |
| 加速外挂 | 服务端检查输入频率和移动速度 |
| 透视/自瞄 | 敌人位置只在视野内发送 |
| 伪造消息 | 消息签名、Session Token |
| 重放攻击 | 每条消息附带时间戳和序列号 |
⚠️ 注意:永远不要信任客户端数据。所有关键判定(碰撞、伤害、得分)必须在服务端完成。
十、实战:多人在线对战
10.1 完整客户端网络管理器
import { io, Socket } from 'socket.io-client';
class NetworkManager {
private socket: Socket;
private playerId: string = '';
private players = new Map<string, any>();
constructor(url: string) {
this.socket = io(url);
this.socket.on('connect', () => {
console.log('已连接');
});
this.socket.on('init', (data) => {
this.playerId = data.playerId;
data.players.forEach((p: any) => {
this.players.set(p.id, p);
});
});
this.socket.on('player_join', (data) => {
this.players.set(data.player.id, data.player);
});
this.socket.on('player_leave', (data) => {
this.players.delete(data.playerId);
});
this.socket.on('player_move', (data) => {
const player = this.players.get(data.playerId);
if (player) {
player.targetX = data.x;
player.targetY = data.y;
}
});
}
move(x: number, y: number) {
this.socket.emit('move', { x, y });
}
shoot(angle: number) {
this.socket.emit('shoot', { angle });
}
get localPlayerId() {
return this.playerId;
}
get allPlayers() {
return this.players;
}
}
// 使用
const network = new NetworkManager('http://localhost:8080');
app.ticker.add(() => {
// 玩家移动时发送
if (playerMoved) {
network.move(player.x, player.y);
}
// 更新其他玩家位置(插值)
network.allPlayers.forEach((data, id) => {
if (id !== network.localPlayerId && data.sprite) {
data.sprite.x += (data.targetX - data.sprite.x) * 0.2;
data.sprite.y += (data.targetY - data.sprite.y) * 0.2;
}
});
});
游戏开发场景
| 场景 | 网络方案 |
|---|---|
| 休闲对战(2-4人) | 快照同步 + Socket.io 房间 |
| 大逃杀(20-100人) | 增量同步 + 空间分区 |
| 实时射击 | 权威服务器 + 客户端预测 |
| 回合制策略 | HTTP 请求 + 状态机 |
| 棋牌游戏 | 房间系统 + 消息驱动 |