diff --git a/cover.png b/cover.png new file mode 100644 index 00000000..69fed502 Binary files /dev/null and b/cover.png differ diff --git a/packages/arancini/README.md b/packages/arancini/README.md index 118ee8e3..63aa9d4f 100644 --- a/packages/arancini/README.md +++ b/packages/arancini/README.md @@ -1,23 +1,28 @@ -# arancini +![cover](https://raw.githubusercontent.com/isaac-mason/arancini/main/packages/arancini/cover.png) -Arancini is a JavaScript object based Entity Component System. +[![Version](https://img.shields.io/npm/v/arancini?style=for-the-badge)](https://www.npmjs.com/package/arancini) +![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/isaac-mason/arancini/release.yml?style=for-the-badge) +[![Downloads](https://img.shields.io/npm/dt/arancini.svg?style=for-the-badge)](https://www.npmjs.com/package/arancini) +[![Bundle Size](https://img.shields.io/bundlephobia/min/arancini?style=for-the-badge&label=bundle%20size)](https://bundlephobia.com/result?p=arancini) + +# arancini - an object-based Entity Component System (ECS) library for JavaScript ``` > npm i arancini ``` -- 💙 ‎ TypeScript friendly -- 💪 ‎ Flexible and extensible +- 🍱 ‎ Entities are regular objects, components are properties - 🔍 ‎ Fast reactive queries powered by bitsets -- 🍃 ‎ Zero dependencies -- 🖇 ‎ [Easy integration with React](https://github.com/isaac-mason/arancini/tree/main/packages/arancini-react) - +- 🧠 ‎ Define Systems with arancini, or bring your own system logic +- 🧩 ‎ Framework agnostic, plug arancini into whatever you like +- ⚛️ ‎ [Easy integration with React](https://github.com/isaac-mason/arancini/tree/main/packages/arancini-react) +- 💙 ‎ TypeScript friendly ## Introduction Arancini is an Entity Component System (ECS) library for JavaScript. It aims to strike a balance between ease of use and performance. -In arancini, entities are regular javascript objects, and components are properties on those objects. You can use arancini to structure demanding applications such as games and simulations. +You can use arancini to structure demanding applications such as games and simulations. If you aren't familiar with Entity Component Systems, this is a good read: https://github.com/SanderMertens/ecs-faq @@ -30,23 +35,23 @@ TL;DR - Entity Component Systems are a data-oriented approach to structuring app A world represents your game or simulation. It contains entities, updates queries, and runs systems. ```ts -import { World } from "arancini"; +import { World } from 'arancini' // (optional) define a type for entities in the world type Entity = { - position?: { x: number; y: number }; - health?: number; - velocity?: { x: number; y: number }; - inventory?: { items: string[] }; -}; + position?: { x: number; y: number } + health?: number + velocity?: { x: number; y: number } + inventory?: { items: string[] } +} // create a world const world = new World({ - components: ["position", "health", "velocity", "inventory"], -}); + components: ['position', 'health', 'velocity', 'inventory'], +}) // initialise the world -world.init(); +world.init() ``` > **Note:** @@ -59,9 +64,9 @@ In arancini, entities are regular objects, and components are properties on thos You can use `world.create` to create an entity from any object. This adds the entity to the world, and adds any components that are defined on the object. ```ts -const playerEntity = { position: { x: 0, y: 0 } }; +const playerEntity = { position: { x: 0, y: 0 } } -world.create(playerEntity); +world.create(playerEntity) ``` ### 📦 Adding and Removing Components @@ -70,10 +75,10 @@ To add and remove components from an entity, you can use `world.add`, `world.rem ```ts /* add a component */ -world.add(playerEntity, "health", 100); +world.add(playerEntity, 'health', 100) /* remove a component */ -world.remove(playerEntity, "health"); +world.remove(playerEntity, 'health') /* add and remove multiple components with a partial entity */ world.update(playerEntity, { @@ -81,16 +86,16 @@ world.update(playerEntity, { velocity: { x: 1, y: 0 }, // remove a component poisioned: undefined, -}); +}) /* add and remove multiple components with an update callback */ world.update(playerEntity, (e) => { // add a component - e.velocity = { x: 1, y: 0 }; + e.velocity = { x: 1, y: 0 } // remove a component - delete e.poisioned; -}); + delete e.poisioned +}) ``` ### 🗑 Destroying Entities @@ -98,7 +103,7 @@ world.update(playerEntity, (e) => { To destroy an entity, use `world.destroy`. ```ts -world.destroy(playerEntity); +world.destroy(playerEntity) ``` > **Note:** Destroying an entity does not remove any properties/components from the entity object, it just removes the entity from the world and all queries. @@ -108,7 +113,7 @@ world.destroy(playerEntity); You can query entities based on their components with `world.query`. Queries are reactive, they will update as entities in the world change. ```ts -const monsters = world.query((q) => q.all("health", "position", "velocity")); +const monsters = world.query((q) => q.all('health', 'position', 'velocity')) ``` > **Note:** Arancini dedupe queries with the same filters, so you can create multiple of the same query without performance penalty! @@ -117,35 +122,35 @@ Arancini supports `all`, `any`, and `none` filters. The query builder has some a ```ts const monsters = world.query((q) => - q.all("health", "position").any("skeleton", "zombie").none("dead"), -); + q.all('health', 'position').any('skeleton', 'zombie').none('dead') +) const monsters = world.query((entities) => entities - .with("health", "position") - .and.any("skeleton", "zombie") - .but.not("dead"), -); + .with('health', 'position') + .and.any('skeleton', 'zombie') + .but.not('dead') +) ``` You can iterate over queries using a `for...of` loop (via `Symbol.iterator`). This will iterate over entities in reverse order, which must be done to avoid issues when making changes that will remove entities from queries. You can also use `query.entities` directly. ```ts -const monsters = world.query((q) => q.all("health", "position", "velocity")); +const monsters = world.query((q) => q.all('health', 'position', 'velocity')) const updateMonsters = () => { /* iterates over entities in reverse order */ for (const monster of monsters) { if (monster.health <= 0) { - world.destroy(monster); + world.destroy(monster) } - monster.position.x += monster.velocity.x; - monster.position.y += monster.velocity.y; + monster.position.x += monster.velocity.x + monster.position.y += monster.velocity.y } -}; +} -console.log(monsters.entities); +console.log(monsters.entities) ``` ### 📡 Query Events @@ -155,17 +160,17 @@ Queries emit events when entities are added or removed. These events are be emitted after internal structures are updated to reflect the change, but before destructive changes are made to entities, e.g. removing components. ```ts -const query = world.query((e) => e.has("position")); +const query = world.query((e) => e.has('position')) const handler = (entity: Entity) => { // ... -}; +} -query.onEntityAdded.add(handler); -query.onEntityAdded.remove(handler); +query.onEntityAdded.add(handler) +query.onEntityAdded.remove(handler) -query.onEntityRemoved.add(handler); -query.onEntityRemoved.remove(handler); +query.onEntityRemoved.add(handler) +query.onEntityRemoved.remove(handler) ``` ### 👀 Ad-hoc Queries @@ -175,9 +180,9 @@ You can use `world.filter` and `world.find` to get ad-hoc query results. This is useful for cases where you want to get results infrequently, without the cost of evaluating a reactive query as the world changes. ```ts -const monsters = world.filter((e) => e.has("health", "position", "velocity")); +const monsters = world.filter((e) => e.has('health', 'position', 'velocity')) -const player = world.find((e) => e.has("player")); +const player = world.find((e) => e.has('player')) ``` ### 🧠 Systems @@ -190,15 +195,15 @@ While arancini has built-in support for systems, it's worth noting that there's ```ts class MovementSystem extends System { - moving = this.query((e) => e.has("position", "velocity")); + moving = this.query((e) => e.has('position', 'velocity')) onUpdate(delta: number, time: number) { for (const entity of this.moving) { - const position = entity.get("position"); - const velocity = entity.get("velocity"); + const position = entity.get('position') + const velocity = entity.get('velocity') - position.x += velocity.x; - position.y += velocity.y; + position.x += velocity.x + position.y += velocity.y } } @@ -212,10 +217,10 @@ class MovementSystem extends System { } // Register the system -world.registerSystem(ExampleSystem); +world.registerSystem(ExampleSystem) // Use `world.step()` to run all registered systems -world.step(1 / 60); +world.step(1 / 60) ``` #### System priority @@ -223,8 +228,8 @@ world.step(1 / 60); Systems can be registered with a priority. The order systems run in is first determined by priority, then by the order systems were registered. ```ts -const priority = 10; -world.registerSystem(MovementSystem, priority); +const priority = 10 +world.registerSystem(MovementSystem, priority) ``` #### Required system queries @@ -233,10 +238,10 @@ System queries can be marked as 'required', which will cause `onUpdate` to only ```ts class ExampleSystem extends System { - requiredQuery = this.query((q) => q.has("position"), { required: true }); + requiredQuery = this.query((q) => q.has('position'), { required: true }) onUpdate() { - const { x, y } = this.requiredQuery.first; + const { x, y } = this.requiredQuery.first } } ``` @@ -249,10 +254,10 @@ Singletons are useful for accessing components that are expected to exist on a s ```ts class ExampleSystem extends System { - player = this.singleton("player", { required: true }); + player = this.singleton('player', { required: true }) onUpdate() { - console.log(player); + console.log(player) } } ``` @@ -265,10 +270,10 @@ Systems can be attached to other systems with `this.attach`. This is useful for ```ts class ExampleSystem extends System { - otherSystem = this.attach(OtherSystem); + otherSystem = this.attach(OtherSystem) onUpdate() { - this.otherSystem.foo(); + this.otherSystem.foo() } } ``` diff --git a/packages/arancini/cover.png b/packages/arancini/cover.png new file mode 100644 index 00000000..2503676e Binary files /dev/null and b/packages/arancini/cover.png differ