插件开发

      一般通过脚本组件的方式就可以实现大部分逻辑,涉及引擎级别或者业务级别的通用能力,可以使用插件的形式进行开发,插件主要是由 Component 和 System 组成。

      Eva.js 的渲染是基于 PixiJS 的,一般 Img/Sprite/Spine 等插件实际上是创建了 Pixi 的渲染对象,像 Stats/EvaX/Transition 等插件不依赖 Pixi。不管是哪种插件都是输出 Component 和 System 给引擎使用,但是开发方案有一些不同,接下来我会先讲解最简单的插件开发方法。

      我们提供了一个插件模板,可以点击 Use this Template 直接使用模版进行开发,里面带了必要的脚手架。

      基础

      开发

      读到这里,相信大家已经对 Eva.js 有所了解并且知道如何在项目中使用了,下面是一个插件的简单的使用方法。

      1. import { Demo, DemoSystem } from ‘./tutorials/lib’
      2. const game = new Game({
      3. systems: [new DemoSystem()]
      4. })
      5. const go = new GameObject()
      6. go.addComponent(new Demo())
      7. game.scene.addChild(go)

      我们可以看到,插件是由 Component 和 System 组成的,并且一个插件中不一定只包含一个 Component。

      所以,开发插件需要实现暴露给用户使用的 Component 和 System。

      插件运行逻辑

      组件(Component)可以赋予游戏对象能力,我们将一些配置和属性记录在组件上。 系统(System)用来读取组件上面的数据,实现组件对应的能力。

      当系统被添加到游戏实例上后,系统在它所需关心的组件在添加、移除、属性变化时,做一系列对应的操作,即可实现一些功能。

      例如在 Img 插件中,当 Img 被添加到游戏对象上时,System 内会创建一个 Pixi 的 Sprite 对象,挂载到 GameObject 对应的 Pixi Container上,当 Img 组件的 resource 发生变化时,System 会去修改对应 Sprite 上面的 texture。

      接下来,我会讲解如何设计一个组件,以及 System 是如何监听组件变化的。

      构建与发布规范

      开发实践

      下面以 <a href=”/read/eva.js-1.2-zh/8b30e8d2cbe110d7.md” “=”” data-pid=”495271”>@eva/plugin-a11y 插件为例,对 Eva.js 插件开发做一个详细的介绍。

      @eva/plugin-a11y 用于为游戏对象添加无障碍的能力。在 DOM 开发中,无障碍阅读器是可以阅读到 HTML 元素内容的,目前在 Canvas 里的绘制元素无法实现无障碍化的能力,@eva/plugin-a11y 插件通过定位游戏对象的位置,自动化地添加辅助 DOM,使得游戏对象能被无障碍阅读器聚焦,让游戏拥有无障碍功能。

      首先设计 Component,既需要赋予游戏对象的能力。

      使用案例

      1. import { A11y, A11ySystem } from ‘@eva/plugin-a11y’
      2. const game = new Game({
      3. systems: [new A11ySystem()]
      4. })
      5. const go = new GameObject()
      6. go.addComponent(new A11y({
      7. hint: ‘所需朗读的内容’
      8. }))
      9. game.scene.addChild(go)

      Component 设计

      • 确定组件名称: A11y
      • 设计组件参数:

        • hint 需要朗读的内容

      1. import { Component } from ‘@eva/eva.js’
      2. export default class A11y extends Component {
      3. static componentName: string = ‘A11y’ // 这里是Component的名称,用于 System 监听变化
      4. /
      5. 无障碍标签朗读内容
      6. /
      7. public hint: string
      8. /
      9. 初始化方法,构造函数的参数会传递到这里
      10. /
      11. init(param = {hint: ‘’}) {
      12. const { hint } = param
      13. this.hint = hint
      14. }
      15. }

      System 设计

      • 确定要监听的组件,以及需要监听哪些参数的变化
      • 确定系统名字
      • 根据组件变化实现逻辑

      Step1 确定要监听的组件以及参数

      1. import { System, decorators } from ‘@eva/eva.js’
      2. @decorators.componentObserver({
      3. A11y: [‘hint’] // 监听 A11y 组件的 hint 属性变化
      4. })
      5. class A11ySystem extends System {
      6. }

      在上面的代码中,我们将需要监听变化的组件名称和监听属性传入 @decorators.componentObserver 中,以便创建监听。

      如果只需要监听组件添加移除可以不填写具体的属性,例如

      1. @decorators.componentObserver({
      2. A11y: [] // 监听 A11y 组件的 hint 属性变化
      3. })

      如果监听的属性不是直接挂载到组件对象上的,还有一级嵌套

      例如监听组件 A 的 style 属性下的 size 属性

      可以这样写:

      1. @decorators.componentObserver({
      2. A: [{
      3. prop: [‘style’, ‘size’]
      4. }]
      5. })

      如果想要深度监听 style 属性,可以这样写

      1. @decorators.componentObserver({
      2. A: [{
      3. prop: [‘style’],
      4. deep: true
      5. }]
      6. })

      如果想监听多个组件变化,可以这样写

      1. @decorators.componentObserver({
      2. A: [{
      3. prop: [‘style’],
      4. deep: true
      5. }]
      6. B: [‘props’]
      7. })

      Step2 设置系统名字

      给 System 设置名字

      1. import { System, decorators } from ‘@eva/eva.js’
      2. @decorators.componentObserver({
      3. A11y: [‘hint’] // 监听 A11y 组件的 hint 属性变化
      4. })
      5. class A11ySystem extends System {
      6. static systemName = ‘A11ySystem’;
      7. }

      Step3 根据组件变化实现逻辑

      在此之前,我们做了一些监听配置,那么我们如何拿到对应的变化呢?

      我们知道 System 有 update 生命周期,我们在生命周期中可以获取到当前帧 Component 的变化。

      1. import { System, decorators, ComponentChanged } from ‘@eva/eva.js’
      2. @decorators.componentObserver({
      3. A11y: [‘hint’] // 监听 A11y 组件的 hint 属性变化
      4. })
      5. class A11ySystem extends System {
      6. static systemName = ‘A11ySystem’;
      7. private elemMap = new Map()
      8. update () {
      9. const changes: ComponentChanged[] = this.componentObserver.clear() // 获取当前帧所有需要监听的组件大变化,并且进行清理
      10. for (const changed of changes) {
      11. switch (changed.type) {
      12. case OBSERVER_TYPE.ADD:
      13. this.add(changed);
      14. break;
      15. case OBSERVER_TYPE.CHANGE:
      16. this.change(changed)
      17. break;
      18. case OBSERVER_TYPE.REMOVE:
      19. this.remove(changed);
      20. break;
      21. }
      22. }
      23. }
      24. add(changed) {
      25. if (changed.componentName === ‘A11y’) { // 如果有多个Component的话需要分开处理
      26. const component = changed.component as A11y
      27. const elem = document.createElement(‘div’)
      28. elem.setAttribute(‘aria-label’, component.hint);
      29. this.elemMap.set(component, elem)
      30. document.body.append(elem) // 添加到body上
      31. }
      32. }
      33. remove(changed) {
      34. if (changed.componentName === ‘A11y’) { // 如果有多个Component的话需要分开处理
      35. const component = changed.component as A11y
      36. const elem = this.elemMap.get(component)
      37. elem.remove() // 移除elem
      38. }
      39. }
      40. change(changed) {
      41. if (changed.componentName === ‘A11y’) { // 如果有多个Component的话需要分开处理
      42. if (changed.prop?.prop[0] === ‘hint’){ //如果有多个监听属性需要分开处理
      43. const component = changed.component as A11y
      44. elem.setAttribute(‘aria-label’, component.hint);
      45. }
      46. }
      47. }
      48. }

      ComponentChanged 对应的类型是这样的,可以参考,不需要在代码里实现

      1. export interface PureObserverProp {
      2. deep: boolean;
      3. prop: string[];
      4. }
      5. export enum ObserverType {
      6. ADD = ‘ADD’,
      7. REMOVE = ‘REMOVE’,
      8. CHANGE = ‘CHANGE’,
      9. }
      10. export interface ComponentChanged {
      11. type: ObserverType;
      12. component: Component;
      13. componentName: string;
      14. prop?: PureObserverProp;
      15. gameObject?: GameObject;
      16. systemName?: string;
      17. }

      现在我们把DOM创建好,并且放到了 body 上面,按照能力来讲,我们已经完成了具体的功能,因为屏幕阅读器已经可以阅读游戏中的元素了,但是看起来目前欠缺一些内容,例如:无法通过触发 DOM 点击事件来触发游戏里面的点击,DOM 的没有宽高和定位。

      如果想实现这些功能,就要去在当前组件下拿到别的组件去实现功能了,如果想触发点击事件,需要判断 Event 组件是否安装,如果安装的话,可以根据 Event 上绑定的事件,触发对应的事件。如果想获取宽高位置的话,可以获取游戏对象的 Transform 组件

      增加 Event 组件的监听,在上述 add remove 等方法里做对应操作即可。

      1. @decorators.componentObserver({
      2. A11y: [‘hint’] // 监听 A11y 组件的 hint 属性变化
      3. Event: [] // Event 增加删除监听
      4. })
      5. class A11ySystem extends System {
      6. }

      对于位置和宽高,可以在 A11y 组件被添加时拿到对应 GameObject 的 Transform,这里仅举个例子

      1. add(changed) {
      2. if (changed.componentName === ‘A11y’) { // 如果有多个Component的话需要分开处理
      3. const component = changed.component as A11y
      4. const elem = document.createElement(‘div’)
      5. elem.setAttribute(‘aria-label’, component.hint);
      6. this.elemMap.set(component, elem)
      7. document.body.append(elem) // 添加到body上
      8. const transform = changed.gameObject.transform
      9. elem.style.width = transform.size.width + ‘px’
      10. elem.style.height = transform.size.width + ‘px’
      11. elem.style.x = transform.position.x + ‘px’
      12. elem.style.y = transform.position.y + ‘px’
      13. }
      14. }

      基于 PixiJS 的插件

      以图片组件举例:

      1. import {
      2. GameObject,
      3. decorators,
      4. resource,
      5. ComponentChanged,
      6. RESOURCE_TYPE,
      7. OBSERVER_TYPE,
      8. } from ‘@eva/eva.js’;
      9. import {
      10. RendererManager,
      11. ContainerManager,
      12. RendererSystem,
      13. Renderer,
      14. } from ‘@eva/plugin-renderer’;
      15. @decorators.componentObserver({
      16. Img: [{prop: [‘resource’], deep: false}],
      17. })
      18. export default class Img extends Renderer { // 基于 PixiJS 渲染的插件,我们的 System 需要继承于统一的一个 Renderer 类
      19. rendererSystem: RendererSystem;
      20. init() { // 在init中去获取 rendererSystem 以便后续添加 Pixi 对象,并且需要将当前系统注册到rendererManager中。
      21. this.rendererSystem = this.game.getSystem(RendererSystem) as RendererSystem;
      22. this.rendererSystem.rendererManager.register(this);
      23. }
      24. rendererUpdate(gameObject: GameObject) { // rendererUpdate 代替 Update 方法,因为update在 Renderer 类中已经实现
      25. const {width, height} = gameObject.transform.size;
      26. if (this.imgs[gameObject.id]) {
      27. this.imgs[gameObject.id].sprite.width = width;
      28. this.imgs[gameObject.id].sprite.height = height;
      29. }
      30. }
      31. async componentChanged(changed: ComponentChanged) { // 在 Renderer 类中实现了 update 方法,并且将 Img 对应的组件变化传递给 componentChanged
      32. if (changed.componentName === ‘Img’) {
      33. const component: ImgComponent = changed.component as ImgComponent;
      34. if (changed.type === OBSERVER_TYPE.ADD) {
      35. const sprite = new Sprite(null);
      36. resource.getResource(component.resource).then(({instance}) => {
      37. if (!instance) {
      38. console.error(
      39. GameObject:${changed.gameObject.name}'s Img resource load error,
      40. );
      41. }
      42. sprite.image = instance;
      43. });
      44. this.imgs[changed.gameObject.id] = sprite;
      45. this.containerManager
      46. .getContainer(changed.gameObject.id)
      47. .addChildAt(sprite.sprite, 0); // 将创建的 Pixi 渲染对象放进 GameObject 对应的 Pixi 容器中
      48. } else if (changed.type === OBSERVER_TYPE.CHANGE) {
      49. const {instance} = await resource.getResource(component.resource);
      50. if (!instance) {
      51. console.error(
      52. GameObject:${changed.gameObject.name}'s Img resource load error,
      53. );
      54. }
      55. this.imgs[changed.gameObject.id].image = instance;
      56. } else if (changed.type === OBSERVER_TYPE.REMOVE) {
      57. const sprite = this.imgs[changed.gameObject.id];
      58. this.containerManager
      59. .getContainer(changed.gameObject.id)
      60. .removeChild(sprite.sprite);
      61. delete this.imgs[changed.gameObject.id];
      62. }
      63. }
      64. }

      生命周期

      image.png

      总结

      通过 Component 和 System 的结合,我们可以实现各种各样的通用插件,在日常开发中,我们仅需要 CustomComponent 提供的能力开发游戏逻辑即可。

      ,