Ejemplo de juego de sincronización de cuadros de Cocos Creator.

Recientemente he tenido la libertad de volver a estudiar la sincronización de cuadros en la estrategia de sincronización. Primero, hablemos de la diferencia entre sincronización de cuadros y sincronización de estado.

1: Sincronización de cuadros:

La sincronización de cuadros es una tecnología de sincronización de red que se usa comúnmente en juegos multijugador para garantizar que el estado del juego entre diferentes jugadores sea consistente. En la sincronización de cuadros, todos los jugadores logran un estado de juego consistente ejecutando la lógica del juego y renderizando de acuerdo con la misma secuencia de cuadros.

El siguiente es el proceso básico de sincronización de cuadros de juego:

  1. El servidor envía datos del marco: el servidor del juego, como anfitrión, es responsable de generar los datos del marco del juego y enviarlos a todos los clientes. Los datos del cuadro contienen toda la información necesaria sobre el estado actual del juego, como la posición del jugador, el estado del objeto, etc.

  2. El cliente recibe datos del cuadro: el cliente recibe los datos del cuadro enviados por el servidor y simula y representa la lógica del juego en función de los datos recibidos.

  3. Sincronización de fotogramas del cliente: el cliente ejecuta la lógica del juego y renderiza en secuencia de acuerdo con la secuencia de fotogramas enviada por el servidor en función de los datos del fotograma recibidos. Esto garantiza que todos los clientes jueguen en el mismo orden y estado, manteniendo la coherencia.

  4. Sincronización de entrada del jugador: cada cliente envía información de entrada del jugador al servidor, y el servidor recopila y procesa toda la entrada del jugador.

  5. Procesamiento de la lógica del servidor: el servidor realiza el procesamiento de la lógica del juego en función de la información de entrada del jugador recibida. Esto garantiza que el estado del juego de todos los clientes se base en la misma entrada.

  6. Actualizar datos del cuadro: después de que el servidor ejecuta la lógica del juego, genera nuevos datos del cuadro y los envía a todos los clientes, repitiendo el proceso anterior.

A través de la sincronización de cuadros, todos los jugadores pueden ver el mismo estado del juego en diferentes clientes, lo que garantiza la equidad y coherencia del juego.

 

ventaja:

  1. Equidad: la sincronización de cuadros garantiza que el estado del juego visto por todos los jugadores en diferentes clientes sea consistente, evitando situaciones injustas.

  2. Previsibilidad: debido a que todos los clientes ejecutan la lógica del juego de acuerdo con la misma secuencia de cuadros, los jugadores pueden predecir las acciones y resultados de otros jugadores, aumentando la estrategia y la competencia.

  3. Simplifique la comunicación de red: la sincronización de cuadros solo necesita transmitir datos de cuadros y la entrada del jugador, en lugar de transmitir cada operación y cambio de estado en el juego, lo que reduce la cantidad de datos y la sobrecarga en la comunicación de red. Amigable para la reproducción

  4. Permitir conexión y reconexión: la sincronización de fotogramas permite a los jugadores volver a unirse después de desconectarse o desconectarse durante el juego, porque el estado y la entrada del jugador se sincronizan en función de los datos del fotograma.

defecto:

  1. Latencia e inestabilidad de la red: la latencia de la red puede provocar retrasos y congelaciones en la sincronización de cuadros, especialmente cuando la calidad de la conexión de red entre los reproductores es inestable. Una latencia más alta afecta la capacidad de respuesta y la naturaleza en tiempo real del juego.

  2. Requisitos de ancho de banda y volumen de datos: la sincronización de cuadros requiere una transmisión frecuente de datos de cuadros y entradas del reproductor entre el servidor y el cliente, por lo que requiere un gran ancho de banda de red y volumen de datos. Para juegos multijugador a gran escala o condiciones de red deficientes, la carga y el costo de la red pueden aumentar.

  3. Problemas de complejidad y sincronización: la implementación de la sincronización de tramas implica mecanismos y algoritmos de sincronización complejos, incluido el procesamiento del retraso de la red, el procesamiento de la predicción de entrada, la compensación y la interpolación, etc. Estas mecánicas deben diseñarse y ajustarse cuidadosamente para garantizar la coherencia y la fluidez del juego.

2: sincronización de estado

