In this series of tutorials, I'll show you how to make a Geometry Wars-inspired twin-stick shooter, with neon graphics, crazy particle effects, and awesome music, for iOS using C++ and OpenGL ES 2.0. In this final part, we'll add the background grid which warps based on the in-game action.

Overview

In the series so far we've created the gameplay, virtual gamepad, and particle effects. In this final part, we will create a dynamic, warping background grid.

Warning: Loud!

As mentioned in the previous part, you'll notice a dramatic drop in framerate if you're still running the code in debug mode. See that tutorial for details on how to switch to release mode for full compiler optimization (and a faster build).

The Warping Grid

One of the coolest effects in Geometry Wars is the warping background grid. We'll examine how to create a similar effect in Shape Blaster. The grid will react to bullets, black holes, and the player respawning. It's not difficult to make and it looks awesome.

We'll make the grid using a spring simulation. At each intersection of the grid, we'll put a small weight and attach a spring on each side. These springs will only pull and never push, much like a rubber band. To keep the grid in position, the masses at the border of the grid will be anchored in place. Below is a diagram of the layout.

We'll create a class called Grid to create this effect. However, before we work on the grid itself, we need to make two helper classes: Spring and PointMass.

The PointMass Class

The PointMass class represents the masses to which we will attach the springs. Springs never connect directly to other springs; instead, they apply a force to the masses they connect, which in turn may stretch other springs.

There are a few interesting points about this class. First, notice that it stores the inverse of the mass, 1 / mass. This is often a good idea in physics simulations because physics equations tend to use the inverse of the mass more often, and because it gives us an easy way to represent infinitely heavy, immovable objects by setting the inverse mass to zero.

Second, the class also contains a damping variable. This is used roughly as friction or air resistance; it gradually slows the mass down. This helps make the grid eventually come to rest and also increases the stability of the spring simulation.

The PointMass::update() method does the work of moving the point mass each frame. It begins by doing symplectic Euler integration, which just means we add the acceleration to the velocity and then add the updated velocity to the position. This differs from standard Euler integration in which we would update the velocity after updating the position.

Tip: Symplectic Euler is better for spring simulations because it conserves energy. If you use regular Euler integration and create springs without damping, they will tend to stretch further and further each bounce as they gain energy, eventually breaking your simulation.

After updating the velocity and position, we check whether the velocity is very small, and if so we set it to zero. This can be important for performance due to the nature of denormalized floating-point numbers.

(When floating-point numbers get very small, they use a special representation called a denormalized number. This has the advantage of allowing floats to represent smaller numbers, but it comes at a price. Most chipsets can't use their standard arithmetic operations on denormalized numbers and instead must emulate them using a series of steps. This can be tens to hundreds of times slower than performing operations on normalized floating-point numbers. Since we multiply our velocity by our damping factor each frame, it will eventually become very small. We don't actually care about such tiny velocities, so we simply set it to zero.)

The PointMass::increaseDamping() method is used to temporarily increase the amount of damping. We will use this later for certain effects.

The Spring Class

A spring connects two point masses, and, if stretched past its natural length, applies a force pulling the masses together. Springs follow a modified version of Hooke's Law with damping:

\[f = −kx − bv\]

\(f\) is the force produced by the spring.

\(k\) is the spring constant, or the stiffness of the spring.

\(x\) is the distance the spring is stretched beyond its natural length.

When we create a spring, we set the natural length of the spring to be just slightly less than the distance between the two end points. This keeps the grid taut even when at rest, and improves the appearance somewhat.

The Spring::update() method first checks whether the spring is stretched beyond its natural length. If it is not stretched, nothing happens. If it is, we use the modified Hooke's Law to find the force from the spring and apply it to the two connected masses.

Creating the Grid

Now that we have the necessary nested classes, we're ready to create the grid. We start by creating PointMass objects at each intersection on the grid. We also create some immovable anchor PointMass objects to hold the grid in place. We then link up the masses with springs.

The first for loop creates both regular masses and immovable masses at each intersection of the grid. We won't actually use all of the immovable masses, and the unused masses will simply be garbage collected some time after the constructor ends. We could optimize by avoiding creating unnecessary objects, but since the grid is usually only created once, it won't make much difference.

In addition to using anchor point masses around the border of the grid, we will also use some anchor masses inside the grid. These will be used to very gently help pull the grid back to its original position after being deformed.

Since the anchor points never move, they don't need to be updated each frame; we can simply hook them up to the springs and forget about them. Therefore, we don't have a member variable in the Grid class for these masses.

There are a number of values you can tweak in the creation of the grid. The most important ones are the stiffness and damping of the springs. (The stiffness and damping of the border anchors and interior anchors are set independently of the main springs.) Higher stiffness values will make the springs oscillate more quickly, and higher damping values will cause the springs to slow down sooner.

Manipulating the Grid

In order for the grid to move, we must update it each frame. This is very simple as we already did all the hard work in the PointMass and Spring classes:

Now we will add some methods that manipulate the grid. You can add methods for any kind of manipulation you can think of. We will implement three types of manipulations here: pushing part of the grid in a given direction, pushing the grid outwards from some point, and pulling the grid in towards some point. All three will affect the grid within a given radius from some target point. Below are some images of these manipulations in action:

We will use all three of these methods in Shape Blaster for different effects.

Rendering the Grid

We'll draw the grid by drawing line segments between each neighbouring pair of points. First, we'll add an extension method taking a tSpriteBatch pointer as a parameter that allows us to draw line segments by taking a texture of a single pixel and stretching it into a line.

This transform will give the grid a perspective view where far away points appear closer together on the screen. Now we can draw the grid by iterating through the rows and columns and drawing lines between them:

In the above code, p is our current point on the grid, left is the point directly to its left and up is the point directly above it. We draw every third line thicker both horizontally and vertically for visual effect.

Interpolation

We can optimize the grid by improving the visual quality for a given number of springs without significantly increasing the performance cost. We are going to do two such optimizations.

We will make the grid denser by adding line segments inside the existing grid cells. We do so by drawing lines from the midpoint of one side of the cell to the midpoint of the opposite side. The image below shows the new interpolated lines in red.

Drawing the interpolated lines is straightforward. If you have two points, a and b, their midpoint is (a + b) / 2. So, to draw the interpolated lines, we add the following code inside the for loops of our Grid::draw() method:

The second improvement is to perform interpolation on our straight line segments to make them into smoother curves. In the original XNA version of this game, the code relied on XNA's Vector2.CatmullRom() method which performs Catmull-Rom interpolation. You pass the method four sequential points on a curved line, and it will return points along a smooth curve between the second and third points you provided.

Since this algorithm doesn't exist in either C or C++'s standard library, we'll have to implement it ourselves. Luckily there is a reference implementation available to use. I've provided a MathUtil::catmullRom() method based on this reference implementation:

The fifth argument to MathUtil::catmullRom() is a weighting factor that determines which point on the interpolated curve it returns. A weighting factor of 0 or 1 will respectively return the second or third point you provided, and a weighting factor of 0.5 will return the point on the interpolated curve halfway between the two points. By gradually moving the weighting factor from zero to one and drawing lines between the returned points, we can produce a perfectly smooth curve. However, to keep the performance cost low, we will only take a single interpolated point into consideration, at a weighting factor of 0.5. We then replace the original straight line in the grid with two lines that meet at the interpolated point.

The diagram below shows the effect of this interpolation:

Since the line segments in the grid are already small, using more than one interpolated point generally does not make a noticeable difference.

Often, the lines in our grid will be very straight and won't require any smoothing. We can check for this and avoid having to draw two lines instead of one: we check if the distance between the interpolated point and the midpoint of the straight line is greater than one pixel; if it is, we assume the line is curved and we draw two line segments.

The modification to our Grid::draw() method for adding Catmull-Rom interpolation for the horizontal lines is shown below.

The image below shows the effects of the smoothing. A green dot is drawn at each interpolated point to better illustrate where the lines are smoothed.

Using the Grid in Shape Blaster

Now it's time to use the grid in our game. We start by declaring a public, static Grid variable in GameRoot and creating the grid in the GameRoot::onInitView. We'll create a grid with roughly 600 points like so.

Though the original XNA version of the game uses 1,600 points (rather than 600), this becomes way too much to handle just even for the powerful hardware in the iPhone. Fortunately, the original code left the amount of points customizable, and at about 600 grid points, we can still render them and still maintain an optimal frame rate.

Then we call Grid::update() and Grid::draw() from the GameRoot::onRedrawView() method in GameRoot. This will allow us to see the grid when we run the game. However, we still need to make various game objects interact with the grid.

Bullets will repel the grid. We already made a method to do this called Grid::applyExplosiveForce(). Add the following line to the Bullet::update() method.

This makes the black hole suck in the grid with a varying amount of force. We've reused the mSprayAngle variable, which will cause the force on the grid to pulsate in sync with the angle it sprays particles (although at half the frequency due to the division by two). The force passed in will vary sinusoidally between 10 and 30.

Finally, we will create a shockwave in the grid when the player's ship respawns after death. We will do so by pulling the grid along the z-axis and then allowing the force to propagate and bounce through the springs. Again, this only requires a small modification to PlayerShip::update().

What's Next?

We have the basic gameplay and effects implemented. It's up to you to turn it into a complete and polished game with your own flavour. Try adding some interesting new mechanics, some cool new effects, or a unique story. In case you aren't sure where to start, here are a few suggestions: