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

[v0.4] Add default initializer #173

Open
soulofmischief opened this issue Jan 23, 2025 · 8 comments
Open

[v0.4] Add default initializer #173

soulofmischief opened this issue Jan 23, 2025 · 8 comments

Comments

@soulofmischief
Copy link

soulofmischief commented Jan 23, 2025

bitECS could benefit from a symbol-keyed default initializer. I've implemented one in #172, if it's something you want to include. If not, feel free to close this issue and corresponding PR.

It has the following semantics:

import { $default, Default, addComponent, addEntity, createWorld } from 'bitECS'

const world = createWorld()
const eid = addEntity( world )

const MyComponent = {
  x: [] as number[],
  y: [] as number[],
  [$default]( eid, { x = 1, y = 1 } = {}) {
    MyComponent.x[ eid ] = x
    MyComponent.y[ eid ] = y
  }
}

addComponent( world, eid, MyComponent )
assert( MyComponent.x[ eid ] === 1 )

// or:
addComponent( world, eid, Default( MyComponent, { x: 42 }))
@NateTheGreatt
Copy link
Owner

NateTheGreatt commented Jan 23, 2025

onAdd observers are able to facilitate this feature :)

import { addComponent, addEntity, createWorld, observe, onAdd } from 'bitecs'

const world = createWorld()
const eid = addEntity(world)

const Position = {
  x: [] as number[],
  y: [] as number[]
}

observe(world, onAdd(Position), (eid) => {
  // Set default values when component is added
  Position.x[eid] = 1
  Position.y[eid] = 1
})

addComponent(world, eid, Position)

console.log(Position.x[eid]) // 1
console.log(Position.y[eid]) // 1

onSet observers also facilitate this:

observe(world, onSet(Position), (eid,params) => {
  Position.x[eid] = params.x
  Position.y[eid] = params.y
})

addComponent(world, eid, set(Position, { x: 2 , y: 2 }))

console.log(Position.x[eid]) // 2
console.log(Position.y[eid]) // 2

hopefully this is sufficient for your needs?

@soulofmischief
Copy link
Author

soulofmischief commented Jan 24, 2025

That's true, personally I like keeping things in the component itself and made this modification in my fork to test it out in a project, plus it's nice to avoid double initialization. It also decouples the default initializer from any particular world, eliminating the need for wrappers if there isn't a single or global world. Feel free to close in favor of onAdd!

@soulofmischief
Copy link
Author

Actually, let's say I have a relation with a store. I can do this:

export const CanTarget = createRelation(
  withStore(() => ({
    priority: [] as number[],

    [$default]( eid, { target, priority = 0 } = {}) {
      if ( target !== undefined ) {
        CanTarget( target ).priority[ eid ] = priority
      }
    }
  })),
)

const source = addEntity( world )
const target = addEntity( world )

addComponent( world, source,
  Default( CanTarget( target ), { target, priority: 3 }),
)

What would be your preferred solution for achieving this behavior with observables? Currently, the other eid is not passed to onAdd(). Should onAdd() just be modified to return both the source and target eid in a relation?

@NateTheGreatt
Copy link
Owner

NateTheGreatt commented Jan 24, 2025

perhaps this?

export const CanTarget = createRelation(
  // for 0.4 i can update withStore to supply the target, although not necessary for this particular case
  withStore(() => {
    const comp = { priority: [] as number[] }
    observe(world, onAdd(comp), (eid) => { comp.priority[eid] = 0 })
    observe(world, onSet(comp), (eid, params) => { comp.priority[eid] = params.priority })
    return comp
  })
)

const source = addEntity( world )
const target = addEntity( world )

addComponent(world, source, set(CanTarget( target ), { priority: 3 }))

@soulofmischief
Copy link
Author

Thanks, I'll try that out!

@soulofmischief
Copy link
Author

@NateTheGreatt

test( 'should properly set relation defaults', () => {
	const world = createWorld()

	const CanTarget = createRelation(
		// for 0.4 i can update withStore to supply the target, although not necessary for this particular case
		withStore(() => {
			const comp = { priority: [] as number[] }
			observe(world, onAdd(comp), (eid) => { comp.priority[eid] = 0 })
			observe(world, onSet(comp), (eid, params) => { comp.priority[eid] = params.priority })
			return comp
		})
	)

	const source = addEntity( world )
	const target = addEntity( world )

	addComponent(world, source, set(CanTarget( target ), { priority: 3 }))

	expect(CanTarget(target).priority[source]).toBe(3)
})

// AssertionError: expected +0 to be 3 // Object.is equality

It seems onSet() runs before onAdd() in this case.

@soulofmischief
Copy link
Author

soulofmischief commented Jan 24, 2025

Relatedly, what's the idiomatic way with bitECS to access stores of relations established a prefab?

If I set CanTarget() on one prefab to another, is there a better way than iterating all of an entity's components and filtering for IsA and comparing each entityId?

If one prefab sets CanTarget() for another prefab, and another inherits it and also sets CanTarget for the same prefab, but with a different priority, how is this reconciled in the store, when each prefab passes a different EID to onSet?

It would just be nice to do something like

const world = createWorld()
const Player = addPrefab( world )
const Enemy = addPrefab( world )

addComponent( world, Player, CanTarget( Enemy ))

and to also have a way to automatically resolve inherited priority values.

I can construct a blueprint or class which instantiates these relationships each time an entity is instantiated, but it feels more like data than behavior, that these relationships should be defined at the prefab level and can be performantly recovered later.

Is this an abuse of relationships or prefabs?

@NateTheGreatt
Copy link
Owner

NateTheGreatt commented Jan 26, 2025

Relatedly, what's the idiomatic way with bitECS to access stores of relations established a prefab?

calling a relation on a target passes back the component for that pair in particular. the component is a store for any/all entities who have that relation to that entity. prefabs are entities themselves, except for the fact that they will not appear in queries.

const world = createWorld()
const Contains = createRelation( withStore(() => ({ amount: [] as number[] }) ))
const Water = {}

const Bottle = addPrefab( world )
addComponent(world, Bottle, Contains(Water))

const Jug = addPrefab( world )
addComponent(world, Jug, Contains(Water))

const containsWaterComponent = Contains(Water)
containsWaterComponent.amount[Bottle] = 1
containsWaterComponent.amount[Jug] = 10

const bottleInstance = addEntity(world)
addComponent(world, bottleInstance, IsA(Bottle))
assert(containsWaterComponent.amount[bottleInstance] === containsWaterComponent.amount[Bottle])

keep in mind that because component stores are fully custom you will need to implement onGet and onSet for bitecs to know how to get and set data to your component stores when inheriting component values in the hierarchy.

observe(world, onSet(Contains(Water)), (eid, params) => { Contains(Water).amount[eid] = params.amount })
observe(world, onGet(Contains(Water)), (eid) => Contains(Water).amount[eid] })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants