重构NetManager支持MessagePair,新增TSRPCWsClient。

This commit is contained in:
janing
2025-12-18 13:25:04 +08:00
parent c12e439add
commit fb940452db
41 changed files with 976 additions and 681 deletions

View File

@@ -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";
/**
* 应用启动状态
@@ -47,14 +50,53 @@ export class AppStatusBoot extends BaseState {
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();
// 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() {
console.log("[AppStatusBoot] 网络连接完成(待配置)");
}
/**

View File

@@ -1,8 +1,9 @@
import { find } from "cc";
import { BaseState } from "../../Framework/FSM/BaseState";
import { UIMgr } from "../../Framework/UI/UIMgr";
import { PlayerInfo } from "../../Shared/protocols/MsgResLogin";
import { UIGame } from "../Game/UIGame";
import { World } from "../Game/World";
import { PlayerInfo } from "../../Shared/protocols/PtlLogin";
/**
* 应用游戏状态
@@ -80,7 +81,7 @@ export class AppStatusGame extends BaseState {
}
// 获取世界根节点
const worldRoot = this._uiGame.getWorldRoot();
const worldRoot = find("Game")
if (!worldRoot) {
throw new Error("世界根节点未找到");
}

View File

@@ -1,9 +1,10 @@
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 { 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 { RemotePlayer } from './RemotePlayer';
@@ -33,6 +34,9 @@ export class World {
/** 玩家模型预制体 */
private playerPrefab: Prefab = null;
/** 摄像机控制器 */
private cameraController: CameraController = null;
private constructor() { }
public static getInstance(): World {
@@ -68,7 +72,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);
@@ -113,6 +117,10 @@ export class World {
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);
}
@@ -198,6 +206,12 @@ export class World {
}
this.localPlayerController = null;
// 销毁摄像机控制器
if (this.cameraController) {
this.cameraController.node.removeComponent(CameraController);
this.cameraController = null;
}
// 销毁所有远程玩家
this.remotePlayers.forEach((remotePlayer) => {
remotePlayer.destroy();

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "db7f0511-7e71-4088-9822-186f68082db6",
"uuid": "3ba46d91-073d-44d6-8157-05f1af758121",
"files": [],
"subMetas": {},
"userData": {}

View File

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

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b5331a2b-20fa-4653-8013-deb54bad8d2e",
"uuid": "9b552413-09a9-4ec6-b389-ea371b734a41",
"files": [],
"subMetas": {},
"userData": {}

View File

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

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "9fdf30ed-1223-4112-81f6-75339229fd1b",
"uuid": "4c559445-88e3-4fe2-a87c-8c9ef390aa60",
"files": [],
"subMetas": {},
"userData": {}

View File

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

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4414c606-7995-4f83-b83c-30c9fe840a6b",
"uuid": "871b5daf-492f-4ff7-adac-a13bbf6b82ce",
"files": [],
"subMetas": {},
"userData": {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,14 @@
/**
* 网络协议类型
*/
export enum NetProtocolType {
/** HTTP/HTTPS 协议 */
Http = "http",
/** WebSocket 协议 */
WebSocket = "websocket"
}
/**
* 网络配置接口
*/
@@ -5,6 +16,9 @@ export interface NetConfig {
/** 服务器地址 */
serverUrl: string;
/** 网络协议类型 默认 Http */
protocolType?: NetProtocolType;
/** 超时时间(ms) 默认 30000 */
timeout?: number;
@@ -16,14 +30,19 @@ export interface NetConfig {
/** 最大重连次数 默认 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
};

View File

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

View File

@@ -1,16 +1,19 @@
import { NetConfig, DefaultNetConfig } from './NetConfig';
import { MessagePair, MessagePairRegistry } from '../../App/Msg/MessagePairBase';
import { DefaultNetConfig, NetConfig, NetProtocolType } from './NetConfig';
import { NetEvent } from './NetEvent';
import { 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;
@@ -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,43 +70,53 @@ 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);
this._isConnected = true;
this._reconnectCount = 0;
const result = await this._client.connect();
if (!result.isSucc) {
console.error('[NetManager] WebSocket connection failed:', result.errMsg);
this.emit(NetEvent.Error, result.errMsg);
this.emit(NetEvent.Connected);
console.log('[NetManager] Client created successfully');
// client 可能不需要显式连接
// 对于 WsClient 需要调用 connect()
// 尝试重连
if (this._config.autoReconnect) {
this.scheduleReconnect();
}
return false;
}
}
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();
}
@@ -114,20 +128,20 @@ export class NetManager {
* 断开连接
*/
disconnect(): void {
if (!this._isConnected) {
if (!this._isConnected || !this._client) {
return;
}
// HttpClient 无需显式断开连接
// 如果使用 WebSocket可以在这里调用 disconnect()
this._isConnected = false;
this._client = null;
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();
}
@@ -152,12 +166,11 @@ 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);
}
/**
@@ -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
}));
}
}

View File

@@ -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';
/**
* 平台类型枚举
@@ -24,6 +26,9 @@ export interface ClientConfig {
/** 服务器地址 */
server: string;
/** 网络协议类型 */
protocolType?: NetProtocolType;
/** 是否使用 JSON 格式 (默认 true) */
json?: boolean;
@@ -36,9 +41,13 @@ export interface ClientConfig {
/**
* 平台适配器
* 根据当前平台返回对应的 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');
}
let client: HttpClientBrowser<T> | HttpClientMiniapp<T>;
console.log(`[PlatformAdapter] Creating ${protocolType} client for ${platform}:`, defaultConfig.server);
// 根据平台创建对应的客户端
if (platform === PlatformType.MiniApp) {
console.log('[PlatformAdapter] Creating MiniApp client:', defaultConfig.server);
client = new HttpClientMiniapp(this._serviceProto, defaultConfig) as HttpClientMiniapp<T>;
// 根据协议类型和平台创建对应的客户端
if (protocolType === NetProtocolType.WebSocket) {
return this.createWebSocketClient<T>(platform, defaultConfig);
} else {
// 浏览器和其他 XMLHttpRequest 兼容环境使用 Browser 客户端
console.log('[PlatformAdapter] Creating Browser client:', defaultConfig.server);
client = new HttpClientBrowser(this._serviceProto, defaultConfig) as HttpClientBrowser<T>;
return this.createHttpClient<T>(platform, defaultConfig);
}
}
return client;
/**
* 创建 HTTP 客户端
*/
private static createHttpClient<T extends BaseServiceType>(platform: PlatformType, config: ClientConfig): INetClient<T> {
let client: HttpClientBrowser<T> | HttpClientMiniapp<T>;
if (platform === PlatformType.MiniApp) {
client = new HttpClientMiniapp(this._serviceProto, config) as HttpClientMiniapp<T>;
} else {
client = new HttpClientBrowser(this._serviceProto, config) as HttpClientBrowser<T>;
}
// 包装 HTTP 客户端使其符合 INetClient 接口
return {
callApi: async <Req, Res>(apiName: string, req: Req) => {
return await client.callApi(apiName, req);
}
} as INetClient<T>;
}
/**
* 创建 WebSocket 客户端
*/
private static createWebSocketClient<T extends BaseServiceType>(platform: PlatformType, config: ClientConfig): INetClient<T> {
let wsClient: WsClientBrowser<T> | WsClientMiniapp<T>;
if (platform === PlatformType.MiniApp) {
wsClient = new WsClientMiniapp(this._serviceProto, config) as WsClientMiniapp<T>;
} else {
wsClient = new WsClientBrowser(this._serviceProto, config) as WsClientBrowser<T>;
}
// 使用包装器统一接口
return new WebSocketClientWrapper<T>(wsClient);
}
/**

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
/**
* 移动请求消息
*/
export interface MsgReqMove {
/** X坐标 */
x: number;
/** Y坐标 */
y: number;
}

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,5 @@
import { Position } from './base';
/**
*
*/
export interface ReqLogin {
/** 玩家ID用于识别玩家 */
playerId: string;
/** 玩家昵称(可选,新玩家时使用) */
playerName?: string;
}
/**
*
*/
@@ -44,9 +33,9 @@ export interface PlayerInfo {
}
/**
*
*
*/
export interface ResLogin {
export interface MsgResLogin {
/** 是否成功 */
success: boolean;

View File

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

View File

@@ -0,0 +1,15 @@
import { Position } from './base';
/**
* 移动响应消息
*/
export interface MsgResMove {
/** 是否成功 */
success: boolean;
/** 消息 */
message: string;
/** 新位置(成功时返回) */
position?: Position;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": 5,
"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,16 +258,16 @@ 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"
@@ -232,7 +276,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
}
]
},
"PtlLogin/PlayerInfo": {
"MsgResLogin/PlayerInfo": {
"type": "Interface",
"properties": [
{
@@ -302,26 +346,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 +361,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
"name": "message",
"type": {
"type": "String"
},
"optional": true
}
},
{
"id": 2,
@@ -350,19 +374,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": [
{