一、问题背景:小游戏里的“断连噩梦”

玩过微信小游戏的人应该都有过这种体验:刚攒了半天的积分要提交,或者正要领签到奖励,突然手机切后台接了个电话,再切回来游戏就卡着不动了——屏幕上的加载圈转个不停,点啥都没反应,只能关了重开,之前的操作全白费。 这种问题的核心,就是游戏的网络请求出了“死局”:要么请求发出去没收到响应,要么响应因为网络波动丢了,游戏又没做合理的超时判断和重试,就一直傻等,最后卡成“植物人”。很多新手开发者做网络逻辑时,只会简单发请求,完全没考虑过手机网络的“坑”——手机网络本来就不稳定,从4G切5G、切后台、进电梯,都可能断连,要是没处理好,玩家体验直接崩盘。

二、核心逻辑拆解:先搞懂“超时”和“重试”到底要解决啥

要解决这个问题,得先把两个核心概念掰明白,别觉得是专业名词,其实特别好懂。

2.1 超时:给请求设个“闹钟”

超时就是给每个网络请求定个“最晚等待时间”——比如你发个提交积分的请求,最多等3秒,要是3秒内没收到服务器的回应,就别等了,主动触发“请求失败”的逻辑。要是没设超时,请求就会一直挂着,游戏就卡了。

2.2 重试:给请求“再来一次”的机会,但不能瞎试

重试就是请求失败后,再发一次,但绝对不能随便试:比如你提交积分,要是因为服务器崩了,你一秒发10次,不仅服务器扛不住,玩家手机的流量、电量也会被耗光;要是你连续试了10次都失败,还一直试,游戏还是会卡。所以重试得有规矩:最多试几次、每次隔多久试、哪些请求能重试。

三、具体实现:用Cocos Creator做一套靠谱的逻辑

这里我们专门用Cocos Creator的TypeScript(这是Cocos官方推荐的开发语言,新手也能快速上手)来写完整的代码,每个步骤都有注释,保证能看懂。 先明确技术栈:Cocos Creator 3.x + TypeScript

3.1 第一步:写一个通用的网络请求工具

这个工具要包含超时判断、重试逻辑,所有网络请求都用这个工具发,不用每次都写重复代码。

// 技术栈:Cocos Creator 3.x + TypeScript
// 通用网络请求工具类
class HttpRequest {
    // 配置参数:可以根据自己的游戏调整
    private config = {
        baseUrl: 'https://your-game-server.com/api', // 服务器地址,换成自己的
        timeout: 3000, // 单个请求的超时时间,单位毫秒,这里设3秒
        maxRetry: 2, // 最大重试次数,总共会发1次初始请求+2次重试,共3次
        retryDelay: 1000 // 重试间隔,单位毫秒,每次重试前等1秒
    };

    // 核心请求方法:返回Promise,方便后续处理结果
    public request(options: {
        url: string; // 请求的接口路径,比如'submitScore'
        method?: 'GET' | 'POST'; // 请求方法,默认POST
        params?: any; // 请求参数,比如{score: 100, userId: '123'}
        canRetry?: boolean; // 这个请求能不能重试,比如提交积分能重试,登录请求就别重试
    }): Promise<any> {
        // 把请求方法默认设为POST,因为游戏大多用POST传数据
        options.method = options.method || 'POST';
        // 把能不能重试默认设为true,特殊请求再改
        options.canRetry = options.canRetry !== undefined ? options.canRetry : true;

        // 返回一个Promise,处理请求的成功/失败
        return new Promise((resolve, reject) => {
            // 定义一个函数,用来发送请求,同时记录当前是第几次请求(初始是第0次)
            const sendRequest = (retryCount: number = 0) => {
                // 先创建一个XHR对象,用来发网络请求(Cocos里发请求的基础对象)
                const xhr = new XMLHttpRequest();
                // 拼接完整的请求地址
                const fullUrl = this.config.baseUrl + options.url;
                // 打开请求连接:方法、地址、是否异步(必须是异步,不然游戏会卡)
                xhr.open(options.method, fullUrl, true);

                // 设置请求头:告诉服务器传的是JSON格式的数据
                xhr.setRequestHeader('Content-Type', 'application/json');

                // 监听请求成功的事件:服务器返回200状态码时触发
                xhr.onload = () => {
                    // 先检查服务器返回的状态码,200才是成功
                    if (xhr.status >= 200 && xhr.status < 300) {
                        // 把服务器返回的字符串转成JSON对象
                        const response = JSON.parse(xhr.responseText);
                        // 调用成功的回调,把结果传出去
                        resolve(response);
                    } else {
                        // 状态码不是200,说明服务器出错了,触发失败逻辑
                        this.handleRequestError(retryCount, options, sendRequest, reject);
                    }
                };

                // 监听网络错误的事件:比如断网、服务器连不上时触发
                xhr.onerror = () => {
                    this.handleRequestError(retryCount, options, sendRequest, reject);
                };

                // 监听超时事件:超过config里的timeout时间没收到响应时触发
                xhr.ontimeout = () => {
                    this.handleRequestError(retryCount, options, sendRequest, reject);
                };

                // 给XHR设置超时时间,必须写在open之后
                xhr.timeout = this.config.timeout;

                // 发送请求:把参数转成JSON字符串传过去
                xhr.send(JSON.stringify(options.params));
            };

            // 第一次调用sendRequest,开始发请求
            sendRequest();
        });
    }

