@game-vir/entity v15.1.1
@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 theaddEntity
andupdateAllEntities
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 theupdate()
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:
- Clone the repo.
- Run
npm ci
.. - Run
cd packages/entity
. - Run
npm start
. - 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
- When updating an entity's position, update it by modifying
this.view.x
orthis.view.y
. Do not update the positionthis.hitbox
(unless you really know what you're doing and you setpreventAutomaticHitboxUpdates
totrue
). - Do not set
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago