HIDE NAV

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;
        }
    }

}