    // 处理请求失败的逻辑:判断要不要重试
    private handleRequestError(
        retryCount: number,
        options: any,
        sendRequest: (count: number) => void,
        reject: (reason: any) => void
    ) {
        // 先判断能不能重试,以及重试次数有没有超过最大限制
        if (options.canRetry && retryCount < this.config.maxRetry) {
            // 可以重试:先等config里的retryDelay时间,再发下一次请求
            setTimeout(() => {
                // 重试次数加1,再调用sendRequest
                sendRequest(retryCount + 1);
            }, this.config.retryDelay);
        } else {
            // 不能重试或者重试次数用完了:直接返回失败,别再试了
            reject('请求失败:' + options.url);
        }
    }
}

// 导出这个工具类,方便其他地方用
export default HttpRequest;

3.2 第二步:在游戏里调用这个工具

写好工具后,用的时候特别简单,比如提交积分的逻辑,只需要几行代码:

// 技术栈:Cocos Creator 3.x + TypeScript
// 导入我们写的工具类
import HttpRequest from './HttpRequest';

// 创建一个工具类的实例
const http = new HttpRequest();

// 提交积分的函数
const submitScore = async (score: number, userId: string) => {
    try {
        // 调用request方法,传参数:接口地址、参数、不能重试的话设canRetry为false
        const result = await http.request({
            url: '/submitScore',
            params: { score, userId },
            // 这里如果是提交积分,其实可以重试,所以不用改canRetry,默认是true
        });
        // 请求成功:比如弹出积分提交成功的提示
        console.log('积分提交成功:', result);
        alert('积分提交成功!');
    } catch (error) {
        // 请求失败:比如弹出网络错误的提示,别让游戏卡着
        console.log('积分提交失败:', error);
        alert('网络出错,请检查网络后重试!');
    }
};

// 比如玩家点击“提交积分”按钮时,调用这个函数
// submitScore(100, 'user_123456');

3.3 第三步:特殊请求的处理逻辑

不是所有请求都能重试,比如登录请求:要是你登录失败后重试,服务器可能会重复验证,甚至封你的账号。所以这类请求要把canRetry设为false:

// 技术栈:Cocos Creator 3.x + TypeScript
// 登录的函数,不能重试
const login = async (username: string, password: string) => {
    try {
        const result = await http.request({
            url: '/login',
            params: { username, password },
            canRetry: false // 登录请求不能重试,直接设为false
        });
        console.log('登录成功:', result);
    } catch (error) {
        console.log('登录失败:', error);
        alert('登录失败,请检查账号密码!');
    }
};

四、进阶优化:避免更复杂的坑

上面的逻辑已经能解决大部分问题,但还有几个细节要注意,不然还是可能出问题。

4.1 区分“临时错误”和“永久错误”

不是所有失败都要重试:比如服务器返回“账号不存在”,这是永久错误,再试100次也没用;但如果是“网络超时”“服务器暂时不可用”,这是临时错误,才需要重试。 怎么区分?可以在工具类里加个判断:比如服务器返回的错误码是404(找不到接口)、401(未授权),就别重试;如果是500(服务器内部错误)、503(服务不可用),再重试。 修改工具类的handleRequestError方法:

// 技术栈:Cocos Creator 3.x + TypeScript
// 修改后的handleRequestError方法
private handleRequestError(
    retryCount: number,
    options: any,
    sendRequest: (count: number) => void,
    reject: (reason: any) => void,
    xhr?: XMLHttpRequest // 把XHR传进来,用来判断状态码
) {
    // 先判断状态码:如果是4开头的(客户端错误),就别重试
    const isClientError = xhr && xhr.status >= 400 && xhr.status < 500;
    if (isClientError) {
        reject('请求失败:' + options.url + ',错误码:' + xhr.status);
        return;
    }
    // 其他逻辑不变
    if (options.canRetry && retryCount < this.config.maxRetry) {
        setTimeout(() => {
            sendRequest(retryCount + 1);
        }, this.config.retryDelay);
    } else {
        reject('请求失败:' + options.url);
    }
}

4.2 避免“重复请求”

