JONNYMATIC
BACK TO LOG INDEX
LOG_ENTRY :: GAME-DEVELOPMENT / SOFTWARE

GameDev: How to Implement a "Camera Shake" Effect

Recovered production post, originally published 2016 — ported into the new stack.

In modern videogames, a camera, or “screen”, shake can add some really nice aesthetic touch and tactility to your game at a very low cost. When I went to research some concrete implementation details, however, I failed to find many examples to guide my efforts. I decided to venture forth and write up a quick article on how to do so for most 2D games.

How “Shaking” Works

I originally started thinking about this topic while driving home from work on the freeway. I noticed that my side view mirror was loose and the vibrations of the road made the image unstable. I began working on the theory that a shake is really just a large vibration — representable by sine and cosine waves with a frequency and amplitude. A camera shake is much more violent and harsh than a cosine wave, so I figured it could be modeled with randomized amplitudes at a fixed frequency, linearly interpolated, with a decay function so the effect eventually wears off.

LIVE_DEMO.EXE

As you can see by the demo above, we get a pretty flexible shaking effect by allowing for different frequencies, durations and amplitudes.

The Code

Here is the initialization code for generating a shake graph for 1D. By the end of the constructor, we should have an array filled with randomized samples that we will use for the motion.

code :: javascript
/**
* @class Initializes a 1D shaking function
* @param {int} duration The length of the shake in milliseconds
* @param {int} frequency The frequency of the shake in Hertz
*/
var Shake = function(duration, frequency)
{
  this.duration = parseInt(duration);
  this.frequency = parseInt(frequency);

  // The sample count = number of peaks/valleys in the Shake
  var sampleCount = (duration/1000) * frequency;

  // Populate the samples array with randomized values between -1.0 and 1.0
  this.samples = [];
  for(var i = 0; i < sampleCount; i++) {
      this.samples.push(Math.random() * 2 - 1);
  }

  this.startTime = null;
  this.t = null;
  this.isShaking = false;
};

To generate the graphs of the shake function, we need to be able to retrieve the amplitude at any time (t) during the duration of the shake:

code :: javascript
Shake.prototype.amplitude = function(t)
{
  if(t == undefined) {
      if(!this.isShaking) return 0;
      t = this.t;
  }

  // Get the previous and next sample
  var s = t / 1000 * this.frequency;
  var s0 = Math.floor(s);
  var s1 = s0 + 1;

  // Get the current decay
  var k = this.decay(t);

  return (this.noise(s0) + (s - s0)*(this.noise(s1) - this.noise(s0))) * k;
};

amplitude() calls out to noise(), which is nothing more than a lookup into the samples array populated by the constructor above — with one important detail: it returns 0 once you ask for a sample past the end of the array, rather than throwing or clamping to the last value. That’s what makes the interpolation in amplitude() taper smoothly to 0 right at the tail of the shake, on top of the separate decay() multiplier below.

code :: javascript
Shake.prototype.noise = function(s)
{
  // Retrieve the randomized value from the samples
  if(s >= this.samples.length) return 0;
  return this.samples[s];
};

Lastly, the decay function — important as it smoothly transitions the shaking back to a still screen. It’s simply a linear function of t that starts at 1.0 and ends at 0.0 when t = duration.

code :: javascript
Shake.prototype.decay = function(t)
{
  // Linear decay
  if(t >= this.duration) return 0;
  return (this.duration - t) / this.duration;
};

Rand vs. Perlin

In my research for this article, I found a lot of posts online saying that randomized numbers couldn’t produce an effective screen shake and that perlin noise is the only way to make it seem “natural”. Well I love perlin noise, but I really don’t see the need for it here. A screen shake is a violent and haphazard function of time and the granularity one might gain from a perlin noise function is lost. With the right settings, an array of randomized numbers does the trick just fine.

Want to poke at the code directly? The demo above is also published as a standalone, dependency-free page — download it and open index.html with no build step, no server: view source / run locally →

recovered from the pre-migration archive — jonnymatic.com