Skip to content

Instantly share code, notes, and snippets.

@martindilling
Created January 4, 2019 14:26
Show Gist options
  • Save martindilling/083ac00da1285ea0e126aaf7c326d7e8 to your computer and use it in GitHub Desktop.
Save martindilling/083ac00da1285ea0e126aaf7c326d7e8 to your computer and use it in GitHub Desktop.
Simple Entity Component System game engine in Javascript
window.ECS = {
Engine: {
Game: null,
Entity: null,
},
Components: {},
Systems: {},
Entities: {},
/**
* @type {ECS.Engine.Game}
*/
game: null,
};
/*================================================
Game
================================================*/
(function () { 'use strict';
function gameLoop() {
for (let i = 0, len = this.systems.length; i < len; i++) {
this.systems[i](this.entities);
}
if (this.running !== false) {
requestAnimationFrame(gameLoop.bind(this));
}
}
ECS.Engine.Game = class Game {
constructor(systems) {
this.running = false;
this.entities = {};
this.systems = systems || [];
}
addEntity(entity) {
this.entities[entity.id] = entity;
return this;
}
setSystems(systems) {
this.systems = systems;
return this;
}
start() {
this.running = true;
console.log('starting', self);
requestAnimationFrame(gameLoop.bind(this));
}
stop() {
this.running = false;
}
};
}());
/*================================================
Entity
================================================*/
(function () { 'use strict';
ECS.Engine.Entity = class Entity {
constructor() {
this.id = Entity.count;
this.components = {};
Entity.count++;
}
add(component) {
this.components[component.__name] = component;
return this;
}
remove(name) {
delete this.components[typeof name === 'function' ? name.prototype.__name : name];
return this;
}
print() {
console.log(JSON.stringify(this, null, 4));
return this;
}
};
ECS.Engine.Entity.count = 0;
}());
/*================================================
Components
================================================*/
// Appearance
(function () {
'use strict';
ECS.Components.Appearance = class Appearance {
constructor(color, size) {
this.__name = 'appearance';
this.color = color || { r: 0, g: 100, b: 150 };
this.size = size || (1 + (Math.random() * 30 | 0));
}
};
ECS.Components.Health = class Health {
constructor(value) {
this.__name = 'health';
this.value = value || 20;
}
};
ECS.Components.Position = class Position {
constructor(x, y) {
this.__name = 'position';
this.x = x || 20 + (Math.random() * (800 - 20) | 0);
this.y = y || 20 + (Math.random() * (600 - 20) | 0);
}
};
ECS.Components.Velocity = class Velocity {
constructor(x, y) {
this.__name = 'velocity';
const rand = function () {
let num = Math.floor(Math.random() * 3) + 1; // this will get a number between 1 and 3;
num *= Math.floor(Math.random() * 2) === 1 ? 1 : -1; // this will add minus sign in 50% of cases
return num;
};
this.x = x || rand();
this.y = y || rand();
}
};
ECS.Components.PlayerControlled = class PlayerControlled {
constructor() {
this.__name = 'playerControlled';
}
};
ECS.Components.Collision = class Collision {
constructor() {
this.__name = 'collision';
}
};
})();
/*================================================
Systems
================================================*/
// Log
(function () {
'use strict';
ECS.Systems.log = function log(entities) {
// Here, we've implemented systems as functions which take in an array of
// entities. An optimization would be to have some layer which only
// feeds in relevant entities to the system, but for demo purposes we'll
// assume all entities are passed in and iterate over them.
var curEntity;
// iterate over all entities
for (var entityId in entities) {
curEntity = entities[entityId];
// console.log(curEntity);
if (curEntity.components.appearance &&
curEntity.components.playerControlled &&
curEntity.components.position) {
// console.log('Is Player');
}
if (curEntity.components.position &&
curEntity.components.position.x < 200 &&
curEntity.components.position.y < 200) {
// console.log('Top Left Corner: ' + curEntity.components.position.x + 'x' + curEntity.components.position.y);
}
}
// ECS.game.endGame();
};
})();
// Move
(function () {
'use strict';
ECS.Systems.move = function move(entities) {
// Here, we've implemented systems as functions which take in an array of
// entities. An optimization would be to have some layer which only
// feeds in relevant entities to the system, but for demo purposes we'll
// assume all entities are passed in and iterate over them.
var curEntity;
// iterate over all entities
for (var entityId in entities) {
curEntity = entities[entityId];
if (curEntity.components.position &&
curEntity.components.velocity) {
curEntity.components.position.x += curEntity.components.velocity.x;
curEntity.components.position.y += curEntity.components.velocity.y;
}
}
};
})();
// Render
(function () {
'use strict';
ECS.Systems.render = function render(entities) {
// Here, we've implemented systems as functions which take in an array of
// entities. An optimization would be to have some layer which only
// feeds in relevant entities to the system, but for demo purposes we'll
// assume all entities are passed in and iterate over them.
var curEntity;
const app = document.getElementById('app');
// iterate over all entities
for (var entityId in entities) {
curEntity = entities[entityId];
if (curEntity.components.appearance &&
curEntity.components.position &&
!curEntity.components.playerControlled) {
const appearance = curEntity.components.appearance;
const position = curEntity.components.position;
let el = document.getElementById('e:' + entityId);
if (!el) {
el = document.createElement('div');
el.id = `e:${entityId}`;
el.style.position = 'absolute';
app.append(el);
}
el.style.top = `${position.y}px`;
el.style.left = `${position.x}px`;
el.style.width = `${appearance.size}px`;
el.style.height = `${appearance.size}px`;
el.style.background = '#125690';
if (position.x < 0 || position.x > 800 || position.y < 0 || position.y > 600) {
el.outerHTML = "";
delete entities[entityId];
}
}
if (curEntity.components.appearance &&
curEntity.components.position &&
curEntity.components.playerControlled) {
const appearance = curEntity.components.appearance;
const position = curEntity.components.position;
let el = document.getElementById('e:' + entityId);
if (!el) {
el = document.createElement('div');
el.id = `e:${entityId}`;
el.style.position = 'absolute';
app.append(el);
}
el.style.top = `${position.y}px`;
el.style.left = `${position.x}px`;
el.style.width = `${appearance.size}px`;
el.style.height = `${appearance.size}px`;
el.style.background = '#763269';
}
}
// ECS.game.endGame();
};
})();
(function () { 'use strict';
ECS.game = new ECS.Engine.Game([
ECS.Systems.log,
ECS.Systems.move,
ECS.Systems.render,
]);
for (let i = 0; i < 20; i++) {
ECS.game.addEntity(
(new ECS.Engine.Entity())
.add(new ECS.Components.Appearance())
.add(new ECS.Components.Position())
.add(new ECS.Components.Velocity())
.add(new ECS.Components.Health())
.add(new ECS.Components.Collision())
);
}
ECS.game.addEntity(
(new ECS.Engine.Entity())
.add(new ECS.Components.Appearance())
.add(new ECS.Components.Position())
.add(new ECS.Components.Velocity())
.add(new ECS.Components.Health())
.add(new ECS.Components.Collision())
.add(new ECS.Components.PlayerControlled())
);
// Kick off the game
ECS.game.start();
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment