3、脚本

Cocos Creator 脚本用于实现用户定义的(游戏)行为,支持 JavaScript 和 TypeScript 两种编程语言。通过编写脚本组件,并将它挂载到场景节点中来驱动场景中的物体。

在组件脚本的编写过程中,开发者可以通过声明属性,将脚本中需要调节的变量映射到 属性检查器 中,以便策划和美术进行调整。与此同时,也可以通过注册特定的回调函数,来帮助初始化、更新甚至销毁节点。

语言支持

Cocos Creator 支持 TypeScriptJavaScript 两种编程语言。但需要注意的是,JavaScript 只支持以插件脚本的形式导入使用。

Cocos Creator 支持 TypeScript 4.1.0,使用时有以下需要注意:

脚本使用

创建脚本

image-20240829095231773

在创建脚本时,名称不能为空,输入框默认为 NewComponent

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('NewComponent')
export class NewComponent extends Component {
   start() {

   }

   update(deltaTime: number) {
       
   }
}
  1. 项目中所有脚本的类名 ClassName (如上例中的NewComponent) 不允许重复,即使脚本文件在不同的目录下,各自的代码里也不允许有相同的类名。
  2. 脚本文件名称和脚本的类名不同,在输入初始的文件名之后,文件名会被处理为类名。脚本文件生成后,对文件的后续操作脚本重命名,新的文件名不会再去生成并替换代码里的类名,不再影响了。
  3. 我们推荐用户使用 TypeScript 来编写脚本,目前 资源管理器 中仅支持创建 TypeScript 文件。但如果用户想要使用 JavaScript 来编写脚本的话,可以直接在操作系统的文件夹中创建 JavaScript 文件,或在其他代码编辑软件中创建 JavaScript 文件。

添加脚本到场景节点中

将脚本添加到场景节点中,实际上就是为这个节点添加一个脚本组件。

层级管理器 选中某个节点,此时 属性检查器 面板会显示该节点的属性。以下两种添加方式:

脚本运行环境

Cocos Creator 3.0 引擎的 API 都存在模块 cc 中,使用标准的 ES6 模块导入语法将其导入:

import {
    Component,  // 导入类 Component
    _decorator, // 导入命名空间 _decorator
    Vec3 // 导入类 Vec3
} from 'cc';

// 将整个 Cocos Creator 模块导入为命名空间 Cocos Creator
import * as modules from 'cc'; 

@_decorator.ccclass("MyComponent")
export class MyComponent extends Component {
    public v = new Vec3();
}

注意,由于历史原因,cc 是 Cocos Creator 3.0 保留使用的标识符,其行为 相当于 在任何模块顶部定义了名为 cc 的对象。因此,开发者不应该将 cc 用作任何 全局对象 的名称。

装饰器

ccclass

将装饰器 ccclass 应用在类上时,此类称为 cc 类。cc 类注入了额外的信息以控制 Cocos Creator 对该类对象的序列化、编辑器对该类对象的展示等。因此,未声明 ccclass 的组件类,也无法作为组件添加到节点上。

ccclass 装饰器的参数 name 指定了 cc 类的名称,cc 类名是 独一无二 的,这意味着即便在不同目录下的同名类也是不允许的。

组件类装饰器

此类装饰器是只能用来修饰 Component 的子类。

executeInEditMode

默认情况下,所有组件都只会在运行时执行,也就是说它们的生命周期回调在编辑器模式下并不会触发。executeInEditMode 允许当前组件在编辑器模式下运行,默认值为 false

const { ccclass, executeInEditMode } = _decorator;

@ccclass('Example')
@executeInEditMode(true)
export class Example extends Component {
    update (dt: number) {
        // 会在编辑器下每帧执行
    }
}

requireComponent

requireComponent 参数用来指定当前组件的依赖组件,默认值为 null。当组件添加到节点上时,如果依赖的组件不存在,引擎会自动将依赖组件添加到同一个节点,防止脚本出错。该选项在运行时同样有效。

const { ccclass, requireComponent } = _decorator;

@ccclass('Example')
@requireComponent(Sprite)
export class Example extends Component {
}

executionOrder

executionOrder 用来指定脚本生命周期回调的执行优先级。小于 0 的脚本将优先执行,大于 0 的脚本将最后执行。

该优先级设定只对 onLoadonEnablestartupdatelateUpdate 有效,对 onDisableonDestroy 无效。

const { ccclass, executionOrder } = _decorator;

@ccclass('Example')
@executionOrder(3)
export class Example extends Component {
}

disallowMultiple

同一节点上只允许添加一个同类型(含子类)的组件,防止逻辑发生冲突,默认值为 false。

const { ccclass, disallowMultiple } = _decorator;

@ccclass('Example')
@disallowMultiple(true)
export class Example extends Component {
}

menu

@menu(path) 用来将当前组件添加到组件菜单中,方便用户查找。

需要注意该菜单是添加在 属性检查器 面板中按下添加组件按钮后的下拉框内。

const { ccclass, menu } = _decorator;

@ccclass('Example')
@menu('foo/bar')
export class Example extends Component {
}

属性装饰器

属性装饰器 property 可以被应用在 cc 类的属性或访问器上。属性装饰器用于控制 Cocos Creator 编辑器中对该属性的序列化、属性检查器 中对该属性的展示等。

@property({
    type: Node,
    visible: true,
})
targetNode: Node | null = null;

生命周期

Cocos Creator 为组件脚本提供了生命周期的回调函数。开发者只需要定义特定的回调函数,Creator 就会在特定的时期自动执行相关脚本,开发者不需要手工调用它们。

目前提供给开发者的生命周期回调函数主要有(按生命周期触发先后排列):

onLoad

组件脚本的初始化阶段,我们提供了 onLoad 回调函数。onLoad 回调会在节点首次激活时触发,比如所在的场景被载入,或者所在节点被激活的情况下。在 onLoad 阶段,保证了你可以获取到场景中的其他节点,以及节点关联的资源数据。onLoad 总是会在任何 start 方法调用前执行,这能用于安排脚本的初始化顺序。

import { _decorator, Component, Node, SpriteFrame, find } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("test")
export class test extends Component {
    @property({type:SpriteFrame})
    bulletSprite=null;
    @property({type:Node})
    gun=null;

    _bulletRect=null;

    onLoad(){
        this._bulletRect=this.bulletSprite.getRect();
        this.gun = find('hand/weapon', this.node);
    }
}

onEnable

当组件的 enabled 属性从 false 变为 true 时,或者所在节点的 active 属性从 false 变为 true 时,会激活 onEnable 回调。倘若节点第一次被创建且 enabledtrue,则会在 onLoad 之后,start 之前被调用。

start

start 回调函数会在组件第一次激活前,也就是第一次执行 update 之前触发。start 通常用于初始化一些中间状态的数据,这些数据可能在 update 时会发生改变,并且被频繁的 enable 和 disable。

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("starttest")
export class starttest extends Component {

    private _timer: number = 0.0;

    start () {
        this._timer = 1.0;
    }

    update (deltaTime: number) {
        this._timer += deltaTime;
        if(this._timer >= 10.0){
            console.log('I am done!');
            this.enabled = false;
        }
     }
}

update

游戏开发的一个关键点是在每一帧渲染前更新物体的行为,状态和方位。这些更新操作通常都放在 update 回调中。

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("updatetest")
export class updatetest extends Component {

    update (deltaTime: number) {
        this.node.setPosition(0.0,40.0*deltaTime,0.0);
    }
}

增量时间deltaTime

首先我们得知道动画是什么。动画,动画,动态的图画,即当图画连续动起来就能形成动画,那么在人类眼中,1秒中需要24张图画快速更换才能在大脑中形成视觉暂缓而成的动画。这里我们就把图画的24张叫做24帧,也就是说人眼中需要1秒24帧的动画才能流畅播放。

因为硬件设备不同性能,A设备1秒能60帧,B设备1秒能120帧。也就是说:每分钟,A能触发update方法60次,B能触发update方法120次。

假设,要让一个角色速度为3m/s,连续移动

update(){
	// 增量距离: 将要移动的距离 = 速度3m/s * 时长1s
	distance = speed * 1;
	// 累计距离:成为新的位置
	position = position + distance;
}

这样就会导致一个问题,如果A设备为60帧,B设备120帧,那么A设备的玩家一分钟移动了3 * 60 = 180m,B设备的玩家移动了3 * 120 = 360m

所以为了解决这个问题,那么就引入了增量事件deltaTime,这个变量代表了,当前帧与上一帧中间的间隔时间,它并不是固定的,根据设备运行动态变动的,随时都不一样的。

update(deltaTime){
	// 增量距离: 将要移动的距离 = 速度3m/s * 时长1s
	distance = speed * deltaTime;
	// 累计距离:成为新的位置
	position = position + distance;
}

lateUpdate

update 会在所有动画更新前执行,但如果我们要在动效(如动画、粒子、物理等)更新之后才进行一些额外操作,或者希望在所有组件的 update 都执行完之后才进行其它操作,那就需要用到 lateUpdate 回调。

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("lateupdatetest")
export class lateupdatetest extends Component {

    lateUpdate (deltaTime: number) {
        this.node.setPosition(0.0,50,0.0);
    }
}

onDisable

当组件的 enabled 属性从 true 变为 false 时,或者所在节点的 active 属性从 true 变为 false 时,会激活 onDisable 回调。

onDestroy

当组件或者所在节点调用了 destroy(),则会调用 onDestroy 回调,并在当帧结束时统一回收组件。

脚本使用

访问节点和组件

你可以在 属性检查器 里修改节点和组件,也能在脚本中动态修改。动态修改的好处是能够在一段时间内连续地修改属性、过渡属性,实现渐变效果。脚本还能够响应玩家输入,能够修改、创建和销毁节点或组件,实现各种各样的游戏逻辑。要实现这些效果,你需要先在脚本中获得你要修改的节点或组件。

获得组件所在的节点

获得组件所在的节点很简单,只要在组件方法里访问 this.node 变量

start() {
    let node = this.node;
    node.setPosition(0.0, 0.0, 0.0);
}

获得其他组件

如果你经常需要获得同一个节点上的其它组件,这就要用到 getComponent 这个 API。

import { _decorator, Component, Label } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("test")
export class test extends Component {
    private label: any = null

    start() {
        this.label = this.getComponent(Label);
        let text = this.name + 'started';
        // Change the text in Label Component
        this.label.string = text;
    }
}

你也可以为 getComponent 传入一个类名。对用户定义的组件而言,类名就是脚本的文件名,并且 区分大小写。例如 "SinRotate.ts" 里声明的组件,类名就是 "SinRotate"。

let rotate = this.getComponent("SinRotate");

如果在节点上找不到你要的组件,getComponent 将返回 null。

获得其它节点及其组件

仅仅能访问节点自己的组件通常是不够的,脚本通常还需要进行多个节点之间的交互。例如,一门自动瞄准玩家的大炮,就需要不断获取玩家的最新位置。Cocos Creator 提供了一些不同的方法来获得其它节点或组件。

1、利用属性检查器设置节点
// Cannon.ts

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("Cannon")
export class Cannon extends Component {
    // 声明 Player 属性
    @property({ type: Node })
    private player = null;
}

这段代码在 properties 里面声明了一个 player 属性,默认值为 null,并且指定它的对象类型为 Node,然后就可以将节点拖到这个Player控件中。

player-in-inspector-null

2、查找子节点

有时候,游戏场景中会有很多个相同类型的对象,像是炮塔、敌人和特效,它们通常都有一个全局的脚本来统一管理。如果用 属性检查器 来一个一个将它们关联到这个脚本上,那工作就会很繁琐。为了更好地统一管理这些对象,我们可以把它们放到一个统一的父物体下,然后通过父物体来获得所有的子物体

// CannonManager.ts

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("CannonManager")
export class CannonManager extends Component {

    start() {
        let cannons = this.node.children;
        //...
    }

}

你还可以使用 getChildByName

this.node.getChildByName("Cannon 01");

如果子节点的层次较深,你还可以使用 findfind 将根据传入的路径进行逐级查找:

find("Cannon 01/Barrel/SFX", this.node);
3、全局名字查找

find 只传入第一个参数时,将从场景根节点开始逐级查找:

this.backNode = find("Canvas/Menu/Back");

访问已有变量里的值

如果你已经在一个地方保存了节点或组件的引用,你也可以直接访问它们,例如现在可以通过import{ } from + 文件名(不含路径) 来获取到对方 exports 的对象。

Global

// Global.ts, now the filename matters
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("Global")
export class Global extends Component {

    public static backNode: any = null;
    public static backLabel: any = null;
}

Back

// Back.ts
import { _decorator, Component, Node, Label } from 'cc';
const { ccclass, property } = _decorator;
// this feels more safe since you know where the object comes from
import{Global}from "./Global";

@ccclass("Back")
export class Back extends Component {
    onLoad() {
        Global.backNode = this.node;
        Global.backLabel = this.getComponent(Label);
    }
}

常用节点和组件接口

节点状态和层级操作

假设我们在一个组件脚本中,通过 this.node 访问当前脚本所在节点。

1、激活或关闭节点
this.node.active = false;

设置 active 属性和在编辑器中切换节点的激活、关闭状态,效果是一样的。当一个节点是关闭状态时,它的所有组件都将被禁用。同时,它所有子节点,以及子节点上的组件也会跟着被禁用。要注意的是,子节点被禁用时,并不会改变它们的 active 属性,因此当父节点重新激活的时候它们就会回到原来的状态。

2、更改节点的父节点
this.node.parent = parentNode;

或者

this.node.removeFromParent();
parentNode.addChild(this.node);
3、索引节点的子节点
this.node.children //返回节点的所有子节点数组。
this.node.children.length //返回节点的子节点数量。

更改节点的变换(位置、旋转、缩放)

1、更改节点位置

有以下两种方法:

2、更改节点旋转
this.node.setRotation(90, 90, 90);

