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 TypeScript Entity-Component System (ECS) for Node and web browsers.
ECS is a pattern commonly used in game development to associate components (state) with stateless entities (game objects). Systems then operate on collections of entities of shared composition.
For example, a system could add a Burn
component to entities with Position
and Health
components when their position intersects with a lava pit.
Entities are integers. Components are associated with entities inside of a World
.
import { createWorld } from "@javelin/ecs"
const world = createWorld()
Entities are created with the world.create
method.
const entity = world.create([
{ _t: 1, x: 0, y: 0 }, // Position
{ _t: 2, x: 0, y: 0 }, // Velocity
])
Components are just plain objects; unremarkable, 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 }
Entities can be removed (and all components subsequently de-referenced) via the world.destroy
method:
world.destroy(entity)
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 = world.create([position])
Components created via factory are automatically pooled; however, you must manually release the component back to the pool when it should be discarded:
world.destroy(entity)
Position.destroy(position)
World
can do this automatically if you register the component factory via the world.registerComponentFactory
method.
world.registerComponentFactory(Position)
world.destroy(entity) // position automatically released
A system is just a function executed each simulation tick. Systems execute queries to access entities' components.
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 world:
for (const [position, player] of world.query(players)) {
// render each player with a name tag
draw(position, player.name)
}
Queried components are readonly by default. A mutable copy of a component can be obtained via mut(ComponentType)
.
import { query, mut } from "@javelin/ecs"
const burning = query(mut(Health), Burn)
for (const [health, burn] of world.query(burning)) {
health.value -= burn.damagePerTick
}
Components are versioned as alluded to earlier. world.mut
simply increments the component's _v
property. If you are optimizing query performance and want to conditionally mutate a component (i.e. you are using a generic query), you can manually call world.mut(component)
to obtain a mutable reference, e.g.:
import { query, mut } from "@javelin/ecs"
const burning = query(Health, Burn)
for (const [health, burn] of world.query(burning)) {
world.mut(health).value -= burn.damagePerTick
}
changed
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 { changed, query } from "@javelin/ecs"
// ...
const healthy = query(Player, Health).filter(changed(Health))
for (const [health] of world.query(healthy)) {
// `health` has changed since last tick
}
A query can take one or more filters as arguments to filter
.
query.filter(changed, awake, ...);
The ECS also provides the following filters
committed
ignores "ephemeral" entities, i.e. entities that were created or destroyed last tickcreated
detects newly created entitiesdestroyed
detects recently destroyed entitiestag
isolates entities by tags, which are discussed belowEntities can be tagged with bit flags via the world.addTag
method.
enum Tags {
Awake = 1,
Dying = 2,
}
world.addTag(entity, Tags.Awake | Tags.Dying)
The world.hasTag
method can be used to check if an entity has a tag. world.removeTag
removes tags from an entity.
world.hasTag(entity, Tags.Awake) // -> true
world.hasTag(entity, Tags.Dying) // -> true
world.removeTag(entity, Tags.Awake)
world.hasTag(entity, Tags.Awake) // -> false
tag
produces a filter that will exclude entities which do not have the provided tag(s):
enum Tags {
Nasty = 2 ** 0,
Goopy = 2 ** 1,
}
const nastyAndGoopy = createQuery(Player).filter(tag(Tags.Nasty | Tags.Goopy))
for (const [player] of world.query(nastyAndGoopy)) {
// `player` belongs to an entity with Nasty and Goopy tags
}
The tuple of components yielded by world.query()
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 s of world.query(shocked)) {
results.push(s)
}
// 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 shocked = []
for (const [enemy] of world.query(shocked)) {
shocked.push(enemy)
}
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 created between the first and second queries.
const moved = world.query(createQuery(Position).filter(changed))
for (const [position] of world.query(moved)) // 100 iterations
for (const [position] of world.query(moved)) // 0 iterations
The solution is to simply use a unique filter per query.
const moved1 = world.query(createQuery(Position).filter(changed))
const moved2 = world.query(createQuery(Position).filter(changed))
for (const [position] of world.query(moved1)) // 100 iterations
for (const [position] of world.query(moved2)) // 100 iterations
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
FAQs
Unknown package
The npm package @javelin/ecs receives a total of 126 weekly downloads. As such, @javelin/ecs popularity was classified as not popular.
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.