NateTheGreatt/bitECS

How to define two-dimensional array aka matrix?

ProgrammingLife opened this issue · 2 comments

I've noticed about this approach:

const List = defineComponent({ values: [Types.f32, 3] }) 

but what if I need to define a matrix of a size that I don't know?
I suppose I need smth like this: values: [ Types.f32, Types.f32 ] since I wanna define two-dimensional matrix without a fixed size.

It's useful when we need to define something like Waypoints system where object moves from one point to another. How we can do it?

const Waypoints = defineComponent( { x: [ Types.f32, 2 ], y: [ Types.f32, 2 ] } );

it's not the best solution to have only two waypoints for moving the object from one point to second and back :)
What should we do if we need more than more than 2 waypoints, say 3 points? Define another component x: [ Types.f32, 3 ] ? :)
Each object may have different number of waypoints.

As a possible workaround:
We can define a fixed-size array with big number of length and define a second variable in the component like: arrLength, so we will know actual of that array. It's like a little bit overhead but anyway it's a solution.

@ProgrammingLife so couldn't you instead store the matrix as a flat array?

// to flatten:
const matrix = [[1,2,3],[4,5,6],[7,8,9]];
matrix.flat();

// and unflat:
const flat = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    
const newArray = [], size = 3;
while (flat.length > 0) newArray.push(flat.splice(0, size));

I'd assume that a flattened list is easier to store mem-wise.

A multidimensional array wouldn't help here. No matter what, a Component's data in bitECS has always a defined size.

My two cents is that bitECS's design is fundamentally incompatible with data of variable/unknown length, so this type of data generally does not belong in the ECS. If you really need variable data, you should store it outside of the ECS:

const Waypoints = defineComponent();

const hasWaypoints = defineQuery([Waypoints])
const oldWaypoints = exitQuery(hasWaypoints);

const waypointPositions = new Map<number, Float32Array[]>();

function waypointsGCSystem(world) {
 const exited = oldWaypoints(world)
 for (let i = 0; i < exited.length; i++) {
  const eID = exited[i];
  // When the component is removed, i.a. when the entity stops existing, we can remove its waypoints
  waypointPositions.delete(eID);
 }
}

function addWaypoint(world, eID, x, y) {
 // Ensure the GC system knows to check this entity
 if(!hasComponent(world, Waypoints, eID) addComponent(world, Waypoints, eID)
 // Ensure the allEntityWaypoints array exists
 if(!waypointPositions.has(eID)) waypointPositions.set(eID, []);

 const allEntityWaypoints = waypointPositions.get(eID)!;

 const position = new Float32Array(2);
 position[0] = x;
 position[1] = y;

 allEntityWaypoints.push(position);
}

function getWaypoints(eID) {
 return waypointPositions.get(eID);
}

Note that you do not even need a Component here: The Waypoints tag is just so that we know we can remove all waypoints when the entity is removed, but you could do that deletion manually somewhere else, or not at all depending on your use case.

Of course, this is less performant that bitECS's structs of arrays, but their performance comes from the fact that they are fixed-size and can therefore be represented through ArrayBuffers. Making the choice of what tradeoffs to make is up to you: a limited amount of waypoints that use up more memory, but are very fast, or a slower approach that can have an unlimited amount of waypoints? The choice is yours.