Skip to content

Instantly share code, notes, and snippets.

@jeffersonmourak
Created July 3, 2021 21:54
Show Gist options
  • Select an option

  • Save jeffersonmourak/301f6fc9806e2f368a3c9172cfc7b357 to your computer and use it in GitHub Desktop.

Select an option

Save jeffersonmourak/301f6fc9806e2f368a3c9172cfc7b357 to your computer and use it in GitHub Desktop.
Space Inverders Game
/**
* Game Engine
*/
class Canvas {
/**
* Stores the DOM elements that will be used on this code.
*/
#elements = {};
/**
* Game Settings.
*/
#settings = {};
/**
* Runtime Variables.
*/
#runtime = {
key: null,
enemies: [],
player: null,
time: {
start: Date.now(),
lastFrame: Date.now(),
delta: 0,
},
};
/**
* Contructor.
*
* @param {HTMLElement} el Parent element that will receive the game.
*/
constructor({
el = document.body,
width = 600,
height = 600,
fps = 30,
} = {}) {
this.#elements.body = el;
this.#settings = {
width,
height,
fps,
fpsInterval: 1000 / fps,
pallete: ["transparent", "white", "red", "green"],
};
this.#elements.canvas = this.#createCanvasEl();
this.context = this.#elements.canvas.getContext("2d");
this.#elements.body.appendChild(this.#elements.canvas);
this.#setup();
this.#listenKeyboard();
this.render();
}
/**
* Renders the frames for the game.
*/
render() {
window.requestAnimationFrame(() => {
this.render();
});
const now = Date.now();
this.#runtime.time.delta = now - this.#runtime.time.lastFrame;
if (this.#runtime.time.delta > this.#settings.fpsInterval) {
this.context.clearRect(0, 0, this.#settings.width, this.#settings.height);
this.#draw();
this.#runtime.time.lastFrame =
now - (this.#runtime.time.delta % this.#settings.fpsInterval);
}
}
/**
* Setup Gameobjects before start drawing.
*/
#setup() {
const { enemies } = this.#runtime;
const enemyW = Enemy.bitMap[0].length;
const enemyH = Enemy.bitMap.length;
const enemiesPerRow = 7;
this.#runtime.player = new Player(this.context, { ...this.#settings }, {});
for (let i = 0; i < enemiesPerRow * 4; i++) {
let x = Enemy.toPixel(enemyW * 2 + 2) * (i % enemiesPerRow);
let y =
Enemy.toPixel(enemyH + 10) * Math.floor(i / enemiesPerRow) +
Enemy.toPixel(10);
enemies.push(
new Enemy(
this.context,
{ ...this.#settings },
{ x, y, speed: 1, rtl: true }
)
);
}
}
/**
* Draw a frame for the game.
*/
#draw() {
this.drawBackground();
this.drawEnemies();
this.drawPlayer();
}
/**
* Listen Keyboard entries.
*/
#listenKeyboard() {
document.addEventListener("keydown", (event) =>
this.controller(event.key, false)
);
document.addEventListener("keyup", (event) =>
this.controller(event.key, true)
);
}
/**
* Creates Canvas element to be manipulated.
* @returns {HTMLElement}
*/
#createCanvasEl() {
const element = document.createElement("canvas");
element.width = this.#settings.width;
element.height = this.#settings.height;
element.id = `canvas-${Math.random().toString(36).substring(7)}`;
return element;
}
// PUBLIC
/**
* Draw the background.
*/
drawBackground() {
this.context.fillStyle = "black";
this.context.fillRect(0, 0, this.#settings.width, this.#settings.height);
}
/**
* Draw Enemies entities.
*/
drawEnemies() {
const { enemies } = this.#runtime;
for (const enemy of enemies) {
enemy.runtime.delta = this.#runtime.time.delta;
if (
enemies[0].runtime.onWall ||
enemies[enemies.length - 1].runtime.onWall
) {
enemy.runtime.rtl = !enemy.runtime.rtl;
}
if (
enemy.runtime.bullet &&
enemy.runtime.bullet.collides(this.#runtime.player)
) {
this.#runtime.player.destroy();
enemy.runtime.bullet.destroy();
}
enemy.draw();
}
}
/**
* Draws Player Entity.
*/
drawPlayer() {
const { player } = this.#runtime;
switch (this.#runtime.key) {
case "ArrowRight":
player.runtime.x += Character.toPixel(3);
break;
case "ArrowLeft":
player.runtime.x -= Character.toPixel(3);
break;
case " ":
player.shoot();
}
if (player) {
player.draw();
}
for (let bI = 0; bI < player.runtime.bullets.length; bI++) {
const bullet = player.runtime.bullets[bI];
for (let eI = 0; eI < this.#runtime.enemies.length; eI++) {
if (
!bullet.runtime.destroyed &&
bullet.collides(this.#runtime.enemies[eI])
) {
this.#runtime.enemies[eI].destroy();
bullet.destroy();
}
}
}
}
/**
* Saves What key is being pressed.
*
* @param {String} key Key Pressed
* @param {Boolean} released Is the key released.
*/
controller(key, released) {
this.#runtime.key = !released ? key : null;
}
}
/**
* Character
* @extends GameObject.
*/
class Character extends GameObject {
/**
* How to draw the character.
*/
static bitMap = [];
static pixelSize = 3;
/**
* Calculates the pixel size of a given number.
*
* @param {Number} size Size to be calculated
* @returns {Number} pixel perfect size.
*/
static toPixel(size) {
return size * Character.pixelSize;
}
drawBitMap(Entity) {
let { x, y } = this.runtime;
let { pallete } = this.settings;
for (let by = 0; by < Entity.bitMap.length; by++) {
for (let bx = 0; bx < Entity.bitMap[by].length; bx++) {
this.context.fillStyle = pallete[Entity.bitMap[by][bx]];
this.context.fillRect(
x + bx * Entity.pixelSize,
y + by * Entity.pixelSize,
Entity.pixelSize,
Entity.pixelSize
);
}
}
}
}
class EnemyBullet extends Character {
static bitMap = [
[0, 2, 0],
[0, 2, 0],
[0, 2, 0],
[2, 2, 2],
];
constructor(context, settings, runtime) {
super(context, settings, runtime);
this.runtime = {
...runtime,
w: EnemyBullet.bitMap[0].length * EnemyBullet.pixelSize,
h: EnemyBullet.bitMap.length * EnemyBullet.pixelSize,
};
}
draw() {
this.drawBitMap(EnemyBullet);
this.runtime.y += Character.toPixel(1);
}
}
class Enemy extends Character {
static bitMap = [
[0, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0],
[0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0],
[0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0],
[0, 2, 2, 0, 2, 2, 2, 0, 2, 2, 0],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2],
[2, 0, 2, 0, 0, 0, 0, 0, 2, 0, 2],
[0, 0, 0, 2, 2, 0, 2, 2, 0, 0, 0],
];
constructor(context, settings, runtime) {
super(context, settings, runtime);
this.runtime = {
...runtime,
x: runtime.x + 30,
offset: 10 * Enemy.pixelSize,
w: Enemy.bitMap[0].length * Enemy.pixelSize,
h: Enemy.bitMap.length * Enemy.pixelSize,
bullet: null,
destroyed: false,
};
}
draw() {
let { x, y, speed, rtl, offset, bullet, destroyed } = this.runtime;
const {
tl: [tlX],
tr: [trX],
} = this.getVertex();
const { width, height } = this.settings;
if (!destroyed) {
this.drawBitMap(Enemy);
}
let onWall = (tlX <= offset && !rtl) || (trX > width - offset && rtl);
rtl = tlX <= offset && !rtl ? !rtl : rtl;
if (!bullet && Math.random() * 100 > 99.85) {
bullet = new EnemyBullet(this.context, this.settings, { x, y });
}
if (bullet && bullet.runtime.y > height) {
bullet = null;
}
this.runtime = {
...this.runtime,
onWall,
bullet,
x: x + speed * Enemy.pixelSize * (rtl ? 1 : -1),
};
if (bullet) {
bullet.draw();
}
}
}
/**
* Game Object
*/
class GameObject {
runtime = {};
/**
* Initializes the Gameobject.
*
* @param {Canvas2DContext} context Canvas Context.
* @param {GameSettings} settings Game Settings.
* @param {RuntimeObj} runtime Initial Runtime Variables.
*/
constructor(context, settings, runtime) {
this.context = context;
this.settings = settings;
this.runtime = {
...this.runtime,
...runtime,
};
}
/**
* Draws the game object into canvas
*/
draw() {}
/**
* Calculates the vertexes of an object.
*
* @returns {VertexObject}
*/
getVertex() {
const { x, y, w, h } = this.runtime;
return {
tl: [x, y],
tr: [x + w, y],
bl: [x, y + h],
br: [x + w, y + h],
};
}
/**
* Calculates if the objects are colliding.
*
* @param {GameObject} gameObject Target Gameobject.
* @returns {Boolean}
*/
collides(gameObject) {
const { x, y, w, h } = this.runtime;
const { x: gX, y: gY, w: gW, h: gH } = gameObject.runtime;
if (this.runtime.destroyed || gameObject.runtime.destroyed) {
return false;
}
return x < gX + gW && x + w > gX && y < gY + gH && y + h > gY;
}
/**
* Destroy the object.
*/
destroy() {
this.runtime.destroyed = true;
}
}
/**
* Character
* @extends GameObject.
*/
class Character extends GameObject {
/**
* How to draw the character.
*/
static bitMap = [];
static pixelSize = 3;
/**
* Calculates the pixel size of a given number.
*
* @param {Number} size Size to be calculated
* @returns {Number} pixel perfect size.
*/
static toPixel(size) {
return size * Character.pixelSize;
}
drawBitMap(Entity) {
let { x, y } = this.runtime;
let { pallete } = this.settings;
for (let by = 0; by < Entity.bitMap.length; by++) {
for (let bx = 0; bx < Entity.bitMap[by].length; bx++) {
this.context.fillStyle = pallete[Entity.bitMap[by][bx]];
this.context.fillRect(
x + bx * Entity.pixelSize,
y + by * Entity.pixelSize,
Entity.pixelSize,
Entity.pixelSize
);
}
}
}
}
class EnemyBullet extends Character {
static bitMap = [
[0, 2, 0],
[0, 2, 0],
[0, 2, 0],
[2, 2, 2],
];
constructor(context, settings, runtime) {
super(context, settings, runtime);
this.runtime = {
...runtime,
w: EnemyBullet.bitMap[0].length * EnemyBullet.pixelSize,
h: EnemyBullet.bitMap.length * EnemyBullet.pixelSize,
};
}
draw() {
this.drawBitMap(EnemyBullet);
this.runtime.y += Character.toPixel(1);
}
}
class Enemy extends Character {
static bitMap = [
[0, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0],
[0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0],
[0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0],
[0, 2, 2, 0, 2, 2, 2, 0, 2, 2, 0],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2],
[2, 0, 2, 0, 0, 0, 0, 0, 2, 0, 2],
[0, 0, 0, 2, 2, 0, 2, 2, 0, 0, 0],
];
constructor(context, settings, runtime) {
super(context, settings, runtime);
this.runtime = {
...runtime,
x: runtime.x + 30,
offset: 10 * Enemy.pixelSize,
w: Enemy.bitMap[0].length * Enemy.pixelSize,
h: Enemy.bitMap.length * Enemy.pixelSize,
bullet: null,
destroyed: false,
};
}
draw() {
let { x, y, speed, rtl, offset, bullet, destroyed } = this.runtime;
const {
tl: [tlX],
tr: [trX],
} = this.getVertex();
const { width, height } = this.settings;
if (!destroyed) {
this.drawBitMap(Enemy);
}
let onWall = (tlX <= offset && !rtl) || (trX > width - offset && rtl);
rtl = tlX <= offset && !rtl ? !rtl : rtl;
if (!bullet && Math.random() * 100 > 99.85) {
bullet = new EnemyBullet(this.context, this.settings, { x, y });
}
if (bullet && bullet.runtime.y > height) {
bullet = null;
}
this.runtime = {
...this.runtime,
onWall,
bullet,
x: x + speed * Enemy.pixelSize * (rtl ? 1 : -1),
};
if (bullet) {
bullet.draw();
}
}
}
class PlayerBullet extends Character {
static bitMap = [
[3, 3, 3],
[0, 3, 0],
[0, 3, 0],
[0, 3, 0],
];
constructor(context, settings, runtime) {
super(context, settings, runtime);
this.runtime = {
...runtime,
w: PlayerBullet.bitMap[0].length * PlayerBullet.pixelSize,
h: PlayerBullet.bitMap.length * PlayerBullet.pixelSize,
};
}
draw() {
let { destroyed } = this.runtime;
if (!destroyed) {
this.drawBitMap(PlayerBullet);
}
this.runtime.y -= Character.toPixel(1);
}
}
class Player extends Character {
static bitMap = [
[0, 0, 0, 3, 0, 0, 0],
[0, 0, 3, 3, 3, 0, 0],
[3, 3, 3, 3, 3, 3, 3],
[3, 3, 3, 3, 3, 3, 3],
];
constructor(context, settings, runtime) {
super(context, settings, runtime);
const w = Character.toPixel(Player.bitMap[0].length);
const h = Character.toPixel(Player.bitMap.length);
this.runtime = {
...runtime,
delta: Date.now(),
bullets: [],
w,
h,
y: settings.height - h - Character.toPixel(2),
x: Math.round((settings.width - w) / 2),
};
}
/**
* Shoots a bullet upwards.
*/
shoot() {
const { delta, x, y } = this.runtime;
if (Date.now() - delta > 1000) {
this.runtime.bullets.push(
new PlayerBullet(this.context, this.settings, { x, y })
);
this.runtime.delta = Date.now();
}
}
destroy() {
this.runtime.destroyed = true;
this.runtime.destroyedAt = Date.now();
}
draw() {
let { x, y, bullets, destroyed, destroyedAt } = this.runtime;
if (destroyed && Date.now() - destroyedAt < 3000) {
return;
}
this.drawBitMap(Player);
this.runtime.bullets = bullets.filter((bullet) => bullet.runtime.y > 0);
for (let i = 0; i < this.runtime.bullets.length; i++) {
bullets[i].draw();
}
}
}
/**
* Game Engine
*/
class Canvas {
/**
* Stores the DOM elements that will be used on this code.
*/
#elements = {};
/**
* Game Settings.
*/
#settings = {};
/**
* Runtime Variables.
*/
#runtime = {
key: null,
enemies: [],
player: null,
time: {
start: Date.now(),
lastFrame: Date.now(),
delta: 0,
},
};
/**
* Contructor.
*
* @param {HTMLElement} el Parent element that will receive the game.
*/
constructor({
el = document.body,
width = 600,
height = 600,
fps = 30,
} = {}) {
this.#elements.body = el;
this.#settings = {
width,
height,
fps,
fpsInterval: 1000 / fps,
pallete: ["transparent", "white", "red", "green"],
};
this.#elements.canvas = this.#createCanvasEl();
this.context = this.#elements.canvas.getContext("2d");
this.#elements.body.appendChild(this.#elements.canvas);
this.#setup();
this.#listenKeyboard();
this.render();
}
/**
* Renders the frames for the game.
*/
render() {
window.requestAnimationFrame(() => {
this.render();
});
const now = Date.now();
this.#runtime.time.delta = now - this.#runtime.time.lastFrame;
if (this.#runtime.time.delta > this.#settings.fpsInterval) {
this.context.clearRect(0, 0, this.#settings.width, this.#settings.height);
this.#draw();
this.#runtime.time.lastFrame =
now - (this.#runtime.time.delta % this.#settings.fpsInterval);
}
}
/**
* Setup Gameobjects before start drawing.
*/
#setup() {
const { enemies } = this.#runtime;
const enemyW = Enemy.bitMap[0].length;
const enemyH = Enemy.bitMap.length;
const enemiesPerRow = 7;
this.#runtime.player = new Player(this.context, { ...this.#settings }, {});
for (let i = 0; i < enemiesPerRow * 4; i++) {
let x = Enemy.toPixel(enemyW * 2 + 2) * (i % enemiesPerRow);
let y =
Enemy.toPixel(enemyH + 10) * Math.floor(i / enemiesPerRow) +
Enemy.toPixel(10);
enemies.push(
new Enemy(
this.context,
{ ...this.#settings },
{ x, y, speed: 1, rtl: true }
)
);
}
}
/**
* Draw a frame for the game.
*/
#draw() {
this.drawBackground();
this.drawEnemies();
this.drawPlayer();
}
/**
* Listen Keyboard entries.
*/
#listenKeyboard() {
document.addEventListener("keydown", (event) =>
this.controller(event.key, false)
);
document.addEventListener("keyup", (event) =>
this.controller(event.key, true)
);
}
/**
* Creates Canvas element to be manipulated.
* @returns {HTMLElement}
*/
#createCanvasEl() {
const element = document.createElement("canvas");
element.width = this.#settings.width;
element.height = this.#settings.height;
element.id = `canvas-${Math.random().toString(36).substring(7)}`;
return element;
}
// PUBLIC
/**
* Draw the background.
*/
drawBackground() {
this.context.fillStyle = "black";
this.context.fillRect(0, 0, this.#settings.width, this.#settings.height);
}
/**
* Draw Enemies entities.
*/
drawEnemies() {
const { enemies } = this.#runtime;
for (const enemy of enemies) {
enemy.runtime.delta = this.#runtime.time.delta;
if (
enemies[0].runtime.onWall ||
enemies[enemies.length - 1].runtime.onWall
) {
enemy.runtime.rtl = !enemy.runtime.rtl;
}
if (
enemy.runtime.bullet &&
enemy.runtime.bullet.collides(this.#runtime.player)
) {
this.#runtime.player.destroy();
enemy.runtime.bullet.destroy();
}
enemy.draw();
}
}
/**
* Draws Player Entity.
*/
drawPlayer() {
const { player } = this.#runtime;
switch (this.#runtime.key) {
case "ArrowRight":
player.runtime.x += Character.toPixel(3);
break;
case "ArrowLeft":
player.runtime.x -= Character.toPixel(3);
break;
case " ":
player.shoot();
}
if (player) {
player.draw();
}
for (let bI = 0; bI < player.runtime.bullets.length; bI++) {
const bullet = player.runtime.bullets[bI];
for (let eI = 0; eI < this.#runtime.enemies.length; eI++) {
if (
!bullet.runtime.destroyed &&
bullet.collides(this.#runtime.enemies[eI])
) {
this.#runtime.enemies[eI].destroy();
bullet.destroy();
}
}
}
}
/**
* Saves What key is being pressed.
*
* @param {String} key Key Pressed
* @param {Boolean} released Is the key released.
*/
controller(key, released) {
this.#runtime.key = !released ? key : null;
}
}
window.canvas = new Canvas({
fps: 60,
});
/**
* Game Object
*/
class GameObject {
runtime = {};
/**
* Initializes the Gameobject.
*
* @param {Canvas2DContext} context Canvas Context.
* @param {GameSettings} settings Game Settings.
* @param {RuntimeObj} runtime Initial Runtime Variables.
*/
constructor(context, settings, runtime) {
this.context = context;
this.settings = settings;
this.runtime = {
...this.runtime,
...runtime,
};
}
/**
* Draws the game object into canvas
*/
draw() {}
/**
* Calculates the vertexes of an object.
*
* @returns {VertexObject}
*/
getVertex() {
const { x, y, w, h } = this.runtime;
return {
tl: [x, y],
tr: [x + w, y],
bl: [x, y + h],
br: [x + w, y + h],
};
}
/**
* Calculates if the objects are colliding.
*
* @param {GameObject} gameObject Target Gameobject.
* @returns {Boolean}
*/
collides(gameObject) {
const { x, y, w, h } = this.runtime;
const { x: gX, y: gY, w: gW, h: gH } = gameObject.runtime;
if (this.runtime.destroyed || gameObject.runtime.destroyed) {
return false;
}
return x < gX + gW && x + w > gX && y < gY + gH && y + h > gY;
}
/**
* Destroy the object.
*/
destroy() {
this.runtime.destroyed = true;
}
}
class PlayerBullet extends Character {
static bitMap = [
[3, 3, 3],
[0, 3, 0],
[0, 3, 0],
[0, 3, 0],
];
constructor(context, settings, runtime) {
super(context, settings, runtime);
this.runtime = {
...runtime,
w: PlayerBullet.bitMap[0].length * PlayerBullet.pixelSize,
h: PlayerBullet.bitMap.length * PlayerBullet.pixelSize,
};
}
draw() {
let { destroyed } = this.runtime;
if (!destroyed) {
this.drawBitMap(PlayerBullet);
}
this.runtime.y -= Character.toPixel(1);
}
}
class Player extends Character {
static bitMap = [
[0, 0, 0, 3, 0, 0, 0],
[0, 0, 3, 3, 3, 0, 0],
[3, 3, 3, 3, 3, 3, 3],
[3, 3, 3, 3, 3, 3, 3],
];
constructor(context, settings, runtime) {
super(context, settings, runtime);
const w = Character.toPixel(Player.bitMap[0].length);
const h = Character.toPixel(Player.bitMap.length);
this.runtime = {
...runtime,
delta: Date.now(),
bullets: [],
w,
h,
y: settings.height - h - Character.toPixel(2),
x: Math.round((settings.width - w) / 2),
};
}
/**
* Shoots a bullet upwards.
*/
shoot() {
const { delta, x, y } = this.runtime;
if (Date.now() - delta > 1000) {
this.runtime.bullets.push(
new PlayerBullet(this.context, this.settings, { x, y })
);
this.runtime.delta = Date.now();
}
}
destroy() {
this.runtime.destroyed = true;
this.runtime.destroyedAt = Date.now();
}
draw() {
let { x, y, bullets, destroyed, destroyedAt } = this.runtime;
if (destroyed && Date.now() - destroyedAt < 3000) {
return;
}
this.drawBitMap(Player);
this.runtime.bullets = bullets.filter((bullet) => bullet.runtime.y > 0);
for (let i = 0; i < this.runtime.bullets.length; i++) {
bullets[i].draw();
}
}
}
@jeffersonmourak
Copy link
Author

TO DO.

  • Fix dead bullets from spawning.
  • Fix not dying after the first death.
  • Add Fortress Characters.
  • Add Animations.
  • Measure Code Performance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment