3mcd / javelin Goto Github PK
View Code? Open in Web Editor NEWECS and multiplayer game development toolkit
Home Page: https://javelin.games
ECS and multiplayer game development toolkit
Home Page: https://javelin.games
It would be useful to have a query filter that tests for the absence of a component. For example, I'm experimenting with maintaining an entity hierarchy datastructure and using components to declare the hierarchy and a system to enforce it. For this system, I'd like any entity without a Child
component to be considered a 'root' entity and added to the root node directly. To do so I'll need to filter by entities without the Child
component (not just detached, as they never had it) to add them to the tree.
I don't think this can be implemented as a custom filter as it appears that custom filters require the presence of their topic Component type, which is obviously incompatible with a not
filter.
Currently systems are registered via the createWorld
factory:
const world = createWorld([systemA, systemB]);
It would be useful to be able to add a system after the world is instantiated:
world.addSystem(systemC);
Defining component initializers can be a little tedious. It seems that with the schema information, it should be possible to provide a default component initializer with full typing support:
const defaultComponentInitializer<S extends Schema> = (component: ComponentType<S, any>, props: PropsOfSchema<S>) => {
Object.assign(component, props);
};
... or similar, such that initializing a component without a user-defined initializer is simply
const Position = createComponentType({
type: 1,
schema: {
x: number,
y: number,
z: number,
},
});
world.spawn(world.component(Position, { x: 0, y: 3, z: 12 }));
What do you think? I'd happily contribute the code if you agree with the concept.
Is there a lifecycle hook or a mechanism that I can tap into to listen every change in the world?
My case is to cache all entities into localStorage at specific interval so it can resume the simulation when browser accidently closed the tab or crashed.
(maybe something like queryAll()
entities ๐ค, don't know if it's good idea)
I'd like to have an idiomatic way of handling events with Javelin ECS. Pulling off events in a "pure" fashion in ECS is hard.
I propose we pass events as another argument to systems so they can remain as self-contained as possible, without relying on a global/singleton event bus.
Systems can target other systems using a new method, world.dispatch
.
Example API:
input.ts
import { physics } from "./physics";
function input(dt: number, world: World) {
const playerEntity = getPlayerEntity(world);
if (keyboard.space) {
world.dispatch(physics, { type: "apply_force", entity: playerEntity, force: [0, 0, 10] });
}
}
physics.ts
type PhysicsEvent = { type: "apply_force", entity: number, force: Vector3 }
const engine = new PhysicsEngine();
const bodies = new Map<number, PhysicsEngineBody>();
function physics(dt: number, world: World, events: PhysicsEvent[]) {
events.forEach(e => {
if (e.type === "apply_force") {
const body = bodies.get(e.entity);
engine.applyForce(body, e.force);
}
});
// ...
}
Per-client component filtering can be done by managing a separate MessageProducer
for each connected client. However, there is no way to narrow the components any further than by their type, so all components that match the type
field are eligible for the next reliable/unreliable update.
I'd like to have a way to only select components that meet a certain criteria, e.g.
function createMessageProducerForClient(client: Client) {
return createMessageProducer({
components: [
// always send this client's position
{
query: query(playerOfClient(client)(Player), Position),
select: [Position],
priority: Infinity
},
// send dynamic (moving) positions most of the time
{
query: query(dynamic(Position)),
select: [Position],
priority: 100
},
// send static positions only when they change (reliably)
{
query: query(static(Position)),
select: [Position]
},
]
})
}
I'm currently looking into integration of Javelin ECS with React, specifically react-three-fiber. I saw #104 , which was a helpful starting point - however there are some significant performance concerns around calling setState
every game step. React reconciliation begins to become a bottleneck.
I'd like to set up a collection of hooks which help organize a tree of React components to render the entities of my game, including utilization of observed component changes to strategically update components which don't change as frequently. To that end my goal is to create at least the following:
function CharacterRenderer() {
// this hook keeps an updated list of entity IDs which match the provided
// components. it doesn't trigger re-renders until the entity list changes.
const entities = useEntities(Character, Position);
return <>{entities.map(id => <CharacterEntity id={id} key={id} />)}</>;
}
// memoization ensures this component only re-renders when
// internal data changes
const CharacterEntity = React.memo(({ id }) => {
// this hook just stores a component reference retrieved from World,
// saving us a lookup each frame. We rely on `useEntities` in the parent
// to ensure that this component is never rendered with an entity which
// does not have Position, otherwise an error would throw here
const position = useComponent(id, Position);
// this hook automatically re-renders when the observed component is modified
const character = useObservedComponent(id, Character);
const ref = useRef();
// this hook runs every frame (provided by react-three-fiber) and
// will handle frequently-changing properties like transforms without
// triggering a re-render at all
useFrame(() => {
ref.current.position.set(position.x, position.y, position.z);
});
return <mesh ref={ref}>{character.model === 'red' ? <RedGeometry /> : <BlueGeometry />}</mesh>;
});
I believe the useComponent
and useObservedComponent
hooks could be done today, but I believe in order to create an efficient useEntities
, I'll need to have some sort of understanding of when an entity first matches a query, and when it no longer matches the query. An event-based approach might be the most convenient.
A naive approach might be to add a callback to each Archetype for when an entity is added or removed, then have the Queries register callbacks for their Archetypes and emit events when things change. The naive part is that if an Entity is moved between two Archetypes which both match a Query, it would issue the removed and added events immediately after each other - but while it's not ideal it is at least still correct and probably sufficient as a first pass and could probably be corrected with a bit of logic in the Query.
I'm trying to create a thing that works like react-redux to allow react components render things into the canvas (using react-pixi-fiber) based on data inside the ECS world.
The pseudo-code is
import { useContext, cloneElement } from "react"
import { createComponentFilter, query, World } from "@javelin/ecs"
const connect = (component, selector: ReturnType<query>, topic) => (WrappedReactComponent) => {
const world = useContext(ContextContainsWorld)
// there will be lots of components of same type to be render, using same React component
// so we return an array
const reactComponentsToRender = []
for (const [entity, [...data]] of selector(world)) {
reactComponentsToRender.push(<WrappedComponent data={data} topic={topic} />)
}
return (reactOwnProps) => {
return reactComponentsToRender.map(element => cloneElement(element, reactOwnProps))
}
}
In this way, we can create a <Pawn />
component, and render 10 Pawn if there are 10 PawnComponent in the ECS world.
Use it like:
import Position from "../ecs/components/position"
import RightClickTopic from "../ecs/topics/rightClick"
function Pawn(props) {
return // some react-pixi-fiber components
// ... onClick={() => props.topic.push(message)} ...
}
const visible = createComponentFilter<typeof Position>(() => component =>
component.x >= 0 && component.x <= 800 && component.y >= 0,
)
const culledPositions = query(visible(Position))
export default connect(Position, culledPositions, RightClickTopic)(Pawn)
Do you use react and redux? What do you think?
One possible problem is that I'm not calling query inside a system...
Another problem is that I need to make data immutable, so react will rerender iff data is changed.
Hi there! I've been building my own TypeScript ECS, but I found Javelin and I agree with basically all the implementation decisions, so I'd like to use it!
Javelin does seem to have one opinion I'm having a bit of trouble adjusting to - it appears that all components must be serializable (just based on schema constraints).
I like that a lot in principle. Has a lot of great implications for networking or webworker integration.
I'm curious though, how live state like a physics RigidBody would be best stored, accessed, and disposed.
In other ECS, like ecsy for instance, there's a concept of a "State Component" which lives only at runtime. In my own implementation I had that concept, so to add a RigidBody you might attach a serializable BodyConfig
component, and then the System would query BodyConfig, Not(Body)
entities and add a Body
to those on frame 1 which stores a reference to the actual RigidBody from the physics simulation library.
When the entity is destroyed, or the body should otherwise be removed, the world would remove the BodyConfig
component so that it's left with just Body
- and the system would have another query for Body, Not(BodyConfig)
which disposes the rigidbody and removes the Body
.
There are some downsides to that approach (including forgetting to do the cleanup step), and it's a bit obtuse to learn. But it does have nice properties of easily and efficiently associating non-serializable state to an entity utilizing the ECS framework.
How would you recommend dealing with object references like that? I noticed in one of the code examples in the docs you have a getBodyByEntity
function - is that the idea, just to store things in global state and lookup using entity IDs?
It can't be opened...
Your lib seems very promising, I'm learning to use it. Thank you for creating and maintaining this.
This release includes the new world.addSystem
method to add systems after a world has already been initialized.
Currently following code will have an any type for the position
import { createComponentType, string, number } from '@javelin/ecs';
export const VisibleItem = createComponentType({
name: 'VisibleItem',
type: 1,
schema: {
x: number,
y: number,
texture: string,
},
initialize: (position, x = 0, y = 0) => {
position.x = x;
position.y = y;
},
});
Is it possible to properly type initialize?: I;
using information from the schema: S;
?
/create release
Might be the second of a couple of issues I'll file as I begin digging into the library.
How do you feel about automatically assigning an incrementing ID to components? It can be a bit of a pain to have to manually manage something which would be pretty trivial for the computer to track.
Since components are already registered with the World
, that seems like it would be a good place to automatically provide it a unique ID. That way you could conceivably even import third-party components from libraries, register them to the World
, and their IDs would be guaranteed unique with your own components. Right now it would be difficult to pull in exotic components and be confident your manually selected IDs are not colliding with theirs.
I'm storing tilemap data using the Tile component, and treat them as a list to render them, this works fine.
But if I want to run A* routing algorithm on the tilemap, I have to treat them as 2D array again, but the entity list is a 1D array now...
So I'd like to put a number
field in the component to store 4 entity ids, which is in the NSWE of this entity.
So in the map system, I can use this information to reconstruct the 2D tilemap array.
Is this approach OK?
For a 1.0 release, we should have a developer tool that a user can embed into a web application to debug local and remote ECS instances. At a bare minimum, the tool would receive snapshots of the ECS state at regular intervals.
Is there a way to remove all world resources (systems, entities, componentTypes)?
Maybe something like world.dispose()
. My use case is for transitioning between different world.
Creating release for 0.19.3
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.