脚本生命周期
附加到实体的每个脚本实例都遵循明确的生命周期。理解它对于掌握代码何时运行、如何与应用其他部分交互十分重要。PlayCanvas 提供专门的生命周期方法,你可以在脚本中定义它们,引擎会在适当时机自动调用。
可以将其类比为演员表演的阶段:后台准备(initialize)、台上演出(update),以及最后谢幕(destroy 事件)。
如果实体通过 Script 组件附加了多个脚本,它们的 initialize、postInitialize、update、postUpdate 会按该实体脚本列表中的顺序调用,各帧间顺序保持一致。
生命周期方法
下面分别介绍关键生命周期方法。
initialize()
调用时机:
- 每个脚本实例调用一次。
- 脚本实例创建且实体启用后调用。
- 所有脚本属性解析并赋予初始值后调用,初始值可以来自默认值或编辑器设置。
- 应用加载、实体层级构建完成后调用,但早于第一次
update或首帧渲染。 - 应用启动时实体或脚本被禁用,则在二者首次同时启用时调用
initialize。
用途:
避免在 constructor 中编写启动逻辑,改用 initialize(),因为构造函数的执行顺序不受保证。
通过 entity.clone() 克隆实体时,不会立即调用其脚本的 initialize。只有副本随后加入场景层级,例如调用 this.app.root.addChild(clonedEntity),且实体和脚本都处于启用状态时,才会调用。
示例:
- ESM
- 经典脚本
import { Script } from 'playcanvas';
export class MyScript extends Script {
static scriptName = 'myScript';
initialize() {
// Subscribe to some script lifecycle events
this.on('enable', () => {
console.log('script enabled');
});
this.on('disable', () => {
console.log('script disabled');
});
this.once('destroy', () => {
console.log('script destroyed');
});
}
}
var MyScript = pc.createScript('myScript');
MyScript.prototype.initialize = function() {
// Subscribe to some script lifecycle events
this.on('enable', () => {
console.log('script enabled');
});
this.on('disable', () => {
console.log('script disabled');
});
this.once('destroy', () => {
console.log('script destroyed');
});
};
postInitialize()
调用时机:
- 每个脚本实例调用一次。
- 场景中所有已启用实体上的脚本实例执行完
initialize()后调用。
用途:
- 适合依赖其他脚本或实体已完成
initialize()的初始化逻辑。 - 避免一个脚本在另一个脚本尚未完成配置时就访问其属性而产生竞态。
示例:
- ESM
- 经典脚本
import { Script } from 'playcanvas';
export class MyScript extends Script {
static scriptName = 'myScript';
initialize() {
// Get a reference to another entity in the scene hierarchy
this.otherEntity = this.app.root.findByName('OtherEntity');
// Let's assume that when the initialize method of OtherEntity runs,
// it allocates a property called 'material'. At this point, we cannot
// be sure that OtherEntity's initialize method has executed...
}
postInitialize() {
// But we can be sure it has executed by the time we get to here...
const material = this.otherEntity.material;
}
}
var MyScript = pc.createScript('myScript');
MyScript.prototype.initialize = function() {
// Get a reference to another entity in the scene hierarchy
this.otherEntity = this.app.root.findByName('OtherEntity');
// Let's assume that when the initialize method of OtherEntity runs,
// it allocates a property called 'material'. At this point, we cannot
// be sure that OtherEntity's initialize method has executed...
};
MyScript.prototype.postInitialize = function() {
// But we can be sure it has executed by the time we get to here...
const material = this.otherEntity.material;
};
update(dt)
调用时机:
- 脚本实例、所属实体及全部祖先均启用时,每帧调用。
参数:
- dt(delta time):距上一帧的秒数,对编写与帧率无关的逻辑十分重要。
用途:
- 这是脚本运行时行为的核心。
- 用于:
- 处理连续输入。
- 更新位置、旋转和缩放,实现移动或动画。
- 检查游戏条件,如碰撞和胜负状态。
- 其他需要随时间反复执行的逻辑。
update 执行频繁,应尽可能高效。能放在 initialize 等其他阶段的重计算或对象分配,不应放在这里。
示例:
- ESM
- 经典脚本
import { Script } from 'playcanvas';
export class Rotator extends Script {
static scriptName = 'rotator';
update(dt) {
// Rotate the entity 10 degrees per second around the world Y axis
this.entity.rotate(0, 10 * dt, 0);
}
}
var Rotator = pc.createScript('rotator');
Rotator.prototype.update = function(dt) {
// Rotate the entity 10 degrees per second around the world Y axis
this.entity.rotate(0, 10 * dt, 0);
};
postUpdate(dt)
调用时机:
- 脚本实例和所属实体启用时,每帧调用。
- 当前帧所有脚本实例的
update()执行后调用。
参数:
- dt(delta time):与
update()相同。
用途:
- 适合需要在主要更新全部完成后运行的逻辑。
- 常见例子是跟随相机:玩家的
update移动玩家,相机的postUpdate再根据玩家新位置调整自身,实现平滑跟随。
示例:
- ESM
- 经典脚本
import { Script } from 'playcanvas';
export class TrackingCamera extends Script {
static scriptName = 'trackingCamera';
initialize() {
this.player = this.app.root.findByName('Player');
}
postUpdate(dt) {
// We know the player's position has been updated by now...
const playerPos = this.player.getPosition();
this.entity.lookAt(playerPos);
}
}
var TrackingCamera = pc.createScript('trackingCamera');
TrackingCamera.prototype.initialize = function() {
this.player = this.app.root.findByName('Player');
};
TrackingCamera.prototype.postUpdate = function(dt) {
// We know the player's position has been updated by now...
const playerPos = this.player.getPosition();
this.entity.lookAt(playerPos);
};
生命周期事件
除了 initialize、postInitialize、update 和 postUpdate,脚本实例还会在关键时刻发出事件。订阅事件可在状态变化时执行自定义逻辑,尤其适用于管理资源、切换行为和最终清理。
三个主要生命周期事件为 enable、disable 和 destroy。
enable 事件
触发时机:
- 脚本实例变为启用状态时触发,包括:
- 首次初始化,且脚本组件和实体初始均为启用状态。
- 通过代码将
this.enabled从 false 改为 true。 - 所属实体或祖先启用,而脚本自身已标记为启用。
用途:
- 在脚本从非活动状态恢复为活动状态时执行操作。
- 适用于:
- 恢复已暂停行为,例如继续动画,或重新注册禁用时移除的事件监听器。
- 更新视觉状态,表示已启用。
订阅方式:
// Typically inside initialize()...
this.on('enable', () => {
console.log('script enabled');
});
如果脚本初始启用,enable 会在初始化阶段触发。若要确保 onEnable 中的配置在初始启用时也执行,可以在 initialize 中订阅后,用 if (this.enabled) 判断并直接调用处理函数。
disable 事件
触发时机:
- 脚本实例变为禁用状态时触发,包括:
- 通过代码将
this.enabled从true改为false。 - 所属实体或任意祖先被禁用。
destroy事件触发前,因为脚本会在销毁前隐式禁用。
- 通过代码将
用途:
- 在脚本停止活动时执行操作。
- 适用于:
- 暂停行为,例如停止动画,或移除仅活动时需要的事件监听器。
- 释放仅启用时需要的临时资源。
- 更新视觉状态,表示已禁用。
订阅方式:
// Typically inside initialize()...
this.on('disable', () => {
console.log('script disabled');
});
state 事件
触发时机:
- 脚本实例的有效运行状态在启用和禁用之间变化时触发,包括:
- 通过代码修改脚本实例的
this.enabled。 - 所属 Script 组件的
enabled状态变化。 - 所属实体或任意祖先的
enabled状态变化。
- 通过代码修改脚本实例的
用途:
- 用一个回调处理脚本活动状态的所有变化。
- 适合无论刚刚启用还是禁用都要执行操作,并根据新状态处理的情况。
- 如果启用和禁用时的处理类似,只取决于最终状态,比单独处理
enable和disable更简洁。
参数:
- enabled(布尔值):脚本实例的新状态,刚启用为
true,刚禁用为false。
订阅方式:
// Typically inside initialize()...
this.on('state', (enabled) => {
console.log(`script ${enabled ? 'enabled' : 'disabled'}`);
});
destroy 事件
触发时机:
- 脚本实例即将销毁时触发,包括:
- 所属实体销毁。
- 包含该脚本实例的 Script 组件从实体上移除。
- 显式销毁脚本实例,例如
this.destroy(),但直接调用较少见。
用途:
- 这是脚本的最终清理阶段,对防止内存泄漏和正确结束功能十分重要。
- 必须考虑:
- 取消所有事件订阅,例如
this.app.off(...)、someEntity.off(...),以及取消自身事件的this.off(...)。 - 释放脚本创建或引用的外部资源和 DOM 元素。
- 清空其他对象引用,帮助垃圾回收。
- 取消所有事件订阅,例如
订阅方式:
// Typically inside initialize()...
this.once('destroy', () => {
console.log('script destroyed');
});
通常使用 this.once('destroy', ...),因为销毁处理函数只需执行一次。
如果脚本通过 on 或 once 注册了事件处理函数,请记得在 destroy 处理中通过 off 移除,否则垃圾回收器可能无法释放脚本占用的内存。