或通过欧拉角设置本地旋转:

this.node.setRotationFromEuler(90, 90, 90);
3、更改节点缩放
this.node.setScale(2, 2, 2);

创建和销毁节点

创建节点

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("test")
export class test extends Component {

    start(){
        let node =new Node('box');
        node.setPosition(0,0,-10);
    }
}

需要注意的是,在上述的示例中通过 new Node 创建出来的节点并不会主动添加到场景内,直至用户调用 director.getScene().addChild(node) 来添加到场景内或者通过 node.parent = {a valid node} 来作为某个节点的子节点。

克隆节点

import { _decorator, Component, Node,instantiate, director } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("test")
export class test extends Component {

    @property({type:Node})
    private target: Node = null;

    start(){
        let scene = director.getScene();
        let node = instantiate(this.target);

        scene.addChild(node);
        node.setPosition(0, 0,-10);
    }
}

创建预制节点

import { _decorator, Component, Prefab, instantiate, director } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("test")
export class test extends Component {

    @property({type:Prefab})
    private target: Prefab = null;

    start(){
        let scene = director.getScene();
        let node = instantiate(this.target);

        scene.addChild(node);
        node.setPosition(0,0,0);
    }
}

销毁节点

通过 node.destroy() 函数,可以销毁节点。值得一提的是,销毁节点并不会立刻被移除,而是在当前帧逻辑更新结束后,统一执行。当一个节点销毁后,该节点就处于无效状态,可以通过 isValid 判断当前节点是否已经被销毁。

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("test")
export class test extends Component {

    @property({type:Node})
    private target: Node = null;

    private positionz: number = -20;

    start(){
        // 5秒后销毁节点
        setTimeout(function () {
            this.target.destroy();
          }.bind(this), 5000);
    }
    update(deltaTime: number){
        console.info(this.target.isValid);
        this.positionz += 1*deltaTime;
        if (this.target.isValid) {
            this.target.setPosition(0.0,0.0,this.positionz);
          }
    }
}

计时器

开始一个计时器将每隔 5s 执行一次

this.schedule(function() {
    // 这里的 this 指向 component
    this.doSomething();
}, 5);

更灵活的计时器(将在 10 秒后开始计时,每 5 秒执行一次回调,重复 3 + 1 次。)

// 以秒为单位的时间间隔
let interval = 5;
// 重复次数
let repeat = 3;
// 开始延时
let delay = 10;
this.schedule(function() {
    // 这里的 this 指向 component
    this.doSomething();
}, interval, repeat, delay);

只执行一次的计时器

this.scheduleOnce(function() {
    // 这里的 this 指向 component
    this.doSomething();
}, 2);

取消计时器

this.count = 0;
this.callback = function () {
    if (this.count == 5) {
        // 在第六次执行回调时取消这个计时器
        this.unschedule(this.callback);
    }
    this.doSomething();
    this.count++;
}
this.schedule(this.callback, 1);

取消组件中所有的计时器:unscheduleAllCallbacks

加载和切换场景

在 Cocos Creator 中,我们使用场景文件名(不包含扩展名)来索引指代场景。并通过以下接口进行加载和切换操作:

director.loadScene("MyScene");

通过常驻节点进行场景资源管理和参数传递

引擎同时只会运行一个场景,当切换场景时,默认会将场景内所有节点和其他实例销毁。如果我们需要用一个组件控制所有场景的加载,或在场景之间传递参数数据,就需要将该组件所在节点标记为「常驻节点」,使它在场景切换时不被自动销毁,常驻内存。

director.addPersistRootNode(myNode);

上面的接口会将 myNode 变为常驻节点,这样挂在上面的组件都可以在场景之间持续作用,我们可以用这样的方法来储存玩家信息,或下一个场景初始化时需要的各种数据。 需要注意的是,目标节点必须为位于层级的根节点,否则设置无效。

如果要取消一个节点的常驻属性:

director.removePersistRootNode(myNode);

需要注意的是上面的 API 并不会立即销毁指定节点,只是将节点还原为可在场景切换时销毁的节点。

场景加载回调

director.loadScene("MyScene", onSceneLaunched);

onSceneLaunched 就是声明在本脚本中的一个回调函数,在场景加载后可以用来进一步的进行初始化或数据传递的操作。

预加载场景

director.loadScene 会在加载场景之后自动切换运行新场景,有些时候我们需要在后台静默加载新场景,并在加载完成后手动进行切换。那就可以预先使用 preloadScene 接口对场景进行预加载:

director.preloadScene("table", function () {
    console.log('Next scene preloaded');
});

之后在合适的时间调用 loadScene,就可以真正切换场景。

director.loadScene("table");

就算预加载还没完成,你也可以直接调用 director.loadScene,预加载完成后场景就会启动。