Square
An Entity Component System(ECS) Based Game Framework
Install
npm install square-ecs
Example
app.js
import Application from "./square2d/Application.js";
import { InputSystem, RenderingSystem, ShapeRenderer, GravitySystem, MovementSystem } from "./systems.js";
import { BoxShapeComponent, VectorComponent } from "./components.js";
const app = new Application({
config: {
canvas: {
width: 400,
height: 400,
}
},
initialState: {
score: 0,
loading: true
},
systems: [
RenderingSystem,
ShapeRenderer,
InputSystem,
GravitySystem,
MovementSystem
]
});
app.on("init", app => {
const player = app.entityPool.getEntity();
player.tag("visible");
player.tag("controllable");
player.tag("player");
player.attach("position", new VectorComponent(20, 20));
player.attach("shape", new BoxShapeComponent(32, 32));
player.attach("speed", 100);
player.attach("jumpforce", 500);
player.attach("velocity", new VectorComponent);
app.add(player);
});
app.start();
systems.js
import { RenderableQuery, KinematicBodyQuery } from "./queries.js";
export const RenderingSystem = app => {
const canvas = document.createElement("canvas");
canvas.width = app.config.canvas.width;
canvas.height = app.config.canvas.height;
canvas.style.background = "#000";
const context = canvas.getContext("2d");
app.on("init", () => {
document.body.appendChild(canvas);
});
app.on("update", () => {
context.clearRect(0, 0, canvas.width, canvas.height);
app.emit("render", context, canvas);
});
}
export const InputSystem = app => {
app.state.keyboard = {};
const handleKey = ({ key, type }) => {
app.state.keyboard[key === " " ? "Spacebar" : key] = type === "keydown";
};
window.addEventListener("keydown", handleKey);
window.addEventListener("keyup", handleKey);
};
export const MovementSystem = app => {
app.on("update", dt => {
const [player] = app.query("controllable");
if(!player.tags.has("jumping") && app.state.keyboard.Spacebar) {
player.velocity.y -= player.jumpforce * dt;
}
});
};
export const ShapeRenderer = app => {
app.on("render", ctx => {
const entities = app.query(RenderableQuery);
entities.forEach(entity => {
ctx.fillStyle = '#fff';
ctx.fillRect(entity.position.x, entity.position.y, entity.shape.width, entity.shape.height);
});
});
}
export const GravitySystem = app => {
const GRAVITY_CONSTANT = 6.5;
app.on("update", dt => {
const entities = app.query(KinematicBodyQuery);
entities.forEach(entity => {
entity.position.y += entity.velocity.y;
if(entity.position.y < 200) {
entity.tag("jumping");
entity.velocity.y += 5 * GRAVITY_CONSTANT * dt;
} else {
entity.untag("jumping");
entity.velocity.y = 0;
}
});
});
}
queries.js
export const RenderableQuery = ['@position', '@shape', 'visible'];
export const KinematicBodyQuery = ['@position', '@shape', '@velocity'];
components.js
export class BoxShapeComponent {
constructor(width, height) {
this.type = "box";
this.width = width;
this.height = height;
}
}
export class VectorComponent {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}