
Layaair V3
- 3 installs
- 8 repo stars
- Updated July 24, 2026
- zpqq132555/skills
Development guidance for the LayaAir 3.x game engine covering the ECS component system, decorators, Promise-based resource loading, the new Tween API, pools, and 2D/3D.
About
Provides TypeScript-strict development standards for LayaAir 3.x's ECS system with @regClass/@property decorators, Promise-based loading, chain/parallel Tween, and 2D/3D physics. A developer uses it when writing or refactoring LayaAir 3.x code or playable ads with 3.x-specific APIs.
- ECS with @regClass/@property decorators and new chain/parallel Tween (3.3+)
- Covers Promise-based Laya.loader.load, Laya.Pool, Sprite3D, and scenes
Layaair V3 by the numbers
- 3 all-time installs (skills.sh)
- Ranked #212 of 247 Game Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/zpqq132555/skills --skill layaair-v3Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 8 |
| Last updated | July 24, 2026 |
| Repository | zpqq132555/skills ↗ |
What it does
Development guidance for the LayaAir 3.x game engine covering the ECS component system, decorators, Promise-based resource loading, the new Tween API, pools, and 2D/3D.
Files
LayaAir 3.x 开发规范
⚠️ LayaAir 3.x(TypeScript):所有模式和示例均兼容 LayaAir 3.x 版本(3.0+),Tween 新 API 需 3.3+。
官方文档:https://www.layaair.com/3.x/doc/
API 参考:https://layaair.com/3.x/api/
GitHub 文档源码:https://github.com/layabox/LayaAir-Doc-ZH
---
技能用途
此技能为 LayaAir 3.x 项目提供全面的开发规范指导(TypeScript 严格模式优先):
若用户只提到 LayaAir 但没有明确版本,优先根据 API、项目结构和语言特征判断;只有在上下文明显指向 3.x 时才加载本技能。
优先级 1:代码质量与规范
- TypeScript 严格类型、
@regClass()和@property()装饰器 - 异常处理(不静默错误)
console.log仅用于开发环境- 正确的事件注册/注销配对(
onEnable/onDisable)
优先级 2:LayaAir 3.x 架构
- ECS 组件系统:
@regClass+@property装饰器、Script生命周期 - 事件系统:
EventDispatcher.on/off/event、脚本内置事件方法 - 资源管理:
Laya.loader.load()→ Promise、Laya.loader.fetch() - Tween 缓动(3.3+):
Laya.Tween.create()链式 API、chain()/parallel() - Timer 定时器:
Laya.timer.loop/once/frameLoop/callLater - 场景管理:
Laya.Scene.open/close/destroy、.ls场景文件 - 对象池:
Laya.Pool.getItemByClass/recover - 2D 显示对象:
Sprite、Text、Image、动画节点 - 3D 系统:
Sprite3D、Camera、Light、Material - UI 系统:UI 组件基类、容器布局、Dialog 弹窗
- 物理系统:2D
RigidBody+ Box2D / 3DRigidbody3D+ Bullet/PhysX
优先级 3:性能与可试玩广告优化
- DrawCall 优化、
drawCallOptimize、CacheAs 缓存 - 动态图集、对象池复用
- 屏幕适配模式(
fixedwidth/full/showall) - 包体 <5MB 策略
---
快速参考指南
| 任务 | 参考文档 |
|---|---|
| 组件系统、生命周期、装饰器 | 组件系统 |
| 装饰器完整参考(@regClass/@property/@runInEditor/@classInfo 全部参数) | 装饰器参考 |
| EventDispatcher、事件监听 | 事件模式 |
| 资源加载、释放 | 资源管理 |
| Tween 缓动、Timer 定时器 | 缓动与动画 |
| 场景管理、对象池 | 场景与对象池 |
| 2D/3D 显示对象 | 显示系统 |
| UI 组件系统 | UI 系统 |
| 2D/3D 物理系统 | 物理系统 |
| TypeScript 代码规范 | 质量与规范 |
| 性能优化、屏幕适配 | 性能优化 |
| 架构审查清单 | 架构审查 |
| 代码质量审查 | 质量审查 |
---
⚡ 快速 API 速查
项目入口与引擎初始化
// 方式一:启动脚本(Entry.ts)
export async function main() {
Laya.Scene.open('Scene.ls');
}
// 方式二:组件脚本(推荐,挂载到场景根节点)
const { regClass, property } = Laya;
@regClass()
export class Main extends Laya.Script {
public onStart(): void {
console.log("Game start");
}
}
// 引擎初始化回调
Laya.addBeforeInitCallback(() => {
Laya.Config.useWebGL2 = true;
});
Laya.addAfterInitCallback(() => {
console.log("引擎初始化完成");
});组件脚本(3.x 核心特性)
const { regClass, property } = Laya;
@regClass()
export class PlayerScript extends Laya.Script {
@property({ type: Number, tips: "移动速度" })
public speed: number = 5;
@property({ type: Laya.Sprite3D })
public target: Laya.Sprite3D;
@property({ type: Laya.Prefab })
public bulletPrefab: Laya.Prefab;
public onAwake(): void {
// 组件首次激活,只执行一次
}
public onEnable(): void {
// 每次添加到舞台(含对象池取出),注册事件
}
public onStart(): void {
// 第一次 onUpdate 之前,只执行一次
}
public onUpdate(): void {
// 每帧调用
}
public onLateUpdate(): void {
// 每帧 onUpdate 之后
}
public onDisable(): void {
// 从舞台移除,注销事件
}
public onDestroy(): void {
// 节点销毁
}
// 脚本内置鼠标事件
public onMouseClick(evt: Laya.Event): void { }
public onMouseDown(evt: Laya.Event): void { }
public onMouseUp(evt: Laya.Event): void { }
// 脚本内置键盘事件
public onKeyDown(evt: Laya.Event): void { }
public onKeyUp(evt: Laya.Event): void { }
// 脚本内置物理事件
public onTriggerEnter(other: any, self?: any, contact?: any): void { }
public onTriggerStay(other: any, self?: any, contact?: any): void { }
public onTriggerExit(other: any, self?: any, contact?: any): void { }
public onCollisionEnter(other: any, self?: any, contact?: any): void { }
public onCollisionStay(other: any, self?: any, contact?: any): void { }
public onCollisionExit(other: any, self?: any, contact?: any): void { }
}事件系统
// 注册事件
node.on(Laya.Event.CLICK, this, this.onClick);
node.once(Laya.Event.CLICK, this, this.onClick);
// 注销事件
node.off(Laya.Event.CLICK, this, this.onClick);
node.offAll(Laya.Event.CLICK);
node.offAllCaller(this); // 3.x 新增:注销 caller 所有事件
// 派发自定义事件
node.event("customEvent", data);
node.on("customEvent", this, (data) => { });
// 检查
node.hasListener(Laya.Event.CLICK);资源加载(Promise 风格)
// 单资源
Laya.loader.load("resources/image.png").then((res: Laya.Texture) => {
let img = new Laya.Image();
img.texture = res;
});
// 多资源
Laya.loader.load(["a.png", "b.json"]).then((res: any[]) => { });
// 带类型加载
Laya.loader.load(url, Laya.Loader.IMAGE).then((res) => { });
// fetch(不解析不缓存)
Laya.loader.fetch("data.json", "json").then((json) => { });
// 加载并使用缓存
Laya.loader.load("res.png").then(() => {
let tex = Laya.loader.getRes("res.png") as Laya.Texture;
});
// 释放资源
Laya.loader.clearRes("res.png");场景管理
// 打开场景
Laya.Scene.open("path/Scene.ls", false, { score: 100 });
// 接收参数
public onOpened(param: any): void {
console.log(param.score);
}
// 关闭场景
Laya.Scene.close("path/Scene.ls");
this.close();
Laya.Scene.closeAll();
// 销毁与 GC
Laya.Scene.destroy("scene.ls");
Laya.Scene.gc();Tween 缓动(3.3+ 新 API)
// 基础缓动
Laya.Tween.create(sprite).duration(1000).to("x", 500);
// from(从指定值到当前值)
Laya.Tween.create(sprite).duration(1000).from("x", -100);
// go(指定起始和结束值)
Laya.Tween.create(sprite).duration(500).go("x", 0, 300);
// 缓动函数 + 回调
Laya.Tween.create(sprite).duration(1000)
.to("x", 600).ease(Laya.Ease.cubicOut)
.then(this.onComplete, this);
// 串行动画
Laya.Tween.create(sprite).duration(1000).to("x", 600)
.chain().duration(2000).to("y", 400);
// 并行动画
Laya.Tween.create(sprite).duration(1000).to("x", 600)
.parallel().duration(2000).to("y", 400);
// 终止
tween.kill(); // 保持当前状态
tween.kill(true); // 跳到终态
// 震动效果
Laya.Tween.create(sprite).duration(1000)
.to("x", 0).interp(Laya.Tween.shake, 10);
// 兼容旧 API(3.3 前)
Laya.Tween.to(sprite, { x: 500, y: 300 }, 1000, Laya.Ease.sineOut);
Laya.Tween.from(sprite, { x: 0 }, 500, Laya.Ease.backOut);
Laya.Tween.clearAll(sprite);Timer 定时器
Laya.timer.once(1000, this, () => { }); // 延迟一次
Laya.timer.loop(1000, this, this.onTick); // 循环执行
Laya.timer.frameOnce(60, this, () => { }); // 60 帧后
Laya.timer.frameLoop(1, this, this.onFrame); // 每帧
Laya.timer.callLater(this, this.method); // 当前帧延迟
Laya.timer.pause();
Laya.timer.resume();
Laya.timer.clear(this, this.onTick);
Laya.timer.clearAll(this);对象池
// 获取(无则创建)
let bullet = Laya.Pool.getItemByClass("bullet", Bullet);
let obj = Laya.Pool.getItemByCreateFun("enemy", () => new Enemy());
// 回收
Laya.Pool.recover("bullet", bullet);
Laya.Pool.recoverByClass(instance);
// 清理
Laya.Pool.clearBySign("bullet");2D 显示对象
// Sprite
let sp = new Laya.Sprite();
sp.loadImage("atlas/comp/image.png");
sp.pos(100, 200);
sp.anchorX = 0.5;
sp.anchorY = 0.5;
sp.zIndex = 10;
sp.cacheAs = "bitmap";
Laya.stage.addChild(sp);
// Text
let txt = new Laya.Text();
txt.text = "Hello LayaAir 3.x";
txt.fontSize = 50;
txt.color = "#ffffff";
txt.bold = true;
txt.overflow = "ellipsis"; // visible|hidden|scroll|shrink|ellipsis
txt.align = "center";
Laya.stage.addChild(txt);
// 模板变量
txt.text = "第{n=1}页";
txt.setVar("n", 2);
// 节点操作
parent.addChild(child);
parent.removeChild(child);
child.removeSelf();
node.destroy(true);
let c = parent.getChildByName("name");3D 基础
// 摄像机射线检测
let point = new Laya.Vector2(Laya.stage.mouseX, Laya.stage.mouseY);
let ray = new Laya.Ray(new Laya.Vector3(), new Laya.Vector3());
camera.viewportPointToRay(point, ray);
scene.physicsSimulation.rayCastAll(ray, outs);
// 灯光
let dirLight = new Laya.Sprite3D();
let dirCom = dirLight.addComponent(Laya.DirectionLightCom);
dirCom.color = new Laya.Color(1, 1, 1, 1);
dirCom.shadowMode = Laya.ShadowMode.SoftLow;
// 点光源
let pointLight = new Laya.Sprite3D();
let pointCom = pointLight.addComponent(Laya.PointLightCom);
pointCom.range = 3.0;屏幕适配
// 移动端推荐
Laya.stage.scaleMode = "fixedwidth";
Laya.stage.designWidth = 1080;
Laya.stage.designHeight = 1920;
// PC 端推荐
Laya.stage.scaleMode = "showall";
// 3D 游戏推荐
Laya.stage.scaleMode = "full";3.x vs 2.x 关键区别
| 特性 | 3.x(本技能) | 2.x |
|---|---|---|
| 装饰器 | @regClass() + @property() | @prop 注释注入 |
| 资源加载 | Laya.loader.load() → Promise | Laya.loader.load() → Handler 回调 |
| Tween(3.3+) | Laya.Tween.create() 链式 API | Laya.Tween.to/from() |
| 场景文件 | .ls(包含 Scene2D + Scene3D) | .scene(统一) |
| 场景管理 | Laya.Scene.open() | Laya.Scene.open() |
| 事件注销 | 新增 offAllCaller(this) | 无此 API |
| 组件分组 | @classInfo({ menu, caption }) | 无 |
| IDE 运行 | @runInEditor | 不支持 |
---
目录结构建议
src/
├── Main.ts # 入口组件脚本(@regClass)
├── config/ # 全局配置
│ └── GameConfig.ts
├── manager/ # 单例管理器
│ ├── GameManager.ts
│ ├── AudioManager.ts
│ └── DataManager.ts
├── scene/ # 场景运行时脚本
│ ├── GameScene.ts
│ └── UIScene.ts
├── script/ # 通用 Script 组件
│ ├── PlayerScript.ts
│ └── EnemyScript.ts
├── common/ # 公共工具
│ ├── ObjectPool.ts
│ └── EventBus.ts
└── ui/ # UI 相关组件
└── DialogScript.ts
assets/
├── Scene.ls # 场景文件
├── resources/ # 动态加载资源
├── atlas/ # 图集配置
└── prefab/ # 预制体组件系统 — LayaAir 3.x
📖 LayaAir 3.x 核心特性:ECS 组件系统 + 装饰器(@regClass/@property),替代 2.x 的@prop注释。
---
1. 组件脚本(Script)生命周期
完整生命周期流程
组件挂载到节点
↓
onAdded() ← 被添加到节点后(即使节点未激活也调用)
↓
onReset() ← 重置参数(实现此函数则自动回收到对象池)
↓
onAwake() ← 组件首次激活,只执行一次
↓
onEnable() ← 组件被启用(每次添加到舞台都执行)
↓
onStart() ← 第一次 onUpdate 之前,只执行一次
↓
onUpdate() ← 每帧调用
onLateUpdate() ← 每帧 onUpdate 之后调用
↓
onPreRender() ← 渲染之前
onPostRender() ← 渲染之后
↓
onDisable() ← 组件被禁用(如从舞台移除)
↓
onDestroy() ← 节点销毁时(只执行一次)生命周期各阶段职责
| 生命周期 | 适合做的事 | 禁止做的事 |
|---|---|---|
onAdded | 极早期初始化,此时组件已挂载到节点 | 依赖激活状态的逻辑 |
onAwake | 获取 owner 引用、初始化内部状态(只执行一次) | 访问其他组件(可能未初始化) |
onEnable | 注册事件监听、启动定时器(对象池取出也会触发) | 仅应初始化的逻辑 |
onStart | 跨组件初始化、UI 绑定 | 每帧逻辑 |
onUpdate | 逻辑更新、移动、状态机 | 创建临时对象、find 查找 |
onLateUpdate | 摄像机跟随、后处理逻辑 | - |
onPreRender | 渲染前数据准备 | 耗时逻辑 |
onDisable | 注销事件监听、暂停定时器 | 释放已销毁的引用 |
onDestroy | 释放资源、清理定时器、清空引用 | - |
关键区别:
onAwake→ 只执行一次(首次激活时)onEnable→ 每次添加到舞台都执行(包括从对象池取出时)- 对象池复用场景必须在
onEnable中做初始化
---
2. 装饰器系统
@regClass() — 注册组件脚本类(必须)
const { regClass, property } = Laya;
@regClass()
export class MyScript extends Laya.Script { }- 所有继承
Laya.Script的类必须使用@regClass() - 否则无法在 IDE 中使用、属性不会被序列化
@property() — 暴露属性到 IDE
📖 完整参数详见 装饰器参考,此处仅列出常用写法。
// 完整参数
@property({ type: Number, caption: "速度", tips: "移动速度" })
public speed: number = 5;
// 简写
@property(Number)
public count: number = 0;
// 引擎对象类型
@property({ type: Laya.Sprite3D })
public target: Laya.Sprite3D;
@property({ type: Laya.Prefab })
public bulletPrefab: Laya.Prefab;
@property({ type: Laya.Camera })
public mainCamera: Laya.Camera;
// 字符串
@property({ type: String, caption: "名称" })
public playerName: string = "Hero";
// 布尔
@property({ type: Boolean })
public isAlive: boolean = true;
// 枚举
enum Direction { Up, Down, Left, Right }
@property(Direction)
public dir: Direction = Direction.Up;
// 字符串枚举(必须用标准写法)
enum StrEnum { A = "a", B = "b" }
@property({ type: StrEnum })
public strDir: StrEnum;
// 数组
@property({ type: ["number"] })
public scores: number[];
// 带滑动条的数值
@property({ type: Number, range: [0, 1], percentage: true })
public alpha: number = 1;
// 隐藏联动
@property(Boolean)
public showAdv: boolean = false;
@property({ type: Number, hidden: "!data.showAdv" })
public advParam: number = 0;
// 资源引用
@property({ type: String, isAsset: true, assetTypeFilter: "Image" })
public imagePath: string;@property() 常用参数速查
📖 全部 30+ 参数详见 装饰器参考
| 参数 | 类型 | 说明 |
|---|---|---|
type | 类型 | 必填。属性类型 |
caption | string | IDE 面板显示别名 |
tips | string | 鼠标悬停提示 |
serializable | boolean | 是否序列化保存(默认 true) |
private | boolean | 是否在面板隐藏 |
hidden | boolean \ | string |
readonly | boolean \ | string |
range | [min, max] | 滑动条范围 |
min / max | number | 最小/最大值 |
inspector | string | 输入控件类型("color" "vec3" 等) |
enumSource | array \ | string |
isAsset | boolean | 是否引用资源 |
catalog | string | 属性分类标签 |
catalogCaption | string | 分类别名 |
catalogOrder | number | 分类排序 |
onChange | string | 属性变化回调函数名 |
@runInEditor — 在 IDE 编辑模式运行
const { regClass, runInEditor } = Laya;
@regClass()
@runInEditor
export class EditorScript extends Laya.Script {
onUpdate(): void {
// 在 IDE 编辑器中也会执行
}
}@classInfo() — 组件分组管理
const { regClass, classInfo } = Laya;
@regClass()
@classInfo({ menu: "MyScripts", caption: "玩家控制器" })
export class PlayerController extends Laya.Script { }---
3. 标准脚本组件模板
const { regClass, property } = Laya;
@regClass()
export class EnemyScript extends Laya.Script {
@property({ type: Number, tips: "血量" })
public hp: number = 100;
@property({ type: Number, tips: "移动速度" })
public speed: number = 3;
@property({ type: Laya.Sprite3D, tips: "目标节点" })
public targetNode: Laya.Sprite3D;
private _isAlive: boolean = true;
public onAwake(): void {
// 初始化,只执行一次
}
public onEnable(): void {
// 注册事件(每次添加到舞台)
this.owner.on(Laya.Event.CLICK, this, this.onClicked);
}
public onStart(): void {
this._isAlive = true;
}
public onUpdate(): void {
if (!this._isAlive) return;
// 更新逻辑
}
public onDisable(): void {
// 注销事件(每次从舞台移除)
this.owner.off(Laya.Event.CLICK, this, this.onClicked);
}
public onDestroy(): void {
this.targetNode = null;
Laya.timer.clearAll(this);
}
private onClicked(evt: Laya.Event): void {
this.hp -= 10;
if (this.hp <= 0) {
this._isAlive = false;
this.owner.destroy(true);
}
}
}---
4. 脚本内置事件方法
LayaAir 3.x 的 Script 内置了鼠标、键盘、物理事件方法,无需手动注册:
鼠标事件
public onMouseDown(evt: Laya.Event): void { }
public onMouseUp(evt: Laya.Event): void { }
public onMouseClick(evt: Laya.Event): void { }
public onMouseMove(evt: Laya.Event): void { }
public onMouseOver(evt: Laya.Event): void { }
public onMouseOut(evt: Laya.Event): void { }
public onMouseDrag(evt: Laya.Event): void { }
public onMouseDragEnd(evt: Laya.Event): void { }键盘事件
public onKeyDown(evt: Laya.Event): void { }
public onKeyPress(evt: Laya.Event): void { }
public onKeyUp(evt: Laya.Event): void { }物理碰撞事件(2D/3D 通用)
public onTriggerEnter(other: any, self?: any, contact?: any): void { }
public onTriggerStay(other: any, self?: any, contact?: any): void { }
public onTriggerExit(other: any, self?: any, contact?: any): void { }
public onCollisionEnter(other: any, self?: any, contact?: any): void { }
public onCollisionStay(other: any, self?: any, contact?: any): void { }
public onCollisionExit(other: any, self?: any, contact?: any): void { }---
5. 与 2.x 组件系统的对比
| 特性 | 3.x | 2.x |
|---|---|---|
| 类注册 | @regClass() | 无需装饰器 |
| 属性暴露 | @property({ type }) | /** @prop {name, type} */ |
| IDE 运行 | @runInEditor | 不支持 |
| 组件分组 | @classInfo() | 不支持 |
| 内置事件 | onMouseClick 等直接重写 | 需手动 on(Event) |
| 生命周期 | 新增 onAdded、onReset、onPreRender、onPostRender | 无 |
| 对象池 | onReset 自动支持 | 手动实现 |
````markdown
组件装饰器完整参考 — LayaAir 3.x
📖 LayaAir 3.x 通过装饰器让 IDE 识别自定义组件脚本、暴露属性到属性面板、实现编辑器内运行和组件分组管理。
---
1. @regClass() — 注册组件脚本
每个组件脚本文件必须且只能有一个类使用 @regClass():
const { regClass } = Laya;
@regClass()
export class MyScript extends Laya.Script {
}规则:
- 未标记
@regClass()的类不会被 IDE 识别,无法添加到节点 - 一个 TS 文件只能有一个
@regClass()类 - 非 Script 子类也可使用
@regClass()(用于自定义对象类型被其他组件引用时) - 发布时,未被引用的
@regClass()类会被裁剪
---
2. @property() — 暴露属性到 IDE
2.1 基础用法
const { regClass, property } = Laya;
@regClass()
export class MyScript extends Laya.Script {
// 标准写法(推荐,支持 caption/tips 等完整功能)
@property({ type: String, caption: "IDE显示用的别名", tips: "这是一个文本对象" })
public text1: string = "";
// 简写方式(仅定义类型时使用)
@property(String)
public text2: string = "";
}2.2 类型定义
TS 基本类型
| 类型标识 | 说明 | 等价简写 |
|---|---|---|
"number" | 数字类型 | Number |
"string" | 单行字符串 | String |
"boolean" | 布尔值 | Boolean |
"int" | 整数 | { type: Number, fractionDigits: 0 } |
"uint" | 正整数 | { type: Number, fractionDigits: 0, min: 0 } |
"text" | 多行文本 | { type: String, multiline: true } |
"any" | 任意类型 | 只序列化,不可编辑 |
@property(Number) // 数字
num: number;
@property(String) // 单行字符串
str: string;
@property(Boolean) // 布尔
bool: boolean;
@property("int") // 整数
int: number;
@property("uint") // 正整数
uint: number;
@property("text") // 多行文本
text: string;
@property("any") // 任意类型(仅序列化)
a: any;引擎对象类型
@property({ type: Laya.Camera })
private camera: Laya.Camera;
@property({ type: Laya.Scene3D })
private scene3D: Laya.Scene3D;
@property({ type: Laya.Sprite3D })
private cube: Laya.Sprite3D;
@property({ type: Laya.Sprite })
private sprite: Laya.Sprite;
@property({ type: Laya.Node })
private node: Laya.Node;
@property({ type: Laya.Prefab })
private prefab: Laya.Prefab;
@property({ type: Laya.Image })
private image: Laya.Image;
@property({ type: Laya.Label })
private label: Laya.Label;
@property({ type: Laya.Button })
private button: Laya.Button;
@property({ type: Laya.Box })
private box: Laya.Box;
@property({ type: Laya.List })
private list: Laya.List;
@property({ type: Laya.Animation })
private animation: Laya.Animation;
@property({ type: Laya.Vector3 })
private vector3: Laya.Vector3;
@property({ type: Laya.Color })
private color: Laya.Color;
@property({ type: Laya.DirectionLightCom })
private dirLight: Laya.DirectionLightCom;
@property({ type: Laya.ShurikenParticleRenderer })
private particle: Laya.ShurikenParticleRenderer;类型化数组
支持 7 种:Int8Array、Uint8Array、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array
@property(Int8Array)
i8a: Int8Array;
@property(Float32Array)
f32a: Float32Array;数组类型
@property({ type: ["number"] })
arr1: number[];
@property({ type: ["string"] })
arr2: string[];
@property({ type: [Laya.Prefab] })
prefabs: Laya.Prefab[];枚举类型
// 普通枚举(可用简写)
enum TestEnum { A, B, C }
@property(TestEnum)
enumVal: TestEnum;
// 字符串枚举(必须使用标准写法,不能简写!)
enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT' }
@property({ type: Direction })
dir: Direction;字典类型(Record)
// Record 类型,第二个参数为值的类型
@property({ type: ["Record", Number] })
dict: Record<string, number>;
@property({ type: ["Record", String] })
dictStr: Record<string, string>;自定义对象类型
// Animal.ts — 自定义数据对象(也需要 @regClass())
const { regClass, property } = Laya;
@regClass()
export default class Animal {
@property({ type: Number })
weight: number;
}
// MyScript.ts — 引用自定义对象
import Animal from "./Animal";
@regClass()
export class MyScript extends Laya.Script {
@property({ type: Animal })
animal: Animal;
}2.3 访问器(Getter/Setter)装饰器
@regClass()
class Animal {
private _weight: number = 0;
// Getter 和 Setter 同时存在时,装饰 Getter
@property({ type: Number })
get weight(): number {
return this._weight;
}
// 没有 Setter 则为只读属性
set weight(value: number) {
this._weight = value;
}
}2.4 @property() 全部参数速查
基础参数
| 参数 | 类型 | 说明 |
|---|---|---|
type | 类型 | 必填。属性的值类型 |
caption | string | IDE 属性面板的显示别名 |
tips | string | 鼠标悬停提示说明 |
name | string | 属性名称,一般不需设定 |
序列化与可见性
| 参数 | 类型 | 说明 |
|---|---|---|
serializable | boolean | 是否序列化保存到 .ls 文件(默认 true) |
private | boolean | 是否在 IDE 面板上隐藏。默认下划线属性为 true,非下划线为 false |
hidden | boolean \ | string |
readonly | boolean \ | string |
数字控制
| 参数 | 类型 | 说明 |
|---|---|---|
min | number | 最小值 |
max | number | 最大值 |
range | [number, number] | 滑动条范围,如 [0, 5] |
step | number | 鼠标滑动/滚轮的最小精度 |
fractionDigits | number | 小数点后保留位数 |
percentage | boolean | 配合 range: [0,1] 显示为百分比 |
字符串控制
| 参数 | 类型 | 说明 |
|---|---|---|
multiline | boolean | 是否多行输入 |
password | boolean | 密码输入模式 |
submitOnTyping | boolean | true=每次输入提交;false=失焦后提交 |
prompt | string | 输入框占位提示文本 |
输入控件与验证
| 参数 | 类型 | 说明 |
|---|---|---|
inspector | string \ | null |
validator | string | 验证表达式:"if (value == data.text1) return '不能相同'" |
enumSource | array \ | string |
reverseBool | boolean | 反转布尔值显示 |
nullable | boolean | 是否允许 null(默认 true) |
颜色控制
| 参数 | 类型 | 说明 |
|---|---|---|
showAlpha | boolean | 是否提供透明度 alpha 修改 |
defaultColor | string | 非 null 时的默认颜色,如 "rgba(217, 232, 0, 1)" |
colorNullable | boolean | 显示 checkbox 决定颜色是否为 null |
数组控制
| 参数 | 类型 | 说明 |
|---|---|---|
fixedLength | boolean | 固定数组长度,不允许修改 |
arrayActions | string[] | 允许的操作:"append" "insert" "delete" "move" |
elementProps | object | 数组元素的属性,如 { range: [0, 100] } |
资源相关
| 参数 | 类型 | 说明 |
|---|---|---|
isAsset | boolean | 说明此属性引用资源 |
assetTypeFilter | string | 资源类型过滤,如 "Image" |
useAssetPath | boolean | true=使用原始路径,false=使用 res://uuid 格式(默认 false) |
分类与排序
| 参数 | 类型 | 说明 |
|---|---|---|
catalog | string | 属性分类标签名,相同值归为一组 |
catalogCaption | string | 分类栏目的中文别名 |
catalogOrder | number | 分类栏目排序,数值越小越靠前 |
position | string | 显示顺序:"before x" "after x" "first" "last" |
addIndent | number | 增加缩进层级 |
回调
| 参数 | 类型 | 说明 |
|---|---|---|
onChange | string | 属性变化时调用的函数名(需在当前类中定义) |
2.5 常用组合示例
const { regClass, property } = Laya;
enum TestEnum { A, B, C }
@regClass()
export class DemoScript extends Laya.Script {
// ========== 基础类型 ==========
@property({ type: Number, caption: "血量", tips: "角色血量", min: 0, max: 100 })
public hp: number = 100;
@property({ type: String, caption: "名称" })
public playerName: string = "Hero";
@property(Boolean)
public isAlive: boolean = true;
// ========== 带滑动条的数值 ==========
@property({ type: Number, range: [0, 1], percentage: true, caption: "透明度" })
public alpha: number = 1;
@property({ type: Number, range: [0, 360], step: 1, fractionDigits: 0, caption: "角度" })
public angle: number = 0;
// ========== 下拉框 ==========
@property(TestEnum)
public enumVal: TestEnum;
@property({ type: Number, enumSource: [{ name: "是", value: 1 }, { name: "否", value: 0 }] })
public yesNo: number;
// ========== 颜色 ==========
@property({ type: Laya.Color, showAlpha: false })
public color: Laya.Color;
@property({ type: String, inspector: "color" })
public colorStr: string;
// ========== 隐藏/只读联动 ==========
@property(Boolean)
public showAdvanced: boolean = false;
@property({ type: Number, hidden: "!data.showAdvanced", caption: "高级参数" })
public advancedParam: number = 0;
@property({ type: Boolean })
public lockName: boolean = false;
@property({ type: String, readonly: "data.lockName", caption: "名称" })
public charName: string = "";
// ========== 序列化控制(弧度/角度转换)==========
@property({ type: Number })
_radian: number = 0;
@property({ type: Number, caption: "角度值", serializable: false })
get degree(): number {
return this._radian * (180 / Math.PI);
}
set degree(value: number) {
this._radian = value * (Math.PI / 180);
}
// ========== 私有属性显示到面板 ==========
@property({ type: "number", private: false })
_velocity: number = 0;
// ========== 资源引用 ==========
@property({ type: String, isAsset: true, assetTypeFilter: "Image" })
public imagePath: string;
@property({ type: Laya.Prefab })
public prefab: Laya.Prefab;
// ========== 数组 ==========
@property({ type: ["number"], fixedLength: true })
public scores: number[] = [0, 0, 0];
@property({ type: [Number], elementProps: { range: [0, 100] } })
public values: number[];
// ========== 输入验证 ==========
@property(String)
public text1: string;
@property({ type: String, validator: "if (value == data.text1) return '不能与 text1 值相同'" })
public text2: string = "";
// ========== 属性分类 ==========
@property({ type: "boolean", catalog: "adv", catalogCaption: "高级设置", catalogOrder: 1 })
public debugMode: boolean;
@property({ type: Number, catalog: "adv" })
public debugLevel: number = 0;
// ========== 属性变化回调 ==========
@property({ type: Boolean, onChange: "onDebugChanged" })
public enableDebug: boolean;
onDebugChanged(): void {
console.log("Debug mode changed:", this.enableDebug);
}
}2.6 嵌套数组与字典(特殊用法)
// 二维字符串数组
@property([["string"]])
test1: string[][] = [["a", "b"], ["c", "d"]];
// 字典数组
@property([["Record", "string"]])
test2: Array<Record<string, string>> = [{ name: "A", value: "a" }];
// 字典值为数组
@property({ type: ["Record", [Number]], elementProps: { elementProps: { range: [0, 10] } } })
test3: Record<string, number[]> = { a: [1, 2, 3] };
// 字典值为 Prefab 数组
@property(["Record", [Laya.Prefab]])
test4: Record<string, Laya.Prefab[]>;2.7 动态下拉框
// 提供动态选项的 getter(数据仅用于编辑器,不序列化)
@property({ type: [["Record", String]], serializable: false })
get itemsProvider(): Array<Record<string, string>> {
return [{ name: "Item0", value: "0" }, { name: "Item1", value: "1" }];
}
// enumSource 设为字符串,表示使用该属性名作为下拉数据源
@property({ type: String, enumSource: "itemsProvider" })
enumItems: string;---
3. @runInEditor — IDE 编辑模式运行
让组件在 IDE 编辑器内也触发生命周期方法(onEnable、onStart、onUpdate 等):
const { regClass, property, runInEditor } = Laya;
@regClass()
@runInEditor // 放在类之前,与 @regClass() 谁先谁后均可
export class EditorScript extends Laya.Script {
@property({ type: Laya.Sprite3D })
sp3: Laya.Sprite3D;
onEnable(): void {
console.log("编辑器中也会执行", this.sp3.name);
}
}⚠️ 注意事项:
- 不建议在
@runInEditor中做复杂动画/物理逻辑 - IDE 场景编辑器帧率较低,效果与实际运行有差异
- 静态物体更有利于 IDE 编辑
---
4. @classInfo() — 组件分组管理
4.1 组件列表分类
将组件加入 IDE 增加组件列表的自定义分类:
const { regClass, classInfo } = Laya;
@regClass()
@classInfo({
menu: "MyScript", // 分类菜单路径
caption: "Main", // 组件在列表中的显示名
})
export class Main extends Laya.Script {
onStart(): void {
console.log("Game start");
}
}4.2 属性分组
将多个属性显示在一个可折叠的组内:
const { regClass, property, classInfo } = Laya;
@regClass()
@classInfo({
properties: [
{
name: "Group1",
inspector: "Group",
options: {
members: ["b", "c"] // 指定组内属性名
// 也支持范围语法:["b~c"]
},
position: "after a" // 可选,指定分组显示位置
}
]
})
export class MyScript extends Laya.Script {
@property(String)
public a: string = "";
@property(String)
public b: string = "";
@property(String)
public c: string = "";
@property(String)
public d: string = "";
}---
5. 装饰器解构模式
所有装饰器必须从 Laya 解构后使用:
// ✅ 正确 — 从 Laya 解构
const { regClass, property } = Laya;
const { regClass, property, runInEditor } = Laya;
const { regClass, property, classInfo } = Laya;
const { regClass, property, runInEditor, classInfo } = Laya;
// ❌ 错误 — 不能直接用 Laya.regClass
@Laya.regClass() // 不支持---
6. 完整模板
const { regClass, property, classInfo } = Laya;
enum GameState { Idle, Running, Paused, Over }
@regClass()
@classInfo({
menu: "Game/Player",
caption: "玩家控制器",
properties: [
{
name: "MovementGroup",
inspector: "Group",
options: { members: ["speed", "jumpForce"] },
position: "after hp"
}
]
})
export class PlayerController extends Laya.Script {
// === 基础属性 ===
@property({ type: Number, caption: "血量", min: 0, max: 100 })
public hp: number = 100;
// === 运动参数(分组)===
@property({ type: Number, caption: "移动速度", range: [0, 20], step: 0.5 })
public speed: number = 5;
@property({ type: Number, caption: "跳跃力", min: 0 })
public jumpForce: number = 10;
// === 引用 ===
@property({ type: Laya.Camera, tips: "主摄像机" })
public mainCamera: Laya.Camera;
@property({ type: Laya.Prefab, tips: "子弹预制体" })
public bulletPrefab: Laya.Prefab;
// === 高级设置 ===
@property(Boolean)
public showDebug: boolean = false;
@property({ type: GameState, hidden: "!data.showDebug" })
public forceState: GameState;
@property({ type: Laya.Color, catalog: "Visual", catalogCaption: "视觉效果" })
public hitColor: Laya.Color;
// === 生命周期 ===
onAwake(): void { }
onEnable(): void { }
onStart(): void { }
onUpdate(): void { }
onDisable(): void { }
onDestroy(): void { }
}````
显示系统 — LayaAir 3.x
📖 LayaAir 3.x 的 2D 和 3D 显示对象体系。
---
1. 2D 显示对象
Sprite(精灵)— 所有 2D 节点的基类
// 创建 Sprite
let sp = new Laya.Sprite();
Laya.stage.addChild(sp);
// 位置与变换
sp.pos(100, 200);
sp.size(200, 100);
sp.anchorX = 0.5;
sp.anchorY = 0.5;
sp.rotation = 45;
sp.scale(2, 2);
sp.alpha = 0.8;
sp.visible = true;
// 加载图片
sp.loadImage("atlas/comp/image.png");
// 通过 texture
Laya.loader.load("img.png").then(() => {
sp.texture = Laya.loader.getRes("img.png");
});
// 渲染优化
sp.zIndex = 10; // 渲染排序
sp.cacheAs = "bitmap"; // 静态缓存
sp.drawCallOptimize = true; // 3.3+ 动态合批
// 鼠标交互
sp.mouseEnabled = true;
sp.hitTestPrior = true; // 优先检测自身节点操作
// 添加子节点
parent.addChild(child);
parent.addChildAt(child, 0);
parent.addChildren(a, b, c);
// 查找节点
let c = parent.getChildByName("hero") as Laya.Sprite;
let c2 = parent.getChildAt(0);
let idx = parent.getChildIndex(child);
// 修改层级
parent.setChildIndex(child, 0);
parent.replaceChild(newChild, oldChild);
// 移除节点
parent.removeChild(child);
child.removeSelf();
parent.removeChildByName("hero");
parent.removeChildAt(0);
parent.removeChildren(0, 5);
// 包含关系
parent.contains(child); // 是否包含
parent.isAncestorOf(child); // 是否祖先
// 属性
parent.numChildren; // 子节点数量
child.parent; // 父节点
// 销毁
node.destroy(true); // true = 递归销毁子节点Text(基础文本)
let txt = new Laya.Text();
Laya.stage.addChild(txt);
txt.text = "Hello LayaAir 3.x";
txt.font = "Arial";
txt.fontSize = 50;
txt.color = "#ffffff";
txt.bold = true;
txt.italic = true;
txt.underline = true;
txt.align = "center"; // left | center | right
txt.valign = "middle"; // top | middle | bottom
txt.wordWrap = true;
txt.leading = 10; // 行间距
txt.padding = [10, 10, 10, 10]; // 上右下左
// 溢出模式
txt.overflow = "visible"; // visible | hidden | scroll | shrink | ellipsis
// 描边
txt.stroke = 2;
txt.strokeColor = "#000000";
// 模板变量
txt.text = "第{n=1}页";
txt.setVar("n", 2);
// UBB 语法支持
txt.text = "[b]粗体[/b] [color=#FF0000]红色[/color] [size=60]大字[/size]";
txt.text = "[img]res/icon.png[/img]"; // 内嵌图片Image(图像组件)
let img = new Laya.Image();
img.skin = "res/icon.png";
img.sizeGrid = "30,30,30,30"; // 九宫格
Laya.stage.addChild(img);绘图 API
let sp = new Laya.Sprite();
let g = sp.graphics;
// 矩形
g.drawRect(0, 0, 200, 100, "#FF0000", "#000000", 2);
// 圆形
g.drawCircle(100, 100, 50, "#00FF00");
// 线条
g.drawLine(0, 0, 200, 200, "#0000FF", 3);
// 多边形
g.drawPoly(100, 100, [0,0, 100,0, 50,80], "#FFFF00");
// 清除
g.clear();
Laya.stage.addChild(sp);---
2. 3D 显示对象
Sprite3D(3D 精灵)
// 创建 3D 精灵
let sp3d = new Laya.Sprite3D("myCube");
scene3d.addChild(sp3d);
// Transform
sp3d.transform.position = new Laya.Vector3(0, 1, 0);
sp3d.transform.localPosition = new Laya.Vector3(0, 1, 0);
sp3d.transform.rotation = new Laya.Quaternion(0, 0, 0, 1);
sp3d.transform.localRotationEuler = new Laya.Vector3(0, 45, 0);
sp3d.transform.localScale = new Laya.Vector3(1, 1, 1);
// 世界变换
sp3d.transform.translate(new Laya.Vector3(0, 0, 1));
sp3d.transform.rotate(new Laya.Vector3(0, 1, 0), 10);
sp3d.transform.lookAt(targetPos, Laya.Vector3.Up);
// 前方向
let forward = new Laya.Vector3();
sp3d.transform.getForward(forward);
// 添加组件
let meshRenderer = sp3d.addComponent(Laya.MeshRenderer);
let script = sp3d.addComponent(MyScript);
// 获取组件
let comp = sp3d.getComponent(MyScript);
// 激活/隐藏
sp3d.active = true;Camera(3D 摄像机)
// 创建
let cameraNode = new Laya.Sprite3D("Camera");
let camera = cameraNode.addComponent(Laya.Camera);
scene3d.addChild(cameraNode);
// 投影模式
camera.orthographic = false; // 透视投影
camera.fieldOfView = 60; // FOV
camera.nearPlane = 0.3;
camera.farPlane = 1000;
// 正交投影
camera.orthographic = true;
camera.orthographicVerticalSize = 10;
// 清除标记
camera.clearFlag = Laya.CameraClearFlags.SolidColor;
camera.clearColor = new Laya.Color(0.2, 0.2, 0.2, 1);
// 视口
camera.viewport = new Laya.Viewport(0, 0, Laya.stage.width, Laya.stage.height);
// 射线检测
let point = new Laya.Vector2(Laya.stage.mouseX, Laya.stage.mouseY);
let ray = new Laya.Ray(new Laya.Vector3(), new Laya.Vector3());
camera.viewportPointToRay(point, ray);
scene3d.physicsSimulation.rayCastAll(ray, outs);
// 图层管理
camera.removeAllLayers();
camera.addLayer(1);
// lookAt
camera.transform.lookAt(new Laya.Vector3(0, 0, 0), new Laya.Vector3(0, 1, 0));
// 渲染到纹理
let renderTarget = new Laya.RenderTexture(512, 512);
camera.renderTarget = renderTarget;Light(灯光)
// 方向光
let dirNode = new Laya.Sprite3D("DirLight");
let dirLight = dirNode.addComponent(Laya.DirectionLightCom);
dirLight.color = new Laya.Color(1, 1, 1, 1);
dirLight.intensity = 1.0;
dirLight.shadowMode = Laya.ShadowMode.SoftLow;
dirLight.shadowDistance = 50;
dirLight.shadowResolution = 1024;
scene3d.addChild(dirNode);
// 点光源
let pointNode = new Laya.Sprite3D("PointLight");
let pointLight = pointNode.addComponent(Laya.PointLightCom);
pointLight.color = new Laya.Color(1, 0.5, 0, 1);
pointLight.range = 5.0;
pointLight.intensity = 2.0;
scene3d.addChild(pointNode);
// 聚光灯
let spotNode = new Laya.Sprite3D("SpotLight");
let spotLight = spotNode.addComponent(Laya.SpotLightCom);
spotLight.range = 10;
spotLight.spotAngle = 30;
scene3d.addChild(spotNode);Material(材质)
// PBR 材质
let mat = new Laya.PBRStandardMaterial();
mat.albedoColor = new Laya.Color(1, 0, 0, 1);
mat.metallic = 0.8;
mat.smoothness = 0.6;
mat.albedoTexture = tex;
// 应用材质
meshRenderer.material = mat;
// Unlit 材质
let unlitMat = new Laya.UnlitMaterial();
unlitMat.albedoColor = new Laya.Color(0, 1, 0, 1);---
3. 常用数学类
// Vector2/3/4
let v2 = new Laya.Vector2(1, 2);
let v3 = new Laya.Vector3(1, 2, 3);
let v4 = new Laya.Vector4(1, 2, 3, 4);
// Vector3 运算
Laya.Vector3.add(a, b, out);
Laya.Vector3.subtract(a, b, out);
Laya.Vector3.scale(a, 2, out);
Laya.Vector3.normalize(a, out);
Laya.Vector3.dot(a, b);
Laya.Vector3.cross(a, b, out);
Laya.Vector3.distance(a, b);
Laya.Vector3.lerp(a, b, t, out);
// 常用常量
Laya.Vector3.Zero; // (0, 0, 0)
Laya.Vector3.One; // (1, 1, 1)
Laya.Vector3.Up; // (0, 1, 0)
// Quaternion
let q = new Laya.Quaternion();
Laya.Quaternion.createFromEuler(0, 45, 0, q);
// Color
let color = new Laya.Color(1, 0, 0, 1); // RGBA 0~1事件模式 — LayaAir 3.x
📖 LayaAir 3.x 基于EventDispatcher事件系统,所有Sprite、Script、Node、Stage均继承自EventDispatcher。
---
1. 事件系统基础
注册与注销模式
const { regClass, property } = Laya;
@regClass()
export class PlayerScript extends Laya.Script {
public onEnable(): void {
// ✅ 注册事件(与 onDisable 配对)
this.owner.on(Laya.Event.CLICK, this, this.onClick);
Laya.stage.on(Laya.Event.RESIZE, this, this.onResize);
}
public onDisable(): void {
// ✅ 注销事件(必须与 onEnable 配对)
this.owner.off(Laya.Event.CLICK, this, this.onClick);
Laya.stage.off(Laya.Event.RESIZE, this, this.onResize);
}
private onClick(evt: Laya.Event): void {
console.log("clicked at:", evt.stageX, evt.stageY);
}
private onResize(): void {
// 处理尺寸变化
}
}EventDispatcher API
| 方法 | 说明 |
|---|---|
node.on(type, caller, fn) | 持续监听 |
node.once(type, caller, fn) | 只触发一次后自动注销 |
node.off(type, caller, fn) | 注销指定监听 |
node.offAll(type) | 注销该类型所有监听 |
node.offAllCaller(caller) | 3.x 新增:注销指定 caller 的所有事件 |
node.hasListener(type) | 是否有该类监听 |
node.event(type, data?) | 派发事件 |
// 3.x 新增便捷 API:注销 caller 所有事件
onDestroy(): void {
this.owner.offAllCaller(this); // 一次清除所有监听
}---
2. Laya.Event 常用常量
鼠标/触摸事件
| 常量 | 说明 |
|---|---|
Laya.Event.CLICK | 点击 |
Laya.Event.DOUBLE_CLICK | 双击 |
Laya.Event.MOUSE_DOWN | 鼠标/触摸按下 |
Laya.Event.MOUSE_UP | 鼠标/触摸抬起 |
Laya.Event.MOUSE_MOVE | 鼠标/触摸移动 |
Laya.Event.MOUSE_OVER | 鼠标悬浮进入 |
Laya.Event.MOUSE_OUT | 鼠标悬浮离开 |
Laya.Event.MOUSE_WHEEL | 鼠标滚轮 |
Laya.Event.RIGHT_CLICK | 右键点击 |
Laya.Event.RIGHT_MOUSE_DOWN | 右键按下 |
触摸事件
| 常量 | 说明 |
|---|---|
Laya.Event.TOUCH_BEGIN | 触摸开始 |
Laya.Event.TOUCH_MOVE | 触摸移动 |
Laya.Event.TOUCH_END | 触摸结束 |
Laya.Event.TOUCH_CANCEL | 触摸取消 |
系统/生命周期事件
| 常量 | 说明 |
|---|---|
Laya.Event.CHANGE | 值改变(Slider/Input 等) |
Laya.Event.COMPLETE | 加载/动画完成 |
Laya.Event.PROGRESS | 加载进度 |
Laya.Event.ERROR | 错误 |
Laya.Event.RESIZE | 舞台尺寸改变 |
Laya.Event.BLUR | 失去焦点 |
Laya.Event.FOCUS | 获得焦点 |
Laya.Event.KEY_DOWN | 键盘按下 |
Laya.Event.KEY_UP | 键盘抬起 |
Laya.Event.KEY_PRESS | 键盘输入 |
---
3. 触摸/鼠标事件详解
// Event 对象属性
node.on(Laya.Event.MOUSE_DOWN, this, (evt: Laya.Event) => {
evt.stageX; // 舞台 X 坐标
evt.stageY; // 舞台 Y 坐标
evt.target; // 事件原始目标节点
evt.currentTarget; // 当前处理节点
evt.touchId; // 触摸 ID(多点触控)
evt.stopPropagation(); // 阻止冒泡
});
// 多点触摸
Laya.stage.on(Laya.Event.TOUCH_BEGIN, this, (evt: Laya.Event) => {
const touches = evt.touches;
for (const touch of touches) {
console.log(touch.stageX, touch.stageY);
}
});---
4. 自定义事件
// 方式一:直接使用 event + on
@regClass()
export class ClickHandler extends Laya.Script {
public onAwake(): void {
this.owner.on("gameOver", this, (score: number) => {
console.log("Game Over! Score:", score);
});
}
public onMouseClick(evt: Laya.Event): void {
this.owner.event("gameOver", 100);
}
}
// 方式二:全局事件总线(通过 Stage 或自定义 EventDispatcher)
const EventBus = new Laya.EventDispatcher();
// 发送
EventBus.event("levelComplete", { level: 5, score: 1000 });
// 监听
EventBus.on("levelComplete", this, (data: { level: number; score: number }) => {
console.log(`完成关卡 ${data.level},得分 ${data.score}`);
});
// 注销
EventBus.off("levelComplete", this);---
5. Handler(回调封装)
// 创建一次性 Handler(执行后自动回收,默认行为)
Laya.Handler.create(caller, callback, args);
// 创建持久 Handler(不自动回收,需手动 recover)
Laya.Handler.create(caller, callback, args, false);3.x 注意:资源加载推荐使用 Promise(.then()),Handler 仍可用但非首选。---
6. 脚本内置事件方法 vs 手动注册
LayaAir 3.x Script 提供内置事件方法,无需手动调用 on/off:
@regClass()
export class GameScript extends Laya.Script {
// ✅ 内置方法:自动绑定到 owner 节点
public onMouseClick(evt: Laya.Event): void {
console.log("被点击了");
}
public onKeyDown(evt: Laya.Event): void {
if (evt.keyCode === Laya.Keyboard.SPACE) {
this.jump();
}
}
}
// ❌ 不需要手动注册
// this.owner.on(Laya.Event.CLICK, this, this.onMouseClick); // 不需要何时用手动注册:监听非 owner 节点的事件、全局事件(Stage)、自定义事件总线。
---
7. 事件最佳实践
✅ 推荐做法
// 1. on/off 配对,在 onEnable/onDisable 中做
public onEnable(): void {
Laya.stage.on("scoreChanged", this, this.onScoreChanged);
}
public onDisable(): void {
Laya.stage.off("scoreChanged", this, this.onScoreChanged);
}
// 2. 销毁时使用 offAllCaller 兆底
public onDestroy(): void {
Laya.stage.offAllCaller(this);
}
// 3. 一次性事件用 once
node.once(Laya.Event.COMPLETE, this, () => { });❌ 常见错误
// 1. 忘记注销事件 → 内存泄漏
public onEnable(): void {
node.on(Laya.Event.CLICK, this, this.onClick);
}
// 缺少 onDisable 中的 off!
// 2. 在 onUpdate 中注册事件 → 重复注册
public onUpdate(): void {
this.owner.on(Laya.Event.CLICK, this, this.onClick); // ❌ 每帧注册一次!
}
// 3. 箭头函数无法正确 off
public onEnable(): void {
node.on("evt", this, () => { }); // ❌ 匿名函数无法注销
}物理系统 — LayaAir 3.x
📖 LayaAir 3.x 支持 2D 物理(Box2D 2.4.1)和 3D 物理(Bullet/PhysX),均可在脚本中通过碰撞回调处理。
---
1. 2D 物理系统(Box2D)
核心组件
| 组件 | 说明 |
|---|---|
Laya.RigidBody | 2D 刚体 |
Laya.BoxCollider | 矩形碰撞体 |
Laya.CircleCollider | 圆形碰撞体 |
Laya.PolygonCollider | 多边形碰撞体 |
Laya.ChainCollider | 链式碰撞体 |
刚体类型
| 类型 | 说明 |
|---|---|
dynamic | 动态刚体(受力、重力影响) |
kinematic | 运动学刚体(手动控制移动,不受力) |
static | 静态刚体(不移动,如地面) |
刚体属性
// RigidBody 关键属性
rigidBody.type = "dynamic";
rigidBody.gravityScale = 1.0; // 重力缩放
rigidBody.angularVelocity = 0; // 角速度
rigidBody.angularDamping = 0.1; // 角阻尼
rigidBody.linearVelocity = { x: 0, y: 0 }; // 线速度
rigidBody.linearDamping = 0.1; // 线性阻尼
rigidBody.bullet = false; // 高速物体开启 CCD
rigidBody.allowSleep = true; // 允许休眠
rigidBody.fixedRotation = false; // 固定旋转碰撞体属性
collider.friction = 0.2; // 摩擦系数(0~1)
collider.restitution = 0.5; // 弹性恢复(0=无弹力, 1=完全弹力)
collider.density = 1.0; // 密度
collider.isSensor = false; // true=传感器(只触发事件不产生碰撞)2D 关节
| 关节 | 说明 |
|---|---|
DistanceJoint | 距离关节 |
RevoluteJoint | 旋转关节(铰链) |
PrismaticJoint | 移动关节 |
PulleyJoint | 滑轮关节 |
MotorJoint | 马达关节 |
GearJoint | 齿轮关节 |
MouseJoint | 鼠标关节 |
WeldJoint | 焊接关节 |
WheelJoint | 车轮关节 |
RopeJoint | 绳索关节 |
2D 物理碰撞回调
const { regClass } = Laya;
@regClass()
export class Physics2DScript extends Laya.Script {
// Trigger(传感器模式,isSensor=true)
onTriggerEnter(other: any, self?: any, contact?: any): void {
console.log("触发器进入:", other);
}
onTriggerStay(other: any, self?: any, contact?: any): void { }
onTriggerExit(other: any, self?: any, contact?: any): void {
console.log("触发器离开");
}
// Collision(碰撞模式,isSensor=false)
onCollisionEnter(other: any, self?: any, contact?: any): void {
console.log("碰撞开始");
}
onCollisionStay(other: any, self?: any, contact?: any): void { }
onCollisionExit(other: any, self?: any, contact?: any): void {
console.log("碰撞结束");
}
}2D 物理分组过滤
// category:自身所属分组(二进制位掩码),如 0x0001、0x0002
// mask:可以碰的分组掩码,如 0x0001 | 0x0004
// 玩家
rigidBody_player.category = 0x0001;
rigidBody_player.mask = 0x0002 | 0x0004; // 碰敌人和子弹
// 敌人
rigidBody_enemy.category = 0x0002;
rigidBody_enemy.mask = 0x0001 | 0x0004; // 碰玩家和子弹---
2. 3D 物理系统(Bullet / PhysX)
核心组件
| 组件 | 说明 |
|---|---|
Laya.Rigidbody3D | 3D 刚体 |
Laya.PhysicsCollider | 静态碰撞器 |
Laya.CharacterController | 角色控制器 |
碰撞形状
| 形状 | 说明 |
|---|---|
BoxColliderShape | 盒体 |
SphereColliderShape | 球体 |
CapsuleColliderShape | 胶囊体 |
ConeColliderShape | 锥体 |
CylinderColliderShape | 圆柱体 |
MeshColliderShape | 网格(精确但性能低) |
CompoundColliderShape | 复合形状 |
StaticPlaneColliderShape | 无限平面 |
3D 刚体属性
rigidbody3D.isKinematic = false; // 运动学模式
rigidbody3D.mass = 1.0; // 质量
rigidbody3D.gravity = new Laya.Vector3(0, -9.81, 0);
rigidbody3D.linearVelocity = new Laya.Vector3(0, 0, 0);
rigidbody3D.linearDamping = 0.0;
rigidbody3D.linearFactor = new Laya.Vector3(1, 1, 1); // 约束轴
rigidbody3D.angularVelocity = new Laya.Vector3(0, 0, 0);
rigidbody3D.angularDamping = 0.0;
rigidbody3D.angularFactor = new Laya.Vector3(1, 1, 1);3D 物理射线
@regClass()
export class RaycastScript extends Laya.Script {
@property({ type: Laya.Camera })
public camera: Laya.Camera;
onMouseClick(evt: Laya.Event): void {
let point = new Laya.Vector2(evt.stageX, evt.stageY);
let ray = new Laya.Ray(new Laya.Vector3(), new Laya.Vector3());
this.camera.viewportPointToRay(point, ray);
let outs: Laya.HitResult[] = [];
let scene = this.owner.scene as Laya.Scene3D;
if (scene.physicsSimulation.rayCastAll(ray, outs)) {
for (let hit of outs) {
console.log("命中:", hit.collider.owner.name);
console.log("点:", hit.point);
console.log("法线:", hit.normal);
}
}
}
}3D 碰撞回调
@regClass()
export class Physics3DScript extends Laya.Script {
onTriggerEnter(other: any, self?: any, contact?: any): void {
console.log("3D 触发器进入:", other.owner.name);
}
onTriggerExit(other: any, self?: any, contact?: any): void {
console.log("3D 触发器离开");
}
onCollisionEnter(other: any, self?: any, contact?: any): void {
console.log("3D 碰撞开始:", other.owner.name);
}
onCollisionExit(other: any, self?: any, contact?: any): void {
console.log("3D 碰撞结束");
}
}3D 约束
| 约束 | 说明 |
|---|---|
FixedConstraint | 固定约束(焊接) |
HingeConstraint | 铰链约束(门、轮) |
SpringConstraint | 弹簧约束 |
ConfigurableConstraint | 可配置约束(可模拟任意约束) |
角色控制器
@regClass()
export class PlayerController extends Laya.Script {
private _character: Laya.CharacterController;
onAwake(): void {
this._character = this.owner.getComponent(Laya.CharacterController);
}
onUpdate(): void {
let moveDir = new Laya.Vector3(0, 0, 0);
// 根据输入计算移动方向
this._character.move(moveDir);
}
}资源管理 — LayaAir 3.x
📖 LayaAir 3.x 资源加载基于 Promise 异步模式,替代 2.x 的 Handler 回调模式。
---
1. 基础加载 API
单资源加载
// Promise 风格(推荐)
Laya.loader.load("resources/image.png").then((res: Laya.Texture) => {
let img = new Laya.Image();
img.texture = res;
this.owner.addChild(img);
});
// async/await 风格
public async onStart(): Promise<void> {
const tex = await Laya.loader.load("resources/image.png");
let sp = new Laya.Sprite();
sp.texture = tex;
this.owner.addChild(sp);
}带类型加载
Laya.loader.load(url, Laya.Loader.IMAGE).then((res) => { });
Laya.loader.load(url, Laya.Loader.JSON).then((json) => { });
Laya.loader.load(url, Laya.Loader.ATLAS).then(() => { });多资源加载
Laya.loader.load(["a.png", "b.json"]).then((results: any[]) => {
// results 数组顺序对应传入的 url 数组
});
// 混合类型
Laya.loader.load([
"image.jpg",
{ url: "config.json", type: Laya.Loader.JSON },
{ url: "ui.atlas", type: Laya.Loader.ATLAS },
]).then((results) => { });fetch(不解析不缓存)
// 获取原始数据,不走引擎缓存
Laya.loader.fetch("data.json", "json").then((json: any) => {
console.log(json);
});
Laya.loader.fetch("bin.dat", "arraybuffer").then((buf: ArrayBuffer) => { });---
2. 常用资源类型
| 常量 | 类型 | 说明 |
|---|---|---|
Laya.Loader.IMAGE | image | 图片/纹理 |
Laya.Loader.JSON | json | JSON 数据 |
Laya.Loader.ATLAS | atlas | 图集文件 |
Laya.Loader.HIERARCHY | hierarchy | 场景/预制体(.ls/.lh) |
Laya.Loader.MATERIAL | material | 材质文件 |
Laya.Loader.MESH | mesh | 网格模型 |
Laya.Loader.TEXTURE2D | texture2d | 2D 纹理 |
Laya.Loader.TEXTURECUBE | texturecube | 立方体纹理 |
Laya.Loader.SPINE | spine | Spine 骨骼动画 |
Laya.Loader.FONT | font | 字体 |
Laya.Loader.SOUND | sound | 音频 |
---
3. 获取缓存资源
// load 后从缓存获取
Laya.loader.load("res/img.png").then(() => {
let tex = Laya.loader.getRes("res/img.png") as Laya.Texture;
});
// 检查资源是否已加载
if (Laya.loader.getRes("res.png")) {
// 已加载,直接使用
}---
4. 资源释放
// 释放单个资源
Laya.loader.clearRes("res/img.png");
// 清除纹理资源
Laya.loader.clearTextureRes("res/img.png");
// 场景级释放
Laya.Scene.destroy("scene.ls"); // 销毁场景并释放关联资源
Laya.Scene.gc(); // GC 未被引用的资源资源释放最佳实践
@regClass()
export class SceneScript extends Laya.Script {
private _loadedUrls: string[] = [];
public async onStart(): Promise<void> {
const urls = ["a.png", "b.json", "c.atlas"];
this._loadedUrls = urls;
await Laya.loader.load(urls);
}
public onDestroy(): void {
// 场景销毁时释放资源
for (const url of this._loadedUrls) {
Laya.loader.clearRes(url);
}
this._loadedUrls.length = 0;
}
}---
5. 预制体加载与实例化
@regClass()
export class SpawnerScript extends Laya.Script {
@property({ type: Laya.Prefab })
public bulletPrefab: Laya.Prefab;
// 方式一:通过 IDE 绑定 Prefab(推荐)
public fire(): void {
if (this.bulletPrefab) {
let bullet = this.bulletPrefab.create() as Laya.Sprite3D;
this.owner.addChild(bullet);
}
}
// 方式二:代码加载
public async loadAndSpawn(): Promise<void> {
const prefab = await Laya.loader.load("prefab/Bullet.lh", Laya.Loader.HIERARCHY);
let bullet = prefab.create() as Laya.Sprite3D;
this.owner.addChild(bullet);
}
}---
6. 加载进度监听
private async loadWithProgress(): Promise<void> {
const urls = ["a.png", "b.atlas", "c.json"];
// 使用 Laya.loader.load 的第三个参数监听进度
await Laya.loader.load(urls, null,
Laya.Handler.create(this, (progress: number) => {
console.log(`加载进度: ${Math.floor(progress * 100)}%`);
}, null, false)
);
}---
7. 与 2.x 的对比
| 特性 | 3.x | 2.x |
|---|---|---|
| 返回值 | Promise | Handler 回调 |
| async/await | ✅ 支持 | ❌ 需要回调 |
| fetch | Laya.loader.fetch() | 无 |
| 场景格式 | .ls / .lh | .scene |
| 加载 API | Laya.loader.load() | Laya.loader.load() |
| 类型常量 | Laya.Loader.IMAGE 等 | Laya.Loader.IMAGE 等 |
场景与对象池 — LayaAir 3.x
📖 LayaAir 3.x 场景管理使用.ls文件格式(统一包含 Scene2D + Scene3D),对象池通过Laya.Pool管理。
---
1. 场景管理
场景文件格式
- 3.x:
.ls(场景文件,包含 2D 和 3D 内容)、.lh(预制体文件) - 2.x:
.scene(已废弃)
打开场景
// 打开场景
Laya.Scene.open("path/Scene.ls");
// 带参数打开
Laya.Scene.open("path/GameScene.ls", false, { level: 5, score: 1000 });
// 第二个参数 closeOther:是否关闭其他场景
Laya.Scene.open("path/GameScene.ls", true); // 关闭其他场景后打开接收场景参数
const { regClass } = Laya;
@regClass()
export class GameScene extends Laya.Script {
// 在场景运行时脚本中接收参数
onOpened(param: any): void {
if (param) {
console.log(`关卡: ${param.level}, 分数: ${param.score}`);
}
}
}关闭场景
// 按路径关闭
Laya.Scene.close("path/Scene.ls");
// 关闭当前场景(在场景的 Runtime 脚本中)
this.close();
// 关闭所有场景
Laya.Scene.closeAll();销毁与 GC
// 销毁场景并释放关联资源
Laya.Scene.destroy("scene.ls");
// GC 未使用的资源
Laya.Scene.gc();Loading 页面
// 设置 Loading 页面
const loadingSprite = new Laya.Sprite();
// ... 构建 loading UI
Laya.Scene.setLoadingPage(loadingSprite);
// 显示/隐藏
Laya.Scene.showLoadingPage();
Laya.Scene.hideLoadingPage();---
2. 对象池(Laya.Pool)
基础 API
// 按类名获取(推荐)—— 无则自动实例化
let bullet = Laya.Pool.getItemByClass("bullet", Bullet);
// 按工厂函数获取 —— 无则通过工厂创建
let enemy = Laya.Pool.getItemByCreateFun("enemy", () => {
let sp = new Laya.Sprite();
sp.loadImage("res/enemy.png");
return sp;
});
// 直接从池中获取(无则返回 null)
let item = Laya.Pool.getItem("bullet");
// 获取对象池数组
let pool = Laya.Pool.getPoolBySign("bullet");
console.log("池中数量:", pool.length);回收
// 按标识回收
Laya.Pool.recover("bullet", bullet);
// 按类回收
Laya.Pool.recoverByClass(instance);清理
// 清理指定池
Laya.Pool.clearBySign("bullet");---
3. 对象池 + 组件生命周期最佳实践
const { regClass, property } = Laya;
@regClass()
export class BulletScript extends Laya.Script {
@property({ type: Number })
public speed: number = 10;
private static readonly POOL_KEY = "bullet";
onEnable(): void {
// ✅ 每次从对象池取出时都会触发
// 在此重置状态
this.speed = 10;
}
onUpdate(): void {
(this.owner as Laya.Sprite).y -= this.speed;
// 超出屏幕回收
if ((this.owner as Laya.Sprite).y < -50) {
this.recycle();
}
}
onDisable(): void {
// 从舞台移除时触发(回收到池之前)
}
/** 实现 onReset 则自动支持对象池回收 */
onReset(): void {
this.speed = 10;
(this.owner as Laya.Sprite).pos(0, 0);
(this.owner as Laya.Sprite).alpha = 1;
}
public recycle(): void {
this.owner.removeSelf();
Laya.Pool.recover(BulletScript.POOL_KEY, this.owner);
}
// 管理器调用
public static spawn(parent: Laya.Sprite, x: number, y: number): Laya.Sprite {
let bullet = Laya.Pool.getItemByClass(
BulletScript.POOL_KEY, Laya.Sprite
) as Laya.Sprite;
bullet.pos(x, y);
parent.addChild(bullet);
return bullet;
}
}关键生命周期与对象池的关系
| 生命周期 | 首次创建 | 从池取出 | 回收入池 | 销毁 |
|---|---|---|---|---|
onAwake | ✅ | ❌ | - | - |
onEnable | ✅ | ✅ | - | - |
onStart | ✅ | ❌ | - | - |
onDisable | - | - | ✅ | ✅ |
onReset | - | - | ✅ | - |
onDestroy | - | - | - | ✅ |
- `onEnable`:对象池取出后一定会执行 → 适合做状态重置
- `onAwake`:只执行一次 → 适合做一次性初始化
- `onReset`:回收时触发 → 实现此方法可自动支持对象池
---
4. 场景切换模式
叠加场景(多场景共存)
// UI 场景叠加在游戏场景上
Laya.Scene.open("scene/Game.ls"); // 游戏场景
Laya.Scene.open("scene/GameUI.ls", false); // UI 叠加(closeOther=false)替换场景
// 关闭当前再打开新场景
Laya.Scene.open("scene/Result.ls", true); // closeOther=true场景间通信
// 通过全局事件总线
const EventBus = new Laya.EventDispatcher();
// 场景 A 发送
EventBus.event("gameOver", { score: 1000 });
// 场景 B 接收
EventBus.on("gameOver", this, (data) => {
console.log(data.score);
});缓动与动画 — LayaAir 3.x
📖 LayaAir 3.3+ 全面重构了 Tween 系统,使用链式 API、支持 chain/parallel 组合。
---
1. Tween 缓动(3.3+ 新 API)
基础用法
// to:缓动到目标值
Laya.Tween.create(sprite).duration(1000).to("x", 500);
// 多属性
Laya.Tween.create(sprite).duration(1000).to("x", 500).to("y", 300).to("alpha", 0);
// from:从指定值缓动到当前值
Laya.Tween.create(sprite).duration(1000).from("x", -100);
// go:指定起始和结束值
Laya.Tween.create(sprite).duration(500).go("x", 0, 300);缓动函数
Laya.Tween.create(sprite).duration(1000)
.to("x", 600)
.ease(Laya.Ease.cubicOut);
// 常用缓动函数:
// Laya.Ease.linearNone - 线性
// Laya.Ease.sineIn/Out/InOut - 正弦
// Laya.Ease.cubicIn/Out/InOut - 三次方
// Laya.Ease.quartIn/Out/InOut - 四次方
// Laya.Ease.quintIn/Out/InOut - 五次方
// Laya.Ease.circIn/Out/InOut - 圆形
// Laya.Ease.bounceIn/Out/InOut - 弹跳
// Laya.Ease.backIn/Out/InOut - 回弹
// Laya.Ease.elasticIn/Out/InOut - 弹性
// Laya.Ease.expoIn/Out/InOut - 指数回调
let tween = Laya.Tween.create(sprite).duration(1000)
.to("x", 500)
.onStart((tweener) => { console.log("开始"); })
.onUpdate((tweener) => { console.log("更新中"); })
.then(this.onComplete, this); // 完成回调串行动画(chain)
// 先移动 X,完成后再移动 Y
Laya.Tween.create(sprite)
.duration(1000).to("x", 600)
.chain()
.duration(2000).to("y", 400);并行动画(parallel)
// X 和 Y 同时缓动,但时长不同
Laya.Tween.create(sprite)
.duration(1000).to("x", 600)
.parallel()
.duration(2000).to("y", 400);循环与延迟
// 延迟 500ms 后开始
Laya.Tween.create(sprite).delay(500).duration(1000).to("x", 500);
// 循环
Laya.Tween.create(sprite).duration(1000).to("x", 500)
.repeat(3); // 重复 3 次
// 无限循环 + 反转
Laya.Tween.create(sprite).duration(1000).to("x", 500)
.yoyo(true).repeat(-1);终止缓动
let tween = Laya.Tween.create(sprite).duration(1000).to("x", 500);
tween.kill(); // 立即停止,保持当前状态
tween.kill(true); // 立即停止,跳到最终状态特殊效果
// 震动效果
Laya.Tween.create(sprite).duration(1000)
.to("x", 0).interp(Laya.Tween.shake, 10); // 幅度 10
// 支持 Vector2/3/4、Color、字符串颜色等类型
Laya.Tween.create(material).duration(1000)
.to("albedoColor", new Laya.Color(1, 0, 0, 1));---
2. 兼容旧 API(3.3 前版本或过渡期)
// Tween.to — 从当前值到目标值
Laya.Tween.to(sprite, { x: 500, y: 300, alpha: 1 }, 1000,
Laya.Ease.sineOut,
Laya.Handler.create(this, this.onComplete));
// Tween.from — 从指定值到当前值
Laya.Tween.from(sprite, { x: 0, y: 0 }, 500,
Laya.Ease.backOut);
// 停止
Laya.Tween.clearAll(sprite); // 清除目标所有缓动
Laya.Tween.clear(tweenInstance); // 清除指定实例---
3. Timer 定时器
核心 API
// 延迟执行一次(毫秒)
Laya.timer.once(1000, this, () => {
console.log("1 秒后执行");
});
// 循环执行(毫秒)
Laya.timer.loop(1000, this, this.onTick);
// 帧延迟一次
Laya.timer.frameOnce(60, this, () => {
console.log("60 帧后执行");
});
// 每帧执行
Laya.timer.frameLoop(1, this, this.onFrameLoop);
// 当前帧延迟执行(渲染前触发,避免重复计算)
Laya.timer.callLater(this, this.refresh);暂停与恢复
Laya.timer.pause(); // 暂停所有定时器
Laya.timer.resume(); // 恢复所有定时器清理
// 清除指定定时器
Laya.timer.clear(this, this.onTick);
// 清除当前对象的所有定时器
Laya.timer.clearAll(this);
// 立即执行并删除
Laya.timer.runCallLater(this, this.refresh);
Laya.timer.runTimer(this, this.onTick);定时器最佳实践
@regClass()
export class GameScript extends Laya.Script {
onEnable(): void {
// 启动时注册定时器
Laya.timer.loop(1000, this, this.onSecondTick);
}
onDisable(): void {
// 移除时清理定时器
Laya.timer.clear(this, this.onSecondTick);
}
onDestroy(): void {
// 销毁时兜底清理
Laya.timer.clearAll(this);
}
private onSecondTick(): void {
// 每秒执行
}
}---
4. 帧率控制
// 固定帧率
Laya.stage.frameRate = Laya.Stage.FRAME_FAST; // 60 FPS
Laya.stage.frameRate = Laya.Stage.FRAME_SLOW; // 30 FPS
// 鼠标交互优化(推荐省电场景)
Laya.stage.frameRate = Laya.Stage.FRAME_MOUSE;
// 有鼠标活动 → 60 FPS,静止 2 秒 → 降为 30 FPS---
5. 新旧 Tween 对比
| 特性 | 3.3+ 新 API | 旧 API |
|---|---|---|
| 创建方式 | Laya.Tween.create(target) | Laya.Tween.to(target, props) |
| 链式调用 | ✅ .duration().to().ease() | ❌ 参数式 |
| 串行动画 | chain() | 需手动在回调中串联 |
| 并行动画 | parallel() | 需同时创建多个 Tween |
| 回调 | .then(), .onStart(), .onUpdate() | Handler.create() |
| 终止 | tween.kill() / tween.kill(true) | Tween.clear() / Tween.clearAll() |
| 类型支持 | Number, Vector, Color, 字符串颜色 | 仅 Number |
| 震动 | .interp(Laya.Tween.shake, amp) | 无内置 |
UI 系统 — LayaAir 3.x
📖 LayaAir 3.x UI 系统包含 17 个基础组件和 9 个容器组件,支持相对布局、数据绑定和弹窗管理。
---
1. UI 组件概览
基础组件(17 个)
| 组件 | 类名 | 说明 |
|---|---|---|
| Image | Laya.Image | 图片显示(支持九宫格) |
| Label | Laya.Label | 文本标签 |
| TextInput | Laya.TextInput | 单行输入框 |
| TextArea | Laya.TextArea | 多行输入框 |
| Button | Laya.Button | 按钮(支持多态皮肤) |
| CheckBox | Laya.CheckBox | 复选框 |
| Radio | Laya.Radio | 单选框 |
| ComboBox | Laya.ComboBox | 下拉选项框 |
| Clip | Laya.Clip | 位图切片 |
| FontClip | Laya.FontClip | 字体切片 |
| ProgressBar | Laya.ProgressBar | 进度条 |
| HSlider | Laya.HSlider | 水平滑动条 |
| VSlider | Laya.VSlider | 垂直滑动条 |
| HScrollBar | Laya.HScrollBar | 水平滚动条 |
| VScrollBar | Laya.VScrollBar | 垂直滚动条 |
| ColorPicker | Laya.ColorPicker | 取色器 |
| Tab | Laya.Tab | 导航标签组 |
容器组件(9 个)
| 组件 | 类名 | 说明 |
|---|---|---|
| Box | Laya.Box | 基础容器 |
| HBox | Laya.HBox | 水平布局容器 |
| VBox | Laya.VBox | 垂直布局容器 |
| Panel | Laya.Panel | 面板容器(可滚动) |
| List | Laya.List | 列表组件 |
| Tree | Laya.Tree | 树状列表 |
| RadioGroup | Laya.RadioGroup | 单选框组 |
| ViewStack | Laya.ViewStack | 导航容器 |
| Dialog | Laya.Dialog | 弹窗视图 |
---
2. 常用 UI 组件代码示例
Button(按钮)
@regClass()
export class UIScript extends Laya.Script {
@property({ type: Laya.Button })
public btnStart: Laya.Button;
onEnable(): void {
this.btnStart.on(Laya.Event.CLICK, this, this.onBtnStart);
}
onDisable(): void {
this.btnStart.off(Laya.Event.CLICK, this, this.onBtnStart);
}
private onBtnStart(): void {
console.log("开始按钮被点击");
}
}Label(文本标签)
@property({ type: Laya.Label })
public lblScore: Laya.Label;
updateScore(score: number): void {
this.lblScore.text = `分数: ${score}`;
this.lblScore.color = "#FFFF00";
this.lblScore.fontSize = 30;
}Image(图片)
@property({ type: Laya.Image })
public imgBg: Laya.Image;
onStart(): void {
this.imgBg.skin = "res/bg.png";
this.imgBg.sizeGrid = "30,30,30,30"; // 九宫格拉伸
}ProgressBar(进度条)
@property({ type: Laya.ProgressBar })
public hpBar: Laya.ProgressBar;
updateHP(current: number, max: number): void {
this.hpBar.value = current / max; // 0~1
}List(列表)
@property({ type: Laya.List })
public itemList: Laya.List;
onStart(): void {
// 设置列表数据
this.itemList.array = [
{ name: "物品1", count: 5 },
{ name: "物品2", count: 10 },
];
// 渲染回调
this.itemList.renderHandler = Laya.Handler.create(this, this.onRenderItem, null, false);
// 选择回调
this.itemList.selectHandler = Laya.Handler.create(this, this.onSelectItem, null, false);
}
private onRenderItem(cell: Laya.Box, index: number): void {
let data = this.itemList.array[index];
let label = cell.getChildByName("name") as Laya.Label;
label.text = data.name;
}
private onSelectItem(index: number): void {
console.log("选中:", index);
}---
3. 相对布局
// 相对于父容器的布局属性
ui.left = 0; // 距左边距
ui.right = 0; // 距右边距
ui.top = 0; // 距上边距
ui.bottom = 0; // 距下边距
ui.centerX = 0; // 水平居中偏移
ui.centerY = 0; // 垂直居中偏移
// 示例:底部居中按钮
btn.centerX = 0;
btn.bottom = 50;
// 全屏背景
bg.left = 0;
bg.right = 0;
bg.top = 0;
bg.bottom = 0;---
4. Dialog 弹窗
const { regClass, property } = Laya;
@regClass()
export class GameDialog extends Laya.Script {
@property({ type: Laya.Button })
public btnClose: Laya.Button;
@property({ type: Laya.Button })
public btnConfirm: Laya.Button;
onEnable(): void {
this.btnClose.on(Laya.Event.CLICK, this, this.onClose);
this.btnConfirm.on(Laya.Event.CLICK, this, this.onConfirm);
}
onDisable(): void {
this.btnClose.off(Laya.Event.CLICK, this, this.onClose);
this.btnConfirm.off(Laya.Event.CLICK, this, this.onConfirm);
}
private onClose(): void {
// 关闭弹窗
(this.owner as Laya.Dialog).close();
}
private onConfirm(): void {
// 确认操作
this.owner.event("confirm");
(this.owner as Laya.Dialog).close();
}
}Dialog 管理
// 打开弹窗
Laya.Dialog.open("dialog/Settings.ls");
// 关闭所有弹窗
Laya.Dialog.closeAll();
// 弹窗遮罩
// 在 IDE 中设置 Dialog 的 isModal 属性---
5. 灰化与禁用
// 禁用(变灰 + 不可交互)
btn.disabled = true;
// 仅灰化(视觉效果)
btn.gray = true;
// 恢复
btn.disabled = false;
btn.gray = false;---
6. dataSource 数据绑定
// UI 组件支持 dataSource 进行数据绑定
let item = new Laya.Box();
item.dataSource = {
name: { text: "物品名称" },
count: { text: "99" },
icon: { skin: "res/icon.png" }
};
// 子节点 name 匹配的组件会自动绑定对应属性性能优化 — LayaAir 3.x
LayaAir 3.x 性能优化核心:减少 DrawCall、对象池复用、CacheAs 缓存、屏幕适配。
---
1. CacheAs 缓存渲染
// 静态容器(不动、子节点不变)→ 减少 DrawCall
panel.cacheAs = "bitmap";
// CacheAs 模式选择
// "bitmap" → 完全静态,烘焙为位图纹理(最优)
// "normal" → 合并子节点渲染,但不生成纹理
// "none" → 关闭缓存(默认)
// 子节点有动画 → 不要用 bitmap(每帧重建反而更慢)
// 低频更新内容 → 适合 bitmap✅ CacheAs 最佳实践
@regClass()
export class UIScript extends Laya.Script {
@property({ type: Laya.Sprite })
public staticBg: Laya.Sprite;
onStart(): void {
// 静态背景开启缓存
this.staticBg.cacheAs = "bitmap";
}
// 需要更新时先关闭再重开
refreshPanel(): void {
this.staticBg.cacheAs = "none";
// 修改子节点...
this.staticBg.cacheAs = "bitmap";
}
}---
2. DrawCall 优化
drawCallOptimize(3.3+ 动态合批)
// 3.3+ 新增:动态合批,自动优化 DrawCall
sp.drawCallOptimize = true;
// 测试数据:65 DC → 5 DC图集合并
- 同一图集内的 Sprite 渲染只产生 1 次 DrawCall
- 避免混用多个图集与纯色 Sprite
- 使用 IDE 的自动图集配置
节点可见性
// ✅ visible=false 不参与渲染管线(推荐隐藏)
node.visible = false;
// ⚠️ alpha=0 仍参与渲染管线
node.alpha = 0; // 不推荐用于隐藏
// ✅ 视口裁剪:超出屏幕设置 visible=false
if (sp.x + sp.width < 0 || sp.x > Laya.stage.width) {
sp.visible = false;
}鼠标检测优化
// 不需要交互的节点关闭鼠标检测
sp.mouseEnabled = false;
// 穿透父节点直接检测子节点
parent.mouseThrough = true;---
3. 对象池复用
// ✅ 使用 Laya.Pool 复用高频创建/销毁的对象
const bullet = Laya.Pool.getItemByClass("bullet", Bullet);
parent.addChild(bullet);
// 回收而非销毁
function recycleBullet(b: Laya.Sprite): void {
b.removeSelf();
Laya.Pool.recover("bullet", b);
}
// ❌ 频繁 new + destroy
function fire(): void {
let b = new Bullet(); // ❌ 每次 new
parent.addChild(b);
}
function remove(b: Bullet): void {
b.destroy(true); // ❌ 每次 destroy
}---
4. onUpdate 性能优化
@regClass()
export class OptimizedScript extends Laya.Script {
// ✅ 预缓存引用
private _sp: Laya.Sprite;
private _transform: Laya.Transform3D;
onAwake(): void {
this._sp = this.owner as Laya.Sprite;
this._transform = (this.owner as Laya.Sprite3D).transform;
}
onUpdate(): void {
// ✅ 使用缓存引用
this._sp.x += 1;
// ❌ 每帧查找
// (this.owner as Laya.Sprite).getChildByName("hero").x += 1;
// ❌ 每帧创建对象
// let pos = new Laya.Vector3(0, 0, 0); // 每帧 GC 压力
}
}减少 GC 压力
@regClass()
export class NoGCScript extends Laya.Script {
// ✅ 预分配复用对象
private _tempVec3: Laya.Vector3 = new Laya.Vector3();
private _tempVec2: Laya.Vector2 = new Laya.Vector2();
onUpdate(): void {
// 复用临时向量而非每帧 new
this._tempVec3.x = 0;
this._tempVec3.y = 1;
this._tempVec3.z = 0;
// 使用 this._tempVec3 ...
}
}---
5. 内存管理
// 1. 及时销毁不用的节点
node.destroy(true);
// 2. 释放资源
Laya.loader.clearRes("res/img.png");
// 3. 场景级 GC
Laya.Scene.gc();
// 4. 定时器/事件清理
onDestroy(): void {
Laya.timer.clearAll(this);
Laya.stage.offAllCaller(this);
}---
6. 屏幕适配
适配模式
| 模式 | 适用场景 |
|---|---|
noscale | 默认,不缩放 |
full | 画布取物理分辨率,3D 游戏推荐 |
fixedwidth | 移动端推荐,保宽、高度自适应 |
fixedheight | 保高、宽度自适应 |
fixedauto | 自动选择保宽/保高 |
showall | PC 推荐,设计尺寸全显示 |
// 移动端推荐配置
Laya.stage.scaleMode = "fixedwidth";
Laya.stage.designWidth = 1080;
Laya.stage.designHeight = 1920;
// PC/Web 推荐
Laya.stage.scaleMode = "showall";
Laya.stage.designWidth = 1920;
Laya.stage.designHeight = 1080;
// 3D 游戏(需要精确分辨率)
Laya.stage.scaleMode = "full";关键属性
Laya.Browser.pixelRatio; // 设备像素比 DPR
Laya.Browser.clientWidth; // 逻辑宽度
Laya.Browser.clientHeight; // 逻辑高度
Laya.Browser.width; // 物理宽度
Laya.Browser.height; // 物理高度
Laya.stage.designWidth; // 设计宽度
Laya.stage.designHeight; // 设计高度---
7. 帧率策略
// 固定高帧率
Laya.stage.frameRate = Laya.Stage.FRAME_FAST; // 60 FPS
// 固定低帧率(省电)
Laya.stage.frameRate = Laya.Stage.FRAME_SLOW; // 30 FPS
// 智能帧率(推荐省电场景)
Laya.stage.frameRate = Laya.Stage.FRAME_MOUSE;
// 有交互 → 60 FPS,静止 2s → 30 FPS---
8. 动态图集
// 开启动态图集(自动将小图合并到大图集)
Laya.stage.dynamicAtlas = true;
// 适用于:大量动态加载的小图
// 不适用于:大图、纹理频繁变化---
9. 可试玩广告优化要点
| 优化项 | 措施 |
|---|---|
| 包体 <5MB | 压缩纹理、删除未用资源、代码剥离 |
| 首屏加载 <3s | 首屏精简资源、延迟加载 |
| DrawCall <30 | 图集合并、CacheAs、drawCallOptimize |
| 内存 <150MB | 对象池、及时释放、避免大纹理 |
| 帧率 ≥30 FPS | onUpdate 零分配、视口裁剪 |
质量与规范 — LayaAir 3.x
TypeScript 严格模式开发规范,适用于 LayaAir 3.x 项目。
---
1. TypeScript 严格模式
tsconfig.json 推荐配置
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}---
2. 装饰器规范
✅ 正确用法
const { regClass, property } = Laya;
@regClass()
export class GameScript extends Laya.Script {
// 所有 IDE 可见属性必须使用 @property
@property({ type: Number, tips: "移动速度" })
public speed: number = 5;
@property({ type: Laya.Sprite3D })
public target: Laya.Sprite3D;
// 非序列化属性:private + 下划线前缀
private _isActive: boolean = false;
private _timer: number = 0;
}❌ 常见错误
// 1. 忘记 @regClass → IDE 无法识别
export class BadScript extends Laya.Script { } // ❌
// 2. @property 类型不匹配
@property({ type: String })
public count: number = 0; // ❌ 声明 String 但实际是 number
// 3. 没有 export
@regClass()
class NotExported extends Laya.Script { } // ❌ 必须 export---
3. 访问修饰符
@regClass()
export class PlayerScript extends Laya.Script {
// public: IDE 暴露属性 + 外部可访问
@property({ type: Number })
public speed: number = 5;
// private: 内部状态
private _hp: number = 100;
// protected: 子类可访问
protected _isAlive: boolean = true;
// readonly: 常量
private static readonly MAX_HP: number = 100;
private static readonly POOL_KEY: string = "player";
}---
4. 事件注册/注销配对
✅ 标准模式
@regClass()
export class SafeScript extends Laya.Script {
public onEnable(): void {
this.owner.on(Laya.Event.CLICK, this, this.onClick);
Laya.stage.on("gameEvent", this, this.onGameEvent);
}
public onDisable(): void {
this.owner.off(Laya.Event.CLICK, this, this.onClick);
Laya.stage.off("gameEvent", this, this.onGameEvent);
}
public onDestroy(): void {
// 兜底清理
Laya.stage.offAllCaller(this);
Laya.timer.clearAll(this);
}
}---
5. 异常处理
// ✅ 资源加载异常处理
public async onStart(): Promise<void> {
try {
const res = await Laya.loader.load("resources/config.json");
if (!res) {
console.error("资源加载失败: config.json");
return;
}
this.initWithConfig(res);
} catch (e) {
console.error("加载异常:", e);
}
}
// ✅ 空引用保护
public onUpdate(): void {
if (!this.target || !this.target.activeInHierarchy) return;
// 安全使用 target
}---
6. 命名规范
| 类型 | 规范 | 示例 |
|---|---|---|
| 类名 | PascalCase | PlayerScript, GameManager |
| 公开属性 | camelCase | speed, maxHP |
| 私有属性 | _前缀 + camelCase | _timer, _isAlive |
| 常量 | UPPER_SNAKE | MAX_HP, POOL_KEY |
| 方法 | camelCase | onStart(), fireBullet() |
| 事件名 | camelCase 字符串 | "gameOver", "scoreChanged" |
| 文件名 | PascalCase | PlayerScript.ts, GameManager.ts |
---
7. console.log 规范
// ❌ 生产代码中不要留 console.log
public onUpdate(): void {
console.log(this.owner.x); // ❌ 每帧打印,严重影响性能
}
// ✅ 使用条件编译或开关
private static readonly DEBUG = false;
private log(msg: string): void {
if (PlayerScript.DEBUG) {
console.log(`[Player] ${msg}`);
}
}---
8. 类型安全
// ✅ 明确类型转换
let sp = this.owner as Laya.Sprite;
let sp3d = this.owner as Laya.Sprite3D;
// ✅ 获取组件时指定类型
let comp = node.getComponent(MyScript);
// ✅ 事件回调参数类型
private onClick(evt: Laya.Event): void {
let target = evt.target as Laya.Sprite;
}
// ❌ 避免 any
private handleData(data: any): void { } // ❌
private handleData(data: { name: string; score: number }): void { } // ✅架构审查 — LayaAir 3.x
审查 LayaAir 3.x 项目架构时的检查清单。
---
1. 项目结构检查
✅ 推荐结构
src/
├── Main.ts # 入口组件脚本(@regClass)
├── config/ # 全局配置
├── manager/ # 单例管理器
├── scene/ # 场景运行时脚本
├── script/ # 通用 Script 组件
├── common/ # 公共工具
└── ui/ # UI 相关组件
assets/
├── Scene.ls # 场景文件
├── resources/ # 动态加载资源
├── atlas/ # 图集配置
└── prefab/ # 预制体检查项
- [ ] 入口文件使用
@regClass()装饰器 - [ ] 场景文件使用
.ls格式 - [ ] 资源文件放在
assets/resources/下以支持动态加载 - [ ] 预制体放在
assets/prefab/下 - [ ] 脚本按功能分类(manager/scene/script/ui)
---
2. 组件脚本检查
装饰器
- [ ] 所有 Script 子类都有
@regClass() - [ ] 所有 IDE 可见属性都有
@property() - [ ]
@property的 type 与 TypeScript 类型一致 - [ ] 类使用
export导出
生命周期
- [ ]
onAwake只做一次性初始化 - [ ]
onEnable+onDisable事件配对 - [ ]
onDestroy清理定时器和引用 - [ ] 对象池场景使用
onEnable重置状态(而非onAwake) - [ ]
onUpdate中无临时对象分配
---
3. 事件系统检查
- [ ] 所有
on()都有对应的off() - [ ] 事件注册在
onEnable,注销在onDisable - [ ]
onDestroy中用offAllCaller(this)兜底 - [ ] 不在
onUpdate中注册事件 - [ ] 不使用匿名函数注册事件(无法注销)
- [ ] 自定义事件使用有意义的字符串名称
---
4. 资源管理检查
- [ ] 使用 Promise/async-await 加载(而非 Handler)
- [ ] 加载有错误处理(try-catch 或 null 检查)
- [ ] 场景销毁时释放加载的资源
- [ ] 大资源使用加载进度提示
- [ ] 预制体通过
@property({ type: Laya.Prefab })绑定
---
5. 性能检查
- [ ] 静态 UI 容器开启
cacheAs = "bitmap" - [ ] 不需要交互的节点
mouseEnabled = false - [ ] 使用
visible = false隐藏节点(而非alpha = 0) - [ ] 高频对象使用对象池(子弹、特效、敌人)
- [ ]
onUpdate中预缓存引用,无每帧new操作 - [ ] 临时向量 / 矩阵预分配复用
- [ ] 定时器在不需要时及时清理
---
6. 单例管理器模式
const { regClass } = Laya;
@regClass()
export class GameManager extends Laya.Script {
private static _instance: GameManager;
public static get instance(): GameManager { return GameManager._instance; }
onAwake(): void {
if (GameManager._instance) {
this.owner.destroy();
return;
}
GameManager._instance = this;
}
onDestroy(): void {
if (GameManager._instance === this) {
GameManager._instance = null;
}
}
}---
7. 场景管理模式
- [ ] 使用
Laya.Scene.open()切换场景 - [ ] 场景参数通过
onOpened(param)接收 - [ ] UI 场景叠加使用
closeOther=false - [ ] 场景间通信通过全局事件总线
- [ ] 场景销毁时执行
Laya.Scene.gc()
质量审查 — LayaAir 3.x
审查 LayaAir 3.x 代码质量时的检查清单。
---
1. 代码审查清单
TypeScript 质量
- [ ] 启用
strict模式 - [ ] 无隐式
any类型 - [ ] 类型转换明确(
as Laya.Sprite) - [ ] 无未使用的变量/导入
- [ ] 访问修饰符正确(public/private/protected)
- [ ] 常量使用
static readonly
组件规范
- [ ]
@regClass()和export齐全 - [ ]
@property()类型匹配 - [ ] 生命周期方法使用正确
- [ ] 事件注册/注销配对
- [ ] 定时器清理完整
错误处理
- [ ] 资源加载有 try-catch
- [ ] 空引用有保护检查
- [ ] 无静默错误(至少 console.error)
- [ ] Promise 有 catch 处理
---
2. 常见代码问题
问题 1:事件泄漏
// ❌ 没有 off
onEnable(): void {
Laya.stage.on("event", this, this.handler);
}
// 缺少 onDisable 中的 off!
// ✅ 修复
onDisable(): void {
Laya.stage.off("event", this, this.handler);
}问题 2:onUpdate 内存泄漏
// ❌ 每帧创建对象
onUpdate(): void {
let pos = new Laya.Vector3(this.owner.x, this.owner.y, 0);
}
// ✅ 预分配
private _pos: Laya.Vector3 = new Laya.Vector3();
onUpdate(): void {
this._pos.setValue(this.owner.x, this.owner.y, 0);
}问题 3:忘记清理定时器
// ❌ 定时器未清理
onStart(): void {
Laya.timer.loop(1000, this, this.tick);
}
// ✅ 配对清理
onDestroy(): void {
Laya.timer.clearAll(this);
}问题 4:装饰器缺失
// ❌ 缺少 @regClass
export class BadScript extends Laya.Script {
public speed: number = 5; // IDE 无法识别
}
// ✅ 完整
@regClass()
export class GoodScript extends Laya.Script {
@property({ type: Number })
public speed: number = 5;
}问题 5:匿名函数事件
// ❌ 匿名函数无法注销
onEnable(): void {
this.owner.on(Laya.Event.CLICK, this, () => {
console.log("clicked");
});
}
// ✅ 命名函数
private onClick(): void {
console.log("clicked");
}
onEnable(): void {
this.owner.on(Laya.Event.CLICK, this, this.onClick);
}
onDisable(): void {
this.owner.off(Laya.Event.CLICK, this, this.onClick);
}问题 6:资源加载无错误处理
// ❌ 无错误处理
async onStart(): Promise<void> {
const res = await Laya.loader.load("config.json");
this.parse(res); // res 可能为 null
}
// ✅ 安全加载
async onStart(): Promise<void> {
try {
const res = await Laya.loader.load("config.json");
if (!res) {
console.error("加载失败: config.json");
return;
}
this.parse(res);
} catch (e) {
console.error("加载异常:", e);
}
}---
3. 审查优先级
| 优先级 | 检查项 |
|---|---|
| P0 - 必须修复 | 事件泄漏、内存泄漏、无错误处理、装饰器缺失 |
| P1 - 应该修复 | 类型不安全(any)、定时器未清理、性能问题 |
| P2 - 建议优化 | 命名不规范、缺少注释、结构不清晰 |
---
4. 自动检查脚本
在代码审查中重点搜索的模式:
# 搜索未配对的事件
.on( → 检查是否有对应的 .off(
.loop( → 检查是否有 .clear( 或 .clearAll(
.once( → 一般安全,但检查回调是否可能被多次注册
# 搜索潜在问题
new Laya.Vector → 检查是否在 onUpdate 中
console.log → 检查是否在生产代码中
as any → 避免类型擦除