比如玩家连续点两次“提交积分”,就会发两次请求,要是都成功,积分就会加两次。怎么避免?可以加个“请求锁”:同一个接口正在请求时,不让再发。 修改工具类,加个请求锁的逻辑:

// 技术栈:Cocos Creator 3.x + TypeScript
// 在HttpRequest类里加个属性,用来记录正在请求的接口
private pendingRequests = new Set<string>();

// 修改request方法,加锁判断
public request(options: {
    url: string;
    method?: 'GET' | 'POST';
    params?: any;
    canRetry?: boolean;
}): Promise<any> {
    // 拼接一个唯一的请求标识,比如接口地址+参数转成的字符串
    const requestKey = options.url + JSON.stringify(options.params);
    // 如果这个请求已经在pendingRequests里,说明正在请求,直接返回失败
    if (this.pendingRequests.has(requestKey)) {
        return Promise.reject('请求正在处理中,请不要重复操作');
    }
    // 把请求标识加进去
    this.pendingRequests.add(requestKey);

    // 原来的逻辑不变,最后在请求结束时,把请求标识从pendingRequests里删掉
    return new Promise((resolve, reject) => {
        const sendRequest = (retryCount: number = 0) => {
            const xhr = new XMLHttpRequest();
            // 原来的onload、onerror、ontimeout监听,最后都要加:
            // 请求成功后,删掉请求标识
            xhr.onload = () => {
                this.pendingRequests.delete(requestKey);
                if (xhr.status >= 200 && xhr.status < 300) {
                    const response = JSON.parse(xhr.responseText);
                    resolve(response);
                } else {
                    this.handleRequestError(retryCount, options, sendRequest, reject, xhr);
                }
            };
            // 网络错误时,删掉请求标识
            xhr.onerror = () => {
                this.pendingRequests.delete(requestKey);
                this.handleRequestError(retryCount, options, sendRequest, reject, xhr);
            };
            // 超时时,删掉请求标识
            xhr.ontimeout = () => {
                this.pendingRequests.delete(requestKey);
                this.handleRequestError(retryCount, options, sendRequest, reject, xhr);
            };
            // 原来的逻辑不变
        };
        sendRequest();
    });
}

五、应用场景、优缺点和注意事项

5.1 应用场景

这套逻辑适合所有Cocos开发的小游戏、轻度手游,尤其是依赖网络的玩法:比如积分提交、签到、排行榜更新、对战匹配、道具购买这些核心网络操作。

5.2 技术优缺点

优点:

  1. 通用:所有网络请求都用一个工具,代码不重复,维护方便;
  2. 灵活:超时时间、重试次数、重试间隔都能自己改,适配不同的游戏场景;
  3. 靠谱:有超时判断、重试逻辑、请求锁,能解决大部分网络问题;
  4. 易上手:代码逻辑简单,新手也能看懂,改参数就行。 缺点:
  5. 不适合复杂场景:比如需要断点续传、大文件上传的游戏,这套逻辑不够用;
  6. 依赖服务器:要是服务器本身有问题,再怎么重试也没用;
  7. 对弱网的适配有限:比如网络特别差的时候,还是可能失败。

5.3 注意事项

  1. 超时时间不能设太长:比如超过5秒,玩家会觉得卡;也不能太短:比如1秒,很多网络请求本来能成功,也会被当成超时;
  2. 重试次数不能太多:最多2-3次,不然服务器扛不住;
  3. 重试间隔要合理:比如1秒左右,别太密;
  4. 重要请求要做幂等:比如提交积分,要保证同一个请求发多次,只会加一次积分(这个需要服务器配合,比如给每个请求加唯一的请求ID,服务器根据ID判断是不是重复请求);
  5. 游戏切后台时,要暂停请求:比如玩家切后台接电话,网络会断,要是请求还在发,可能会触发重试,切回来后又发一次,所以可以在游戏切后台时,取消正在进行的请求,切回来后再重新发(Cocos有对应的事件监听,比如cc.game.on(cc.game.EVENT_HIDE, () => { 取消请求 }))。

六、总结

小游戏的网络问题,本质上是手机网络的不稳定性和游戏体验的矛盾,做好超时和重试,就是给这个矛盾找个平衡点:既不让游戏一直傻等,也不让请求乱发。 上面的逻辑已经很完善了,新手可以直接把工具类复制到自己的项目里,改改参数就能用;进阶开发者可以再根据自己的游戏场景优化,比如加弱网检测、请求优先级、批量请求等。 记住,做网络逻辑的核心是“站在玩家的角度想”:玩家遇到网络问题时,别让游戏卡着,要给个明确的提示,还要尽量帮玩家把操作补回来——比如提交积分失败,提示玩家检查网络后再试,而不是让玩家关游戏重开。