Trying to collect my thoughts on level design, and more specifically, puzzle design for End of Line. I’ve started documenting all the atoms available, just like I did for The Last Rocket. (I’m borrowing my loose definition of an atom, an action/outcome pair, from Raph Koster.)
This is what I see as the current atoms available for EOL puzzles (doesn’t include new elements added late this week):
MICRO
Active
- kill yourself to get to another area of the map
- kill yourself to return to origin
- kill yourself to attract a flyer
- kill yourself to attract a hopper
- kill a flyer to prevent repair (of hoppers, terminals, and EOL)
- kill a flyer to clear a path
- kill a hopper to prevent repair (of terminals and EOL)
- kill a hopper to attract a flyer
- kill a hopper to press a switch
- kill a hopper to clear a path
- kill a terminal to attract a flyer
- kill a terminal to attract a hopper
- attract a flyer to press a switch
- attract a hopper to press a switch
- attract an ice hopper to create a path
- push a crate to clear a path
- push a crate to press a switch
- push a crate to release a switch
- push a crate onto a conveyor to...
- press a switch to lower a gate
- press a switch to raise a gate
- release a switch to lower a gate
- release a switch to raise a gate
- collect fire to cross fire
- collect fire to kill fire flyer
- collect fire to kill fire hopper
- collect fire to kill iron crate
- collect ice to cross water
- collect ice to kill ice flyer
- collect ice to kill ice hopper
Passive
- convey yourself
- convey a crate
- pass through an open gate
- pass through a destroyed iron crate
- pass over ice hopper ice path
Compound
- use gates to control hopper flow
- push crate across ice (hopper or Ice EOL created)
- push a crate into the path of a hopper to press a switch
MACRO
- kill all iron crates
- kill all flyers
- kill all hoppers
- (optional) kill all terminals (for special ending?)
- kill yourself on
- spikes
- fire (except Fire EOL)
- melting ice (except Ice EOL)
ENVIRO
- place conveyors to prevent crate movement
- place conveyors to prevent hopper movement
- kill a hopper on spikes
- a hoppers path towards a switch creates a timer
- obscure switches with crates
- obscure switches with iron crates
- obscure switches with flyers
- flyers as first responders (hoppers don't repair if a flyer is available)
And those are just the ones we’ve discovered so far. Level design, and especially puzzle design, is a time consuming process. First you need to define your mechanics and invent some props. Then you explore and learn how they interact. It’s that foundation and insight that allows you to discover interesting combinations that result in challenging puzzles.
One thing that makes a game feel more polished is “corner rounding” or “collision softening”. Especially in a top down game like End of Line. What am I on about?
You know when you miss a single tile opening by a few pixels and your character comes to a complete stop? Corner rounding sees the slight mis-alignment and nudges the character in the right direction so they don’t get snagged on the corner.
Here’s basically how we do it: Before we resolve player collisions with the foreground we test for overlap with individual tiles. Based on the size and position of the rectangle created by the overlap, we determine which direction to nudge the player in. We also take some steps to ensure that the player doesn’t lose any velocity as a result of the forgivable collision.
Here’s (kind of) what that looks like in code:
package {
import org.flixel.*;
import org.flixel.system.FlxTile;
public class Actor extends FlxSprite {
// collision softening
public var softener:Number = 1.0; // pixels to move per frame during softening
public var totalCollisions : uint = 0;
public var softenX : Number = 0;
public var softenY : Number = 0;
public function Actor(X:Number, Y:Number,SimpleGraphic:Class=null) {
super(X, Y,SimpleGraphic);
}
override public function postUpdate():void
{
// reset for next update
totalCollisions = 0;
softenX = 0;
softenY = 0;
super.postUpdate();
}
static public function softCollide(tilemap:FlxTilemap, actor:Actor):Boolean {
if (tilemap.overlapsWithCallback(actor, actor.softenCollisions)) {
// apply softening
if (actor.totalCollisions == 1) {
actor.x += actor.softenX;
actor.y += actor.softenY;
}
// save pre-collision velocity
var oldVX:Number = actor.velocity.x;
var oldVY:Number = actor.velocity.y;
var collided:Boolean = FlxG.collide(tilemap, actor);
// restore pre-collision velocity if softening was applied
if (collided && actor.totalCollisions == 1) {
actor.velocity.x = oldVX;
actor.velocity.y = oldVY;
}
return collided;
}
return false;
}
public function softenCollisionsX(tile : FlxTile, actor : Actor, softness : Number = 6) : void {
actor; // should be this
var diffX : Number = 0;
if (x < tile.x) {
// self to left
diffX = x + width - tile.x;
if (diffX <= softness) {
softenX = -softener;
}
} else {
// self to right
diffX = tile.x + tile.width - x;
if (diffX <= softness) {
softenX = softener;
}
}
}
public function softenCollisionsY(tile : FlxTile, actor : Actor, softness : Number = 6) : void {
actor; // should be this
var diffY : Number = 0;
if (y < tile.y) {
// self above
diffY = y + height - tile.y;
if (diffY <= softness) {
softenY = -softener;
}
} else {
// self below
diffY = tile.y + tile.height - y;
if (diffY <= softness) {
softenY = softener;
}
}
}
public function softenCollisions(tile : FlxTile, actor : Actor) : Boolean {
if ((x + width > tile.x) && (x < tile.x + tile.width) && (y + height > tile.y) && (y < tile.y + tile.height)) {
if (totalCollisions < 1) {
if (velocity.x == 0) {
// moving vertically
softenCollisionsX(tile, actor);
} else if (velocity.y == 0) {
// moving horizontally
softenCollisionsY(tile, actor);
}
}
totalCollisions++;
}
return true;
}
}
}
Then in our PlayState.as update() function, instead of calling:
You’ve seen Ice EOL’s ability. Fire EOL melts immovable iron crates. Here’s a quick look at how the animation evolved.
First attempt was too slow:
Better:
The follow through looked good too:
Except the trail it left created problems when destroying from above or below so I panicked and created a simpler, animation that could be used from all directions:
I hated this and went to bed, defeated. The next morning I woke up and revisited the original idea and ended on this:
I’m about to lay down a flurry of posts (some art, some code, some level design thoughts) but first: housekeeping.
Shirt and poster orders have been sent off to our friends at United Pixelworkers and Buy Olympia! These guys are handling fulfillment too, so there won’t be so much as a hiccup in our End of Line’s polishing phase.
One more thing: From any login page, non-backers can now chip in $25 and get the equivalent rewards. If you pledged $1 or $15, you can chip in the difference from Your Account page to get the same level of access. So if you or any friends feel like they’re missing out, problem solved!
At first, we didn’t add a reset key. We wanted puzzles beatable at all times. Player’s wouldn’t be able to trap themselves or remove key objects from reach.
But as our list of game objects increased, this became more and more difficult. Creating puzzles was slower because we needed to test for and prevent every unbeatable scenario. Levels also required more elements to handle these scenarios. Many rooms were cornered by conveyer belts to remove stuck boxes from walls.
Some games use player death to reset puzzles, but this wasn’t an option for End of Life: players often progress through levels by dying.
So we added a reset key. Players can now press the Return key to reset a level. Designing puzzles is faster and more focused. Levels are less cluttered.