Game Programming Crash Course (for J2ME): Artificial Intelligence

Koder & Ko logo
 
 
Home | Tools Setup | MIDlet | Background | Sprites | Collision Detection | User Input | Artificial Intelligence | Optimization
 
 
Koder & Ko

J2ME Tutori$
 

It's not really intelligent, it's just a very fast, but stupid, function to follow the y-axis value of the ball. But when we add some randomness to the behaviour and tune the abilities, it almost seems to become intelligent. I dare to refer to it as AI on this page.

Adding a computer-controlled opponent is quite similar to adding the user-controlled pad, except we do not need the user input. We are going to do the following: 1) Add a function that moves the opponent pad 2) Add a sprite controlled by the AI 3) Make sure the computer-controlled sprite stays within the screen using collision detection 4) Make sure the ball bounces of the computer-controlled sprite using collision detection.

We can reuse the Image object we used for our pad sprite, so we do not have to import another image to our game.

Here is the code, after we have added "artificial intelligence" to our game. As always, the changes are explained in detail below.

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; import java.util.Random; // Added 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"); } 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); padSprite.defineReferencePixel(1, 10); padSprite.setRefPixelPosition(padX, padY); AISprite = new Sprite(padImg, 3, 20); // Added AISprite.defineReferencePixel(3, 10); // Added AISprite.setRefPixelPosition(AIX, AIY); // 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(); moveAI(); // Added ballSprite.setRefPixelPosition(ballX, ballY); ballSprite.paint(g); padSprite.setRefPixelPosition(padX, padY); padSprite.paint(g); AISprite.setRefPixelPosition(AIX, AIY); // Added AISprite.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)) { ballDirection = 1; } else if (ballDirection == 3 && ballSprite.collidesWith(padSprite, false)) { ballDirection = 2; } else if (ballDirection == 1 && ballSprite.collidesWith(AISprite, false)) { // Added ballDirection = 0; // Added } else if (ballDirection == 2 && ballSprite.collidesWith(AISprite, false)) { // Added ballDirection = 3; // Added } } 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; } } private void moveAI() { // Added entirely new method Random random = new Random(); actX = getWidth() / 3 + Math.abs(random.nextInt() % (getWidth() / 8)); if (ballY < AIY && ballX > actX && AIY > AISprite.getHeight() / 2) AIY -= AIYVel; if (ballY > AIY && ballX > actX && AIY < getHeight() - AISprite.getHeight() / 2) AIY += AIYVel; } 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; private Sprite padSprite; private int padX = 10; private int padY = getHeight() / 2; private final static int padYVel = 2; private Sprite AISprite; // Added private int AIX = getWidth() - 10; // Added private int AIY = getHeight() / 2; // Added private final static int AIYVel = 2; // Added private int actX; // Added }

I know; the amount of code seems to grow quite aggressively. I prefer to show the entire listing, though, to make sure you can follow all the changes. Here's a breakdown of the additions.

import java.util.Random;

We include the Random class, to be able to make the opponent act in a human random manner. It is used in the moveAI() method.

AISprite = new Sprite(padImg, 3, 20); AISprite.defineReferencePixel(3, 10); AISprite.setRefPixelPosition(AIX, AIY);

Here we setup the AI sprite. This should be nothing new, but two things is worth noting 1) We did not import another image, we just reused the existing padImg object 2) We define the reference pixel to be on the right side of the pad (instead of on the left side, as we did for the user-controlled sprite).

moveAI(); (...) AISprite.setRefPixelPosition(AIX, AIY); AISprite.paint(g);

As always, we add this to the updateScreen method to call the moveAI() method that moves the AI sprite, and to have the sprite moved to the right location on the canvas.

} else if (ballDirection == 1 && ballSprite.collidesWith(AISprite, false)) { ballDirection = 0; } else if (ballDirection == 2 && ballSprite.collidesWith(AISprite, false)) { ballDirection = 3;

This code is doing the collision detection between the AI sprite, and the ball. These checks are only necessary for ball direction 1 and 2 (as you might recall from the previous page, we did these checks for direction 0 and 3, when the ball is heading toward the user-controlled pad).

private void moveAI() { Random random = new Random(); actX = getWidth() / 3 + Math.abs(random.nextInt() % (getWidth() / 8)); if (ballY < AIY && ballX > actX && AIY > AISprite.getHeight() / 2) AIY -= AIYVel; if (ballY > AIY && ballX > actX && AIY < getHeight() - AISprite.getHeight() / 2) AIY += AIYVel; }

This is where the "artificial intelligence" is implemented. On the first line we instantiate a new Random object. With this object, we are able to get a random number, which is stored in the actX variable. This variable defines when the AI is allowed to move.

This might be a bit difficult to grasp, but imagine we did not use some kind of randomizer here. The result would be that the AI would follow the ball every time it moved up or down, and the ball would never be able to get pass the AI sprite. It is not a funny game, when the AI will never lose. What this randomize function does is illustrated below.

AI Random

First we get a third of the width of the screen by using getWidth() / 3. The AI is never allowed to move, when the ball is in the black area in the illustration. We then add a random value between 0 and getWidth() / 8 to a third of the screen. This is the light red area in the illustration, and if the ball is in this area the AI is sometimes allowed to move (depending on the random value). Finally, when the ball is in the dark red area the AI is always allowed to move.

The last two lines might seem a bit complex, but can be explained easily: If the ball sprite is above the AI sprite (that is, ballY is lower than AIY) AND the ball is on the right side of the "random line" mentioned above (that is, ballX is higher than actX), and the AI sprite is not at the top of the screen: Then move up the AI sprite. The next line does the same, if the ball is below the AI sprite. If you're in doubt about what "AIsprite.getHeight() / 2" is used for, go to the user input page, where it is explained for the padSprite in detail.

private Sprite AISprite; private int AIX = getWidth() - 10; private int AIY = getHeight() / 2; private final static int AIYVel = 2; private int actX;

Finally we have the object and variables used for the AI sprite. We do not need an Image object (since we reuse the one from padImg), so all we need is a new Sprite object, as well as variables to handle the position and velocity of the AI sprite. actX is, as mentioned above, a variable that defines when the AI sprite is allowed to move.

The Result

Our game is almost complete. We have a game with a ball, a pad that is controlled by the user, as well as a super-intelligent AI opponent. But for some reason the game playing experience is a bit boring. We'll see what we can do about this on the next page.

Emulator User Input

Next Page: We add a score board and increasing difficulty

 
 
 

Call +45 2078 7221 or write ave at koderko.dk

Koder & Ko | Nordre Fasanvej 166 | 2000 Frederiksberg | Denmark