Skip to content
脚本的生命周期

脚本的生命周期

阅读本文大概需要 15 分钟

什么是生命周期

生命周期就是指一个对象从诞生到死亡

生命周期(Life Cycle)的概念应用很广泛,特别是在政治、经济、环境、技术、社会等诸多领域经常出现,其基本涵义可以通俗地理解为“从摇篮到坟墓”(Cradle-to-Grave)的整个过程。而对于脚本而言,生命周期代表着一个脚本从激活(Activate)到销毁(Destroy)的全过程,也代表着代码中脚本函数的执行过程与执行顺序。

脚本的生命周期包括什么

onStart( ) : void

当脚本被实例后,会在第一帧更新之前调用 onStart 函数

注:编辑器在为任何脚本调用 onUpdate 等函数之前,将在所有脚本上调用 onStart 函数

onUpdate(dt : number) : void

编辑器会在游戏每帧调用一次 onUpdate 函数

这是用于帧更新的主要函数

注:其中(dt : number)为时间差值,表示当前帧与上一帧的延迟 / 秒

onDestroy( ) : void

脚本存在的最后一帧执行完,且在 onUpdate 函数执行完毕后,调用此函数

useUpdate : boolean

控制编辑器是否开启 onUpdate 函数的调用

默认编辑器不会开启脚本 onUpdate 的生命周期,需要开发者自行调用

ts
this. useUpdate = true;
this. useUpdate = true;

isRunningClient( ) : boolean

判断当前脚本是否执行在客户端,反之则运行在服务端

有关编辑器客户端与服务端的区别,请看网络同步原理和结构

脚本示例:

ts
@Component
export default class TestScript extends Script {
    protected onStart(): void {

        //开启onUpdate的函数
        this.useUpdate = true;

        //向控制台输出当前脚本是否执行在客户端
        this.myLog(`The script is running client? ===> ${this.isRunningClient()}`);

        if (this.isRunningClient()) {   //客户端===>

            //根据GUID持有cube对象
            let cube = GameObject.findGameObjectById(`48A8055A40BBA143D723B19BDB2D21ED`);

            this.myLog(`Into Client onStart()`);

            //向服务器派发删除cube事件,并将cube对象发送至服务端
            Event.dispatchToServer("DeleteCube", cube);
        }
        else {                          //服务端===>

            this.myLog(`Into Server onStart()`);

            //监听客户端删除cube的事件
            Event.addClientListener("DeleteCube", (player, cube: GameObject) => {

                //删除cube对象
                cube.destroy();

            });
        }
    }

    protected onUpdate(dt: number): void {

        if (this.isRunningClient()) {

            this.myLog(`Into Client onUpdate() > dTime:${dt}`);

        }
        else {

            this.myLog(`Into Server onUpdate() > dTime:${dt}`);
        }
    }

    protected onDestroy(): void {


        if (this.isRunningClient()) {

            this.myLog(`Into Client onDestroy()`);

        }
        else {
            
            this.myLog(`Into Server onDestroy()`);
        }

    }

    public myLog(msg:string)
    {
        console.log(`TestLog ===> ${msg}`);
        
    }
}
@Component
export default class TestScript extends Script {
    protected onStart(): void {

        //开启onUpdate的函数
        this.useUpdate = true;

        //向控制台输出当前脚本是否执行在客户端
        this.myLog(`The script is running client? ===> ${this.isRunningClient()}`);

        if (this.isRunningClient()) {   //客户端===>

            //根据GUID持有cube对象
            let cube = GameObject.findGameObjectById(`48A8055A40BBA143D723B19BDB2D21ED`);

            this.myLog(`Into Client onStart()`);

            //向服务器派发删除cube事件,并将cube对象发送至服务端
            Event.dispatchToServer("DeleteCube", cube);
        }
        else {                          //服务端===>

            this.myLog(`Into Server onStart()`);

            //监听客户端删除cube的事件
            Event.addClientListener("DeleteCube", (player, cube: GameObject) => {

                //删除cube对象
                cube.destroy();

            });
        }
    }

    protected onUpdate(dt: number): void {

        if (this.isRunningClient()) {

            this.myLog(`Into Client onUpdate() > dTime:${dt}`);

        }
        else {

            this.myLog(`Into Server onUpdate() > dTime:${dt}`);
        }
    }

    protected onDestroy(): void {


        if (this.isRunningClient()) {

            this.myLog(`Into Client onDestroy()`);

        }
        else {
            
            this.myLog(`Into Server onDestroy()`);
        }

    }

    public myLog(msg:string)
    {
        console.log(`TestLog ===> ${msg}`);
        
    }
}

