强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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

特性原生 WebSocketSocket.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, '&lt;').replace(/>/g, '&gt;').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 请求 + 状态机
棋牌游戏房间系统 + 消息驱动

扩展阅读