NateTheGreatt / bitECS

Flexible, minimal, data-oriented ECS library for Typescript
Mozilla Public License 2.0
942 stars 84 forks source link

Nested arrays #64

Closed grorp closed 2 days ago

grorp commented 3 years ago

I couldn't get arrays in arrays working. Here is an example:

"use strict";

import {
    createWorld,
    defineComponent,
    Types,
    addEntity,
    addComponent,
} from "https://cdn.skypack.dev/bitecs";

const testWorld = createWorld();

const TestComponent = defineComponent({
    // Array of floats
    array: [Types.f32, 3],
    // Array of arrays of floats
    array2: [[Types.f32, 3], 3],
});

const testEntity = addEntity(testWorld);
addComponent(testWorld, TestComponent, testEntity);

// Works
TestComponent.array[0][testEntity] = 0.1;
TestComponent.array[1][testEntity] = 0.2;
TestComponent.array[2][testEntity] = 0.3;

// Doesn't work
// TypeError: TestComponent.array2[1][0] is undefined
TestComponent.array2[0][0][testEntity] = 0.4;
TestComponent.array2[0][1][testEntity] = 0.5;
TestComponent.array2[0][2][testEntity] = 0.6;

TestComponent.array2[1][0][testEntity] = 0.7;
TestComponent.array2[1][1][testEntity] = 0.8;
TestComponent.array2[1][2][testEntity] = 0.9;

TestComponent.array2[2][0][testEntity] = 1.0;
TestComponent.array2[2][1][testEntity] = 1.1;
TestComponent.array2[2][2][testEntity] = 1.2;
SupremeTechnopriest commented 3 years ago

Nested arrays are not supported. In my opinion, component data should be flat where ever possible. Arrays incur a start up performance hit and use a lot of memory. I don't think we will be supporting nested arrays, but correct me if I am wrong @NateTheGreatt.

dannyfritz commented 3 years ago

Nested arrays are not supported. In my opinion, component data should be flat where ever possible. Arrays incur a start up performance hit and use a lot of memory. I don't think we will be supporting nested arrays, but correct me if I am wrong @NateTheGreatt.

Food for thought, "nested arrays" can be handled in a flat manner. If the size is known, it can always be mapped to the buffer appropriately. It does complicate the Proxy object and mapping though.

SupremeTechnopriest commented 3 years ago

Its definitely possible. What is the use case here? Can you share the actual component you are trying to create @grorp?

grorp commented 3 years ago

Thank you for the fast replies! I'm making simple visuals/physics for a game:

const Vector3D = [Types.f32, 3];
const Quaternion = [Types.f32, 4];
const Color = [Types.ui8c, 3];

const Body = defineComponent({
    color: Color,
    boxes: [[Vector3D, 2], 10], // The two points are opposite corners.
});

const Physics = defineComponent({
    position: Vector3D,
    rotation: Quaternion,
});
const DynamicPhysics = defineComponent({
    velocity: Vector3D,
    rotationalVelocity: Quaternion,
});

Are arrays really more expensive than objects? 😯

I could change the Vector3Ds, Quaternions and Colors to objects and make boxes a [Vector3D, 20]. This wouldn't of course make the code nicer. But the other variant would make your code more complicated, so...

Edit: One more question: Are there dynamic arrays? Because not every Body has 10 boxes. Most only have 2 or 3.

NateTheGreatt commented 3 years ago

hi @grorp, thanks for creating the issue! this feature is doable, i shall add it to my to-do list :+1:

Are arrays really more expensive than objects?

they only take up more boot-time, as bitECS preallocates everything up-front. preallocating a single typedarray is fast, but preallocating an array of subarrays for each EID is a bit slower due to the need to iterate over the entire array of all possible entities. i plan on utilizing workers to reduce this boot time overhead in the future.

One more question: Are there dynamic arrays? Because not every Body has 10 boxes. Most only have 2 or 3.

sort of, you can achieve a dynamic-array-like behavior like so:

const DynamicArrayComponent = defineComponent({
  someDynamicArray: {
    array: [f32, 12],
    length: ui8,
  }
})

const push = (darray, eid, value) => { darray.array[eid][darray.length[eid]++] = value }

push(DynamicArrayComponent.someDynamicArray, 0, 1)

the space will still need to be preallocated, but you can manually handle a custom length inside of some standard array-like functions if you so desire

grorp commented 2 years ago

I discovered a while ago that arrays have the entity ID before the index, like this:

Component.array[entityID][14]

How would you do it with nested arrays? The possibilities I see are:

Component.array[14].array[15].array[entityID][16]

or

Component.array[entityID][14].array[15].array[16]

or, my personal favourite, because it is consistent with the rest of bitECS:

Component.array[14].array[15].array[16][entityID]
NateTheGreatt commented 2 years ago

this would be consistent with the current API:

const innerArray = Component.array[entityID][arrayID]

the other way around doesn't make sense imo. what does 14 represent in Component.array[14] and what would it return?

grorp commented 2 years ago

You got me confused. I was thinking of this component:

const Component = defineComponent({
    array: [[[Types.f32, 20], 20], 20],
});

What would I write if I wanted to access the 14th element of the 14th element of the 14th element of Component.array for the entity with the ID 3? (Which way around?)

NateTheGreatt commented 2 years ago

the semantics that i have prefered are such that we use the entityID for accessing a value for a specific entity from a component.

const x = Position.x[eid]

because arrays are themselves values in this case, we use the entityID to access a specific array for an entity and then pull values from that array.

const array = Component.array[eid]
const value = array[0]

to continue this pattern for nested structures, we should still be using the entityID first to obtain arrays as values. if arrays can hold arrays, then we would still use the entityID to get the first array value which contains other arrays.

const arrays = Component.arrays[eid]
const firstArray = arrays[0]
const firstNestedArray = firstArray[0]

Component.arrays[eid][0][0]

What would I write if I wanted to access the 14th element of the 14th element of the 14th element of Component.array for the entity with the ID 3? (Which way around?)

Component.arrays[eid][14][14][14]

first, we get the nested array structure that belongs to the entity (the value). then, we can dig into that nested structure.

this also has the benefit of allowing references. for example:

const nestedArray = Component.arrays[eid][14]

someFunction(nestedArray)

using this direction of chaining, we have the ability to pass around references for specific entity arrays. using the opposite direction of chaining does not give us this privilege, unfortunately, which removes the compatibility with libraries like glMatrix (among others) which use arrays as containers for vector types:

const Transform = defineComponent({
  position: [f32, 2],
  rotation: [f32, 2]
})

const position = Transform.position[eid]

const vectorA = new Float32Array([1,2])
const vectorB = new Float32Array([3,4])

glMatrix.vec2.add(position, vectorA, vectorB)

console.log(position) // => Float32Array(2) [ 4, 6 ]
grorp commented 2 years ago

Thank you for the very detailed response. It makes sense that it works like that.