Skip to content

Commit

Permalink
feat: stop using bitsets for now, remove registerComponents
Browse files Browse the repository at this point in the history
  • Loading branch information
isaac-mason committed Feb 13, 2024
1 parent ae7a77f commit 9096041
Show file tree
Hide file tree
Showing 29 changed files with 166 additions and 889 deletions.
10 changes: 10 additions & 0 deletions .changeset/fifty-toes-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@arancini/core": minor
"arancini": minor
---

feat: stop using bitsets for query evaluation, evaluate queries using object keys

The bitset implementation as-is is slower than just checking object keys, even for large numbers of component types.

This may be revisited in the future, but for now, arancini will use object keys for query evaluation to improve performance and simplify the library.
8 changes: 8 additions & 0 deletions .changeset/late-keys-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@arancini/core": minor
"arancini": minor
---

feat: remove World components constructor parameter and `registerComponents`

There is no longer any need to register components when creating a world.
78 changes: 78 additions & 0 deletions apps/benchmarks/src/query-evaulation-tmp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { BitSet } from '@arancini/core'
import { ObjectPool } from '@arancini/pool'

const withBitSets = (n) => {
const componentRegistry = {
a: 0,
b: 1,
c: 2,
d: 3,
e: 4,
f: 5,
g: 6,
h: 7,
i: 8,
j: 9,
k: 10,
}

const queryBitSet = new BitSet()
queryBitSet.add(componentRegistry.foo, componentRegistry.bar)

const pool = new ObjectPool(() => new BitSet())

for (let i = 0; i < n; i++) {
const entityBitSet = pool.request()
entityBitSet.reset()
// const entityBitSet = new BitSet()
entityBitSet.add(componentRegistry.foo, componentRegistry.bar)

entityBitSet.containsAll(queryBitSet)

pool.recycle(entityBitSet)
}
}

const withObjects = (n) => {
const query = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
]

for (let i = 0; i < n; i++) {
const entity = {
a: true,
b: true,
c: true,
d: true,
e: true,
f: true,
g: true,
}

query.every((key) => entity[key])
}
}

const bench = () => {
const n = 10000000

console.time('withBitSets')
withBitSets(n)
console.timeEnd('withBitSets')

console.time('withObjects')
withObjects(n)
console.timeEnd('withObjects')
}

