Security News
GitHub Removes Malicious Pull Requests Targeting Open Source Repositories
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
@javelin/ecs
Advanced tools
@javelin/ecs
A simple, performant ECS for TypeScript.
Run yarn perf
to run performance tests.
Example perf on 2018 MacBook Pro where 350k entities are iterated per tick at 60 FPS:
========================================
perf_storage
========================================
entities | 300000
components | 4
queries | 4
ticks | 100
iter_tick | 353500
avg_tick | 13.7ms
========================================
perf_storage_pooled
========================================
entities | 300000
components | 4
queries | 4
ticks | 100
iter_tick | 353500
avg_tick | 14.62ms
Entities are integers. Components are associated with entities by a container cleverly named Storage
.
import { createStorage } from "@javelin/ecs"
const storage = createStorage()
Entities are created with the storage.create()
method.
const entity = storage.create([
{ _t: 1, x: 0, y: 0 }, // Position
{ _t: 2, x: 0, y: 0 }, // Velocity
])
Components are simple JS objects other than a few reserved properties:
_t
is a unique integer identifying the component's type_e
references the entity the component is actively associated with_v
maintains the current version of the component, which is useful for change detectionA position component assigned to entity 5
that has been modified three times might look like:
{ _t: 1, _e: 5, _v: 3, x: 123.4, y: 567.8 }
The createComponentFactory
helper is provided to make component creation easier.
import { createComponentFactory, number } from "@javelin/ecs"
const Position = createComponentFactory({
type: 1,
schema: {
x: number,
y: number,
},
})
const position = Position.create()
const entity = storage.create([position])
Components created via factory are automatically pooled, but you must manually release the component back to the pool when it should be discarded:
storage.destroy(entity)
Position.destroy(position)
The ECS can do this automatically if you register the component factory via the storage.registerComponentFactory
method.
storage.registerComponentFactory(Position)
storage.destroy(entity) // position automatically released
The ECS has no concept of systems. A system is just the code in your program that executes queries and reads/writes component state.
Queries are created with the createQuery
function, which takes one or more component types (or factories).
const players = createQuery(Position, Player)
The query can then be executed for a given storage (your game usually has one storage):
for (const [position, player] of players.run(storage)) {
// render each player with a name tag
draw(position, player.name)
}
As alluded to earlier, components are versioned. The version of a component can be incremented using the storage.incrementVersion()
method, which just increments the component's _v
property:
const burning = query(Health, Burn)
for (const [health, burn] of burning.run(storage)) {
health.value -= burn.damagePerTick
storage.incrementVersion(health)
}
A component's version can be useful when you want to query components that have changed since your game's last tick.
createChangedFilter
produces a filter that excludes entities whose components haven't changed since the entity was last iterated with the filter instance. This filter uses the component's version (_v
) to this end.
import { createChangedFilter, query } from "@javelin/ecs"
// ...
const healthy = query(Player, Health)
const changed = createChangedFilter(Health)
for (const [health] of healthy.run(storage, changed)) {
// `health` has changed since last tick
}
A query can take one or more filters as arguments to run
.
bodies.run(storage, changed, awake, ...);
The ECS also provides createAddedFilter
to detect newly added entities, and createTagFilter
to find tagged entities, which are discussed below.
Entities can be tagged with bit flags via the storage.tag
method.
enum Tags {
Awake = 1,
Dying = 2,
}
storage.tag(entity, Tags.Awake | Tags.Dying)
The storage.hasTag
method can be used to check if an entity has a tag. storage.removeTag
removes tags from an entity.
storage.hasTag(entity, Tags.Awake) // -> true
storage.hasTag(entity, Tags.Dying) // -> true
storage.removeTag(entity, Tags.Awake)
storage.hasTag(entity, Tags.Awake) // -> false
createTagFilter
produces a filter that will exclude entities which do not have the provided tag(s):
enum Tags {
Nasty = 1, // 2^0
Goopy = 2, // 2^1
}
const nastyAndGoopy = createTagFilter(Tags.Nasty | Tags.Goopy)
for (const [player] of createQuery(Player).run(storage, nastyAndGoopy)) {
// `player` belongs to an entity with Nasty and Goopy tags
}
When interfacing with third-party libraries with their own state, you will often want to clean up library objects when an entity is removed. This is pretty easy to do with tags and queries.
enum Tag {
Removing = 1,
}
const bodies = query(Body)
const removed = createTagFilter(Tag.Removing)
const p2BodiesByEntityId = {}
function physicsSystem() {
for (const [body] of bodies.run(storage, removed)) {
p2World.removeBody(p2BodiesByEntityId[body._e])
}
}
const players = query(Body, Health)
function damageSystem() {
for (const [body, health] of players.run(storage)) {
if (health <= 0) {
storage.addTag(body._e, Tag.Removing)
}
}
}
The tuple of components yielded by query.run()
is re-used each iteration. This means that you can't store the results of a query for use later like this:
const results = []
for (const r of query.run(storage)) {
results.push(r)
}
// Every index of `results` corresponds to the same array!
The same applies to Array.from()
, or any other method that expands an iterator into another container. If you do need to store components between queries (e.g. you are optimizing a nested query), you could push the components of interest into a temporary array, e.g.
const positions = []
for (const [position] of query.run(storage)) {
positions.push(position)
}
Try nested queries before you prematurely optimize. Iteration is pretty fast thanks to Archetypes, and a nested query will probably perform fine.
A filter does not have access to the query that executed it, meaning it can't track state for multiple queries. For example, if two queries use the same changed
filter, no entities will be yielded by the second query unless entities were added between the first and second queries.
const changed1 = changed()
for (const [position] of query(Position).run(storage, changed1)) {
// 100 iterations
}
// No new entities...
for (const [position] of query(Position).run(storage, changed1)) {
// 0 iterations
}
The solution is to simply use a unique filter per query.
const changed1 = changed()
const changed2 = changed()
for (const [position] of query(Position).run(storage, changed1)) {
// 100 iterations
}
for (const [position] of query(Position).run(storage, changed2)) {
// 100 iterations
}
FAQs
Unknown package
We found that @javelin/ecs demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
Security News
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
Security News
Node.js will be enforcing stricter semver-major PR policies a month before major releases to enhance stability and ensure reliable release candidates.