Pong 2020 - Pt.5
Collision now causes a reflection of the ball on both paddles in addition to declipping to reduce physics glitches. Score counters are added. The ball always reflects at the same angles, however.
The Pong Pt.5 video lesson documents the writing of this code and explains its structure.
Pong.java
package ceccs;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Pong extends Application {
double gameHeight = 500;
double gameWidth = 1000;
double padOffset = 5;
Ball ball;
Paddle padL, padR;
boolean downPressedL, upPressedL, downPressedR, upPressedR;
int scorePlayerL = 0;
int scorePlayerR = 0;
double scoreFontSize = 36;
Text lScoreText, rScoreText;
@Override
public void start(Stage primaryStage) throws Exception {
ball = new Ball(490, 240, -1, -.25);
padL = new Paddle(padOffset, gameHeight/2 - padL.height/2);
padR = new Paddle(gameWidth - padR.width - padOffset, gameHeight/2 - padL.height/2);
Pane pane = new Pane();
pane.getChildren().addAll(ball.rectangle, padL.rectangle, padR.rectangle);
Scene scene = new Scene(pane, gameWidth, gameHeight);
lScoreText = new Text(gameWidth/2 - 4 * scoreFontSize,2*scoreFontSize,"0");
lScoreText.setFont(new Font(scoreFontSize));
lScoreText.setFill(Color.BLACK);
rScoreText = new Text(gameWidth/2 + 2 * scoreFontSize,2*scoreFontSize,"0");
rScoreText.setFont(new Font(scoreFontSize));
rScoreText.setFill(Color.BLACK);
pane.getChildren().addAll(lScoreText, rScoreText);
primaryStage.setScene(scene);
primaryStage.setTitle("PONG");
primaryStage.setResizable(false);
primaryStage.show();
ball.updateGraphics();
padL.updateGraphics();
padR.updateGraphics();
scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
switch(event.getCode()){
case W:
upPressedL = true;
break;
case S:
downPressedL = true;
break;
case DOWN:
downPressedR = true;
break;
case UP:
upPressedR = true;
break;
}
}
});
scene.setOnKeyReleased(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
switch(event.getCode()){
case W:
upPressedL = false;
break;
case S:
downPressedL = false;
break;
case DOWN:
downPressedR = false;
break;
case UP:
upPressedR = false;
break;
}
}
});
AnimationTimer timer = new AnimationTimer() {
@Override
public void handle(long now) {
//input updates
checkInput();
//physics updates
padL.updatePosition();
padR.updatePosition();
ball.updatePosition();
checkGameBounds();
checkLeftPaddle();
checkRightPaddle();
//graphics updates
ball.updateGraphics();
padL.updateGraphics();
padR.updateGraphics();
}
};
timer.start();
}
public static void main(String[] args){
launch(args);
}
void checkGameBounds(){
// top and bottom boundary reflections
if (ball.y >= (gameHeight - ball.size) || (ball.y < 0)){
ball.reflectY();
}
// scoring and respawn on left and right boundaries
if (ball.x > (gameWidth)){
ball.x = gameWidth/2 - ball.size;
ball.reflectX(0);
ball.vy = ball.initVy;
scorePlayerL++;
System.out.println("L-" + scorePlayerL + " R-" + scorePlayerR);
lScoreText.setText(String.valueOf(scorePlayerL));
}
if (ball.x < -ball.size) {
ball.x = gameWidth/2 - ball.size;
ball.reflectX(0);
ball.vy = ball.initVy;
scorePlayerR++;
System.out.println("L-" + scorePlayerL + " R-" + scorePlayerR);
rScoreText.setText(String.valueOf(scorePlayerR));
}
//paddle boundaries and bounce
if (padL.y < 0) {
padL.y = 0;
padL.vy = padL.vy * -.5;
} else if (padL.y > gameHeight - padL.height) {
padL.y = gameHeight - padL.height;
padL.vy = padL.vy * -.5;
}
if (padR.y < 0) {
padR.y = 0;
padR.vy = padR.vy * -.5;
} else if (padR.y > gameHeight - padR.height) {
padR.y = gameHeight - padR.height;
padR.vy = padR.vy * -.5;
}
}
void checkLeftPaddle(){
double ballLeft = ball.x;
double ballRight = ball.x + ball.size;
double ballTop = ball.y;
double ballBottom = ball.y + ball.size;
double padLeft = padL.x;
double padRight = padL.x + padL.width;
double padTop = padL.y;
double padBottom = padL.y + padL.height;
double xOverlap = Math.min(ballRight, padRight) - Math.max(ballLeft, padLeft);
double yOverlap = Math.min(ballBottom, padBottom) - Math.max(ballTop, padTop);
if (xOverlap > 0 && yOverlap > 0) {
// Collision Detected!
// Declip ball and paddle
double ballCenter = ball.x + ball.size/2;
double paddleCenter = padL.x + padL.width/2;
if( ballCenter > paddleCenter) {
// ball is right of center
ball.x = padL.x + padL.width;
ball.reflectX(padL.vy);
} else {
ball.x = padL.x - ball.size;
}
// bounce
System.out.println("BING!");
}
}
void checkRightPaddle(){
double ballLeft = ball.x;
double ballRight = ball.x + ball.size;
double ballTop = ball.y;
double ballBottom = ball.y + ball.size;
double padLeft = padR.x;
double padRight = padR.x + padR.width;
double padTop = padR.y;
double padBottom = padR.y + padR.height;
double xOverlap = Math.min(ballRight, padRight) - Math.max(ballLeft, padLeft);
double yOverlap = Math.min(ballBottom, padBottom) - Math.max(ballTop, padTop);
if (xOverlap > 0 && yOverlap > 0) {
// Collision Detected!
// Declip ball and paddle
double ballCenter = ball.x + ball.size/2;
double paddleCenter = padR.x + padR.width/2;
if( ballCenter > paddleCenter) {
// ball is right of center
ball.x = padR.x + padR.width;
} else {
ball.x = padR.x - ball.size;
ball.reflectX(padR.vy);
}
// bounce
System.out.println("BING!");
}
}
void checkInput(){
// left paddle
if(downPressedL && !upPressedL){
padL.moveDownward();
}else if(upPressedL && !downPressedL){
padL.moveUpward();
} else {
padL.slowDown();
}
// right paddle
if(downPressedR && !upPressedR){
padR.moveDownward();
}else if(upPressedR && !downPressedR){
padR.moveUpward();
} else {
padR.slowDown();
}
}
}
Ball.java
package ceccs;
import javafx.scene.shape.Rectangle;
public class Ball {
double x, y, vx, vy, size;
double initVy;
Rectangle rectangle;
Ball(double startX, double startY, double startVx, double startVy){
x = startX;
y = startY;
vx = startVx;
vy = startVy;
initVy = startVy;
size = 20;
rectangle = new Rectangle(size, size);
}
void updateGraphics(){
rectangle.setX(x);
rectangle.setY(y);
}
void updatePosition(){
x = x + vx;
y = y + vy;
}
void reflectX(double paddleVy){
x = x - vx;
vx = -1 * vx;
vy = vy + paddleVy;
}
void reflectY(){
y = y - vy;
vy = -1 * vy;
}
}
Paddle.java
package ceccs;
import javafx.scene.shape.Rectangle;
public class Paddle {
double x, y, vy;
Rectangle rectangle;
static double speed = 3;
static double stopSpeed = .001;
static double accel = .01;
static double width = 20;
static double height = 100;
Paddle(double startX, double startY){
x = startX;
y = startY;
rectangle = new Rectangle(startX, startX, width, height);
}
void updateGraphics(){
rectangle.setX(x);
rectangle.setY(y);
}
void updatePosition(){
y = y + vy;
}
void moveUpward(){
if (vy > -speed) {
vy = vy - accel;
}
if (vy < -speed) {
vy = -speed;
}
}
void moveDownward(){
if (vy < speed) {
vy = vy + accel;
}
if (vy > speed) {
vy = speed;
}
}
void slowDown(){
if (vy > 0) {
vy -= accel;
} else if (vy < 0) {
vy += accel;
}
if (Math.abs(vy) < stopSpeed) {
vy = 0;
}
}
}