bench()
3 changes: 1 addition & 2 deletions packages/arancini-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
"build:before": "rm -rf dist"
},
"dependencies": {
"@arancini/events": "6.1.3",
"@arancini/pool": "6.1.3"
"@arancini/events": "6.1.3"
},
"devDependencies": {
"@isaac-mason/eslint-config-typescript": "^0.0.4",
Expand Down
95 changes: 0 additions & 95 deletions packages/arancini-core/src/bit-set.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/arancini-core/src/entity-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class EntityContainer<Entity> {

export const addEntityToContainer = <E extends AnyEntity>(
container: EntityContainer<E>,
entity: E
entity: E,
): void => {
if (entity && !container.has(entity)) {
container.entities.push(entity)
Expand All @@ -62,7 +62,7 @@ export const addEntityToContainer = <E extends AnyEntity>(

export const removeEntityFromContainer = <E extends AnyEntity>(
container: EntityContainer<E>,
entity: E
entity: E,
): void => {
if (!container.has(entity)) {
return
Expand Down
12 changes: 0 additions & 12 deletions packages/arancini-core/src/entity-metadata.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/arancini-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ export {
type With,
type Without,
} from './query'
export { World, type AnyEntity, type WorldOptions } from './world'
export { World, type AnyEntity } from './world'
81 changes: 33 additions & 48 deletions packages/arancini-core/src/query.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { BitSet } from './bit-set'
import { EntityContainer } from './entity-container'
import { ARANCINI_SYMBOL, EntityWithMetadata } from './entity-metadata'
import type { ComponentRegistry, World } from './world'
import type { World } from './world'

export type With<E, P extends keyof E> = E & Required<Pick<E, P>>

Expand All @@ -28,19 +26,13 @@ export type QueryCondition<E> = {

export type QueryConditions<E> = QueryCondition<E>[]

export type QueryBitSets = {
type: QueryConditionType
bitset: BitSet
}[]

export type QueryDescription<E, R> = (q: QueryBuilder<E>) => QueryBuilder<R>

export class Query<E> extends EntityContainer<E> {
constructor(
public world: World,
public key: string,
public conditions: QueryConditions<E>,
public bitSets: QueryBitSets
public conditions: QueryConditions<E>
) {
super()
}
Expand All @@ -51,13 +43,13 @@ export class Query<E> extends EntityContainer<E> {
}

export const getQueryResults = <E>(
queryBitSets: QueryBitSets,
queryConditions: QueryConditions<E>,
entities: Iterable<E>
): E[] => {
const matches: E[] = []

for (const entity of entities) {
if (evaluateQueryBitSets(queryBitSets, entity)) {
if (evaluateQueryConditions(queryConditions, entity)) {
matches.push(entity)
}
}
Expand All @@ -66,35 +58,48 @@ export const getQueryResults = <E>(
}

export const getFirstQueryResult = <E>(
queryBitSets: QueryBitSets,
queryConditions: QueryConditions<E>,
entities: Iterable<E>
): E | undefined => {
for (const entity of entities) {
if (evaluateQueryBitSets(queryBitSets, entity)) {
if (evaluateQueryConditions(queryConditions, entity)) {
return entity
}
}

return undefined
}

export const evaluateQueryBitSets = <E>(
queryBitSets: QueryBitSets,
// export const getFirstQueryResult = <E>(
// queryBitSets: QueryBitSets,
// entities: Iterable<E>
// ): E | undefined => {
// for (const entity of entities) {
// if (evaluateQueryBitSets(queryBitSets, entity)) {
// return entity
// }
// }

// return undefined
// }

export const evaluateQueryConditions = <E>(
conditions: QueryConditions<E>,
entity: E
): boolean => {
const { bitset } = (entity as EntityWithMetadata<E>)[ARANCINI_SYMBOL]

for (const queryPart of queryBitSets) {
if (queryPart.type === 'all' && !bitset.containsAll(queryPart.bitset)) {
for (const condition of conditions) {
if (
condition.type === 'all' &&
!condition.components.every((c) => entity[c])
) {
return false
} else if (
queryPart.type === 'any' &&
!bitset.containsAny(queryPart.bitset)
condition.type === 'any' &&
!condition.components.some((c) => entity[c])
) {
return false
} else if (
queryPart.type === 'not' &&
bitset.containsAny(queryPart.bitset)
condition.type === 'not' &&
condition.components.some((c) => entity[c])
) {
return false
}
Expand Down Expand Up @@ -136,41 +141,21 @@ export const getQueryConditions = (
}

export const getQueryDedupeString = (
componentRegistry: ComponentRegistry,
// componentRegistry: ComponentRegistry,
queryConditions: QueryConditions<unknown>
): string => {
return queryConditions
.map(({ type, components }) => {
if (type === 'all') {
return components
.map((c) => componentRegistry[c])
.sort()
.join(',')
return components.sort().join(',')
}

return [
`${type}:${components
.map((c) => componentRegistry[c])
.sort()
.join(',')}`,
]
return [`${type}:${components.sort().join(',')}`]
})
.sort()
.join('&')
}

export const getQueryBitSets = (
componentRegistry: ComponentRegistry,
conditions: QueryConditions<any>
) => {
return conditions.map((condition) => ({
type: condition.type,
bitset: new BitSet(
condition.components.map((c) => componentRegistry[c as string])
),
}))
}

export class QueryBuilder<E> {
T!: E

Expand Down
Loading

0 comments on commit 9096041

Please sign in to comment.