CocosCreator+Tsrpc实现的房间多人在线状态同步全栈项目解决方案

237 阅读4分钟

介绍

没有写很多功能,先上一张功能介绍图,感兴趣的朋友可以在此项目的基础上做升级和拓展,源码在文章结尾。

image.png

效果图如下

1739268050907压缩后.gif

交互流程

image.png

node+tsrpc后端

后端服务的搭建这里不做很多描述,可以看下官方的文档,搭建很简单,该文章的功能也是借鉴tsrpc作者的这篇文章思路实现的。我的需求没有那么高要求所以只做了状态同步的部分。

这是我完整代码工程的目录结构,以下会根据业务讲解代码的逻辑。

在这里插入图片描述

1、定义类型

GameState.ts文件中定义,分别是 玩家的输入房间内玩家状态当前玩家信息

在这里插入图片描述

2、定义协议

PtlJoin.ts该文件定义好后启动项目会生成ApiJoin.ts 在这里插入图片描述

ApiJoin.ts协议最终的代码。

在这里插入图片描述

3、房间类业务逻辑

Room类中joinRoom()方法实现了玩家加入房间的功能,包括房间状态的初始化、玩家信息的存储、状态广播以及监听玩家输入等。通过这些步骤,确保了每个玩家能够顺利加入房间并开始游戏。 在这里插入图片描述

代码解释

1、成员变量: rooms:一个对象,键为 roomId,值为 RoomState 类型的对象。用于存储不同房间的状态。 conns:一个数组,存储所有连接到服务器的 WebSocket 连接对象 (WsConnection<ServiceType>)

2、joinRoom()加入房间方法:

  • 参数: request: ReqJoin: 包含玩家请求加入房间的信息,如 roomId playerIdconn: WsConnection<ServiceType>: 当前玩家的 WebSocket 连接对象。
  • 功能: 1)检查是否存在指定的 roomId,如果不存在则创建一个新的房间状态 { players: {} }。 2)设置当前连接的 playerId roomId 属性。 3)将当前连接添加到 conns 数组中。 4)初始化新玩家的状态(位置、缩放比例、名称),并将其添加到房间的 players 对象中。 5)创建一个 CurrentPlayer 对象,包含房间 ID、玩家 ID 和玩家名称。 6)调用 broadcastGameState 方法广播最新的游戏状态给所有玩家。 7)调用 onMessageInput 方法监听玩家输入。 8)返回一个包含成功标志、房间内所有玩家信息、房间 ID 和当前玩家 ID 的响应对象。
  • 返回值: success: 布尔值,表示操作是否成功。 players: 当前房间内的所有玩家信息。 roomId: 玩家加入的房间 ID。 currentPlayerId: 当前加入房间的玩家 ID。

以下onmessageInput()为监听客户端玩家移动的输入,broadcastGameState为广播最新的房间状态,就是将房间内玩家的位置信息广播给该房间的所有人。

在这里插入图片描述

以下是离开房间的方法

在这里插入图片描述

最后在入口文件index.ts文件中调用。

在这里插入图片描述 这里服务端完成了。

CocosCreator客户端

在这里插入图片描述

这里主要直接贴核心脚本代码 GameController.ts 代码都有注释,感兴趣的朋友可以瞅瞅

import { _decorator, Component, find, instantiate, Node, Prefab, tween, Tween, Vec3 } from 'cc';
import { PlayerController } from './player/PlayerController';
import { VirtualJoystick } from './input/VirtualJoystick';
import { CameraController } from './camera/CameraController';
import { network } from './global/NetworkManager';
import { MsgRoomState } from './tsrpc/shared/protocols/room/MsgRoomState';
import { RoomStateType } from './tsrpc/shared/protocols/room/RoomStateType';
import { RoomState, PlayerInput, CurrentPlayer } from './tsrpc/shared/protocols/player/GameState';
import { PlayerAnimState } from './player/PlayerAnimState';
const { ccclass, property } = _decorator;

@ccclass('GameController')
export class GameController extends Component {

    @property(Prefab)
    playerPrefab:Prefab = null; // 玩家预制体
    player:Node = null;

    @property(Prefab)
    virtualJoystickPrefab:Prefab = null; // 摇杆预制体
    virtualJoystick:Node = null;

    @property(CameraController)
    camera:CameraController = null; // 相机控制器

    // 当前玩家ID
    playerId:string = "";

    // 缓动列表
    _tweens: Tween<any>[] = [];

    onLoad() {
        // 加载摇杆
        if(this.virtualJoystickPrefab){
            this.virtualJoystick = instantiate(this.virtualJoystickPrefab);
            this.virtualJoystick.parent = find("Canvas/UI");
        }
        this.joinRoom();
    }

    /**
     * 加入房间
     */
    joinRoom(){
        this.playerId = Math.random().toString(36).substr(2, 9); // 生成唯一玩家ID
        network.ws.callApi("room/Join", { roomId: "99999999", playerId:this.playerId }).then(res => {
            console.log(res);
            if(!res.isSucc) return;
            // 创建房间内已存在的其它玩家
            const players = res.res.players.players;
            if(players){
                for(let key in players){
                    let other = players[key];
                    if(key == this.playerId){
                        continue;
                    }
                    let otherPlayer:CurrentPlayer = {
                        roomId: res.res.roomId,
                        playerId: key,
                        playerName: other.name
                    };
                    let input:PlayerInput = {
                        playerId: key,
                        scale: other.scale,
                        pos: { x: other.x, y: other.y }
                    }
                    this.joinState(otherPlayer,input);
                }
            }

            // 创建自己
            let currentPlayer:CurrentPlayer = {
                roomId: res.res.roomId,
                playerId: res.res.currentPlayerId,
                playerName: res.res.currentPlayerName
            };
            this.joinState(currentPlayer,null);
            // 监听房间状态
            this.onMessageRoomState();
        });
    }

    /**
     * 监听房间状态
     */
    onMessageRoomState(){
        network.ws.listenMsg("room/RoomState",(res:MsgRoomState)=>{
            // console.log(res);
            if(res.type === RoomStateType.JOIN_STATE){
                this.joinState(res.data as CurrentPlayer,null);
            }else if(res.type === RoomStateType.LEAVE_STATE){
                this.leaveState(res.data as CurrentPlayer);
            }else if(res.type === RoomStateType.INPUT_STATE){
                this.updatePlayersPos(res.data as RoomState);
            }
        })
    }

    /**
     * 加入房间创建玩家
     * @param player 
     */
    joinState(player:CurrentPlayer,input?:PlayerInput){
        // 加载玩家角色
        if(this.playerPrefab){
            let playerNode = instantiate(this.playerPrefab);
            playerNode.name = player.playerId;
            playerNode.setPosition(1, 1);

            let PNode = playerNode.getComponent(PlayerController);
            PNode.nickname.string = "玩家" + player.playerId;
            PNode.playerId = player.playerId;
            PNode.roomId = player.roomId;
            if(player.playerId === this.playerId){
                // 绑定摇杆
                PNode.virtualJoystick = this.virtualJoystick.getComponent(VirtualJoystick);
                // 相机跟随
                if(this.camera){
                    this.camera.player = playerNode;
                }
            }
            
            // 其它玩家初始化位置和方向
            if(input){
                playerNode.setPosition(input.pos.x, input.pos.y);
                PNode.spine.node.setScale(input.scale,1);
            }
            playerNode.parent = this.node;
        }
    }

    /**
     * 离开房间
     * @param player 
     */
    leaveState(player:CurrentPlayer){
        let playerNode = this.node.children.find(node => node.name === player.playerId);
        if(playerNode){
            playerNode.destroy();
        }
    }

    /**
     * 更新其它玩家的状态
     * @param input 
     */
    updatePlayersPos(input: RoomState) {
        for (let key in input.players) {
            let player = input.players[key];
            let playerNode = this.node.children.find(node => node.name === key);
            
            if (playerNode && playerNode.name !== this.playerId) {
                let playerControl = playerNode.getComponent(PlayerController);
                let playerSpine = playerControl.spine;
                let pos = playerNode.getPosition();
                        
                // 旧的位置
                let oldPos = new Vec3(pos.x, pos.y);
                // 新的位置
                let newPos = new Vec3(player.x, player.y);
                
                if(!oldPos.equals(newPos)){
                    // 停止当前所有tween动画
                    this._tweens?.forEach(v => v.stop());
                    this._tweens = [];
                    playerSpine.node.setScale(player.scale,1);

                    // 获取 Spine 动画状态
                    let animationState = playerSpine.getState();

                    // 如果当前没有播放 "Move" 动画,才切换
                    if (!animationState || animationState.getCurrent(0) === null || animationState.getCurrent(0).animation.name !== PlayerAnimState.WALK) {
                        playerSpine.setAnimation(0, PlayerAnimState.WALK, true);
                    }

                    // 启动tween动画平滑过渡到新位置
                    this._tweens.push(
                        tween(playerNode)
                            .to(0.1, { position: newPos }) // 平滑过渡到新位置,持续 0.1 秒
                            .call(() => {
                                // 当tween完成时,设置为 "Idle" 动画
                                if (!animationState || animationState.getCurrent(0) === null || animationState.getCurrent(0).animation.name !== PlayerAnimState.IDLE) {
                                    playerSpine.setAnimation(0, PlayerAnimState.IDLE, true); // 切换到 IDLE 动画
                                }
                            })
                            .start() // 启动tween动画
                    );
                    // 更新旧位置为新的位置
                    playerNode.setPosition(newPos);
                }
            }
        }
        this.updatePlayerLayers();
    }

    // 按y坐标更新层级
    updatePlayerLayers() {
        // 获取所有玩家节点(可以根据实际情况调整)
        let allPlayers = this.node.children;

        // 按 Y 坐标排序玩家节点
        allPlayers.sort((a, b) => b.position.y - a.position.y); // 高的在前,低的在后

        // 更新排序后的节点层级
        allPlayers.forEach((player, index) => {
            player.setSiblingIndex(index); // 根据排序后的索引更新层级
        });
    }
}

完整代码可看这里,有不足的地方欢迎指正,一起沟通和交流。