Principles Of HTML5 Game Design

About The Authors

Mikołaj is a Full-Stack Developer (mainly focused on Node.js), working in Merixstudio, creative software house from Poznan, Poland. Member of Merixgames, a …

More about Mikołaj Stolarski & ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

In this article, Mikołaj Stolarski & Tomasz Grajewski would like to present a few ideas of how to implement different visual effects in -based HTML5 games. These examples will be based on effects they made in their game, Skytte. They will explain the basic ideas supporting them and provide the effects used in our work. You will take a look at the patterns that are commonly used to make games and game effects and also explore the theory and some code examples supporting these patterns.

Visual effects in games define their overall look and feel, and gameplay. Players are attracted to high visual quality, which generate more traffic and reach. It’s key for creating successful games and providing a lot of fun for players.

In this article, we’d like to present a few ideas of how to implement different visual effects in <canvas>-based HTML5 games. These examples will be based on effects we made in our game, Skytte. We will explain the basic ideas supporting them and provide the effects used in our work.

What You Will Learn

Before we get going, let’s set out the things we hope you’ll learn from this article:

  • Basic game design We’ll look at patterns that are commonly used to make games and game effects like: game loops, sprites, collisions, and particle systems.
  • Basic implementation of visual effects We will also explore the theory and some code examples supporting these patterns.

Common Patterns

Let’s start with some common patterns and elements used in game development.

Sprites

These are simply 2-D images that represent an object in the game. Sprites can be used for static objects, but also animated objects, when each sprite represents a frame of sequential animation. They can also be used for making user interface elements.

Usually games contain between dozens and hundreds of sprites. To reduce memory usage and the processing power needed to handle these images, many games use sprite sheets.

Sprite Sheets

These are used to group a set of single sprites in one image. This reduces the amount of files in the game, resulting in reduced memory and processing power usage. Sprite sheets contain many single sprites stacked next to each other in rows and columns, and like the sprites they contain can be used statically or for animation.

Spritesheet example
Sprite sheet example. (Image credit: Kriplozoik)

Here’s an article from Code + Web to help you better understand the benefits of using sprite sheets.

Game Loops

It’s important to realize that game objects don’t really move on screen. An illusion of movement is achieved by rendering a snapshot of a game world to the screen, advancing the game time a small amount (usually 1/60th of a second), and then rendering things again. This is literally a stop-motion effect and is used in both 2-D and 3-D games. A game loop is a mechanism that implements this stop-motion. It’s the main component needed to run a game. It runs continuously over time, performing various tasks. On each iteration it processes user input, moves entities, checks for collisions, and renders the game (preferably in this order). It also controls game time that’s elapsed between frames.

Below is a very basic game loop in JavaScript:

var lastUpdate;

function tick() {
  var now = window.Date.now();

  if (lastUpdate) {
    var elapsed = (now-lastUpdate) / 1000;
    lastUpdate = now;

    // Update all game objects here.
    update(elapsed);
    // ...and render them somehow.
    render();
  } else {
    // Skip first frame, so elapsed is not 0.
    lastUpdate = now;
  }

  // This makes the `tick` function run 60 frames per second (or slower, depends on monitor's refresh rate).
  window.requestAnimationFrame(tick);
};

Note that the above example is very simplistic. It uses variable delta time (the elapsed variable) and it’s recommended to upgrade this code to use fixed delta time. See this article for more details.

Collision Detection

Collision detection refers to finding the intersections between objects. This is essential for many games because it’s used to detect if a player hits a wall or a bullet hits an enemy, and so on. When a collision is detected it can be used for game logic; for example, when a bullet hits the player, the health score is reduced by 10 points.

There are a lot of collision detection algorithms, and because it’s a performance-heavy operation, it’s important to choose the best method wisely. To read more about collision detection, algorithms and how they can be implemented, here’s an article from MDN.

Particles And Particle Systems

Particles are basically sprites used by a particle system. In game development, a particle system is a component that consists of a particle emitter and particles assigned to that emitter. It’s used to simulate various effects, like fire, explosions, smoke, and rain effects. Particles are emitted over time and each emitter has its own parameters to define various variables used for the simulated effect, such as velocity, color, a particle’s lifetime or duration, gravity, friction, and wind speed.

Euler Integration

Euler integration is a method for numerically integrating the equations of motion. Each object’s position is calculated based on its velocity, mass and force, and needs to be recalculated for each tick in the game loop. The Euler method is the most basic and useful for games like side-scrolling shooters, but there are also other methods like Verlet integration and RK4 integration which are better for other tasks. Below I’ll show a simple implementation of the idea.

