Advertisement
  1. Game Development
  2. Programming
Gamedevelopment

Make a Neon Vector Shooter for iOS: Virtual Gamepads and Black Holes

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Cross-Platform Vector Shooter: iOS.
Make a Neon Vector Shooter for iOS: Particle Effects

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 part, we'll add the virtual gamepad controls and the "black hole" enemies.

Overview

In the series so far we've set up the basic gameplay for our neon twin stick shooter, Shape Blaster. Next, we'll add two on-screen "virtual gamepads" to control the ship with.


Input is a must for any video game, and iOS provides us an interesting and ambiguous challenge with multi-touch input. I'll show you one approach, based on the concept of virtual gamepads, where we'll simulate hardware gamepads by using only touch and a bit of complex logic to figure things out. After adding the virtual gamepads for multi-touch input, we will also add black holes to the game.

Virtual Gamepads

On-screen, touch-based controls are the primary means of input for the majority of iPhone- and iPad-based apps and games. In fact, iOS allows the use of a multi-touch interface, meaning reading several touch points at the same time. The beauty of touch based interfaces is that you can define the interface to be whatever you want, whether it's one button, a virtual control stick, or a sliding control. What we'll implement is a touch interface I'll call "virtual gamepads."

A gamepad typically describes a standard, plus-shaped physical control similar the plus interface on a Game Boy system or PlayStation controller (also known as a directional pad or D-pad). A gamepad allows movement in both the up and down axis, and the left and right axis. The result is that you are able to signal eight distinct directions, with the addition of "no direction." In Shape Blaster, our gamepad interface will not be physical, but on-screen, hence a virtual gamepad.

wii-controller-d-pad
A typical physical gamepad; the directional pad in this case is plus-shaped.

directional-pad

Although there are only four inputs, there are eight directions (plus neutral) available.

To have a virtual gamepad in our game, we must recognize touch input when it happens, and convert it into a form the game already understands.

The virtual gamepad implemented here works in three steps:

  1. Determine the touch type.
  2. Determine whether it's in the area of an on-screen gamepad.
  3. Emulate the touch as a key press or mouse movement.

In each step we'll focus solely on the touch we have, and keep track of the last touch event we had to compare. We'll also keep track of the touch ID, which determines which finger is touching which gamepad.

The screenshot below shows how the gamepads will appear on screen:

final-shot

Screenshot of the final gamepads in position.

Adding Multi-Touch to Shape Blaster

In the Utility library, let's look at the event class we'll primarily make use of. tTouchEvent encapsulates everything we need to handle touch events at a basic level.

The EventType allows us to define the type of events we'll allow without getting too complicated. mLocation will be the actual touch point, and mID will be the finger ID, which starts at zero and adds one for each finger touched on-screen. If we define the constructor to only take const references, we'll be able to instantiate event classes without having to explicitly create named variables for them.

We'll use tTouchEvent exclusively to send touch events from the OS to our Input class. We'll also later use it to update the graphic representation of the gamepads in the VirtualGamepad class.

The Input Class

The original XNA and C# version of the Input class can handle mouse, keyboard, and actual physical gamepad inputs. The mouse is used to fire at an arbitrary point on screen from any position; the keyboard can be used to both move and shoot in given directions. Since we've chosen to emulate the original input (to stay true to a "direct port"), we'll keep most of the original code the same, using the names keyboard and mouse, even though we have neither on iOS devices.

Here's how our Input class will look. For every device, we'll need to keep a "current snapshot" and "previous snapshot" so we can tell what's changed between the last input event and the current input event. In our case, mMouseState and mKeyboardState are the "current snapshot", and mLastMouseState and mLastKeyboardState represent the "previous snapshot."

Updating Input

On a PC, any event we get is "distinct", meaning that a mouse movement is different than pushing the letter A, and even the letter A is different enough from the letter S that we can tell it's not exactly the same event.

With iOS, we only ever get touch input events, and one touch is not distinct enough from another for us to tell whether it's meant to be a mouse movement or a key press, or even which key it is. All events look exactly the same from our point of view.

To help figure out this ambiguity, we'll introduct two new members, mFreshMouseState and mFreshKeyboardState. Their purpose is to aggregate, or "catch all", the events in a particular frame, without modifying the other state variables otherwise. Once we're satisfied a frame has passed, we can update the current state with the "fresh" members by calling Input::update. Input::update also tells our input state to advance.

Since we'll do it once per frame, we'll call Input::update() from within GameRoot::onRedrawView():

Now let's look at how we turn touch input into either a simulated mouse or keyboard. First, we'll plan on having two different rectangular areas that represent the virtual gamepads. Anything outside of these areas we'll consider "definitely a mouse event"; anything inside, we'll consider "definitely a keyboard event."

hitboxes

Anything inside the red boxes we'll map to our simulated keyboard input; anything else we'll treat like mouse input.

Let's look at Input::onTouch(), which gets all touch events. We'll take a big picture look at the method and just note areas TODO where more specific code should be:

The code is simple enough, but there's some powerful logic happening that I'll point out:

  1. We determine where the left and right gamepads are going to be on-screen, so that we can see if we're in them when we touch down or let go. These are stored into the leftPoint and rightPoint local variables.
  2. We determine the mouseDown state: if we're "pressing" with a finger, we need to know if it's within leftPoint's rect or rightPoint's rect, and if so take action to update the fresh state for the keyboard. If it's in neither rect, we'll assume it's a mouse event instead and update the fresh state for the mouse.
  3. Finally, we keep track of the touch IDs (or finger IDs) as they are pressed; if we detect a finger lifting off of the surface, and it's associated with an active gamepad, we'll reset the simulated keyboard for said gamepad accordingly.

Now that we see the big picture, let's drill down a bit further.

Filling in the Gaps

When a finger is lifted off of the surface of the iPhone or iPad, we check to see if it's a finger we know is on a gamepad and, if so, we reset all the "simulated keys" for that gamepad:

The situation is somewhat different when there's a touch starting on the surface or moving; we check to see if the touch is within either gamepad. Since the code for both gamepads is similar, we'll only take a look at the left gamepad (which deals with movement).

Whenever we get a touch event, we'll clear the keyboard state completely for that particular gamepad, and check within our the rect area to determine which key or keys to press. So although we have a total of eight directions (plus neutral), we'll only ever check four rectangles: one for up, one for down, one for left, and one for right.

overlay

The nine areas of interest in our gamepad.

Displaying Graphics for the Virtual Gamepad

If you run the game now, you'll have virtual gamepad support, but you won't actually be able to see where the virtual gamepads start or end.

This is where the VirtualGamepad class comes into play. The VirtualGamepad's primary purpose is to draw the gamepads on screen. The way we'll display the gamepad will be the way other games tend to do so if they have gamepads: as a larger "base" circle, and a smaller "control stick" circle we can move. This looks similar to an arcade joystick from the top-down, and easier to draw than some other alternatives.

First, notice that the image files vpad_top.png and vpad_bot.png have been added to the project. Let's modify the Art class to load them:

The VirtualGamepad class will draw both gamepads on screen, and keep State information in the members mLeftStick and mRightStick on where to draw the "control sticks" of the gamepads.

I've chosen some slightly arbitrary positions for the gamepads, which are initialized into the mLeftPoint and mRightPoint members—the calculations place them at about 3.75% in from the left or right edge of the screen, and about 13% in from the bottom of the screen. I based these measurements on a commercial game with similar virtual gamepads but different gameplay.

As previously mentioned, mLeftStick and mRightStick are bitmasks, and their use is to determine where to draw the inner circle of the gamepad. We'll calculate the bitmask in the method VirtualGamepad::UpdateBasedOnKeys().

This method is called immediately after Input::onTouch, so that we can read the "fresh" state members and know that they're up-to-date:

To draw an individual gamepad, we call VirtualGamepad::DrawStickAtPoint(); this method doesn't know nor care which gamepad you're drawing, it only knows where you want it drawn and the state to draw it in. Because we've used bitmasks and calculated ahead of time, our method becomes smaller and easier to read:

Drawing two gamepads becomes much easier as it's just a call to the above method twice. Let's look at VirtualGamepad::draw():

Finally, we need to actually draw the virtual gamepad, so in GameRoot::onRedrawView(), add the following line:

That's it. If you run the game now, you should see the virtual gamepads in full effect. When you touch inside the left gamepad, you should move around. When you touch inside the right gamepad, your firing direction should change. In fact, you can use both gamepads at once, and even move using the left gamepad and touch outside of the right gamepad to get mouse movement. And when you let go, you stop moving and (potentially) stop shooting.

Summary of the Virtual Gamepad Technique

We've fully implemented virtual gamepad support, and it works, but you may find it a bit clunky or hard to use. Why is that the case? This is where the real challenge of touch-based controls on iOS come in with traditional games that weren't initially designed for them.

You're not alone, though. Many games either suffer from these issues, and have overcome them.

Here are a few things I've observed with touch-screen input; you might have some similar observations yourself:

First, game controllers have a different feel than a flat touchscreen; you know where your finger is on a real gamepad, and how to keep your fingers from slipping off. However, on a touchscreen, your fingers may drift slightly too far out of the touch zone, so your input may not be correctly recognized, and you may not realize that's the case until it's too late.

Second, you may have also noticed, when playing with touch controls, that your hand obscures your vision, so you ship may get hit by an enemy underneath your hand that you didn't see to begin with!

Finally, you may find that the touch areas are easier to use on an iPad rather than an iPhone or vice-versa. So we have issues with a different screen size that affects our "input area size", which is definitely something we don't experience so much on a desktop computer. (Most keyboards and mice are the same size and act the same way, or can be adjusted.)

Here are some changes you could make to the input system described in this article:

  • Draw your gamepad's central location where your touch begins; this allows the player's hand to shift ever so slightly without impact, and means they can touch anywhere on screen.
  • Make your "playable area" smaller, and move the gamepad off of the playable area completely. Now your fingers won't obstruct your view.
  • Make separate, distinct user interfaces for iPhone and iPad. This will allow you to tweak the design based on the device type, but it also requires you have different devices to test against.
  • Make enemies or the player ship slightly slower. This potentially lets the user experience the game more easily, but it also potentially makes your game easier to win.
  • Ditch virtual gamepads altogether and use another scheme. You're in charge, after all!

Again, it's up to you what you want to do and how you want to do it. On the plus side, there are many ways to do touch input. The tough part is getting it right and making your players happy.

Black Holes

One of the most interesting enemies in Geometry Wars is the black hole. Let's examine how we can make something similar in Shape Blaster. We will create the basic functionality now, and we will revisit the enemy in the next tutorial to add particle effects and particle interactions.

black-hole-with-particles

A black hole with orbiting particles

Basic Functionality

The black holes will pull in the player's ship, nearby enemies, and (after the next tutorial) particles, but will repel bullets.

There are many possible functions we can use for attraction or repulsion. The simplest is to use constant force so that the black hole pulls with the same strength regardless of the object's distance. Another option is to have the force increase linearly, from zero at some maximum distance, to full strength for objects directly on top of the black hole. If we'd like to model gravity more realistically, we can use the inverse square of the distance, which means the force of gravity is proportional to 1/(distance^2).

We'll actually be using each of these three functions to handle different objects. The bullets will be repelled with a constant force; the enemies and the player's ship will be attracted with a linear force; and the particles will use an inverse square function.

We'll make a new class for black holes. Let's start with the basic functionality:

The black holes take ten shots to kill. We adjust the scale of the sprite slightly to make it pulsate. If you decide that destroying black holes should also grant points, you must make similar adjustments to the BlackHole class as we did with the Enemy class.

Next, we'll make the black holes actually apply a force on other entities. We'll need a small helper method from our EntityManager:

This method could be made more efficient by using a more complicated spatial partitioning scheme, but for the number of entities we will have, it's fine as it is.

Now we can make the black holes apply force in their BlackHole::update() method:

Black holes only affect entities within a chosen radius (250 pixels). Bullets within this radius have a constant repulsive force applied, while everything else has a linear attractive force applied.

We'll need to add collision handling for black holes to the EntityManager. Add an std::list<BlackHole*> for black holes like we did for the other types of entities, and add the following code in EntityManager::handleCollisions():

Finally, open the EnemySpawner class and have it create some black holes. I limited the maximum number of black holes to two, and gave a one in 600 chance of a black hole spawning each frame.

Conclusion

We've discussed and added virtual gamepads, and added black holes using various force formulas. Shape Blaster is starting to look pretty good. In the next part, we'll add some crazy, over-the-top particle effects.

References

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.