# 组件化开发

# 组件化开发的思想

很多项目的 耦合性比较高,后期改造、维护难度比较大,如果项目 使用“组件化开发” 这种模式,可以对项目进行很大程度上的 解耦,也就降低了维护项目的难度和成本。“组件化开发”的 核心思想是尽可能将业务逻辑模块化,将逻辑都写在脚本里,然后将脚本和组件关联起来,使用组件时对应的脚本就会运行。

# “组件化开发”中的组件

3.组件库的介绍这篇里介绍过组件库,也提到过功能类组件,这些都是“组件化开发”中的组件,可以整体的分为这样几类:
组件分类
该图片地址:https://www.processon.com/mindmap/5f58fba0e401fd60bdeb855b (opens new window)

  • 节点类组件(Node):2D 基础组件(Animation\HTMLDivElement\Sprite\Text)、基础显示组件(Button\Image\Label 等)、容器组件(Box\List\Panel 等)、视图组件(Scene\View\Dialog);
  • 绘图类组件(Graphics):矢量图(Circle\Curves\Line 等)、纹理(Texture\FillTexture)和文本(FillText);
  • 功能类组件(Component):组件(脚本组件、物理组件等)、泛组件(滤镜组件,类似 Component)。

# 组件化开发的方式

组件有自己的 生命周期,在对应的生命周期函数里可以执行一些游戏逻辑,如果 将一个脚本继承自组件,然后在脚本里 重写组件的生命周期函数并写上相应的游戏逻辑 ,这样的开发方式叫做 runTime 脚本进行组件化开发

如果不使用 runtime 方式,可以 给组件挂载一个 script 脚本(不继承自组件,是挂载的方式),该脚本 也有自己的生命周期 并与组件的生命周期是 息息相关的,这种开发方式叫做 script 脚本进行组件化开发

两种方式的差异和应用会在最后一小节“两种方式的区别和应用场景”里讲述。

# script 脚本开发流程

# script 脚本挂载到组件上

其实在上一个章节就提到过怎么将 script 脚本添加到组件上了,这里梳理一下:

  • 准备好 目标组件 ,然后在项目的 src/script 目录下新建一个脚本,使用的是 ts 语言,那脚本就是 xxx.ts 之类的;
  • 将脚本拖拽到层级面板中的 目标组件节点下 或者直接拖拽到 目标组件的属性面板里,这样 script 脚本就挂载到组件上了;
  • 除了拖拽的方法外,还可以在目标组件的 属性面板 最下方点击 “添加组件”,选择“Code”再选择对应的脚本即可。

挂载script脚本到组件上

# 编写 script 脚本

比如在组件激活时(onEnable)将图片放大至两倍,最基本的脚本写法:

export default class scaleDemo extends Laya.Script {
  public img: Laya.Image;
  constructor() {
    super();
  }
  // UI组件的赋值最好在onEnable里编写,别在构造函数里写,因为组件可能还没有激活
  onEnable() {
    // script脚本化开发,获取本组件是通过this.owner来获取的
    this.img = this.owner as Laya.Image;
    this.img.scaleX = 2;
    this.img.scaleY = 2;
  }
  onDisable() {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# script 脚本属性在 IDE 里可视化

1)可以将 script 脚本中的属性在 IDE 中显示出来,示例如下

/** @prop {name:big,tips:"放大",type:int,default:2} */
public big: number = 2;
1
2

属性可视化

2)定义属性常用的字段解释:

  • name:属性在 IDE 中展示的名字;
  • tips:在 IDE 里将鼠标悬浮到属性上时,会展示一个 tips 提示;
  • default:属性在 IDE 中展示的默认值,在 script 脚本中也要给变量赋值这个默认值,否则可能会发生一些错误或者达不到效果;
  • type:属性在 IDE 里的数据类型,Number、Int(ts 对应 number)、String、Boolean(布尔类型,ts 对应 boolean)、Color(颜色,ts 对应 string)、Vec(数组,ts 对应 number[])、Vector(动态数组,ts 对应 any[])、Option(下拉框,ts 对应 string)、EditOption(可编辑下拉框但不能定义默认值,ts 对应 string)、Ease(缓动类型,ts 对应 string)、sNumber(数字滑动条,ts 对应 number)、Prefab(预制体,代码对应 Laya.Prefab)、Node(节点,代码对应 Laya.xxx)、Nodes(对节点,代码是 Laya.xxx 的数组形式) 等等;
  • accept 和 acceptTypes:当 type 为 string 时,可以使用 accept 和 acceptTypes 来具体约束这个 string;比如 accept 可以是 res、files,意思是属性的类型要是 资源路径、文件路径,acceptTypes 可以是 "png,jpg",意思是路径后缀必须是 png、jpg
  • option:当 type 为 Option 时,option 用来存放 下拉框所有选项
  • min 和 max:当 sNumber 时,min 和 max 就是 滑动条的上下限

PS:预制体在 script 脚本中使用,this.prefab.create()或者 this.prefab.json.props.texture

# 重写声明周期函数

Laya.Script 有自己的生命周期函数,script 脚本继承自 Laya.Script,重写对应的生命周期函数,并在其中编写控制组件的逻辑代码

export default class scaleDemo extends Laya.Script {
  /** @prop {name:big,tips:"放大",type:int,default:2} */
  public big: number = 2;
  /** @prop {name:small,tips:"缩小",type:number,default:0.5} */
  public small: number = 0.5;
  public img: Laya.Image;
  constructor() {
    super();
  }
  // UI组件的赋值最好在onEnable里编写,别在构造函数里写,因为组件可能还没有激活
  onEnable() {
    // script脚本化开发,获取本组件是通过this.owner来获取的
    this.img = this.owner as Laya.Image;
  }
  // 键盘按下
  onKeyDown() {
    this.img.scaleX = this.small;
    this.img.scaleY = this.small;
  }
  // 每帧更新
  onUpdate() {
    // 旋转
    this.img.rotation++;
  }
  // 鼠标按下
  onMouseDown() {
    // 按住拖住
    this.img.startDrag();
  }
  // 在舞台上点击
  onStageClick() {
    this.img.scaleX = this.big;
    this.img.scaleY = this.big;
  }
  onDisable() {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 了解整个生命周期

1)script 脚本的生命周期:
Script生命周期
script 脚本除了生命周期函数以外,还有一些鼠标点击、键盘和碰撞等事件,当然也不止这些事件,可以去laya.components.Script (opens new window)这个 API 里查看事件。

2)runtime 脚本的生命周期,也是节点 or 组件的生命周期:
节点及组件生命周期

  • 创建节点:会先创建节点的 script 脚本(执行 script 脚本的构造函数),再创建节点的 子节点(执行子节点的构造函数),这样就完成了一个节点的创建;
  • addToStage:先激活 script 脚本(执行 script 脚本的 onAwake 函数再执行 onEnable 函数),再激活 子节点(执行子节点的 onAwake 函数再执行 onEnable 函数),完成这两步之后才激活 节点本身(执行节点本身的 onAwake 函数再执行 onEnable 函数)
  • removeFromStage:先禁用 script 脚本(执行 script 脚本的 onDisable 函数),再禁用 子节点(执行子节点的 onDisable 函数),完成这两步之后才禁用 节点本身(执行节点本身的 onDisable 函数)
  • destroy:会先判断节点是否已经禁用了,没有禁用的先禁用(removeFromStage);然后会去销毁 script 脚本(执行 script 脚本的 onDestroy 函数),再去销毁 子节点(执行子节点的 onDestroy 函数),完成这两步之后才销毁 节点本身(执行节点本身的 onDestroy 函数)

也就是说 runtime 脚本的生命周期函数只有四个:onAwake(激活)、onEnable(启用)、onDisable(禁用)、onDestroy(销毁),而 script 脚本不止四个并且还带有鼠标、键盘、碰撞事件

# runtime 脚本开发流程

# 关联 runtime 脚本

直接将 runtime 脚本拖拽到组件的 runtime 属性里,或者直接在 runtime 属性里输入 runtime 脚本的路径。
关联runtime脚本

# 编写 runtime 脚本

编写 runtime 脚本需要注意的是

  • runtime 脚本生命周期函数只有四个:onAwake(激活)、onEnable(启用)、onDisable(禁用)、onDestroy(销毁)
  • 没有 script 脚本的 属性可视化,因为本来就是继承自组件,不需要可视化
export default class ListUI extends Laya.Dialog {
  constructor() {
    super();
  }
  onEnable(): void {
    //模拟数据结构
    let arr = [
      {
        top: { value: "1" },
        name: { text: "玩家名字1", fontSize: 30 },
        score: { text: "100" },
      },
      {
        top: { value: "2" },
        name: { text: "玩家名字2", fontSize: 30 },
        score: { text: "90" },
      },
      {
        top: { value: "3" },
        name: { text: "玩家名字3", fontSize: 30 },
        score: { text: "80" },
      },
    ];

    let _list = this.getChildByName("_list") as Laya.List;
    _list.array = arr;

    //刷新List
    _list.refresh();
  }
  onDisable(): void {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 发布模式对 runtime 脚本的影响

之前在2.IDE 的使用#项目设置里提到过“场景的发布模式”,这里简单做一个总结:

  • “内嵌模式” 生成了场景类并将 UI 数据内嵌到类里,使用方便但是代码包体积大;
  • “加载模式” 也生成场景类但将所有 UI 数据放到一个 UI.json 里(UI 数据从类里分离出来了),减少了代码包体积但需要提前加载大的 UI.json;
  • 小游戏问世前常使用“内嵌模式”和“加载模式”,后来常使用“分离模式”和“文件模式”。
  • “分离模式” 也生成场景类但每个场景的 UI 数据有单独的场景数据文件,并且引擎是自动加载场景对应的场景数据文件;
  • “文件模式” 没有生成场景类(所以不能使用 var 要使用 getchild 操作子节点),只有场景数据文件并通过 Scene.load 来加载使用,大大减少了代码包体积。
  • 建议是 将项目设置的发布模式设置为“文件模式”,如果某个场景的 层级复杂、操作频繁并使用了 runtime 脚本,getchild 会很麻烦且效率低,那么可以 单独将这个场景的发布模式设置为“分离模式”,这样就可以使用 var 了;也就是绝大多数的场景使用“文件模式”,个别使用“分离模式”。

# 两种方式的区别和应用场景

# 区别

  • runtime 脚本是 直接继承自组件 ,本组件直接就是 this,其子节点获取方式是 getChildByName 或者 分离模式下搭配 var
  • script 脚本是 继承自 Laya.Script,本组件是 this.owner ,其子节点获取方式是 getChildByName 或者使用 定义属性可视化来绑定子节点(type 设置为 Node,将子节点拖拽到该自定义属性里);
  • 如果两种方式不考虑其他而都使用 getChildByName 来获取子节点,由于 script 脚本多一层 owner,会导致效率会低一些;
  • 对比两种方式的生命周期图,会发现 script 脚本的生命周期函数更多并且还自带鼠标、键盘和碰撞的方法,而 runtime 脚本的生命周期函数只有四个:onAwake(激活)、onEnable(启用)、onDisable(禁用)、onDestroy(销毁)

# 应用场景

  • 像一些主场景、大的页面 UI 这样的,可能需要频繁地访问页面内多个元素(多为显示隐藏等类似简单逻辑),这样的应用场景下可以使用 runtime 脚本;
  • 如果主场景、大的页面 UI 里涉及到的业务很多且很复杂,像 runtime 脚本的四个生命周期函数和 this.on 复杂绑定方式满足不了开发需求,可以使用 script 脚本;当然,UI 中具体的节点也会涉及到一些逻辑,这样的建议使用 script 脚本挂载。
  • 也就是说,像控制显示隐藏这样的简单逻辑的使用 runtime 脚本就足够了,更多地是涉及到复杂业务逻辑就使用 script 脚本(例如游戏总控制、角色控制等)。

关联runtime脚本