You need a basic structure to hold an object’s position, velocity and other movement-related data. We propose two identical structures, but each with different meaning in the world’s space: point and vector. Usually game engines use some kind of vector class, but the distinction between points and vectors is very important and greatly improves code readability (for example, you calculate distance not between two vectors, but two points, which is more natural).

Point

Simply, it represents an element in two-dimmensional space with x and y coordinates which define where the point is located in that space.

function point2(x, y) {
  return {'x': x || 0, 'y': y || 0};
}

Vector

A vector is a geometric object that has length (or magnitude) and direction. In 2-D games vectors are used mostly to describe forces (e.g. gravity, air resistance and wind) and velocities, as well as proscribing movements or how light reflects off an object. Vectors have many uses.

function vector2(x, y) {
  return {'x': x || 0, 'y': y || 0};
}

The above functions create new two-dimensional vectors and points. Internally we don’t use the new operator in this case in JavaScript to gain a lot of performance. Also note that there are some third-party libraries available that you could use to manipulate vectors (glMatrix is a good candidate for this).

What follows are some very common functions used on the two-dimensional structures defined above. First, calculating the distance between two points:

point2.distance = function(a, b) {
  // The x and y variables hold a vector pointing from point b to point a.
  var x = a.x - b.x;
  var y = a.y - b.y;
  // Now, distance between the points is just length (magnitude) of this vector, calculated like this:
  return Math.sqrt(x*x + y*y);
};

The magnitude (length) of a vector can be calculated directly from the last line of the above function like this:

vector2.length = function(vector) {
  return Math.sqrt(vector.x*vector.x + vector.y*vector.y);
};
Vector length
Vector length. (View large version)

Normalization of vectors is also very handy. The function below resizes a vector so it becomes a unit vector; that is, its length is 1, but its direction is maintained.


vector2.normalize = function(vector) {
  var length = vector2.length(vector);

  if (length > 0) {
    return vector2(vector.x / length, vector.y / length);
  } else {
    // zero-length vectors cannot be normalized, as they do not have direction.
    return vector2();
  }
};
Vector normalization
Vector normalization. (View large version)

Another useful case is to have a unit vector whose direction is pointing from one location to another:

// Note that this function is different from `vector2.direction`.
// Please don't confuse them.
point2.direction = function(from, to) {
  var x = to.x - from.x;
  var y = to.y - from.y;
  var length = Math.sqrt(x*x + y*y);

  if (length > 0) {
    return vector2(x / length, y / length);
  } else {
    // `from` and `to` are identical
    return vector2();
  }
};

The dot product is an operation on two vectors (usually unit vectors), which returns a scalar number representing the relation between angles of those vectors.

vector2.dot = function(a, b) {
  return a.x*b.x + a.y*b.y;
};
Vector dot product
Vector dot product.

The dot product is a length of a vector a projected on a vector b. A returned value of 1 means that both vectors point in the same direction. A value of -1 means that vector a points in the opposite direction of vector b. A value of 0 means that vector a is perpendicular to vector b.

Here’s an example of an entity class, so other objects can inherit from it. Only basic properties related to movement are described.

function Entity() {
  ...
  // Center of mass usually.
  this.position = point2();
  // Linear velocity.
  // There is also something like angular velocity, not described here.
  this.velocity = vector2();
  // Acceleration could also be named `force`, like in the Box2D engine.
  this.acceleration = vector2();
  this.mass = 1;
  ...
}

You can use pixels or meters as the unit in your game. We encourage you to use meters, as it’s easier to balance things out during development. Velocity, then, should be meters per second, and acceleration should be meters per second squared.

When using a third-party physics engine, you just store a reference to a physical body (or set of bodies) in your entity class. Then, the physics engine stores the mentioned properties, like position and velocity, inside each body for you.

Basic Euler integration looks like this:

acceleration = force / mass
velocity += acceleration
position += velocity

The code above must be executed in each frame for each object in the game. Here is a basic implementation of the above in JavaScript:

Entity.prototype.update = function(elapsed) {
  // Acceleration is usually 0 and is set from the outside.
  // Velocity is an amount of movement (meters or pixels) per second.
  this.velocity.x += this.acceleration.x * elapsed;
  this.velocity.y += this.acceleration.y * elapsed;

  this.position.x += this.velocity.x * elapsed;
  this.position.y += this.velocity.y * elapsed;

  ...

  this.acceleration.x = this.acceleration.y = 0;
}

