ecs-lite 源码简单分析

初学typescript,分析的不到位欢迎指正。 

ecs-lite

基于 ts 实现的纯 ecs 库,可用于学习交流及 H5 游戏开发!

https://gitee.com/aodazhang/ecs-lite?_from=gitee_search

文档

https://aodazhang.com/ecs-lite/

 

api 

https://aodazhang.com/ecs-lite/api.html#world-%E4%B8%96%E7%95%8C

 

几个关键点及新学到的知识点

/** [类]实体 */
export class Entity {
  constructor(
    /** 实体id */
    public readonly id: number,
    /** 实体名 */
    public readonly name?: string
  ) {}
}
实体集合 class EntitySet extends Set<Entity> 
  public override add(...entities: Entity[]) {
    entities.length === 1
      ? super.add(entities[0])
      : entities.forEach(item => super.add(item))
    return this
  }
-----------------------------------------------------------------------------------------------
/**
 * 生成器创建id
 * @returns 无
 */
export function* createId(): IterableIterator<number> {
  let id = 0
  while (true) {
    yield ++id
  }
}-----------------------------------------------------------------------------------------------
  /** 实体 */
  private readonly entityId = createId()
-------------------------------------------创建实体------------------------------------------------
 
 
public createEntity(name?: string): Entity {
    /**
     * 处理顺序 O(1)
     * 1.实体-组件映射
     * 2.实体名-实体映射
     */
    const entity = new Entity(
      this.entityId.next().value,        
      isString(name) ? name : null
    )
    /** 实体-组件映射 */
    //private readonly entityComponents: Map<Entity, ComponentMap> = new Map()
    //export class ComponentMap extends Map<ComponentConstructor, Component>
    /** [类型]组件构造函数 */
    // export type ComponentConstructor<T extends Component = Component> = new (
    //   ...rest: any[]
    // ) => T
    this.entityComponents.set(entity, new ComponentMap())
    if (!isString(entity.name)) {
      return entity
    }
    //  /** 实体名-实体映射 */
    //  private readonly nameEntities: Map<string, EntitySet> = new Map()
    this.nameEntities.has(entity.name)
      ? this.nameEntities.get(entity.name).add(entity)
      : this.nameEntities.set(entity.name, new EntitySet([entity]))
    return entity
  }

其中 

ComponentMap  较为复杂
  /**
   * 新增实体关联的组件
   * @param entity 实体
   * @param components 组件
   * @returns 世界实例
   */
  public addEntityWithComponents(
    entity: Entity,
    ...components: Component[]
  ): World {
    /**
     * 处理顺序 O(n)
     * 1.实体-组件映射
     * 2.组件-实体映射
     */
    const componentMap = this.entityComponents.get(entity)
    if (!componentMap) {
      return this
    }
    componentMap.add(...components)
    for (const constructor of componentMap.keys()) {
      this.componentEntities.has(constructor)
        ? this.componentEntities.get(constructor).add(entity)
        : this.componentEntities.set(constructor, new EntitySet([entity]))
    }
    return this
  }

主要关键是3个结构的理解

  /** 实体名-实体映射 */
  private readonly nameEntities: Map<string, EntitySet> = new Map()
  /** 实体-组件映射 */
  private readonly entityComponents: Map<Entity, ComponentMap> = new Map()
  /** 组件-实体映射 */
  private readonly componentEntities: Map<ComponentConstructor, EntitySet> =
    new Map()
  /**
   * 更新主循环
   * @returns 无
   */
  private update = (): void => {
    if (this.stop === true) {
      window.cancelAnimationFrame(this.timer)
      return
    }
    const now = performance.now()
    /*
      performance.now是浏览器(Web API)提供的方法,不同浏览器获取到的精度不同。Date.now是Javascript内置方法,
      差异主要在于浏览器遵循的ECMAScript规范。
      Date.now() 方法返回自 1970 年 1 月 1 日 00:00:00 (UTC) 到当前时间的毫秒数。
      performance.now() 方法返回一个精确到毫秒的时间戳,个别浏览器返回的时间戳没有被限制在一毫秒的精确度内,
      以浮点数的形式表示时间,精度最高可达微秒级,因此测试时如果需要比毫秒更高的精度,可以使用这个方法。
      示例(以chrome为例)
      const t1 = performance.now()
      // 538253.3999999762
      const t2 = Date.now()
      // 1664162107633
      Date.now() ≈ performance.timing.navigationStart + performance.now()
      const t1 = performance.timing.navigationStart + performance.now()
      const t2 = Date.now();
      console.log(t2, t1);
      // 1664162454515 1664162454515.9
    */
    const frame = Math.max(now - this.time, 0)
    this.systems.forEach(item => item.update(this, frame))
    this.timer = window.requestAnimationFrame(this.update)
    /**
     * 既然setInterval可以搞定为啥还要用requestAnimationFrame呢?
     *  不同之处在于,setInterval必须指定多长时间再执行,window.requestAnimationFrame()
     * 则是推迟到浏览器下一次重绘时就执行回调。重绘通常是 16ms 执行一次,不过浏览器会自动调节这个速率,
     * 比如网页切换到后台 Tab 页时,requestAnimationFrame()会暂停执行。
     * 如果某个函数会改变网页的布局,一般就放在window.requestAnimationFrame()里面执行,
     * 这样可以节省系统资源,使得网页效果更加平滑。因为慢速设备会用较慢的速率重流和重绘,而速度更快的设备会有更快的速率。
     */
    this.time = now
  }

  /**
   * 启动主循环
   * @returns 世界实例
   */
  public start(): World {
    this.stop = false
    this.timer = window.requestAnimationFrame(this.update)
    this.time = performance.now()
    return this
  }
/** [抽象类]系统 */
export abstract class System {
  /**
   * 更新钩子
   * @param world 世界实例
   * @param frame 帧渲染时间
   * @returns 无
   */
  public abstract update(world: World, frame?: number): void

我们在world 的update 中每帧中调用system的update方法

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

终于进入正题了,进入example-ecs 目录,这里才是实现的地方

const world = new World({
    canvas, // 绘图元素
    map, // 资源仓库
    isGameOver: false, // 是否游戏结束
    speed: 2, // 游戏速度
    gap: canvas.height / 4 // 障碍物间隔
  })
  world.addSystem(new EventSystem(canvas, world))
  world.addSystem(new ObstacleSystem(canvas))
  world.addSystem(new VelocitySystem(canvas))
  world.addSystem(new RotateSystem())
  world.addSystem(new CollisionSystem())
  world.addSystem(new ScoreSystem())
  world.addSystem(new RenderSystem(ctx))

  createScene(world)

  world.start()

先看具体的实体,在createScene 中可以找到

export function createScene(world: World): void {
  createBackground(world)
  createFloor(world)
  createScore(world)
  createBird(world)
}

/**
 * 创建背景
 * @param world 世界实例
 * @returns 无
 */
export function createBackground(world: World): void {
  const { canvas, sprites, spritesData } = getWorldData(world)
  world.addEntityWithComponents(
    world.createEntity('background'),
    new Image(sprites, [
      {
        sx: spritesData.background.sx,
        sy: spritesData.background.sy,
        sw: spritesData.background.sw,
        sh: spritesData.background.sh
      }
    ]),
    new Position(0, 0),
    new Size(canvas.width, canvas.height),
    new Render('image')
  )
}
看一个简单的组件
/** 位置 */
export class Position extends Component {
  /**
   * @param x 坐标x
   * @param y 坐标y
   */
  constructor(public x: number, public y: number) {
    super()
  }
}
 

再看事件系统

export class EventSystem extends System {
  constructor(
    private readonly canvas: HTMLCanvasElement,
    private readonly world: World
  ) {
    super()
    this.canvas.addEventListener('touchstart', this.onClickCanvas)
    this.canvas.addEventListener('click', this.onClickCanvas)
  }

  public update(): void {}

这个啥都没干,再 找一个

/**
 * 速度系统
 * @description 计算物体的x、y轴运动速度
 */
export class VelocitySystem extends System {
  constructor(private readonly canvas: HTMLCanvasElement) {
    super()
  }

  public update(world: World): void {
    for (const [entity, componentMap] of world.view(Position, Size, Velocity)) {
      const position = componentMap.get(Position)
      const size = componentMap.get(Size)
      const velocity = componentMap.get(Velocity)

      velocity.vy += velocity.g
      position.x += velocity.vx
      position.y += velocity.vy

      if (entity.name === 'floor') {
        // 地板循环向左移动:只要 x坐标 < 视口宽度 - 地板图宽度 则 地板图会和背景出现间隙 因此重置为0
        if (position.x < this.canvas.width - size.w) {
          position.x = 0
        }
      }
    }
  }

  public destory(): void {}
}

用一句白话总结一下,

速度 system 在每帧对 拥有 Position, Size, Velocity 组件的实体进行处理,修改了组件的position

组件数据如何跟现实实体数据关联。请看如下

  const world = new World({
    canvas, // 绘图元素
    map, // 资源仓库
    isGameOver: false, // 是否游戏结束
    speed: 2, // 游戏速度
    gap: canvas.height / 4 // 障碍物间隔
  })

在world 的构造函数里面,设置了    现实数据

 

再看一个非常简单的实体

/**
 * 创建计分器
 * @param world 世界实例
 * @returns 无
 */
export function createScore(world: World): void {
  const { canvas } = getWorldData(world)
  world.addEntityWithComponents(
    world.createEntity('score'),
    new Text('得分:0', 1000),
    new Score(0),
    new Position(10, canvas.width / 10),
    new Render('text', 1)
  )
}

这个简单的计分器 实体用到了 4个 组件
/** 文字 */
export class Text extends Component {
  /**
   * @param text 文本
   * @param maxWidth 最大长度
   * @param font 字体
   * @param fillStyle 文本颜色
   */
  constructor(
    public text: string,
    public maxWidth: number,
    public font: string = '18px Arial',
    public fillStyle: string = '#333'
  ) {
    super()
  }
}
/** 分数 */
export class Score extends Component {
  /**
   * @param gameScore 游戏得分
   */
  constructor(public gameScore: number) {
    super()
  }
}
/** 渲染 */
export class Render extends Component {
  /**
   * @param type 渲染类型
   * @param zindex 渲染层级
   */
  constructor(
    public readonly type: 'image' | 'text',
    public readonly zindex: number = 0
  ) {
    super()
  }
}
/** 位置 */
export class Position extends Component {
  /**
   * @param x 坐标x
   * @param y 坐标y
   */
  constructor(public x: number, public y: number) {
    super()
  }
}
 
 

看看有几个system 对其处理了

CollisionSystem   计算小鸟和障碍物之间的碰撞 ,没用到
EventSystem  没用到,啥都不做,构造中加入了事件监听而已
ObstacleSystem 循环生成销毁障碍物
RotateSystem   
旋转系统    计算物体的角运动速度
VelocitySystem    计算物体的x、y轴运动速度
 
下面2个对实体和组件进行了处理
ScoreSystem           , 实现了每帧增加分数
 
 public update(world: World): void {
    const score = world.findNameWithEntities('score')[0]
    if (!score) {
      return
    }
    const componentMap = world.findEntityWithComponents(score)
    const text = componentMap.get(Text)
    const gameScore = componentMap.get(Score)
    text.text = `【ECS实现】得分:${gameScore.gameScore++}`
  }

RenderSystem

export class RenderSystem extends System {
  constructor(private readonly context: CanvasRenderingContext2D) {
    super()
  }

  public update(world: World): void {
    // [路径管理]1.清除上一次绘制区域
    this.context.clearRect(
      0,
      0,
      this.context.canvas.width,
      this.context.canvas.height
    )

    // 根据渲染层级生成渲染队列
    const renderQueue = world.view(Render).sort((a, b) => {
      const [, componentMapA] = a
      const [, componentMapB] = b
      const renderA = componentMapA.get(Render)
      const renderB = componentMapB.get(Render)
      return renderA.zindex - renderB.zindex
    })

    // 渲染队列执行渲染
    for (const [, componentMap] of renderQueue) {
      const render = componentMap.get(Render)
      const position = componentMap.get(Position)
      const rotate = componentMap.get(Rotate)
      const image = componentMap.get(Image)
      const size = componentMap.get(Size)
      const text = componentMap.get(Text)

      // [路径管理]2.保存当前上下文状态
      this.context.save()

      // [路径变换]3.设置变换(位移、旋转、缩放)
      const mx = position?.x + size?.w / 2
      const my = position?.y + size?.h / 2
      this.context.translate(mx, my) // canvas旋转是基于绘图区域原点的,因此需要调整到精灵原点
      this.context.rotate(rotate?.angle)
      this.context.translate(-mx, -my) // 旋转后再次平移回来

      if (render.type === 'image') {
        image.clipCount += image.clipSpeed
        image.clipCount >= image.clips.length - 1 && (image.clipCount = 0)
        const clip = image.clips[Math.floor(image.clipCount)]
        // [路径绘制]5.执行图片绘制
        this.context.drawImage(
          image?.source,
          clip?.sx,
          clip?.sy,
          clip?.sw,
          clip?.sh,
          position?.x,
          position?.y,
          size?.w,
          size?.h
        )
      } else if (render.type === 'text') {
        // [路径样式]4.设置样式(尺寸、颜色、阴影、字体)
        this.context.font = text.font
        this.context.fillStyle = text.fillStyle
        // [路径绘制]5.执行文本绘制
        this.context.fillText(text.text, position.x, position.y, text.maxWidth)
      }

      // [路径管理]6.恢复上一次上下文状态
      this.context.restore()
    }
  }

  public destory(): void {}
}

哈哈,先清屏,我们的计分器在这里

   } else if (render.type === 'text') {
        // [路径样式]4.设置样式(尺寸、颜色、阴影、字体)
        this.context.font = text.font
        this.context.fillStyle = text.fillStyle
        // [路径绘制]5.执行文本绘制
        this.context.fillText(text.text, position.x, position.y, text.maxWidth)
posted @ 2023-01-19 16:59  cslie  阅读(254)  评论(0)    收藏  举报