您的位置:首页 > 移动开发 > IOS开发

Make a Neon Vector Shooter for iOS: The Warping Grid

2016-01-07 10:09 796 查看
http://gamedevelopment.tutsplus.com/tutorials/make-a-neon-vector-shooter-for-ios-the-warping-grid--gamedev-14637

This post is part of a series called Cross-Platform
Vector Shooter: iOS.
Make
a Neon Vector Shooter for iOS: Particle Effects
Make
a Neon Vector Shooter for iOS: First Steps

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 inverseof 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 ofdenormalized
floating-point numbers.
(When floating-point numbers get very small, they use a special representation called adenormalized 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
Lawwith damping:

f=−kx−bvf=−kx−bv

ff is
the force produced by the spring.
kk is
the spring constant, or the stiffness of the spring.
xx is
the distance the spring is stretched beyond its natural length.
bb is
the damping factor.
vv is
the velocity.

The code for the
Spring
class is as
follows:

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:



Bullets repelling the grid outwards.



Sucking the grid inwards.



Wave created by pushing the grid along the z-axis.

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.
Open the
Art
class and declare a texture
for the pixel:

You can set the pixel texture the same way we set up the other images, so we'll add
pixel.png
(
a 1x1px image with the sole pixel set to white) to the project and load it into the
tTexture
:

Now let's add the following method to the
Extensions
class:

This method stretches, rotates, and tints the pixel texture to produce the line we desire.
Next, we need a method to project the 3D grid points onto our 2D screen. Normally this might be done using matrices, but here we'll transform the coordinates manually instead.
Add the following to the
Grid
class:

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.



Advertisement


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 will make bullets repel the grid proportionally to their speed. That was pretty easy.
Now let's work on black holes. Add this line to
BlackHole::update()
:

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:

Tweak and customize the touch controls to your personal preferences.
Add support for hardware game controllers in iOS 7 via the GameController
Framework.
Profile and optimize all the slow parts of the code using Xcode's built-in Instruments tool.
Attempt to add post-processing effects like the Bloom
Shader present in the original XNA version.
Create new enemy types such as snakes or exploding enemies.
Create new weapon types such as seeking missiles or a lightning
gun.
Add a title screen and main menu, and a high score table.
Add some powerups such as a shield or bombs.
Experiment to find powerups that are fun and help your game stand out.
Create multiple levels. Harder levels can introduce tougher enemies and more advanced weapons and powerups.
Add environmental hazards such as lasers.

The sky's the limit!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: