JavaScript: Do you like games? (part 3)

Home/JavaScript, Technical, Written by our colleagues/JavaScript: Do you like games? (part 3)

The last article was a bit long so we’re going to move forward in smaller increments.

Let’s start with a discussion on how movement works in games. The 18th century Swiss mathematician, physicist, astronomer, logician and engineer (quite a resume) Leonhard Euler devised a way for us to predict where an object’s going to be based on two things: its current position and its speed. We actually used it in our previous article in the update() method of our Square class:

this.x += 1 * movementMultiplier;
this.y += 1.5 * movementMultiplier;

Euler integration works wonders in games where speed is constant and it’s also very easy on the processor; unfortunately you’ll quickly find out it’s not quite enough if you’re planning on building a physics engine… it becomes highly inaccurate. Let’s visualize this:

Euler integration

Euler integration

In the image above, the distance between two peaks on the grid is ~30px, the speed of our ball is 60px every frame and we’re running this for 10 frames (at 60fps that’s 1/6 of a second). This seems fine… the movement is constant so it should be easy to work with, right?… well, not really; let’s add a wall in there:

euler intergation wall added

Between frames 5 and 6 you can see an impressively drawn wall; what’s even more impressive is that with the current logic, that ball will pass through the wall like it’s not even there. This might be cool if you’re trying to achieve teleportation, not so much if you’re reaching for collision.

In a game were detecting multiple object collisions is necessary, if you take into account that each object has it’s own trajectory, speed and external forces acting on it… Euler integration proves insufficient.

Another way would be using one of the Runge–Kutta methods but they can put a strain on the processor; it’s perfect for precision physics calculations but probably overkill for our purposes.

The go to method in most games would be to use the equations of the French physicist Loup Verlet, specifically Verlet integration. The main difference is that with Verlet integration you’re calculating the speed of an object and not its position in the next frame. You do this by subtracting the previous position from the current position.

I keep saying speed so I’m going to correct myself; we’re actually calculating velocity. The difference between speed an velocity matters to us, because speed is ignorant of direction, whereas velocity is a vector quantity and it is direction aware; it’s a really small and hugely important distinction.

let velocity = currentPosition - previousPosition;
position += velocity;

Does this mean our collision problem goes away? Not really but it makes it easier to rectify; we can calculate the next state based on our current and previous states, enabling us to resolve any conflicts before we render.

We’ll create a VerletModel in the game engine and this will allow for other objects to use it if needed; for now it should:

  • expose getters and setters for the x and y velocities
  • expose getters and setters for the x and y position values
export default class VerletModel {
  constructor(x=0, y=0) {
    this.previousX = x;
    this.previousY = y;
    this.xPos = x;
    this.yPos = y;
  }

  get vx() {
    return this.xPos - this.previousX;
  }

  set vx(value) {
    this.previousX = this.xPos - value;
  }

  get vy() {
    return this.yPos - this.previousY;
  }

  set vy(value) {
    this.previousY = this.yPos - value;
  }

  set x(value) {
    this.previousX = value - this.vx;
    this.xPos = value;
  }

  get x() {
    return this.xPos;
  }

  set y(value) {
    this.previousY = value - this.vy;
    this.yPos = value;
  }

  get y() {
    return this.yPos;
  }
};

This model could potentially hold variables for other things like: gravity, friction, rotation and so on or it could be extended by another class that adds these features. The reasoning behind this is simple: all games have movement but not all games need complex physics.

Let’s see an example; we’ll create another square that moves as described above and add a bit of acceleration and friction logic to our model. A classic MVC approach should do the trick:

  • SquareModel class which will extend VerletModel: it will store all data related to our square as well as an update() method which will overwrite the default one in VerletModel.
import { VerletModel } from '../../engine';

export default class SquareModel extends VerletModel {
  constructor() {
    super();
    this.temporaryX = 0;
    this.temporaryY = 0;
    this.frictionX = 0;
    this.frictionY = 0;
    this.friction = 0;
    this.accelerationX = 0;
    this.accelerationY = 0;
    this.moving = false;
    this.size = 50; // size of square
  }

  update() {
    // store the current x and y positions
    this.temporaryX = this.x;
    this.temporaryY = this.y;

    // apply friction on the x and y axes
    this.frictionX = this.vx * this.friction;
    this.frictionY = this.vy * this.friction;

    // stop movement if vx or vy dip bellow 0.1
    if(!this.moving) {
      if(Math.abs(this.vx) < 0.1 && Math.abs(this.vy) < 0.1) {
        this.accelerationX = 0;
        this.accelerationY = 0;
        this.frictionX = 0;
        this.frictionY = 0;
      }
    }

    // change x and y positions based on acceleration and friction
    this.x += this.accelerationX + this.frictionX;
    this.y += this.accelerationY + this.frictionY;

    // set the previous x and y position to the temporary x and y values
    this.previousX = this.temporaryX;
    this.previousY = this.temporaryY;
  }
};

