|
OK! I readily admit that having a ball bouncing on the screen does not exactly qualify as a game. Normally people want to be involved when playing a game, so we must add user input to our game.
We are going to do the following: 1) Enable our game to get user input 2) Add a sprite controlled by the user 3) Make sure the user-controlled sprite stays within the screen using collision detection 4) Make sure the ball bounces of the user-controlled sprite using collision detection.
First of all we need to create an image to be used as the "pad" controlled by the user. I have decided to make this 3x20 pixels big, which equals 3px wide and 20px tall.
| |
| |
Tip!
As with the ball image, feel free to grab the image I used for the sprite here: (it might be a bit difficult to hit, so here is a link).
|
|
| |
The file must be imported to the project the same way we imported the ball image on the sprites page. Please do so now. The code after adding the new sprite, as well as adding user input, looks like this:
package dk.koderko.games.pong;
import java.io.IOException;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.game.Sprite;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
public class PongCanvas extends GameCanvas implements Runnable {
public PongCanvas() {
super(false);
}
public void run() {
while(true) {
updateScreen(getGraphics());
try {
Thread.sleep(sleepTime);
} catch (Exception e) {
}
}
}
public void start() {
try {
ballImg = Image.createImage("/ball.png");
padImg = Image.createImage("/pad.png"); // Added
} catch (IOException ioex) {
System.out.println(ioex);
}
ballSprite = new Sprite(ballImg, 3, 3);
ballSprite.defineReferencePixel(2, 2);
ballSprite.setRefPixelPosition(ballX, ballY);
padSprite = new Sprite(padImg, 3, 20); // Added
padSprite.defineReferencePixel(1, 10); // Added
padSprite.setRefPixelPosition(padX, padY); // Added
Thread runner = new Thread(this);
runner.start();
}
private void createBackground(Graphics g) {
g.setColor(0x000000);
g.fillRect(0, 0, getWidth(), getHeight());
}
private void updateScreen(Graphics g) {
createBackground(g);
moveBall();
movePad(); // Added
ballSprite.setRefPixelPosition(ballX, ballY);
ballSprite.paint(g);
padSprite.setRefPixelPosition(padX, padY); // Added
padSprite.paint(g); // Added
flushGraphics();
}
private void moveBall() {
if (ballDirection == 0) {
ballX -= ballXVel;
ballY -= ballYVel;
} else if (ballDirection == 1) {
ballX += ballXVel;
ballY -= ballYVel;
} else if (ballDirection == 2) {
ballX += ballXVel;
ballY += ballYVel;
} else if (ballDirection == 3) {
ballX -= ballXVel;
ballY += ballYVel;
}
if (ballDirection == 0 && ballX < 0) {
ballDirection = 1;
} else if (ballDirection == 0 && ballY < 0) {
ballDirection = 3;
} else if (ballDirection == 1 && ballY < 0) {
ballDirection = 2;
} else if (ballDirection == 1 && ballX > getWidth()) {
ballDirection = 0;
if (sleepTime > 5) sleepTime--;
} else if (ballDirection == 2 && ballY > getHeight()) {
ballDirection = 1;
} else if (ballDirection == 2 && ballX > getWidth()) {
ballDirection = 3;
if (sleepTime > 5) sleepTime--;
} else if (ballDirection == 3 && ballY > getHeight()) {
ballDirection = 0;
} else if (ballDirection == 3 && ballX < 0) {
ballDirection = 2;
}
if (ballDirection == 0 && ballSprite.collidesWith(padSprite, false)) { // Added
ballDirection = 1; // Added
} else if (ballDirection == 3 && ballSprite.collidesWith(padSprite, false)) { // Added
ballDirection = 2; // Added
} // Added
}
private void movePad() { // Added: Entirely new method
int keyState = getKeyStates();
if ((keyState & UP_PRESSED) != 0 && padY > padSprite.getHeight() / 2) {
padY -= padYVel;
} else if ((keyState & DOWN_PRESSED) != 0 && padY < getHeight() - padSprite.getHeight() / 2) {
padY += padYVel;
}
}
private int sleepTime = 30;
private Image ballImg;
private Sprite ballSprite;
private int ballX = getWidth() / 2;
private int ballY = getHeight() / 2;
private final static int ballXVel = 3;
private final static int ballYVel = 1;
private int ballDirection = 1;
private Image padImg; // Added
private Sprite padSprite; // Added
private int padX = 10; // Added
private int padY = getHeight() / 2; // Added
private final static int padYVel = 2; // Added
}
You know the drill. I am now going to bore you with the details of the new code.
padImg = Image.createImage("/pad.png");
We need to create a new Image object from our fantastic artwork.
padSprite = new Sprite(padImg, 3, 20);
padSprite.defineReferencePixel(1, 10);
padSprite.setRefPixelPosition(padX, padY);
This is quite similar to the code used for setting up the ball sprite, but not entirely. First of all the dimensions are different; this sprite is 3 times 20 pixels. Pay special attention to defineReferencePixel(): I set the reference pixel to horizontally be on the left side of the sprite (since I use 1 on the x-axis), but vertically in the middle (since I use 10 on the y-axis). The sprite is 3px wide and 20px tall, so when I set the reference pixel to (1, 10) is it placed as shown below. Finally I place the pad in the right place using setRefPixelPosition() with variables defined in the end of the class.
| |
| |
More Information
Our pad sprite is 3x20 pixels big, so when we specify the reference pixel to be (1, 10) it is to the left in the middle, as illustrated here:
 |
|
| |
movePad();
(...)
padSprite.setRefPixelPosition(padX, padY);
padSprite.paint(g);
These three lines are added to the updateScreen() method, since we want the pad controlled by the user to be updated with every "cycle", just as we update the position of the ball with every cycle. It is simply a call to movePad() - which is implemented below - as well as an update of the position of the sprite, similar to what happens with the ball.
if (ballDirection == 0 && ballSprite.collidesWith(padSprite, false)) {
ballDirection = 1;
} else if (ballDirection == 3 && ballSprite.collidesWith(padSprite, false)) {
ballDirection = 2;
}
Take a deep breath. This takes place in the moveBall method. What happens is that the ball checks if it moves in either of the two directions possible towards the user's pad (on the left side of the screen, direction 0 and 3). There is not reason to check for collision, if the ball is heading towards the opponent on the right side of the screen (direction 1 and 2). The sprite class provides us with a very handy method called collidesWith(). This enables us to check if a sprite collides with another sprite, without having to do the mathematics ourselves.
The parameters are collidesWith(other sprite, opacity collision). The "other sprite" is the sprite we want to check for collision with, and "opacity collision" is used if we have sprites with transparency or similar (imagine you had a soccer ball drawing, but the sprite holding it was square - then you'd only want it to collide when the circle was hit, not when the "invisible square" was hit). We do not have opacity in our sprites, so it does not matter here, but now you know.
The bottom line is that if the ball is heading in our direction, and if the ball sprite collides with the padSprite (controlled by the user), then the ball changes direction.
private void movePad() {
int keyState = getKeyStates();
if ((keyState & UP_PRESSED) != 0 && padY > padSprite.getHeight() / 2) {
padY -= padYVel;
} else if ((keyState & DOWN_PRESSED) != 0 && padY < getHeight() - padSprite.getHeight() / 2) {
padY += padYVel;
}
}
Here is where the magic related to user input happens. getKeyStates() is one of the very useful methods of the GameCanvas class. When you call this method you get a bit mask of all the keys pressed since the last time you polled getKeyStates(). With this information, you are able to check if UP has been pressed, DOWN has been pressed, or any other typical game key (like RIGHT, LEFT, FIRE, etc) if you are doing your own game. In this course it is a simple Pong game, so we only need to be able to move up and down.
But there is more. We need to make sure that the user does not move the pad outside the screen, so we add another parameter. As you might recall we set the reference pixel to be in the middle of the sprite? That is 10px from the top and bottom, and the sprite is a total of 20px. To make sure the sprite cannot move out of the top of the screen we add "padY > padSprite.getHeight() / 2". Translated this equals: "The height of the sprite (20px) divided by 2 is 10px. The pad cannot be less than 10px from the top." The 10px here of course is used to off-set the 10px the reference pixel is placed from the top.
The same goes for the code used to prevent the pad from moving out of the screen in the bottom. Here we need to get the height of the screen (getHeight()), and from that subtract half the height of the sprite.
If the user has pressed up or down, and the border of the screen has not been reached, the pad is moved up or down with the velocity defined in the padYVel variable.
private Image padImg;
private Sprite padSprite;
private int padX = 10;
private int padY = getHeight() / 2;
private final static int padYVel = 2;
This should not be rocket science. We have another Image and Sprite object now. The padX variable defines where on the x-axis the pad should be placed, here it is 10px. The padY defines where on the y-axis it should be placed, and I use getHeight() / 2 to have it placed in the middle as the starting point. Finally the padYVel variable defines the velocity of the pad.
The Result
If you run the game in an emulator now, you will see the ball bouncing off the walls. If you move the pad to where the ball is going, you will see the ball bounce of the pad as well! This is all very nice.
Next Page: We create a computer opponent
|