Game | Part 2 — Improved Keyboard Controls and Entity Abstraction

1. Keyboard Controls

You must have a physical keyboard to control the first several demos. Press the up, down, left, and right arrow keys on your keyboard after clicking inside the demo.

Try the demo in section 3 if you don't have a physical keyboard (I'm looking at you, mobile phone users).

1.a Keyboard Controls via OOP Paradigm

1.b Keyboard Controls via Functional Paradigm

2. Entity Abstraction

2.a Entity Abstraction via OOP Paradigm

2.b Entity Abstraction via Functional Paradigm

index.js

import { canvas, ctx } from "./util.js";
import controls from "./Controls.js";
// import Entity from './Entity.js';
import { newEntity } from './entity-fn.js';

// ==========================================

let entity_1;
let entity_2;

const reset = () => {
  // entity_1 = new Entity({ 
  //   size: { width: 50, height: 50 },
  // })
  entity_1 = newEntity({ 
    size: { width: 50, height: 50 },
  })

  // entity_2 = new Entity({ 
  //   color: 'black',
  //   size: { width: 50, height: 50 },
  //   position: { x: canvas.width / 2, y: canvas.height / 2},
  // })
  entity_2 = newEntity({ 
    color: 'black',
    size: { width: 50, height: 50 },
    position: { x: canvas.width / 2, y: canvas.height / 2},
  })
};
reset();

// ==========================================

const reset_button = document.querySelector('#reset-button');
reset_button.addEventListener('click', () => reset());

// ==========================================

function animate(t1) {
  requestAnimationFrame(animate);

  // Refresh:
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // Update:
  entity_1.update(controls.x(), controls.y());

  // Render:
  entity_1.render();
  entity_2.render();
}
animate();

Entity.js

import { canvas, ctx } from "./util.js";

// constructor({
//   size: { width, height }, 
//   position,
//   color='darkorange',
// }) {
const newEntity = ({
  size: { width, height }, 
  position,
  color='darkorange', // this.color = color;
}) => {

  // this.w = width;
  // this.h = height;
  const [w, h] = [width, height];

  // this.pos = {
  //   x: position?.x || canvas.width / 2 - this.w / 2,
  //   y: position?.y || canvas.height / 2 - this.h / 2,
  // };
  let pos = { 
    x: position?.x || canvas.width / 2 - w / 2,
    y: position?.y || canvas.height / 2 - h / 2,
  };
  
  // update(x, y) {
  //   this.pos.x += x * 3;
  //   this.pos.y += y * 3;
  // }
  const update = (x, y) => {
    pos.x += x * 3;
    pos.y += y * 3;
  };
  

  // render() {
  //   ctx.fillStyle = this.color;
  //   ctx.fillRect(this.pos.x, this.pos.y, this.w, this.h);
  // }
  const render = () => {
    ctx.fillStyle = color;
    ctx.fillRect(pos.x, pos.y, w, h);
  };


  return ({ // class Entity {}
    update,
    render,
  });
}

export { newEntity };

In order to map our OOP based class implementation into a functional one, we simply create a function that returns an object that has the same properties as our class based implementation.

Utilizing the functional paradigm allows us to completely avoid the use of the this keyword and all of its associated complexities.

We simply implement the function newEntity's internal state variables w: number, h: number, and pos: { x: number, y: number} as closures to the methods update and render.

3. Touch Controls for Mobile Phones / Tablets

You must have touch gestures enabled to use the touch controls. Try this demo on a mobile phone or tablet. If you are testing it on a desktop or laptop then open dev tools and enable touch gestures.

3.a Touch Controls via OOP Paradigm

3.b Touch Controls via Functional Paradigm

let keys = {};

const touch_controller_up = document.querySelector('#touch-controller-up');
const touch_controller_down = document.querySelector('#touch-controller-down');
const touch_controller_left = document.querySelector('#touch-controller-left');
const touch_controller_right = document.querySelector('#touch-controller-right');

// ==============================================

// Keyboard Controls:

document.addEventListener("keydown", (e) => {
  if (['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', ' '].indexOf(e.key) >= 0) e.preventDefault();
  keys[e.key] = true;
}, false);

document.addEventListener("keyup", ({ key }) => keys[key] = false, false);

// ==============================================

// Touch Controls:

touch_controller_up.addEventListener('touchstart',    () => keys['ArrowUp']    = true);
touch_controller_up.addEventListener('touchend',      () => keys['ArrowUp']    = false);

touch_controller_down.addEventListener('touchstart',  () => keys['ArrowDown']  = true);
touch_controller_down.addEventListener('touchend',    () => keys['ArrowDown']  = false);

touch_controller_left.addEventListener('touchstart',  () => keys['ArrowLeft']  = true);
touch_controller_left.addEventListener('touchend',    () => keys['ArrowLeft']  = false);

touch_controller_right.addEventListener('touchstart', () => keys['ArrowRight'] = true);
touch_controller_right.addEventListener('touchend',   () => keys['ArrowRight'] = false);

// ==============================================

const x = () => {
  if (keys['ArrowLeft']  || keys['a'])  return -1;
  if (keys['ArrowRight'] || keys['d'])  return 1;
  return 0;
};
const y = () => {
  if (keys['ArrowUp']   || keys['w'])  return -1;
  if (keys['ArrowDown'] || keys['s'])  return 1;
  return 0;
};

// ==============================================

const controls = { x, y };

export default controls;

4. Collision Detection and Velocity Based Position Update

4.a Collision Detection with Walls

const newEntity = ({ ... }) => {

  const [w, h] = [width, height];

  let pos = { ... };

  const update = (x, y) => {

    const new_x = pos.x + x;
    if (new_x < 0) 
      pos.x = 0; // clamp at left-wall
    else if (new_x + w >= canvas.width)
      pos.x = canvas.width - w - 1; // clamp at right-wall
    else 
      pos.x = new_x; // move (inside left/right walls)

    const new_y = pos.y + y;
    if (new_y < 0) 
      pos.y = 0; // clamp at top-wall
    else if (new_y + h >= canvas.height)
      pos.y = canvas.height - h - 1; // clamp at bottom-wall
    else 
      pos.y = new_y; // move (inside top/bottom walls)
    
  };
  
  const render = () => { ... };

  return ({ update, render });
}

4.b Velocity Based Position Update

import { canvas, ctx } from "./util.js";

const newEntity = ({
  size: { width, height }, 
  position,
  ...
}) => {

  const [w, h] = [width, height];

  // position
  let pos = { 
    x: position?.x || canvas.width / 2 - w / 2,
    y: position?.y || canvas.height / 2 - h / 2,
    xr: position?.x + w || canvas.width / 2 - w / 2 + w,
    y1: position?.x + h || canvas.height / 2 - h / 2 + h,
  };

  // velocity
  let vel = { x: 0, y: 0 };

  const update = (x, y) => {

    // move X (with wall collision detection)
    vel.x = x;
    const new_x = pos.x + vel.x; // left of entity
    const new_xr = new_x + w;    // right of entity
    if (
      0 <= new_x                 // left-wall
        && new_xr < canvas.width // right-wall
    ) { 
      pos.x = new_x;
    }

    // move Y (with wall collision detection)
    vel.y = y;
    const new_y = pos.y + vel.y; // top of entity
    const new_yb = new_y + h;    // bottom of entity
    if (
      0 <= new_y                  // top-wall
        && new_yb < canvas.height // bottom-wall
    ) { 
      pos.y = new_y;
    }

  };

  const render = () => { ... };
  
  return ({ update, render });
}

export { newEntity };

4.c Collision Detection Between Entities