Compare commits

...

10 Commits

Author SHA1 Message Date
janing
8f58b890be 接入ResLogin.otherPlayers 2025-12-18 16:04:56 +08:00
janing
03276fe1f6 ResLogin增加房间内其他玩家数据用于初始化 2025-12-18 16:04:11 +08:00
janing
4b3b94a450 同步位置放大1000倍取整 2025-12-18 13:43:28 +08:00
janing
637978357f 地图尺寸改小,同步位置放大1000倍取整 2025-12-18 13:43:16 +08:00
janing
e677b1e11b 新增CameraController,现在项目可以正常运作了。 2025-12-18 13:27:22 +08:00
janing
90175a1665 sync-shared.js脚本优化,现在先清空后拷贝。 2025-12-18 13:26:59 +08:00
janing
2dbb1e8d05 NetWebSocketClinet 2025-12-18 13:26:28 +08:00
janing
eb38cc9217 更换ccc-devtools版本,这个版本没有拦截键盘监听的问题。 2025-12-18 13:26:00 +08:00
janing
fb940452db 重构NetManager支持MessagePair,新增TSRPCWsClient。 2025-12-18 13:25:04 +08:00
janing
c12e439add 重构Api接口到Msg 2025-12-18 12:03:03 +08:00
96 changed files with 3224 additions and 1288 deletions

View File

@@ -0,0 +1,301 @@
[
{
"__type__": "cc.Prefab",
"_name": "PlayerInfo",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "PlayerInfo",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
}
],
"_active": true,
"_components": [
{
"__id__": 8
},
{
"__id__": 10
}
],
"_prefab": {
"__id__": 12
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 8388608,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "text",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
},
{
"__id__": 5
}
],
"_prefab": {
"__id__": 7
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 8388608,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"_contentSize": {
"__type__": "cc.Size",
"width": 42.255859375,
"height": 50.4
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "d0A/8x7mpNx5EvjYIoaCay"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 6
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_string": "label",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 20,
"_fontSize": 20,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": false,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": false,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "e3hL8BSnhFGJfx1WiaCfWA"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "1bkkKqEyFO7LsTGxIKuStm",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 9
},
"_contentSize": {
"__type__": "cc.Size",
"width": 100,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "f4PBh8n8NKX506YXxKc3Tp"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 11
},
"_alignFlags": 18,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "79mURIYGtLP6O8CvZNsGga"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "c46/YsCPVOJYA4mWEpNYRx",
"instance": null,
"targetOverrides": null
}
]

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "c8472648-43f2-4b7c-8881-e5461d212d58",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "PlayerInfo"
}
}

View File

@@ -1,7 +1,7 @@
[ [
{ {
"__type__": "cc.Prefab", "__type__": "cc.Prefab",
"_name": "UIWorld", "_name": "UIGame",
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"_native": "", "_native": "",
@@ -13,7 +13,7 @@
}, },
{ {
"__type__": "cc.Node", "__type__": "cc.Node",
"_name": "UIWorld", "_name": "UIGame",
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"_parent": null, "_parent": null,
@@ -51,7 +51,7 @@
"z": 1 "z": 1
}, },
"_mobility": 0, "_mobility": 0,
"_layer": 1073741824, "_layer": 8388608,
"_euler": { "_euler": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 0, "x": 0,
@@ -105,7 +105,7 @@
"z": 1 "z": 1
}, },
"_mobility": 0, "_mobility": 0,
"_layer": 1073741824, "_layer": 8388608,
"_euler": { "_euler": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 0, "x": 0,
@@ -137,8 +137,8 @@
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 31.078, "x": 0,
"y": -21.978, "y": 0,
"z": 0 "z": 0
}, },
"_lrot": { "_lrot": {
@@ -155,7 +155,7 @@
"z": 1 "z": 1
}, },
"_mobility": 0, "_mobility": 0,
"_layer": 1073741824, "_layer": 8388608,
"_euler": { "_euler": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 0, "x": 0,
@@ -178,7 +178,7 @@
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 42.255859375, "width": 54.462890625,
"height": 50.4 "height": 50.4
}, },
"_anchorPoint": { "_anchorPoint": {
@@ -214,7 +214,7 @@
"b": 255, "b": 255,
"a": 255 "a": 255
}, },
"_string": "label", "_string": "Game",
"_horizontalAlign": 1, "_horizontalAlign": 1,
"_verticalAlign": 1, "_verticalAlign": 1,
"_actualFontSize": 20, "_actualFontSize": 20,
@@ -313,7 +313,7 @@
"__prefab": { "__prefab": {
"__id__": 12 "__id__": 12
}, },
"_alignFlags": 9, "_alignFlags": 18,
"_target": null, "_target": null,
"_left": 0, "_left": 0,
"_right": 0, "_right": 0,

View File

@@ -8,6 +8,6 @@
], ],
"subMetas": {}, "subMetas": {},
"userData": { "userData": {
"syncNodeName": "UIWorld" "syncNodeName": "UIGame"
} }
} }

View File

@@ -259,7 +259,7 @@
}, },
{ {
"__type__": "cc.Node", "__type__": "cc.Node",
"_name": "Plane", "_name": "Game",
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"_parent": { "_parent": {
@@ -326,8 +326,8 @@
"_prefab": null, "_prefab": null,
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 640, "x": 360,
"y": 360, "y": 719.9999999999999,
"z": 0 "z": 0
}, },
"_lrot": { "_lrot": {
@@ -412,7 +412,7 @@
"_priority": 1073741824, "_priority": 1073741824,
"_fov": 45, "_fov": 45,
"_fovAxis": 0, "_fovAxis": 0,
"_orthoHeight": 360, "_orthoHeight": 719.9999999999999,
"_near": 1, "_near": 1,
"_far": 2000, "_far": 2000,
"_color": { "_color": {
@@ -456,8 +456,8 @@
"__prefab": null, "__prefab": null,
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 1280, "width": 720,
"height": 720 "height": 1439.9999999999998
}, },
"_anchorPoint": { "_anchorPoint": {
"__type__": "cc.Vec2", "__type__": "cc.Vec2",

View File

@@ -1,7 +1,10 @@
import { find } from "cc"; import { find } from "cc";
import { BaseState } from "../../Framework/FSM/BaseState"; import { BaseState } from "../../Framework/FSM/BaseState";
import { NetConfig, NetProtocolType } from "../../Framework/Net/NetConfig";
import { NetEvent } from "../../Framework/Net/NetEvent";
import { NetManager } from "../../Framework/Net/NetManager"; import { NetManager } from "../../Framework/Net/NetManager";
import { UIMgr } from "../../Framework/UI/UIMgr"; import { UIMgr } from "../../Framework/UI/UIMgr";
import { serviceProto } from "../../Shared/protocols/serviceProto";
/** /**
* 应用启动状态 * 应用启动状态
@@ -47,14 +50,53 @@ export class AppStatusBoot extends BaseState {
private async initAndConnectNet(): Promise<void> { private async initAndConnectNet(): Promise<void> {
console.log("[AppStatusBoot] 初始化网络管理器..."); console.log("[AppStatusBoot] 初始化网络管理器...");
// TODO: 从配置文件读取服务器地址 // 1. 获取网络管理器实例
// import { serviceProto } from '../../Shared/protocols/serviceProto'; const netManager = NetManager.getInstance();
// const netManager = NetManager.getInstance();
// netManager.setServiceProto(serviceProto); // 2. 设置服务协议 (必须在 init 之前调用)
// netManager.init({ serverUrl: 'http://localhost:3000' }); netManager.setServiceProto(serviceProto);
// await netManager.connect();
// 3. 监听网络事件
netManager.on(NetEvent.Connected, () => {
console.log('✅ 网络已连接');
this.onConnected();
});
netManager.on(NetEvent.Disconnected, () => {
console.log('❌ 网络已断开');
});
netManager.on(NetEvent.Reconnecting, (count: number) => {
console.log(`🔄 正在重连... (${count})`);
});
netManager.on(NetEvent.Error, (error: any) => {
console.error('⚠️ 网络错误:', error);
});
const config: NetConfig = {
serverUrl: 'http://localhost:3000', // TODO: 替换为实际服务器地址
protocolType: NetProtocolType.WebSocket,
timeout: 30000,
autoReconnect: true,
reconnectInterval: 3000,
maxReconnectTimes: 5
};
// 5. 初始化
netManager.init(config);
// 6. 连接服务器 (HttpClient 创建即可用)
const success = await netManager.connect();
if (success) {
console.log('[AppStatusBoot]✅ 网络初始化成功');
} else {
console.error('[AppStatusBoot]❌ 网络初始化失败');
}
}
private onConnected() {
console.log("[AppStatusBoot] 网络连接完成(待配置)");
} }
/** /**

View File

@@ -1,8 +1,9 @@
import { find } from "cc";
import { BaseState } from "../../Framework/FSM/BaseState"; import { BaseState } from "../../Framework/FSM/BaseState";
import { UIMgr } from "../../Framework/UI/UIMgr"; import { UIMgr } from "../../Framework/UI/UIMgr";
import { MsgResLogin, PlayerInfo } from "../../Shared/protocols/MsgResLogin";
import { UIGame } from "../Game/UIGame"; import { UIGame } from "../Game/UIGame";
import { World } from "../Game/World"; import { World } from "../Game/World";
import { PlayerInfo } from "../../Shared/protocols/PtlLogin";
/** /**
* 应用游戏状态 * 应用游戏状态
@@ -13,6 +14,7 @@ import { PlayerInfo } from "../../Shared/protocols/PtlLogin";
* - 游戏主循环 * - 游戏主循环
*/ */
export class AppStatusGame extends BaseState { export class AppStatusGame extends BaseState {
private _cmd: MsgResLogin
private _player: PlayerInfo = null; private _player: PlayerInfo = null;
private _isNewPlayer: boolean = false; private _isNewPlayer: boolean = false;
private _uiGame: UIGame = null; private _uiGame: UIGame = null;
@@ -24,10 +26,11 @@ export class AppStatusGame extends BaseState {
/** /**
* 进入游戏状态 * 进入游戏状态
*/ */
async onEnter(params?: any): Promise<void> { async onEnter(params?: MsgResLogin): Promise<void> {
super.onEnter(params); super.onEnter(params);
console.log("[AppStatusGame] 进入游戏世界"); console.log("[AppStatusGame] 进入游戏世界", params);
this._cmd = params
// 保存玩家信息 // 保存玩家信息
if (params) { if (params) {
@@ -80,13 +83,13 @@ export class AppStatusGame extends BaseState {
} }
// 获取世界根节点 // 获取世界根节点
const worldRoot = this._uiGame.getWorldRoot(); const worldRoot = find("Game")
if (!worldRoot) { if (!worldRoot) {
throw new Error("世界根节点未找到"); throw new Error("世界根节点未找到");
} }
// 初始化世界,传入本地玩家信息 // 初始化世界,传入本地玩家信息
await World.getInstance().init(worldRoot, this._player); await World.getInstance().init(worldRoot, this._player, this._cmd.otherPlayers);
console.log("[AppStatusGame] 游戏初始化完成"); console.log("[AppStatusGame] 游戏初始化完成");
} }
@@ -116,7 +119,10 @@ export class AppStatusGame extends BaseState {
* 更新游戏状态(每帧调用) * 更新游戏状态(每帧调用)
*/ */
onUpdate(dt: number): void { onUpdate(dt: number): void {
// TODO: 游戏主循环逻辑 // 更新世界状态,包括玩家信息显示
World.getInstance().update(dt);
// TODO: 其他游戏主循环逻辑
// - 更新角色位置 // - 更新角色位置
// - 检测碰撞 // - 检测碰撞
// - 更新敌人AI // - 更新敌人AI

View File

@@ -0,0 +1,264 @@
import { _decorator, Camera, Component, find, Node, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
/**
* CameraController 摄像机控制器
* 负责让摄像机跟随指定的目标物体,并提供摄像机坐标转换功能
* 支持世界摄像机和UI摄像机的概念
*/
@ccclass('CameraController')
export class CameraController extends Component {
/** 跟随目标 */
private target: Node = null;
/** 世界摄像机节点 (Main Camera) */
private worldCameraNode: Node = null;
/** 世界摄像机组件 */
private worldCamera: Camera = null;
/** UI摄像机节点 (Canvas/Camera) */
private uiCameraNode: Node = null;
/** UI摄像机组件 */
private uiCamera: Camera = null;
/** 相对偏移量 */
private offset: Vec3 = new Vec3(0, 10, 8);
/** 跟随速度 */
@property({ displayName: '跟随速度' })
public followSpeed: number = 5;
/** 是否平滑跟随 */
@property({ displayName: '平滑跟随' })
public smoothFollow: boolean = true;
onLoad() {
this.findCameras();
}
/**
* 查找所有摄像机
*/
private findCameras(): void {
this.findWorldCamera();
this.findUICamera();
}
/**
* 查找世界摄像机 (Main Camera)
*/
private findWorldCamera(): void {
// 查找名为 "Main Camera" 的世界摄像机
const mainCameraNode = find('Main Camera', this.node.scene);
if (mainCameraNode) {
this.worldCameraNode = mainCameraNode;
this.worldCamera = mainCameraNode.getComponent(Camera);
console.log('[CameraController] 找到世界摄像机:', mainCameraNode.name);
} else {
console.error('[CameraController] 未找到世界摄像机(Main Camera)');
}
}
/**
* 查找UI摄像机 (Canvas/Camera)
*/
private findUICamera(): void {
// 查找Canvas下的Camera作为UI摄像机
const canvasNode = find('Canvas', this.node.scene);
if (canvasNode) {
const uiCameraNode = canvasNode.getChildByName('Camera');
if (uiCameraNode) {
this.uiCameraNode = uiCameraNode;
this.uiCamera = uiCameraNode.getComponent(Camera);
console.log('[CameraController] 找到UI摄像机:', uiCameraNode.name);
} else {
console.error('[CameraController] 未找到Canvas下的Camera节点');
}
} else {
console.error('[CameraController] 未找到Canvas节点');
}
}
/**
* 设置跟随目标
* @param target 跟随的目标节点
*/
public setTarget(target: Node): void {
this.target = target;
if (this.target && this.worldCameraNode) {
// 立即设置初始位置
this.updateCameraPosition(false);
console.log('[CameraController] 设置跟随目标:', target.name);
}
}
/**
* 设置摄像机偏移量
* @param offset 相对于目标的偏移量
*/
public setOffset(offset: Vec3): void {
this.offset.set(offset);
}
/**
* 获取当前跟随目标
*/
public getTarget(): Node {
return this.target;
}
/**
* 获取世界摄像机节点
*/
public getWorldCameraNode(): Node {
return this.worldCameraNode;
}
/**
* 获取世界摄像机组件
*/
public getWorldCamera(): Camera {
return this.worldCamera;
}
/**
* 获取UI摄像机节点
*/
public getUICameraNode(): Node {
return this.uiCameraNode;
}
/**
* 获取UI摄像机组件
*/
public getUICamera(): Camera {
return this.uiCamera;
}
update(deltaTime: number) {
if (this.target && this.worldCameraNode) {
this.updateCameraPosition(this.smoothFollow, deltaTime);
}
}
/**
* 更新摄像机位置
* @param smooth 是否使用平滑跟随
* @param deltaTime 帧时间(smooth为true时需要)
*/
private updateCameraPosition(smooth: boolean = true, deltaTime: number = 0): void {
if (!this.target || !this.worldCameraNode) {
return;
}
// 计算目标位置
const targetPosition = new Vec3();
Vec3.add(targetPosition, this.target.worldPosition, this.offset);
if (smooth && deltaTime > 0) {
// 平滑跟随
const currentPosition = this.worldCameraNode.worldPosition;
const newPosition = new Vec3();
Vec3.lerp(newPosition, currentPosition, targetPosition, this.followSpeed * deltaTime);
this.worldCameraNode.setWorldPosition(newPosition);
} else {
// 立即跟随
this.worldCameraNode.setWorldPosition(targetPosition);
}
// 让摄像机看向目标
this.worldCameraNode.lookAt(this.target.worldPosition);
}
/**
* 停止跟随
*/
public stopFollow(): void {
this.target = null;
console.log('[CameraController] 停止跟随');
}
/**
* 将世界坐标转换为指定摄像机的屏幕坐标
* @param worldPosition 世界坐标Vec3
* @param camera 目标摄像机
* @returns 摄像机坐标系下的屏幕坐标Vec3z为深度
*/
public worldToCameraPosition(worldPosition: Vec3, camera: Camera): Vec3 {
if (!camera) {
console.warn('[CameraController] 摄像机参数为空,无法进行坐标转换');
return new Vec3(0, 0, 0);
}
// 使用摄像机的worldToScreen方法将世界坐标转换为屏幕坐标
const screenPos = new Vec3();
camera.worldToScreen(worldPosition, screenPos);
return screenPos;
}
/**
* 将摄像机屏幕坐标转换为指定Node的本地坐标
* 计算流程:摄像机坐标 -> 世界坐标 -> Node本地坐标
* @param screenPosition 摄像机屏幕坐标
* @param sourceCamera 源摄像机
* @param targetNode 目标Node节点
* @returns 目标Node的本地坐标
*/
public CameraToNode(screenPosition: Vec3, sourceCamera: Camera, targetNode: Node): Vec3 {
if (!sourceCamera || !targetNode) {
console.warn('[CameraController] 摄像机或目标节点参数为空,无法进行坐标转换');
return new Vec3(0, 0, 0);
}
// 第一步:摄像机屏幕坐标转世界坐标
const worldPosition = new Vec3();
sourceCamera.screenToWorld(screenPosition, worldPosition);
// 第二步世界坐标转Node本地坐标
const localPosition = new Vec3();
targetNode.inverseTransformPoint(localPosition, worldPosition);
return localPosition;
}
/**
* 将世界坐标转换为UI摄像机节点的本地坐标
* @param worldPosition 世界坐标
* @returns UI摄像机节点的本地坐标
*/
public worldToUICamera(worldPosition: Vec3, node: Node): Vec3 {
if (!this.worldCamera || !this.uiCameraNode) {
console.warn('[CameraController] 世界摄像机或UI摄像机节点未初始化');
return new Vec3(0, 0, 0);
}
// 第一步:世界坐标转世界摄像机屏幕坐标
const worldCameraPos = this.worldToCameraPosition(worldPosition, this.worldCamera);
// 第二步世界摄像机屏幕坐标转UI摄像机节点本地坐标
const uiCameraLocalPos = this.CameraToNode(worldCameraPos, this.uiCamera, node);
return uiCameraLocalPos;
// // 世界坐标转Node本地坐标
// const localPosition = new Vec3();
// node.inverseTransformPoint(localPosition, worldPosition);
return worldPosition;
}
/**
* 销毁控制器
*/
onDestroy(): void {
this.target = null;
this.worldCameraNode = null;
this.worldCamera = null;
this.uiCameraNode = null;
this.uiCamera = null;
console.log('[CameraController] 摄像机控制器已销毁');
}
}

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24", "ver": "4.0.24",
"importer": "typescript", "importer": "typescript",
"imported": true, "imported": true,
"uuid": "4414c606-7995-4f83-b83c-30c9fe840a6b", "uuid": "b94f3dcb-3630-4ebd-87b3-81ae3d560352",
"files": [], "files": [],
"subMetas": {}, "subMetas": {},
"userData": {} "userData": {}

View File

@@ -1,8 +1,8 @@
import { _decorator, Component, Node, EventKeyboard, KeyCode, Input, input, Vec3 } from 'cc'; import { _decorator, Component, EventKeyboard, Input, input, KeyCode, Vec3 } from 'cc';
import { NetManager } from '../../Framework/Net/NetManager';
import { ReqMove, ResMove } from '../../Shared/protocols/PtlMove';
import { PlayerInfo } from '../../Shared/protocols/PtlLogin';
import { RoleController } from '../../CC/RoleController'; import { RoleController } from '../../CC/RoleController';
import { NetManager } from '../../Framework/Net/NetManager';
import { PlayerInfo } from '../../Shared/protocols/MsgResLogin';
import { MoveMessagePair } from '../Msg/Pair/MoveMessagePair';
const { ccclass } = _decorator; const { ccclass } = _decorator;
@@ -41,7 +41,9 @@ export class PlayerController extends Component {
*/ */
public init(playerInfo: PlayerInfo): void { public init(playerInfo: PlayerInfo): void {
this.playerInfo = playerInfo; this.playerInfo = playerInfo;
this.lastSentPosition.set(playerInfo.position.x, 0, playerInfo.position.y); const x = playerInfo.position.x / 1000
const y = playerInfo.position.y / 1000
this.lastSentPosition.set(x, y, 0);
// 获取 RoleController 组件 // 获取 RoleController 组件
this.roleController = this.node.getComponentInChildren(RoleController); this.roleController = this.node.getComponentInChildren(RoleController);
@@ -107,11 +109,11 @@ export class PlayerController extends Component {
// W - 向前 // W - 向前
if (this.keyStates.get(KeyCode.KEY_W)) { if (this.keyStates.get(KeyCode.KEY_W)) {
this.moveDirection.z -= 1; this.moveDirection.y += 1;
} }
// S - 向后 // S - 向后
if (this.keyStates.get(KeyCode.KEY_S)) { if (this.keyStates.get(KeyCode.KEY_S)) {
this.moveDirection.z += 1; this.moveDirection.y -= 1;
} }
// A - 向左 // A - 向左
if (this.keyStates.get(KeyCode.KEY_A)) { if (this.keyStates.get(KeyCode.KEY_A)) {
@@ -142,7 +144,7 @@ export class PlayerController extends Component {
// 更新朝向(让角色面向移动方向) // 更新朝向(让角色面向移动方向)
if (this.moveDirection.lengthSqr() > 0) { if (this.moveDirection.lengthSqr() > 0) {
const targetAngle = Math.atan2(this.moveDirection.x, -this.moveDirection.z) * (180 / Math.PI); const targetAngle = Math.atan2(this.moveDirection.x, -this.moveDirection.y) * (180 / Math.PI);
this.node.setRotationFromEuler(0, targetAngle, 0); this.node.setRotationFromEuler(0, targetAngle, 0);
} }
@@ -166,7 +168,7 @@ export class PlayerController extends Component {
// 如果移动距离超过阈值,发送移动请求 // 如果移动距离超过阈值,发送移动请求
if (distance >= this.moveSendThreshold) { if (distance >= this.moveSendThreshold) {
this.sendMoveRequest(currentPos.x, currentPos.z); this.sendMoveRequest(currentPos.x, currentPos.y);
this.lastSentPosition.set(currentPos); this.lastSentPosition.set(currentPos);
} }
} }
@@ -174,12 +176,12 @@ export class PlayerController extends Component {
/** /**
* 发送移动请求到服务器 * 发送移动请求到服务器
*/ */
private async sendMoveRequest(x: number, z: number): Promise<void> { private async sendMoveRequest(x: number, y: number): Promise<void> {
try { try {
const netManager = NetManager.getInstance(); const netManager = NetManager.getInstance();
const result = await netManager.callApi<ReqMove, ResMove>('Move', { const result = await netManager.callMsg(new MoveMessagePair(), {
x: x, x: Math.round(x * 1000),
y: z y: Math.round(y * 1000)
}); });
if (!result) { if (!result) {
@@ -190,8 +192,10 @@ export class PlayerController extends Component {
// 服务器可能会修正位置,使用服务器返回的位置 // 服务器可能会修正位置,使用服务器返回的位置
if (result.position) { if (result.position) {
const serverPos = result.position; const serverPos = result.position;
this.node.setPosition(serverPos.x, 0, serverPos.y); const x = serverPos.x / 1000
this.lastSentPosition.set(serverPos.x, 0, serverPos.y); const y = serverPos.y / 1000
this.node.setPosition(x, y, 0);
this.lastSentPosition.set(x, y, 0);
} }
} catch (error) { } catch (error) {
console.error('[PlayerController] 发送移动请求异常:', error); console.error('[PlayerController] 发送移动请求异常:', error);

View File

@@ -0,0 +1,88 @@
import { _decorator, Component, Label, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
export interface PlayerInfoData {
/** 玩家ID */
playerId: string;
/** 玩家名称 */
playerName: string;
/** 屏幕坐标位置用于设置UI位置 */
screenPosition: Vec3;
/** 真实位置(用于显示到文本中) */
realPosition: Vec3;
}
/**
* PlayerInfo 玩家信息显示组件
* 显示玩家名称和坐标信息
*/
@ccclass('PlayerInfo')
export class PlayerInfo extends Component {
/** 文本显示组件 */
private textLabel: Label = null;
/** 当前玩家数据 */
private playerData: PlayerInfoData = null;
onLoad() {
// 查找text子节点的Label组件
const textNode = this.node.getChildByName('text');
if (textNode) {
this.textLabel = textNode.getComponent(Label);
if (!this.textLabel) {
console.error('[PlayerInfo] text子节点没有Label组件');
}
} else {
console.error('[PlayerInfo] 找不到text子节点');
}
}
/**
* 更新玩家信息
* @param playerData 玩家数据
*/
public updatePlayerInfo(playerData: PlayerInfoData): void {
this.playerData = playerData;
if (!this.textLabel) {
console.error('[PlayerInfo] 文本组件未初始化');
return;
}
// 格式化显示文本:玩家名和真实坐标信息
const displayText = `${playerData.playerName}\n坐标: (${Math.round(playerData.realPosition.x)}, ${Math.round(playerData.realPosition.y)})`;
this.textLabel.string = displayText;
// 设置UI位置为屏幕坐标
this.node.setPosition(playerData.screenPosition);
//console.log(`[PlayerInfo] 更新玩家信息: ${playerData.playerName}, 屏幕坐标: (${playerData.screenPosition.x}, ${playerData.screenPosition.y}), 真实坐标: (${playerData.realPosition.x}, ${playerData.realPosition.y})`);
}
/**
* 获取当前玩家数据
*/
public getPlayerData(): PlayerInfoData {
return this.playerData;
}
/**
* 隐藏玩家信息
*/
public hide(): void {
this.node.active = false;
}
/**
* 显示玩家信息
*/
public show(): void {
this.node.active = true;
}
onDestroy() {
this.textLabel = null;
this.playerData = null;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -11,6 +11,7 @@ App/Game/
├── World.ts # 世界管理器,管理所有玩家 ├── World.ts # 世界管理器,管理所有玩家
├── PlayerController.ts # 本地玩家控制器,处理输入和移动 ├── PlayerController.ts # 本地玩家控制器,处理输入和移动
├── RemotePlayer.ts # 远程玩家类,处理其他玩家的显示和同步 ├── RemotePlayer.ts # 远程玩家类,处理其他玩家的显示和同步
├── CameraController.ts # 摄像机控制器,跟随本地玩家
├── UIGame.ts # 游戏主界面UI ├── UIGame.ts # 游戏主界面UI
└── README.md # 本文档 └── README.md # 本文档
``` ```
@@ -82,7 +83,37 @@ await World.getInstance().init(worldRoot, playerInfo);
- `updatePosition(position)`: 更新远程玩家位置(收到 MsgPlayerMove 时调用) - `updatePosition(position)`: 更新远程玩家位置(收到 MsgPlayerMove 时调用)
- `destroy()`: 销毁远程玩家 - `destroy()`: 销毁远程玩家
### 4. UIGame (游戏主界面) ### 4. CameraController (摄像机控制器)
**职责:**
- 自动查找场景中的主摄像机(Main Camera)
- 让摄像机跟随指定的目标物体(通常是本地玩家)
- 提供平滑跟随和即时跟随两种模式
- 自动让摄像机朝向目标
**主要特性:**
- **自动查找**: 自动在场景中查找名为"Main Camera"的摄像机节点
- **平滑跟随**: 使用线性插值实现平滑的摄像机跟随效果
- **偏移设置**: 可配置摄像机相对于目标的偏移量(默认: Y+10, Z+8)
- **朝向目标**: 摄像机会自动朝向跟随目标
**主要方法:**
- `setTarget(target)`: 设置跟随目标节点(本地玩家)
- `setOffset(offset)`: 设置摄像机相对偏移量
- `stopFollow()`: 停止跟随
- `getTarget()`: 获取当前跟随目标
- `getCameraNode()`: 获取摄像机节点
**属性配置:**
- `followSpeed`: 跟随速度(默认: 5)
- `smoothFollow`: 是否启用平滑跟随(默认: true)
**使用说明:**
- CameraController 由 World 管理器自动创建
- 只有本地玩家会绑定摄像机跟随,远程玩家不会
- 摄像机控制器会挂载到世界根节点上
### 5. UIGame (游戏主界面)
**职责:** **职责:**
- 游戏主界面 UI 组件 - 游戏主界面 UI 组件

View File

@@ -1,6 +1,6 @@
import { Node, Vec3, Tween, tween } from 'cc'; import { Node, Tween, tween, Vec3 } from 'cc';
import { Position } from '../../Shared/protocols/base';
import { RoleController } from '../../CC/RoleController'; import { RoleController } from '../../CC/RoleController';
import { Position } from '../../Shared/protocols/base';
/** /**
* RemotePlayer 远程玩家 * RemotePlayer 远程玩家
@@ -38,8 +38,10 @@ export class RemotePlayer {
this.playerNode = playerNode; this.playerNode = playerNode;
this.playerId = playerId; this.playerId = playerId;
this.playerName = playerName; this.playerName = playerName;
this.currentPosition.set(position.x, 0, position.y); const x = position.x / 1000
this.targetPosition.set(position.x, 0, position.y); const y = position.y / 1000
this.currentPosition.set(x, y, 0);
this.targetPosition.set(x, y, 0);
// 获取 RoleController 组件 // 获取 RoleController 组件
this.roleController = this.playerNode.getComponentInChildren(RoleController); this.roleController = this.playerNode.getComponentInChildren(RoleController);
@@ -58,7 +60,7 @@ export class RemotePlayer {
* 收到服务器广播的移动消息时调用 * 收到服务器广播的移动消息时调用
*/ */
public updatePosition(position: Position): void { public updatePosition(position: Position): void {
this.targetPosition.set(position.x, 0, position.y); this.targetPosition.set(position.x / 1000, 0, position.y / 1000);
// 计算移动方向和距离 // 计算移动方向和距离
const direction = this.targetPosition.clone().subtract(this.currentPosition); const direction = this.targetPosition.clone().subtract(this.currentPosition);
@@ -77,7 +79,7 @@ export class RemotePlayer {
// 计算朝向 // 计算朝向
if (distance > 0.01) { if (distance > 0.01) {
const targetAngle = Math.atan2(direction.x, -direction.z) * (180 / Math.PI); const targetAngle = Math.atan2(direction.x, -direction.y) * (180 / Math.PI);
this.playerNode.setRotationFromEuler(0, targetAngle, 0); this.playerNode.setRotationFromEuler(0, targetAngle, 0);
} }

View File

@@ -1,6 +1,7 @@
import { _decorator, Component, Node } from 'cc'; import { _decorator, instantiate, Node, Prefab } from 'cc';
import { ResMgr } from '../../Framework/ResMgr/ResMgr';
import { UIBase } from '../../Framework/UI/UIBase'; import { UIBase } from '../../Framework/UI/UIBase';
import { World } from './World'; import { PlayerInfo, PlayerInfoData } from './PlayerInfo';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;
@@ -10,36 +11,120 @@ const { ccclass, property } = _decorator;
*/ */
@ccclass('UIGame') @ccclass('UIGame')
export class UIGame extends UIBase { export class UIGame extends UIBase {
@property(Node) /** 玩家信息显示容器 */
worldRoot: Node = null; private playerInfoContainer: Node = null;
protected onLoad(): void { /** 玩家信息预制体 */
console.log('[UIGame] onLoad'); private playerInfoPrefab: Prefab = null;
/** 当前显示的玩家信息组件Map<playerId, PlayerInfo> */
private playerInfoComponents: Map<string, PlayerInfo> = new Map();
async onStart() {
this.initPlayerInfoContainer();
await this.loadPlayerInfoPrefab();
} }
protected onEnable(): void { onEnd(): void {
console.log('[UIGame] onEnable'); this.clearPlayerInfoComponents();
}
protected onDisable(): void {
console.log('[UIGame] onDisable');
}
protected onDestroy(): void {
console.log('[UIGame] onDestroy');
} }
/** /**
* 获取 UI 预制体路径 * 获取 UI 预制体路径
*/ */
public onGetUrl(): string { public onGetUrl(): string {
return 'res://UI/UIGame'; return 'res://UI/Game/UIGame';
} }
/** /**
* 获取世界根节点 * 初始化玩家信息容器
*/ */
public getWorldRoot(): Node { private initPlayerInfoContainer(): void {
return this.worldRoot; // 查找或创建玩家信息容器
this.playerInfoContainer = this._node.getChildByName('PlayerInfoContainer');
if (!this.playerInfoContainer) {
this.playerInfoContainer = new Node('PlayerInfoContainer');
this._node.addChild(this.playerInfoContainer);
} }
console.log('[UIGame] 玩家信息容器已初始化');
}
/**
* 加载玩家信息预制体
*/
private async loadPlayerInfoPrefab(): Promise<void> {
try {
this.playerInfoPrefab = await ResMgr.getInstance().load('res', 'UI/Game/PlayerInfo', Prefab);
console.log('[UIGame] 玩家信息预制体加载成功');
} catch (error) {
console.error('[UIGame] 加载玩家信息预制体失败:', error);
}
}
/**
* 更新玩家信息显示
* @param playerDataList 玩家数据列表
*/
public updatePlayerInfo(playerDataList: PlayerInfoData[]): void {
if (!this.playerInfoPrefab || !this.playerInfoContainer) {
console.error('[UIGame] 玩家信息预制体或容器未准备好');
return;
}
// 记录当前更新的玩家ID
const currentPlayerIds = new Set<string>();
for (const playerData of playerDataList) {
currentPlayerIds.add(playerData.playerId);
let playerInfoComponent = this.playerInfoComponents.get(playerData.playerId);
if (!playerInfoComponent) {
// 创建新的玩家信息组件
const playerInfoNode = instantiate(this.playerInfoPrefab);
this.playerInfoContainer.addChild(playerInfoNode);
playerInfoComponent = playerInfoNode.getComponent(PlayerInfo);
if (!playerInfoComponent) {
playerInfoComponent = playerInfoNode.addComponent(PlayerInfo);
}
this.playerInfoComponents.set(playerData.playerId, playerInfoComponent);
console.log('[UIGame] 创建玩家信息组件:', playerData.playerName);
}
// 更新玩家信息
playerInfoComponent.updatePlayerInfo(playerData);
}
// 移除不再存在的玩家信息
for (const [playerId, playerInfoComponent] of this.playerInfoComponents.entries()) {
if (!currentPlayerIds.has(playerId)) {
playerInfoComponent.node.destroy();
this.playerInfoComponents.delete(playerId);
console.log('[UIGame] 移除玩家信息组件:', playerId);
}
}
}
/**
* 清理所有玩家信息组件
*/
private clearPlayerInfoComponents(): void {
for (const [playerId, playerInfoComponent] of this.playerInfoComponents.entries()) {
if (playerInfoComponent && playerInfoComponent.node) {
playerInfoComponent.node.destroy();
}
}
this.playerInfoComponents.clear();
// 释放预制体资源
if (this.playerInfoPrefab) {
ResMgr.getInstance().release('resources', 'res://UI/Game/PlayerInfo');
this.playerInfoPrefab = null;
}
console.log('[UIGame] 已清理所有玩家信息组件');
}
} }

View File

@@ -1,11 +1,15 @@
import { Node, Vec3, instantiate, Prefab } from 'cc'; import { instantiate, Node, Prefab } from 'cc';
import { NetManager } from '../../Framework/Net/NetManager'; import { NetManager } from '../../Framework/Net/NetManager';
import { ResMgr } from '../../Framework/ResMgr/ResMgr'; import { ResMgr } from '../../Framework/ResMgr/ResMgr';
import { UIMgr } from '../../Framework/UI/UIMgr';
import { MsgPlayerJoin } from '../../Shared/protocols/MsgPlayerJoin'; import { MsgPlayerJoin } from '../../Shared/protocols/MsgPlayerJoin';
import { MsgPlayerMove } from '../../Shared/protocols/MsgPlayerMove'; import { MsgPlayerMove } from '../../Shared/protocols/MsgPlayerMove';
import { PlayerInfo } from '../../Shared/protocols/PtlLogin'; import { PlayerInfo } from '../../Shared/protocols/MsgResLogin';
import { CameraController } from './CameraController';
import { PlayerController } from './PlayerController'; import { PlayerController } from './PlayerController';
import { PlayerInfoData } from './PlayerInfo';
import { RemotePlayer } from './RemotePlayer'; import { RemotePlayer } from './RemotePlayer';
import { UIGame } from './UIGame';
/** /**
* World 世界管理器 * World 世界管理器
@@ -33,7 +37,13 @@ export class World {
/** 玩家模型预制体 */ /** 玩家模型预制体 */
private playerPrefab: Prefab = null; private playerPrefab: Prefab = null;
private constructor() {} /** 摄像机控制器 */
private cameraController: CameraController = null;
/** UIGame实例 */
private uiGameInstance: UIGame = null;
private constructor() { }
public static getInstance(): World { public static getInstance(): World {
if (!World.instance) { if (!World.instance) {
@@ -46,11 +56,15 @@ export class World {
* 初始化世界 * 初始化世界
* @param worldRoot 世界根节点 * @param worldRoot 世界根节点
* @param localPlayer 本地玩家信息 * @param localPlayer 本地玩家信息
* @param otherPlayers 其他在线玩家信息
*/ */
public async init(worldRoot: Node, localPlayer: PlayerInfo): Promise<void> { public async init(worldRoot: Node, localPlayer: PlayerInfo, otherPlayers?: PlayerInfo[]): Promise<void> {
this.worldRoot = worldRoot; this.worldRoot = worldRoot;
this.localPlayer = localPlayer; this.localPlayer = localPlayer;
// 获取UIGame实例
this.uiGameInstance = UIMgr.getInstance().get(UIGame) as UIGame;
// 加载玩家模型预制体 // 加载玩家模型预制体
await this.loadPlayerPrefab(); await this.loadPlayerPrefab();
@@ -60,6 +74,9 @@ export class World {
// 创建本地玩家 // 创建本地玩家
await this.createLocalPlayer(); await this.createLocalPlayer();
// 创建其他已在线的玩家
await this.createOtherPlayers(otherPlayers);
console.log('[World] 世界初始化完成'); console.log('[World] 世界初始化完成');
} }
@@ -68,7 +85,7 @@ export class World {
*/ */
private async loadPlayerPrefab(): Promise<void> { private async loadPlayerPrefab(): Promise<void> {
try { try {
this.playerPrefab = await ResMgr.getInstance().load('resources', 'res://Actor/M1/M1', Prefab); this.playerPrefab = await ResMgr.getInstance().load('res', 'Actor/M1/M1', Prefab);
console.log('[World] 玩家模型预制体加载成功'); console.log('[World] 玩家模型预制体加载成功');
} catch (error) { } catch (error) {
console.error('[World] 加载玩家模型预制体失败:', error); console.error('[World] 加载玩家模型预制体失败:', error);
@@ -103,19 +120,76 @@ export class World {
return; return;
} }
const x = this.localPlayer.position.x / 1000
const y = this.localPlayer.position.y / 1000
// 实例化玩家节点 // 实例化玩家节点
this.localPlayerNode = instantiate(this.playerPrefab); this.localPlayerNode = instantiate(this.playerPrefab);
this.localPlayerNode.name = `Player_${this.localPlayer.id}_Local`; this.localPlayerNode.name = `Player_${this.localPlayer.id}_Local`;
this.localPlayerNode.setPosition(this.localPlayer.position.x, 0, this.localPlayer.position.y); this.localPlayerNode.setPosition(x, y, 0);
this.worldRoot.addChild(this.localPlayerNode); this.worldRoot.addChild(this.localPlayerNode);
// 创建本地玩家控制器 // 创建本地玩家控制器
this.localPlayerController = this.localPlayerNode.addComponent(PlayerController); this.localPlayerController = this.localPlayerNode.addComponent(PlayerController);
this.localPlayerController.init(this.localPlayer); this.localPlayerController.init(this.localPlayer);
// 创建并绑定摄像机控制器(只有本地玩家需要)
this.cameraController = this.worldRoot.addComponent(CameraController) as CameraController;
this.cameraController.setTarget(this.localPlayerNode);
console.log('[World] 本地玩家创建完成:', this.localPlayer.name); console.log('[World] 本地玩家创建完成:', this.localPlayer.name);
} }
/**
* 创建其他已在线的玩家
*/
private async createOtherPlayers(otherPlayers?: PlayerInfo[]): Promise<void> {
if (!otherPlayers || otherPlayers.length === 0) {
console.log('[World] 无其他在线玩家');
return;
}
if (!this.playerPrefab) {
console.error('[World] 玩家模型预制体未加载,无法创建其他玩家');
return;
}
console.log('[World] 开始创建其他在线玩家,数量:', otherPlayers.length);
for (const playerInfo of otherPlayers) {
// 跳过本地玩家(双重保险)
if (playerInfo.id === this.localPlayer.id) {
continue;
}
// 检查是否已存在
if (this.remotePlayers.has(playerInfo.id)) {
console.warn('[World] 远程玩家已存在:', playerInfo.id);
continue;
}
try {
// 实例化玩家节点
const playerNode = instantiate(this.playerPrefab);
playerNode.name = `Player_${playerInfo.id}_Remote`;
const x = playerInfo.position.x / 1000;
const y = playerInfo.position.y / 1000;
playerNode.setPosition(x, y, 0);
this.worldRoot.addChild(playerNode);
// 创建远程玩家控制器
const remotePlayer = new RemotePlayer();
remotePlayer.init(playerNode, playerInfo.id, playerInfo.name, playerInfo.position);
this.remotePlayers.set(playerInfo.id, remotePlayer);
console.log('[World] 其他玩家创建完成:', playerInfo.name);
} catch (error) {
console.error('[World] 创建其他玩家失败:', playerInfo.name, error);
}
}
}
/** /**
* 处理玩家加入消息 * 处理玩家加入消息
*/ */
@@ -149,7 +223,9 @@ export class World {
// 实例化玩家节点 // 实例化玩家节点
const playerNode = instantiate(this.playerPrefab); const playerNode = instantiate(this.playerPrefab);
playerNode.name = `Player_${msg.playerId}_Remote`; playerNode.name = `Player_${msg.playerId}_Remote`;
playerNode.setPosition(msg.position.x, 0, msg.position.y); const x = msg.position.x / 1000
const y = msg.position.y / 1000
playerNode.setPosition(x, y, 0);
this.worldRoot.addChild(playerNode); this.worldRoot.addChild(playerNode);
// 创建远程玩家控制器 // 创建远程玩家控制器
@@ -184,6 +260,61 @@ export class World {
return this.localPlayerController; return this.localPlayerController;
} }
/**
* 更新所有玩家信息显示
*/
public updateAllPlayersInfo(): void {
if (!this.uiGameInstance || !this.cameraController) {
return;
}
const uinode = this.uiGameInstance.getNode()
const playerDataList: PlayerInfoData[] = [];
// 添加本地玩家信息
if (this.localPlayerNode && this.localPlayer) {
// 直接使用节点的世界坐标
const worldPos = this.localPlayerNode.worldPosition;
// 正确的坐标转换流程:世界坐标 -> 世界摄像机坐标 -> UI摄像机坐标
const uiScreenPos = this.cameraController.worldToUICamera(worldPos, uinode);
playerDataList.push({
playerId: this.localPlayer.id,
playerName: this.localPlayer.name,
screenPosition: uiScreenPos,
realPosition: worldPos // 使用世界坐标作为真实位置
});
}
// 添加远程玩家信息
for (const [playerId, remotePlayer] of this.remotePlayers.entries()) {
const remotePlayerNode = remotePlayer.getPlayerNode();
if (remotePlayerNode) {
const worldPos = remotePlayerNode.worldPosition;
// 正确的坐标转换流程:世界坐标 -> 世界摄像机坐标 -> UI摄像机坐标
const uiScreenPos = this.cameraController.worldToUICamera(worldPos, uinode);
playerDataList.push({
playerId: playerId,
playerName: remotePlayer.getPlayerName(),
screenPosition: uiScreenPos,
realPosition: worldPos // 使用世界坐标作为真实位置
});
}
}
// 更新UI显示
this.uiGameInstance.updatePlayerInfo(playerDataList);
}
/**
* 在update中定期更新玩家信息显示
*/
public update(deltaTime: number): void {
// 每帧更新玩家信息显示
this.updateAllPlayersInfo();
}
/** /**
* 销毁世界 * 销毁世界
*/ */
@@ -198,6 +329,12 @@ export class World {
} }
this.localPlayerController = null; this.localPlayerController = null;
// 销毁摄像机控制器
if (this.cameraController) {
this.cameraController.node.removeComponent(CameraController);
this.cameraController = null;
}
// 销毁所有远程玩家 // 销毁所有远程玩家
this.remotePlayers.forEach((remotePlayer) => { this.remotePlayers.forEach((remotePlayer) => {
remotePlayer.destroy(); remotePlayer.destroy();

View File

@@ -1,8 +1,8 @@
import { _decorator, EditBox, Button } from 'cc'; import { _decorator, Button, EditBox } from 'cc';
import { UIBase } from '../../Framework/UI/UIBase';
import { NetManager } from '../../Framework/Net/NetManager'; import { NetManager } from '../../Framework/Net/NetManager';
import { ReqLogin, ResLogin } from '../../Framework/Net/LoginProtocol'; import { UIBase } from '../../Framework/UI/UIBase';
import { AppStatusManager } from '../AppStatus/AppStatusManager'; import { AppStatusManager } from '../AppStatus/AppStatusManager';
import { LoginMessagePair } from '../Msg/Pair/LoginMessagePair';
const { ccclass } = _decorator; const { ccclass } = _decorator;
@@ -88,19 +88,16 @@ export class UILogin extends UIBase {
const netManager = NetManager.getInstance(); const netManager = NetManager.getInstance();
// 使用类型化的登录协议 // 使用类型化的登录协议
const result = await netManager.callApi<ReqLogin, ResLogin>('Login', { const result = await netManager.callMsg(new LoginMessagePair(), {
account: account playerId: account
}); },);
if (result && result.success) { if (result && result.success) {
console.log('[UILogin] 登录成功:', result); console.log('[UILogin] 登录成功:', result);
// 登录成功,切换到游戏状态 // 登录成功,切换到游戏状态
const appStatusManager = AppStatusManager.getInstance(); const appStatusManager = AppStatusManager.getInstance();
appStatusManager.changeState('Game', { appStatusManager.changeState('Game', result);
player: result.player,
isNewPlayer: result.isNewPlayer || false
});
// 隐藏登录界面 // 隐藏登录界面
this.hide(); this.hide();

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "9f8d42c2-cee9-4156-b0ee-992cb5d66317",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,76 @@
/**
* 消息对定义基类
* 用于定义成对出现的请求和响应消息
*/
export abstract class MessagePair<TReq, TRes> {
/** 请求消息名称 */
abstract readonly requestName: string;
/** 响应消息名称 */
abstract readonly responseName: string;
/** 请求消息类型检查 */
abstract isValidRequest(msg: any): msg is TReq;
/** 响应消息类型检查 */
abstract isValidResponse(msg: any): msg is TRes;
}
/**
* 消息对注册表
* 用于管理所有的消息对
*/
export class MessagePairRegistry {
private static _instance: MessagePairRegistry;
private _pairs: Map<string, MessagePair<any, any>> = new Map();
static getInstance(): MessagePairRegistry {
if (!this._instance) {
this._instance = new MessagePairRegistry();
}
return this._instance;
}
/**
* 注册消息对
* @param pair 消息对实例
*/
register(pair: MessagePair<any, any>): void {
this._pairs.set(pair.requestName, pair);
console.log(`[MessagePairRegistry] Registered message pair: ${pair.requestName} -> ${pair.responseName}`);
}
/**
* 根据请求消息名获取响应消息名
* @param requestName 请求消息名
* @returns 响应消息名如果未找到返回null
*/
getResponseName(requestName: string): string | null {
const pair = this._pairs.get(requestName);
return pair ? pair.responseName : null;
}
/**
* 根据请求消息名获取消息对
* @param requestName 请求消息名
* @returns 消息对实例如果未找到返回null
*/
getPair(requestName: string): MessagePair<any, any> | null {
return this._pairs.get(requestName) || null;
}
/**
* 检查是否为已注册的请求消息
* @param msgName 消息名
*/
isRegisteredRequest(msgName: string): boolean {
return this._pairs.has(msgName);
}
/**
* 获取所有已注册的消息对
*/
getAllPairs(): MessagePair<any, any>[] {
return Array.from(this._pairs.values());
}
}

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24", "ver": "4.0.24",
"importer": "typescript", "importer": "typescript",
"imported": true, "imported": true,
"uuid": "9fdf30ed-1223-4112-81f6-75339229fd1b", "uuid": "3ba46d91-073d-44d6-8157-05f1af758121",
"files": [], "files": [],
"subMetas": {}, "subMetas": {},
"userData": {} "userData": {}

View File

@@ -0,0 +1,23 @@
import { MessagePairRegistry } from './MessagePairBase';
import { LoginMessagePair } from './Pair/LoginMessagePair';
import { MoveMessagePair } from './Pair/MoveMessagePair';
import { SendMessagePair } from './Pair/SendMessagePair';
/**
* 消息对初始化
* 在应用启动时调用此函数注册所有消息对
*/
export function initMessagePairs(): void {
const registry = MessagePairRegistry.getInstance();
// 注册登录消息对
registry.register(new LoginMessagePair());
// 注册移动消息对
registry.register(new MoveMessagePair());
// 注册发送消息对
registry.register(new SendMessagePair());
console.log('[MessagePairs] All message pairs registered successfully');
}

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24", "ver": "4.0.24",
"importer": "typescript", "importer": "typescript",
"imported": true, "imported": true,
"uuid": "db7f0511-7e71-4088-9822-186f68082db6", "uuid": "9b552413-09a9-4ec6-b389-ea371b734a41",
"files": [], "files": [],
"subMetas": {}, "subMetas": {},
"userData": {} "userData": {}

View File

@@ -0,0 +1,84 @@
import { initMessagePairs } from './MessagePairInit';
import { MsgManager } from './MsgManager';
/**
* 消息系统使用示例
*/
export class MsgExample {
/**
* 初始化消息系统
* 应在游戏启动时调用
*/
static init(): void {
// 初始化消息对注册表
initMessagePairs();
console.log('[MsgExample] Message system initialized');
}
/**
* 登录示例
*/
static async loginExample(): Promise<void> {
const msgMgr = MsgManager.getInstance();
try {
const response = await msgMgr.login({
playerId: 'player123',
playerName: 'TestPlayer'
});
if (response && response.success) {
console.log('登录成功:', response.player);
} else {
console.log('登录失败:', response?.message);
}
} catch (error) {
console.error('登录请求异常:', error);
}
}
/**
* 移动示例
*/
static async moveExample(): Promise<void> {
const msgMgr = MsgManager.getInstance();
try {
const response = await msgMgr.move({
x: 100,
y: 200
});
if (response && response.success) {
console.log('移动成功:', response.position);
} else {
console.log('移动失败:', response?.message);
}
} catch (error) {
console.error('移动请求异常:', error);
}
}
/**
* 发送消息示例
*/
static async sendMessageExample(): Promise<void> {
const msgMgr = MsgManager.getInstance();
try {
const response = await msgMgr.send({
content: 'Hello, World!'
});
if (response) {
console.log('消息发送成功:', response.time);
} else {
console.log('消息发送失败');
}
} catch (error) {
console.error('发送消息异常:', error);
}
}
}

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24", "ver": "4.0.24",
"importer": "typescript", "importer": "typescript",
"imported": true, "imported": true,
"uuid": "b5331a2b-20fa-4653-8013-deb54bad8d2e", "uuid": "4c559445-88e3-4fe2-a87c-8c9ef390aa60",
"files": [], "files": [],
"subMetas": {}, "subMetas": {},
"userData": {} "userData": {}

View File

@@ -0,0 +1,55 @@
import { NetManager } from '../../Framework/Net/NetManager';
import { MsgReqLogin } from '../../Shared/protocols/MsgReqLogin';
import { MsgReqMove } from '../../Shared/protocols/MsgReqMove';
import { MsgReqSend } from '../../Shared/protocols/MsgReqSend';
import { MsgResLogin } from '../../Shared/protocols/MsgResLogin';
import { MsgResMove } from '../../Shared/protocols/MsgResMove';
import { MsgResSend } from '../../Shared/protocols/MsgResSend';
import { LoginMessagePair } from './Pair/LoginMessagePair';
import { MoveMessagePair } from './Pair/MoveMessagePair';
import { SendMessagePair } from './Pair/SendMessagePair';
/**
* 消息管理器 - 提供类型安全的消息发送方法
*/
export class MsgManager {
private static _instance: MsgManager;
static getInstance(): MsgManager {
if (!this._instance) {
this._instance = new MsgManager();
}
return this._instance;
}
private get netManager(): NetManager {
return NetManager.getInstance();
}
/**
* 发送登录请求
* @param loginData 登录数据
* @returns 登录响应
*/
async login(loginData: MsgReqLogin): Promise<MsgResLogin | null> {
return this.netManager.callMsg(new LoginMessagePair(), loginData);
}
/**
* 发送移动请求
* @param moveData 移动数据
* @returns 移动响应
*/
async move(moveData: MsgReqMove): Promise<MsgResMove | null> {
return this.netManager.callMsg(new MoveMessagePair(), moveData);
}
/**
* 发送消息请求
* @param sendData 发送数据
* @returns 发送响应
*/
async send(sendData: MsgReqSend): Promise<MsgResSend | null> {
return this.netManager.callMsg(new SendMessagePair(), sendData);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "871b5daf-492f-4ff7-adac-a13bbf6b82ce",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "6f3ea388-fd67-45a4-9bfc-cae858378b7a",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,21 @@
import { MsgReqLogin } from '../../../Shared/protocols/MsgReqLogin';
import { MsgResLogin } from '../../../Shared/protocols/MsgResLogin';
import { MessagePair } from '../MessagePairBase';
/**
* 登录消息对实现
*/
export class LoginMessagePair extends MessagePair<MsgReqLogin, MsgResLogin> {
readonly requestName = 'ReqLogin';
readonly responseName = 'ResLogin';
isValidRequest(msg: any): msg is MsgReqLogin {
return msg && typeof msg.playerId === 'string';
}
isValidResponse(msg: any): msg is MsgResLogin {
return msg &&
typeof msg.success === 'boolean' &&
typeof msg.message === 'string';
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8a0f8893-e078-4885-8407-70944f67d185",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,23 @@
import { MsgReqMove } from '../../../Shared/protocols/MsgReqMove';
import { MsgResMove } from '../../../Shared/protocols/MsgResMove';
import { MessagePair } from '../MessagePairBase';
/**
* 移动消息对实现
*/
export class MoveMessagePair extends MessagePair<MsgReqMove, MsgResMove> {
readonly requestName = 'ReqMove';
readonly responseName = 'ResMove';
isValidRequest(msg: any): msg is MsgReqMove {
return msg &&
typeof msg.x === 'number' &&
typeof msg.y === 'number';
}
isValidResponse(msg: any): msg is MsgResMove {
return msg &&
typeof msg.success === 'boolean' &&
typeof msg.message === 'string';
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "f95aa765-9371-4644-9694-8eb8dd7d8930",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,19 @@
import { MsgReqSend } from '../../../Shared/protocols/MsgReqSend';
import { MsgResSend } from '../../../Shared/protocols/MsgResSend';
import { MessagePair } from '../MessagePairBase';
/**
* 发送消息对实现
*/
export class SendMessagePair extends MessagePair<MsgReqSend, MsgResSend> {
readonly requestName = 'ReqSend';
readonly responseName = 'ResSend';
isValidRequest(msg: any): msg is MsgReqSend {
return msg && typeof msg.content === 'string';
}
isValidResponse(msg: any): msg is MsgResSend {
return msg && msg.time instanceof Date;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "d9ef0dad-c949-4a46-9306-d4a060a40a3b",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -1,49 +0,0 @@
/**
* 登录协议类型定义
*
* 注意: 这是临时定义,实际项目中应该通过 npm run sync-shared
* 从服务端同步完整的协议定义到 Shared 目录
*/
/**
* 登录请求
*/
export interface ReqLogin {
/** 账号 */
account: string;
/** 密码 (可选) */
password?: string;
}
/**
* 登录响应
*/
export interface ResLogin {
/** 是否成功 */
success: boolean;
/** 消息 */
message?: string;
/** 玩家信息 */
player?: {
/** 玩家ID */
id: string;
/** 玩家名称 */
name: string;
/** 位置 */
position: { x: number; y: number; z: number };
/** 出生点 */
spawnPoint: { x: number; y: number; z: number };
/** 生命值 */
hp: number;
/** 最大生命值 */
maxHp: number;
/** 是否存活 */
isAlive: boolean;
/** 创建时间 */
createdAt: number;
/** 最后登录时间 */
lastLoginAt: number;
};
/** 是否新玩家 */
isNewPlayer?: boolean;
}

View File

@@ -1,3 +1,14 @@
/**
* 网络协议类型
*/
export enum NetProtocolType {
/** HTTP/HTTPS 协议 */
Http = "http",
/** WebSocket 协议 */
WebSocket = "websocket"
}
/** /**
* 网络配置接口 * 网络配置接口
*/ */
@@ -5,6 +16,9 @@ export interface NetConfig {
/** 服务器地址 */ /** 服务器地址 */
serverUrl: string; serverUrl: string;
/** 网络协议类型 默认 Http */
protocolType?: NetProtocolType;
/** 超时时间(ms) 默认 30000 */ /** 超时时间(ms) 默认 30000 */
timeout?: number; timeout?: number;
@@ -16,14 +30,19 @@ export interface NetConfig {
/** 最大重连次数 默认 5 */ /** 最大重连次数 默认 5 */
maxReconnectTimes?: number; maxReconnectTimes?: number;
/** 是否使用JSON格式 默认 true */
json?: boolean;
} }
/** /**
* 默认网络配置 * 默认网络配置
*/ */
export const DefaultNetConfig: Partial<NetConfig> = { export const DefaultNetConfig: Partial<NetConfig> = {
protocolType: NetProtocolType.Http,
timeout: 30000, timeout: 30000,
autoReconnect: true, autoReconnect: true,
reconnectInterval: 3000, reconnectInterval: 3000,
maxReconnectTimes: 5 maxReconnectTimes: 5,
json: true
}; };

View File

@@ -0,0 +1,222 @@
import { NetConfig, NetProtocolType } from './NetConfig';
/**
* 网络配置构建器
* 提供便捷的方法来创建网络配置
*/
export class NetConfigBuilder {
private _config: Partial<NetConfig> = {};
/**
* 设置服务器地址
* @param serverUrl 服务器地址
*/
setServerUrl(serverUrl: string): NetConfigBuilder {
this._config.serverUrl = serverUrl;
return this;
}
/**
* 设置协议类型
* @param protocolType 协议类型
*/
setProtocolType(protocolType: NetProtocolType): NetConfigBuilder {
this._config.protocolType = protocolType;
return this;
}
/**
* 设置为 HTTP 协议
* @param serverUrl HTTP 服务器地址 (如: https://api.example.com/api)
*/
useHttp(serverUrl?: string): NetConfigBuilder {
this._config.protocolType = NetProtocolType.Http;
if (serverUrl) {
this._config.serverUrl = serverUrl;
}
// HTTP 通常不需要重连
this._config.autoReconnect = false;
return this;
}
/**
* 设置为 WebSocket 协议
* @param serverUrl WebSocket 服务器地址 (如: wss://ws.example.com/api)
*/
useWebSocket(serverUrl?: string): NetConfigBuilder {
this._config.protocolType = NetProtocolType.WebSocket;
if (serverUrl) {
this._config.serverUrl = serverUrl;
}
// WebSocket 默认启用重连
this._config.autoReconnect = true;
return this;
}
/**
* 设置超时时间
* @param timeout 超时时间(毫秒)
*/
setTimeout(timeout: number): NetConfigBuilder {
this._config.timeout = timeout;
return this;
}
/**
* 设置重连配置
* @param autoReconnect 是否自动重连
* @param reconnectInterval 重连间隔(毫秒)
* @param maxReconnectTimes 最大重连次数
*/
setReconnect(autoReconnect: boolean, reconnectInterval?: number, maxReconnectTimes?: number): NetConfigBuilder {
this._config.autoReconnect = autoReconnect;
if (reconnectInterval !== undefined) {
this._config.reconnectInterval = reconnectInterval;
}
if (maxReconnectTimes !== undefined) {
this._config.maxReconnectTimes = maxReconnectTimes;
}
return this;
}
/**
* 设置是否使用JSON格式
* @param json 是否使用JSON格式
*/
setJson(json: boolean): NetConfigBuilder {
this._config.json = json;
return this;
}
/**
* 构建配置
*/
build(): NetConfig {
if (!this._config.serverUrl) {
throw new Error('Server URL is required');
}
return {
serverUrl: this._config.serverUrl,
protocolType: this._config.protocolType || NetProtocolType.Http,
timeout: this._config.timeout || 30000,
autoReconnect: this._config.autoReconnect ?? true,
reconnectInterval: this._config.reconnectInterval || 3000,
maxReconnectTimes: this._config.maxReconnectTimes || 5,
json: this._config.json ?? true
};
}
/**
* 创建新的构建器实例
*/
static create(): NetConfigBuilder {
return new NetConfigBuilder();
}
}
/**
* 预定义的网络配置模板
*/
export class NetConfigTemplates {
/**
* 开发环境 HTTP 配置
*/
static developmentHttp(serverUrl: string): NetConfig {
return NetConfigBuilder.create()
.useHttp(serverUrl)
.setTimeout(10000)
.build();
}
/**
* 生产环境 HTTP 配置
*/
static productionHttp(serverUrl: string): NetConfig {
return NetConfigBuilder.create()
.useHttp(serverUrl)
.setTimeout(30000)
.build();
}
/**
* 开发环境 WebSocket 配置
*/
static developmentWebSocket(serverUrl: string): NetConfig {
return NetConfigBuilder.create()
.useWebSocket(serverUrl)
.setTimeout(10000)
.setReconnect(true, 1000, 10) // 更频繁的重连用于开发
.build();
}
/**
* 生产环境 WebSocket 配置
*/
static productionWebSocket(serverUrl: string): NetConfig {
return NetConfigBuilder.create()
.useWebSocket(serverUrl)
.setTimeout(30000)
.setReconnect(true, 3000, 5)
.build();
}
/**
* 游戏实时对战 WebSocket 配置
*/
static gameRealtimeWebSocket(serverUrl: string): NetConfig {
return NetConfigBuilder.create()
.useWebSocket(serverUrl)
.setTimeout(5000) // 短超时
.setReconnect(true, 500, 20) // 快速重连
.build();
}
}
/**
* 网络环境检测工具
*/
export class NetEnvironmentDetector {
/**
* 根据当前环境自动生成配置
* @param baseUrl 基础URL (不包含协议)
* @param isProduction 是否为生产环境
*/
static autoDetect(baseUrl: string, isProduction: boolean = false): {
http: NetConfig;
webSocket: NetConfig;
} {
const httpUrl = isProduction ? `https://${baseUrl}/api` : `http://${baseUrl}/api`;
const wsUrl = isProduction ? `wss://${baseUrl}/api` : `ws://${baseUrl}/api`;
return {
http: isProduction
? NetConfigTemplates.productionHttp(httpUrl)
: NetConfigTemplates.developmentHttp(httpUrl),
webSocket: isProduction
? NetConfigTemplates.productionWebSocket(wsUrl)
: NetConfigTemplates.developmentWebSocket(wsUrl)
};
}
/**
* 检测URL是否为安全连接
*/
static isSecureUrl(url: string): boolean {
return url.startsWith('https://') || url.startsWith('wss://');
}
/**
* 转换HTTP URL为WebSocket URL
*/
static httpToWebSocket(httpUrl: string): string {
return httpUrl.replace(/^https?:/, httpUrl.startsWith('https:') ? 'wss:' : 'ws:');
}
/**
* 转换WebSocket URL为HTTP URL
*/
static webSocketToHttp(wsUrl: string): string {
return wsUrl.replace(/^wss?:/, wsUrl.startsWith('wss:') ? 'https:' : 'http:');
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "3b215a9a-fd5d-404a-8254-dd6f3cffca6a",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -2,6 +2,9 @@
* 网络事件定义 * 网络事件定义
*/ */
export enum NetEvent { export enum NetEvent {
/** 正在连接 */
Connecting = "net_connecting",
/** 连接成功 */ /** 连接成功 */
Connected = "net_connected", Connected = "net_connected",

View File

@@ -1,16 +1,19 @@
import { NetConfig, DefaultNetConfig } from './NetConfig'; import { MessagePair, MessagePairRegistry } from '../../App/Msg/MessagePairBase';
import { DefaultNetConfig, NetConfig, NetProtocolType } from './NetConfig';
import { NetEvent } from './NetEvent'; import { NetEvent } from './NetEvent';
import { PlatformAdapter, ClientConfig } from './PlatformAdapter'; import { ClientConfig, PlatformAdapter } from './PlatformAdapter';
import { INetClient } from './WebSocketClient';
/** /**
* 网络管理器单例 * 网络管理器单例
* 负责管理网络连接和消息通信 * 负责管理网络连接和消息通信
* 支持 HTTP 和 WebSocket 两种协议
*/ */
export class NetManager { export class NetManager {
private static _instance: NetManager | null = null; private static _instance: NetManager | null = null;
/** TSRPC Client 实例 (HttpClient) */ /** TSRPC Client 实例 (HttpClient 或 WsClient) */
private _client: any = null; private _client: INetClient | null = null;
/** 是否已连接 */ /** 是否已连接 */
private _isConnected: boolean = false; private _isConnected: boolean = false;
@@ -27,7 +30,7 @@ export class NetManager {
/** 事件监听器 */ /** 事件监听器 */
private _eventListeners: Map<string, Function[]> = new Map(); private _eventListeners: Map<string, Function[]> = new Map();
private constructor() {} private constructor() { }
/** /**
* 获取单例实例 * 获取单例实例
@@ -58,6 +61,7 @@ export class NetManager {
} as NetConfig; } as NetConfig;
console.log('[NetManager] Initialized with config:', this._config); console.log('[NetManager] Initialized with config:', this._config);
console.log('[NetManager] Protocol:', this._config.protocolType);
console.log('[NetManager] Platform:', PlatformAdapter.getPlatformInfo()); console.log('[NetManager] Platform:', PlatformAdapter.getPlatformInfo());
} }
@@ -66,43 +70,53 @@ export class NetManager {
*/ */
async connect(): Promise<boolean> { async connect(): Promise<boolean> {
try { try {
if (!this._config) {
console.error('[NetManager] Config not set, please call init() first');
return false;
}
console.log('[NetManager] Connecting to server:', this._config.serverUrl); console.log('[NetManager] Connecting to server:', this._config.serverUrl);
// 创建客户端配置 // 创建客户端配置
const clientConfig: ClientConfig = { const clientConfig: ClientConfig = {
server: this._config.serverUrl, server: this._config.serverUrl,
json: true, protocolType: this._config.protocolType,
timeout: this._config.timeout json: this._config.json,
timeout: this._config.timeout,
}; };
// 根据平台创建对应的客户端 // 根据平台和协议类型创建对应的客户端
this._client = PlatformAdapter.createClient(clientConfig); this._client = PlatformAdapter.createClient(clientConfig);
// HttpClient 不需要显式连接,创建即可使用 // 如果是 WebSocket 客户端,需要显式连接
// 如果未来需要 WebSocket 支持,可以在这里添加 connect() 调用 if (this._config.protocolType === NetProtocolType.WebSocket && this._client.connect) {
this.emit(NetEvent.Connecting);
this._isConnected = true; const result = await this._client.connect();
this._reconnectCount = 0; if (!result.isSucc) {
console.error('[NetManager] WebSocket connection failed:', result.errMsg);
this.emit(NetEvent.Error, result.errMsg);
this.emit(NetEvent.Connected); // 尝试重连
console.log('[NetManager] Client created successfully'); if (this._config.autoReconnect) {
this.scheduleReconnect();
// client 可能不需要显式连接 }
// 对于 WsClient 需要调用 connect() return false;
}
}
this._isConnected = true; this._isConnected = true;
this._reconnectCount = 0; this._reconnectCount = 0;
this.emit(NetEvent.Connected); this.emit(NetEvent.Connected);
console.log('[NetManager] Connected successfully'); console.log('[NetManager] Connected successfully');
return true; return true;
} catch (error) { } catch (error) {
console.error('[NetManager] Connection failed:', error); console.error('[NetManager] Connection failed:', error);
this.emit(NetEvent.Error, error); this.emit(NetEvent.Error, error);
// 尝试重连 // 尝试重连
if (this._config.autoReconnect) { if (this._config?.autoReconnect) {
this.scheduleReconnect(); this.scheduleReconnect();
} }
@@ -114,20 +128,20 @@ export class NetManager {
* 断开连接 * 断开连接
*/ */
disconnect(): void { disconnect(): void {
if (!this._isConnected) { if (!this._isConnected || !this._client) {
return; return;
} }
// HttpClient 无需显式断开连接
// 如果使用 WebSocket可以在这里调用 disconnect()
this._isConnected = false; console.log('[NetManager] Disconnecting...');
this._client = null;
if(this._reconnectTimer){ // 清除重连定时器
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer);
this._reconnectTimer = null; this._reconnectTimer = null;
} }
// TODO: 调用客户端的断开连接方法 // 如果是 WebSocket 客户端,调用断开连接方法
if (this._client && typeof this._client.disconnect === 'function') { if (this._client.disconnect) {
this._client.disconnect(); this._client.disconnect();
} }
@@ -152,12 +166,11 @@ export class NetManager {
try { try {
console.log(`[NetManager] Calling API: ${apiName}`, req); console.log(`[NetManager] Calling API: ${apiName}`, req);
// TODO: 根据实际的协议定义调用 API
const result = await this._client.callApi(apiName, req); const result = await this._client.callApi(apiName, req);
if (result.isSucc) { if (result.isSucc) {
console.log(`[NetManager] API ${apiName} success:`, result.res); console.log(`[NetManager] API ${apiName} success:`, result.res);
return result.res; return result.res as Res;
} else { } else {
console.error(`[NetManager] API ${apiName} failed:`, result.err); console.error(`[NetManager] API ${apiName} failed:`, result.err);
return null; return null;
@@ -170,7 +183,7 @@ export class NetManager {
} }
/** /**
* 监听消息 * 监听消息 (仅支持 WebSocket)
* @param msgName 消息名称 * @param msgName 消息名称
* @param handler 处理函数 * @param handler 处理函数
*/ */
@@ -180,16 +193,91 @@ export class NetManager {
return; return;
} }
console.log(`[NetManager] Listening message: ${msgName}`); if (!this._client.listenMsg) {
console.warn('[NetManager] Message listening not supported for HTTP client');
// TODO: 根据实际的 TSRPC 客户端实现监听消息 return;
if (typeof this._client.listenMsg === 'function') {
this._client.listenMsg(msgName, handler);
} }
console.log(`[NetManager] Listening message: ${msgName}`);
this._client.listenMsg(msgName, handler);
} }
/** /**
* 发送消息 * 取消监听消息 (仅支持 WebSocket)
* @param msgName 消息名称
* @param handler 处理函数(可选,不传则取消所有该消息的监听)
*/
unlistenMsg(msgName: string, handler?: Function): void {
if (!this._client) {
console.error('[NetManager] Client not initialized');
return;
}
if (!this._client.unlistenMsg) {
console.warn('[NetManager] Message listening not supported for HTTP client');
return;
}
console.log(`[NetManager] Unlisten message: ${msgName}`);
this._client.unlistenMsg(msgName, handler);
}
/**
* 发送消息并等待响应 (基于消息对系统)
* @param messagePair 消息对实例
* @param requestData 请求消息内容
* @returns Promise<响应消息>
*/
async callMsg<TReq, TRes>(messagePair: MessagePair<TReq, TRes>, requestData: TReq): Promise<TRes | null> {
if (!this._isConnected || !this._client) {
console.error('[NetManager] Not connected');
return null;
}
const requestName = messagePair.requestName;
const responseName = messagePair.responseName;
// 验证请求消息格式
if (!messagePair.isValidRequest(requestData)) {
console.error(`[NetManager] Invalid request data for ${requestName}:`, requestData);
return null;
}
console.log(`[NetManager] Calling message: ${requestName} -> ${responseName}`, requestData);
return new Promise<TRes | null>((resolve, reject) => {
// 创建一次性响应处理器
const responseHandler = (response: TRes) => {
// 验证响应消息格式
if (!messagePair.isValidResponse(response)) {
return;
}
// 取消监听
this.unlistenMsg(responseName, responseHandler);
// 返回响应
console.log(`[NetManager] Received response for ${requestName}:`, response);
resolve(response);
};
// 监听响应
this.listenMsg(responseName, responseHandler);
// 发送请求消息
this.sendMsg(requestName, requestData);
// 设置超时处理
const timeout = this._config?.timeout || 30000;
setTimeout(() => {
this.unlistenMsg(responseName, responseHandler);
console.error(`[NetManager] Request timeout for ${requestName}`);
resolve(null);
}, timeout);
});
}
/**
* 发送消息 (仅支持 WebSocket)
* @param msgName 消息名称 * @param msgName 消息名称
* @param msg 消息内容 * @param msg 消息内容
*/ */
@@ -199,12 +287,13 @@ export class NetManager {
return; return;
} }
console.log(`[NetManager] Sending message: ${msgName}`, msg); if (!this._client.sendMsg) {
console.warn('[NetManager] Message sending not supported for HTTP client');
// TODO: 根据实际的 TSRPC 客户端实现发送消息 return;
if (typeof this._client.sendMsg === 'function') {
this._client.sendMsg(msgName, msg);
} }
console.log(`[NetManager] Sending message: ${msgName}`, msg);
this._client.sendMsg(msgName, msg);
} }
/** /**
@@ -301,7 +390,41 @@ export class NetManager {
/** /**
* 获取客户端实例 * 获取客户端实例
*/ */
get client(): any { get client(): INetClient | null {
return this._client; return this._client;
} }
/**
* 获取协议类型
*/
get protocolType(): NetProtocolType | undefined {
return this._config?.protocolType;
}
/**
* 判断是否为 WebSocket 连接
*/
get isWebSocket(): boolean {
return this._config?.protocolType === NetProtocolType.WebSocket;
}
/**
* 判断是否为 HTTP 连接
*/
get isHttp(): boolean {
return this._config?.protocolType === NetProtocolType.Http;
}
/**
* 获取所有已注册的消息对信息
* @returns 消息对列表
*/
getMessagePairs(): { requestName: string; responseName: string }[] {
const registry = MessagePairRegistry.getInstance();
return registry.getAllPairs().map(pair => ({
requestName: pair.requestName,
responseName: pair.responseName
}));
}
} }

View File

@@ -1,7 +1,9 @@
import { sys } from 'cc'; import { sys } from 'cc';
// 使用别名导入避免命名冲突 // 使用别名导入避免命名冲突
import { BaseServiceType, HttpClient as HttpClientBrowser } from 'tsrpc-browser'; import { BaseServiceType, HttpClient as HttpClientBrowser, WsClient as WsClientBrowser } from 'tsrpc-browser';
import { HttpClient as HttpClientMiniapp } from 'tsrpc-miniapp'; import { HttpClient as HttpClientMiniapp, WsClient as WsClientMiniapp } from 'tsrpc-miniapp';
import { NetProtocolType } from './NetConfig';
import { INetClient, WebSocketClientWrapper } from './WebSocketClient';
/** /**
* 平台类型枚举 * 平台类型枚举
@@ -24,6 +26,9 @@ export interface ClientConfig {
/** 服务器地址 */ /** 服务器地址 */
server: string; server: string;
/** 网络协议类型 */
protocolType?: NetProtocolType;
/** 是否使用 JSON 格式 (默认 true) */ /** 是否使用 JSON 格式 (默认 true) */
json?: boolean; json?: boolean;
@@ -36,9 +41,13 @@ export interface ClientConfig {
/** /**
* 平台适配器 * 平台适配器
* 根据当前平台返回对应的 TSRPC 客户端实现 * 根据当前平台和协议类型返回对应的 TSRPC 客户端实现
* *
* 注意: TSRPC 不同平台的库中 API 是重名的,所以使用别名导入 * 支持协议:
* - HTTP/HTTPS: 用于无状态的API调用
* - WebSocket: 用于实时双向通信
*
* 支持平台:
* - tsrpc-browser: 用于浏览器和 XMLHttpRequest 兼容的环境 * - tsrpc-browser: 用于浏览器和 XMLHttpRequest 兼容的环境
* - tsrpc-miniapp: 用于微信、抖音、QQ 等小程序环境 * - tsrpc-miniapp: 用于微信、抖音、QQ 等小程序环境
*/ */
@@ -94,11 +103,12 @@ export class PlatformAdapter {
} }
/** /**
* 创建对应平台的 TSRPC 客户端实例 * 创建对应平台和协议的 TSRPC 客户端实例
* @param config 客户端配置 * @param config 客户端配置
*/ */
static createClient<T extends BaseServiceType>(config: ClientConfig): HttpClientBrowser<T> | HttpClientMiniapp<T> { static createClient<T extends BaseServiceType>(config: ClientConfig): INetClient<T> {
const platform = this.detectPlatform(); const platform = this.detectPlatform();
const protocolType = config.protocolType || NetProtocolType.Http;
// 默认配置 // 默认配置
const defaultConfig: ClientConfig = { const defaultConfig: ClientConfig = {
@@ -115,19 +125,50 @@ export class PlatformAdapter {
console.warn('[PlatformAdapter] Service protocol not set, please call setServiceProto() first'); console.warn('[PlatformAdapter] Service protocol not set, please call setServiceProto() first');
} }
let client: HttpClientBrowser<T> | HttpClientMiniapp<T>; console.log(`[PlatformAdapter] Creating ${protocolType} client for ${platform}:`, defaultConfig.server);
// 根据平台创建对应的客户端 // 根据协议类型和平台创建对应的客户端
if (platform === PlatformType.MiniApp) { if (protocolType === NetProtocolType.WebSocket) {
console.log('[PlatformAdapter] Creating MiniApp client:', defaultConfig.server); return this.createWebSocketClient<T>(platform, defaultConfig);
client = new HttpClientMiniapp(this._serviceProto, defaultConfig) as HttpClientMiniapp<T>;
} else { } else {
// 浏览器和其他 XMLHttpRequest 兼容环境使用 Browser 客户端 return this.createHttpClient<T>(platform, defaultConfig);
console.log('[PlatformAdapter] Creating Browser client:', defaultConfig.server); }
client = new HttpClientBrowser(this._serviceProto, defaultConfig) as HttpClientBrowser<T>;
} }
return client; /**
* 创建 HTTP 客户端
*/
private static createHttpClient<T extends BaseServiceType>(platform: PlatformType, config: ClientConfig): INetClient<T> {
let client: HttpClientBrowser<T> | HttpClientMiniapp<T>;
if (platform === PlatformType.MiniApp) {
client = new HttpClientMiniapp(this._serviceProto, config) as HttpClientMiniapp<T>;
} else {
client = new HttpClientBrowser(this._serviceProto, config) as HttpClientBrowser<T>;
}
// 包装 HTTP 客户端使其符合 INetClient 接口
return {
callApi: async <Req, Res>(apiName: string, req: Req) => {
return await client.callApi(apiName, req);
}
} as INetClient<T>;
}
/**
* 创建 WebSocket 客户端
*/
private static createWebSocketClient<T extends BaseServiceType>(platform: PlatformType, config: ClientConfig): INetClient<T> {
let wsClient: WsClientBrowser<T> | WsClientMiniapp<T>;
if (platform === PlatformType.MiniApp) {
wsClient = new WsClientMiniapp(this._serviceProto, config) as WsClientMiniapp<T>;
} else {
wsClient = new WsClientBrowser(this._serviceProto, config) as WsClientBrowser<T>;
}
// 使用包装器统一接口
return new WebSocketClientWrapper<T>(wsClient);
} }
/** /**

View File

@@ -1,303 +0,0 @@
# 网络通信模块 (Framework/Net)
## 📋 模块概述
基于 TSRPC 的网络通信层,支持多平台(浏览器、小程序),提供 API 调用和服务器消息监听功能。
## 🎯 核心特性
- ✅ 跨平台支持(浏览器/小程序)
- ✅ 自动平台检测和适配
- ✅ 服务协议动态配置
- ✅ API 调用和消息监听
- ✅ 自动重连机制
- ✅ 完整的事件系统
## 📦 依赖包
| 平台 | NPM 包 |
|------|--------|
| 浏览器 (Web) | `tsrpc-browser` |
| 小程序 (微信/抖音/QQ) | `tsrpc-miniapp` |
## 🗂️ 文件结构
```
Framework/Net/
├── NetManager.ts # 网络管理器(核心)
├── PlatformAdapter.ts # 平台适配器
├── NetConfig.ts # 网络配置
├── NetEvent.ts # 网络事件
├── LoginProtocol.ts # 登录协议(临时)
└── NetExample.ts # 使用示例
```
## 📘 核心类详解
### NetManager - 网络管理器
**职责**: 网络连接管理、消息收发、重连机制
**核心方法**:
```typescript
class NetManager {
// 获取单例
static getInstance(): NetManager;
// 设置服务协议(必须在 init 之前调用)
setServiceProto(serviceProto: ServiceProto): void;
// 初始化网络配置
init(config: NetConfig): void;
// 创建客户端实例并连接
connect(): Promise<boolean>;
// 断开连接并清理资源
disconnect(): void;
// 调用 API
callApi<Req, Res>(apiName: string, req: Req): Promise<Res | null>;
// 监听服务器消息
listenMsg<T>(msgName: string, handler: (msg: T) => void): void;
// 取消监听服务器消息
unlistenMsg(msgName: string, handler?: Function): void;
// 发送消息到服务器
sendMsg<T>(msgName: string, msg: T): Promise<void>;
// 监听网络事件
on(event: NetEvent, callback: Function): void;
// 取消监听网络事件
off(event: NetEvent, callback: Function): void;
}
```
### PlatformAdapter - 平台适配器
**职责**: 根据运行平台创建对应的 TSRPC 客户端
**技术实现**:
- 使用别名导入: `HttpClient as HttpClientBrowser``HttpClient as HttpClientMiniapp`
- 自动检测 Cocos 平台类型 (`sys.platform`)
- 根据平台实例化对应的客户端
**核心方法**:
```typescript
class PlatformAdapter {
// 设置服务协议
static setServiceProto(serviceProto: ServiceProto): void;
// 检测当前运行平台
static detectPlatform(): string;
// 创建对应平台的客户端实例
static createClient(config: NetConfig): HttpClient | null;
// 获取当前平台
static getCurrentPlatform(): string;
// 平台判断
static isMiniApp(): boolean;
static isBrowser(): boolean;
// 获取平台详细信息
static getPlatformInfo(): object;
}
```
### NetConfig - 网络配置
**配置接口**:
```typescript
interface NetConfig {
serverUrl: string; // 服务器地址
timeout?: number; // 超时时间(ms) 默认 30000
autoReconnect?: boolean; // 是否自动重连 默认 true
reconnectInterval?: number; // 重连间隔(ms) 默认 3000
maxReconnectTimes?: number; // 最大重连次数 默认 5
}
// 默认配置
const DefaultNetConfig: Partial<NetConfig>;
```
### NetEvent - 网络事件
**事件类型**:
```typescript
enum NetEvent {
Connected = "net_connected", // 连接成功
Disconnected = "net_disconnected", // 连接断开
Reconnecting = "net_reconnecting", // 正在重连
ReconnectSuccess = "net_reconnect_success", // 重连成功
ReconnectFailed = "net_reconnect_failed", // 重连失败
Error = "net_error", // 网络错误
Timeout = "net_timeout" // 连接超时
}
```
## 📝 使用指南
### 1. 基础使用流程
```typescript
import { NetManager } from './Framework/Net/NetManager';
import { NetConfig } from './Framework/Net/NetConfig';
import { NetEvent } from './Framework/Net/NetEvent';
import { serviceProto } from '../Shared/protocols/serviceProto';
// 1. 获取实例并设置协议
const netManager = NetManager.getInstance();
netManager.setServiceProto(serviceProto); // 必须在 init 之前
// 2. 监听网络事件
netManager.on(NetEvent.Connected, () => {
console.log('✅ 网络已连接');
});
netManager.on(NetEvent.Disconnected, () => {
console.log('❌ 网络已断开');
});
netManager.on(NetEvent.Error, (error: any) => {
console.error('⚠️ 网络错误:', error);
});
// 3. 初始化配置
const config: NetConfig = {
serverUrl: 'http://localhost:3000',
timeout: 30000,
autoReconnect: true,
reconnectInterval: 3000,
maxReconnectTimes: 5
};
netManager.init(config);
// 4. 连接服务器
const success = await netManager.connect();
if (success) {
console.log('✅ 网络初始化成功');
}
```
### 2. 调用 API
```typescript
import { ReqLogin, ResLogin } from '../Shared/protocols/PtlLogin';
// 调用登录 API
const result = await netManager.callApi<ReqLogin, ResLogin>('Login', {
username: 'testUser',
password: '123456'
});
if (result) {
console.log('登录成功:', result);
}
```
### 3. 监听服务器消息
```typescript
import { MsgUserJoin } from '../Shared/protocols/MsgUserJoin';
// 监听用户加入消息
netManager.listenMsg<MsgUserJoin>('UserJoin', (msg) => {
console.log('有新用户加入:', msg);
});
// 取消监听
netManager.unlistenMsg('UserJoin');
```
### 4. 发送消息到服务器
```typescript
import { MsgChat } from '../Shared/protocols/MsgChat';
// 发送聊天消息
await netManager.sendMsg<MsgChat>('Chat', {
content: 'Hello World!'
});
```
## 🔄 协议同步
### 同步脚本配置
**文件**: 项目根目录的 `sync-shared.js`
```javascript
// 服务端共享目录
const serverSharedDir = path.join(__dirname, '../server/src/shared');
// 客户端目标目录
const clientSharedDir = path.join(__dirname, 'assets/scripts/Shared');
```
### 使用步骤
1. **确保服务端项目位置**: 服务端项目应该在 `../server` 目录
2. **运行同步命令**:
```bash
npm run sync-shared
```
3. **导入协议**:
```typescript
import { serviceProto } from '../Shared/protocols/serviceProto';
import { ReqLogin, ResLogin } from '../Shared/protocols/PtlLogin';
```
## ⚠️ 注意事项
1. **协议必须先设置**: 在调用 `init()` 之前,必须先调用 `setServiceProto()`
2. **连接状态检查**: 调用 API 前确保已连接,否则会返回 null
3. **错误处理**: 建议监听 `NetEvent.Error` 事件处理网络错误
4. **资源清理**: 应用退出时调用 `disconnect()` 清理资源
5. **平台兼容**: PlatformAdapter 会自动处理平台差异,无需手动判断
## 🔍 调试技巧
### 启用详细日志
```typescript
// NetManager 内部已有详细的 console.log
// 可以根据日志前缀过滤:
// [NetManager] - 网络管理器日志
// [PlatformAdapter] - 平台适配器日志
```
### 常见问题
**问题1**: `协议未设置` 错误
```typescript
// 解决: 确保在 init 之前调用 setServiceProto
netManager.setServiceProto(serviceProto);
netManager.init(config);
```
**问题2**: `API 调用返回 null`
```typescript
// 解决: 检查网络连接状态
netManager.on(NetEvent.Connected, async () => {
// 在连接成功后再调用 API
const result = await netManager.callApi('Login', data);
});
```
**问题3**: 小程序平台客户端创建失败
```typescript
// 解决: 确保已安装 tsrpc-miniapp
npm install tsrpc-miniapp
```
## 📚 参考资料
- [TSRPC 官方文档](https://tsrpc.cn/)
- [Cocos Creator 官方文档](https://docs.cocos.com/creator/manual/zh/)

View File

@@ -1,11 +0,0 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "6eb67b95-26c3-410a-a9ee-441d4fa23371",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,237 @@
import { BaseServiceType } from 'tsrpc-browser';
import { NetEvent } from './NetEvent';
/**
* WebSocket 连接状态
*/
export enum WsConnectionStatus {
/** 已断开 */
Disconnected = "disconnected",
/** 连接中 */
Connecting = "connecting",
/** 已连接 */
Connected = "connected",
/** 连接失败 */
Failed = "failed"
}
/**
* WebSocket 客户端接口
* 统一 HTTP 和 WebSocket 客户端的接口
*/
export interface INetClient<T extends BaseServiceType = any> {
/** 调用API */
callApi<Req, Res>(apiName: string, req: Req): Promise<{ isSucc: boolean; res?: Res; err?: any }>;
/** 监听消息 (仅 WebSocket) */
listenMsg?(msgName: string, handler: Function): void;
/** 取消监听消息 (仅 WebSocket) */
unlistenMsg?(msgName: string, handler?: Function): void;
/** 发送消息 (仅 WebSocket) */
sendMsg?(msgName: string, msg: any): void;
/** 连接 (仅 WebSocket) */
connect?(): Promise<{ isSucc: boolean; errMsg?: string }>;
/** 断开连接 (仅 WebSocket) */
disconnect?(): void;
/** 获取连接状态 */
status?: WsConnectionStatus;
}
/**
* WebSocket 客户端包装器
* 提供统一的接口和事件处理
*/
export class WebSocketClientWrapper<T extends BaseServiceType = any> implements INetClient<T> {
private _wsClient: any = null;
private _status: WsConnectionStatus = WsConnectionStatus.Disconnected;
private _eventCallbacks: Map<string, Function[]> = new Map();
constructor(wsClient: any) {
this._wsClient = wsClient;
this.setupEventHandlers();
}
/**
* 设置事件处理器
*/
private setupEventHandlers(): void {
if (!this._wsClient) return;
// 监听连接事件
this._wsClient.flows.preConnectFlow.push((v: any) => {
this._status = WsConnectionStatus.Connecting;
this.emit(NetEvent.Connecting);
return v;
});
this._wsClient.flows.postConnectFlow.push((v: any) => {
if (v.isSucc) {
this._status = WsConnectionStatus.Connected;
this.emit(NetEvent.Connected);
} else {
this._status = WsConnectionStatus.Failed;
this.emit(NetEvent.Error, v.errMsg);
}
return v;
});
// 监听断开连接事件
this._wsClient.flows.postDisconnectFlow.push((v: any) => {
this._status = WsConnectionStatus.Disconnected;
this.emit(NetEvent.Disconnected);
return v;
});
}
/**
* 连接服务器
*/
async connect(): Promise<{ isSucc: boolean; errMsg?: string }> {
try {
this._status = WsConnectionStatus.Connecting;
const result = await this._wsClient.connect();
if (result.isSucc) {
this._status = WsConnectionStatus.Connected;
} else {
this._status = WsConnectionStatus.Failed;
}
return result;
} catch (error) {
this._status = WsConnectionStatus.Failed;
return {
isSucc: false,
errMsg: error instanceof Error ? error.message : String(error)
};
}
}
/**
* 断开连接
*/
disconnect(): void {
if (this._wsClient && typeof this._wsClient.disconnect === 'function') {
this._wsClient.disconnect();
}
this._status = WsConnectionStatus.Disconnected;
}
/**
* 调用API
*/
async callApi<Req, Res>(apiName: string, req: Req): Promise<{ isSucc: boolean; res?: Res; err?: any }> {
if (this._status !== WsConnectionStatus.Connected) {
return {
isSucc: false,
err: 'WebSocket not connected'
};
}
try {
return await this._wsClient.callApi(apiName, req);
} catch (error) {
return {
isSucc: false,
err: error
};
}
}
/**
* 监听消息
*/
listenMsg(msgName: string, handler: Function): void {
if (this._wsClient && typeof this._wsClient.listenMsg === 'function') {
this._wsClient.listenMsg(msgName, handler);
}
}
/**
* 取消监听消息
*/
unlistenMsg(msgName: string, handler?: Function): void {
if (this._wsClient && typeof this._wsClient.unlistenMsg === 'function') {
this._wsClient.unlistenMsg(msgName, handler);
}
}
/**
* 发送消息
*/
sendMsg(msgName: string, msg: any): void {
if (this._status !== WsConnectionStatus.Connected) {
console.warn('[WebSocketClient] Cannot send message: not connected');
return;
}
if (this._wsClient && typeof this._wsClient.sendMsg === 'function') {
this._wsClient.sendMsg(msgName, msg);
}
}
/**
* 注册事件监听器
*/
on(event: NetEvent, callback: Function): void {
if (!this._eventCallbacks.has(event)) {
this._eventCallbacks.set(event, []);
}
this._eventCallbacks.get(event)!.push(callback);
}
/**
* 取消事件监听器
*/
off(event: NetEvent, callback: Function): void {
const callbacks = this._eventCallbacks.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
/**
* 触发事件
*/
private emit(event: NetEvent, ...args: any[]): void {
const callbacks = this._eventCallbacks.get(event);
if (callbacks) {
callbacks.forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error('[WebSocketClient] Event callback error:', error);
}
});
}
}
/**
* 获取连接状态
*/
get status(): WsConnectionStatus {
return this._status;
}
/**
* 判断是否已连接
*/
get isConnected(): boolean {
return this._status === WsConnectionStatus.Connected;
}
/**
* 获取原始客户端实例
*/
get rawClient(): any {
return this._wsClient;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4a49d854-9ed8-4344-825f-9439bb287819",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -11,7 +11,7 @@ export interface MsgPlayerJoin {
/** 玩家昵称 */ /** 玩家昵称 */
playerName: string; playerName: string;
/** 玩家位置 */ /** 玩家位置客户端坐标放大1000倍后的整数 */
position: Position; position: Position;
/** 是否新玩家 */ /** 是否新玩家 */

View File

@@ -11,7 +11,7 @@ export interface MsgPlayerMove {
/** 玩家昵称 */ /** 玩家昵称 */
playerName: string; playerName: string;
/** 移动后的位置 */ /** 移动后的位置客户端坐标放大1000倍后的整数 */
position: Position; position: Position;
/** 移动时间戳 */ /** 移动时间戳 */

View File

@@ -0,0 +1,11 @@
/**
* 登录请求消息
*/
export interface MsgReqLogin {
/** 玩家ID用于识别玩家 */
playerId: string;
/** 玩家昵称(可选,新玩家时使用) */
playerName?: string;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "24fdb58b-b594-4bcf-8695-2669bd9bf9ca",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,10 @@
/**
* 移动请求消息
*/
export interface MsgReqMove {
/** X坐标放大1000倍后取整服务器内部除以1000作为实际坐标 */
x: number;
/** Y坐标放大1000倍后取整服务器内部除以1000作为实际坐标 */
y: number;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b244e6e0-857f-41f2-ac85-c6130e61218b",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,7 @@
/**
* 发送消息请求
*/
export interface MsgReqSend {
/** 消息内容 */
content: string;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b6e9ef16-0098-4050-a133-721d339159e5",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -1,16 +1,5 @@
import { Position } from './base'; import { Position } from './base';
/**
*
*/
export interface ReqLogin {
/** 玩家ID用于识别玩家 */
playerId: string;
/** 玩家昵称(可选,新玩家时使用) */
playerName?: string;
}
/** /**
* *
*/ */
@@ -21,10 +10,10 @@ export interface PlayerInfo {
/** 玩家昵称 */ /** 玩家昵称 */
name: string; name: string;
/** 当前位置 */ /** 当前位置客户端坐标放大1000倍后的整数 */
position: Position; position: Position;
/** 出生点 */ /** 出生点客户端坐标放大1000倍后的整数 */
spawnPoint: Position; spawnPoint: Position;
/** 当前生命值 */ /** 当前生命值 */
@@ -44,9 +33,9 @@ export interface PlayerInfo {
} }
/** /**
* *
*/ */
export interface ResLogin { export interface MsgResLogin {
/** 是否成功 */ /** 是否成功 */
success: boolean; success: boolean;
@@ -58,4 +47,7 @@ export interface ResLogin {
/** 是否新玩家 */ /** 是否新玩家 */
isNewPlayer?: boolean; isNewPlayer?: boolean;
/** 房间内其他在线玩家信息 */
otherPlayers?: PlayerInfo[];
} }

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "dcefce3e-1a92-45ac-8d63-94fb1370edae",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,15 @@
import { Position } from './base';
/**
* 移动响应消息
*/
export interface MsgResMove {
/** 是否成功 */
success: boolean;
/** 消息 */
message: string;
/** 新位置成功时返回客户端坐标放大1000倍后的整数 */
position?: Position;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e7ba8c78-ba4b-4dc6-a3de-352a5444421a",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,7 @@
/**
* 发送消息响应
*/
export interface MsgResSend {
/** 发送时间 */
time: Date;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "1fecff7f-ad47-4829-b229-e89e6e2e301e",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -1,26 +0,0 @@
import { Position } from './base';
/**
* 移动请求
*/
export interface ReqMove {
/** 目标位置 X 坐标 */
x: number;
/** 目标位置 Y 坐标 */
y: number;
}
/**
* 移动响应
*/
export interface ResMove {
/** 是否成功 */
success: boolean;
/** 消息 */
message?: string;
/** 实际移动后的位置 */
position?: Position;
}

View File

@@ -1,10 +0,0 @@
// This is a demo code file
// Feel free to delete it
export interface ReqSend {
content: string
}
export interface ResSend {
time: Date
}

View File

@@ -15,7 +15,7 @@ export interface BaseMessage {
} }
/** /**
* 位置坐标 * 位置坐标客户端使用放大1000倍后的整数坐标
*/ */
export interface Position { export interface Position {
x: number; x: number;

View File

@@ -2,34 +2,32 @@ import { ServiceProto } from 'tsrpc-proto';
import { MsgChat } from './MsgChat'; import { MsgChat } from './MsgChat';
import { MsgPlayerJoin } from './MsgPlayerJoin'; import { MsgPlayerJoin } from './MsgPlayerJoin';
import { MsgPlayerMove } from './MsgPlayerMove'; import { MsgPlayerMove } from './MsgPlayerMove';
import { ReqLogin, ResLogin } from './PtlLogin'; import { MsgReqLogin } from './MsgReqLogin';
import { ReqMove, ResMove } from './PtlMove'; import { MsgReqMove } from './MsgReqMove';
import { ReqSend, ResSend } from './PtlSend'; import { MsgReqSend } from './MsgReqSend';
import { MsgResLogin } from './MsgResLogin';
import { MsgResMove } from './MsgResMove';
import { MsgResSend } from './MsgResSend';
export interface ServiceType { export interface ServiceType {
api: { api: {
"Login": {
req: ReqLogin,
res: ResLogin
},
"Move": {
req: ReqMove,
res: ResMove
},
"Send": {
req: ReqSend,
res: ResSend
}
}, },
msg: { msg: {
"Chat": MsgChat, "Chat": MsgChat,
"PlayerJoin": MsgPlayerJoin, "PlayerJoin": MsgPlayerJoin,
"PlayerMove": MsgPlayerMove "PlayerMove": MsgPlayerMove,
"ReqLogin": MsgReqLogin,
"ReqMove": MsgReqMove,
"ReqSend": MsgReqSend,
"ResLogin": MsgResLogin,
"ResMove": MsgResMove,
"ResSend": MsgResSend
} }
} }
export const serviceProto: ServiceProto<ServiceType> = { export const serviceProto: ServiceProto<ServiceType> = {
"version": 3, "version": 6,
"services": [ "services": [
{ {
"id": 0, "id": 0,
@@ -47,19 +45,34 @@ export const serviceProto: ServiceProto<ServiceType> = {
"type": "msg" "type": "msg"
}, },
{ {
"id": 2, "id": 6,
"name": "Login", "name": "ReqLogin",
"type": "api" "type": "msg"
}, },
{ {
"id": 5, "id": 7,
"name": "Move", "name": "ReqMove",
"type": "api" "type": "msg"
}, },
{ {
"id": 1, "id": 8,
"name": "Send", "name": "ReqSend",
"type": "api" "type": "msg"
},
{
"id": 9,
"name": "ResLogin",
"type": "msg"
},
{
"id": 10,
"name": "ResMove",
"type": "msg"
},
{
"id": 11,
"name": "ResSend",
"type": "msg"
} }
], ],
"types": { "types": {
@@ -176,18 +189,18 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlLogin/ReqLogin": { "MsgReqLogin/MsgReqLogin": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
"id": 1, "id": 0,
"name": "playerId", "name": "playerId",
"type": { "type": {
"type": "String" "type": "String"
} }
}, },
{ {
"id": 2, "id": 1,
"name": "playerName", "name": "playerName",
"type": { "type": {
"type": "String" "type": "String"
@@ -196,7 +209,38 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlLogin/ResLogin": { "MsgReqMove/MsgReqMove": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
},
"MsgReqSend/MsgReqSend": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
}
]
},
"MsgResLogin/MsgResLogin": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
@@ -214,25 +258,37 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
}, },
{ {
"id": 4, "id": 2,
"name": "player", "name": "player",
"type": { "type": {
"type": "Reference", "type": "Reference",
"target": "PtlLogin/PlayerInfo" "target": "MsgResLogin/PlayerInfo"
}, },
"optional": true "optional": true
}, },
{ {
"id": 5, "id": 3,
"name": "isNewPlayer", "name": "isNewPlayer",
"type": { "type": {
"type": "Boolean" "type": "Boolean"
}, },
"optional": true "optional": true
},
{
"id": 4,
"name": "otherPlayers",
"type": {
"type": "Array",
"elementType": {
"type": "Reference",
"target": "MsgResLogin/PlayerInfo"
}
},
"optional": true
} }
] ]
}, },
"PtlLogin/PlayerInfo": { "MsgResLogin/PlayerInfo": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
@@ -302,26 +358,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlMove/ReqMove": { "MsgResMove/MsgResMove": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
},
"PtlMove/ResMove": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
@@ -336,8 +373,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
"name": "message", "name": "message",
"type": { "type": {
"type": "String" "type": "String"
}, }
"optional": true
}, },
{ {
"id": 2, "id": 2,
@@ -350,19 +386,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlSend/ReqSend": { "MsgResSend/MsgResSend": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
}
]
},
"PtlSend/ResSend": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,4 @@
<script type="module" crossorigin src="/dist/assets/index.02b86726.js"></script> <script type="module" crossorigin src="/dist/assets/index.691dfd76.js"></script>
<link rel="stylesheet" href="/dist/assets/index.1d01bced.css"> <link rel="stylesheet" href="/dist/assets/index.741f95c0.css">
<div id="app"></div> <div id="dev-app" style="width: 400px;height: 100%;display: flex;flex-direction: column;justify-content: center;"></div>

View File

@@ -19,11 +19,13 @@
</head> </head>
<body> <body>
<%- include(cocosToolBar, {config: config}) %> <%- include(cocosToolBar, {config: config}) %>
<div style="display: flex;flex: auto;align-items: center;">
<%- include ./dist/index.html %>
<div id="content" class="content"> <div id="content" class="content">
<div class="contentWrap"> <div class="contentWrap">
<div id="GameDiv" class="wrapper"> <div id="GameDiv" class="wrapper">
<div id="Cocos3dGameContainer"> <div id="Cocos3dGameContainer">
<canvas id="GameCanvas"></canvas> <canvas id="GameCanvas" tabindex="-1" style="background-color: '';"></canvas>
</div> </div>
<div id="splash"> <div id="splash">
<div class="progress-bar stripes"><span></span></div> <div class="progress-bar stripes"><span></span></div>
@@ -34,14 +36,40 @@
<div class="error" id="error"> <div class="error" id="error">
<div class="title">Error <i>(Please open the console to see detailed errors)</i></div> <div class="title">Error <i>(Please open the console to see detailed errors)</i></div>
<div class="error-main"></div> <div class="error-main"></div>
<div class="error-stack"></div>
</div> </div>
</div> </div>
</div> </div>
<p class="footer"> <p class="footer">
<% include ./dist/index.html %> Created with <a href="https://www.cocos.com/products" target="_blank" title="Cocos Creator">Cocos Creator</a>
</p> </p>
</div> </div>
</div>
<%- include(cocosTemplate, {}) %> <%- include(cocosTemplate, {}) %>
</body> </body>
</html> </html>
<script>
document.getElementsByClassName('toolbar')[0].insertAdjacentHTML('afterbegin', '<div><button id="btn-show-tree">Tree</button></div>');
const devtoolsBtn = document.getElementById('btn-show-tree');
let isOpen = !!localStorage.getItem('ccc_devtools_show');
toggle(isOpen);
devtoolsBtn.addEventListener('click', () => {
isOpen = !isOpen;
toggle(isOpen);
}, false);
function toggle(isOpen) {
const devApp = document.getElementById('dev-app');
window.ccdevShow = isOpen;
if (isOpen) {
devApp.style.display = 'flex';
devtoolsBtn.classList.add('checked');
localStorage.setItem('ccc_devtools_show', 1);
} else {
devApp.style.display = 'none';
devtoolsBtn.classList.remove('checked');
localStorage.removeItem('ccc_devtools_show');
}
}
</script>

View File

@@ -1 +1 @@
{"name":"ccc-devtools","version":"2022/7/17","author":"Next","repo":"https://github.com/potato47/ccc-devtools.git"} {"name":"ccc-devtools","version":"2022/12/11","author":"Next","repo":"https://github.com/potato47/ccc-devtools.git"}

View File

@@ -1,3 +1,9 @@
{ {
"__version__": "1.0.6" "__version__": "1.0.6",
"general": {
"designResolution": {
"width": 720,
"height": 1440
}
}
} }

View File

@@ -35,6 +35,10 @@ async function syncShared() {
// 确保目标目录存在 // 确保目标目录存在
await fs.ensureDir(CLIENT_SHARED_PATH); await fs.ensureDir(CLIENT_SHARED_PATH);
// 清空目标目录(删除旧文件)
console.log('🗑️ 正在清空目标目录...');
await fs.emptyDir(CLIENT_SHARED_PATH);
// 复制目录 // 复制目录
console.log('📦 正在复制文件...'); console.log('📦 正在复制文件...');
await fs.copy(SERVER_SHARED_PATH, CLIENT_SHARED_PATH, { await fs.copy(SERVER_SHARED_PATH, CLIENT_SHARED_PATH, {

View File

@@ -1,11 +1,12 @@
{ {
/* Base configuration. Do not edit this field. */ /* Base configuration. Do not edit this field. */
"extends": "./temp/tsconfig.cocos.json", "extends": "./temp/tsconfig.cocos.json",
/* Add your custom configuration here. */ /* Add your custom configuration here. */
"compilerOptions": { "compilerOptions": {
"strict": false, "strict": false,
"target": "es2020", "target": "es2020",
"downlevelIteration": true "downlevelIteration": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true
} }
} }

View File

@@ -80,6 +80,14 @@ Roguelike 游戏服务端开发任务追踪
- [x] 移动API实现 (ApiMove.ts) - [x] 移动API实现 (ApiMove.ts)
- [x] 位置验证和边界检查 - [x] 位置验证和边界检查
- [x] 登录时广播玩家加入 - [x] 登录时广播玩家加入
- [x] **架构重构从HTTP API到WebSocket消息** (2025-12-18)
- [x] 删除旧的API实现 (src/api目录)
- [x] 删除旧的API协议 (PtlLogin.ts, PtlMove.ts, PtlSend.ts)
- [x] 创建消息协议文件 (MsgReq*/MsgRes*)
- [x] 创建消息处理器 (src/msg目录)
- [x] 更新服务器配置为消息监听模式
- [x] 重新生成serviceProto.ts文件
- [x] 修复所有消息名称映射问题
### 进行中 ### 进行中
- 等待下一阶段开发... - 等待下一阶段开发...
@@ -87,6 +95,27 @@ Roguelike 游戏服务端开发任务追踪
### 待办事项 ### 待办事项
- 按照上述规划顺序实施开发 - 按照上述规划顺序实施开发
## 重要架构说明
### 🚨 消息协议设计规范 (2025-12-18更新)
**本项目使用WebSocket服务器后续所有接口设计必须使用消息Message方式而非API方式**
#### 消息命名规范:
- 请求消息:`MsgReq{功能名}.ts` (如MsgReqLogin.ts)
- 响应消息:`MsgRes{功能名}.ts` (如MsgResLogin.ts)
- 广播消息:`Msg{事件名}.ts` (如MsgPlayerMove.ts)
#### 消息处理器位置:
- 文件位置:`src/msg/MsgReq{功能名}.ts`
- 处理函数:使用 `MsgCall<MsgReq{功能名}>` 参数
- 响应方式:`call.conn.sendMsg('Res{功能名}', response)`
- 注册方式:`server.listenMsg('Req{功能名}', handler)`
#### 协议文件生成:
- 使用 `npm run proto` 自动生成 `serviceProto.ts`
- 禁止直接修改 `serviceProto.ts` 文件
- 协议文件会自动去掉 "Msg" 前缀进行注册
## 技术栈 ## 技术栈
- TSRPC 框架 - TSRPC 框架
- TypeScript - TypeScript
@@ -94,5 +123,6 @@ Roguelike 游戏服务端开发任务追踪
## 备注 ## 备注
- 本文档由 AI 助手维护 - 本文档由 AI 助手维护
- 更新日期: 2025-12-14 - 更新日期: 2025-12-18
- 开发过程中会根据实际情况调整任务优先级和细节 - 开发过程中会根据实际情况调整任务优先级和细节
- **重要**项目已从HTTP API架构迁移到WebSocket消息架构请严格按照消息协议规范开发

View File

@@ -1,74 +0,0 @@
import { ApiCall } from "tsrpc";
import { ReqLogin, ResLogin, PlayerInfo } from "../shared/protocols/PtlLogin";
import { playerManager } from "../managers/PlayerManager";
/**
* 登录API
* 处理玩家登录流程:
* 1. 查找已有角色
* 2. 若无角色,自动注册并创建角色
* 3. 分配出生点
* 4. 返回角色信息
*/
export default async function (call: ApiCall<ReqLogin, ResLogin>) {
const { playerId, playerName } = call.req;
// 验证玩家ID
if (!playerId || playerId.trim().length === 0) {
call.error('玩家ID不能为空');
return;
}
try {
// 检查玩家是否已存在
const isNewPlayer = !playerManager.hasPlayer(playerId);
// 获取或创建玩家(自动处理注册和创角)
const player = playerManager.getOrCreatePlayer(playerId, playerName);
// 设置玩家在线状态
playerManager.setPlayerOnline(playerId, call.conn.id);
// 保存玩家ID到连接对象供其他API使用
(call.conn as any).playerId = playerId;
// 转换为协议格式
const playerInfo: PlayerInfo = {
id: player.id,
name: player.name,
position: { ...player.position },
spawnPoint: { ...player.spawnPoint },
hp: player.hp,
maxHp: player.maxHp,
isAlive: player.isAlive,
createdAt: player.createdAt,
lastLoginAt: player.lastLoginAt
};
// 广播玩家加入消息给其他在线玩家
const { broadcastToAll } = await import('../utils/broadcast');
broadcastToAll('PlayerJoin', {
playerId: player.id,
playerName: player.name,
position: { ...player.position },
isNewPlayer,
timestamp: Date.now()
}, call.conn.id);
// 返回成功结果
call.succ({
success: true,
message: isNewPlayer ? '创建角色成功,欢迎来到游戏世界!' : '登录成功,欢迎回来!',
player: playerInfo,
isNewPlayer
});
console.log(`玩家 ${player.name} (${playerId}) ${isNewPlayer ? '首次登录' : '登录成功'}`);
console.log(` 在线玩家数: ${playerManager.getOnlinePlayerCount()}`);
} catch (error) {
console.error('登录失败:', error);
call.error(`登录失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}

View File

@@ -1,86 +0,0 @@
import { ApiCall } from "tsrpc";
import { ReqMove, ResMove } from "../shared/protocols/PtlMove";
import { playerManager } from "../managers/PlayerManager";
import { broadcastToAll } from "../utils/broadcast";
import { MsgPlayerMove } from "../shared/protocols/MsgPlayerMove";
/**
* 移动API
* 处理玩家移动请求,验证位置合法性,更新玩家位置并广播给其他玩家
*/
export default async function (call: ApiCall<ReqMove, ResMove>) {
const { x, y } = call.req;
// 从连接中获取玩家ID需要在登录时保存
const playerId = (call.conn as any).playerId;
if (!playerId) {
call.error('未登录,请先登录');
return;
}
// 获取玩家信息
const player = playerManager.getPlayer(playerId);
if (!player) {
call.error('玩家不存在');
return;
}
// 检查玩家是否存活
if (!player.isAlive) {
call.error('玩家已死亡,无法移动');
return;
}
// 验证坐标是否为数字
if (typeof x !== 'number' || typeof y !== 'number') {
call.error('坐标格式错误');
return;
}
// 验证坐标是否为整数
if (!Number.isInteger(x) || !Number.isInteger(y)) {
call.error('坐标必须为整数');
return;
}
try {
// 尝试更新玩家位置
const success = playerManager.updatePlayerPosition(playerId, x, y);
if (!success) {
call.succ({
success: false,
message: '移动失败,位置无效或超出世界边界'
});
return;
}
// 获取更新后的位置
const newPosition = { x, y };
// 广播移动消息给所有其他玩家
const moveMsg: MsgPlayerMove = {
playerId: player.id,
playerName: player.name,
position: newPosition,
timestamp: Date.now()
};
broadcastToAll('MsgPlayerMove', moveMsg, call.conn.id);
// 返回成功结果
call.succ({
success: true,
message: '移动成功',
position: newPosition
});
console.log(`玩家 ${player.name} 移动到 (${x}, ${y})`);
} catch (error) {
console.error('移动失败:', error);
call.error(`移动失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}

View File

@@ -1,26 +0,0 @@
import { ApiCall } from "tsrpc";
import { server } from "..";
import { ReqSend, ResSend } from "../shared/protocols/PtlSend";
// This is a demo code file
// Feel free to delete it
export default async function (call: ApiCall<ReqSend, ResSend>) {
// Error
if (call.req.content.length === 0) {
call.error('Content is empty')
return;
}
// Success
let time = new Date();
call.succ({
time: time
});
// Broadcast
server.broadcastMsg('Chat', {
content: call.req.content,
time: time
})
}

View File

@@ -3,13 +3,13 @@
*/ */
export const WorldConfig = { export const WorldConfig = {
/** 世界宽度 */ /** 世界宽度 */
WORLD_WIDTH: 800, WORLD_WIDTH: 1440,
/** 世界高度 */ /** 世界高度 */
WORLD_HEIGHT: 800, WORLD_HEIGHT: 1440,
/** 出生区域半径(距离中心点) */ /** 出生区域半径(距离中心点) */
SPAWN_RADIUS: 200, SPAWN_RADIUS: 10,
/** 世界中心点 X 坐标 */ /** 世界中心点 X 坐标 */
get CENTER_X() { get CENTER_X() {

View File

@@ -1,7 +1,6 @@
import * as path from "path";
import { WsServer } from "tsrpc"; import { WsServer } from "tsrpc";
import { serviceProto } from './shared/protocols/serviceProto';
import { worldManager } from './managers/WorldManager'; import { worldManager } from './managers/WorldManager';
import { serviceProto } from './shared/protocols/serviceProto';
// Create the Server // Create the Server
export const server = new WsServer(serviceProto, { export const server = new WsServer(serviceProto, {
@@ -12,7 +11,11 @@ export const server = new WsServer(serviceProto, {
// Initialize before server start // Initialize before server start
async function init() { async function init() {
await server.autoImplementApi(path.resolve(__dirname, 'api')); // 不再使用API改为消息监听
// await server.autoImplementApi(path.resolve(__dirname, 'api'));
// 注册消息监听器
await registerMessageHandlers();
// 初始化游戏世界 // 初始化游戏世界
console.log('正在初始化游戏世界...'); console.log('正在初始化游戏世界...');
@@ -20,6 +23,23 @@ async function init() {
console.log('游戏世界初始化完成'); console.log('游戏世界初始化完成');
}; };
// 注册消息处理器
async function registerMessageHandlers() {
// 登录请求消息
const LoginHandler = await import('./msg/MsgReqLogin');
server.listenMsg('ReqLogin', LoginHandler.default);
// 移动请求消息
const MoveHandler = await import('./msg/MsgReqMove');
server.listenMsg('ReqMove', MoveHandler.default);
// 发送消息请求
const SendHandler = await import('./msg/MsgReqSend');
server.listenMsg('ReqSend', SendHandler.default);
console.log('消息处理器注册完成');
}
// Entry function // Entry function
async function main() { async function main() {
await init(); await init();

View File

@@ -1,5 +1,5 @@
/** /**
* 位置坐标接口 * 位置坐标接口服务器内部使用实际坐标与客户端通信时需要乘以1000
*/ */
export interface Position { export interface Position {
x: number; x: number;

View File

@@ -0,0 +1,99 @@
import { MsgCall } from "tsrpc";
import { playerManager } from "../managers/PlayerManager";
import { MsgReqLogin } from "../shared/protocols/MsgReqLogin";
import { MsgResLogin, PlayerInfo } from "../shared/protocols/MsgResLogin";
import { gameToClientPosition } from "../utils/coordinate";
/**
* 登录消息处理器
* 处理玩家登录流程:
* 1. 查找已有角色
* 2. 若无角色,自动注册并创建角色
* 3. 分配出生点
* 4. 返回角色信息
*/
export default async function (call: MsgCall<MsgReqLogin>) {
const { playerId, playerName } = call.msg;
// 验证玩家ID
if (!playerId || playerId.trim().length === 0) {
call.conn.sendMsg('ResLogin', {
success: false,
message: '玩家ID不能为空'
});
return;
}
try {
// 检查玩家是否已存在
const isNewPlayer = !playerManager.hasPlayer(playerId);
// 获取或创建玩家(自动处理注册和创角)
const player = playerManager.getOrCreatePlayer(playerId, playerName);
// 设置玩家在线状态
playerManager.setPlayerOnline(playerId, call.conn.id);
// 保存玩家ID到连接对象供其他消息处理器使用
(call.conn as any).playerId = playerId;
// 转换为协议格式(将服务器内部坐标转换为客户端坐标)
const playerInfo: PlayerInfo = {
id: player.id,
name: player.name,
position: gameToClientPosition(player.position),
spawnPoint: gameToClientPosition(player.spawnPoint),
hp: player.hp,
maxHp: player.maxHp,
isAlive: player.isAlive,
createdAt: player.createdAt,
lastLoginAt: player.lastLoginAt
};
// 获取房间内其他在线玩家信息
const otherOnlinePlayers = playerManager.getOnlinePlayers().filter(p => p.id !== playerId);
const otherPlayersInfo: PlayerInfo[] = otherOnlinePlayers.map(p => ({
id: p.id,
name: p.name,
position: gameToClientPosition(p.position),
spawnPoint: gameToClientPosition(p.spawnPoint),
hp: p.hp,
maxHp: p.maxHp,
isAlive: p.isAlive,
createdAt: p.createdAt,
lastLoginAt: p.lastLoginAt
}));
// 广播玩家加入消息给其他在线玩家
const { broadcastToAll } = await import('../utils/broadcast');
broadcastToAll('PlayerJoin', {
playerId: player.id,
playerName: player.name,
position: gameToClientPosition(player.position),
isNewPlayer,
timestamp: Date.now()
}, call.conn.id);
// 返回成功结果
const response: MsgResLogin = {
success: true,
message: isNewPlayer ? '创建角色成功,欢迎来到游戏世界!' : '登录成功,欢迎回来!',
player: playerInfo,
isNewPlayer,
otherPlayers: otherPlayersInfo
};
call.conn.sendMsg('ResLogin', response);
console.log(`玩家 ${player.name} (${playerId}) ${isNewPlayer ? '首次登录' : '登录成功'}`);
console.log(` 在线玩家数: ${playerManager.getOnlinePlayerCount()}`);
} catch (error) {
console.error('登录失败:', error);
call.conn.sendMsg('ResLogin', {
success: false,
message: `登录失败: ${error instanceof Error ? error.message : '未知错误'}`
});
}
}

View File

@@ -0,0 +1,107 @@
import { MsgCall } from "tsrpc";
import { playerManager } from "../managers/PlayerManager";
import { MsgPlayerMove } from "../shared/protocols/MsgPlayerMove";
import { MsgReqMove } from "../shared/protocols/MsgReqMove";
import { broadcastToAll } from "../utils/broadcast";
import { clientToGamePosition, gameToClientPosition } from "../utils/coordinate";
/**
* 移动消息处理器
* 处理玩家移动请求,验证位置合法性,更新玩家位置并广播给其他玩家
*/
export default async function (call: MsgCall<MsgReqMove>) {
const { x, y } = call.msg;
// 从连接中获取玩家ID需要在登录时保存
const playerId = (call.conn as any).playerId;
if (!playerId) {
call.conn.sendMsg('ResMove', {
success: false,
message: '未登录,请先登录'
});
return;
}
// 获取玩家信息
const player = playerManager.getPlayer(playerId);
if (!player) {
call.conn.sendMsg('ResMove', {
success: false,
message: '玩家不存在'
});
return;
}
// 检查玩家是否存活
if (!player.isAlive) {
call.conn.sendMsg('ResMove', {
success: false,
message: '玩家已死亡,无法移动'
});
return;
}
// 验证坐标是否为数字
if (typeof x !== 'number' || typeof y !== 'number') {
call.conn.sendMsg('ResMove', {
success: false,
message: '坐标格式错误'
});
return;
}
// 验证坐标是否为整数
if (!Number.isInteger(x) || !Number.isInteger(y)) {
call.conn.sendMsg('ResMove', {
success: false,
message: '坐标必须为整数'
});
return;
}
// 将客户端坐标放大1000倍转换为游戏内坐标
const gamePosition = clientToGamePosition({ x, y });
try {
// 尝试更新玩家位置(使用游戏内坐标)
const success = playerManager.updatePlayerPosition(playerId, gamePosition.x, gamePosition.y);
if (!success) {
call.conn.sendMsg('ResMove', {
success: false,
message: '移动失败,位置无效或超出世界边界'
});
return;
}
// 获取更新后的位置(转换为客户端坐标)
const newPosition = gameToClientPosition(gamePosition);
// 广播移动消息给所有其他玩家(使用客户端坐标)
const moveMsg: MsgPlayerMove = {
playerId: player.id,
playerName: player.name,
position: newPosition,
timestamp: Date.now()
};
broadcastToAll('PlayerMove', moveMsg, call.conn.id);
// 返回成功结果
call.conn.sendMsg('ResMove', {
success: true,
message: '移动成功',
position: newPosition
});
console.log(`玩家 ${player.name} 移动到游戏内坐标 (${gamePosition.x}, ${gamePosition.y}),客户端坐标 (${newPosition.x}, ${newPosition.y})`);
} catch (error) {
console.error('移动失败:', error);
call.conn.sendMsg('ResMove', {
success: false,
message: `移动失败: ${error instanceof Error ? error.message : '未知错误'}`
});
}
}

View File

@@ -0,0 +1,28 @@
import { MsgCall } from "tsrpc";
import { server } from "..";
import { MsgReqSend } from "../shared/protocols/MsgReqSend";
/**
* 发送消息处理器
*/
export default async function (call: MsgCall<MsgReqSend>) {
// 验证消息内容
if (call.msg.content.length === 0) {
call.conn.sendMsg('ResSend', {
time: new Date()
});
return;
}
// 返回成功响应
let time = new Date();
call.conn.sendMsg('ResSend', {
time: time
});
// 广播消息
server.broadcastMsg('Chat', {
content: call.msg.content,
time: time
});
}

View File

@@ -11,7 +11,7 @@ export interface MsgPlayerJoin {
/** 玩家昵称 */ /** 玩家昵称 */
playerName: string; playerName: string;
/** 玩家位置 */ /** 玩家位置客户端坐标放大1000倍后的整数 */
position: Position; position: Position;
/** 是否新玩家 */ /** 是否新玩家 */

View File

@@ -11,7 +11,7 @@ export interface MsgPlayerMove {
/** 玩家昵称 */ /** 玩家昵称 */
playerName: string; playerName: string;
/** 移动后的位置 */ /** 移动后的位置客户端坐标放大1000倍后的整数 */
position: Position; position: Position;
/** 移动时间戳 */ /** 移动时间戳 */

View File

@@ -0,0 +1,11 @@
/**
* 登录请求消息
*/
export interface MsgReqLogin {
/** 玩家ID用于识别玩家 */
playerId: string;
/** 玩家昵称(可选,新玩家时使用) */
playerName?: string;
}

View File

@@ -0,0 +1,10 @@
/**
* 移动请求消息
*/
export interface MsgReqMove {
/** X坐标放大1000倍后取整服务器内部除以1000作为实际坐标 */
x: number;
/** Y坐标放大1000倍后取整服务器内部除以1000作为实际坐标 */
y: number;
}

View File

@@ -0,0 +1,7 @@
/**
* 发送消息请求
*/
export interface MsgReqSend {
/** 消息内容 */
content: string;
}

View File

@@ -1,16 +1,5 @@
import { Position } from './base'; import { Position } from './base';
/**
*
*/
export interface ReqLogin {
/** 玩家ID用于识别玩家 */
playerId: string;
/** 玩家昵称(可选,新玩家时使用) */
playerName?: string;
}
/** /**
* *
*/ */
@@ -21,10 +10,10 @@ export interface PlayerInfo {
/** 玩家昵称 */ /** 玩家昵称 */
name: string; name: string;
/** 当前位置 */ /** 当前位置客户端坐标放大1000倍后的整数 */
position: Position; position: Position;
/** 出生点 */ /** 出生点客户端坐标放大1000倍后的整数 */
spawnPoint: Position; spawnPoint: Position;
/** 当前生命值 */ /** 当前生命值 */
@@ -44,9 +33,9 @@ export interface PlayerInfo {
} }
/** /**
* *
*/ */
export interface ResLogin { export interface MsgResLogin {
/** 是否成功 */ /** 是否成功 */
success: boolean; success: boolean;
@@ -58,4 +47,7 @@ export interface ResLogin {
/** 是否新玩家 */ /** 是否新玩家 */
isNewPlayer?: boolean; isNewPlayer?: boolean;
/** 房间内其他在线玩家信息 */
otherPlayers?: PlayerInfo[];
} }

View File

@@ -0,0 +1,15 @@
import { Position } from './base';
/**
* 移动响应消息
*/
export interface MsgResMove {
/** 是否成功 */
success: boolean;
/** 消息 */
message: string;
/** 新位置成功时返回客户端坐标放大1000倍后的整数 */
position?: Position;
}

View File

@@ -0,0 +1,7 @@
/**
* 发送消息响应
*/
export interface MsgResSend {
/** 发送时间 */
time: Date;
}

View File

@@ -1,26 +0,0 @@
import { Position } from './base';
/**
* 移动请求
*/
export interface ReqMove {
/** 目标位置 X 坐标 */
x: number;
/** 目标位置 Y 坐标 */
y: number;
}
/**
* 移动响应
*/
export interface ResMove {
/** 是否成功 */
success: boolean;
/** 消息 */
message?: string;
/** 实际移动后的位置 */
position?: Position;
}

View File

@@ -1,10 +0,0 @@
// This is a demo code file
// Feel free to delete it
export interface ReqSend {
content: string
}
export interface ResSend {
time: Date
}

View File

@@ -15,7 +15,7 @@ export interface BaseMessage {
} }
/** /**
* 位置坐标 * 位置坐标客户端使用放大1000倍后的整数坐标
*/ */
export interface Position { export interface Position {
x: number; x: number;

View File

@@ -2,34 +2,32 @@ import { ServiceProto } from 'tsrpc-proto';
import { MsgChat } from './MsgChat'; import { MsgChat } from './MsgChat';
import { MsgPlayerJoin } from './MsgPlayerJoin'; import { MsgPlayerJoin } from './MsgPlayerJoin';
import { MsgPlayerMove } from './MsgPlayerMove'; import { MsgPlayerMove } from './MsgPlayerMove';
import { ReqLogin, ResLogin } from './PtlLogin'; import { MsgReqLogin } from './MsgReqLogin';
import { ReqMove, ResMove } from './PtlMove'; import { MsgReqMove } from './MsgReqMove';
import { ReqSend, ResSend } from './PtlSend'; import { MsgReqSend } from './MsgReqSend';
import { MsgResLogin } from './MsgResLogin';
import { MsgResMove } from './MsgResMove';
import { MsgResSend } from './MsgResSend';
export interface ServiceType { export interface ServiceType {
api: { api: {
"Login": {
req: ReqLogin,
res: ResLogin
},
"Move": {
req: ReqMove,
res: ResMove
},
"Send": {
req: ReqSend,
res: ResSend
}
}, },
msg: { msg: {
"Chat": MsgChat, "Chat": MsgChat,
"PlayerJoin": MsgPlayerJoin, "PlayerJoin": MsgPlayerJoin,
"PlayerMove": MsgPlayerMove "PlayerMove": MsgPlayerMove,
"ReqLogin": MsgReqLogin,
"ReqMove": MsgReqMove,
"ReqSend": MsgReqSend,
"ResLogin": MsgResLogin,
"ResMove": MsgResMove,
"ResSend": MsgResSend
} }
} }
export const serviceProto: ServiceProto<ServiceType> = { export const serviceProto: ServiceProto<ServiceType> = {
"version": 3, "version": 6,
"services": [ "services": [
{ {
"id": 0, "id": 0,
@@ -47,19 +45,34 @@ export const serviceProto: ServiceProto<ServiceType> = {
"type": "msg" "type": "msg"
}, },
{ {
"id": 2, "id": 6,
"name": "Login", "name": "ReqLogin",
"type": "api" "type": "msg"
}, },
{ {
"id": 5, "id": 7,
"name": "Move", "name": "ReqMove",
"type": "api" "type": "msg"
}, },
{ {
"id": 1, "id": 8,
"name": "Send", "name": "ReqSend",
"type": "api" "type": "msg"
},
{
"id": 9,
"name": "ResLogin",
"type": "msg"
},
{
"id": 10,
"name": "ResMove",
"type": "msg"
},
{
"id": 11,
"name": "ResSend",
"type": "msg"
} }
], ],
"types": { "types": {
@@ -176,18 +189,18 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlLogin/ReqLogin": { "MsgReqLogin/MsgReqLogin": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
"id": 1, "id": 0,
"name": "playerId", "name": "playerId",
"type": { "type": {
"type": "String" "type": "String"
} }
}, },
{ {
"id": 2, "id": 1,
"name": "playerName", "name": "playerName",
"type": { "type": {
"type": "String" "type": "String"
@@ -196,7 +209,38 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlLogin/ResLogin": { "MsgReqMove/MsgReqMove": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
},
"MsgReqSend/MsgReqSend": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
}
]
},
"MsgResLogin/MsgResLogin": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
@@ -214,25 +258,37 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
}, },
{ {
"id": 4, "id": 2,
"name": "player", "name": "player",
"type": { "type": {
"type": "Reference", "type": "Reference",
"target": "PtlLogin/PlayerInfo" "target": "MsgResLogin/PlayerInfo"
}, },
"optional": true "optional": true
}, },
{ {
"id": 5, "id": 3,
"name": "isNewPlayer", "name": "isNewPlayer",
"type": { "type": {
"type": "Boolean" "type": "Boolean"
}, },
"optional": true "optional": true
},
{
"id": 4,
"name": "otherPlayers",
"type": {
"type": "Array",
"elementType": {
"type": "Reference",
"target": "MsgResLogin/PlayerInfo"
}
},
"optional": true
} }
] ]
}, },
"PtlLogin/PlayerInfo": { "MsgResLogin/PlayerInfo": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
@@ -302,26 +358,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlMove/ReqMove": { "MsgResMove/MsgResMove": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
},
"PtlMove/ResMove": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
@@ -336,8 +373,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
"name": "message", "name": "message",
"type": { "type": {
"type": "String" "type": "String"
}, }
"optional": true
}, },
{ {
"id": 2, "id": 2,
@@ -350,19 +386,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlSend/ReqSend": { "MsgResSend/MsgResSend": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
}
]
},
"PtlSend/ResSend": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {

View File

@@ -0,0 +1,50 @@
import { Position } from '../models/Position';
/**
* 坐标转换工具
* 处理服务器内部坐标和客户端坐标之间的转换
* 客户端坐标 = 服务器坐标 * 1000取整
* 服务器坐标 = 客户端坐标 / 1000
*/
/**
* 将服务器内部坐标转换为客户端坐标
* @param gamePos 服务器内部坐标(实际坐标)
* @returns 客户端坐标放大1000倍后取整
*/
export function gameToClientPosition(gamePos: Position): Position {
return {
x: Math.round(gamePos.x * 1000),
y: Math.round(gamePos.y * 1000)
};
}
/**
* 将客户端坐标转换为服务器内部坐标
* @param clientPos 客户端坐标放大1000倍后的整数
* @returns 服务器内部坐标(实际坐标)
*/
export function clientToGamePosition(clientPos: Position): Position {
return {
x: clientPos.x / 1000,
y: clientPos.y / 1000
};
}
/**
* 批量转换服务器坐标为客户端坐标
* @param gamePositions 服务器内部坐标数组
* @returns 客户端坐标数组
*/
export function gameToClientPositions(gamePositions: Position[]): Position[] {
return gamePositions.map(gameToClientPosition);
}
/**
* 批量转换客户端坐标为服务器坐标
* @param clientPositions 客户端坐标数组
* @returns 服务器内部坐标数组
*/
export function clientToGamePositions(clientPositions: Position[]): Position[] {
return clientPositions.map(clientToGamePosition);
}