Cocos Creator协程组件!你真的懂协程吗?

相信很多有Unity经验的程序在使用CocosCreator时都在问, cc有协程吗?对一直使用js/ts的程序来説,不就是await/async吗?
其实两者不一様,协程的核心理念是把一个同歩运算变成异歩运算,过程中把函数分阶段执行, 减轻在某些同歩运算高消耗CPU导致表现上出现卡住情况从而改善用户体验,与线程不同的是,协程也是在主线程中运行, 所以不需要考虑线程安全性问题。
用Unity的定义是:
image

以下我会分享一下我使用Cocos协程的经验,记住协程函数一定要加*号在前

案例1. 分帧实例prefab:

相信有很多人在为了改善卡顿现像, 把实例prefab改为分帧处理(比如每帧3-5个prefab), 通常都是在update函数中, 进行处理,但若果在协程中, 处理十分优雅。

   *_createPrefabObj(createPreFrame:number,maxCount:number){
        for(let i = 0; i < maxCount;++i){
             const n = cc.instantiate(this.prefab);
             this.node.addChild(n);
             if(i > 0 && i%createPreFrame == 0){
                 //下一帧继续
                 yield;
             }
        }
    }

   start(){
     //其中3代表对应createPreFrame,10代表对应maxCount
     Corotuine.instance.start(this,this._createPrefabObj,3,10);
   }

案例2. 为label赋值后,需要取得label的尺寸进行业务处理 + 字符流输出效果

一般来说,label赋值后可以调用_forceUpdateRender直接取得Label尺寸,但这样做其实加大了一帧的处理时间,很多时候可以会用scheduleOnce或者setTimeout定时器等待1~N帧,再取得label尺寸,但在协程中可以十分优雅处理

      *_labelSpeak(str:string){
          this.label.string = "";
          const arrStr = Array.from(str);
          for(let i = 0 ; i < arrStr.length;++i){
             this.label.string += arrStr[i];
             //这里使用等待两帧,若果只待一帧 ,直接yield; 即可
             yield Coroutine.createWaitForFrame(2);
             this.lable.setPosition(new cc.Vec2(this.label.node.width,0)); 
          }
    }
   
   onLoad(){
     //这里示范用字符串调用
     Coroutine.instance.start(this,'_labelSpeak','hello world');
   }

案例3. 奇怪的需求,要求一个流程中处理一下,等待几秒,再处理一下再等待几秒。

有时候了为满足策划在表现上的需求,总会有一些要连续使用定时器的情况,大量的scheduleOnce+回调,在协程中可以优雅处理

   *_animeEffect(){
        this.node.x += 10;
        //等待两秒
        yield Coroutine.createWaitForSecond(2);
        this.node.y += 10;
       //等待5秒
        yield Coroutine.createWaitForSecond(5);
    }

    start(){
        Coroutine.instance.start(this,this._animeEffect);
    }

案例4 将一个同步处理封装成异步

    *_asyncProc(callback){
        for(let i = 0 ; i < 100;++i){
           yield:
        }
       callback(100);
      }

   packFunc(){
      return new Promise((resolve,reject)=>{
             Coroutine.instance.start(this,"_asyncProc",resolve);
     });
   }

  start(){
     const v = await packFunc();
  }

商店地址

Creator模拟Unity协程(Coroutine)组件

引擎 路径 Demo版本
3.x Coroutine3x/assets/scripts/librarys/Coroutine.ts 3.7.1
2.x Coroutine2x/assets/Script/librarys/Coroutine.ts 2.4.6

若觉得有用,就请作者君喝杯咖啡吧!

5赞

但是,js的await/async 也是主线程运行的吧,只是promise完成的callback调度。就和协程一样的。
js多线程只有开worker才是。

一般来说,在js里io方面异步,而如果你利用setTimeout或者promise 只是在执行时编译器把这个函数执行优先度下降,但如果想把一个for循环加长时间去执行,那你可以怎麽搞呢?
所以不能把promise 当成协程,两者在理念上是不一样的。由期是在游戏开发中,协程更像是希望花指定帧数去完成某事,而不是延迟去执行,你试试promise 函数里写一个1000000次的for循环,你看看你会不会卡帧,但在协程里,我可以指定这个1000000次循环分多少帧去执行,从而避免卡帧

能不能讲一下你是怎么模拟的

详细可以看源码,我这里给你一个关键,就是js 的Generator进行函数切片

用await/async写成这样是一样的?

async _createPrefabObj(createPreFrame: number, maxCount: number) {
        const delay = function (time = 0) {
            return new Promise(resolve => {
                setTimeout(resolve, time); // setTimeout也可以改成cc的定时器
            });
        };

        for (let i = 0; i < maxCount; ++i) {
            const n = cc.instantiate(this.prefab);
            this.node.addChild(n);
            if (i > 0 && i % createPreFrame == 0) {
                //下一帧继续
                await delay();
            }
        }
    }

谢谢,我先了解一下

我理解在promise里面的for循环,添加await一个setTimeout(0)。不也能实现分帧加载么?
和你的 yield Coroutine.createWaitForFrame(2);不是一个作用?

不一样,你这个依赖了setTimeout,要知道setTimout 是不稳定的,有可能你这处理是隔了2帧,因为执行的优先顺序是编译器决定。

改成这样?

const delay = (time = 0) => {
            return new Promise(resolve => {
                this.scheduleOnce(time);
            });
        }

你这个方法是等待后,一口气跑完了,但可以自己输出看看,但我的方法,你把帧率降低了,是会看到1帧 1帧的输出

let本身就解决这个问题了?
D-Chat_20240110114902

你在unity 使用协程是用了IEnumerator 建立函数,其实就是js 的Generator

那我把setTimeout改为director.getScheduler().schedule()不就一定隔1帧咯?
不过我觉得用setTimeOut(0)应该也没问题,加到宏任务,当前帧应该就会执行了。

对的
个i值是一个闭包的结果
但协程核心是函数切片,把函数变成链,yield就是用来隔分链的节点,不需要有额外闭包

为啥我要在promise里面写循环啊,我做一个promise,setTimeout(n)以后去resolve,用函数返回出来,然后写个async函数,每循环await 这个函数不就行了?

wait(n : number): promise<void> {
       return new Promise(() => {
               setTimeout(resolve,n);                  
       })
}

async loop() {
     for (let i = 0; i < 10000; ++i) {
          // do something 
         await wait(1);
     }
}

不就和你那yield一样嘛。
实际上逻辑运行起来也没什么差别,用js原生的更容易理解点。

setTimeout(0) 是肯定不行的,因为若果你把帧下降你会发现还未到下一帧setTimout的回调已经执行
而且利用schedule这个方法你还要创建一堆回调,和临时函数,相返这个协程使用了Generator 方法,把函数转化成链执行,是不需要额外创建任何临时函数和回调。

其实为什么unity用c#的IEnumrator做他的coroutine,是因为c#本身的async/await用的线程池,多线程的,区别比较大。
js的async 是在主线程里面的,也是函数代码分片后变成callback的,其实触发分片用promise回调和用enumrator外部调度,在逻辑本身上是一样的。所以就没有弄couroutine类似的东西了。

1赞

我在unity也用 async/await,unitask就干这事的

你这样说,我可要怀疑你的js功底了