客户端 Log:

服务端 Log:

如何合理利用脚本的生命周期

初始化

1)通常会将对象属性(例如:位置、状态等)的初始化做成一个函数,放在 onStart 中执行

2)来自服务器或者客户端的事件的监听,很多时候会写在 onStart 函数中

脚本示例:

ts
@Component
export default class TestScript extends Script {

    //声明一些属性
    public v3: Vector;
    public level: number;
    public name: string;

    protected onStart(): void {
    
        //在游戏开始第一帧初始化属性
        this.initUser();
        
        //在游戏开始第一帧注册事件监听
        this. initEvents();
        
    }
    
    //初始化属性的函数
    public initUser()
    {
        this.v3 = Vector.ZERO;
        this.level = 0;
        this.name = `userName`;
    
    }
    
    //初始化事件监听
    public initEvents()
    {
    
        Event.addServerListener("eventName",parm);
    
    }
   
}
@Component
export default class TestScript extends Script {

    //声明一些属性
    public v3: Vector;
    public level: number;
    public name: string;

    protected onStart(): void {
    
        //在游戏开始第一帧初始化属性
        this.initUser();
        
        //在游戏开始第一帧注册事件监听
        this. initEvents();
        
    }
    
    //初始化属性的函数
    public initUser()
    {
        this.v3 = Vector.ZERO;
        this.level = 0;
        this.name = `userName`;
    
    }
    
    //初始化事件监听
    public initEvents()
    {
    
        Event.addServerListener("eventName",parm);
    
    }
   
}

onUpdate 的函数‘潜规则’

1)尽量减少在 onUpdate 函数中写循环逻辑、递归

避免死循环或循环内出现空引用阻塞程序执行

2)逻辑代码尽量写成函数在 onUpdate 中调用,提高代码阅读性

3)在 onUpdate 函数执行的逻辑中,引用的对象尽量都做判空处理

提高定位逻辑问题效率,同时避免空引用阻塞程序执行

4)若非必要使用 onUpdate,尽量使用其他函数代替(例:计时器可用 setTimeout)

例:在做连击的判断中,需要对计时器做终止或重新计时的需求,此时 setTimeout 无法满足

ts
@Component
export default class TestScript extends Script {

    /** 是否可点击 */
    isCanHit = true;
    /** 点击CD */
    hitCD:number = 2;
    /** 控制点击的计时器 */
    canHitTimer:number = 0;

    /** 是否连击 */
    isCombo = false;
    /** 有效连击的CD */
    comboCD = 5;
    /** 控制连击的计时器 */
    comboTimer:number = 0;
    /** 连击次数 */
    comboCount:number = 0;
    /** 最大连击数 */
    maxComboCount:number = 0;

    protected onStart(): void {
    
        //开启onUpdate的函数
        this.useUpdate = true;
        
    }
    
    protected onUpdate(dt: number): void {
        
        //检查并计时连击与点击
        this.checkHit_Combo(dt);
        
    }
    
    checkHit_Combo(dt: number)
    {
        if (!this.isCanHit) {
            this.canHitTimer += dt;

            if (this.canHitTimer >= this.hitCD) {
                this.isCanHit = true;
                this.canHitTimer = 0;
            }
        }

        if (this.isCombo) {
            this.comboTimer += dt;
            if (this.comboTimer >= this.comboCD) {
                this.comboTimer = 0;
                this.comboCount = 0;

                this.isCombo = false;
            }
        }
    }
    
    //当用户点击
    puclic hit()
    {
        if (this.isCanHit) {
        
            this.canHitTimer = 0;
            this.isCanHit = false;
            
            if (this.isCombo) {
                this.comboCount++;
                this.comboTimer = 0;
                if(this.comboCount >= this.maxComboCount)
                {
                    this.comboCount = 0;
                }
            }
            else {
                this.comboTimer = 0;
                this.comboCount = 0;
                this.isCombo = true;
            }
            
            console.log(` this.comboCount ===> ${ this.comboCount}`);
        }
    }
}
@Component
export default class TestScript extends Script {

    /** 是否可点击 */
    isCanHit = true;
    /** 点击CD */
    hitCD:number = 2;
    /** 控制点击的计时器 */
    canHitTimer:number = 0;

    /** 是否连击 */
    isCombo = false;
    /** 有效连击的CD */
    comboCD = 5;
    /** 控制连击的计时器 */
    comboTimer:number = 0;
    /** 连击次数 */
    comboCount:number = 0;
    /** 最大连击数 */
    maxComboCount:number = 0;

    protected onStart(): void {
    
        //开启onUpdate的函数
        this.useUpdate = true;
        
    }
    
    protected onUpdate(dt: number): void {
        
        //检查并计时连击与点击
        this.checkHit_Combo(dt);
        
    }
    
    checkHit_Combo(dt: number)
    {
        if (!this.isCanHit) {
            this.canHitTimer += dt;

            if (this.canHitTimer >= this.hitCD) {
                this.isCanHit = true;
                this.canHitTimer = 0;
            }
        }

        if (this.isCombo) {
            this.comboTimer += dt;
            if (this.comboTimer >= this.comboCD) {
                this.comboTimer = 0;
                this.comboCount = 0;

                this.isCombo = false;
            }
        }
    }
    
    //当用户点击
    puclic hit()
    {
        if (this.isCanHit) {
        
            this.canHitTimer = 0;
            this.isCanHit = false;
            
            if (this.isCombo) {
                this.comboCount++;
                this.comboTimer = 0;
                if(this.comboCount >= this.maxComboCount)
                {
                    this.comboCount = 0;
                }
            }
            else {
                this.comboTimer = 0;
                this.comboCount = 0;
                this.isCombo = true;
            }
            
            console.log(` this.comboCount ===> ${ this.comboCount}`);
        }
    }
}

关闭监听(disconnectListener)

通常在 onStart 函数中或者在 UI 脚本中,我们会大量的使用 addListener 来监听事件

但是在该脚本所对应的对象被销毁(Destroy)的时候,其注册在系统内的监听事件并没有被关闭

所以,我们可以在生命周期中的onDestroy函数中增加关闭事件监听的逻辑

代码示例:

ts
@Component
export default class TestEvents extends Script {

    //声明事件数组
    myEvents = new Array<EventListener>();

    //声明一个计数变量
    public temp:number;

    protected async onStart(): Promise`<void>` {

        //初始化计数变量为0
        this.temp = 0;

        //根据GUID持有cube对象
        let cube = await GameObject.findGameObjectById(`48A8055A40BBA143D723B19BDB2D21ED`);


        //添加本地事件监听,并将监听器对象保存到事件数组
        this.myEvent.push(Event.addLocalListener("TestEvent1",()=>{
            console.log("========================>");
            console.log(`this.temp ===> ${this.temp}`);
        }));

        //当按下按键‘K’
        this.myEvent.push(InputUtil.OnKeyDown(Keys.K,()=>{
            this.temp ++;
            Event.dispatchToLocal("TestEvent1");
        }));

        //当按下横排按键‘L’
        this.myEvent.push(InputUtil.OnKeyDown(Keys.L,()=>{
            cube.Destroy();
            Event.dispatchToLocal("TestEvent1");
        }));
    }

    protected onDestroy(): void {

        console.log(`Into onDestroy()`);

        //在对象被销毁时,遍历所有事件对象,关闭所有事件监听
        this.myEvent.forEach(element => {
            element.disconnect();
        });
    }
}
@Component
export default class TestEvents extends Script {

    //声明事件数组
    myEvents = new Array<EventListener>();

    //声明一个计数变量
    public temp:number;

    protected async onStart(): Promise`<void>` {

        //初始化计数变量为0
        this.temp = 0;

        //根据GUID持有cube对象
        let cube = await GameObject.findGameObjectById(`48A8055A40BBA143D723B19BDB2D21ED`);


        //添加本地事件监听,并将监听器对象保存到事件数组
        this.myEvent.push(Event.addLocalListener("TestEvent1",()=>{
            console.log("========================>");
            console.log(`this.temp ===> ${this.temp}`);
        }));

        //当按下按键‘K’
        this.myEvent.push(InputUtil.OnKeyDown(Keys.K,()=>{
            this.temp ++;
            Event.dispatchToLocal("TestEvent1");
        }));

        //当按下横排按键‘L’
        this.myEvent.push(InputUtil.OnKeyDown(Keys.L,()=>{
            cube.Destroy();
            Event.dispatchToLocal("TestEvent1");
        }));
    }

    protected onDestroy(): void {

        console.log(`Into onDestroy()`);

        //在对象被销毁时,遍历所有事件对象,关闭所有事件监听
        this.myEvent.forEach(element => {
            element.disconnect();
        });
    }
}

编写脚本时有关生命周期的注意事项

开启 onUpdate 函数

useUpdate 的值默认为 false

必须手动修改 useUpdate 为 true 时,onUpdate 函数才会执行

异步函数

在 onStart 中,我们经常使用异步寻找等函数或语法

在使用异步的时候,要将函数添加 async 标识

例:protected async onStart(): Promise<void>