Controlling the spaceship
We have our player's spaceship floating aimlessly on the screen starting 50 pixels from the left and 50 pixels from the top and drifting slowly to the right. Now, we can give the player the power to control the spaceship.
Remember the design for the controls is a one finger tap and hold to boost, release to quit boosting and decelerate.
Detecting touches
The SurfaceView
class that we extended for our view is perfect for handling screen touches.
All we need to do is override the onTouchEvent
method within our TDView
class. Let's see the code in full, and then we can examine it more closely to make sure we understand what is going on. Enter this method in the TDView
class and import the necessary classes in the usual way. I have highlighted the parts of the code that we will be customizing later:
// SurfaceView allows us to handle the onTouchEvent @Override public boolean onTouchEvent(MotionEvent motionEvent) { // There are many different events in MotionEvent // We care about just 2 - for now. switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) { // Has the player lifted their finger up? case MotionEvent.ACTION_UP: // Do something here break; // Has the player touched the screen? case MotionEvent.ACTION_DOWN: // Do something here break; } return true; }
This is how the onTouchEvent
method works so far. The player touches the screen; this can be any kind of contact at all. It could be a swipe, a pinch, multiple fingers, and so on. A detailed message is sent to the onTouchEvent
method.
The details of the event are contained in the MotionEvent
class parameter, as we can see in our code. The MotionEvent
class holds lots of data. It knows how many fingers were placed on the screen, the coordinates of each, and if any gestures were made as well.
As we are implementing a simple tap and hold to boost, release to stop boosting control scheme; we can simply switch using the motionEvent.getAction() & MotionEvent.ACTION_MASK
condition and cater for just two of many possible different cases.
The case MotionEvent.ACTION_UP:
will, as the name suggests, tell us when the player removes a finger from the screen. Then, perhaps unsurprisingly, case MotionEvent.ACTION_DOWN:
tells us if the player places a finger on the screen.
Note
What we can find out through the MotionEvent
class is quite vast. Why not take a look at the full scope of its potential here: http://developer.android.com/reference/android/view/MotionEvent.html. We will also explore this class further in the next project that we start to build in Chapter 5, Platformer – Upgrading the Game Engine.
Adding boosters to the spaceship
Now, all we need to do is think about how we will use these events to control the spaceship. First of all, the spaceship needs to know if it is boosting or not boosting. This suggests a Boolean member variable. Add this code just after the class declaration in the PlayerShip
class:
private boolean boosting;
We then need to initialize it when a PlayerShip
object is created. So add this to the PlayerShip
constructor:
boosting = false;
Now, we need to let the onTouchEvent
method toggle boosting
between true and false, boosting and not boosting. Add these methods to the PlayerShip
class:
public void setBoosting() { boosting = true; } public void stopBoosting() { boosting = false; }
Now, we can call these public methods from our onTouchEvent
method to control the state of whether the spaceship is boosting or not. Add this new code in the onTouchEvent
method:
// Has the player lifted there finger up? case MotionEvent.ACTION_UP: player.stopBoosting(); break; // Has the player touched the screen? case MotionEvent.ACTION_DOWN: player.setBoosting(); break;
Now, our view is talking to our model; all we need to do is make the boosting variable do something depending on which state it is in. The logical place for this code will be the PlayerShip
class's update
method.
We will change the speed
variable of our spaceship based on whether the ship is currently boosting. At first this seems simple, but there are a few minor issues with just increasing the speed based on whether the ship is boosting:
- One problem is that the
update
method is called 60 times every second. So, it wouldn't take much boosting to have the ship flying at ridiculous speeds. We need to constrain the ship's speed. - Another problem is that our spaceship will rise up the screen when boosting, and there is nothing to stop it whizzing straight off the top of the screen, never to be seen again. We need to constrain the ship's x and y coordinates within the screen.
- When the ship is not boosting and the speed steadily returns to zero, what will bring the ship back down again? We will need a simple gravity physics simulation.
To solve these three problems, we can add code to our PlayerShip
class. However, before we do this, a quick word about gameplay balance. The code which we will see very soon uses different integer values, for example, we initialize GRAVITY
to -12
and MAX_SPEED
to 20
. These numbers have no bearing in reality!
They are simply the arbitrary numbers that make the gameplay balanced. Feel free to play with all these arbitrary figures to make the game harder, easier, or even impossible. At the end of Chapter 4, Tappy Defender – Going Home, we will look more closely at game iteration and look again at difficulty and balance.
With three of our previously stated problems in mind, add the following member variables just after the class declaration in the PlayerShip
class:
private final int GRAVITY = -12; // Stop ship leaving the screen private int maxY; private int minY; //Limit the bounds of the ship's speed private final int MIN_SPEED = 1; private final int MAX_SPEED = 20;
Now, we made a start to solve our three problems, we can add code to our PlayerShip
class's update
method. We will delete the one line of code, we put in there in the previous chapter. That was just there to take a quick look at our ship in action. Enter the new code of our PlayerShip
class's update
method. We will take a closer look afterward:
public void update() { // Are we boosting? if (boosting) { // Speed up speed += 2; } else { // Slow down speed -= 5; } // Constrain top speed if (speed > MAX_SPEED) { speed = MAX_SPEED; } // Never stop completely if (speed < MIN_SPEED) { speed = MIN_SPEED; } // move the ship up or down y -= speed + GRAVITY; // But don't let ship stray off screen if (y < minY) { y = minY; } if (y > maxY) { y = maxY; } }
In order from the top of the previous block of code, we are increasing and decreasing the speed variable by apparently arbitrary amounts, each frame of the game, based on if the ship is boosting or not.
We then constrain the speed of the ship to a maximum of 20 and a minimum of 1, as specified by the variables we added earlier. With the line y -= speed + GRAVITY
, we move the graphic on screen either up or down based on speed and gravity. The apparently arbitrary values for GRAVITY
and MAX_SPEED
work nicely to allow the player to awkwardly and precariously bounce along through space.
Finally, we stop the ship from ever disappearing off the screen by making sure the ship graphic is never drawn beyond maxY
and minY
. You have probably noticed that, as of yet, we haven't initialized maxY
and minY
. Furthermore, what will we initialize them to anyway as many Android devices have vastly different screen resolutions?
What we need to do is discover the resolution of the Android device at run time and use the information to initialize MaxY
and minY
.
Detecting the screen resolution
We know that we need the maximum y coordinate of the player's screen. Later in the project when we start adding backgrounds and enemy ships, we will realize that we also need the maximum x coordinate as well. With this in mind, let's see how we can get this information and make it available to the PlayerShip
class.
The most expedient time to detect the screen resolution is as the app is starting, and before our view and the model have been instantiated. This implies that our GameActivity
class is a good place to do it. We will now add code to the onCreate
method of the GameActivity
class. Add this new code to the onCreate
class, before the call to new...
that creates our TDView
object:
// Get a Display object to access screen details Display display = getWindowManager().getDefaultDisplay(); // Load the resolution into a Point object Point size = new Point(); display.getSize(size);
The previous code declares and initializes an object of the Display
type using getWindowManager().getDefaultDisplay();
. Then we create a new object of type Point
. The Point
object can hold two coordinates and we then pass it as an argument into the getSize
method of our new Display
object.
We now have the resolution of the Android device our game is running on, neatly stored in size
. Now pass this on to the parts of our code which require it. First, we will change the arguments we pass in the call to new
, which initializes our TDView
object. Change the call to new
as shown next to pass in the screen resolution to the TDView
constructor:
// Create an instance of our Tappy Defender View
// Also passing in this.
// Also passing in the screen resolution to the constructor
gameView = new TDView(this, size.x, size.y);
Then, of course, we need to update the TDView
constructor itself. In the TDView.java
file, amend the TDView
constructor's signature so that the declaration now looks like this:
TDView(Context context, int x, int y) {
Now, still in the constructor, change the way we initialize the player of our PlayerShip
object:
player = new PlayerShip(context, x, y);
Of course, we must now amend the constructor declaration within the PlayerShip
class itself, to this:
public PlayerShip(Context context, int screenX, int screenY) {
In addition, we can now initialize our maxY
and minY
variables within the PlayerShip
constructor. Before we see the code, we need to consider exactly how this will work.
The coordinates of the bitmap that holds our spaceship graphic is drawn with the top-left corner at the x = 0 and y = 0 coordinates passed in to drawBitmap()
in the TDView
class's draw
method. This means that there are pixels off to the right and after the coordinates at which we begin to draw the ship. Take a look at this next image to visualize this:
Therefore, we must set our minY
and maxY
values with this in mind. As the image illustrates, the top pixel of the bitmap is indeed drawn exactly at the ships y. We can then be confident that minY
should be zero.
The bottom of the ship, however, is drawn at y + the height of the bitmap.
We can now add two lines of code to our constructor to initialize these variables:
maxY = screenY - bitmap.getHeight(); minY = 0;
You can now run the game and test out your boosters!