El siguiente es el proceso general de sincronización de estado:

  1. Recopilación de entradas del jugador: cada cliente recopila información de entrada de los jugadores locales, como operaciones clave, movimientos del mouse, etc. Esta información de entrada generalmente se registra en forma de eventos y se envía al servidor en el momento adecuado.

  2. Entrada de procesamiento del servidor: después de que el servidor recibe la información de entrada del jugador, ejecuta la lógica y los cálculos del juego correspondientes. El servidor puede simular las acciones del jugador y actualizar el estado del juego.

  3. Actualización del estado del juego: el servidor actualiza el estado, la ubicación, los atributos y otra información de los objetos del juego en función de los resultados de la ejecución de la lógica del juego.

  4. Transmisión de estado: el servidor transmite el estado actualizado del juego a todos los clientes. Las transmisiones suelen emplear protocolos de comunicación de red como TCP o UDP para garantizar una entrega confiable de actualizaciones de estado.

  5. Recepción del cliente y estado de la aplicación: cada cliente recibe actualizaciones del estado del juego transmitidas por el servidor y las aplica a la simulación y renderizado del juego local. El cliente actualiza la escena del juego, la posición del personaje, la animación, etc. en función de las actualizaciones de estado recibidas.

A través del proceso anterior, la entrada del jugador se procesa y calcula en el lado del servidor, y los resultados se transmiten a todos los clientes, logrando así la sincronización del estado del juego multijugador.

Durante el proceso de sincronización de estado, se deben considerar factores como el retraso de la red, las limitaciones de ancho de banda y la compresión de datos para garantizar la eficiencia y el tiempo real de las actualizaciones de estado. Algunas técnicas de optimización, como la interpolación de diferencias, la predicción y la compensación, se pueden utilizar para reducir la latencia y suavizar los cambios de estado, proporcionando una mejor experiencia de juego.

3: El siguiente ejemplo se realiza utilizando la estrategia de sincronización de cuadros.

Este servidor de ejemplo utiliza la solución de sincronización de red de terceros colyseus y el cliente utiliza cocos Creator2.4.8

Tenga cuidado de asegurarse de que la versión de Colyseus en el servidor sea compatible con la versión de Colyseus en el cliente; de ​​lo contrario, es fácil causar errores debido a la incompatibilidad de la versión. Para informes de errores específicos, puede leer mi artículo anterior: Razones comunes del error de Colyseus informes.

No entraré en detalles sobre cómo presentar Colyseus, solo búscalo en el sitio web oficial.

a: El cliente procesa la clase de mensaje del servidor Colyseus:

import Player from "../../server/src/rooms/entity/Player";
import { MyRoomState } from "../../server/src/rooms/schema/MyRoomState";
import BallPlayer from "./BallPlayer";
import BallGameData, { FrameData, FrameDataItem, FrameListData, ballGameData } from "./GameData";
import { gameManager } from "./GameManager";
import BaseComp from "./common/ui/baseComp";
import { deepClone } from "./common/utils/util";

const { ccclass, property } = cc._decorator;

/**
 * 
 * 
 * 帧同步和状态同步结合的方式进行同步
 * 
 * 帧同步:连接建立完毕后 frameIndex = 0 每隔一段时间,服务器广播当前帧的状态,客户端收到后,更新当前帧的状态
 * 
 */
@ccclass("NetworkManager")
export default class NetworkManager extends BaseComp {

    @property hostname = "localhost";
    @property port = 2567;
    @property useSSL = false;

    public client: Colyseus.Client = null;
    public room: Colyseus.Room<MyRoomState> = null;

    /** 客户端缓存的所有帧数据 */
    public frameList: FrameListData[] = [];

    private serverFrameRate: number = 20;

    /** 默认16ms的帧速率 */
    public frameSpeed = 16;

    /** 服务端帧插值 */
    private serverFrameAcc: number = 3;

    /** 当前游戏进行到了第几帧 */
    private _frameIndex: number = 0;

    private lastTickTimeOut: any = null;

    public get frameIndex() {
        return this._frameIndex;
    }

    private set frameIndex(f: number) {
        this._frameIndex = f;
    }

    /** 房间是否初始化完毕 */
    public roomIsInit: boolean = false;

    private interval: any = null;

    __preload(): void {
        this.openFilter = true;
        super.__preload();
        gameManager.networkManager = this;
    }

    onLoad() {
        const url = `${this.useSSL ? "wss" : "ws"}://${this.hostname}${([443, 80].includes(this.port) || this.useSSL) ? "" : `:${this.port}`}`;
        console.log("url si ", url);
        // @ts-ignore
        this.client = new Colyseus.Client(url);
        console.log("client is ", this.client);
        // @ts-ignore
        console.log("colyseus version is ", Colyseus.VERSION);
        this.connect();
    }

    async connect() {
        try {
            console.log("joinOrCreate is ", this.client.joinOrCreate);
            this.room = await this.client.joinOrCreate<MyRoomState>("my_room");
            console.log("room is ", this.room);
            console.log(this.room.state.playerMap);
            /** 停止游戏 */
            cc.game.pause();

            this.room.send("link", { frame: this.frameIndex });
            this.roomIsInit = true;
            // 向服务端询问游戏所有帧数据
            this.room.send("all_frames");
            this.interval = setInterval(this.sendCmd.bind(this), 1000 / 60);

            this.room.state.playerMap.onAdd = (player, sessionId) => {
                console.log("player added ", player, " sessionId is ", sessionId);
                return function () {
                    return true;
                }
            };

            this.room.onStateChange((state) => {
                console.log('state change is ', state);
                this.updatePlayerInfo(state.playerMap);
            });

            // this.room.state.playerMap.onChange = (player, sessionId) => {

            // }

            this.room.onLeave((code) => {
                console.log('leave is ', code);
            });

            this.room.onMessage("*", this._messageHandler.bind(this));

        } catch (e) {
            console.log("e is ", e);
        }
    }

    /** 客户端以特定时间发送指令集 */
    sendCmd() {
        if (!this.roomIsInit) return;
        const player = gameManager.playerManager.playerMap.get(ballGameData.selfSesssioId);
        if (!player) return;
        // 获取当前帧的指令集
        const cmd = gameManager.joystickManager.getCmd();
        if (cmd) {
            this.room.send("cmd", cmd);
        }
    }

    /** 更新玩家信息 */
    updatePlayerInfo(players: any) {
        players.forEach((v, k) => {
            if (v) {
                if (v.id == ballGameData.selfSesssioId) {
                    // 更新自己的位置
                    return;
                }
                let player = gameManager.playerManager.playerMap.get(v.id);
                if (!player) {
                    gameManager.playerManager.pushPlayer(v.id);
                    player = gameManager.playerManager.playerMap.get(v.id);
                }
                const playerComp = player.getComponent(BallPlayer);
                playerComp.updatePlayerInfo(v);
            }
        })
    }

    /**
     * 服务端消息
     * @param  {any} type
     * @param  {any} message
     */
    _messageHandler(type: any, message: any) {
        switch (type) {
            case "joinSuccess":
                ballGameData.selfSesssioId = message;
                if (gameManager.playerManager.playerMap.get(ballGameData.selfSesssioId)) return;
                gameManager.playerManager.pushPlayer(ballGameData.selfSesssioId);
                break;
            case "f":
                // 当前帧的数据 服务器每隔50ms发送一次 客户端需要进行补帧操作
                this.receiveFrameData(message);
                break;
            case "all_frames":
                /** 游戏服务器上的所有帧数据 */
                this.receiveAllFrameData(message);
                // 执行自己的游戏循环
                this.nextTick();
                break;
            case "bye":
                break;
        }
    }

    /**
     * 接收服务器的帧数据
     */
    receiveFrameData(data: Array<any>) {
        for (let i = 0, len = data.length; i < len; i++) {
            const frameIndex = data[0];
            this.frameList[frameIndex] = data[1] as FrameListData;
            if (!data[1]) {
                this.frameList[frameIndex] = [];
            }
            // 客户端对帧数据进行补帧操作 预测
            this.prediction(frameIndex, this.frameList[frameIndex]);
        }
    }

    /**
     * 接收服务端所有的帧数据
     * @param  {Array<Array<any>>} data 帧数据数组
     */
    receiveAllFrameData(data: Array<Array<any>>) {
        for (let i = 0, len = data.length; i < len; i++) {
            const frameIndex = data[i][0];
            this.frameList[frameIndex] = data[i][1] as FrameListData;
            if (!data[i][1]) {
                this.frameList[frameIndex] = [];
            }
            // 客户端对帧数据进行补帧操作 预测
            this.prediction(frameIndex, this.frameList[frameIndex]);
        }
        console.log("服务端所有帧数据:", this.frameList);
    }

    /**
     * 预测 补帧
     * @param  {number} frameIndex 当前帧的索引
     * @param  {FrameListData} serverFrameData 当前帧的数据
     */
    prediction(frameIndex: number, frameData: FrameListData) {
        for (let i = 1; i <= this.serverFrameAcc - 1; i++) {
            if (!this.frameList[frameIndex + i]) {
                this.frameList[frameIndex + i] = frameData;
            }
        }
    }

    /**
     * 执行当前帧
     */
    runTick() {
        let frame = null;
        if (this.frameList.length > 1) {
            frame = this.frameList[this.frameIndex];
        }

        // 如果frame 为null的话 说明当前帧没有数据 服务端也没有,说明客户端的帧快于服务端
        if (frame) {
            // console.log(`帧索引:${this.frameIndex}, 帧数据:${frame}`);
            if (frame.length > 0) {
                frame.forEach((item: FrameData) => {
                    const player = gameManager.playerManager.playerMap.get(item.id);
                    const actionData = item.data;
                    if (player) {
                        const playerComp = player.getComponent(BallPlayer);
                        playerComp.updatePlayerFromServer(actionData);
                    }
                });
            }
            // 前进帧
            this.frameIndex++;
            cc.game.step();
        } else {
            // console.warn("没有帧数据:", frame);
        }

    }

    nextTick() {
        this.lastTickTimeOut && clearTimeout(this.lastTickTimeOut);
        this.runTick();
        if (this.frameList.length - this.frameIndex > 100) {
            console.log("追帧...");
            this.frameSpeed = 0;
        } else if (this.frameList.length - this.frameIndex > 3) {
            this.frameSpeed = 0;
        } else {
            this.frameSpeed = 1000 / (this.serverFrameRate * (this.serverFrameAcc + 1));
        }
        // 16ms调用一次nextTick()
        this.lastTickTimeOut = setTimeout(this.nextTick.bind(this), this.frameSpeed);
    }

    start() {

    }

    update(dt) {

    }

    onDestroy(): void {
        clearInterval(this.interval);
    }
}

b: La clase de control del jugador es responsable de registrar las operaciones del jugador para cargarlas en el servidor:

import BallGameData, { ballGameData } from "./GameData";
import { gameManager } from "./GameManager";
import { eventManager } from "./common/managers/eventManager";
import BaseComp from "./common/ui/baseComp";
import { angleToRand, clamp, randToAngle } from "./common/utils/util";

const { ccclass, property } = cc._decorator;

@ccclass
export default class JoyStickManager extends BaseComp {

    private Handle: cc.Sprite = null;

    /** 当前摇杆的方向 */
    private _dir: cc.Vec2 = cc.v2(0);

    private _width: number = 0;
    private _height: number = 0;

    /** 当前摇杆的角度值 */
    private _angle: number = 0;

    /** 是否在移动 */
    private isMoving: boolean = false;

    public set angle(a: number) {
        this._angle = a;
    }

    public get angle() {
        return this._angle;
    }

    /** 获得当前摇杆的方向 */
    public get dir(): cc.Vec2 {
        return this._dir;
    }

    private set dir(d: cc.Vec2) {
        this._dir = d;
    }

    __preload(): void {
        this.openFilter = true;
        super.__preload();
        gameManager.joystickManager = this;
    }

    /*** 获取客户端玩家的所有指令集 */
    getCmd(): any {
        return { dir: this.dir, angle: this.angle, moving: this.isMoving };
    }

    onLoad() {
        this.Handle.node.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this);
        this.Handle.node.on(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
        this.Handle.node.on(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this);
        this.Handle.node.on(cc.Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this);

        this._width = this.node.width;
        this._height = this.node.height;

    }

    private onTouchStart(event: cc.Event.EventTouch) {
        let touchPos = this.node.convertToNodeSpaceAR(event.getLocation());
        let dir = touchPos;
        this.dir = dir.normalize();
    }

    private onTouchMove(event: cc.Event.EventTouch) {
        this.isMoving = true;
        let touchPos = this.node.convertToNodeSpaceAR(event.getLocation());
        let dir = touchPos;
        this.dir = dir.normalize();

        const cocosAngle = this.dir.angle(cc.v2(0, 1));

        // v1 dot v2 > 0 同向  v1 dot v2 < 0 反向
        // v1 cross v2 > 0 同侧 v1 cross v2 < 0 反侧
        let targetAngle = randToAngle(cocosAngle);
        let resAngle = targetAngle;
        if (cc.v2(0, 1).clone().cross(this.dir) > 0) {
            // 在y轴的左侧
            // eventManager.emit("updateDir", { angle: targetAngle, dir: this.dir });
            resAngle = targetAngle;
        } else {
            // 在y轴的右侧
            // eventManager.emit("updateDir", { angle: -targetAngle, dir: this.dir });
            resAngle = -targetAngle;
        }
        this.angle = resAngle;

        const angle = this.dir.angle(cc.v2(1, 0));
        const R = this._width / 2;
        const maxX = R * Math.cos(angleToRand(this.angle + 90));
        const maxY = R * Math.sin(angleToRand(this.angle + 90));

        if (Math.abs(touchPos.x) > maxX) {
            touchPos.x = maxX;
        }

        if (Math.abs(touchPos.y) > maxY) {
            touchPos.y = maxY;
        }

        this.Handle.node.setPosition(touchPos);
    }

