JavaFX Basic Motion Engine - 2025 Broadcast
This program demonstrates a basic motion engine coded in JavaFX.
MyFirstGameApp.java
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.media.AudioClip;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.stage.Stage;
//current progress: speed modifier buttons, better paddle/ball physics (paddle speed modifies)
//TODO: method for ball center, randomizer for initial velocity
//TODO: ball gets faster every hit
public class MyFirstGameApp extends Application {
StackPane outerStackPane;
HBox uiOverlayHbox;
Label p1ScoreLabel, p2ScoreLabel;
int p1Score, p2Score;
int numPointsForWin = 10;
AnimationTimer aTimer;
//physics data
Pane physPane;
double physW, physH;
Rectangle ball;
double ballX, ballY, ballVx, ballVy, ballAx, ballAy, ballSize;
double paddleHeight, paddleWidth;
double paddleSpeed, speedModifier;
Rectangle paddleL, paddleR;
double padLeftX, padLeftY, padLeftVy, padRightX, padRightY, padRightVy;
boolean p1UpPressed, p1DownPressed, p1FasterPressed,
p2UpPressed, p2DownPressed, p2FasterPressed;
AudioClip bipSoudnd;
@Override
public void start(Stage primaryStage) throws Exception {
/**Changes: smaller ball, thinner paddle, add speed modifier */
physW = 800;
physH = 600;
ballSize = 20;
paddleWidth = ballSize/2;
paddleHeight = physH / 6;
paddleSpeed = paddleHeight * 4;
speedModifier = 1.50;
//load sounds
bipSoudnd = new AudioClip(getClass().getResource("/sound/bip.wav").toExternalForm());
ball = new Rectangle(ballSize,ballSize, Color.HOTPINK);
paddleL = new Rectangle(paddleWidth, paddleHeight, Color.BLUE);
paddleR = new Rectangle(paddleWidth, paddleHeight, Color.BLUE);
physPane = new Pane(paddleL, paddleR, ball);
p1ScoreLabel = new Label("0");
p1ScoreLabel.setFont(new Font(ballSize));
p2ScoreLabel = new Label("0");
p2ScoreLabel.setFont(new Font(ballSize));
uiOverlayHbox = new HBox(p1ScoreLabel, p2ScoreLabel);
uiOverlayHbox.setAlignment(Pos.TOP_CENTER);
uiOverlayHbox.setSpacing(physW*.6);
outerStackPane = new StackPane(physPane, uiOverlayHbox);
Scene scn = new Scene(outerStackPane,physW,physH);
primaryStage.setScene(scn);
primaryStage.setResizable(false);
primaryStage.setTitle("Bad Pong :`(");
primaryStage.show();
aTimer = new AnimationTimer() {
long prevFrameTime = 0;
double minimumFrameTime = (1/60);
@Override
public void handle(long now) {
double t = getFrameTime(now);
if(t > minimumFrameTime) {
updateInput();
updatePhysics(t);
updateGraphics();
prevFrameTime = now;
}
}
double getFrameTime(long now){
double timeElapsed = (now - prevFrameTime) / 1e9;
return timeElapsed;
}
};
aTimer.start();
scn.setOnKeyPressed(event ->{
switch (event.getCode()){
case W -> p1UpPressed = true;
case S -> p1DownPressed = true;
case SHIFT -> p1FasterPressed = true;
case UP -> p2UpPressed = true;
case DOWN -> p2DownPressed = true;
case CONTROL -> p2FasterPressed = true;
}
});
scn.setOnKeyReleased(event ->{
switch (event.getCode()){
case W -> p1UpPressed = false;
case S -> p1DownPressed = false;
case SHIFT -> p1FasterPressed = false;
case UP -> p2UpPressed = false;
case DOWN -> p2DownPressed = false;
case CONTROL -> p2FasterPressed = false;
}
});
//initial physics data
ballX = physW/2-ballSize/2;
ballY = physH/2-ballSize/2;
ballVx = 400;
ballVy = 500;
ballAx = 0;
ballAy = 0;
padLeftX = ballSize;
padLeftY = physH/2 - paddleHeight/2;
padRightX = physW - ballSize - paddleWidth;
padRightY = physH/2 - paddleHeight/2;
}
public static void main(String[] args) {
launch(args);
}
void updatePhysics(double t){
/** t is the frame time elapsed */
//position rate updates
//acceleration updates velocity
ballVx += ballAx * t;
ballVy += ballAy * t;
//velocity updates position
ballX += ballVx * t;
ballY += ballVy * t;
//boundary check in x
//out of bounds causes scoring
// reset ball after score
//note: boundary value changed
if (ballX > (physW)) { // collision detection
//left side player score
p1Score++;
System.out.println("Left player scores! " + p1Score);
ballX = (physW/2 - ballSize/2); // re-center the ball
ballY = (physH/2 - ballSize/2);
ballVx = -ballVx; //send to opposite player
}
///left
if (ballX < 0 - ballSize) {
p2Score++;
System.out.println("Right player scores! " + p2Score);
ballX = (physW/2 - ballSize/2); // re-center the ball
ballY = (physH/2 - ballSize/2);
ballVx = -ballVx; //send to opposite player
}
//boundary check in y
if (ballY > (physH - ballSize)) { //detection
ballY = (physH - ballSize); //decipping
ballVy = -ballVy; //reflection
}
if (ballY < 0) {
ballY = 0; //declipping
ballVy = -ballVy;
}
//Paddle Left
padLeftY += padLeftVy * t;
//boundaries
//bottom
if (padLeftY > (physH - paddleHeight)) {
padLeftY = (physH - paddleHeight);
}
//top
if (padLeftY < 0) {
padLeftY = 0;
}
//left paddle-ball hitbox detection
double ballLeftSide = ballX;
double ballRightSide = ballX + ballSize;
double ballTop = ballY;
double ballBottom = ballY + ballSize;
double paddleL_leftSide = padLeftX;
double paddleL_rightSide = padLeftX + paddleWidth;
double paddleL_top = padLeftY;
double paddleL_bottom = padLeftY + paddleHeight;
double xOverlap_L = Math.min(ballRightSide, paddleL_rightSide) - Math.max(ballLeftSide, paddleL_leftSide);
double yOverlap_L = Math.min(ballBottom, paddleL_bottom) - Math.max(ballTop, paddleL_top);
if (xOverlap_L > 0 && yOverlap_L > 0) {
ballX = paddleL_rightSide; //declip
ballVx = -ballVx; //refect
/** paddle vy affects ball vy*/
ballVy += padLeftVy/2;
bipSoudnd.play(); //sound
}
//right paddle
padRightY += padRightVy * t;
//screen boundaries
//bottom
if(padRightY > (physH-paddleHeight)){
padRightY = (physH-paddleHeight);
}
//top
if(padRightY < 0) {
padRightY = 0;
}
//TODO: hitbox detection for R paddle
//NOTE: ball top, bottom, left and right are already calculated above
double paddleR_leftSide = padRightX;
double paddleR_rightSide = padRightX + paddleWidth;
double paddleR_top = padRightY;
double paddleR_bottom = padRightY + paddleHeight;
double xOverlap_R = Math.min(ballRightSide, paddleR_rightSide) - Math.max(ballLeftSide, paddleR_leftSide);
double yOverlap_R = Math.min(ballBottom, paddleR_bottom) - Math.max(ballTop, paddleR_top);
if(xOverlap_R > 0 && yOverlap_R > 0){
ballX = paddleR_leftSide - ballSize; //declip
ballVx = -ballVx; //refect
/** paddle vy affects ball vy*/
ballVy += padRightVy/2;
bipSoudnd.play(); //sound
}
}
void updateGraphics(){
ball.setLayoutX(ballX);
ball.setLayoutY(ballY);
paddleL.setLayoutX(padLeftX);
paddleL.setLayoutY(padLeftY);
//add padR to graphics update
paddleR.setLayoutX(padRightX);
paddleR.setLayoutY(padRightY);
p1ScoreLabel.setText(String.valueOf(p1Score));
p2ScoreLabel.setText(String.valueOf(p2Score));
}
void updateInput(){
/** speed modifier implemented to p1up/down and p2 up/down*/
if(p1UpPressed && !p1DownPressed){
if(p1FasterPressed) {
padLeftVy = -paddleSpeed*speedModifier;
} else {
padLeftVy = -paddleSpeed;
}
}
if(!p1UpPressed && p1DownPressed){
if(p1FasterPressed) {
padLeftVy = paddleSpeed*speedModifier;
} else {
padLeftVy = paddleSpeed;
}
}
if(!p1UpPressed && !p1DownPressed){
padLeftVy = 0;
}
if(p1UpPressed && p1DownPressed){
padLeftVy = 0;
}
//right paddle control update
if(p2UpPressed && !p2DownPressed){
if(p2FasterPressed){
padRightVy = -paddleSpeed * speedModifier;
} else {
padRightVy = -paddleSpeed;
}
}
if(!p2UpPressed && p2DownPressed){
if(p2FasterPressed) {
padRightVy = paddleSpeed * speedModifier;
} else {
padRightVy = paddleSpeed;
}
}
if(!p2UpPressed && !p2DownPressed){
padRightVy = 0;
}
if(p2UpPressed && p2DownPressed){
padRightVy = 0;
}
}
}