From 3612ee74ead2188ac1d2257689bb47016e45de8d Mon Sep 17 00:00:00 2001 From: janing Date: Sun, 14 Dec 2025 23:35:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=B8=E6=88=8F=E9=80=BB=E8=BE=91=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/assets/scripts/App/Game.meta | 9 + .../scripts/App/Game/PlayerController.ts | 207 +++++++++++ .../scripts/App/Game/PlayerController.ts.meta | 9 + client/assets/scripts/App/Game/README.md | 325 ++++++++++++++++++ client/assets/scripts/App/Game/README.md.meta | 11 + .../assets/scripts/App/Game/RemotePlayer.ts | 156 +++++++++ .../scripts/App/Game/RemotePlayer.ts.meta | 9 + client/assets/scripts/App/Game/UIGame.ts | 45 +++ client/assets/scripts/App/Game/UIGame.ts.meta | 9 + client/assets/scripts/App/Game/World.ts | 228 ++++++++++++ client/assets/scripts/App/Game/World.ts.meta | 9 + 11 files changed, 1017 insertions(+) create mode 100644 client/assets/scripts/App/Game.meta create mode 100644 client/assets/scripts/App/Game/PlayerController.ts create mode 100644 client/assets/scripts/App/Game/PlayerController.ts.meta create mode 100644 client/assets/scripts/App/Game/README.md create mode 100644 client/assets/scripts/App/Game/README.md.meta create mode 100644 client/assets/scripts/App/Game/RemotePlayer.ts create mode 100644 client/assets/scripts/App/Game/RemotePlayer.ts.meta create mode 100644 client/assets/scripts/App/Game/UIGame.ts create mode 100644 client/assets/scripts/App/Game/UIGame.ts.meta create mode 100644 client/assets/scripts/App/Game/World.ts create mode 100644 client/assets/scripts/App/Game/World.ts.meta diff --git a/client/assets/scripts/App/Game.meta b/client/assets/scripts/App/Game.meta new file mode 100644 index 0000000..2a5d8ed --- /dev/null +++ b/client/assets/scripts/App/Game.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "7884d98f-e4a9-4e92-aed3-214cddfcd2b4", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/client/assets/scripts/App/Game/PlayerController.ts b/client/assets/scripts/App/Game/PlayerController.ts new file mode 100644 index 0000000..fa200ee --- /dev/null +++ b/client/assets/scripts/App/Game/PlayerController.ts @@ -0,0 +1,207 @@ +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 { RoleController } from '../../CC/RoleController'; + +const { ccclass } = _decorator; + +/** + * PlayerController 本地玩家控制器 + * 负责处理本地玩家的输入、移动和动画 + */ +@ccclass('PlayerController') +export class PlayerController extends Component { + /** 玩家信息 */ + private playerInfo: PlayerInfo = null; + + /** 角色控制器(控制动画) */ + private roleController: RoleController = null; + + /** 移动速度 */ + private moveSpeed: number = 5; + + /** 当前移动方向 */ + private moveDirection: Vec3 = new Vec3(0, 0, 0); + + /** 按键状态 */ + private keyStates: Map = new Map(); + + /** 是否正在移动 */ + private isMoving: boolean = false; + + /** 上次发送移动请求的位置 */ + private lastSentPosition: Vec3 = new Vec3(); + + /** 移动阈值(超过这个距离才发送移动请求) */ + private moveSendThreshold: number = 0.5; + + /** + * 初始化玩家控制器 + */ + public init(playerInfo: PlayerInfo): void { + this.playerInfo = playerInfo; + this.lastSentPosition.set(playerInfo.position.x, 0, playerInfo.position.y); + + // 获取 RoleController 组件 + this.roleController = this.node.getComponentInChildren(RoleController); + if (!this.roleController) { + console.warn('[PlayerController] 未找到 RoleController 组件'); + } else { + // 初始播放待机动画 + this.roleController.PlayAnimation('idle', true); + } + + console.log('[PlayerController] 初始化完成:', playerInfo.name); + } + + protected onEnable(): void { + // 注册键盘事件 + input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this); + input.on(Input.EventType.KEY_UP, this.onKeyUp, this); + } + + protected onDisable(): void { + // 取消注册键盘事件 + input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this); + input.off(Input.EventType.KEY_UP, this.onKeyUp, this); + } + + /** + * 键盘按下事件 + */ + private onKeyDown(event: EventKeyboard): void { + this.keyStates.set(event.keyCode, true); + } + + /** + * 键盘抬起事件 + */ + private onKeyUp(event: EventKeyboard): void { + this.keyStates.set(event.keyCode, false); + } + + protected update(dt: number): void { + // 计算移动方向 + this.updateMoveDirection(); + + // 如果有移动方向,移动角色 + if (this.moveDirection.lengthSqr() > 0) { + this.move(dt); + } else { + // 没有移动,播放待机动画 + if (this.isMoving) { + this.isMoving = false; + if (this.roleController) { + this.roleController.PlayAnimation('idle', true); + } + } + } + } + + /** + * 更新移动方向 + */ + private updateMoveDirection(): void { + this.moveDirection.set(0, 0, 0); + + // W - 向前 + if (this.keyStates.get(KeyCode.KEY_W)) { + this.moveDirection.z -= 1; + } + // S - 向后 + if (this.keyStates.get(KeyCode.KEY_S)) { + this.moveDirection.z += 1; + } + // A - 向左 + if (this.keyStates.get(KeyCode.KEY_A)) { + this.moveDirection.x -= 1; + } + // D - 向右 + if (this.keyStates.get(KeyCode.KEY_D)) { + this.moveDirection.x += 1; + } + + // 归一化移动方向 + if (this.moveDirection.lengthSqr() > 0) { + this.moveDirection.normalize(); + } + } + + /** + * 移动角色 + */ + private move(dt: number): void { + // 计算移动增量 + const moveOffset = this.moveDirection.clone().multiplyScalar(this.moveSpeed * dt); + + // 更新节点位置 + const currentPos = this.node.position.clone(); + currentPos.add(moveOffset); + this.node.setPosition(currentPos); + + // 更新朝向(让角色面向移动方向) + if (this.moveDirection.lengthSqr() > 0) { + const targetAngle = Math.atan2(this.moveDirection.x, -this.moveDirection.z) * (180 / Math.PI); + this.node.setRotationFromEuler(0, targetAngle, 0); + } + + // 播放移动动画 + if (!this.isMoving) { + this.isMoving = true; + if (this.roleController) { + this.roleController.PlayAnimation('move', true); + } + } + + // 检查是否需要发送移动请求 + this.checkSendMoveRequest(currentPos); + } + + /** + * 检查是否需要发送移动请求 + */ + private checkSendMoveRequest(currentPos: Vec3): void { + const distance = Vec3.distance(currentPos, this.lastSentPosition); + + // 如果移动距离超过阈值,发送移动请求 + if (distance >= this.moveSendThreshold) { + this.sendMoveRequest(currentPos.x, currentPos.z); + this.lastSentPosition.set(currentPos); + } + } + + /** + * 发送移动请求到服务器 + */ + private async sendMoveRequest(x: number, z: number): Promise { + try { + const netManager = NetManager.getInstance(); + const result = await netManager.callApi('Move', { + x: x, + y: z + }); + + if (!result) { + console.error('[PlayerController] 移动请求失败'); + return; + } + + // 服务器可能会修正位置,使用服务器返回的位置 + if (result.position) { + const serverPos = result.position; + this.node.setPosition(serverPos.x, 0, serverPos.y); + this.lastSentPosition.set(serverPos.x, 0, serverPos.y); + } + } catch (error) { + console.error('[PlayerController] 发送移动请求异常:', error); + } + } + + /** + * 获取玩家信息 + */ + public getPlayerInfo(): PlayerInfo { + return this.playerInfo; + } +} diff --git a/client/assets/scripts/App/Game/PlayerController.ts.meta b/client/assets/scripts/App/Game/PlayerController.ts.meta new file mode 100644 index 0000000..21116be --- /dev/null +++ b/client/assets/scripts/App/Game/PlayerController.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "9e1f7c66-cb9e-4e5f-8642-605d3568c4ac", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/client/assets/scripts/App/Game/README.md b/client/assets/scripts/App/Game/README.md new file mode 100644 index 0000000..b99fa30 --- /dev/null +++ b/client/assets/scripts/App/Game/README.md @@ -0,0 +1,325 @@ +# Game 模块 + +## 概述 + +Game 模块是游戏的核心业务模块,负责管理游戏世界、玩家控制、网络同步等功能。该模块基于玩家输入发送给服务器,服务器广播给所有玩家的在线游戏开发思路,确保帧间同步。 + +## 目录结构 + +``` +App/Game/ +├── World.ts # 世界管理器,管理所有玩家 +├── PlayerController.ts # 本地玩家控制器,处理输入和移动 +├── RemotePlayer.ts # 远程玩家类,处理其他玩家的显示和同步 +├── UIGame.ts # 游戏主界面UI +└── README.md # 本文档 +``` + +## 核心类说明 + +### 1. World (世界管理器) + +**职责:** +- 管理游戏世界中的所有玩家(本地玩家 + 远程玩家) +- 加载玩家模型预制体 +- 监听网络消息(MsgPlayerJoin、MsgPlayerMove) +- 创建和销毁玩家对象 + +**主要方法:** +- `init(worldRoot, localPlayer)`: 初始化世界,传入世界根节点和本地玩家信息 +- `destroy()`: 销毁世界,清理所有资源 +- `getLocalPlayerController()`: 获取本地玩家控制器 + +**使用示例:** +```typescript +import { World } from './App/Game/World'; +import { PlayerInfo } from './Shared/protocols/PtlLogin'; + +const worldRoot = this.node; // 世界根节点 +const playerInfo: PlayerInfo = { ... }; // 登录返回的玩家信息 + +await World.getInstance().init(worldRoot, playerInfo); +``` + +### 2. PlayerController (本地玩家控制器) + +**职责:** +- 监听键盘输入(WASD) +- 控制本地玩家移动 +- 控制角色动画(待机、移动) +- 向服务器发送移动请求(PtlMove) + +**主要特性:** +- **WASD 移动控制**: W(前)、S(后)、A(左)、D(右) +- **自动朝向**: 角色会自动面向移动方向 +- **动画切换**: 移动时播放 move 动画,静止时播放 idle 动画 +- **阈值同步**: 移动距离超过 0.5 单位时才向服务器发送移动请求,减少网络负担 + +**主要方法:** +- `init(playerInfo)`: 初始化玩家控制器 +- `getPlayerInfo()`: 获取玩家信息 + +**内部实现:** +```typescript +// 键盘输入 → 计算移动方向 → 更新本地位置 → 检查阈值 → 发送网络请求 +``` + +### 3. RemotePlayer (远程玩家) + +**职责:** +- 显示其他玩家的角色模型 +- 接收服务器广播的移动消息 +- 平滑移动到目标位置(使用 Tween 补间) +- 控制远程玩家的动画 + +**主要特性:** +- **平滑移动**: 使用 Tween 让远程玩家平滑移动到目标位置 +- **自动朝向**: 根据移动方向自动旋转角色 +- **动画同步**: 移动时播放 move 动画,到达目标后播放 idle 动画 + +**主要方法:** +- `init(playerNode, playerId, playerName, position)`: 初始化远程玩家 +- `updatePosition(position)`: 更新远程玩家位置(收到 MsgPlayerMove 时调用) +- `destroy()`: 销毁远程玩家 + +### 4. UIGame (游戏主界面) + +**职责:** +- 游戏主界面 UI 组件 +- 提供世界根节点(用于创建玩家) +- 继承自 Framework/UI/UIBase + +**主要方法:** +- `onGetUrl()`: 返回 UI 预制体路径 `res://UI/UIGame` +- `getWorldRoot()`: 获取世界根节点 + +## 协议使用 + +### 1. 登录返回数据 (ResLogin) + +登录成功后,服务器返回玩家信息,包含: +- `player`: 玩家详细信息(id、name、position 等) +- `isNewPlayer`: 是否新玩家 + +该数据在进入 Game 状态时传递给 World 和 PlayerController。 + +### 2. 移动请求 (PtlMove) + +**请求 (ReqMove):** +```typescript +{ + x: number, // 目标 X 坐标 + y: number // 目标 Y 坐标(对应 3D 中的 Z 轴) +} +``` + +**响应 (ResMove):** +```typescript +{ + success: boolean, + message?: string, + position?: Position // 服务器可能修正的位置 +} +``` + +### 3. 玩家加入广播 (MsgPlayerJoin) + +当有新玩家登录或加入游戏时,服务器广播给所有在线玩家: +```typescript +{ + playerId: string, + playerName: string, + position: Position, + isNewPlayer: boolean, + timestamp: number +} +``` + +### 4. 玩家移动广播 (MsgPlayerMove) + +当有玩家移动时,服务器广播给所有在线玩家: +```typescript +{ + playerId: string, + playerName: string, + position: Position, + timestamp: number +} +``` + +## 工作流程 + +### 初始化流程 + +``` +登录成功 (ResLogin) + ↓ +进入 AppStatusGame 状态 + ↓ +加载 UIGame 界面 + ↓ +初始化 World + ↓ +加载玩家模型预制体 (res://Actor/M1/M1) + ↓ +注册网络监听 (MsgPlayerJoin, MsgPlayerMove) + ↓ +创建本地玩家 (PlayerController) + ↓ +游戏开始 +``` + +### 本地玩家移动流程 + +``` +按下 WASD 键 + ↓ +PlayerController 计算移动方向 + ↓ +更新本地节点位置 + ↓ +播放 move 动画 + ↓ +检查移动距离是否超过阈值 (0.5) + ↓ +发送 PtlMove 请求到服务器 + ↓ +服务器返回确认 + ↓ +(可选)使用服务器修正的位置 +``` + +### 远程玩家同步流程 + +``` +服务器广播 MsgPlayerMove + ↓ +World 接收消息 + ↓ +查找对应的 RemotePlayer + ↓ +RemotePlayer.updatePosition() + ↓ +计算移动方向和距离 + ↓ +启动 Tween 补间动画 + ↓ +播放 move 动画 + ↓ +平滑移动到目标位置 + ↓ +到达后播放 idle 动画 +``` + +## 角色模型和动画 + +### 模型路径 +- 预制体路径: `res://Actor/M1/M1` +- 使用 ResMgr 加载 + +### RoleController 脚本 + +模型下挂载了 `RoleController` 脚本,可以控制角色动画: +- `PlayAnimation(action, loop)`: 播放指定动画 + - `action`: "idle"(待机) | "move"(移动) | "attack"(攻击) | "death"(死亡) + - `loop`: 是否循环播放 + +**使用示例:** +```typescript +const roleController = playerNode.getComponentInChildren(RoleController); +roleController.PlayAnimation('move', true); // 播放移动动画并循环 +roleController.PlayAnimation('idle', true); // 播放待机动画并循环 +``` + +## 输入控制 + +### 键盘映射 +- **W**: 向前移动(世界坐标 -Z 方向) +- **S**: 向后移动(世界坐标 +Z 方向) +- **A**: 向左移动(世界坐标 -X 方向) +- **D**: 向右移动(世界坐标 +X 方向) + +### 移动特性 +- **多方向合成**: 可以同时按多个方向键,移动方向会自动合成并归一化 +- **移动速度**: 默认 5 单位/秒 +- **自动朝向**: 角色会自动旋转面向移动方向 + +## 网络同步策略 + +### 本地玩家 +- **客户端预测**: 立即更新本地位置,无需等待服务器确认 +- **阈值同步**: 移动距离超过 0.5 单位时才发送网络请求,减少网络流量 +- **服务器修正**: 如果服务器返回修正后的位置,使用服务器位置 + +### 远程玩家 +- **延迟补偿**: 使用 Tween 补间动画平滑移动到目标位置 +- **基于速度的时间**: 根据距离和移动速度计算补间时间,确保移动看起来自然 + +## 常见问题 + +### 1. 如何修改移动速度? + +在 `PlayerController.ts` 中修改: +```typescript +private moveSpeed: number = 5; // 修改为你想要的速度 +``` + +### 2. 如何修改网络同步阈值? + +在 `PlayerController.ts` 中修改: +```typescript +private moveSendThreshold: number = 0.5; // 修改为你想要的阈值 +``` + +### 3. 如何添加更多动画? + +在 RoleController 中添加对应的动画剪辑,然后在 PlayerController 或 RemotePlayer 中调用: +```typescript +this.roleController.PlayAnimation('attack', false); // 播放攻击动画,不循环 +``` + +### 4. 如何处理玩家离线? + +目前未实现玩家离线处理,可以添加 `MsgPlayerLeave` 消息监听: +```typescript +netManager.listenMsg('PlayerLeave', (msg) => { + const remotePlayer = this.remotePlayers.get(msg.playerId); + if (remotePlayer) { + remotePlayer.destroy(); + this.remotePlayers.delete(msg.playerId); + } +}); +``` + +## 待实现功能 + +- [ ] 玩家名称显示 +- [ ] 玩家血条显示 +- [ ] 玩家离线处理 +- [ ] 战斗系统 +- [ ] 技能系统 +- [ ] 物品拾取 +- [ ] 聊天系统 UI +- [ ] 小地图 + +## 注意事项 + +1. **坐标系转换**: Cocos 3D 使用 (x, y, z),服务器协议使用 (x, y),需要注意: + - 服务器的 y 对应 Cocos 的 z + - Cocos 的 y 是高度,固定为 0 + +2. **网络消息监听**: 所有网络消息监听在 World 中注册,退出时需要取消监听 + +3. **资源释放**: 退出游戏时需要调用 `World.clear()` 清理所有资源 + +4. **单例模式**: World 使用单例模式,确保全局只有一个实例 + +5. **UI 预制体**: 需要在 Cocos Creator 中创建 `res://UI/UIGame` 预制体,并添加 `worldRoot` 节点 + +## 相关文档 + +- [Framework/FSM 模块文档](../../Framework/FSM/README.md) +- [Framework/Net 模块文档](../../Framework/Net/README.md) +- [Framework/ResMgr 模块文档](../../Framework/ResMgr/README.md) +- [Framework/UI 模块文档](../../Framework/UI/README.md) +- [App/AppStatus 模块文档](../AppStatus/README.md) diff --git a/client/assets/scripts/App/Game/README.md.meta b/client/assets/scripts/App/Game/README.md.meta new file mode 100644 index 0000000..d818848 --- /dev/null +++ b/client/assets/scripts/App/Game/README.md.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.0.1", + "importer": "text", + "imported": true, + "uuid": "fe661d46-0792-4a28-9fcc-11ef37219910", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/client/assets/scripts/App/Game/RemotePlayer.ts b/client/assets/scripts/App/Game/RemotePlayer.ts new file mode 100644 index 0000000..b7576f0 --- /dev/null +++ b/client/assets/scripts/App/Game/RemotePlayer.ts @@ -0,0 +1,156 @@ +import { Node, Vec3, Tween, tween } from 'cc'; +import { Position } from '../../Shared/protocols/base'; +import { RoleController } from '../../CC/RoleController'; + +/** + * RemotePlayer 远程玩家 + * 负责管理远程玩家的显示和位置同步 + */ +export class RemotePlayer { + /** 玩家节点 */ + private playerNode: Node = null; + + /** 玩家ID */ + private playerId: string = ''; + + /** 玩家名称 */ + private playerName: string = ''; + + /** 当前位置 */ + private currentPosition: Vec3 = new Vec3(); + + /** 目标位置 */ + private targetPosition: Vec3 = new Vec3(); + + /** 角色控制器(控制动画) */ + private roleController: RoleController = null; + + /** 移动补间 */ + private moveTween: Tween = null; + + /** 是否正在移动 */ + private isMoving: boolean = false; + + /** + * 初始化远程玩家 + */ + public init(playerNode: Node, playerId: string, playerName: string, position: Position): void { + 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); + + // 获取 RoleController 组件 + this.roleController = this.playerNode.getComponentInChildren(RoleController); + if (!this.roleController) { + console.warn('[RemotePlayer] 未找到 RoleController 组件'); + } else { + // 初始播放待机动画 + this.roleController.PlayAnimation('idle', true); + } + + console.log('[RemotePlayer] 初始化完成:', playerName); + } + + /** + * 更新位置 + * 收到服务器广播的移动消息时调用 + */ + public updatePosition(position: Position): void { + this.targetPosition.set(position.x, 0, position.y); + + // 计算移动方向和距离 + const direction = this.targetPosition.clone().subtract(this.currentPosition); + const distance = direction.length(); + + // 如果距离太小,不做处理 + if (distance < 0.1) { + return; + } + + // 停止之前的移动补间 + if (this.moveTween) { + this.moveTween.stop(); + this.moveTween = null; + } + + // 计算朝向 + if (distance > 0.01) { + const targetAngle = Math.atan2(direction.x, -direction.z) * (180 / Math.PI); + this.playerNode.setRotationFromEuler(0, targetAngle, 0); + } + + // 播放移动动画 + if (!this.isMoving) { + this.isMoving = true; + if (this.roleController) { + this.roleController.PlayAnimation('move', true); + } + } + + // 计算移动时间(根据距离,假设速度为5单位/秒) + const moveSpeed = 5; + const moveDuration = distance / moveSpeed; + + // 创建移动补间 + this.moveTween = tween(this.playerNode) + .to(moveDuration, { position: this.targetPosition }, { + onUpdate: (target, ratio) => { + // 更新当前位置 + this.currentPosition.set(this.playerNode.position); + }, + onComplete: () => { + // 移动完成,播放待机动画 + this.isMoving = false; + if (this.roleController) { + this.roleController.PlayAnimation('idle', true); + } + this.moveTween = null; + } + }) + .start(); + } + + /** + * 获取玩家ID + */ + public getPlayerId(): string { + return this.playerId; + } + + /** + * 获取玩家名称 + */ + public getPlayerName(): string { + return this.playerName; + } + + /** + * 获取玩家节点 + */ + public getPlayerNode(): Node { + return this.playerNode; + } + + /** + * 销毁远程玩家 + */ + public destroy(): void { + // 停止移动补间 + if (this.moveTween) { + this.moveTween.stop(); + this.moveTween = null; + } + + // 销毁节点 + if (this.playerNode) { + this.playerNode.destroy(); + this.playerNode = null; + } + + this.roleController = null; + + console.log('[RemotePlayer] 已销毁:', this.playerName); + } +} diff --git a/client/assets/scripts/App/Game/RemotePlayer.ts.meta b/client/assets/scripts/App/Game/RemotePlayer.ts.meta new file mode 100644 index 0000000..2d10fd6 --- /dev/null +++ b/client/assets/scripts/App/Game/RemotePlayer.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "48c1adc3-a2a6-4f7f-97b2-4df92ee822ee", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/client/assets/scripts/App/Game/UIGame.ts b/client/assets/scripts/App/Game/UIGame.ts new file mode 100644 index 0000000..6966a38 --- /dev/null +++ b/client/assets/scripts/App/Game/UIGame.ts @@ -0,0 +1,45 @@ +import { _decorator, Component, Node } from 'cc'; +import { UIBase } from '../../Framework/UI/UIBase'; +import { World } from './World'; + +const { ccclass, property } = _decorator; + +/** + * UIGame 游戏主界面 + * 显示游戏UI和世界场景 + */ +@ccclass('UIGame') +export class UIGame extends UIBase { + @property(Node) + worldRoot: Node = null; + + protected onLoad(): void { + console.log('[UIGame] onLoad'); + } + + protected onEnable(): void { + console.log('[UIGame] onEnable'); + } + + protected onDisable(): void { + console.log('[UIGame] onDisable'); + } + + protected onDestroy(): void { + console.log('[UIGame] onDestroy'); + } + + /** + * 获取 UI 预制体路径 + */ + public onGetUrl(): string { + return 'res://UI/UIGame'; + } + + /** + * 获取世界根节点 + */ + public getWorldRoot(): Node { + return this.worldRoot; + } +} diff --git a/client/assets/scripts/App/Game/UIGame.ts.meta b/client/assets/scripts/App/Game/UIGame.ts.meta new file mode 100644 index 0000000..f6ea045 --- /dev/null +++ b/client/assets/scripts/App/Game/UIGame.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "1fa0f67a-24a5-4acc-a866-d5c288f16fc7", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/client/assets/scripts/App/Game/World.ts b/client/assets/scripts/App/Game/World.ts new file mode 100644 index 0000000..e37f3ef --- /dev/null +++ b/client/assets/scripts/App/Game/World.ts @@ -0,0 +1,228 @@ +import { Node, Vec3, instantiate, Prefab } from 'cc'; +import { NetManager } from '../../Framework/Net/NetManager'; +import { ResMgr } from '../../Framework/ResMgr/ResMgr'; +import { MsgPlayerJoin } from '../../Shared/protocols/MsgPlayerJoin'; +import { MsgPlayerMove } from '../../Shared/protocols/MsgPlayerMove'; +import { PlayerInfo } from '../../Shared/protocols/PtlLogin'; +import { PlayerController } from './PlayerController'; +import { RemotePlayer } from './RemotePlayer'; + +/** + * World 世界管理器 + * 负责管理游戏世界中的所有玩家 + * 包括本地玩家和远程玩家的创建、更新和销毁 + */ +export class World { + private static instance: World = null; + + /** 世界根节点 */ + private worldRoot: Node = null; + + /** 本地玩家信息 */ + private localPlayer: PlayerInfo = null; + + /** 本地玩家控制器 */ + private localPlayerController: PlayerController = null; + + /** 本地玩家节点 */ + private localPlayerNode: Node = null; + + /** 远程玩家列表 Map */ + private remotePlayers: Map = new Map(); + + /** 玩家模型预制体 */ + private playerPrefab: Prefab = null; + + private constructor() {} + + public static getInstance(): World { + if (!World.instance) { + World.instance = new World(); + } + return World.instance; + } + + /** + * 初始化世界 + * @param worldRoot 世界根节点 + * @param localPlayer 本地玩家信息 + */ + public async init(worldRoot: Node, localPlayer: PlayerInfo): Promise { + this.worldRoot = worldRoot; + this.localPlayer = localPlayer; + + // 加载玩家模型预制体 + await this.loadPlayerPrefab(); + + // 注册网络消息监听 + this.registerNetworkListeners(); + + // 创建本地玩家 + await this.createLocalPlayer(); + + console.log('[World] 世界初始化完成'); + } + + /** + * 加载玩家模型预制体 + */ + private async loadPlayerPrefab(): Promise { + try { + this.playerPrefab = await ResMgr.getInstance().load('resources', 'res://Actor/M1/M1', Prefab); + console.log('[World] 玩家模型预制体加载成功'); + } catch (error) { + console.error('[World] 加载玩家模型预制体失败:', error); + } + } + + /** + * 注册网络消息监听 + */ + private registerNetworkListeners(): void { + const netManager = NetManager.getInstance(); + + // 监听玩家加入消息 + netManager.listenMsg('PlayerJoin', (msg: MsgPlayerJoin) => { + this.onPlayerJoin(msg); + }); + + // 监听玩家移动消息 + netManager.listenMsg('PlayerMove', (msg: MsgPlayerMove) => { + this.onPlayerMove(msg); + }); + + console.log('[World] 网络消息监听注册完成'); + } + + /** + * 创建本地玩家 + */ + private async createLocalPlayer(): Promise { + if (!this.playerPrefab) { + console.error('[World] 玩家模型预制体未加载'); + return; + } + + // 实例化玩家节点 + 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.worldRoot.addChild(this.localPlayerNode); + + // 创建本地玩家控制器 + this.localPlayerController = this.localPlayerNode.addComponent(PlayerController); + this.localPlayerController.init(this.localPlayer); + + console.log('[World] 本地玩家创建完成:', this.localPlayer.name); + } + + /** + * 处理玩家加入消息 + */ + private onPlayerJoin(msg: MsgPlayerJoin): void { + console.log('[World] 玩家加入:', msg.playerName); + + // 如果是本地玩家,不处理 + if (msg.playerId === this.localPlayer.id) { + return; + } + + // 创建远程玩家 + this.createRemotePlayer(msg); + } + + /** + * 创建远程玩家 + */ + private async createRemotePlayer(msg: MsgPlayerJoin): Promise { + if (!this.playerPrefab) { + console.error('[World] 玩家模型预制体未加载'); + return; + } + + // 检查是否已存在 + if (this.remotePlayers.has(msg.playerId)) { + console.warn('[World] 远程玩家已存在:', msg.playerId); + return; + } + + // 实例化玩家节点 + const playerNode = instantiate(this.playerPrefab); + playerNode.name = `Player_${msg.playerId}_Remote`; + playerNode.setPosition(msg.position.x, 0, msg.position.y); + this.worldRoot.addChild(playerNode); + + // 创建远程玩家控制器 + const remotePlayer = new RemotePlayer(); + remotePlayer.init(playerNode, msg.playerId, msg.playerName, msg.position); + + this.remotePlayers.set(msg.playerId, remotePlayer); + + console.log('[World] 远程玩家创建完成:', msg.playerName); + } + + /** + * 处理玩家移动消息 + */ + private onPlayerMove(msg: MsgPlayerMove): void { + // 如果是本地玩家,不处理 + if (msg.playerId === this.localPlayer.id) { + return; + } + + // 更新远程玩家位置 + const remotePlayer = this.remotePlayers.get(msg.playerId); + if (remotePlayer) { + remotePlayer.updatePosition(msg.position); + } + } + + /** + * 获取本地玩家控制器 + */ + public getLocalPlayerController(): PlayerController { + return this.localPlayerController; + } + + /** + * 销毁世界 + */ + public destroy(): void { + // 注意: TSRPC 的 listenMsg 不提供取消监听的方法 + // 在实际使用中,监听会在连接断开时自动清除 + + // 销毁本地玩家 + if (this.localPlayerNode) { + this.localPlayerNode.destroy(); + this.localPlayerNode = null; + } + this.localPlayerController = null; + + // 销毁所有远程玩家 + this.remotePlayers.forEach((remotePlayer) => { + remotePlayer.destroy(); + }); + this.remotePlayers.clear(); + + // 释放资源 + if (this.playerPrefab) { + ResMgr.getInstance().release('resources', 'res://Actor/M1/M1'); + this.playerPrefab = null; + } + + this.worldRoot = null; + this.localPlayer = null; + + console.log('[World] 世界已销毁'); + } + + /** + * 清理单例 + */ + public static clear(): void { + if (World.instance) { + World.instance.destroy(); + World.instance = null; + } + } +} diff --git a/client/assets/scripts/App/Game/World.ts.meta b/client/assets/scripts/App/Game/World.ts.meta new file mode 100644 index 0000000..8231a28 --- /dev/null +++ b/client/assets/scripts/App/Game/World.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "80116901-37bd-4c32-85b3-da5aefa12b10", + "files": [], + "subMetas": {}, + "userData": {} +}