Compare commits
10 Commits
7301adbb43
...
8f58b890be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f58b890be | ||
|
|
03276fe1f6 | ||
|
|
4b3b94a450 | ||
|
|
637978357f | ||
|
|
e677b1e11b | ||
|
|
90175a1665 | ||
|
|
2dbb1e8d05 | ||
|
|
eb38cc9217 | ||
|
|
fb940452db | ||
|
|
c12e439add |
301
client/assets/res/UI/Game/PlayerInfo.prefab
Normal file
301
client/assets/res/UI/Game/PlayerInfo.prefab
Normal 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
|
||||
}
|
||||
]
|
||||
13
client/assets/res/UI/Game/PlayerInfo.prefab.meta
Normal file
13
client/assets/res/UI/Game/PlayerInfo.prefab.meta
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"__type__": "cc.Prefab",
|
||||
"_name": "UIWorld",
|
||||
"_name": "UIGame",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_native": "",
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "UIWorld",
|
||||
"_name": "UIGame",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": null,
|
||||
@@ -51,7 +51,7 @@
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_layer": 8388608,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
@@ -105,7 +105,7 @@
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_layer": 8388608,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
@@ -137,8 +137,8 @@
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 31.078,
|
||||
"y": -21.978,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -155,7 +155,7 @@
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_layer": 8388608,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
@@ -178,7 +178,7 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 42.255859375,
|
||||
"width": 54.462890625,
|
||||
"height": 50.4
|
||||
},
|
||||
"_anchorPoint": {
|
||||
@@ -214,7 +214,7 @@
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_string": "label",
|
||||
"_string": "Game",
|
||||
"_horizontalAlign": 1,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 20,
|
||||
@@ -313,7 +313,7 @@
|
||||
"__prefab": {
|
||||
"__id__": 12
|
||||
},
|
||||
"_alignFlags": 9,
|
||||
"_alignFlags": 18,
|
||||
"_target": null,
|
||||
"_left": 0,
|
||||
"_right": 0,
|
||||
@@ -8,6 +8,6 @@
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"syncNodeName": "UIWorld"
|
||||
"syncNodeName": "UIGame"
|
||||
}
|
||||
}
|
||||
@@ -259,7 +259,7 @@
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Plane",
|
||||
"_name": "Game",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
@@ -326,8 +326,8 @@
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 640,
|
||||
"y": 360,
|
||||
"x": 360,
|
||||
"y": 719.9999999999999,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -412,7 +412,7 @@
|
||||
"_priority": 1073741824,
|
||||
"_fov": 45,
|
||||
"_fovAxis": 0,
|
||||
"_orthoHeight": 360,
|
||||
"_orthoHeight": 719.9999999999999,
|
||||
"_near": 1,
|
||||
"_far": 2000,
|
||||
"_color": {
|
||||
@@ -456,8 +456,8 @@
|
||||
"__prefab": null,
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 1280,
|
||||
"height": 720
|
||||
"width": 720,
|
||||
"height": 1439.9999999999998
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { find } from "cc";
|
||||
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 { UIMgr } from "../../Framework/UI/UIMgr";
|
||||
import { serviceProto } from "../../Shared/protocols/serviceProto";
|
||||
|
||||
/**
|
||||
* 应用启动状态
|
||||
@@ -15,15 +18,15 @@ export class AppStatusBoot extends BaseState {
|
||||
constructor(fsm: any) {
|
||||
super(fsm, "Boot");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 进入启动状态
|
||||
*/
|
||||
async onEnter(params?: any): Promise<void> {
|
||||
super.onEnter(params);
|
||||
|
||||
|
||||
console.log("[AppStatusBoot] 开始初始化应用...");
|
||||
|
||||
|
||||
try {
|
||||
// 初始化UI
|
||||
console.log("[AppStatusBoot] 初始化UI管理器...");
|
||||
@@ -31,32 +34,71 @@ export class AppStatusBoot extends BaseState {
|
||||
|
||||
// 1. 初始化并连接网络
|
||||
await this.initAndConnectNet();
|
||||
|
||||
|
||||
// 2. 初始化完成,切换到登录状态
|
||||
console.log("[AppStatusBoot] 启动完成,切换到登录状态");
|
||||
this._fsm.changeState("Login");
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("[AppStatusBoot] 初始化失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 初始化并连接网络
|
||||
*/
|
||||
private async initAndConnectNet(): Promise<void> {
|
||||
console.log("[AppStatusBoot] 初始化网络管理器...");
|
||||
|
||||
// TODO: 从配置文件读取服务器地址
|
||||
// import { serviceProto } from '../../Shared/protocols/serviceProto';
|
||||
// const netManager = NetManager.getInstance();
|
||||
// netManager.setServiceProto(serviceProto);
|
||||
// netManager.init({ serverUrl: 'http://localhost:3000' });
|
||||
// await netManager.connect();
|
||||
|
||||
console.log("[AppStatusBoot] 网络连接完成(待配置)");
|
||||
|
||||
// 1. 获取网络管理器实例
|
||||
const netManager = NetManager.getInstance();
|
||||
|
||||
// 2. 设置服务协议 (必须在 init 之前调用)
|
||||
netManager.setServiceProto(serviceProto);
|
||||
|
||||
// 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() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出启动状态
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { find } from "cc";
|
||||
import { BaseState } from "../../Framework/FSM/BaseState";
|
||||
import { UIMgr } from "../../Framework/UI/UIMgr";
|
||||
import { MsgResLogin, PlayerInfo } from "../../Shared/protocols/MsgResLogin";
|
||||
import { UIGame } from "../Game/UIGame";
|
||||
import { World } from "../Game/World";
|
||||
import { PlayerInfo } from "../../Shared/protocols/PtlLogin";
|
||||
|
||||
/**
|
||||
* 应用游戏状态
|
||||
@@ -13,22 +14,24 @@ import { PlayerInfo } from "../../Shared/protocols/PtlLogin";
|
||||
* - 游戏主循环
|
||||
*/
|
||||
export class AppStatusGame extends BaseState {
|
||||
private _cmd: MsgResLogin
|
||||
private _player: PlayerInfo = null;
|
||||
private _isNewPlayer: boolean = false;
|
||||
private _uiGame: UIGame = null;
|
||||
|
||||
|
||||
constructor(fsm: any) {
|
||||
super(fsm, "Game");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 进入游戏状态
|
||||
*/
|
||||
async onEnter(params?: any): Promise<void> {
|
||||
async onEnter(params?: MsgResLogin): Promise<void> {
|
||||
super.onEnter(params);
|
||||
|
||||
console.log("[AppStatusGame] 进入游戏世界");
|
||||
|
||||
|
||||
console.log("[AppStatusGame] 进入游戏世界", params);
|
||||
this._cmd = params
|
||||
|
||||
// 保存玩家信息
|
||||
if (params) {
|
||||
this._player = params.player || null;
|
||||
@@ -36,93 +39,96 @@ export class AppStatusGame extends BaseState {
|
||||
console.log(`[AppStatusGame] 玩家信息:`, this._player);
|
||||
console.log(`[AppStatusGame] 是否新玩家: ${this._isNewPlayer}`);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 1. 加载游戏场景
|
||||
await this.loadGameScene();
|
||||
|
||||
|
||||
// 2. 初始化游戏
|
||||
await this.initGame();
|
||||
|
||||
|
||||
// 3. 开始监听服务器广播
|
||||
this.listenServerMessages();
|
||||
|
||||
|
||||
// 4. 开始游戏
|
||||
this.startGame();
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("[AppStatusGame] 进入游戏失败:", error);
|
||||
// 返回登录
|
||||
this._fsm.changeState("Login");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 加载游戏场景
|
||||
*/
|
||||
private async loadGameScene(): Promise<void> {
|
||||
console.log("[AppStatusGame] 加载游戏场景...");
|
||||
|
||||
|
||||
// 加载游戏UI
|
||||
this._uiGame = await UIMgr.getInstance().load(UIGame);
|
||||
|
||||
|
||||
console.log("[AppStatusGame] 游戏场景加载完成");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 初始化游戏
|
||||
*/
|
||||
private async initGame(): Promise<void> {
|
||||
console.log("[AppStatusGame] 初始化游戏...");
|
||||
|
||||
|
||||
if (!this._uiGame) {
|
||||
throw new Error("UIGame 未加载");
|
||||
}
|
||||
|
||||
// 获取世界根节点
|
||||
const worldRoot = this._uiGame.getWorldRoot();
|
||||
const worldRoot = find("Game")
|
||||
if (!worldRoot) {
|
||||
throw new Error("世界根节点未找到");
|
||||
}
|
||||
|
||||
// 初始化世界,传入本地玩家信息
|
||||
await World.getInstance().init(worldRoot, this._player);
|
||||
|
||||
await World.getInstance().init(worldRoot, this._player, this._cmd.otherPlayers);
|
||||
|
||||
console.log("[AppStatusGame] 游戏初始化完成");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 监听服务器广播消息
|
||||
*/
|
||||
private listenServerMessages(): void {
|
||||
console.log("[AppStatusGame] 开始监听服务器广播...");
|
||||
|
||||
|
||||
// 网络消息监听已在 World 中注册
|
||||
// World 会自动处理 MsgPlayerJoin 和 MsgPlayerMove
|
||||
|
||||
|
||||
console.log("[AppStatusGame] 服务器广播监听已设置");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 开始游戏
|
||||
*/
|
||||
private startGame(): void {
|
||||
console.log("[AppStatusGame] 游戏开始!");
|
||||
|
||||
|
||||
// 游戏已启动,玩家可以通过 WASD 控制角色移动
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新游戏状态(每帧调用)
|
||||
*/
|
||||
onUpdate(dt: number): void {
|
||||
// TODO: 游戏主循环逻辑
|
||||
// 更新世界状态,包括玩家信息显示
|
||||
World.getInstance().update(dt);
|
||||
|
||||
// TODO: 其他游戏主循环逻辑
|
||||
// - 更新角色位置
|
||||
// - 检测碰撞
|
||||
// - 更新敌人AI
|
||||
// - 同步网络状态
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 暂停游戏
|
||||
*/
|
||||
@@ -132,7 +138,7 @@ export class AppStatusGame extends BaseState {
|
||||
// - 停止游戏更新
|
||||
// - 显示暂停菜单
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 恢复游戏
|
||||
*/
|
||||
@@ -142,60 +148,60 @@ export class AppStatusGame extends BaseState {
|
||||
// - 继续游戏更新
|
||||
// - 隐藏暂停菜单
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 玩家死亡
|
||||
*/
|
||||
onPlayerDeath(): void {
|
||||
console.log("[AppStatusGame] 玩家死亡");
|
||||
|
||||
|
||||
// TODO: 处理玩家死亡
|
||||
// - 显示死亡界面
|
||||
// - 显示复活选项或返回登录
|
||||
|
||||
|
||||
// 延迟后返回登录
|
||||
setTimeout(() => {
|
||||
this._fsm.changeState("Login");
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 退出游戏(返回登录)
|
||||
*/
|
||||
quitGame(): void {
|
||||
console.log("[AppStatusGame] 退出游戏");
|
||||
|
||||
|
||||
// TODO: 断开连接或通知服务器
|
||||
// const netManager = NetManager.getInstance();
|
||||
// await netManager.disconnect();
|
||||
|
||||
|
||||
// 返回登录
|
||||
this._fsm.changeState("Login");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 延迟辅助函数
|
||||
*/
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 退出游戏状态
|
||||
*/
|
||||
onExit(): void {
|
||||
super.onExit();
|
||||
console.log("[AppStatusGame] 离开游戏状态");
|
||||
|
||||
|
||||
// 清理世界
|
||||
World.clear();
|
||||
|
||||
|
||||
// 卸载游戏UI
|
||||
if (this._uiGame) {
|
||||
UIMgr.getInstance().unload(UIGame);
|
||||
this._uiGame = null;
|
||||
}
|
||||
|
||||
|
||||
this._player = null;
|
||||
}
|
||||
}
|
||||
|
||||
264
client/assets/scripts/App/Game/CameraController.ts
Normal file
264
client/assets/scripts/App/Game/CameraController.ts
Normal 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 摄像机坐标系下的屏幕坐标(Vec3),z为深度
|
||||
*/
|
||||
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] 摄像机控制器已销毁');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4414c606-7995-4f83-b83c-30c9fe840a6b",
|
||||
"uuid": "b94f3dcb-3630-4ebd-87b3-81ae3d560352",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { _decorator, Component, Node, EventKeyboard, KeyCode, Input, input, Vec3 } from 'cc';
|
||||
import { NetManager } from '../../Framework/Net/NetManager';
|
||||
import { ReqMove, ResMove } from '../../Shared/protocols/PtlMove';
|
||||
import { PlayerInfo } from '../../Shared/protocols/PtlLogin';
|
||||
import { _decorator, Component, EventKeyboard, Input, input, KeyCode, Vec3 } from 'cc';
|
||||
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;
|
||||
|
||||
@@ -41,7 +41,9 @@ export class PlayerController extends Component {
|
||||
*/
|
||||
public init(playerInfo: PlayerInfo): void {
|
||||
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 组件
|
||||
this.roleController = this.node.getComponentInChildren(RoleController);
|
||||
@@ -107,11 +109,11 @@ export class PlayerController extends Component {
|
||||
|
||||
// W - 向前
|
||||
if (this.keyStates.get(KeyCode.KEY_W)) {
|
||||
this.moveDirection.z -= 1;
|
||||
this.moveDirection.y += 1;
|
||||
}
|
||||
// S - 向后
|
||||
if (this.keyStates.get(KeyCode.KEY_S)) {
|
||||
this.moveDirection.z += 1;
|
||||
this.moveDirection.y -= 1;
|
||||
}
|
||||
// A - 向左
|
||||
if (this.keyStates.get(KeyCode.KEY_A)) {
|
||||
@@ -134,7 +136,7 @@ export class PlayerController extends Component {
|
||||
private move(dt: number): void {
|
||||
// 计算移动增量
|
||||
const moveOffset = this.moveDirection.clone().multiplyScalar(this.moveSpeed * dt);
|
||||
|
||||
|
||||
// 更新节点位置
|
||||
const currentPos = this.node.position.clone();
|
||||
currentPos.add(moveOffset);
|
||||
@@ -142,7 +144,7 @@ export class PlayerController extends Component {
|
||||
|
||||
// 更新朝向(让角色面向移动方向)
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -163,10 +165,10 @@ export class PlayerController extends Component {
|
||||
*/
|
||||
private checkSendMoveRequest(currentPos: Vec3): void {
|
||||
const distance = Vec3.distance(currentPos, this.lastSentPosition);
|
||||
|
||||
|
||||
// 如果移动距离超过阈值,发送移动请求
|
||||
if (distance >= this.moveSendThreshold) {
|
||||
this.sendMoveRequest(currentPos.x, currentPos.z);
|
||||
this.sendMoveRequest(currentPos.x, currentPos.y);
|
||||
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 {
|
||||
const netManager = NetManager.getInstance();
|
||||
const result = await netManager.callApi<ReqMove, ResMove>('Move', {
|
||||
x: x,
|
||||
y: z
|
||||
const result = await netManager.callMsg(new MoveMessagePair(), {
|
||||
x: Math.round(x * 1000),
|
||||
y: Math.round(y * 1000)
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
@@ -190,8 +192,10 @@ export class PlayerController extends Component {
|
||||
// 服务器可能会修正位置,使用服务器返回的位置
|
||||
if (result.position) {
|
||||
const serverPos = result.position;
|
||||
this.node.setPosition(serverPos.x, 0, serverPos.y);
|
||||
this.lastSentPosition.set(serverPos.x, 0, serverPos.y);
|
||||
const x = serverPos.x / 1000
|
||||
const y = serverPos.y / 1000
|
||||
this.node.setPosition(x, y, 0);
|
||||
this.lastSentPosition.set(x, y, 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[PlayerController] 发送移动请求异常:', error);
|
||||
|
||||
88
client/assets/scripts/App/Game/PlayerInfo.ts
Normal file
88
client/assets/scripts/App/Game/PlayerInfo.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/App/Game/PlayerInfo.ts.meta
Normal file
9
client/assets/scripts/App/Game/PlayerInfo.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ App/Game/
|
||||
├── World.ts # 世界管理器,管理所有玩家
|
||||
├── PlayerController.ts # 本地玩家控制器,处理输入和移动
|
||||
├── RemotePlayer.ts # 远程玩家类,处理其他玩家的显示和同步
|
||||
├── CameraController.ts # 摄像机控制器,跟随本地玩家
|
||||
├── UIGame.ts # 游戏主界面UI
|
||||
└── README.md # 本文档
|
||||
```
|
||||
@@ -82,7 +83,37 @@ await World.getInstance().init(worldRoot, playerInfo);
|
||||
- `updatePosition(position)`: 更新远程玩家位置(收到 MsgPlayerMove 时调用)
|
||||
- `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 组件
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Node, Vec3, Tween, tween } from 'cc';
|
||||
import { Position } from '../../Shared/protocols/base';
|
||||
import { Node, Tween, tween, Vec3 } from 'cc';
|
||||
import { RoleController } from '../../CC/RoleController';
|
||||
import { Position } from '../../Shared/protocols/base';
|
||||
|
||||
/**
|
||||
* RemotePlayer 远程玩家
|
||||
@@ -38,8 +38,10 @@ export class RemotePlayer {
|
||||
this.playerNode = playerNode;
|
||||
this.playerId = playerId;
|
||||
this.playerName = playerName;
|
||||
this.currentPosition.set(position.x, 0, position.y);
|
||||
this.targetPosition.set(position.x, 0, position.y);
|
||||
const x = position.x / 1000
|
||||
const y = position.y / 1000
|
||||
this.currentPosition.set(x, y, 0);
|
||||
this.targetPosition.set(x, y, 0);
|
||||
|
||||
// 获取 RoleController 组件
|
||||
this.roleController = this.playerNode.getComponentInChildren(RoleController);
|
||||
@@ -58,7 +60,7 @@ export class RemotePlayer {
|
||||
* 收到服务器广播的移动消息时调用
|
||||
*/
|
||||
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);
|
||||
@@ -77,7 +79,7 @@ export class RemotePlayer {
|
||||
|
||||
// 计算朝向
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { World } from './World';
|
||||
import { PlayerInfo, PlayerInfoData } from './PlayerInfo';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@@ -10,36 +11,120 @@ const { ccclass, property } = _decorator;
|
||||
*/
|
||||
@ccclass('UIGame')
|
||||
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 {
|
||||
console.log('[UIGame] onEnable');
|
||||
}
|
||||
|
||||
protected onDisable(): void {
|
||||
console.log('[UIGame] onDisable');
|
||||
}
|
||||
|
||||
protected onDestroy(): void {
|
||||
console.log('[UIGame] onDestroy');
|
||||
onEnd(): void {
|
||||
this.clearPlayerInfoComponents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 UI 预制体路径
|
||||
*/
|
||||
public onGetUrl(): string {
|
||||
return 'res://UI/UIGame';
|
||||
return 'res://UI/Game/UIGame';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界根节点
|
||||
* 初始化玩家信息容器
|
||||
*/
|
||||
public getWorldRoot(): Node {
|
||||
return this.worldRoot;
|
||||
private initPlayerInfoContainer(): void {
|
||||
// 查找或创建玩家信息容器
|
||||
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] 已清理所有玩家信息组件');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Node, Vec3, instantiate, Prefab } from 'cc';
|
||||
import { instantiate, Node, Prefab } from 'cc';
|
||||
import { NetManager } from '../../Framework/Net/NetManager';
|
||||
import { ResMgr } from '../../Framework/ResMgr/ResMgr';
|
||||
import { UIMgr } from '../../Framework/UI/UIMgr';
|
||||
import { MsgPlayerJoin } from '../../Shared/protocols/MsgPlayerJoin';
|
||||
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 { PlayerInfoData } from './PlayerInfo';
|
||||
import { RemotePlayer } from './RemotePlayer';
|
||||
import { UIGame } from './UIGame';
|
||||
|
||||
/**
|
||||
* World 世界管理器
|
||||
@@ -33,7 +37,13 @@ export class World {
|
||||
/** 玩家模型预制体 */
|
||||
private playerPrefab: Prefab = null;
|
||||
|
||||
private constructor() {}
|
||||
/** 摄像机控制器 */
|
||||
private cameraController: CameraController = null;
|
||||
|
||||
/** UIGame实例 */
|
||||
private uiGameInstance: UIGame = null;
|
||||
|
||||
private constructor() { }
|
||||
|
||||
public static getInstance(): World {
|
||||
if (!World.instance) {
|
||||
@@ -46,11 +56,15 @@ export class World {
|
||||
* 初始化世界
|
||||
* @param worldRoot 世界根节点
|
||||
* @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.localPlayer = localPlayer;
|
||||
|
||||
// 获取UIGame实例
|
||||
this.uiGameInstance = UIMgr.getInstance().get(UIGame) as UIGame;
|
||||
|
||||
// 加载玩家模型预制体
|
||||
await this.loadPlayerPrefab();
|
||||
|
||||
@@ -60,6 +74,9 @@ export class World {
|
||||
// 创建本地玩家
|
||||
await this.createLocalPlayer();
|
||||
|
||||
// 创建其他已在线的玩家
|
||||
await this.createOtherPlayers(otherPlayers);
|
||||
|
||||
console.log('[World] 世界初始化完成');
|
||||
}
|
||||
|
||||
@@ -68,7 +85,7 @@ export class World {
|
||||
*/
|
||||
private async loadPlayerPrefab(): Promise<void> {
|
||||
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] 玩家模型预制体加载成功');
|
||||
} catch (error) {
|
||||
console.error('[World] 加载玩家模型预制体失败:', error);
|
||||
@@ -103,19 +120,76 @@ export class World {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = this.localPlayer.position.x / 1000
|
||||
const y = this.localPlayer.position.y / 1000
|
||||
|
||||
// 实例化玩家节点
|
||||
this.localPlayerNode = instantiate(this.playerPrefab);
|
||||
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.localPlayerController = this.localPlayerNode.addComponent(PlayerController);
|
||||
this.localPlayerController.init(this.localPlayer);
|
||||
|
||||
// 创建并绑定摄像机控制器(只有本地玩家需要)
|
||||
this.cameraController = this.worldRoot.addComponent(CameraController) as CameraController;
|
||||
this.cameraController.setTarget(this.localPlayerNode);
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
// 创建远程玩家控制器
|
||||
@@ -184,6 +260,61 @@ export class World {
|
||||
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;
|
||||
|
||||
// 销毁摄像机控制器
|
||||
if (this.cameraController) {
|
||||
this.cameraController.node.removeComponent(CameraController);
|
||||
this.cameraController = null;
|
||||
}
|
||||
|
||||
// 销毁所有远程玩家
|
||||
this.remotePlayers.forEach((remotePlayer) => {
|
||||
remotePlayer.destroy();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { _decorator, EditBox, Button } from 'cc';
|
||||
import { UIBase } from '../../Framework/UI/UIBase';
|
||||
import { _decorator, Button, EditBox } from 'cc';
|
||||
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 { LoginMessagePair } from '../Msg/Pair/LoginMessagePair';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@@ -19,34 +19,34 @@ const { ccclass } = _decorator;
|
||||
export class UILogin extends UIBase {
|
||||
/** 账号输入框 */
|
||||
private _inputAccount: EditBox | null = null;
|
||||
|
||||
|
||||
/** 登录按钮 */
|
||||
private _btnLogin: Button | null = null;
|
||||
|
||||
|
||||
/** 是否正在登录中 */
|
||||
private _isLogging: boolean = false;
|
||||
|
||||
|
||||
/**
|
||||
* 获取UI资源路径
|
||||
*/
|
||||
onGetUrl(): string {
|
||||
return 'res://UI/Login/UILogin';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* UI初始化
|
||||
*/
|
||||
async onStart(params?: any): Promise<void> {
|
||||
console.log('[UILogin] 登录界面初始化', params);
|
||||
|
||||
|
||||
// 使用GetChild查找子节点
|
||||
this._inputAccount = this.GetChild('mid/input_account', EditBox);
|
||||
this._btnLogin = this.GetChild('mid/btn_login', Button);
|
||||
|
||||
|
||||
// 使用SetClick绑定点击事件
|
||||
this.SetClick(this._btnLogin, this.onLoginClick, this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 登录按钮点击
|
||||
*/
|
||||
@@ -55,21 +55,21 @@ export class UILogin extends UIBase {
|
||||
console.log('[UILogin] 正在登录中,请稍候...');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 获取账号
|
||||
const account = this._inputAccount?.string?.trim() || '';
|
||||
|
||||
|
||||
if (!account) {
|
||||
console.warn('[UILogin] 请输入账号');
|
||||
// TODO: 显示提示UI
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log(`[UILogin] 开始登录,账号: ${account}`);
|
||||
|
||||
|
||||
// 开始登录
|
||||
this._isLogging = true;
|
||||
|
||||
|
||||
try {
|
||||
await this.login(account);
|
||||
} catch (error) {
|
||||
@@ -79,29 +79,26 @@ export class UILogin extends UIBase {
|
||||
this._isLogging = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 执行登录
|
||||
* @param account 账号
|
||||
*/
|
||||
private async login(account: string): Promise<void> {
|
||||
const netManager = NetManager.getInstance();
|
||||
|
||||
|
||||
// 使用类型化的登录协议
|
||||
const result = await netManager.callApi<ReqLogin, ResLogin>('Login', {
|
||||
account: account
|
||||
});
|
||||
|
||||
const result = await netManager.callMsg(new LoginMessagePair(), {
|
||||
playerId: account
|
||||
},);
|
||||
|
||||
if (result && result.success) {
|
||||
console.log('[UILogin] 登录成功:', result);
|
||||
|
||||
|
||||
// 登录成功,切换到游戏状态
|
||||
const appStatusManager = AppStatusManager.getInstance();
|
||||
appStatusManager.changeState('Game', {
|
||||
player: result.player,
|
||||
isNewPlayer: result.isNewPlayer || false
|
||||
});
|
||||
|
||||
appStatusManager.changeState('Game', result);
|
||||
|
||||
// 隐藏登录界面
|
||||
this.hide();
|
||||
} else {
|
||||
@@ -109,7 +106,7 @@ export class UILogin extends UIBase {
|
||||
// TODO: 显示错误提示UI
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* UI清理
|
||||
*/
|
||||
|
||||
9
client/assets/scripts/App/Msg.meta
Normal file
9
client/assets/scripts/App/Msg.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "9f8d42c2-cee9-4156-b0ee-992cb5d66317",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
76
client/assets/scripts/App/Msg/MessagePairBase.ts
Normal file
76
client/assets/scripts/App/Msg/MessagePairBase.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "9fdf30ed-1223-4112-81f6-75339229fd1b",
|
||||
"uuid": "3ba46d91-073d-44d6-8157-05f1af758121",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
23
client/assets/scripts/App/Msg/MessagePairInit.ts
Normal file
23
client/assets/scripts/App/Msg/MessagePairInit.ts
Normal 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');
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "db7f0511-7e71-4088-9822-186f68082db6",
|
||||
"uuid": "9b552413-09a9-4ec6-b389-ea371b734a41",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
84
client/assets/scripts/App/Msg/MsgExample.ts
Normal file
84
client/assets/scripts/App/Msg/MsgExample.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b5331a2b-20fa-4653-8013-deb54bad8d2e",
|
||||
"uuid": "4c559445-88e3-4fe2-a87c-8c9ef390aa60",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
55
client/assets/scripts/App/Msg/MsgManager.ts
Normal file
55
client/assets/scripts/App/Msg/MsgManager.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/App/Msg/MsgManager.ts.meta
Normal file
9
client/assets/scripts/App/Msg/MsgManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "871b5daf-492f-4ff7-adac-a13bbf6b82ce",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client/assets/scripts/App/Msg/Pair.meta
Normal file
9
client/assets/scripts/App/Msg/Pair.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "6f3ea388-fd67-45a4-9bfc-cae858378b7a",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
21
client/assets/scripts/App/Msg/Pair/LoginMessagePair.ts
Normal file
21
client/assets/scripts/App/Msg/Pair/LoginMessagePair.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8a0f8893-e078-4885-8407-70944f67d185",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
23
client/assets/scripts/App/Msg/Pair/MoveMessagePair.ts
Normal file
23
client/assets/scripts/App/Msg/Pair/MoveMessagePair.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "f95aa765-9371-4644-9694-8eb8dd7d8930",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
19
client/assets/scripts/App/Msg/Pair/SendMessagePair.ts
Normal file
19
client/assets/scripts/App/Msg/Pair/SendMessagePair.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "d9ef0dad-c949-4a46-9306-d4a060a40a3b",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,29 +1,48 @@
|
||||
/**
|
||||
* 网络协议类型
|
||||
*/
|
||||
export enum NetProtocolType {
|
||||
/** HTTP/HTTPS 协议 */
|
||||
Http = "http",
|
||||
|
||||
/** WebSocket 协议 */
|
||||
WebSocket = "websocket"
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络配置接口
|
||||
*/
|
||||
export interface NetConfig {
|
||||
/** 服务器地址 */
|
||||
serverUrl: string;
|
||||
|
||||
|
||||
/** 网络协议类型 默认 Http */
|
||||
protocolType?: NetProtocolType;
|
||||
|
||||
/** 超时时间(ms) 默认 30000 */
|
||||
timeout?: number;
|
||||
|
||||
|
||||
/** 是否自动重连 默认 true */
|
||||
autoReconnect?: boolean;
|
||||
|
||||
|
||||
/** 重连间隔(ms) 默认 3000 */
|
||||
reconnectInterval?: number;
|
||||
|
||||
|
||||
/** 最大重连次数 默认 5 */
|
||||
maxReconnectTimes?: number;
|
||||
|
||||
/** 是否使用JSON格式 默认 true */
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认网络配置
|
||||
*/
|
||||
export const DefaultNetConfig: Partial<NetConfig> = {
|
||||
protocolType: NetProtocolType.Http,
|
||||
timeout: 30000,
|
||||
autoReconnect: true,
|
||||
reconnectInterval: 3000,
|
||||
maxReconnectTimes: 5
|
||||
maxReconnectTimes: 5,
|
||||
json: true
|
||||
};
|
||||
|
||||
222
client/assets/scripts/Framework/Net/NetConfigBuilder.ts
Normal file
222
client/assets/scripts/Framework/Net/NetConfigBuilder.ts
Normal 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:');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "3b215a9a-fd5d-404a-8254-dd6f3cffca6a",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -2,24 +2,27 @@
|
||||
* 网络事件定义
|
||||
*/
|
||||
export enum NetEvent {
|
||||
/** 正在连接 */
|
||||
Connecting = "net_connecting",
|
||||
|
||||
/** 连接成功 */
|
||||
Connected = "net_connected",
|
||||
|
||||
|
||||
/** 连接断开 */
|
||||
Disconnected = "net_disconnected",
|
||||
|
||||
|
||||
/** 正在重连 */
|
||||
Reconnecting = "net_reconnecting",
|
||||
|
||||
|
||||
/** 重连成功 */
|
||||
ReconnectSuccess = "net_reconnect_success",
|
||||
|
||||
|
||||
/** 重连失败 */
|
||||
ReconnectFailed = "net_reconnect_failed",
|
||||
|
||||
|
||||
/** 网络错误 */
|
||||
Error = "net_error",
|
||||
|
||||
|
||||
/** 连接超时 */
|
||||
Timeout = "net_timeout"
|
||||
}
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import { NetConfig, DefaultNetConfig } from './NetConfig';
|
||||
import { MessagePair, MessagePairRegistry } from '../../App/Msg/MessagePairBase';
|
||||
import { DefaultNetConfig, NetConfig, NetProtocolType } from './NetConfig';
|
||||
import { NetEvent } from './NetEvent';
|
||||
import { PlatformAdapter, ClientConfig } from './PlatformAdapter';
|
||||
import { ClientConfig, PlatformAdapter } from './PlatformAdapter';
|
||||
import { INetClient } from './WebSocketClient';
|
||||
|
||||
/**
|
||||
* 网络管理器单例
|
||||
* 负责管理网络连接和消息通信
|
||||
* 支持 HTTP 和 WebSocket 两种协议
|
||||
*/
|
||||
export class NetManager {
|
||||
private static _instance: NetManager | null = null;
|
||||
|
||||
/** TSRPC Client 实例 (HttpClient) */
|
||||
private _client: any = null;
|
||||
|
||||
|
||||
/** TSRPC Client 实例 (HttpClient 或 WsClient) */
|
||||
private _client: INetClient | null = null;
|
||||
|
||||
/** 是否已连接 */
|
||||
private _isConnected: boolean = false;
|
||||
|
||||
|
||||
/** 网络配置 */
|
||||
private _config: NetConfig | null = null;
|
||||
|
||||
|
||||
/** 重连计数 */
|
||||
private _reconnectCount: number = 0;
|
||||
|
||||
|
||||
/** 重连定时器 */
|
||||
private _reconnectTimer: any = null;
|
||||
|
||||
|
||||
/** 事件监听器 */
|
||||
private _eventListeners: Map<string, Function[]> = new Map();
|
||||
|
||||
private constructor() {}
|
||||
private constructor() { }
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
@@ -58,6 +61,7 @@ export class NetManager {
|
||||
} as NetConfig;
|
||||
|
||||
console.log('[NetManager] Initialized with config:', this._config);
|
||||
console.log('[NetManager] Protocol:', this._config.protocolType);
|
||||
console.log('[NetManager] Platform:', PlatformAdapter.getPlatformInfo());
|
||||
}
|
||||
|
||||
@@ -66,46 +70,56 @@ export class NetManager {
|
||||
*/
|
||||
async connect(): Promise<boolean> {
|
||||
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);
|
||||
|
||||
|
||||
// 创建客户端配置
|
||||
const clientConfig: ClientConfig = {
|
||||
server: this._config.serverUrl,
|
||||
json: true,
|
||||
timeout: this._config.timeout
|
||||
protocolType: this._config.protocolType,
|
||||
json: this._config.json,
|
||||
timeout: this._config.timeout,
|
||||
};
|
||||
|
||||
// 根据平台创建对应的客户端
|
||||
// 根据平台和协议类型创建对应的客户端
|
||||
this._client = PlatformAdapter.createClient(clientConfig);
|
||||
|
||||
// HttpClient 不需要显式连接,创建即可使用
|
||||
// 如果未来需要 WebSocket 支持,可以在这里添加 connect() 调用
|
||||
|
||||
// 如果是 WebSocket 客户端,需要显式连接
|
||||
if (this._config.protocolType === NetProtocolType.WebSocket && this._client.connect) {
|
||||
this.emit(NetEvent.Connecting);
|
||||
|
||||
const result = await this._client.connect();
|
||||
if (!result.isSucc) {
|
||||
console.error('[NetManager] WebSocket connection failed:', result.errMsg);
|
||||
this.emit(NetEvent.Error, result.errMsg);
|
||||
|
||||
// 尝试重连
|
||||
if (this._config.autoReconnect) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this._isConnected = true;
|
||||
this._reconnectCount = 0;
|
||||
|
||||
this.emit(NetEvent.Connected);
|
||||
console.log('[NetManager] Client created successfully');
|
||||
|
||||
// client 可能不需要显式连接
|
||||
// 对于 WsClient 需要调用 connect()
|
||||
|
||||
this._isConnected = true;
|
||||
this._reconnectCount = 0;
|
||||
|
||||
|
||||
this.emit(NetEvent.Connected);
|
||||
console.log('[NetManager] Connected successfully');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[NetManager] Connection failed:', error);
|
||||
this.emit(NetEvent.Error, error);
|
||||
|
||||
|
||||
// 尝试重连
|
||||
if (this._config.autoReconnect) {
|
||||
if (this._config?.autoReconnect) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -114,26 +128,26 @@ export class NetManager {
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (!this._isConnected) {
|
||||
if (!this._isConnected || !this._client) {
|
||||
return;
|
||||
}
|
||||
// HttpClient 无需显式断开连接
|
||||
// 如果使用 WebSocket,可以在这里调用 disconnect()
|
||||
|
||||
this._isConnected = false;
|
||||
this._client = null;
|
||||
if(this._reconnectTimer){
|
||||
|
||||
console.log('[NetManager] Disconnecting...');
|
||||
|
||||
// 清除重连定时器
|
||||
if (this._reconnectTimer) {
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = null;
|
||||
}
|
||||
|
||||
// TODO: 调用客户端的断开连接方法
|
||||
if (this._client && typeof this._client.disconnect === 'function') {
|
||||
// 如果是 WebSocket 客户端,调用断开连接方法
|
||||
if (this._client.disconnect) {
|
||||
this._client.disconnect();
|
||||
}
|
||||
|
||||
this._isConnected = false;
|
||||
this._client = null;
|
||||
|
||||
|
||||
this.emit(NetEvent.Disconnected);
|
||||
console.log('[NetManager] Disconnected');
|
||||
}
|
||||
@@ -151,13 +165,12 @@ export class NetManager {
|
||||
|
||||
try {
|
||||
console.log(`[NetManager] Calling API: ${apiName}`, req);
|
||||
|
||||
// TODO: 根据实际的协议定义调用 API
|
||||
|
||||
const result = await this._client.callApi(apiName, req);
|
||||
|
||||
|
||||
if (result.isSucc) {
|
||||
console.log(`[NetManager] API ${apiName} success:`, result.res);
|
||||
return result.res;
|
||||
return result.res as Res;
|
||||
} else {
|
||||
console.error(`[NetManager] API ${apiName} failed:`, result.err);
|
||||
return null;
|
||||
@@ -170,7 +183,7 @@ export class NetManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听消息
|
||||
* 监听消息 (仅支持 WebSocket)
|
||||
* @param msgName 消息名称
|
||||
* @param handler 处理函数
|
||||
*/
|
||||
@@ -180,16 +193,91 @@ export class NetManager {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[NetManager] Listening message: ${msgName}`);
|
||||
|
||||
// TODO: 根据实际的 TSRPC 客户端实现监听消息
|
||||
if (typeof this._client.listenMsg === 'function') {
|
||||
this._client.listenMsg(msgName, handler);
|
||||
if (!this._client.listenMsg) {
|
||||
console.warn('[NetManager] Message listening not supported for HTTP client');
|
||||
return;
|
||||
}
|
||||
|
||||
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 msg 消息内容
|
||||
*/
|
||||
@@ -199,12 +287,13 @@ export class NetManager {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[NetManager] Sending message: ${msgName}`, msg);
|
||||
|
||||
// TODO: 根据实际的 TSRPC 客户端实现发送消息
|
||||
if (typeof this._client.sendMsg === 'function') {
|
||||
this._client.sendMsg(msgName, msg);
|
||||
if (!this._client.sendMsg) {
|
||||
console.warn('[NetManager] Message sending not supported for HTTP client');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[NetManager] Sending message: ${msgName}`, msg);
|
||||
this._client.sendMsg(msgName, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,9 +312,9 @@ export class NetManager {
|
||||
|
||||
this._reconnectCount++;
|
||||
console.log(`[NetManager] Scheduling reconnect (${this._reconnectCount}/${this._config.maxReconnectTimes})...`);
|
||||
|
||||
|
||||
this.emit(NetEvent.Reconnecting, this._reconnectCount);
|
||||
|
||||
|
||||
this._reconnectTimer = setTimeout(() => {
|
||||
this.reconnect();
|
||||
}, this._config.reconnectInterval || 3000);
|
||||
@@ -236,9 +325,9 @@ export class NetManager {
|
||||
*/
|
||||
private async reconnect(): Promise<void> {
|
||||
console.log('[NetManager] Reconnecting...');
|
||||
|
||||
|
||||
const success = await this.connect();
|
||||
|
||||
|
||||
if (success) {
|
||||
this.emit(NetEvent.ReconnectSuccess);
|
||||
} else if (this._config?.autoReconnect) {
|
||||
@@ -301,7 +390,41 @@ export class NetManager {
|
||||
/**
|
||||
* 获取客户端实例
|
||||
*/
|
||||
get client(): any {
|
||||
get client(): INetClient | null {
|
||||
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
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { sys } from 'cc';
|
||||
// 使用别名导入避免命名冲突
|
||||
import { BaseServiceType, HttpClient as HttpClientBrowser } from 'tsrpc-browser';
|
||||
import { HttpClient as HttpClientMiniapp } from 'tsrpc-miniapp';
|
||||
import { BaseServiceType, HttpClient as HttpClientBrowser, WsClient as WsClientBrowser } from 'tsrpc-browser';
|
||||
import { HttpClient as HttpClientMiniapp, WsClient as WsClientMiniapp } from 'tsrpc-miniapp';
|
||||
import { NetProtocolType } from './NetConfig';
|
||||
import { INetClient, WebSocketClientWrapper } from './WebSocketClient';
|
||||
|
||||
/**
|
||||
* 平台类型枚举
|
||||
@@ -9,10 +11,10 @@ import { HttpClient as HttpClientMiniapp } from 'tsrpc-miniapp';
|
||||
export enum PlatformType {
|
||||
/** 浏览器平台 */
|
||||
Browser = "browser",
|
||||
|
||||
|
||||
/** 小程序平台 (微信、抖音、QQ等) */
|
||||
MiniApp = "miniapp",
|
||||
|
||||
|
||||
/** 未知平台 */
|
||||
Unknown = "unknown"
|
||||
}
|
||||
@@ -23,22 +25,29 @@ export enum PlatformType {
|
||||
export interface ClientConfig {
|
||||
/** 服务器地址 */
|
||||
server: string;
|
||||
|
||||
|
||||
/** 网络协议类型 */
|
||||
protocolType?: NetProtocolType;
|
||||
|
||||
/** 是否使用 JSON 格式 (默认 true) */
|
||||
json?: boolean;
|
||||
|
||||
|
||||
/** 超时时间(ms) */
|
||||
timeout?: number;
|
||||
|
||||
|
||||
/** 其他配置 */
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台适配器
|
||||
* 根据当前平台返回对应的 TSRPC 客户端实现
|
||||
* 根据当前平台和协议类型返回对应的 TSRPC 客户端实现
|
||||
*
|
||||
* 注意: TSRPC 不同平台的库中 API 是重名的,所以使用别名导入
|
||||
* 支持协议:
|
||||
* - HTTP/HTTPS: 用于无状态的API调用
|
||||
* - WebSocket: 用于实时双向通信
|
||||
*
|
||||
* 支持平台:
|
||||
* - tsrpc-browser: 用于浏览器和 XMLHttpRequest 兼容的环境
|
||||
* - tsrpc-miniapp: 用于微信、抖音、QQ 等小程序环境
|
||||
*/
|
||||
@@ -94,11 +103,12 @@ export class PlatformAdapter {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建对应平台的 TSRPC 客户端实例
|
||||
* 创建对应平台和协议的 TSRPC 客户端实例
|
||||
* @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 protocolType = config.protocolType || NetProtocolType.Http;
|
||||
|
||||
// 默认配置
|
||||
const defaultConfig: ClientConfig = {
|
||||
@@ -115,19 +125,50 @@ export class PlatformAdapter {
|
||||
console.warn('[PlatformAdapter] Service protocol not set, please call setServiceProto() first');
|
||||
}
|
||||
|
||||
console.log(`[PlatformAdapter] Creating ${protocolType} client for ${platform}:`, defaultConfig.server);
|
||||
|
||||
// 根据协议类型和平台创建对应的客户端
|
||||
if (protocolType === NetProtocolType.WebSocket) {
|
||||
return this.createWebSocketClient<T>(platform, defaultConfig);
|
||||
} else {
|
||||
return this.createHttpClient<T>(platform, defaultConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 HTTP 客户端
|
||||
*/
|
||||
private static createHttpClient<T extends BaseServiceType>(platform: PlatformType, config: ClientConfig): INetClient<T> {
|
||||
let client: HttpClientBrowser<T> | HttpClientMiniapp<T>;
|
||||
|
||||
// 根据平台创建对应的客户端
|
||||
if (platform === PlatformType.MiniApp) {
|
||||
console.log('[PlatformAdapter] Creating MiniApp client:', defaultConfig.server);
|
||||
client = new HttpClientMiniapp(this._serviceProto, defaultConfig) as HttpClientMiniapp<T>;
|
||||
client = new HttpClientMiniapp(this._serviceProto, config) as HttpClientMiniapp<T>;
|
||||
} else {
|
||||
// 浏览器和其他 XMLHttpRequest 兼容环境使用 Browser 客户端
|
||||
console.log('[PlatformAdapter] Creating Browser client:', defaultConfig.server);
|
||||
client = new HttpClientBrowser(this._serviceProto, defaultConfig) as HttpClientBrowser<T>;
|
||||
client = new HttpClientBrowser(this._serviceProto, config) as HttpClientBrowser<T>;
|
||||
}
|
||||
|
||||
return client;
|
||||
// 包装 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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/)
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "6eb67b95-26c3-410a-a9ee-441d4fa23371",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
237
client/assets/scripts/Framework/Net/WebSocketClient.ts
Normal file
237
client/assets/scripts/Framework/Net/WebSocketClient.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4a49d854-9ed8-4344-825f-9439bb287819",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export interface MsgPlayerJoin {
|
||||
/** 玩家昵称 */
|
||||
playerName: string;
|
||||
|
||||
/** 玩家位置 */
|
||||
/** 玩家位置(客户端坐标,放大1000倍后的整数) */
|
||||
position: Position;
|
||||
|
||||
/** 是否新玩家 */
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface MsgPlayerMove {
|
||||
/** 玩家昵称 */
|
||||
playerName: string;
|
||||
|
||||
/** 移动后的位置 */
|
||||
/** 移动后的位置(客户端坐标,放大1000倍后的整数) */
|
||||
position: Position;
|
||||
|
||||
/** 移动时间戳 */
|
||||
|
||||
11
client/assets/scripts/Shared/protocols/MsgReqLogin.ts
Normal file
11
client/assets/scripts/Shared/protocols/MsgReqLogin.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
/**
|
||||
* 登录请求消息
|
||||
*/
|
||||
export interface MsgReqLogin {
|
||||
/** 玩家ID(用于识别玩家) */
|
||||
playerId: string;
|
||||
|
||||
/** 玩家昵称(可选,新玩家时使用) */
|
||||
playerName?: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "24fdb58b-b594-4bcf-8695-2669bd9bf9ca",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
10
client/assets/scripts/Shared/protocols/MsgReqMove.ts
Normal file
10
client/assets/scripts/Shared/protocols/MsgReqMove.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 移动请求消息
|
||||
*/
|
||||
export interface MsgReqMove {
|
||||
/** X坐标(放大1000倍后取整,服务器内部除以1000作为实际坐标) */
|
||||
x: number;
|
||||
|
||||
/** Y坐标(放大1000倍后取整,服务器内部除以1000作为实际坐标) */
|
||||
y: number;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b244e6e0-857f-41f2-ac85-c6130e61218b",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
7
client/assets/scripts/Shared/protocols/MsgReqSend.ts
Normal file
7
client/assets/scripts/Shared/protocols/MsgReqSend.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 发送消息请求
|
||||
*/
|
||||
export interface MsgReqSend {
|
||||
/** 消息内容 */
|
||||
content: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b6e9ef16-0098-4050-a133-721d339159e5",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,61 +1,53 @@
|
||||
import { Position } from './base';
|
||||
|
||||
/**
|
||||
* 登录请求
|
||||
*/
|
||||
export interface ReqLogin {
|
||||
/** 玩家ID(用于识别玩家) */
|
||||
playerId: string;
|
||||
|
||||
/** 玩家昵称(可选,新玩家时使用) */
|
||||
playerName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家角色信息
|
||||
*/
|
||||
export interface PlayerInfo {
|
||||
/** 玩家ID */
|
||||
id: string;
|
||||
|
||||
/** 玩家昵称 */
|
||||
name: string;
|
||||
|
||||
/** 当前位置 */
|
||||
position: Position;
|
||||
|
||||
/** 出生点 */
|
||||
spawnPoint: Position;
|
||||
|
||||
/** 当前生命值 */
|
||||
hp: number;
|
||||
|
||||
/** 最大生命值 */
|
||||
maxHp: number;
|
||||
|
||||
/** 是否存活 */
|
||||
isAlive: boolean;
|
||||
|
||||
/** 创建时间 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后登录时间 */
|
||||
lastLoginAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应
|
||||
*/
|
||||
export interface ResLogin {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
|
||||
/** 消息 */
|
||||
message: string;
|
||||
|
||||
/** 玩家信息 */
|
||||
player?: PlayerInfo;
|
||||
|
||||
/** 是否新玩家 */
|
||||
isNewPlayer?: boolean;
|
||||
}
|
||||
import { Position } from './base';
|
||||
|
||||
/**
|
||||
* 玩家角色信息
|
||||
*/
|
||||
export interface PlayerInfo {
|
||||
/** 玩家ID */
|
||||
id: string;
|
||||
|
||||
/** 玩家昵称 */
|
||||
name: string;
|
||||
|
||||
/** 当前位置(客户端坐标,放大1000倍后的整数) */
|
||||
position: Position;
|
||||
|
||||
/** 出生点(客户端坐标,放大1000倍后的整数) */
|
||||
spawnPoint: Position;
|
||||
|
||||
/** 当前生命值 */
|
||||
hp: number;
|
||||
|
||||
/** 最大生命值 */
|
||||
maxHp: number;
|
||||
|
||||
/** 是否存活 */
|
||||
isAlive: boolean;
|
||||
|
||||
/** 创建时间 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后登录时间 */
|
||||
lastLoginAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应消息
|
||||
*/
|
||||
export interface MsgResLogin {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
|
||||
/** 消息 */
|
||||
message: string;
|
||||
|
||||
/** 玩家信息 */
|
||||
player?: PlayerInfo;
|
||||
|
||||
/** 是否新玩家 */
|
||||
isNewPlayer?: boolean;
|
||||
|
||||
/** 房间内其他在线玩家信息 */
|
||||
otherPlayers?: PlayerInfo[];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "dcefce3e-1a92-45ac-8d63-94fb1370edae",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
15
client/assets/scripts/Shared/protocols/MsgResMove.ts
Normal file
15
client/assets/scripts/Shared/protocols/MsgResMove.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Position } from './base';
|
||||
|
||||
/**
|
||||
* 移动响应消息
|
||||
*/
|
||||
export interface MsgResMove {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
|
||||
/** 消息 */
|
||||
message: string;
|
||||
|
||||
/** 新位置(成功时返回,客户端坐标,放大1000倍后的整数) */
|
||||
position?: Position;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e7ba8c78-ba4b-4dc6-a3de-352a5444421a",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
7
client/assets/scripts/Shared/protocols/MsgResSend.ts
Normal file
7
client/assets/scripts/Shared/protocols/MsgResSend.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 发送消息响应
|
||||
*/
|
||||
export interface MsgResSend {
|
||||
/** 发送时间 */
|
||||
time: Date;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "1fecff7f-ad47-4829-b229-e89e6e2e301e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export interface BaseMessage {
|
||||
}
|
||||
|
||||
/**
|
||||
* 位置坐标
|
||||
* 位置坐标(客户端使用放大1000倍后的整数坐标)
|
||||
*/
|
||||
export interface Position {
|
||||
x: number;
|
||||
|
||||
@@ -2,34 +2,32 @@ import { ServiceProto } from 'tsrpc-proto';
|
||||
import { MsgChat } from './MsgChat';
|
||||
import { MsgPlayerJoin } from './MsgPlayerJoin';
|
||||
import { MsgPlayerMove } from './MsgPlayerMove';
|
||||
import { ReqLogin, ResLogin } from './PtlLogin';
|
||||
import { ReqMove, ResMove } from './PtlMove';
|
||||
import { ReqSend, ResSend } from './PtlSend';
|
||||
import { MsgReqLogin } from './MsgReqLogin';
|
||||
import { MsgReqMove } from './MsgReqMove';
|
||||
import { MsgReqSend } from './MsgReqSend';
|
||||
import { MsgResLogin } from './MsgResLogin';
|
||||
import { MsgResMove } from './MsgResMove';
|
||||
import { MsgResSend } from './MsgResSend';
|
||||
|
||||
export interface ServiceType {
|
||||
api: {
|
||||
"Login": {
|
||||
req: ReqLogin,
|
||||
res: ResLogin
|
||||
},
|
||||
"Move": {
|
||||
req: ReqMove,
|
||||
res: ResMove
|
||||
},
|
||||
"Send": {
|
||||
req: ReqSend,
|
||||
res: ResSend
|
||||
}
|
||||
|
||||
},
|
||||
msg: {
|
||||
"Chat": MsgChat,
|
||||
"PlayerJoin": MsgPlayerJoin,
|
||||
"PlayerMove": MsgPlayerMove
|
||||
"PlayerMove": MsgPlayerMove,
|
||||
"ReqLogin": MsgReqLogin,
|
||||
"ReqMove": MsgReqMove,
|
||||
"ReqSend": MsgReqSend,
|
||||
"ResLogin": MsgResLogin,
|
||||
"ResMove": MsgResMove,
|
||||
"ResSend": MsgResSend
|
||||
}
|
||||
}
|
||||
|
||||
export const serviceProto: ServiceProto<ServiceType> = {
|
||||
"version": 3,
|
||||
"version": 6,
|
||||
"services": [
|
||||
{
|
||||
"id": 0,
|
||||
@@ -47,19 +45,34 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Login",
|
||||
"type": "api"
|
||||
"id": 6,
|
||||
"name": "ReqLogin",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Move",
|
||||
"type": "api"
|
||||
"id": 7,
|
||||
"name": "ReqMove",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Send",
|
||||
"type": "api"
|
||||
"id": 8,
|
||||
"name": "ReqSend",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "ResLogin",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "ResMove",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "ResSend",
|
||||
"type": "msg"
|
||||
}
|
||||
],
|
||||
"types": {
|
||||
@@ -176,18 +189,18 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlLogin/ReqLogin": {
|
||||
"MsgReqLogin/MsgReqLogin": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 1,
|
||||
"id": 0,
|
||||
"name": "playerId",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"id": 1,
|
||||
"name": "playerName",
|
||||
"type": {
|
||||
"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",
|
||||
"properties": [
|
||||
{
|
||||
@@ -214,25 +258,37 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"id": 2,
|
||||
"name": "player",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "PtlLogin/PlayerInfo"
|
||||
"target": "MsgResLogin/PlayerInfo"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"id": 3,
|
||||
"name": "isNewPlayer",
|
||||
"type": {
|
||||
"type": "Boolean"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "otherPlayers",
|
||||
"type": {
|
||||
"type": "Array",
|
||||
"elementType": {
|
||||
"type": "Reference",
|
||||
"target": "MsgResLogin/PlayerInfo"
|
||||
}
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlLogin/PlayerInfo": {
|
||||
"MsgResLogin/PlayerInfo": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
@@ -302,26 +358,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlMove/ReqMove": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "x",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "y",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlMove/ResMove": {
|
||||
"MsgResMove/MsgResMove": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
@@ -336,8 +373,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
"name": "message",
|
||||
"type": {
|
||||
"type": "String"
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
@@ -350,19 +386,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlSend/ReqSend": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "content",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlSend/ResSend": {
|
||||
"MsgResSend/MsgResSend": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
111
client/preview-template/dist/assets/index.691dfd76.js
vendored
Normal file
111
client/preview-template/dist/assets/index.691dfd76.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/preview-template/dist/assets/index.741f95c0.css
vendored
Normal file
1
client/preview-template/dist/assets/index.741f95c0.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
client/preview-template/dist/index.html
vendored
7
client/preview-template/dist/index.html
vendored
@@ -1,3 +1,4 @@
|
||||
<script type="module" crossorigin src="/dist/assets/index.02b86726.js"></script>
|
||||
<link rel="stylesheet" href="/dist/assets/index.1d01bced.css">
|
||||
<div id="app"></div>
|
||||
<script type="module" crossorigin src="/dist/assets/index.691dfd76.js"></script>
|
||||
<link rel="stylesheet" href="/dist/assets/index.741f95c0.css">
|
||||
<div id="dev-app" style="width: 400px;height: 100%;display: flex;flex-direction: column;justify-content: center;"></div>
|
||||
|
||||
|
||||
@@ -19,29 +19,57 @@
|
||||
</head>
|
||||
<body>
|
||||
<%- include(cocosToolBar, {config: config}) %>
|
||||
<div id="content" class="content">
|
||||
<div class="contentWrap">
|
||||
<div id="GameDiv" class="wrapper">
|
||||
<div id="Cocos3dGameContainer">
|
||||
<canvas id="GameCanvas"></canvas>
|
||||
</div>
|
||||
<div id="splash">
|
||||
<div class="progress-bar stripes"><span></span></div>
|
||||
</div>
|
||||
<div id="bulletin">
|
||||
<div id="sceneIsEmpty" class="inner"><%=tip_sceneIsEmpty%></div>
|
||||
</div>
|
||||
<div class="error" id="error">
|
||||
<div class="title">Error <i>(Please open the console to see detailed errors)</i></div>
|
||||
<div class="error-main"></div>
|
||||
<div style="display: flex;flex: auto;align-items: center;">
|
||||
<%- include ./dist/index.html %>
|
||||
<div id="content" class="content">
|
||||
<div class="contentWrap">
|
||||
<div id="GameDiv" class="wrapper">
|
||||
<div id="Cocos3dGameContainer">
|
||||
<canvas id="GameCanvas" tabindex="-1" style="background-color: '';"></canvas>
|
||||
</div>
|
||||
<div id="splash">
|
||||
<div class="progress-bar stripes"><span></span></div>
|
||||
</div>
|
||||
<div id="bulletin">
|
||||
<div id="sceneIsEmpty" class="inner"><%=tip_sceneIsEmpty%></div>
|
||||
</div>
|
||||
<div class="error" id="error">
|
||||
<div class="title">Error <i>(Please open the console to see detailed errors)</i></div>
|
||||
<div class="error-main"></div>
|
||||
<div class="error-stack"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer">
|
||||
Created with <a href="https://www.cocos.com/products" target="_blank" title="Cocos Creator">Cocos Creator</a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="footer">
|
||||
<% include ./dist/index.html %>
|
||||
</p>
|
||||
</div>
|
||||
<%- include(cocosTemplate, {}) %>
|
||||
</body>
|
||||
</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>
|
||||
|
||||
@@ -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"}
|
||||
@@ -1,3 +1,9 @@
|
||||
{
|
||||
"__version__": "1.0.6"
|
||||
"__version__": "1.0.6",
|
||||
"general": {
|
||||
"designResolution": {
|
||||
"width": 720,
|
||||
"height": 1440
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ async function syncShared() {
|
||||
// 确保目标目录存在
|
||||
await fs.ensureDir(CLIENT_SHARED_PATH);
|
||||
|
||||
// 清空目标目录(删除旧文件)
|
||||
console.log('🗑️ 正在清空目标目录...');
|
||||
await fs.emptyDir(CLIENT_SHARED_PATH);
|
||||
|
||||
// 复制目录
|
||||
console.log('📦 正在复制文件...');
|
||||
await fs.copy(SERVER_SHARED_PATH, CLIENT_SHARED_PATH, {
|
||||
@@ -52,7 +56,7 @@ async function syncShared() {
|
||||
|
||||
console.log('✅ 同步完成!');
|
||||
console.log('');
|
||||
|
||||
|
||||
// 列出同步的文件
|
||||
const files = await getFileList(CLIENT_SHARED_PATH);
|
||||
console.log(`共同步 ${files.length} 个文件:`);
|
||||
@@ -60,10 +64,10 @@ async function syncShared() {
|
||||
const relativePath = path.relative(CLIENT_SHARED_PATH, file);
|
||||
console.log(' -', relativePath);
|
||||
});
|
||||
|
||||
|
||||
console.log('');
|
||||
console.log('==========================================');
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 同步失败:', error.message);
|
||||
process.exit(1);
|
||||
@@ -75,14 +79,14 @@ async function syncShared() {
|
||||
*/
|
||||
async function getFileList(dir) {
|
||||
const files = [];
|
||||
|
||||
|
||||
async function walk(currentPath) {
|
||||
const items = await fs.readdir(currentPath);
|
||||
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(currentPath, item);
|
||||
const stat = await fs.stat(itemPath);
|
||||
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
await walk(itemPath);
|
||||
} else {
|
||||
@@ -90,7 +94,7 @@ async function getFileList(dir) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
/* Base configuration. Do not edit this field. */
|
||||
"extends": "./temp/tsconfig.cocos.json",
|
||||
|
||||
/* Add your custom configuration here. */
|
||||
"compilerOptions": {
|
||||
"strict": false,
|
||||
"target": "es2020",
|
||||
"downlevelIteration": true
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
}
|
||||
32
server/.github/instructions/development-tasks.md
vendored
32
server/.github/instructions/development-tasks.md
vendored
@@ -80,6 +80,14 @@ Roguelike 游戏服务端开发任务追踪
|
||||
- [x] 移动API实现 (ApiMove.ts)
|
||||
- [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 框架
|
||||
- TypeScript
|
||||
@@ -94,5 +123,6 @@ Roguelike 游戏服务端开发任务追踪
|
||||
|
||||
## 备注
|
||||
- 本文档由 AI 助手维护
|
||||
- 更新日期: 2025-12-14
|
||||
- 更新日期: 2025-12-18
|
||||
- 开发过程中会根据实际情况调整任务优先级和细节
|
||||
- **重要**:项目已从HTTP API架构迁移到WebSocket消息架构,请严格按照消息协议规范开发
|
||||
|
||||
@@ -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 : '未知错误'}`);
|
||||
}
|
||||
}
|
||||
@@ -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 : '未知错误'}`);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -3,13 +3,13 @@
|
||||
*/
|
||||
export const WorldConfig = {
|
||||
/** 世界宽度 */
|
||||
WORLD_WIDTH: 800,
|
||||
WORLD_WIDTH: 1440,
|
||||
|
||||
/** 世界高度 */
|
||||
WORLD_HEIGHT: 800,
|
||||
WORLD_HEIGHT: 1440,
|
||||
|
||||
/** 出生区域半径(距离中心点) */
|
||||
SPAWN_RADIUS: 200,
|
||||
SPAWN_RADIUS: 10,
|
||||
|
||||
/** 世界中心点 X 坐标 */
|
||||
get CENTER_X() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as path from "path";
|
||||
import { WsServer } from "tsrpc";
|
||||
import { serviceProto } from './shared/protocols/serviceProto';
|
||||
import { worldManager } from './managers/WorldManager';
|
||||
import { serviceProto } from './shared/protocols/serviceProto';
|
||||
|
||||
// Create the Server
|
||||
export const server = new WsServer(serviceProto, {
|
||||
@@ -12,7 +11,11 @@ export const server = new WsServer(serviceProto, {
|
||||
|
||||
// Initialize before server start
|
||||
async function init() {
|
||||
await server.autoImplementApi(path.resolve(__dirname, 'api'));
|
||||
// 不再使用API,改为消息监听
|
||||
// await server.autoImplementApi(path.resolve(__dirname, 'api'));
|
||||
|
||||
// 注册消息监听器
|
||||
await registerMessageHandlers();
|
||||
|
||||
// 初始化游戏世界
|
||||
console.log('正在初始化游戏世界...');
|
||||
@@ -20,6 +23,23 @@ async function init() {
|
||||
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
|
||||
async function main() {
|
||||
await init();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 位置坐标接口
|
||||
* 位置坐标接口(服务器内部使用实际坐标,与客户端通信时需要乘以1000)
|
||||
*/
|
||||
export interface Position {
|
||||
x: number;
|
||||
|
||||
99
server/src/msg/MsgReqLogin.ts
Normal file
99
server/src/msg/MsgReqLogin.ts
Normal 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 : '未知错误'}`
|
||||
});
|
||||
}
|
||||
}
|
||||
107
server/src/msg/MsgReqMove.ts
Normal file
107
server/src/msg/MsgReqMove.ts
Normal 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 : '未知错误'}`
|
||||
});
|
||||
}
|
||||
}
|
||||
28
server/src/msg/MsgReqSend.ts
Normal file
28
server/src/msg/MsgReqSend.ts
Normal 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
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export interface MsgPlayerJoin {
|
||||
/** 玩家昵称 */
|
||||
playerName: string;
|
||||
|
||||
/** 玩家位置 */
|
||||
/** 玩家位置(客户端坐标,放大1000倍后的整数) */
|
||||
position: Position;
|
||||
|
||||
/** 是否新玩家 */
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface MsgPlayerMove {
|
||||
/** 玩家昵称 */
|
||||
playerName: string;
|
||||
|
||||
/** 移动后的位置 */
|
||||
/** 移动后的位置(客户端坐标,放大1000倍后的整数) */
|
||||
position: Position;
|
||||
|
||||
/** 移动时间戳 */
|
||||
|
||||
11
server/src/shared/protocols/MsgReqLogin.ts
Normal file
11
server/src/shared/protocols/MsgReqLogin.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
/**
|
||||
* 登录请求消息
|
||||
*/
|
||||
export interface MsgReqLogin {
|
||||
/** 玩家ID(用于识别玩家) */
|
||||
playerId: string;
|
||||
|
||||
/** 玩家昵称(可选,新玩家时使用) */
|
||||
playerName?: string;
|
||||
}
|
||||
10
server/src/shared/protocols/MsgReqMove.ts
Normal file
10
server/src/shared/protocols/MsgReqMove.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 移动请求消息
|
||||
*/
|
||||
export interface MsgReqMove {
|
||||
/** X坐标(放大1000倍后取整,服务器内部除以1000作为实际坐标) */
|
||||
x: number;
|
||||
|
||||
/** Y坐标(放大1000倍后取整,服务器内部除以1000作为实际坐标) */
|
||||
y: number;
|
||||
}
|
||||
7
server/src/shared/protocols/MsgReqSend.ts
Normal file
7
server/src/shared/protocols/MsgReqSend.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 发送消息请求
|
||||
*/
|
||||
export interface MsgReqSend {
|
||||
/** 消息内容 */
|
||||
content: string;
|
||||
}
|
||||
@@ -1,61 +1,53 @@
|
||||
import { Position } from './base';
|
||||
|
||||
/**
|
||||
* 登录请求
|
||||
*/
|
||||
export interface ReqLogin {
|
||||
/** 玩家ID(用于识别玩家) */
|
||||
playerId: string;
|
||||
|
||||
/** 玩家昵称(可选,新玩家时使用) */
|
||||
playerName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家角色信息
|
||||
*/
|
||||
export interface PlayerInfo {
|
||||
/** 玩家ID */
|
||||
id: string;
|
||||
|
||||
/** 玩家昵称 */
|
||||
name: string;
|
||||
|
||||
/** 当前位置 */
|
||||
position: Position;
|
||||
|
||||
/** 出生点 */
|
||||
spawnPoint: Position;
|
||||
|
||||
/** 当前生命值 */
|
||||
hp: number;
|
||||
|
||||
/** 最大生命值 */
|
||||
maxHp: number;
|
||||
|
||||
/** 是否存活 */
|
||||
isAlive: boolean;
|
||||
|
||||
/** 创建时间 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后登录时间 */
|
||||
lastLoginAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应
|
||||
*/
|
||||
export interface ResLogin {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
|
||||
/** 消息 */
|
||||
message: string;
|
||||
|
||||
/** 玩家信息 */
|
||||
player?: PlayerInfo;
|
||||
|
||||
/** 是否新玩家 */
|
||||
isNewPlayer?: boolean;
|
||||
}
|
||||
import { Position } from './base';
|
||||
|
||||
/**
|
||||
* 玩家角色信息
|
||||
*/
|
||||
export interface PlayerInfo {
|
||||
/** 玩家ID */
|
||||
id: string;
|
||||
|
||||
/** 玩家昵称 */
|
||||
name: string;
|
||||
|
||||
/** 当前位置(客户端坐标,放大1000倍后的整数) */
|
||||
position: Position;
|
||||
|
||||
/** 出生点(客户端坐标,放大1000倍后的整数) */
|
||||
spawnPoint: Position;
|
||||
|
||||
/** 当前生命值 */
|
||||
hp: number;
|
||||
|
||||
/** 最大生命值 */
|
||||
maxHp: number;
|
||||
|
||||
/** 是否存活 */
|
||||
isAlive: boolean;
|
||||
|
||||
/** 创建时间 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后登录时间 */
|
||||
lastLoginAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应消息
|
||||
*/
|
||||
export interface MsgResLogin {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
|
||||
/** 消息 */
|
||||
message: string;
|
||||
|
||||
/** 玩家信息 */
|
||||
player?: PlayerInfo;
|
||||
|
||||
/** 是否新玩家 */
|
||||
isNewPlayer?: boolean;
|
||||
|
||||
/** 房间内其他在线玩家信息 */
|
||||
otherPlayers?: PlayerInfo[];
|
||||
}
|
||||
15
server/src/shared/protocols/MsgResMove.ts
Normal file
15
server/src/shared/protocols/MsgResMove.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Position } from './base';
|
||||
|
||||
/**
|
||||
* 移动响应消息
|
||||
*/
|
||||
export interface MsgResMove {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
|
||||
/** 消息 */
|
||||
message: string;
|
||||
|
||||
/** 新位置(成功时返回,客户端坐标,放大1000倍后的整数) */
|
||||
position?: Position;
|
||||
}
|
||||
7
server/src/shared/protocols/MsgResSend.ts
Normal file
7
server/src/shared/protocols/MsgResSend.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 发送消息响应
|
||||
*/
|
||||
export interface MsgResSend {
|
||||
/** 发送时间 */
|
||||
time: Date;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export interface BaseMessage {
|
||||
}
|
||||
|
||||
/**
|
||||
* 位置坐标
|
||||
* 位置坐标(客户端使用放大1000倍后的整数坐标)
|
||||
*/
|
||||
export interface Position {
|
||||
x: number;
|
||||
|
||||
@@ -2,34 +2,32 @@ import { ServiceProto } from 'tsrpc-proto';
|
||||
import { MsgChat } from './MsgChat';
|
||||
import { MsgPlayerJoin } from './MsgPlayerJoin';
|
||||
import { MsgPlayerMove } from './MsgPlayerMove';
|
||||
import { ReqLogin, ResLogin } from './PtlLogin';
|
||||
import { ReqMove, ResMove } from './PtlMove';
|
||||
import { ReqSend, ResSend } from './PtlSend';
|
||||
import { MsgReqLogin } from './MsgReqLogin';
|
||||
import { MsgReqMove } from './MsgReqMove';
|
||||
import { MsgReqSend } from './MsgReqSend';
|
||||
import { MsgResLogin } from './MsgResLogin';
|
||||
import { MsgResMove } from './MsgResMove';
|
||||
import { MsgResSend } from './MsgResSend';
|
||||
|
||||
export interface ServiceType {
|
||||
api: {
|
||||
"Login": {
|
||||
req: ReqLogin,
|
||||
res: ResLogin
|
||||
},
|
||||
"Move": {
|
||||
req: ReqMove,
|
||||
res: ResMove
|
||||
},
|
||||
"Send": {
|
||||
req: ReqSend,
|
||||
res: ResSend
|
||||
}
|
||||
|
||||
},
|
||||
msg: {
|
||||
"Chat": MsgChat,
|
||||
"PlayerJoin": MsgPlayerJoin,
|
||||
"PlayerMove": MsgPlayerMove
|
||||
"PlayerMove": MsgPlayerMove,
|
||||
"ReqLogin": MsgReqLogin,
|
||||
"ReqMove": MsgReqMove,
|
||||
"ReqSend": MsgReqSend,
|
||||
"ResLogin": MsgResLogin,
|
||||
"ResMove": MsgResMove,
|
||||
"ResSend": MsgResSend
|
||||
}
|
||||
}
|
||||
|
||||
export const serviceProto: ServiceProto<ServiceType> = {
|
||||
"version": 3,
|
||||
"version": 6,
|
||||
"services": [
|
||||
{
|
||||
"id": 0,
|
||||
@@ -47,19 +45,34 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Login",
|
||||
"type": "api"
|
||||
"id": 6,
|
||||
"name": "ReqLogin",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Move",
|
||||
"type": "api"
|
||||
"id": 7,
|
||||
"name": "ReqMove",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Send",
|
||||
"type": "api"
|
||||
"id": 8,
|
||||
"name": "ReqSend",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "ResLogin",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "ResMove",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "ResSend",
|
||||
"type": "msg"
|
||||
}
|
||||
],
|
||||
"types": {
|
||||
@@ -176,18 +189,18 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlLogin/ReqLogin": {
|
||||
"MsgReqLogin/MsgReqLogin": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 1,
|
||||
"id": 0,
|
||||
"name": "playerId",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"id": 1,
|
||||
"name": "playerName",
|
||||
"type": {
|
||||
"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",
|
||||
"properties": [
|
||||
{
|
||||
@@ -214,25 +258,37 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"id": 2,
|
||||
"name": "player",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "PtlLogin/PlayerInfo"
|
||||
"target": "MsgResLogin/PlayerInfo"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"id": 3,
|
||||
"name": "isNewPlayer",
|
||||
"type": {
|
||||
"type": "Boolean"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "otherPlayers",
|
||||
"type": {
|
||||
"type": "Array",
|
||||
"elementType": {
|
||||
"type": "Reference",
|
||||
"target": "MsgResLogin/PlayerInfo"
|
||||
}
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlLogin/PlayerInfo": {
|
||||
"MsgResLogin/PlayerInfo": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
@@ -302,26 +358,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlMove/ReqMove": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "x",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "y",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlMove/ResMove": {
|
||||
"MsgResMove/MsgResMove": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
@@ -336,8 +373,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
"name": "message",
|
||||
"type": {
|
||||
"type": "String"
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
@@ -350,19 +386,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlSend/ReqSend": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "content",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlSend/ResSend": {
|
||||
"MsgResSend/MsgResSend": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
|
||||
50
server/src/utils/coordinate.ts
Normal file
50
server/src/utils/coordinate.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user