小白也能写框架之【十、组件篇:网络】

接之前的教程:
小白也能写框架之【零、框架实际应用演示】 - 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中文社区

小白也能写框架之【九、组件篇:多语言】 - 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 就是数据加密解密器文件,可以自己修改。

1赞

佬,你真是太棒了,d=====( ̄▽ ̄*)b

现在也不叫教程,几乎就是贴代码和示例,希望对刚入门的兄弟有点点用。

后面会除一个真正的实战教程,一步步消化掉框架,顺便熟悉cocos基本常用的API。

1赞

你不如贴个git地址,让大家一次性下载就完事了,这一段段代码拷多费劲 :rofl:

enen 可以把代码全传到gitee上 别人直接fork

Http.ts文件代码和ISocket.ts的重复了

已经更新,谢谢哥们提醒

等最后一个UI弄完就 传一个

嗯,我是打算弄完,就直接传论坛最后一个帖子的

加油搞兄弟 你搞完了 就是我的了 :wink: