Pong game - Computer AI and criticism

Started by
9 comments, last by evil_error 10 years, 7 months ago

Hello all,

I'm only starting out, and I've read somewhere on this forum to start small - so, Pong!

Here is a game I've wrote rather quickly (due to having a good beginner's grasp over programming languages such as Java and C++).

First and foremost, the game is based on this tutorial: link

It is a Java applet, the screen is drawn every 15ms via a Timer. I assume there is no need to explain pong to anyone smile.png

I would appreciate some advice on how to improve the AI further (I don't want the AI to be invincible of course).

Also, I would LOVE to know how to make the drawing of the computer paddle (right) less jittery and jumpy (make it smooth).

And, as should be common, any and all advice and criticism on all other aspects are welcome.

OH! And there is a null pointer error I can't find, though the game runs as predicted :S

Hopefully I wont bore you. Here's my code:

Player paddle class :


public class PlayerPaddle {
	private int yPos = 0;
	final int XPOS = 30;
	
	public PlayerPaddle() {
		setPos(120);
	}
	
	public void setPos(int pos) {
		yPos = pos;
		if(yPos > 230)
			setPos(230);
		else if (yPos<0)
			setPos(0);
	}
	
	public int getPos() {
		return yPos;
	}
	
}

Computer paddle class:


public class ComputerPaddle {
	private int yPos = 0;
	private int score;  // keeps track of score, should be tracked in PongMain though
	private int paddleSpeed;
	final int XPOS = 460;
	
	public ComputerPaddle(int ballPos, int speed) {
		setPos(ballPos);
		setScore(0);
		paddleSpeed = speed;
	}
	
	public void setPos(int pos) {
		yPos = pos;
		if(yPos > 230)
			setPos(230);
		else if(yPos < 0)
			setPos(0);
	}
	
	public int getPos() {
		return yPos;
	}
	
	public void setScore(int score) {
		this.score = score;
	}
	
	public int getScore() {
		return score;
	}
	
	public int getSpeed() {
		return paddleSpeed;
	}
}

Ball class:


public class Ball {
	private int xPos, yPos;
	public int dx, dy;
	
	public Ball() {
		reset();
	}
	
	public void setPos(int x, int y) {
		xPos = x;
		yPos = y;
	}
	
	public int getX() { 
		return xPos;
	}
	
	public int getY() {
		return yPos; 
	}
	
	public void move() {
		setPos(xPos+dx,yPos+dy);
	}
	
	public void reset() {
		setPos(250,140);
		// a 50/50 chance of direction in which the ball is headed
		double ran = Math.random()*10;
		if(ran<=5) {
			dy = -5;
			dx = 5;
		} else {
			dy = 5;
			dx = -5;
		}
	}
}

Computer player simulator class (idk how else to call it):


/* an AI class,
 * has a thread that checks the position of the ball, and changes the current position of the paddle accordingly
 * a paddle (myPaddle) may read the current position at leisure, 
 * the paddle has no 'intelligence', it is being played by an Computer type
 */
public class Computer implements Runnable {
	int currentPos, difficulty;
	// currentPos - position at witch Computer wishes to place the paddle
	// difficulty - time (in ms) during which the Computer doesn't react
	ComputerPaddle myPaddle;
	Ball ball;
	Thread thread = new Thread(this);
	
	Computer(ComputerPaddle p, Ball b, int dif) {
		myPaddle = p;
		difficulty = dif;
		ball = b;
		currentPos = myPaddle.getPos();
		thread.start();
	}
	
	public void run() {	
		while(!Thread.interrupted()) {
			try {
				if(currentPos > ball.getY() - 35)
					currentPos = currentPos - myPaddle.getSpeed();
				else if(currentPos < ball.getY() - 35)
					currentPos = currentPos + myPaddle.getSpeed();
				Thread.sleep(difficulty);
			} catch(InterruptedException g) {}
		}
	}
	
	public int getPos() {
		return currentPos;
	}
}

and finally, the Apllet (PongMain) class:


import java.applet.*;
import java.awt.event.*;
import java.awt.*;
import javax.swing.Timer;


public class PongMain extends Applet implements MouseMotionListener, ActionListener {
	Ball ball;
	PlayerPaddle pLeft;
	ComputerPaddle pRight;
	Computer AI;
	
	Timer time = new Timer(15,this);
	
	Font newFont = new Font("sansserif", Font.ITALIC, 15);
	Graphics bufferGraphics;
	Image offscreen;
	long startingTime;
	
	final int WIDTH = 500, HEIGHT = 300;
	
	// =================
	
	// initializes fields and applet 
	public void init() {
		setSize(WIDTH,HEIGHT);
		setVisible(true);
		
		ball = new Ball();
		pLeft = new PlayerPaddle();
		pRight = new ComputerPaddle((ball.getY() - 35), 15);
		AI = new Computer(pRight, ball, 40);
		
		
		addMouseMotionListener(this);
		
		setBackground(Color.green);
		offscreen = createImage(WIDTH,HEIGHT);
		bufferGraphics = offscreen.getGraphics();
	}
	
	// starts the Timer time and runs it until a player (or a computer) reaches score of 3
	public void start() {
		startingTime = System.currentTimeMillis()/1000;
		
		time.start();
		while(Math.abs(pRight.getScore())<3);
		time.stop();
		
		repaint();		
	}
	
	
	public void stop() {
	}
	
	// every 15ms, this is performed
	public void actionPerformed(ActionEvent arg0) {
		ball.move();
		pRight.setPos(AI.getPos());
		checkCollision();
		repaint();
	}
	
	public void checkCollision() {
		// bounce off of horizontal bounds
		if(ball.getY() <=0 || ball.getY() >= 290) 
			ball.dy = ball.dy*(-1);
		// if the ball reaches the player side (left) and hits the paddle
		// increase the speed (unless its maxed)
		if(ball.getX() == 40 && hitPaddle() ) {
			ball.dx = ball.dx*(-1);
			if(ball.dx >0 && ball.dx < 7) 
				ball.dx++;
			if(ball.dx < 0 && ball.dx > -7)
				ball.dx--;
		}
		// if the ball reaches the computer side (right) and hits the paddle
		// notice the speed does not increase
		if(ball.getX() == 460 && hitComPaddle())
			ball.dx = ball.dx*(-1);
		// if the computer paddle fails, append score and reset
		if(ball.getX() >= WIDTH ) {
			pRight.setScore(pRight.getScore() -1 );
			ball.reset();
		}
		// if the player fails, append score and reset
		if(ball.getX() <= 0) {
			pRight.setScore(pRight.getScore() + 1);
			ball.reset();
		}
	}
	
	public boolean hitPaddle() {
		boolean didHit = false;
		if((pLeft.getPos() - 10) <= ball.getY() && (pLeft.getPos() + 70) > ball.getY()) {
			didHit = true;
			// if the paddle is hit close the the edge, the vertical speed is increased
			if(( ball.getY() >= (pLeft.getPos() - 10) && ball.getY() <= (pLeft.getPos() + 10)) || ( ball.getY() >= (pLeft.getPos() + 50) && ball.getY() <= (pLeft.getPos() + 70)))
				if( ball.dy > 0 && ball.dy < 8) ball.dy++;
				else if( ball.dy > -8) ball.dy--;
		}
		return didHit;
	}
	
	public boolean hitComPaddle() {
		boolean didHit = false;
		if( (pRight.getPos() - 10 ) <= ball.getY() && ( pRight.getPos() + 70) > ball.getY())
			didHit = true;
		return didHit;
	}
	
	
	// as explained in tutorial, the screen is first drawn onto and Image via bufferGraphics
	// once the image is painted completely, it is painted on screen
	// thus:    [INPUT] ---(serial)-->  [BUFFER(IMAGE)] ---(block)--> [SCREEN]
	public void paint(Graphics g) {
		
		bufferGraphics.clearRect(0, 0, WIDTH, HEIGHT);
		
		bufferGraphics.setColor(Color.BLUE);
		bufferGraphics.fillRect(pLeft.XPOS, pLeft.getPos(),  10,  70);
		bufferGraphics.fillRect(pRight.XPOS, pRight.getPos(), 10, 70);
		
		bufferGraphics.setColor(Color.WHITE);
		bufferGraphics.setFont(newFont);
		bufferGraphics.drawString("Score",  140, 15);
		bufferGraphics.drawString("" + pRight.getScore(),  300, 15);
		bufferGraphics.drawString("Speed",  140, 30);
		bufferGraphics.drawString("x: " + Math.abs(4 - Math.abs(ball.dx)) + " y: " + Math.abs(4 - Math.abs(ball.dy)), 300, 30);
		bufferGraphics.fillRect(255,0,2,300);
		
		if(Math.abs(ball.dx) == 5) 
			bufferGraphics.setColor(Color.RED);
		else if(Math.abs(ball.dx) == 6)
			bufferGraphics.setColor(Color.YELLOW);
		else if(Math.abs(ball.dx) == 7)
			bufferGraphics.setColor(Color.WHITE);
		bufferGraphics.fillOval(ball.getX(), ball.getY(),  10, 10);
		
		bufferGraphics.setColor(Color.WHITE);
		bufferGraphics.drawString("Game length", 140, 45);
		bufferGraphics.drawString("" + (System.currentTimeMillis()/1000 - startingTime) + "s", 300, 45);
		
		g.drawImage(offscreen, 0, 0, this);
		try{ 	
			Toolkit.getDefaultToolkit().sync();
		} catch(AWTError error) {}
	}
	
	public void update(Graphics g){
		paint(g);
	}
	
	
	public void mouseMoved(MouseEvent arg0) {
		pLeft.setPos(arg0.getY() - 35);
	}
	
	public void mouseDragged(MouseEvent arg0) {
	}	
}

I can't attach the files (am not permitted, something about .java and .class extensions, sorry)

Advertisement

Well, there are many ways of doing the AI. Here is one suggestion:

In your current solution the AI paddle will constantly track the movement of the ball, and your difficulty setting essentially adjust the movement speed of the paddle. As an alternative, consider having the AI predict the final position of the ball instead, and then have the paddle move to that predicted location. You can change the difficulty by adjusting the accuracy of the prediction, the frequency of re-evaluation, and the response time to new predictions.

Best regards, Omid

If you think of it like air hockey there are usually 2 ways that people will mess up. One is being too slow to move to the balls position which you seem to have already implemented, and the other is being unable to track the ball because it's moving too fast. You can implement the latter rather easily.

When tracking the movement of the ball, don't do it always. Only do it 30 times every second( 30fps which is the average speed the human eye can capture images ). So every 1/30th of a second figure out where the ball is in and give instructions to the paddle to move to that point on the y axis. Difficulty could be changed by changing both the speed of the paddle, and the speed that the ai "eye" can track the ball.

I think this will add a very human element to your AI and make it a bit more believeable. For some of the more difficult AI, Omid's idea is really good. I'd imagine really good air hockey players would be able to figure out where a puck is going to end up and move accordingly rather than just following the puck around.

Thanks for both replise!

@Panda,

there already is delay at which the computer reacts, it sleeps for difficulty (a variable) seconds before it 'looks' again and attempts to reposition the paddle. Currently, the computer is blind for 40ms between each reposition.

edit:

the screen is regenerated every 15ms, which gives me the frame rate of 1000/15 =~ 66 FPS. 40ms is, I believe, an equivalent of skipping 2,something frames. Thats not much :/

To make the drawing more smooth, implement your own draw method.

Example:

public void draw(Graphics g)

{

}

You might have seen this commonly implemented in many open source game projects.

1) Draw on a Canvas with 2 bufferstrategies. Do not use the paint method. I know from experience that the overriden paint method and paintComponent method generally are not meant to be used for 2D games because the painting is slow. You should perform your own painting rather than use the paint method because then the operating system decide when to paint your game which explains the slow painting. Also set IgnorePaintPaint equal to true in the Game constructor.

