首页 > 编程语言 >ecs-lite 源码简单分析

ecs-lite 源码简单分析

时间:2023-01-19 17:12:50浏览次数:40  
标签:canvas const text 源码 ecs new lite world public

初学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)

标签:canvas,const,text,源码,ecs,new,lite,world,public
From: https://www.cnblogs.com/cslie/p/17061040.html

相关文章

  • DBNet源码详解
    参考项目:https://github.com/WenmuZhou/DBNet.pytorch标签制作制作thresholdmap标签make_border_map.py程序入口if__name__=='__main__'if__name__=='__main......
  • win10下python3.9的代理报错问题解决(附web3的polygon爬虫源码)
    背景因为工作中经常需要代理访问,而开了代理,request就会报错SSLError,如下:requests.exceptions.SSLError:HTTPSConnectionPool(host='test-admin.xxx.cn',port=443):Ma......
  • 浅谈SpringAOP功能源码执行逻辑
    如题,该篇博文主要是对Spring初始化后AOP部分代码执行流程的分析,仅仅只是粗略的讲解整个执行流程,喜欢细的小伙伴请结合其他资料进行学习。在看本文之前请一定要有动态代理的......
  • SpringBoot源码学习3——SpringBoot启动流程
    系列文章目录和关于我一丶前言在《SpringBoot源码学习1——SpringBoot自动装配源码解析+Spring如何处理配置类的》中我们学习了SpringBoot自动装配如何实现的,在《Sprin......
  • 浅谈Netty中ServerBootstrap服务端源码(含bind全流程)
    文章目录​​一、梳理Java中NIO代码​​​​二、Netty服务端代码​​​​1、newNioEventLoopGroup()​​​​2、group​​​​3、channel​​​​4、NioServerSocketChanne......
  • 浅谈Redisson底层源码
    Redisson源码分析​​一、加锁时使用lua表达式,执行添加key并设置过期时间​​​​二、加锁成功之后给锁添加对应的事件​​​​三、加锁完成,看门狗自动续命未处理完的线程​......
  • drf快速使用 CBV源码分析 drf之APIView分析 drf之Request对象分析
     目录序列化和反序列化drf介绍和安装使用原生django写接口djangoDRF安装drf快速使用模型序列化类视图路由datagrip使用postman测试接口CBV源码分......
  • 浅谈Zookeeper集群选举Leader节点源码
    写在前面:zookeeper源码比较复杂,本文讲解的重点为各个zookeeper服务节点之间的state选举。至于各个节点之间的数据同步,不在文本的侧重讲解范围内。在没有对zookeeper组件有一......
  • 浅谈Redis基本数据类型底层编码(含C源码)
    文章目录​​一、String​​​​1、int​​​​2、embstr​​​​3、raw​​​​4、bitmap​​​​5、hyperloglog​​​​二、List​​​​1、ziplist​​​​2、quicklist......
  • STM32 PLC底层源码 FX2N源码 断电保持/Keil源码
    STM32PLC底层源码FX2N源码断电保持/Keil源码三菱指令编码注释较多,适合初学者,发编译环境:KeilMDK4.7以上的版本,CPU需要:STM32F103--RAM内存不小64K,Flash程序空间不小于256......