Home Blog Index
Joseph Petitti —

Designing an extensible power up system

Last term I started working on a simple procedurally-generated twinstick shooter, tentatively called Proc Cave Game, with two of my friends as our final project for AI for Interactive Media and Games. Although it started as just another assignment, we continued to work on it over winter break, expanding on what we had started to make a fun and interesting game.

A screenshot of Proc Cave Game

Background

Instead of using an existing engine or framework we decided to implement everything from scratch. The three of us were most familiar with JavaScript, and developing for the web makes a project much more accessible, so we opted for using plain ECMAScript 2015 and the HTML canvas API built into every browser.

Cole Granof did most of the work on the engine, writing a game loop that could run at a constant speed independent of your monitor's refresh rate and robust kinematic simulations based on acceleration and drag.

Matt Puentes worked on an efficient collision system, and an algorithm to connect the randomly generated cave environments using a minimum spanning tree.

I, meanwhile, focused on designing the power up system.

Guiding principles

The Binding of Isaac is another procedurally generated top-down twinstick shooter, and it heavily inspired the design of Proc Cave Game. Something I particularly like about Isaac is the way different items synergize with each other to create unique gameplay. In The Binding of Isaac, most of these synergies are hard-coded by the developer, but I wanted power ups in our game to work together automatically.

I also wanted every power up to be able to "stack," so that getting the same power up twice would make the ability gained from it stronger. This way we could have different levels of power ups, and finding the same one twice wouldn't be disappointing for players.

Finally, I wanted all power ups to be able to apply to both the player and the enemies. This would allow us to apply random power ups to enemies to make them stronger, as well as designing new enemy types around certain power ups.

Implementation

The PowerUp super-class is pretty basic, but defines the interface that individual power ups must implement. Here are some of the methods it defines:

export class PowerUp extends Entity { constructor( position, magnitude = 1, powerUpClass, description ); /** * called when a creature picks up this power up */ apply(creature); /** * tests whether the creature is at * max level for this power up * @return boolean */ isAtMax(creature); /** * what to do when this isn't applied due * to the creature being at max level */ overflowAction(creature); /** * draw the power up in the world as a shiny * circle with a letter in the middle */ draw(); }

You can see the full implementation on GitHub. As you can see, each power up has a magnitude, a number from 1 to 5 that describes how powerful it is. This enforces the design principle that all power ups have to be able to scale, and multiple copies of the same power up stack by essentially having their total combined magnitude.

Each power up also needs to have some way of checking whether the Creature (the player character or an enemy) that picked it up is already at the max level for that power up. For example, the power up QuickShot, which increases bullet speed, just checks whether the creature's existing bullet speed is above a set limit already.

If the creature is close to the max, but not quite at it yet, the isAtMax method can trim the magnitude of the power up so that it still has an affect without going over the limit. Here's how QuickShot does it:

isAtMax(creature) { // bullet speed is already too high if (creature.bulletSpeed >= MAX_BULLET_SPEED) { return true; } // see if we need to trim magnitude const availMag = Math.floor( Math.abs( MAX_BULLET_SPEED - creature.bulletSpeed ) / BULLET_SPEED_FACTOR ); if (availMag < 1) return true; this.magnitude = Math.min( availMag, this.magnitude ); return false; }

MAX_BULLET_SPEED and BULLET_SPEED_FACTOR are constants that determine the maximum bullet speed, and by how much bullet speed is increased by each point of QuickShot respectively.

Applying simple power ups like this are easy, just increase a number by the power up's magnitude times BULLET_SPEED_FACTOR. But what about more complex power ups? FlameThrower makes the player's bullets light enemies on fire. We could give each bullet an attribute causesBurning, but this would be annoying to maintain as we added more an more power ups. Instead, I used one of the most useful features of JavaScript: first-class functions.

In JavaScript, functions are treated as first-class citizens, meaning they can be used in the same way as any other data type: passed as arguments to other functions, returned from functions, assigned to variables, and stored in data structures. This means we can create an array of functions to execute whenever a bullet hits an enemy, and simply add a function to it that causes burning.

See the source code for FlameThrower for an example of this. This approach makes the synergy I mentioned above much easier.

When you get an Xplode power up, it makes your bullets explode into a ring of sub-bullets. By simply passing these sub-bullets the same array of onHitEnemy functions that the parent had, they will inherit the same flamethrower effect.

Now you can have flaming, exploding bullets that bounce off walls, and the game logic does all the work of combining these effects automatically. It can lead to some really complex combinations and emergent gameplay.

Proc Cave Game is released as free software, so you can download the source code, inspect, modify, and redistribute it as much as you want. It's available as source code on GitHub, or you can try it out on the web here.