npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@kadiryazici/ecs

v1.1.1

Published

This was a challange for me, I have never created an ECS before and wanted to test myself. It doesn't provide `THE BEST PERFORMANCE` but I think it is easy to understand.

Downloads

2

Readme

An Entity-Component-System inspired by Bevy Engine.

This was a challange for me, I have never created an ECS before and wanted to test myself. It doesn't provide THE BEST PERFORMANCE but I think it is easy to understand.

Installation

npm install @kadiryazici/ecs
pnpm add @kadiryazici/ecs
yarn add @kadiryazici/ecs

Demo

You can view a demo in /demo/main.ts and Online

Components

To create component we use defineComponent function. This function can take undefined or a function that returns an object as parameter. The reason why parameter is a function is not to have reference issues with default states because when a component created, default values are placed if missing.

import { defineComponent } from '@kadiryazici/ecs';

const Velocity = defineComponent(() => ({
   x: 0,
   y: 0,
}));

// Can be used to tag entities, has no state.
const Player = defineComponent();

We created a component, now it's time to create an instance of it, then we will use it in Entities.

// if you give undefined or {}, Velocity will have default state { x: 0, y,: 0 }
const baseVelocity = Velocity.create();
baseVelocity.state; // { x: 0, y: 0 }

// You can override some default values
const baseVelocity = Velocity.create({ x: 50 });
baseVelocity.state; // { x: 50, y: 0 }

You can create a new component instance and state like this but we won't use them like that.

Entities

Entities contain components for systems to query them. To create an entity we use createEntity function.

import { defineComponent, createEntity } from '@kadiryazici/ecs';

const Velocity = defineComponent(() => ({
   x: 0,
   y: 50,
}));

const Name = defineComponent(() => ({
   value: '',
}));

const Player = createEntity()
   .add(Velocity.create({ x: 25 }))
   .add(Name.create({ value: 'Player' }));

Nice! we have created an entity with Name and Velocity component.

But it is better to return it from a function not to create multiple references to the same state.

const createPlayer = () =>
   createEntity()
      .add(Velocity.create({ x: 25 }))
      .add(Name.create({ value: 'Player' }));

We can add or remove component after an entity created.

const Player = createPlayer();
Player.add(Name.create({ value: }));
Player.remove(Name);

World

World is a store of entities. It stores every unique entity in a Set.

import { createWorld } from '@kadiryazici/ecs';

const world = createWorld();

To add an entity to our world we can use add function.

world
   .add(createPlayer());
   .add(
      createEntity()
         .add(Velocity.create())
         .add(Name.create({ value: 'Enemy' })),
   );

Nice! now we know how to create Entities, Worlds and Components, now it's time to learn how to create and run queries.

Query

Queries filter a world of entities by given components and then return States Tuple of Components of found Entities.

You can mutate components' state after iterating query result, that's why components only accept object. It's for mutation references.

import { createQuery } from '@kadiryazici/ecs';

// This query will search for entities that has Name component, and will return an array of tuple: [name][].
const VelocityQuery = createQuery([Name]);

function somethingSystem() {
   /*
      query is an array of tuple of Name components: 
      
      [
         [{ value: 'Player' }],
         [{ value: 'Enemy' }],
      ]
   */
   const query = VelocityQuery.exec(world);

   for (const [name] of query) {
      name.value = 'Now your name is xXxMurdererxXx2010';
   }

   // You can use forEach too, it's just an array.
   // But personally I prefer for...of.
   query.forEach(([name]) => {
      name.value = 'Or you can use forEach, but for...of better.';
   });
}

You can search for multiple components as well.

import { createQuery } from '@kadiryazici/ecs';

const VelocityNameQuery = createQuery([Name, Velocity]);

function somethingSystem() {
   const query = VelocityNameQuery.exec(world);

   for (const [name, velocity] of query) {
      console.log(name.value);
      velocity.x += 3;
      velocity.y -= 3;
   }
}

With

So far we only queried components we want to receive, what if we want to receive Name component of entities that has Velocity component.

For this we can use With modifier.

import { defineComponent, createQuery, With } from '@kadiryazici/ecs';

// Lets create a third component for our queries.
const Bounds = defineComponent(() => ({
   width: 0,
   height: 0,
}));

/*
   First parameter should always be a tuple/array of components we want to receive.
   Other parameters are just spread, you can give as much modifiers as you want.
*/
const NameQueryWithVelocity = createQuery([Name], With(Velocity));

With modifier can get infinite number of component arguments.

