15.1.1 • Published 5 months ago

@game-vir/entity v15.1.1

Weekly downloads
-
License
(MIT or CC0 1.0)
Repository
github
Last release
5 months ago

@game-vir/entity

An entity system for Pixi.js graphics that lends itself well to game use. Also included are some helpful entity maths.

Reference docs: https://electrovir.github.io/game-vir/entity/

Install

npm i @game-vir/entity

Usage

Use defineEntitySuite to get a defineEntity method for defining entities and an entityStore instance for storing entity instances.

  • defineEntity: use this as the super class of a new entity class definition.

    import {Graphics} from 'pixi.js';
    import {defineEntitySuite, entityPositionParamsShape} from '@game-vir/entity';
    
    const {defineEntity} = defineEntitySuite<{
        /** Optionally provide a Context type. */
        movementSpeed: number;
    }>();
    
    export class Block extends defineEntity({
        key: 'Block',
        paramsShape: entityPositionParamsShape,
        paramsMap: {
            hitbox: {
                x: true,
                y: true,
            },
            view: {
                x: true,
                y: true,
            },
        },
    }) {
        public override update(): void {
            this.params.x += this.context.movementSpeed;
            this.params.y += this.context.movementSpeed;
        }
    
        public override createView() {
            /**
             * View and hitbox position don't need to be manually set their counterparts from params;
             * they will be updated to match them because of the above `paramsMap` definition.
             */
            return {
                view: new Graphics().rect(0, 0, 100, 100).fill('red'),
                /** Hitboxes are optional. */
                hitbox: this.hitboxSystem.createBox({}, 100, 100),
            };
        }
    }
  • entityStore is primarily interacted with via the addEntity and updateAllEntities methods.

    • addEntity: construct a new instance of the given entity class and adds it to the entity store. This is the easies way to construct new entities.

      import {createPixi, defineEntitySuite} from '@game-vir/entity';
      import {Block} from './define-entity.example.js';
      
      const {EntityStore} = defineEntitySuite<{movementSpeed: number}>();
      
      const entityStore = new EntityStore({
          pixi: await createPixi(),
          context: {movementSpeed: 6},
          registeredEntities: [Block],
      });
      
      entityStore.addEntity(Block, {x: 15, y: 20});
    • updateAllEntities: update all entities. This calls the update() method on every entity instance currently within the entity store. This should be called on every game tick or animation frame.

      import {createPixi, defineEntitySuite} from '@game-vir/entity';
      
      const {EntityStore} = defineEntitySuite<{movementSpeed: number}>();
      
      const entityStore = new EntityStore({
          pixi: await createPixi(),
          context: {movementSpeed: 6},
          registeredEntities: [],
      });
      
      entityStore.pixi.ticker.add(() => {
          entityStore.updateAllEntities();
      });

Example

Here's a full usage example. This can be seen in action through the following steps:

  1. Clone the repo.
  2. Run npm ci..
  3. Run cd packages/entity.
  4. Run npm start.
  5. Open the browser link logged to your console.
import {assertWrap} from '@augment-vir/assert';
import {and, defineShape} from 'object-shape-tester';
import {Graphics, GraphicsContext} from 'pixi.js';
import {Angle, createPixi, defineEntitySuite, entityPositionParamsShape, Vector} from '@game-vir/entity';

/** Create an entity suite. */
const {defineEntity, defineLogicEntity, EntityStore} = defineEntitySuite<{movementSpeed: number}>();

/** Define entities. */

/** Define a standard entity (with a view) that bounces back and forth. */
class Block extends defineEntity({
    key: 'Block',
    paramsShape: defineShape(
        and(entityPositionParamsShape, {
            direction: 1,
        }),
    ),
    paramsMap: {
        view: {
            x: true,
            y: true,
        },
    },
}) {
    public static readonly bonkCount = 20;
    public static readonly graphicContext = new GraphicsContext()
        .rect(0, 0, 10, 10)
        .fill('magenta');

    public override update(): void {
        this.params.x += this.context.movementSpeed * this.params.direction;
        this.params.y += this.context.movementSpeed * this.params.direction;

        if (!this.isInBounds({entirely: true})) {
            this.params.direction = -1 * this.params.direction;
            this.createBonk();
        }
    }

    protected createBonk() {
        const degreesPerBonk = 360 / Block.bonkCount;

        for (let i = 0; i < Block.bonkCount; i++) {
            const move = new Vector(
                BlockBonk.moveSpeed,
                new Angle({degrees: degreesPerBonk * i}, {digits: 4}),
                {digits: 4},
            ).toComponents();
            /** Entities can easily create more entities. */
            this.addEntity(BlockBonk, {
                move,
                ticksSinceCreation: 0,
                x: this.view.x,
                y: this.view.y,
            });
        }
    }

    public override createView() {
        const graphic = new Graphics(Block.graphicContext);

        graphic.x = this.params.x;
        graphic.y = this.params.y;

        return {
            view: graphic,
        };
    }
}

