Created
January 4, 2019 14:26
-
-
Save martindilling/083ac00da1285ea0e126aaf7c326d7e8 to your computer and use it in GitHub Desktop.
Simple Entity Component System game engine in Javascript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}()); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*================================================ | |
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