    private onTouchEnd(event: cc.Event.EventTouch) {
        console.log("end...");
        this.Handle.node.setPosition(cc.v2(0));
        // this.dir = cc.v2(0);
        this.isMoving = false;

        gameManager.playerManager.stop(ballGameData.selfSesssioId);
        // gameManager.networkManager.room.send("stop", { dir: { x: this.dir.x, y: this.dir.y } });
    }

    start() {

    }

    // update (dt) {}
}

c: El jugador realiza un procesamiento lógico después de recibir el mensaje del socket:

import Player from "../../server/src/rooms/entity/Player";
import { FrameDataItem } from "./GameData";
import BaseComp from "./common/ui/baseComp";

const { ccclass, property } = cc._decorator;

@ccclass
export default class BallPlayer extends BaseComp {

    private candy: cc.Sprite = null;
    private username: cc.Label = null;

    speed: number = 200;

    private _dir: cc.Vec2 = cc.v2(0);
    private _moving: boolean = false;

    public get dir() {
        return this._dir;
    }

    public set dir(d: cc.Vec2) {
        this._dir = d;
    }

    public set moving(m: boolean) {
        this._moving = m;
    }

    public get moving() {
        return this._moving;
    }

    __preload() {
        this.openFilter = true;
        super.__preload();
    }

    onLoad() {

    }

    start() {

    }

    /** 更新玩家信息 */
    updatePlayerInfo(player: Player) {
        this.moving = false;
        this.node.scale = player.scale;
        this.node.angle = player.angle;
        this.dir = cc.v2(player.dir.x, player.dir.y);

        if (cc.v2(player.x, player.y).clone().sub(cc.v2(this.node.x, this.node.y)).mag() > 5) {
            console.log("tween...");
            // tween动画
            cc.tween(this.node).to(0.05, { x: player.x, y: player.y }).start();
        } else {
            this.node.x = player.x;
            this.node.y = player.y;
        }

    }

    /** 执行服务端的帧数据 ***/
    updatePlayerFromServer(data: FrameDataItem) {
        const angle = data.angle;
        this.dir = cc.v2(data.dir.x, data.dir.y);
        this.moving = data.moving;
        this.node.angle = angle;

        if (this.moving) {
            this.node.x += this.dir.x * this.speed * (16 / 1000);
            this.node.y += this.dir.y * this.speed * (16 / 1000);
        }

    }

    update(dt) {

    }
}

 Lógica de la sala de servidores:


import { Room, Client, Delayed } from "@colyseus/core";
import { MyRoomState } from "./schema/MyRoomState";
import { IncomingMessage } from "http";
import Player, { Vec2 } from "./entity/Player";
import { MessageType } from "./models/ServerData";

const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

/** 是否是帧同步 */
const isFrameSync: boolean = true;

/**
 * 
 * 1: mongo存储用户信息
 * 2: 进游戏之前,先登录
 * 
 */

export class MyRoom extends Room<MyRoomState> {
  maxClients = 4;
  patchRate: number = 20;

  /** 当前游戏进行的帧数 */
  public frame_index: number = 0;
  /** 帧间隔 服务端计时器 */
  public frame_interval: Delayed = null;
  /** 游戏中帧列表 */
  public frame_list: Array<Array<{ id: string, data: any }>> = [];
  /** 帧插值 服务端发送 0,3,6,9隔帧发送减少带宽压力 客户端负责补帧 */
  public frame_acc: number = 3;

  onCreate(options: any) {
    console.log("create..");
    /** 启动时钟 */
    this.clock.start();

    this.setState(new MyRoomState());
    /** 初始化游戏进行中的帧数 */
    this.resetFrameInfo();

    this.setPatchRate(20);
    if (isFrameSync) {
      this.setSimulationInterval(this.update.bind(this), 1000 / 60);
    }

    this.frame_interval = this.clock.setInterval(this._tick.bind(this), 50);
    // this.broadcastPatch();
    this.onMessage("*", this._messageHandler.bind(this));
  }

  private _tick() {
    const curFrame = this.getFrameByIndex(this.frame_index);
    this.broadcast("f", [this.frame_index, curFrame]);
    this.frame_index += this.frame_acc;
  }

  private getFrameByIndex(index: number) {
    if (!this.frame_list[index]) {
      this.frame_list[index] = [];
    }
    return this.frame_list[index];
  }

  resetFrameInfo() {
    this.frame_index = 0;
    this.frame_list = [];
  }

  generateRoomIdSingle(): string {
    let result = "";
    for (let i = 0; i < 4; i++) {
      result += LETTERS.charAt(Math.floor(Math.random() * LETTERS.length));
    }
    return result;
  }

  /**
   * 收到客户端消息
   * @param  {Client} client 客户端
   * @param  {any} type 消息类型
   * @param  {any} message 消息内容
   */
  private _messageHandler(client: Client, type: any, message: any) {
    const sessionId = client.sessionId;
    const playerId = client.id;

    console.log("sessionId is ", sessionId);
    switch (type) {
      case MessageType.MOVE:
        this.movePlayer(playerId, message);
        break;
      case MessageType.STOP:
        this.stopPlayer(playerId);
        break;
      case MessageType.LINK:
        this.state.currentFrame = message.frame;
        break;
      case MessageType.CMD:
        this.onCmd(playerId, message);
        break;
      case MessageType.ALLFRAMES:
        this.onGetAllFrames(client, message);
        break;
      default:
        break;
    }
  }

  /**
   * 获得服务器上的所有游戏帧,并且发给客户端 让客户端补帧
   * @param  {Client} client 客户端
   * @param  {any} message 消息内容
   */
  onGetAllFrames(client: Client, message: any) {
    let frames: any = [];
    for (let i = 0, len = this.frame_list.length; i < len; i++) {
      if (this.frame_list[i]) {
        frames.push([i, this.frame_list[i]]);
      }
    }
    if (this.frame_list.length == 0) {
      frames.push([0, []]);
    }

    this.send(client, MessageType.ALLFRAMES, frames);
  }

  /**
   * 客户端发过来的指令
   * @param  {string} id 客户端的id
   * @param  {any} data 指令内容
   */
  onCmd(id: string, data: any) {
    console.log(`客户端id: ${id}: 操作指令:`, data);
    // 存在同一帧情况下,多个客户端同时发送指令的情况
    this.pushFrameList({ id, data });
  }


  /**
   * 向帧列表中添加数据
   * @param  {
   
   {id:string,data: any}} data
   */
  private pushFrameList(data: { id: string, data: any }) {
    if (!this.frame_list[this.frame_index]) {
      this.frame_list[this.frame_index] = [];
    }
    const selfIsCurrentFrame = this.frame_list[this.frame_index].find(item => item.id == data.id);
    if (!selfIsCurrentFrame)
      this.frame_list[this.frame_index].push(data);
  }

  movePlayer(id: string, data: { dir: { x: number, y: number }, angle: number, pos: { x: number, y: number } }) {
    const player = this.state.playerMap.get(id);
    if (!player) return;

    // 客户端传进来的方向向量
    player.dir = new Vec2(data.dir.x, data.dir.y);
    player.angle = data.angle;
    player.isMoving = true;
  }

  stopPlayer(id: string) {
    const player = this.state.playerMap.get(id);
    if (!player) return;

    player.isMoving = false;
  }

  onAuth(client: Client<any>, options: any, request?: IncomingMessage) {
    console.log("onAuth");
    return true;
  }

  onJoin(client: Client, options: any) {
    console.log(client.sessionId, "joined!");

    this.send(client, "joinSuccess", client.id);
    // 存储用户
    this.state.playerMap.set(client.id, new Player(client.id));
  }

  onLeave(client: Client, consented: boolean) {
    console.log(client.sessionId, "left!");
    this.send(client, "bye", { id: client.id });
  }

  onDispose() {
    console.log("room", this.roomId, "disposing...");
    this.frame_interval.clear();
    this.resetFrameInfo();
  }

  update(dt: number) {
    if (!this.state) return;
    if (this.state.playerMap && this.state.playerMap.size === 0) return;

    const interval = dt / 1000;
    for (let player of this.state.playerMap.values()) {
      player.update(interval);
    }
  }
}

Supongo que te gusta

Origin blog.csdn.net/lck8989/article/details/131247543
Recomendado
Clasificación