Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ECS #13

Closed
1 of 17 tasks
DrSensor opened this issue Sep 9, 2022 · 4 comments
Closed
1 of 17 tasks

ECS #13

DrSensor opened this issue Sep 9, 2022 · 4 comments
Assignees
Labels
data binding Everything related to data binding decorator All issues related to ES decorator enhancement New feature or request meta thread Spec and Draft but in several comments. Mostly this kind of issue are locked to keep it clean. priority: medium Something that should be tackled

Comments

@DrSensor
Copy link
Owner

DrSensor commented Sep 9, 2022

This ECS feature are more like my toy experiment rather than serious one. Although no one ask for this 😂, code composition for multiple logics that constantly in a loop while updating the retained GUI (DOM) are damn so hard.

Note ECS usages are not limited to gamedev or logic that constantly in a loop. ECS is a composition pattern that specifically optimized for doing mass control.

  • Struct of Array (any type)
  • Struct of TypedArray
  • Struct of Boolean as BitSet (use case: Tag component)
  • SparseSet and BitSet entities
    • @allocate decorator
    • function to kill(entity) (reset component value)
    • function to suspend(entity) and resume(entity) (doesn't reset component value)
  • Archetype graph
    • function to .add(components) from an entity
    • function to .remove(components) from an entity
    • query(components)
  • async system = () => ...
    • global dt (delta time)
    • local system.dt
    • pseudo async primitive that doesn't queue into microtask
    • explicit OMT system across spectrum: worker, wasm, compute shader. (require more exploration)
  • Resumability a.k.a data snapshot #15
@DrSensor DrSensor added enhancement New feature or request priority: low Low hanging issue. Might not worth to tackle it now. labels Sep 9, 2022
@DrSensor DrSensor self-assigned this Sep 11, 2022
@DrSensor
Copy link
Owner Author

DrSensor commented Sep 24, 2022

For now I only need query all value in multiple instance. For example,

<render-scope>
  <link as="script" href="position.js" data-persist>
  <input type="range" min="0" max="100" value="10" value:="x">
</render-scope>

...long content

<render-scope>
  <link as="script" href="position.js" data-persist>
  <input type="range" min="0" max="100" value="20" value:="x">
</render-scope>
import { u8 } from "wiles/types"
import { all, count, spawn } from "wiles/std"

class Position {
  @spawn(100, u8) // cap it to 100 instance **across pages**
  accessor x: number
}

// let's say I use this function in a widget/island build using Preact
const sumMin10 = () => {
  let total = 0
  for (let i = 0, x = all(Position).x; i < count(Position); i++)
    if (x[i] >= 10) total += x[i]
  return total
}

export { default as Position, sumLess10 }

This scenario doesn't require managing entity id via SparseSet or BitSet. The accessor just proxy to an item in UInt8Array. If size not specified, it will default to Array with dynamic capacity. For example,

import { all, spawn } from "wiles/std"

class Position {
  @spawn // no cap, grow as needed
  accessor x: number
}

// let's say I use this function in a widget/island build using Preact
const sumMin10 = () => {
  let total = 0 // simpler query since there is no cap (.length === number of instances)
  for (const x of all(Position).x) if (x >= 10) total += x
  return total
}

export { default as Position, sumLess10 }

All examples above are realtime! It changes for-loop behavior when there is a new instance or the value of 'x' is changed. To make it deterministic, you need to copy both count and the backed array. For example,

const sumMin10 = () => {
  let total = 0
  const // copy both count and backed array
    countPosition = count(Position),
    x = Object.getPrototypeOf(all(Position).x).constructor.from(all(Position).x) // or simply do UInt8Array.from(all(Position).x)
  for (let i = 0; i < countPosition; i++)
    if (x[i] >= 10) total += x[i]
  return total
}

But most of the time it really doesn't matter since Javascript is a single threaded language.

🎶 "Never Gonna Give You Up" by Rick Ashley

@DrSensor
Copy link
Owner Author

DrSensor commented Sep 28, 2022

I think I solve how to share data across spectrum (worker, wasm, shader). Basically spawn decorator need to know which buffer to use. For example,

// assume that WorldBuffer can be from 3rd-party lib

type AnyBufferConstructor = ArrayBufferConstructor | SharedArrayBufferConstructor

const init = <T extends AnyBufferConstructor>(
  Buffer: T,
  capacity?: number,
) => class Single {
  static #buffer: InstanceType<T>
  static get buffer() {
    return this.#buffer
  }
  offset: number
  constructor(
    length: number | 100,
    type: TypedArrayConstructor | UInt8ArrayConstructor,
  ) {
    Single.#buffer ??= new Buffer(capacity ?? length)
    this.offset = type.BYTES_PER_ELEMENT * length
  }
}

import { u8 } from "wiles/type"
import { spawn } from "wiles/std"

export default class {
  @spawn(100, u8, WorldBuffer)
  x: number
}

const WorldBuffer = init(ArrayBuffer, 500) // limit to 5 instances

Now if we want share it with worker thread, we need to tell it to use SharedArrayBuffer instead of ArrayBuffer.

const WorldBuffer = init(SharedArrayBuffer, 500)
new Worker(
  "sumMin10.js",
  { type: "module" },
).postMessage(WorldBuffer.buffer)

@DrSensor
Copy link
Owner Author

DrSensor commented Oct 7, 2022

@frameloop decorator

Basically it just wrap method in requestAnimationFrame and some logic to make the system in loop deterministic and suspend/resume-able.

import { u16 } from "wiles/types"
import { frameloop, spawn, all, count } from "wiles/std"

export default class {
  @spawn(200, u16)
  accessor x // infer value

  @frameloop.once // explicit trigger but only once (i.e <button on:click="activateSinMove">)
  activateSinMove() {
    const { dt } = frameloop
    const { x } = all(this)
    for (let i = count(this); i--;) {
      x[i] = Math.sin(x[i])
    }
    // automatically update all x when this method end
  }
}

🤔 TODO: explain how to pause/suspend and resume the frameloop (especially async one), especially when there is a lot of frameloop function running

Repository owner locked and limited conversation to collaborators Oct 16, 2022
@DrSensor DrSensor added meta thread Spec and Draft but in several comments. Mostly this kind of issue are locked to keep it clean. priority: medium Something that should be tackled and removed priority: low Low hanging issue. Might not worth to tackle it now. labels Oct 16, 2022
@DrSensor DrSensor added decorator All issues related to ES decorator data binding Everything related to data binding labels Oct 29, 2022
@DrSensor
Copy link
Owner Author

Moved to #55 #56

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
data binding Everything related to data binding decorator All issues related to ES decorator enhancement New feature or request meta thread Spec and Draft but in several comments. Mostly this kind of issue are locked to keep it clean. priority: medium Something that should be tackled
Projects
None yet
Development

No branches or pull requests

1 participant