import { createQuery, With } from '@kadiryazici/ecs';

const NameQueryWithVelocity = createQuery([Name], With(Velocity, Bounds));

function somethingSystem() {
   const query = NameQueryWithVelocity.exec(world);
   for(const [name] of query) {
      ...
   }
}

Multiple received components and With modifier.

import { createQuery, With } from '@kadiryazici/ecs';

const NameBoundsQuery = createQuery([Name, Bounds], With(Velocity, SomeComponent));

function somethingSystem() {
   const query = NameBoundsQuery.exec(world);
   for (const [name, bounds] of query) {
      something(name.value);
      draw(bounds.width, bounds.height);
   }
}

If you want you can repeat modifiers, they will be merged when the query executes.

It will be With(Shadow, Light, Foot, Head).

import { createQuery } from '@kadiryazici/ecs';

const Query = createQuery([Name, Velocity, Color], With(Shadow, Light), With(Foot, Head));

Without

We know about With modifier and how to use it, what if we want to receive Name component of entities that don't have Velocity and Bounds component.

import { createQuery, Without, With } from '@kadiryazici/ecs';

const NameQuery = createQuery([Name], Without(Velocity, Bounds));

// Can be used multiple times as well
// Will be converted into `Without(Bounds, Velocity)`
const NameQuery = createQuery([Name], Without(Velocity), Without(Bounds));

// Can be mixed with With modifier.
const NameQuery = createQuery([Name], With(RigidBody, Velocity, Bounds), Without(Shadow));

function somethingSystem() {
   const query = NameQuery.exec(world);

   for (const [name] of query) {
      console.log(name.value);
   }
}

Systems

Systems are just functions that run queries and manages their states. You actually learned how to create systems above.

Let's create a System that updates Position by Velocity of Entities that has RigidBody component but don't have FixedBody.

import { createQuery, With, Without, createWorld, createEntity } from '@kadiryazici/ecs';
import type { World } from '@kadiryazici/ecs';

const PositionVelocityQuery = createQuery([Position, Velocity], With(RigidBody), Without(FixedBody));

// Systems are just functions, you can pass them whatever you want.
function movementSystem(world: World, delta: number) {
   const query = PositionVelocityQuery.exec(world);

   for (const [position, velocity] of query) {
      position.x += velocity.x * delta;
      posiyion.y += velocity.y * delta;
   }
}

const world = createWorld();

world.add(
   createEntity()
      .add(Position.new({ x: 50, y: 75 }))
      .add(Velocity.new())
      .add(RigidBody.new()),
);

movementSystem(world, Game.getDeltaTime());

Special Component EntityId

If you also want to receive Entity ID from query you can use this special component.

For example if you want to remove enemies that player shot you need to remove entity from the world.

import { EntityId, World} from '@kadiryazici/ecs';

const BulletPositionsQuery = createQuery([Position], With(Bullet));
const EnemyPositionsQuery = createQuery([Position, EntityId], With(Enemy));

function collisionSystem(world: World) {
   const bulletPositions = PlayerPositionQuery.exec(world);
   const enemyPositions = EnemyPositionsQuery.exec(world);

   for (const [bulletPosition] of bulletPositions) {
      for (const [enemyPosition, enemyEID] of enemyPositions) {
         if (isColliding(bulletPosition.value, enemyPosition.value)) {
            // Bullet hit th enemy, so we remove it.
            world.remove(enemyEID);
         }
      }
   }
}

function gameLoop() {
   ...
   somethingSystem();
   // you can make systems with arguments, you don't have to keep a reference to the world all the time.
   collisionSystem(world);
   ...

   gameLoop();
}

Marking component optional with Optional(component: Component)

Lets assume that you want to receive Name and Velocity states but optionally Player component to check if it is Player. You can do this by creating two separate queries but it isn't nice to use.

For this we have an Optional modifier, you can mark a component as optional and you receive null or state of component.

import { type World, createEntity, defineComponent, Optional } from '@kadiryazici/ecs';

const Name = defineComponent(() => ({ value: '' }));
const Velocity = defineComponent(() => ({ value: new Vec2(0, 0) }));
const Player = defineComponent();

const NameAndVelocityQuery = createQuery([Name, Velocity, Optional(Player)]);

function loggerSystem(world: World) {
   const query = NameAndVelocityQuery.exec(world)

   // player will be undefined or an empty object.
   for (const [name, velocity, player] of query) {
      if(player !== undefined) // Player exists for this entity.
   }
}

Example Project

You can see a little complicated example in my other repo: Bomberman Clone