游戏逻辑模块
This commit is contained in:
9
client/assets/scripts/App/Game.meta
Normal file
9
client/assets/scripts/App/Game.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "7884d98f-e4a9-4e92-aed3-214cddfcd2b4",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
207
client/assets/scripts/App/Game/PlayerController.ts
Normal file
207
client/assets/scripts/App/Game/PlayerController.ts
Normal file
@@ -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<KeyCode, boolean> = 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<void> {
|
||||
try {
|
||||
const netManager = NetManager.getInstance();
|
||||
const result = await netManager.callApi<ReqMove, ResMove>('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;
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/App/Game/PlayerController.ts.meta
Normal file
9
client/assets/scripts/App/Game/PlayerController.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "9e1f7c66-cb9e-4e5f-8642-605d3568c4ac",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
325
client/assets/scripts/App/Game/README.md
Normal file
325
client/assets/scripts/App/Game/README.md
Normal file
@@ -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)
|
||||
11
client/assets/scripts/App/Game/README.md.meta
Normal file
11
client/assets/scripts/App/Game/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "fe661d46-0792-4a28-9fcc-11ef37219910",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
156
client/assets/scripts/App/Game/RemotePlayer.ts
Normal file
156
client/assets/scripts/App/Game/RemotePlayer.ts
Normal file
@@ -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<Node> = 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);
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/App/Game/RemotePlayer.ts.meta
Normal file
9
client/assets/scripts/App/Game/RemotePlayer.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "48c1adc3-a2a6-4f7f-97b2-4df92ee822ee",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
45
client/assets/scripts/App/Game/UIGame.ts
Normal file
45
client/assets/scripts/App/Game/UIGame.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/App/Game/UIGame.ts.meta
Normal file
9
client/assets/scripts/App/Game/UIGame.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "1fa0f67a-24a5-4acc-a866-d5c288f16fc7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
228
client/assets/scripts/App/Game/World.ts
Normal file
228
client/assets/scripts/App/Game/World.ts
Normal file
@@ -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<playerId, RemotePlayer> */
|
||||
private remotePlayers: Map<string, RemotePlayer> = 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<void> {
|
||||
this.worldRoot = worldRoot;
|
||||
this.localPlayer = localPlayer;
|
||||
|
||||
// 加载玩家模型预制体
|
||||
await this.loadPlayerPrefab();
|
||||
|
||||
// 注册网络消息监听
|
||||
this.registerNetworkListeners();
|
||||
|
||||
// 创建本地玩家
|
||||
await this.createLocalPlayer();
|
||||
|
||||
console.log('[World] 世界初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载玩家模型预制体
|
||||
*/
|
||||
private async loadPlayerPrefab(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/App/Game/World.ts.meta
Normal file
9
client/assets/scripts/App/Game/World.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "80116901-37bd-4c32-85b3-da5aefa12b10",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user