1.5.5 • Published 1 month ago

@prsm/ngn v1.5.5

Weekly downloads
-
License
-
Repository
-
Last release
1 month ago

ngn

An ECS framework for 2d or 3d web projects.

Installation

pnpm install @prsm/ngn

API overview

createWorld

const {
  world,
  createEntity,
  getEntity,
  onEntityCreated,
  query,
  addSystem,
  removeSystem,
  start,
  stop,
  step,
} = createWorld();
  • world

    Stores all the entities and components. Is passed to all systems. Contains a time object that looks like:

    • world.time.delta - time since last frame in ms.
    • world.time.scale - time scale (default: 1, valid: 0 - 1).
    • world.time.elapsed - time since start was called in ms.
    • world.time.elapsedScaled - time since start was called, scaled by time.scale in ms.
    • world.time.fps - frames per second.
  • createEntity

    const {
      id,
      addTag,
      removeTag,
      getTag,
      addComponent,
      hasComponent,
      getComponent,
      removeComponent,
      destroy,
    } = createEntity({ optional: "default values" });

    Forcefully setting the entity ID

    You can forcefully set the entity ID by providing it as the second optional argument to createEntity. This is a feature that's probably not very useful in the context of this library alone, but this is a critical feature that @prsm/ngn-net relies on. An authoritative server must be able to assign IDs to entities.

    // this entity will have id 1
    const firstEntity = createEntity({});
    // now this entity has id 1, and `firstEntity` has id 2
    const secondEntity = createEntity({}, 1);
    // this entity has id 3
    const thirdEntity = createEntity({});
    • addTag

      Adds a tag to the entity. Tags are only useful for querying entities. An entity can only have one tag.

      entity.addTag("coin");
    • removeTag

      Removes the tag from the entity.

      entity.removeTag();
    • getTag

      Returns the tag of the entity.

      const tag = entity.getTag();
    • addComponent

      Adds a component to the entity. Components are functions that return an object. An entity can only have one of each type of a component.

      const Position = () => ({ x: 50, y: 50 });
      const Velocity = () => ({ x: 0, y: 0 });
      entity.addComponent(Position).addComponent(Velocity);
      
      // entity:
      // {
      //   ...,
      //   components: [
      //     { x: 50, y: 50 }, <-- Position
      //     { x: 0, y: 0 },   <-- Velocity
      //   ],
      // }
    • hasComponent

      Returns true if the entity has the component.

      const hasPosition = entity.hasComponent(Position);
    • getComponent

      Returns the component of the entity.

      const position = entity.getComponent<typeof Position>(Position);
    • removeComponent

      Removes the component from the entity. Provide either the component function or the string name of the component (.name property).

      entity.removeComponent(Position);
      // is the same as:
      entity.removeComponent("Position");
    • destroy

      Destroys the entity. Removes it from the world.

      entity.destroy();
  • getEntity

    Returns the entity with the given ID.

    const entity = getEntity(1);
  • query

    Queries the world for entities with the given tags and components. Returns a function that, when called, is given an array of entities that match the query. query accepts an object with the following properties:

    {
      and: [], // matched Entities will have all of these components
      or: [], // matched Entities will have any of these components
      not: [], // matched Entities will have none of these components
      tags: [], // matched Entities will have any of these tags
    }
    createEntity().addComponent(Position).addComponent(Velocity);
    createEntity().addComponent(Position).addComponent(Velocity).addComponent(Dead);
    
    const movables = query({ and: [Position, Velocity], not: [Dead] });
    
    movables((entities) => {
      for (const entity of entities) {
        const position = entity.getComponent(Position);
        const velocity = entity.getComponent(Velocity);
    
        position.x += velocity.x;
        position.y += velocity.y;
      }
    });

    For optimum performance, query results are cached while entity state is clean. When an entity is created, destroyed, or has a component added or removed, the cache is invalidated.

  • addSystem

    Adds a system to the world. Systems are either:

    • A function that receives the world as an argument. It does not have to return anything. The world is mutable.
    • An object with an update function that receives the world as an argument. It does not have to return anything. The world is mutable.

    Systems are called in the order they were added.

    const movableSystem = (world) => {
      movables((entities) => {
        // ...
      });
    };
    
    addSystem(movableSystem);
  • removeSystem

    Removes a system from the world. Preserves the order of the remaining systems.

    removeSystem(movableSystem);

    defineMain

    Defines the main program loop. The callback will be called every frame once start is called.

    defineMain(() => {
      // ..
    });
  • start

    Starts the main program loop. Does not do anything other than call the callback provided to defineMain.

    You can use your own loop instead of this one if you prefer, but the builtin loop does things like calculate fps and frame delta for you. These values are stored in world.time. If you create your own loop, it would be a good idea to calculate these values yourself and populate world.time with them.

    start();
  • stop

    Stops the main program loop (which was defined by passing it to defineMain).

    // if gameover, or something
    stop();
  • step

    Calls all systems once. Passes the world to each system. You should do this in your main program loop, e.g.:

    const main = () => {
      step();
    };
    
    defineMain(main);
    
    start();
    
    // later on:
    stop();