You can see we’re using the x, y, vx and vy setters and getters from VerletModel to calculate friction which we then add to the acceleration.

  • SquareView class which will extend Scene (for now; as this should extend DisplayObject, which we don’t have yet)
import { Scene } from '../../engine';

export default class SquareView extends Scene {
  constructor(model, ctrl) {
    super();
    this.model = model;
    this.ctrl = ctrl;
  }

  update() {
    // store a reference to this scene on the model
    this.model.scene = this.scene;

    // update the model and controller
    this.model.update();
    this.ctrl.update();

    // wrap the square to the scene bounds
    Scene.wrap(this.model);
  }

  render() {
    const halfSize = this.model.size * 0.5;

    // draw a green square
    this.ctx.save();
    this.ctx.beginPath();
    this.ctx.fillStyle = '#090';
    this.ctx.fillRect(
      this.model.x,
      this.model.y,
      this.model.size,
      this.model.size
    );
    this.ctx.closePath();
    this.ctx.restore();

    // draw an orange circle inside the square, when it's moving
    if(this.model.moving) {
      this.ctx.save();
      this.ctx.beginPath();
      this.ctx.fillStyle = '#f90';
      this.ctx.arc(
        this.model.x + halfSize,
        this.model.y + halfSize,
        7,
        0,
        2*Math.PI
      );
      this.ctx.fill();
      this.ctx.closePath();
      this.ctx.restore();
    }
  }
};

The SquareView receives an instance of the SquareModel and SquareCtrl which it updates in its own update() method. You’ll also see that we’re storing a scene reference on the model, as this is needed to properly wrap the square to its parent scene. This is annoying and should be done automatically but we won’t worry about that yet.

  • SquareCtrl class which will extend Keyboard
import { Keyboard, KeyboardEvents } from '../../engine';

export default class SquareCtrl extends Keyboard {
  constructor(model) {
    super();

    const onKeyUp = () => {
      if(this.UP || this.DOWN) {
        this.model.accelerationY = 0;
        this.model.friction = 0.94;
        this.model.moving = false;
      }
      if(this.RIGHT || this.LEFT) {
        this.model.accelerationX = 0;
        this.model.friction = 0.96;
        this.model.moving = false;
      }
    }

    this.model = model;
    this.on(KeyboardEvents.KEY_UP, onKeyUp.bind(this));
  }

  update() {
    // change the accleration amount based on pressed key
    // apply friction; between 0 and 1 where:
    //   1 = no friction
    //   0 = maximum friction
    //   counter intuitive but fine for now
    // set moving flag to true
    if(this.UP) {
      this.model.accelerationY = -0.5;
      this.model.friction = 1;
      this.model.moving = true;
    }
    if(this.DOWN) {
      this.model.accelerationY = 0.5;
      this.model.friction = 1;
      this.model.moving = true;
    }
    if(this.LEFT) {
      this.model.accelerationX = -0.5;
      this.model.friction = 1;
      this.model.moving = true;
    }
    if(this.RIGHT) {
      this.model.accelerationX = 0.5;
      this.model.friction = 1;
      this.model.moving = true;
    }
  }
};

The SquareCtrl has one job: to change the acceleration based on the currently pressed key. When we release a key, the friction is set to 0.94. This value will be multiplied with our velocity and the result added to our acceleration.

In the end we’ll get a friction effect which slows down the object on every frame.

The acceleration increases by 0.5 on every frame if we keep the key pressed. We do have a bit of code duplication in our update() method but it’s fine for the purposes of this demo.

Let’s put everything together:

import { SquareModel, SquareCtrl, SquareView } from './square';
import { Game, Scene } from '../engine';

require('../../scss/styles.scss');

const scene = new Scene(20, 33, 400, 200);
const game = new Game(scene);

const squareModel = new SquareModel();
const squareCtrl = new SquareCtrl(squareModel);
const squareView = new SquareView(squareModel, squareCtrl);

scene.add([
  squareView,
]);

At this point you might be thinking: Aren’t we just adding more complexity to our code? Well yes, the complexity is increasing; some of it can’t be avoided and part of it is due to our MVC approach.

As always, there are improvements to be made here; for starters, the way the scene reference is added on the model is suboptimal, we also have code duplication in our controller and quite a bit of logic in our MVC.

We’ll slowly move away from MVC and go towards a Flux architecture as these articles progress but all things considered we did learn more about how movement works in games.

Next time we’ll create a DisplayObject class and cover: sprite sheets, sprites, animated sprites and frame rate control.

Radu B. Gaspar

This article was originally published here.

2017-01-05T12:22:38+00:00 January 5th, 2017|Categories: JavaScript, Technical, Written by our colleagues|Tags: , , , , |