This guide is a continuation of the previous Simple Pong project. I’d encourage you to begin there if you haven’t already.
In this guide, you will learn how to implement extra features in your Pong game. Of course, what you choose to implement is ultimately up to you. However, for best results, do these in order.
Part 2: Better AI Logic
As it stands, the AI is pretty unintelligent. It just tracks the y position of the ball and moves the paddle in that direction. This means it cannot accurately predict where the ball is going when the ball is traveling at speeds faster than the paddle can move.
There are several options we can use to predict where the ball is going and preemptively move the paddle towards that position. I’m going to do this the easiest way I can conceptually imagine it.
I have also created a video tutorial on this topic, which you may watch on YouTube.
Future Ball Position Prediction
Rather than using complicated math to determine the ball’s current position, and where it’s going to hit, we can simply create a second invisible ball and have it move ahead of the main gameBall. Then, we can detect the y position where this invisible ball goes past the PC’s paddle. This is the location the paddle must travel to in order to intercept the ball upon its next return.
Besides being easy to conceptualize, this method of collision detection also ensures the program will work if we make the game window larger, or something else, in the future.
Getting Started…
Begin by opening (or making a copy of) your previous project.
In the PongGame class, we need to declare and initialize some new variables. First, we need a new Ball object to predict where the old ball is going. I’ve decided to call my new ball “futureBall” since it’s just like the gameBall, but several steps ahead.
You can just add this new ball after you declare the gameBall near the top of the PongGame class.
private Ball gameBall, futureBall;
Code language: PHP (php)
I want this Ball to start off just like the original Ball. It will, in essence, start as a copy of the original ball. There are two ways we can do this:
- We can create getters for each instance variable/property of the gameBall
- We can create a copy constructor
A copy constructor is arguably the faster option here.
Recall that copy constructors are simply constructors that take the same object type as their parameter and clone all the values from it. So in the Ball class, create a copy constructor after the standard constructor.
//ball constructor assigns values to instance variables
public Ball(int x, int y, int cx, int cy, int speed, Color color, int size) {
this.x = x;
this.y = y;
this.cx = cx;
this.cy = cy;
this.speed = speed;
this.color = color;
this.size = size;
}
//copy constructor
public Ball(Ball b){
this.x = b.x;
this.y = b.y;
this.cx = b.cx;
this.cy = b.cy;
this.speed = b.speed;
this.color = Color.WHITE;
this.size = b.size;
}
Code language: JavaScript (javascript)
In my copy constructor, I’ve kept all the values the same as the Ball we’re taking the values from (Ball b). The only difference is I made the copy ball WHITE instead of whatever its original color was. This will allow me to easily identify the futureBall once it’s copied. Although the ball will ultimately become invisible at the end, for testing purposes, it’s still helpful to paint it onto the screen.
Return to the PongGame class and initialize the futureBall with the gameBall as its constructor argument.
gameBall = new Ball(300, 200, 3, 3, 3, Color.YELLOW, 10);
futureBall = new Ball(gameBall);
Code language: JavaScript (javascript)
Now the futureBall is a white clone of gameBall.
Moving futureBall Ahead
Now that we have a futureBall, we need to advance its position ahead of the gameBall and predict where it will collide with the PC paddle on the right.
This can all be done in the PongGame class. Begin by creating another instance variable of type int, to keep track of the predicted Y collision point on the PC’s goal. Initially, since we don’t know where this will be, I will set it to negative one. I called this variable “detectedCollideY”
In addition to the detected location variable, I’m also going to create a boolean to keep track of if the PC’s paddle reached its target or not. This will be useful for another feature in the near future…
So now, my new instance variables and my PongGame constructor looks like this:
PongGame.java
private int detectedCollideY; //the location we think the ball will collide with the PC's paddle
private boolean pcGotToTarget; //whether or not the PC has reached the target paddle location
/**
* Standard constructor for a PongGame
*/
public PongGame() {
//Make the ball and paddles
gameBall = new Ball(300, 200, 3, 3, 3, Color.YELLOW, 10);
//futureBall is a clone of gameBall, but with a white color
futureBall = new Ball(gameBall);
userPaddle = new Paddle(10, 200, 75, 3, Color.BLUE);
pcPaddle = new Paddle(610, 200, 75, 3, Color.RED);
//Set instance variables to zero to start
userMouseY = 0;
userScore = 0; pcScore = 0;
bounceCount = 0;
//set detectedCollideY to -1
detectedCollideY = -1;
pcGotToTarget = false;
//listen for motion events on this object
addMouseMotionListener(this);
}
Code language: PHP (php)
Now let’s work on moving the futureBall ahead of the gameBall. Remember, the futureBall behaves exactly like the gameBall. So we can repeat much of the logic in the gameLogic() method. In gameLogic, the gameBall does a few things –
- It moves
gameBall.moveBall();
- We check for bouncing off the top/bottom of screen
gameBall.bounceOffEdges(0, WINDOW_HEIGHT);
- We check if the ball collides with either paddle
pcPaddle.checkCollision(gameBall)
So we need to do the same things with the futureBall. The key difference is that we will advance the future ball several steps at a time using a loop and assume it always bounces off the user’s paddle on the left. Then, when the futureBall goes past the PC paddle’s x position, we know the location the paddle needs to move to. At this point, we set detectedCollideY to its value and have the paddle move in that direction. We can also stop moving futureBall until the next time it’s needed.
Start by painting futureBall in the paintComponent method after everything else is painted. This way, we can see where it’s going.
futureBall.paint(g);
Code language: CSS (css)
Since we ultimately need to check if the ball goes past the PC paddle’s x position, we should also create a simple getter in the Paddle class.Paddle.java
public int getX(){
return x;
}
Code language: PHP (php)
With that set, go to the gameLogic class and begin working on the loop for the futureBall position changes and detection.
PongGame.java
public void gameLogic() {
//move the ball one frame
gameBall.moveBall();
//edge check/bounce
gameBall.bounceOffEdges(0, WINDOW_HEIGHT);
//if we haven't yet detected the collision point
if(detectedCollideY == -1){
//loop ten steps ahead
for(int i = 0; i < 10; i++){
//move the future ball one step
futureBall.moveBall();
futureBall.bounceOffEdges(0, WINDOW_HEIGHT);
//if the future ball collides with the x position of the user paddle, reverse the x direction
if(futureBall.getX() < userPaddle.getX() + Paddle.PADDLE_WIDTH){
futureBall.reverseX();
}
//if prediction ball goes past the pc paddle x, we know this is where the paddle needs to move to
if(futureBall.getX() > pcPaddle.getX()){
detectedCollideY = futureBall.getY();
//for testing
System.out.println("future collision at: " + detectedCollideY);
//stop the loop here
break;
}
}
}
//move the pc paddle towards the detected collision point
pcPaddle.moveTowards(detectedCollideY);
Code language: JavaScript (javascript)
Examine the code in the for loop above. First, it checks if we know the collision point yet. If the detectedCollideY variable is -1, we must not know it yet. Then, we loop ten steps using a for loop. Within this loop, we advance the futureBall and check for collisions the same way we did with the gameBall. The only difference is that we don’t check for a collision with the left paddle, but rather, we always assume the user hits it. This way it bounces back where it should and we can find the final point it collides on the right.
Finally, when the futureBall’s x position is beyond the x position of the PC paddle, we know that the futureBall is at the location the gameBall will pass the goal, assuming the user can return the ball. So we set detectedCollideY to the Y position of futureBall. Finally, the pcPaddle is set to move towards this detected location.
Also, please remove the line that moves the pcPaddle to the ball’s y position, since we’re not using that logic anymore.
pcPaddle.moveTowards(gameBall.getY()); // delete this
Code language: JavaScript (javascript)
You should now be able to test the code and see the paddle move towards the proper location represented by the white futureBall.
Note that since we didn’t do anything to reset the detectedCollideY or the futureBall’s positioning, it will only work for the first bounce.
The next step is to reset the futureBall and the detectedCollideY after the actual gameBall collides with the pcPaddle.
Depending on if you originally followed along with the first video, or if you downloaded the original copy from GitHub, ypur paddle collision logic might look slightly different.
In the gameLogic method, you may have paddle collision logic that starts like this:
//check if ball collides with either paddle
if(pcPaddle.checkCollision(gameBall) || userPaddle.checkCollision(gameBall)){
Code language: JavaScript (javascript)
Alternatively, you may have two separate methods (as I coded it in the YouTube video). If you had it using the || or operator, separate it into two separate if blocks. We need to check if it collides with the pcPaddle separate from the userPaddle. Once the ball collides with the PC’s paddle, we reset the futureBall (make it clone the gameBall again) and the other instance variables that go along with it.
//check if ball collides with either paddle
if(userPaddle.checkCollision(gameBall)){
gameBall.reverseX();
bounceCount ++;
}
//if we collide with pcPaddle
if(pcPaddle.checkCollision(gameBall)){
gameBall.reverseX();
//we need to reset the prediction ball
futureBall = new Ball(gameBall);
//reset the detected collision point
detectedCollideY = -1;
//this is for later...
pcGotToTarget = false;
bounceCount ++;
}
Code language: JavaScript (javascript)
To make testing easier, I’ve set the bounce count to increase speed after 2 bounces. I’ve also made the user paddle much taller, so I don’t have to try as hard to test consecutive bounces.
Test the program. Now, the PC should be able to predict the location the yellow ball is going to each time.
Now we mostly have the AI logic working. However, we still need to make sure we’re resetting the futureBall and the other variables when the user misses the ball. So in the PongGame’s reset method, reset the futureBall too.
public void reset(){
//reset gameBall and paddles
gameBall = new Ball(320, 220, 3, 3, 3, Color.YELLOW, 10);
userPaddle = new Paddle(10, 200, 75, 3, Color.BLUE);
pcPaddle = new Paddle(610, 200, 75, 3, Color.RED);
bounceCount = 0;
//reset futureBall
futureBall = new Ball(gameBall);
detectedCollideY = -1;
pcGotToTarget = false;
}
Code language: JavaScript (javascript)
Now when the user misses the ball, the game resets and the futureBall collision detection resets as well.
At this point, the AI can flawlessly predict the location the ball will go towards every single time. This means that, aside from a few rare scenarios, the AI cannot possibly lose. I’m going to set my paddle back down to normal size and make the futureBall invisible again by removing it from the paintComponent method.
Now the “AI” for ball collision prediction is fully working. However, the paddle just moves to the exact right position every time and sits there. This makes the game feel very robotic (because it is).
It’s also no fun losing every time because the computer is unbeatable and knows exactly where the ball is heading in advance.
More Realistic AI Behaviors
The first thing we can do to make the PC’s movements feel more human-like is to not just magically move to the right position and stop there in advance every time, but to have the PC’s paddle oscillate or bounce up and down once it gets to the right position. That way, it feels like the PC is “thinking” of where to move. Depending on how much oscillation you have, this could also factor into the PC losing a round. For example, if the PC paddle oscillates up 20 pixels from the actual target location, and that difference causes the ball to miss the PC’s paddle, then the user scores a point.
Recall that earlier I created a boolean instance variable called pcGotToTarget. It’s not doing anything yet, but we set it to false every time we reset the futureBall. We’re going to use this boolean to help us with the oscillations.
Here is how the logic works:
- Set pcGotToTarget to True when pcPaddle reaches the detectedCollideY position
- When pcGotToTarget is true, we will begin bouncing the paddle up and down
- This is achieved by having another “oscillateTowards” variable, which is 0 or the window height.
- Each frame, we’ll move the pcPaddle up or down towards the oscillateTowards variable.
- When the pcPaddle’s center Y position is above or below a preset limit, the oscillateTowards variable will be swapped, causing the paddle to move up and down within these bounds.
- Once the ball bounces off the paddle or the game is reset, the oscillation stops until the PC paddle is back at the detectedCollideY again, as was established in the previous section.
So to start, we need to detect if the PC’s paddle has reached the target. This involves knowing the center position of the PC’s paddle and matching it to detectedCollideY. So we need to create a getter for the PC paddle’s center position in the Paddle class.Paddle.java
public int getCenterY(){
return y + height / 2;
}
Code language: PHP (php)
Return to PongGame and the gameLogic method. After we figure out the Y collision location from futureBall, let’s check if the PC’s paddle is within 3 pixels of the actual target.
//if the pcPaddle is within 3 pixels of the detected collision point, stop moving
if((Math.abs(pcPaddle.getCenterY() - detectedCollideY) < 3) && !pcGotToTarget){
pcGotToTarget = true;
System.out.println("pc got to target");
}
Code language: JavaScript (javascript)
So if the absolute value of the difference between the center of the paddle and the target collide location is less than 3, I set the boolean pcGotToTarget to true. Now, we know the PC’s paddle is centered on the location the gameBall will eventually collide. That means we can start oscillating up and down now. I included the println statement for testing.
At the top of the PongGame class, create a new private int called “oscillateTowards”
Set this to 0 or WINDOW_HEIGHT in the constructor (either direction would work fine to start)
At this point, the instance variables and constructor for my PongGame look like this:
PongGame.java
public class PongGame extends JPanel implements MouseMotionListener {
//Constants for window width and height, in case we want to change the width/height later
static final int WINDOW_WIDTH = 640, WINDOW_HEIGHT = 480;
private Ball gameBall, futureBall;
private Paddle userPaddle, pcPaddle;
private int userScore, pcScore;
private int userMouseY; //to track the user's mouse position
private int bounceCount; //to count number of ball bounces between paddles
private int detectedCollideY; //the location we think the ball will collide with the PC's paddle
private boolean pcGotToTarget; //whether or not the PC has reached the target paddle location
private int oscillateTowards; //the y position to oscillate towards
/**
* Standard constructor for a PongGame
*/
public PongGame() {
//Make the ball and paddles
gameBall = new Ball(300, 200, 3, 3, 3, Color.YELLOW, 10);
//futureBall is a clone of gameBall, but with a white color
futureBall = new Ball(gameBall);
userPaddle = new Paddle(10, 200, 75, 3, Color.BLUE);
pcPaddle = new Paddle(610, 200, 75, 3, Color.RED);
//Set instance variables to zero to start
userMouseY = 0;
userScore = 0; pcScore = 0;
bounceCount = 0;
//set detectedCollideY to -1
detectedCollideY = -1;
pcGotToTarget = false;
oscillateTowards = 0;
//listen for motion events on this object
addMouseMotionListener(this);
}
Code language: PHP (php)
With the oscillateTowards variable now available, let’s put it into action. Back in the gameLogic method, surround the statement that moves the PC Paddle with an if condition that checks if pcGotToTarget is false.
//if the pcPaddle is within 3 pixels of the detected collision point, stop moving
if((Math.abs(pcPaddle.getCenterY() - detectedCollideY) < 3) && !pcGotToTarget){
pcGotToTarget = true;
System.out.println("pc got to target");
}
if(!pcGotToTarget){
//move the pc paddle towards the detected collision point
pcPaddle.moveTowards(detectedCollideY);
}
Code language: JavaScript (javascript)
Now the game won’t keep trying to move the paddle to the target position once it gets within a 3 pixel range. More importantly, we can now add an else block after that check to handle the oscillation logic.
if(!pcGotToTarget){
//move the pc paddle towards the detected collision point
pcPaddle.moveTowards(detectedCollideY);
}
else{
//if the pc paddle is within 10 pixels of the detected collision point, oscillate up and down
if(pcPaddle.getCenterY() > detectedCollideY + 10){
//once we reach a point 10 below collision point, start moving up towards 0
oscillateTowards = 0;
}
else if(pcPaddle.getCenterY() < detectedCollideY - 10){
//once we reach a point 10 above, start moving down towards WINDOW_HEIGHT
oscillateTowards = WINDOW_HEIGHT;
}
//move the pc paddle up or down by having it moveTowards oscillateTowards
pcPaddle.moveTowards(oscillateTowards);
}
Code language: JavaScript (javascript)
The above code checks if the paddle has reached a spot 10 above the center target we originally detected. If it does, we reverse direction and target a new target position. Once the paddle reaches a position 10 below its original target, we do the opposite and have it move back up again. I used the points 0 and WINDOW_HEIGHT, but really any number 0 or smaller and WINDOW_HEIGHT or larger would have achieved the same thing. For example, you could use -1000 to 1000 here. It’s still not going more than about 10 above or below the original target.
Now, if you test the game, the PC’s paddle should appear to move up and down around the target point rather than stopping exactly in its center.
While this still looks a little robotic, I think its slightly better than just stopping exactly at the location the ball is guaranteed to hit.
Resolve a Corner Issue…
You may have already noticed this, but if the Pong ball collides with a paddle at a 45 degree angle to the paddle’s corner, it may appear to bounce back and forth inside the paddle before being ejected out the side.
This happens due to an issue with collision detection and the point where we reverse the x direction of the ball in the gameLogic method.
The easiest way to resolve this issue is to simply nudge the ball out a few more steps after it collides with a paddle. This will prevent it from getting stuck inside the paddle, and we don’t have to worry about dealing with changing the actual collision detection logic.
If you don’t already have a setter for the x variable in the Ball class, make sure you create that as well. Then, in the gameLogic method of PongGame where we check if the ball collides with a paddle, add a statement to both sections to “nudge” the ball one space outside of the paddle. For collisions with the left paddle, we want to set the gameBall’s x location to 1 pixel right of the paddle, so it’s the paddle’s X position, plus the width of the paddle, plus one.
For the PC’s paddle, it’s the opposite. We want to nudge it to the left. Recall in the first Pong guide that we actually performed our detection outside, to the left of the actual PC paddle. We did that to account for the x position of the ball actually starting in the upper left corner. So we set the ball’s X position to ten value less than the paddle’s x position for the PC paddle collision.
//check if ball collides with either paddle
if(userPaddle.checkCollision(gameBall)){
gameBall.reverseX();
//nudge the ball one pixel to the right of the user paddle
gameBall.setX(userPaddle.getX() + Paddle.PADDLE_WIDTH + 1);
bounceCount ++;
}
//if we collide with pcPaddle
if(pcPaddle.checkCollision(gameBall)){
gameBall.reverseX();
//nudge prediction ball to the left of the pc paddle
gameBall.setX(pcPaddle.getX() - 10);
//we need to reset the prediction ball
futureBall = new Ball(gameBall);
//reset the detected collision point
detectedCollideY = -1;
//this is for later...
pcGotToTarget = false;
bounceCount ++;
}
Code language: JavaScript (javascript)
That should fix the issue.
Randomize PC Losses
As with anything, there are numerous ways you could make the PC lose. We could also add different difficulties to the game, making the PC more or less likely to lose.
I want to do something relatively simple, based on random chance. I have decided I want there to be a 33% chance that the PC “accidentally” misses the next ball each time the ball bounces off the PC’s paddle.
This means that roughly one time every 6 bounces (or 3 back and forths), the PC will miss.
To begin, I created a new boolean instance variable in the PongGame class to keep track of whether or not the pc should accidentally miss the next ball.PongGame.java
//near the top with other variable declarations...
private boolean pcAccidentalMiss; //whether or not the PC "accidentally" misses the next ball
Code language: PHP (php)
I then set the variable to false in both the PongGame constructor and the reset() method.
Back in gameLogic, after the ball bounces off the PC’s paddle, I calculate that 33% chance of missing using Java’s Math.Random object.
if(pcPaddle.checkCollision(gameBall)){
gameBall.reverseX();
//nudge prediction ball one pixel to the left of the pc paddle
gameBall.setX(pcPaddle.getX() - 1);
//we need to reset the prediction ball
futureBall = new Ball(gameBall);
//reset the detected collision point
detectedCollideY = -1;
//track if we made it to the target position
pcGotToTarget = false;
bounceCount ++;
//there is a 1/3 chance of setting pcAccidentalMiss to true
if((int)(Math.random() * 3) == 0){
pcAccidentalMiss = true;
System.out.println("pc should miss next bounce");
}
}
Code language: JavaScript (javascript)
Now we have a way to enable the boolean. So the final step is to make the PC miss.
Recall that the PC paddle has a height of 75 pixels, and it’s set to oscillate up and down around the exact point the ball will hit. This means to make it miss, all we have to do is move the detectedCollideY up or down by a certain amount.
For simple testing, I’m just going to increase it by 75 when the boolean is true, after we detect the actual location it’s supposed to hit.
I do this in the same loop where I calculated the actual position the futureBall collides with the paddle. If the boolean is true, I offset the variable and now, it should be targeting a position slightly off from the real position.
//if we haven't yet detected the collision point
if(detectedCollideY == -1){
//loop ten steps ahead
for(int i = 0; i < 10; i++){
//move the future ball one step
futureBall.moveBall();
futureBall.bounceOffEdges(0, WINDOW_HEIGHT);
//if the future ball collides with the x position of the user paddle, reverse the x direction
if(futureBall.getX() < userPaddle.getX() + Paddle.PADDLE_WIDTH){
futureBall.reverseX();
}
//if prediction ball goes past the pc paddle x, we know this is where the paddle needs to move to
if(futureBall.getX() > pcPaddle.getX()){
detectedCollideY = futureBall.getY();
//if the PC is set to accidentally miss, increase detectedCollideY by 100
if(pcAccidentalMiss){
detectedCollideY += 75;
}
//stop the loop here
break;
}
}
}
Code language: JavaScript (javascript)
When I test this, after a few bounces, it should miss, as shown in this video.
For your reference, you may find all the code discussed up to this point in the following GitHub repo:
Part 3: Compartmentalization
Now that I have added a significant amount of code to the gameLogic method, and it’s mostly working the way I want, it makes sense to begin separating the code into distinct methods. For example, we could try separating the gameBall logic from the futureBall logic. Or, better yet, see if there’s a way we can combine part of the logic regarding the ball positioning since both are Ball objects and they ultimately behave similarly, despite the futureBall being many steps ahead of the gameBall.
If you’ve been following along up to this point, examine the gameLogic function and review what each part does. Some of it involves many levels of nested if statements – is there a way we could simplify this?
- gameLogic() method – is executed once per frame
- Advances the position of gameBall and checks for bouncing off top/bottom of screen
- Checks if we’ve determined where the PC paddle needs to move to
- If not, advances the futureBall ahead 10 steps, and checks for bouncing off top/bottom of screen, just like the gameBall does
- Checks for bounces off left paddle area (always assumes a bounce is successful)
- Determines the y position where the futureBall crosses the PC player’s goal
- Ends the loop when the next position is determined
- Checks if pcPaddle is where it needs to be
- Advances towards the target if it’s not
- Begins oscillating paddle up and down if it is
- Moves the User’s paddle towards their mouse y position
- Checks for collision between gameBall and userPaddle
- Increases speed once every 3 bounces
- Checks win/loss conditions
Now, that’s a bit more logic than we reasonably need in a single method. Here’s how I would suggest breaking it gameLogic into smaller functions:
- Condense the repeated moveBall and bounceOffEdges into a single handleAllMovement method in the Ball class
- Separate the paddle movement logic into a separate private method within PongGame
- Implement the logic used to check if the paddle is near a certain location into an “isNear” boolean method in the Paddle class
- Implement the logic for oscillations in a new “oscillate” method in the Paddle class rather than as a direct part of gameLogic
- Separate the paddle bounce logic, bounce count, etc. into another method in PongGame
- Separate loss checking into a new method under PongGame
Initially, when the game logic was very simple, it was easy enough for me to remember what each part of code did in my head. Now, since the overall complexity of the entire project is increasing, it’s becoming more difficult to manage. This may be a good time to stop and create a UML diagram illustrating the functions of the different classes. Especially if I think I’ll be adding even more functionality to the game in the future.
More details coming soon… guide is a work in progress.