/** Define a standard entity (with a view) that emits from Block when it bounces. */
class BlockBonk extends defineEntity({
    key: 'BlockBonk',
    paramsShape: defineShape(
        and(entityPositionParamsShape, {
            move: {
                x: -1,
                y: -1,
            },
            ticksSinceCreation: -1,
        }),
    ),
    paramsMap: {
        view: {
            x: true,
            y: true,
        },
    },
}) {
    public static readonly moveSpeed = 4;
    public static readonly maxLife = 20;
    public static readonly graphicContext = new GraphicsContext().rect(0, 0, 4, 4).fill('yellow');

    public override update(): void {
        this.params.ticksSinceCreation++;

        if (this.params.ticksSinceCreation > BlockBonk.maxLife) {
            /** Automatically clean up the bounce particles when they reach their end of life. */
            this.destroy();
            return;
        }
        this.view.alpha = Math.min(
            1,
            (BlockBonk.maxLife + 3 - this.params.ticksSinceCreation) / BlockBonk.maxLife,
        );

        this.params.x += this.params.move.x;
        this.params.y += this.params.move.y;
    }

    public override createView() {
        const graphic = new Graphics(BlockBonk.graphicContext);

        graphic.x = this.params.x;
        graphic.y = this.params.y;

        return {
            view: graphic,
        };
    }
}

/** Define a logic entity which doesn't have a Pixi.js view. */
class Fps extends defineLogicEntity({
    key: 'Fps',
    paramsShape: undefined,
}) {
    protected fpsCounts: number[] = [];

    public override update(): void {
        this.fpsCounts.push(this.pixi.ticker.FPS);
        if (this.fpsCounts.length > 100) {
            const averageFps = Math.round(
                this.fpsCounts.reduce((a, b) => a + b) / this.fpsCounts.length,
            );
            this.fpsCounts = [];
            assertWrap.instanceOf(document.body.querySelector('.fps'), HTMLElement).innerText =
                String(averageFps);
        }
    }
}

/** Create the view */

const entityStore = new EntityStore({
    pixi: await createPixi({
        background: 'black',
        height: 500,
        width: 500,
    }),
    context: {
        movementSpeed: 6,
    },
    registeredEntities: [
        Block,
        Fps,
        BlockBonk,
    ],
});
document.body.append(entityStore.pixi.canvas);

/** Add entities to the view. */
entityStore.addEntity(Block, {direction: 1, x: 0, y: 0});
entityStore.addEntity(Block, {direction: -1, x: 490, y: 240});
entityStore.addEntity(Block, {direction: 1, x: 2, y: 252});
entityStore.addEntity(Fps);

/** Start updates. */
entityStore.pixi.ticker.add(() => {
    entityStore.updateAllEntities();
});

Common footguns

  1. When updating an entity's position, update it by modifying this.view.x or this.view.y. Do not update the position this.hitbox (unless you really know what you're doing and you set preventAutomaticHitboxUpdates to true).
  2. Do not set
15.1.1

5 months ago

15.1.0

5 months ago

15.0.1

5 months ago

15.0.0

5 months ago

14.10.0

5 months ago

14.9.1

5 months ago

14.9.0

5 months ago

14.8.0

5 months ago

14.7.0

5 months ago

14.6.2

5 months ago

14.6.1

5 months ago

14.6.0

5 months ago

14.5.0

5 months ago

14.4.0

5 months ago

14.3.1

5 months ago

14.3.0

5 months ago

14.2.0

6 months ago

14.1.0

6 months ago

14.0.1

6 months ago

14.0.0

6 months ago

13.1.0

6 months ago

13.0.0

6 months ago

12.0.0

6 months ago

11.2.1

6 months ago

11.2.0

6 months ago