In this tutorial, we’ll implement fully destructible pixel terrain, in the style of games like Cortex Command and Worms. You’ll learn how to make the world explode wherever you shoot it – and how to make the “dust” settle on the ground to create new land.
Note: Although this tutorial is written in Processing and compiled with Java, you should be able to use the same techniques and concepts in almost any game development environment.
Final Result Preview
You can play the demo yourself, too. WASD to move, left click to shoot explosive bullets, right click to spray pixels.
Step 1: The Terrain
In our sidescrolling sandbox, the terrain will be the core mechanic of our game. Similar algorithms often have one image for the terrain’s texture, and another as a black and white mask to define which pixels are solid. In this demo, the terrain and its texture all are one image, and pixels are solid based on whether or not they’re transparent. The mask approach would be more appropriate if you want to define each pixel’s properties, like how likely it’ll dislodge or how bouncy the pixel will be.
To render the terrain, the sandbox draws the static pixels first, then the dynamic pixels with everything else on top.
The terrain also has methods for finding out whether a static pixel at a location is solid or not, and methods for removing and adding pixels. Probably the most effective way of storing the image is as a 1 dimensional array. Getting a 1D index from a 2D coordinate is pretty simple:
index = x + y * width
In order for dynamic pixels to bounce, we’ll need to be able to find out the surface normal at any point. Loop through a square area around the desired point, find all the solid pixels nearby and average their position. Take a vector from that position to the desired point, reverse and normalize it. There’s your normal!
The black lines represent the normals to the terrain at various points.
Here’s how that looks in code:
normal(x,y) { Vector avg for x = -3 to 3 // 3 is an arbitrary number for y =-3 to 3 // user larger numbers for smoother surfaces if pixel is solid at (x + w, y + h) { avg -= (x,y) } } } length = sqrt(avgX * avgX + avgY * avgY) // distance from avg to the center return avg/length // normalize the vector by dividing by that distance }
Step 2: The Dynamic Pixel and Physics
The “Terrain” itself stores all of the non-moving static pixels. Dynamic pixels are pixels currently in motion, and are stored separately from the static pixels. As the terrain explodes and settles, pixels get switched between static and dynamic states as they dislodge and collide. Each pixel is defined by a number of properties:
- Position and velocity (required for the physics to work).
- Not just the location, but also the pixel’s previous location. (We can scan between the two points to detect collisions.)
- Other properties include the pixel’s color, stickiness, and bounciness.
In order for the pixel to move, its position needs to be forwarded with its velocity. Euler integration, while inaccurate for complex simulations, is simple enough for us to efficiently move our particles:
position = position + velocity * elapsedTime
The elapsedTime
is the amount of time elapsed since the last update. The accuracy of any simulation can be entirely broken if the elapsedTime
is too variable or too large. This isn’t as much of an issue for dynamic pixels, but it will be for other collision detection schemes.
We’ll use fixed-size timesteps, by taking the elapsed time and splitting it up into constant sized chunks. Each chunk is a full “update” to the physics, with any left over being sent into the next frame.
elapsedTime = lastTime - currentTime lastTime = currentTime // reset lastTime // add time that couldn't be used last frame elapsedTime += leftOverTime // divide it up in chunks of 16 ms timesteps = floor(elapsedTime / 16) // store time we couldn't use for the next frame. leftOverTime = elapsedTime - timesteps for (i = 0; i < timesteps; i++) { update(16/1000) // update physics }
Step 3: Collision Detection
Detecting collisions for our flying pixels is as simple as drawing some lines.
Bresenham’s line algorithm was developed in 1962 by a gentleman named Jack E. Bresenham. To this day, it’s been used for drawing simple aliased lines efficiently. The algorithm sticks strictly to integers and uses mostly addition and subtraction in order to efficeintly plot lines. Today we’ll use it for a different purpose: collision detection.
I’m using code borrowed from an article on gamedev.net. While most implementations of Bresenham’s line algorithm reorders the drawing order, this particular one allows us to always scan from start to end. The order is important for collision detection, otherwise we’ll be detecting collisions at the wrong end of the the pixel’s path.
The slope is an essential part of Bresenham’s line algorithm. The algorithm works by splitting up the slope into its “rise” and “run” components. If, for example, the line’s slope were 1/2, we can plot the line by placing two dots horizontally, going up (and right) one, and then two more.
The algorithm I’m showing here accounts for all scenarios, whether the lines has a positive or negative slope or if it’s vertical. The author explains how he derives it on gamedev.net.
rayCast(int startX, int startY, int lastX, int lastY) { int deltax = (int) abs(lastX - startX) int deltay = (int) abs(lastY - startY) int x = (int) startX int y = (int) startY int xinc1, xinc2, yinc1, yinc2 // Determine whether x and y is increasing or decreasing if (lastX >= startX) { // The x-values are increasing xinc1 = 1 xinc2 = 1 } else { // The x-values are decreasing xinc1 = -1 xinc2 = -1 } if (lastY >= startY) { // The y-values are increasing yinc1 = 1 yinc2 = 1 } else { // The y-values are decreasing yinc1 = -1 yinc2 = -1 } int den, num, numadd, numpixels if (deltax >= deltay) { // There is at least one x-value for every y-value xinc1 = 0 // Don't change the x when numerator >= denominator yinc2 = 0 // Don't change the y for every iteration den = deltax num = deltax / 2 numadd = deltay numpixels = deltax // There are more x-values than y-values } else { // There is at least one y-value for every x-value xinc2 = 0 // Don't change the x for every iteration yinc1 = 0 // Don't change the y when numerator >= denominator den = deltay num = deltay / 2 numadd = deltax numpixels = deltay // There are more y-values than x-values } int prevX = (int)startX int prevY = (int)startY for (int curpixel = 0; curpixel <= numpixels; curpixel++) { if (terrain.isPixelSolid(x, y)) return (prevX, prevY) and (x, y) prevX = x prevY = y num += numadd // Increase the numerator by the top of the fraction if (num >= den) { // Check if numerator >= denominator num -= den // Calculate the new numerator value x += xinc1 // Change the x as appropriate y += yinc1 // Change the y as appropriate } x += xinc2 // Change the x as appropriate y += yinc2 // Change the y as appropriate } return null // nothing was found }
Step 4: Collision Handling
The dynamic pixel can do one of two things during a collision.
- If it’s moving slowly enough, the dynamic pixel is removed and a static one is added to the terrain where it collided. Sticking would be our simplest solution. In Bresenham’s line algorithm, it’s best to keep track of a previous point and a current point. When a collision is detected, the “current point” will be the first solid pixel the ray hits, while the “previous point” is the empty space just prior to it. The previous point is exactly the location wehre we need to stick the pixel.
- If it’s moving too fast, then we bounce it off the terrain. This is where our surface normal algorithm comes in! Reflect the ball’s initial velocity across the normal in order to bounce it.
The angle either side of the normal is the same.
// Project velocity onto the normal, multiply by 2, and subtract it from velocity normal = getNormal(collision.x, collision.y) // project velocity onto the normal using dot product projection = velocity.x * normal.x + velocity.y * normal.y // velocity -= normal * projection * 2
Step 5: Bullets and Explosions!
Bullets act exactly like dynamic pixels. Motion is integrated the same way, and collision detection uses the same algorithm. Our only difference is the collision handling
After a collision is detected, bullets explode by removing all static pixels within a radius, and then placing dynamic pixels in their place with their velocities pointed outwards. I use a function to scan a square area around an explosion’s radius to find out which pixels to dislodge. Afterwards, the pixel’s distance from the center is used to establish a speed.
explode(x,y, radius) { for (xPos = x - radius; xPos <= x + radius; xPos++) { for (yPos = y - radius; yPos <= y + radius; yPos++) { if (sq(xPos - x) + sq(yPos - y) < radius * radius) { if (pixel is solid) { remove static pixel add dynamic pixel } } } } }
Step 6: The Player
The player isn’t a core part of the destructable terrain mechanic, but it does involve some collision detection which will be definitely relevant to problems that’ll arrive in the future. I’ll explain how collision is detected and handled in the demo for the player.
- For each edge, loop from one corner to the next, checking each pixel.
- If the pixel is solid, start at the center of the player and scan towards that pixel into you hit a solid pixel.
- Move the player away from the first solid pixel you hit.
Step 7: Optimizing
Thousands of pixels are being handled at once, resulting in quite a bit of strain on the physics engine. Like anything else, to make this fast I’d recommend using a language that’s reasonably fast. The demo is compiled in Java.
You can do things to optimize on the algorithm level as well. For example, the number of particles from explosions can be reduced by lower the destruction resolution. Normally we find every pixel and turn it into a 1×1 dynamic pixel. Instead, scan every 2×2 pixels, or 3×3, and launch a dynamic pixel of that size. In the demo we use 2×2 pixels.
If you are using Java, garbage collection will be an issue. The JVM will periodically find objects in memory that aren’t being used anymore, like the dynamic pixels that are discarded in exchange for static pixels, and try and get rid of those to make room for more objects. Deleting objects, tons of objects, takes time though, and every time the JVM does a cleanup, our game will freeze briefly.
One possible solutions it to use a cache of some sort. Instead of creating/destroying objects all the time, you can simply hold dead objects (like dynamic pixels) to be reused later.
Use primitives wherever possible. For example, using objects for positions and velocities is going to make things a little harder for the garbage collection. It would be even better if you could store everything as primitives in one-dimensional arrays.
Step 8: Making it Your Own
There are many different directions you can take with this game mechanic. Features can be added and customized to match any game style you’d like.
For example, collisions between dynamic and static pixels can be handled differently. A collision mask under the terrain can be used to define each static pixel’s stickiness, bounciness, and strength, or likelihood of getting dislodged by an explosion.
There are a variety of different things you can do to guns as well. Bullets can be given a “penetration depth,” to allow it to move through so many pixels before exploding. Traditional gun mechanics can also be applied, like a varied rate of fire or, like a shotgun, multiple bullets can be fired at once. You can even, like for the bouncy particles, have bullets bounce off of metal pixels.
Conclusion
2D terrain destruction isn’t completely unique. For example, the classics Worms and Tanks remove parts of the terrain on explosions. Cortex Command utilizes similar bouncy particles that we use here. Other games might as well, but I haven’t heard of them yet. I look forward in seeing what other developers will do with this mechanic.
Most of what I’ve explained here is fully implemented in the demo. Please take a look at its source if anything seems ambiguous or confusing. I’ve added comments to the source to make it as clear as possible. Thanks for reading!