For more details, refer to an open source Java basic game project that uses a Canvas, Graphics and Bufferstrategy class along with the Java API to fill in the blanks.

Good luck.

On a side note: for code readability, avoid using magic number. assign the magic number to a variable and use the variable in replace of the number you have currently used. You should also put your collision logic in a collision class for better code structure.

Similar to this: Collision collision = new Collision(ball,paddle);

Thanks for the tips. Interesting point about paint method being slow, will improve.

Thanks again! smile.png

hey - can you elaborate the "set ignorepaintpaint in Game constructor to true"?

Thanks for the tips. Interesting point about paint method being slow, will improve.

Thanks again! smile.png

hey - can you elaborate the "set ignorepaintpaint in Game constructor to true"?

The setIgnorePaint is a method that belongs in the Component class, which is a super class of a Canvas class. Your Game class will be a subclass of the Canvas class. By that scenario, you can call setIgnorePaint(true) in your constructor of the Game class. The setIgnorePaint method takes in a boolean parameter.

The example is a common convention of how most people write their game.

Example:

public class Game extends Canvas

{

public Game()

{

setIgnorePaint(true);

}

}

This method tells your operating system whether to ignore the painting messages being targeted to your Game class.

The reason you want to do this is because 2D games is an application that needs to have full control in painting rather than let something like the AWT draw the graphics context or surface for you. The reason you do not want the AWT to redraw your graphics context is because when your game redraws, those message need to get through the operating system and that can take some time to process. 2D games needs to act quick. Everything you see in front of your 2D games is actually redrawing a lot of things in front of you at a quick rate in a one second time frame when you are playing it.

The only time I can think of when you would let the AWT redrawn or repaint your graphics context or components (ie: GUI component like JPanel or JLabel) would be if you were developing a GUI application that does not require the painting speed required from a 2D game. A GUI application like Tic-Tac-Toe would be such an example because it uses a combination of Java GUI component like JLabel and JPanel and JButton.

Here is a good tutorial that does a nice job in provide the big picture of making a basic game and putting all the pieces together nicely along with really nice explanations about this subject and more of it.

http://www.cokeandcode.com/info/tut2d.html

Thanks a lot! I will definitely check that out!

Hey, quick question - here are the relevant classes:

Sprite ( a wrapper for image):


import java.awt.Graphics;
import java.awt.Image;

public class Sprite {
	private Image image;
	
	public Sprite(Image image) {
		this.image = image;
	}
	
	public int getWidth() {
		return image.getWidth(null);
	}
	
	public int getHeight() {
		return image.getHeight(null);
	}
	
	public void draw(Graphics g, int x, int y) {
		g.drawImage(image, x, y, null);
	}
	
}

and SpriteStore, for efficient handling of sprites, etc:


import java.util.HashMap;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.net.URL;
import java.awt.GraphicsConfiguration;
import java.awt.Transparency;
import java.awt.Image;
import java.io.IOException;

import javax.imageio.ImageIO;

public class SpriteStore {
	// singleton logic
	private static SpriteStore single;
	
	public static SpriteStore get() {
		return single;
	}
	
	// 
	
	private HashMap sprites = new HashMap();
	
	public Sprite getSprite(String ref) {
		if(sprites.get(ref) != null)
			return (Sprite)sprites.get(ref);
		// loader
		BufferedImage sourceImage = null;
		try {
			URL url = this.getClass().getClassLoader().getResource(ref);
			if(url == null)
				fail("Can't find that :( " + ref);
			sourceImage = ImageIO.read(url);			
		} catch(IOException e) {
			fail("Failed to load :( " + ref);
		}
		
		// accelerated graphics memory allocation
		GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
		Image image = gc.createCompatibleImage(sourceImage.getWidth(), sourceImage.getHeight(), Transparency.BITMASK);
		
		image.getGraphics().drawImage(sourceImage,0,0,null);
		
		Sprite sprite = new Sprite(image);
		sprites.put(ref, sprite);
		
		return sprite;
	}
	
	private void fail(String msg) {
		System.err.println(msg);
		System.exit(0);
	}
	
	
	
}

Everything is well understood except one thing - where exactly do I put my .jpg and .gif files in order to load them? (if it matters, I'm using Eclipse); which path (String ref) do I use in that case?
Thanks! biggrin.png

Hey, quick question - here are the relevant classes:

Sprite ( a wrapper for image):


import java.awt.Graphics;
import java.awt.Image;

public class Sprite {
	private Image image;
	
	public Sprite(Image image) {
		this.image = image;
	}
	
	public int getWidth() {
		return image.getWidth(null);
	}
	
	public int getHeight() {
		return image.getHeight(null);
	}
	
	public void draw(Graphics g, int x, int y) {
		g.drawImage(image, x, y, null);
	}
	
}

and SpriteStore, for efficient handling of sprites, etc:


import java.util.HashMap;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.net.URL;
import java.awt.GraphicsConfiguration;
import java.awt.Transparency;
import java.awt.Image;
import java.io.IOException;

import javax.imageio.ImageIO;

public class SpriteStore {
	// singleton logic
	private static SpriteStore single;
	
	public static SpriteStore get() {
		return single;
	}
	
	// 
	
	private HashMap sprites = new HashMap();
	
	public Sprite getSprite(String ref) {
		if(sprites.get(ref) != null)
			return (Sprite)sprites.get(ref);
		// loader
		BufferedImage sourceImage = null;
		try {
			URL url = this.getClass().getClassLoader().getResource(ref);
			if(url == null)
				fail("Can't find that :( " + ref);
			sourceImage = ImageIO.read(url);			
		} catch(IOException e) {
			fail("Failed to load :( " + ref);
		}
		
		// accelerated graphics memory allocation
		GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
		Image image = gc.createCompatibleImage(sourceImage.getWidth(), sourceImage.getHeight(), Transparency.BITMASK);
		
		image.getGraphics().drawImage(sourceImage,0,0,null);
		
		Sprite sprite = new Sprite(image);
		sprites.put(ref, sprite);
		
		return sprite;
	}
	
	private void fail(String msg) {
		System.err.println(msg);
		System.exit(0);
	}
	
	
	
}

Everything is well understood except one thing - where exactly do I put my .jpg and .gif files in order to load them? (if it matters, I'm using Eclipse); which path (String ref) do I use in that case?
Thanks! biggrin.png

Create a folder called "sprites". Put the 3 pictures: alien, ship and shot ending with gif extensions in that folder. Then put that same folder in the src folder of your project. And viola the game will run with those images!

Given the nature of the directory of the image in the Game class, it only accepts .gif files. However you can easily change .gif to .jpg so it will accept .jpg files instead. Note: The gif and jpg extensions are not case-senstive so capitalized them or having them lower-case will still load the images.

I notice you took out the package statements in the .java files. You should keep the package statements so you can use java packages. It is important to start learning how package works as you start working on projects that contained many .java files. You would not want 26 java files in one src folder for the sake of organization.

Had you kept the packages statements the way they were, the 7 .java files provided would be in a "folder insider a folder" structure. Those java files would have been in a folder called spaceinvaders. The spaceinvaders folder would have been a folder called "newdawn." The "newdawn" folder would have been in a folder called "org." The org folder would be in the folder called "src".

This topic is closed to new replies.

Advertisement