elapsed is the amount of time in seconds that has passed since the last frame (since the last call to this method). For games running at 60 frames per second, the elapsed value is usually 160 of a second, which is 0.016(6)s.

The article on delta time mentioned earlier also covers this problem.

To move objects, you can change their acceleration or velocity. Two functions shown below should be used for this purpose:

Entity.prototype.applyForce = function(force, scale) {
  if (typeof scale === 'undefined') {
    scale = 1;
  }
  this.acceleration.x += force.x * scale / this.mass;
  this.acceleration.y += force.y * scale / this.mass;
};

Entity.prototype.applyImpulse = function(impulse, scale) {
  if (typeof scale === 'undefined') {
    scale = 1;
  }
  this.velocity.x += impulse.x * scale / this.mass;
  this.velocity.y += impulse.y * scale / this.mass;
};

To move an object to the right you could do this:

// 10 meters per second in the right direction (x=10, y=0).
var right = vector2(10, 0);

if (keys.left.isDown)
  // The -1 inverts a vector, i.e. the vector will point in the opposite direction,
  // but maintain magnitude (length).
  spaceShip.applyImpulse(right, -1);
if (keys.right.isDown)
  spaceShip.applyImpulse(right, 1);

Note that objects set in motion stay in motion. You need to implement some kind of deceleration to stop a moving object (air drag or friction, maybe).

Weapon Effects

Now I’ll explain how certain weapon effects are made in our HTML5 game, Skytte.

Plasma

This is the most basic weapon in our game, just one shot each time. There are no special algorithms used for this weapon. When a plasma bullet is fired the game simply draws a single sprite that’s rotated over time.

A simple plasma bullet can be spawned like this:

// PlasmaProjectile inherits from Entity class
var plasma = new PlasmaProjectile();

// Move right (assuming that X axis is pointing right).
var direction = vector2(1, 0);

// 20 meters per second.
plasma.applyImpulse(direction, 20);

Blaster

This weapon is a little more complex. It also draws simple sprites as bullets but there’s some code that spreads them out a little and applies a random speed. This gives a more devastating feeling to this weapon, so players feel they can inflict more damage than with the plasma weapon and have better crowd control when among enemies.

The code works similarly to the plasma weapon code, but it spawns three bullets and each has a slightly different direction.

// BlaserProjectile inherits from Entity class
var topBullet = new BlasterProjectile();  // This bullet will move slightly up.
var middleBullet = new BlasterProjectile();  // This bullet will move horizontally.
var bottomBullet = new BlasterProjectile();  // This bullet will move slightly down.
var direction;

// Angle 0 is pointing directly to the right.
// We start with the bullet moving slightly upwards.
direction = vector2.direction(radians(-5));  // Convert angle to an unit vector
topBullet.applyImpulse(direction, 30);

direction = vector2.direction(radians(0));
middleBullet.applyImpulse(direction, 30);

direction = vector2.direction(radians(5));
middleBullet.applyImpulse(direction, 30);

Some math functions are needed for the above code to work:

function radians(angle) {
  return angle * Math.PI / 180;
}

// Note that this function is different from `point2.direction`.
// Please don't confuse them.
vector2.direction = function(angle) {
  /*
   * Converts an angle in radians to a unit vector. Angle of 0 gives vector x=1, y=0.
   */
  var x = Math.cos(angle);
  var y = Math.sin(angle);
  return vector2(x, y);
};

Ray

This one’s interesting. The weapon shoots a laser ray but it’s procedurally generated in each frame (this will be explained later). To detect hits, it creates a rectangular collider that deals damage every second for as long as it collides with the enemy.

Rockets

This weapon shoots guided missiles. The rocket is a sprite with a particle emitter attached to its end. There’s also some more sophisticated logic, like searching for the nearest enemy or limiting the turn value for a rocket to give it less maneuverability. Also, rockets don’t start to seek for enemy targets immediately – they fly straight for a time to avoid unrealistic behavior.

Rockets in Skytte move toward their nearest neighbor. This is achieved by calculating the proper force needed for the projectile to move in any given direction. To avoid moving only in straight lines the force calculated shouldn't be too big.

Assuming that Rocket is a class inheriting from the Entity class described earlier.

Rocket.prototype.update = function(elapsed) {
  var direction;

  if (this.target) {
    // Assuming that `this.target` points to the nearest enemy ship.
    direction = point2.direction(this.position, this.target.position);
  } else {
    // No target, so fly ahead.
    // This will fail for objects that are still, so remember to apply some initial velocity when spawning rockets.
    direction = vector2.normalize(this.velocity);
  }

  // You can use any number here, depends on the speed of the rocket, target and units used.
  this.applyForce(direction, 10);

  // Simple inheritance here, calling parent's `update()`, so rocket actually moves.
  Entity.prototype.update.apply(this, arguments);
};

Flak

Flak was designed to shoot many small bullets (something like a shotgun), which are little dot sprites. It has some specific logic to randomly generate the position of these dots within a cone-shaped area.

Flak weapon bullets cone area
Flak weapon bullets cone area.

To generate random points in a cone-shaped area:

// Firstly get random angle in degrees in the allowed span. Note that the span below always points to the right.
var angle = radians(random.uniform(-40, 40));

// Now get how far from the barrel the projectile should spawn.
var distance = random.uniform(5, 150);

// Join angle and distance to create an offset from the gun's barrel.
var direction = vector2.direction(angle);
var offset = vector2(direction.x * distance, direction.y * distance);

// Now calculate absolute position in the game world (you need a position of the barrel for this purpose):
var position = point2.move(barrel, offset);

The random.uniform() function returns a random floating point number between two values. A simple implementation could look like this:

random.uniform = function(min, max) {
  return min + (max-min) * Math.random();
};

Electro

Electro is fancy weapon that shoots lightning at enemies within a particular radius. It has a limited range but can shoot at several enemies at once and always hits successfully. It uses the same algorithm to draw curved lines to simulate lightning as the ray weapon but with a higher curve factor.

Techniques Used

Procedural Curved Line

To create the laser beam effect and electro weapon we've developed an algorithm to count and transform the linear distance between the player's ship and the enemy. In other words, we measured the distance between two objects, found the middle point and moved it randomly alongside the section. We repeat this action for every new section created.

To draw these sections we use HTML5 `` draw function lineTo(). To achieve the glowing color we used multiple lines drawn over one another with more opaque color and a higher stroke width.

Procedural curved line
Procedural curved line. (View large version)

To find and offset a point between two other points:

var offset, midpoint;

midpoint = point2.midpoint(A, B);

// Calculate an unit-length vector pointing from A to B.
offset = point2.direction(A, B);

// Rotate this vector 90 degrees clockwise.
offset = vector2.perpendicular(offset);

// We want our offset to work in two directions perpendicular to the segment AB: up and down.
if (random.sign() === -1) {
  // Rotate offset by 180 degrees.
  offset.x = -offset.x;
  offset.y = -offset.y;
}

// Move the midpoint by an offset.
var offsetLength = Math.random() * 10;  // Offset by 10 pixels for example.
midpoint.x += offset.x * offsetLength;
midpoint.y += offset.y * offsetLength;

Below are functions used in the above code:
point2.midpoint = function(a, b) {
  var x = (a.x+b.x) / 2;
  var y = (a.y+b.y) / 2;
  return point2(x, y);
};

vector2.perpendicular = function(v) {
  /*
   * Rotates a vector by 90 degrees clockwise.
   */
  return vector2(-v.y, v.x);
};

random.sign = function() {
  return Math.random() < 0.5 ? -1 : 1;
};

Finding Nearest Neighbor

To find the nearest enemy for a rocket and the electro weapon, we iterate over an array of active enemies and compare their positions with the position of a rocket, or the shooting point in the case of the electro weapon. When a rocket locks on its target, it flies toward it until it hits or flies off the screen. For the electro weapon, it waits for a target to be in range.

A basic implementation may look like this:

function nearest(position, entities) {
  /*
   * Given position and an array of entites, this function finds which entity is closest
   * to `position` and distance.
   */
  var distance, nearest = null, nearestDistance = Infinity;

  for (var i = 0; i < entities.length; i++) {
    // Allow list of entities to contain the compared entity and ignore it silently.
    if (position !== entities[i].position) {
      // Calculate distance between two points, usually centers of mass of each entity.
      distance = point2.distance(position, entities[i].position);

      if (distance < nearestDistance) {
        nearestDistance = distance;
        nearest = entities[i];
      }
    }
  }

  // Return the closest entity and distance to it, as it may come handy in some situations.
  return {'entity': nearest, 'distance': nearestDistance};
}

Conclusion

These topics cover only the basic ideas that support them. We hope after reading the article you now have a better idea of how to start developing such things. Check out the resources below and try doing something similar yourself.

Resources

Further Reading

Smashing Editorial (rb, ml, og, mrn)