介绍
没有写很多功能,先上一张功能介绍图,感兴趣的朋友可以在此项目的基础上做升级和拓展,源码在文章结尾。
效果图如下
交互流程
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
和playerId
。conn
: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); // 根据排序后的索引更新层级
});
}
}
完整代码可看这里,有不足的地方欢迎指正,一起沟通和交流。