Extras

Some optional extras are available that you may find useful.

  • inputSystem

    This input system recognizes keyboard, mouse and gamepad input and has a simple API.

    import { input, inputSystem } from "ecs-ngn";
    
    addSystem(inputSystem);

    ButtonState

    For keyboard and mouse devices, when retrieving the state of a button will return a ButtonState object with the following shape:

    export interface ButtonState {
      // This is true for one frame only, the frame the button was pressed.
      justPressed: boolean;
      // This is true for as long as the button is being pressed.
      pressed: boolean;
      // This is true for one frame only, the frame the button was released.
      justReleased: boolean;
    }

    Gamepads return a GamepadButtonState which includes all of the same properties as ButtonState, with the addition of:

    export interface GamepadButtonState extends ButtonState {
      // This is true for as long as the button is being touched (e.g. the touchpad on a PS5 controller)
      touched: boolean;
      // This is the value of the button, between 0 and 1. For triggers, this is the amount the trigger is pressed.
      value: number;
    }

    So, for example, if you were handling the jumping of a character, which should happen as soon as the jump button is pressed, you would check for justPressed:

    if (input.keyboard.getKey("Space").justPressed) {
      // jump!
    }

    API

    • input is an object that looks like:

      • input.mouse

        • input.mouse.useMapping(m: MouseMapping)

          Defines a human-readable mapping to mouse buttons and axes.

          By default, the StandardMouse mapping is used and you probably don't need to call this.

        • input.mouse.getButton(): { pressed: boolean, justPressed: boolean, justReleased: boolean }

          Returns the state of a mouse button, e.g.:

          const { pressed, justPressed, justReleased } = input.mouse.getButton("Mouse1");
        • input.mouse.getAxis(axis: string): number

          Returns the value of a mouse axis. With the StandardMouse mapping, the axes are: Horizontal, Vertical, and Wheel.

        • input.mouse.getPosition(): [x: number, y: number]

          Returns the position of the mouse.

      • input.keyboard

        • input.keyboard.useMapping(m: KeyboardMapping)

          Defines a human-readable mapping to keyboard keys.

          By default, the StandardKeyboard mapping is used and you probably don't need to call this, unless you want to rename a key, e.g.:

            const MyKeyboardMapping = (): KeyboardMapping => {
              return {
                // ...
                [KeyboardKey.Space]: "FireLazerz",
              }
            };
          
            input.keyboard.useMapping(MyKeyboardMapping);
            input.keyboard.getKey("FireLazerz");
        • input.keyboard.getKey(b: string): { pressed: boolean, justPressed: boolean, justReleased: boolean }

          Returns the state of a keyboard key. The key should be the human readable name value defined in the mapping used.

      • input.gamepad

        • input.gamepad(index: number).useMapping(m: GamepadMapping)

          Defines a human-readable mapping to gamepad buttons and axes.

          The default mapping is assigned by inspecting the Gamepad.id property.

          PlayStation5, Xbox, and SCUF Vantage 2 mappings are included and handled automatically. PRs that add additional mappings are welcome.

        • input.gamepad(index: number).getButton(button: string): { pressed: boolean, touched: boolean, value: number, justPressed: boolean, justReleased: boolean }

          Returns the state of a gamepad button.

        • input.gamepad(index: number).getAxis(axis: string): number

          Returns the value of a gamepad axis.

  • logSystem

    This log system takes advantage of world.time.delta to expire log entries over time. By default, this is 10 seconds, but this is configurable.

    The whole point of this system is to draw debug messages to the canvas, but have them disappear after a while.

    import { createLogSystem } from "ecs-ngn";
    
    const logSystem = createLogSystem({ maxLifetime: 5_000 });
    
    addSystem(logSystem);
    
    logSystem.log("some useful debug message");
    
    logSystem.expiringLogs.forEach(({ message }, index) => {
      drawTextToCanvas(message, { x: 0, y: index * 20 });
    });
1.5.5

1 month ago

1.5.3

11 months ago

1.5.2

11 months ago

1.5.1

11 months ago

1.5.0

11 months ago