Android Game Programming by Example
上QQ阅读APP看书,第一时间看更新

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!