接之前的教程:
小白也能写框架之【零、框架实际应用演示】 - Creator 3.x - Cocos中文社区
小白也能写框架之【一、新建框架工程】 - Creator 3.x - Cocos中文社区
小白也能写框架之【二、带颜色的日志管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【三、带进度的分包管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【四、带加密的数据管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【五、资源管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【六、音频管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【七、事件管理器】 - Creator 3.x - Cocos中文社区
小白也能写框架之【八、任务管理器】 - Creator 3.x - Cocos中文社区
小白也能实现网络同步
一、因为涉及到服务端,而我服务端习惯使用go开发,所以服务端我简单使用第三方模块快速实现
二、客户端
1、文件代码一:assets\Core\Scripts\Components\NetWork\Http.ts
这个文件就是简单的httpGET和POST
import { logMgr } from "../../Managers/LogMgr";
/** Http类,用于发送HTTP请求 */
export class Http {
/**
* 发送GET请求
* @param url 请求的URL
* @param params 查询参数
* @param callback 回调函数,接收返回数据
* @param retries 重试次数
* @param timeout 超时时间
*/
public static async get(url: string, params: Record<string, any> = {}, callback: (data: any) => void, retries: number = 3, timeout: number = 5000): Promise<void> {
const queryString = Http.formEncode(params);
const fullUrl = `${url}?${queryString}`;
await Http.httpRequest(fullUrl, { method: 'GET' }, callback, retries, timeout);
}
/**
* 发送POST请求
* @param url 请求的URL
* @param body 请求体
* @param callback 回调函数,接收返回数据
* @param contentType 内容类型
* @param retries 重试次数
* @param timeout 超时时间
*/
public static async post(url: string, body: any, callback: (data: any) => void, contentType: string = 'json', retries: number = 3, timeout: number = 5000): Promise<void> {
const { headers, bodyData } = Http.preparePostData(body, contentType);
await Http.httpRequest(url, { method: 'POST', headers, body: bodyData }, callback, retries, timeout);
}
/**
* 准备POST请求的数据
* @param body 请求体
* @param contentType 内容类型
* @returns headers和bodyData
*/
private static preparePostData(body: any, contentType: string): { headers: Record<string, string>, bodyData: string } {
const headers: Record<string, string> = {};
let bodyData = '';
switch (contentType) {
case 'json':
headers['Content-Type'] = 'application/json';
bodyData = typeof body === 'string' ? body : JSON.stringify(body);
break;
case 'form':
headers['Content-Type'] = 'application/x-www-form-urlencoded';
bodyData = typeof body === 'string' ? body : Http.formEncode(body);
break;
}
return { headers, bodyData };
}
/**
* 发送HTTP请求
* @param url 请求的URL
* @param options 请求选项
* @param callback 回调函数,接收返回数据
* @param retries 重试次数
* @param timeout 超时时间
*/
private static async httpRequest(url: string, options: RequestInit, callback: (data: any) => void, retries: number, timeout: number): Promise<void> {
for (let attempt = 0; attempt < retries; attempt++) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
options.signal = controller.signal;
try {
const response = await fetch(url, options);
clearTimeout(id);
if (response.ok) {
const data = await response.json();
callback(data); // 调用回调函数,传递数据
return;
} else {
logMgr.err(`${options.method} 请求失败: ${response.statusText}`);
}
} catch (error) {
clearTimeout(id);
logMgr.err(`${options.method} 请求异常: ${error}`);
}
if (attempt < retries - 1) {
logMgr.warn(`${options.method} 重试中... 剩余尝试次数: ${retries - attempt - 1}`);
await Http.delay(timeout);
}
}
throw new Error('HTTP请求失败,已达到最大重试次数。');
}
/**
* 将对象编码为查询字符串
* @param data 要编码的数据
* @returns 编码后的字符串
*/
private static formEncode(data: Record<string, any>): string {
return Object.keys(data)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
.join('&');
}
/**
* 延迟指定的毫秒数
* @param ms 延迟时间
* @returns Promise
*/
private static delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
2、文件代码二:assets\Core\Scripts\Components\NetWork\ISocket.ts
/** 定义WebSocket数据类型 */
export type WsData = string | Blob | ArrayBufferView | ArrayBuffer;
/** WebSocket接口定义 */
export interface ISocket {
/** 连接成功时的回调 */
onConnected: () => void;
/** 收到消息时的回调 */
onMessage: (msg: WsData) => void;
/** 错误处理回调 */
onError: (error: string) => void;
/** 连接关闭时的回调 */
onClosed: () => void;
/**
* 连接到WebSocket服务器
* @param urlOrIp URL地址或IP地址
* @param port 端口号(可选)
* @returns 是否成功发起连接
*/
connect(urlOrIp: string, port?: number): boolean;
/**
* 发送数据
* @param data 要发送的数据
* @returns 是否成功发送数据
*/
send(data: WsData): boolean;
/**
* 关闭WebSocket连接
* @param code 关闭代码(可选)
* @param reason 关闭原因(可选)
*/
close(code?: number, reason?: string): void;
/**
* 获取当前连接状态
* @returns 是否处于活动状态
*/
isActive: boolean;
}
3、文件代码三:assets\Core\Scripts\Components\NetWork\WsCoder.ts
import { Crypt } from "../../Utils/Crypt";
/** 消息结构 */
export class Message {
constructor(public Cmd: number, public Data: Uint8Array) {}
}
/** 消息编码器,提供WebSocket消息的编码加密和解码解密功能 */
export class WsCoder {
/**
* 消息打包(使用大端序)
* @param msg 要打包的消息
* @param key 加密密钥(可选)
* @returns 打包后的字节数组
*/
public static Pack(msg: Message, key: string = ''): Uint8Array {
if (!msg) throw new Error("消息不能为空");
const data = key ? Crypt.byteEncrypt(msg.Data, key) : msg.Data;
const buffer = new Uint8Array(4 + data.length);
this.setUint32(buffer, 0, msg.Cmd);
buffer.set(data, 4);
return buffer;
}
/**
* 消息解包(使用大端序)
* @param buffer 要解包的字节数组
* @param key 解密密钥(可选)
* @returns 解包后的消息
*/
public static Unpack(buffer: Uint8Array, key: string = ''): Message {
if (buffer.length < 4) throw new Error("消息长度不足");
const cmd = this.getUint32(buffer, 0);
const data = key ? Crypt.byteDecrypt(buffer.slice(4), key) : buffer.slice(4);
return new Message(cmd, data);
}
/**
* 通过 DataView 设置 Uint32 值(大端序)
* @param buffer 目标缓冲区
* @param offset 偏移量
* @param value 要设置的值
*/
private static setUint32(buffer: Uint8Array, offset: number, value: number) {
new DataView(buffer.buffer).setUint32(offset, value, false);
}
/**
* 通过 DataView 获取 Uint32 值(大端序)
* @param buffer 源缓冲区
* @param offset 偏移量
* @returns 获取的值
*/
private static getUint32(buffer: Uint8Array, offset: number): number {
return new DataView(buffer.buffer).getUint32(offset, false);
}
}
4、文件代码四:assets\Core\Scripts\Components\NetWork\Ws.ts
import { ISocket, WsData } from "./ISocket";
import { Message, WsCoder } from "./WsCoder";
/** WebSocket类,实现ISocket接口 */
export class Ws implements ISocket {
private ws: WebSocket | null = null; /** WebSocket对象 */
/** 连接成功时的回调 */
onConnected() {}
/** 收到消息时的回调 */
onMessage(msg: WsData) {}
/** 错误处理回调 */
onError(err: any) {}
/** 连接关闭时的回调 */
onClosed() {}
/**
* 连接到WebSocket服务器
* @param urlOrIp URL地址或IP地址
* @param port 端口号(可选)
* @returns 是否成功发起连接
*/
connect(urlOrIp: string, port?: number): boolean {
if (this.isConnecting) return false;
const url = port ? `${urlOrIp}:${port}` : urlOrIp;
try {
this.ws = new WebSocket(url);
this.ws.binaryType = "arraybuffer";
this.ws.onopen = this.onConnected.bind(this);
this.ws.onmessage = (event) => this.onMessage(event.data);
this.ws.onerror = (event) => this.onError(event);
this.ws.onclose = this.onClosed.bind(this);
return true;
} catch (error) {
this.onError(error instanceof Error ? error.message : 'Unknown error');
return false;
}
}
/**
* 发送数据
* @param data 指定格式数据
* @returns 是否发送成功
*/
send(data: WsData): boolean {
if (this.isActive) {
this.ws!.send(data);
return true;
}
return false;
}
/**
* 发送命令和数据
* @param cmd 主命令码
* @param buffer 数据
* @param key 加密密钥(可选)
* @returns 是否发送成功
*/
sendBuffer(cmd: number, buffer: Uint8Array, key: string = ''): boolean {
const message = new Message(cmd, buffer);
const packedData = WsCoder.Pack(message, key);
return this.send(packedData);
}
/**
* 关闭WebSocket连接
* @param code 关闭代码(可选)
* @param reason 关闭原因(可选)
*/
close(code?: number, reason?: string): void {
this.ws?.close(code, reason);
}
/**
* 获取当前连接状态
* @returns 是否处于活动状态
*/
get isActive(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}
/**
* 检查是否正在连接
* @returns 是否正在连接
*/
private get isConnecting(): boolean {
return this.ws?.readyState === WebSocket.CONNECTING;
}
}
三、结构截图:
四、示例代码:
1、先加载实例化网络节点示例分包
2、分包逻辑代码:assets\SubGame_002\Scripts\Game.ts
import { EDITOR } from 'cc/env';
import { _decorator, Component, Node, log, instantiate, Prefab, Vec3, EventTouch, input, Input, Camera, find, UITransform } from 'cc';
import { Ws } from '../../Core/Scripts/Components/NetWork/Ws';
const { ccclass, property } = _decorator;
@ccclass('Game')
export class Game extends Component {
/** WebSocket实例 */
private wsClient: Ws = null;
/** 当前客户端ID */
private myid: string = "";
/** 地鼠预制体 */
@property(Prefab)
gopherPrefab: Prefab = null;
/** 地鼠节点映射 */
private gophers: Map<string, Node> = new Map();
/** 主摄像机 */
private mainCamera: Camera = null;
onLoad() {
if (EDITOR) return; // 编辑器模式下直接返回
this.mainCamera = find('Canvas/Camera').getComponent(Camera); // 获取主摄像机
this.wsClient = new Ws(); // 创建WebSocket实例
this.wsClient.onConnected = () => log('WebSocket连接成功'); // 连接成功回调
this.wsClient.onMessage = this.onMessage.bind(this); // 消息到达回调
const url = 'ws://127.0.0.1';
const port = 5000;
const isConnected = this.wsClient.connect(url, port); // 连接WebSocket服务器
log(isConnected ? '正在连接到WebSocket服务器...' : 'WebSocket连接失败');
input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this); // 监听触摸移动事件
}
onDestroy() {
input.off(Input.EventType.TOUCH_MOVE, this.onTouchMove, this); // 移除触摸移动事件监听
}
/** 处理收到的消息 */
onMessage = (msg: any) => {
log('收到消息:', msg);
const cmds = { "iam": this.iam, "set": this.set, "dis": this.dis };
if (msg) {
const parts = msg.split(" ");
const cmd = cmds[parts[0]];
if (cmd) {
cmd.apply(this, parts.slice(1));
} else {
log('未知命令:', parts[0]);
}
}
}
/** 设置当前客户端的ID */
iam = (id: string) => {
this.myid = id;
log('我的id', id);
this.createGopher(id); // 创建自己的地鼠
}
/** 创建地鼠 */
createGopher = (id: string) => {
if (!this.gophers.has(id)) {
const gopher = instantiate(this.gopherPrefab);
this.node.addChild(gopher);
this.gophers.set(id, gopher);
}
}
/** 设置地鼠的位置 */
set = (id: string, x: string, y: string) => {
let gopher = this.gophers.get(id);
if (!gopher) {
this.createGopher(id);
gopher = this.gophers.get(id);
}
gopher.setPosition(new Vec3(parseFloat(x), parseFloat(y), 0));
}
/** 移除地鼠 */
dis = (id: string) => {
const gopher = this.gophers.get(id);
if (gopher) {
this.node.removeChild(gopher);
this.gophers.delete(id);
}
}
/** 处理触摸移动事件 */
onTouchMove = (event: EventTouch) => {
if (this.myid !== "") {
const touch = event.getLocation();
const worldPos = this.mainCamera.screenToWorld(new Vec3(touch.x, touch.y, 0));
const localPos = this.node.getComponent(UITransform).convertToNodeSpaceAR(worldPos);
this.set(this.myid, localPos.x.toString(), localPos.y.toString());
this.wsClient.send([localPos.x, localPos.y].join(" "));
}
}
}
五、补充说明
1、实际就几个函数onConnected、onMessage、onError、onClosed、send、sendBuffer
2、send和sendBuffer区别,send用于发送明文数据,sendBuffer用于发送加密数据(需要握手协议)后面实战教程会用到
3、WsCoder.ts 就是数据加密解密器文件,可以自己修改。