Davis Burnside

Physics-based 2D game in Java, Pt 2

This post will demonstrate how the PhysicsSim game from part 1 can be rebuilt with a different structure. As I mentioned before, the structure of a program is a key factor in whether that program can be easily modified, understood, and reused for other projects.

If you are following along with a blank Project, download the image files and place them in the same folder where your code will go.

Download images only

If you don't want to follow along, you can get final finished code as well.

Download finished code and images

Any program can be thought of as a system of mini-programs; chunks of code that act as little cogs to make the whole machine turn. Let's look at the web of interaction between the 4 classes that comprised the PhysicsSim V1...

GameController:
Displayed program window (inherits from JFrame)
Accepted Mouse interaction (implements MouseListener)
Processed Mouse interaction (modifies velocity of Ball)
Ran Game timer
Computed game physics
Owned / Displayed GameCanvas instance on-screen
Owned Ball / Target Instance
Determines game state (Playing / Game over)

GameCanvas:
Held static reference to GameController's Ball / Target
painted Ball and Target to screen

Ball / Target:
Held information about their own state (location, size, etc)

Let’s imagine now a slightly different structure. We’ll start by rethinking how the Ball and Target should relate to each other. In part 1, one was the child of the other. Since they both have virtually the same functionality, (having measures of velocity, size, and position that is used by the physicsCalculations method), it’s more logical to make them siblings (inheriting from a common parent).

A new class, Entity, can be that common parent.

Add 3 new classes to your project, Entity, Ball, and Target,

create Entity.java


public class Entity {
    
    double xPos = 0;
    double yPos = 0;

    double xVelocity = 0;
    double yVelocity = 0;

    int size;
}
	    

create Ball.java


public class Ball extends Entity{

    double bounciness= 0.95;
    double friction = 0;
}
	    

create Target.java


public class Target extends Entity {
    
    double velocityModifier = 0.3;
}
	    

Now, let’s change how the game is rendered to screen. Instead of calling the repaint method on a single JComponent, such as the GameCanvas, we can use individual JComponents (JLabels with Images instead of Text) to represent the Ball and Target.

Old way: JFrame held one JComponent onto which all game objects were painted/rendered.

New way: JFrame holds a multiple JComponents, each of represents a different game object.

Let’s also shift some of GameController’s Game Logic responsibilities to another class. This is because the GameController is now also responsible for owning and displaying the Game objects (Target and Ball). Choking a single class up with the entire program’s workload is rarely a good idea, so the LogicHandler class will be introduced to pick up the physics calculations and game-state checking.

GameController

First, add the GameController class to your project.

create GameController.java


import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.net.URL;
import java.util.TimerTask;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;

public class GameController extends JFrame implements MouseListener {

    boolean runGame = true;
    int boardSizeX = 600;
    int boardSizeY = 600;
    final long cycleRate = 60;
    java.util.Timer timer;

    int gameAreaBufferSize = 40;
    int windowBarHeight = 20;

    ImageIcon ballImage;
    JLabel ballLabel;

    ImageIcon targetImage;
    JLabel targetLabel;

    int backgroundWidth = 800;
    int backgroundHeight = 800;
    ImageIcon backgroundImage;
    JLabel backgroundLabel;
    
    LogicHandler logicHandler = new LogicHandler();


    GameController() {

	super();

	setupJFrame();
	createEntityLabels();
	setLocationsOfEntityLabels();
	loadImages();
	createTimerTask();
    }

    void setupJFrame() {

	int totalBufferSize = (2 * gameAreaBufferSize);

	setSize(boardSizeX + totalBufferSize, boardSizeY + totalBufferSize + windowBarHeight);
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setVisible(true);
	this.setResizable(false);
	addMouseListener(this);
    }
    
    void createEntityLabels() {

	backgroundLabel = new JLabel();
	this.add(backgroundLabel);

	targetLabel = new JLabel();
	this.add(targetLabel);

	ballLabel = new JLabel();
	this.add(ballLabel);
    }

    void setLocationsOfEntityLabels() {

	int mainBallDiameter = logicHandler.mainBall.size;
	int mainBallRadius = mainBallDiameter / 2;
	int mainBallX = (int) logicHandler.mainBall.xPos
		+ gameAreaBufferSize 
		- mainBallRadius
		+ (int) logicHandler.screenShakeEntity.xPos;
	int mainBallY = (int) logicHandler.mainBall.yPos 
		+ gameAreaBufferSize 
		- mainBallRadius
		+ (int) logicHandler.screenShakeEntity.yPos;
	ballLabel.setBounds(mainBallX, mainBallY, mainBallDiameter, mainBallDiameter);
	
	int targetDiameter = logicHandler.target.size;
	int targetRadius = targetDiameter / 2;
	int targetX = (int) logicHandler.target.xPos 
		+ gameAreaBufferSize 
		- targetRadius 
		+ (int) logicHandler.screenShakeEntity.xPos;
	int targetY = (int) logicHandler.target.yPos 
		+ gameAreaBufferSize 
		- targetRadius
		+ (int) logicHandler.screenShakeEntity.yPos;
	targetLabel.setBounds(targetX, targetY, targetDiameter, targetDiameter);
	
	int baseLocationX = -100;
	int baseLocationY = -100;
	int backgroundX = baseLocationX 
		+ (int) logicHandler.screenShakeEntity.xPos 
		+ gameAreaBufferSize;
	int backgroundY = baseLocationY 
		+ (int) logicHandler.screenShakeEntity.yPos 
		+ gameAreaBufferSize;
	backgroundLabel.setBounds(backgroundX, backgroundY, backgroundWidth, backgroundHeight);
    }
    
    void loadImages() {

	try {
	    URL url;
	    Image image;

	    int mainBallDiameter = logicHandler.mainBall.size;
	    url = this.getClass().getResource("Ball.png");
	    image = ImageIO.read(url).getScaledInstance(mainBallDiameter, mainBallDiameter, Image.SCALE_SMOOTH);
	    ballImage = new ImageIcon(image);
	    ballLabel.setIcon(ballImage);
	    
	    int targetDiameter = logicHandler.target.size;
	    url = this.getClass().getResource("Target.png");
	    image = ImageIO.read(url).getScaledInstance(targetDiameter, targetDiameter, Image.SCALE_SMOOTH);
	    targetImage = new ImageIcon(image);
	    targetLabel.setIcon(targetImage);
	    
	    url = this.getClass().getResource("background.png");
	    image = ImageIO.read(url).getScaledInstance(backgroundWidth, backgroundHeight, Image.SCALE_SMOOTH);
	    backgroundImage = new ImageIcon(image);
	    backgroundLabel.setIcon(backgroundImage);

	} catch (Exception e) {
	    System.out.println("Load failed");
	    System.err.println(e);
	}
    }


    void createTimerTask() {

	TimerTask timerTask = new TimerTask() {

	    public void run() {

		if (runGame) {


		}
	    }
	};
	timer = new java.util.Timer();
	timer.schedule(timerTask, 0, 1000 / cycleRate);
    }

    @Override
    public void mousePressed(MouseEvent e) {

	int clickX = e.getX();
	int clickY = e.getY();
	String clickLocation = clickX + ", " + clickY;
	System.out.println("Mouse Pressed at " + clickLocation);
    }

    @Override
    public void mouseClicked(MouseEvent e) {
    }
    @Override
    public void mouseReleased(MouseEvent e) {
    }
    @Override
    public void mouseEntered(MouseEvent e) {
    }
    @Override
    public void mouseExited(MouseEvent e) {
    }

    public static void main(String[] args) {

	GameController gameWindow = new GameController();
    }

}

	    

Notice that most of the setup has already been taken care of: the class variables and timer are already present and the mouse interaction is already set up. (If you want an more in-depth look at what is happening inside the GameController, I explained it in better detail in part 1).

There are also some new variables in this version of GameController. There is an instance of the LogicHandler class, which I will cover in a moment. There are JLabels representing the Ball and Target, and an associated image reference for each. There is also another JLabel, the backgroundLabel, which is has no gameplay significance but is used for a cool visual effect later.

Also notice that I broke up the responsibilities of the GameController's constructor. The constructor from part 1 was a little messy. Now, it’s responsibilities have been have been moved to 5 separate methods that are more human-friendly to read.

setupJFrame() prepares a windowed application for viewing.

createEntityLabels() initializes the JLabel objects and adds them to the view heirarchy of the JFrame.

setLocationsOfEntityLabels() sets initial variables of the ball, target, and backgroundLabel, and sets their position inside the JFrame.

loadImages() attemps to load images from disk and assign them to a JLabel object.

createTimerTask() kicks off the game timer, starting the game.

LogicHandler

create LogicHandler.java

import java.awt.Point;
import static java.lang.Math.abs;

public class LogicHandler {

    Ball mainBall;
    Target target;
    Entity screenShakeEntity;

    LogicHandler() {

	// Create the ball
	mainBall = new Ball();
	mainBall.xPos = 200;
	mainBall.yPos = 200;
	mainBall.size = 50;

	// Create the Target
	target = new Target();
	target.xPos = 40;
	target.yPos = 100;
	target.size = 80;

	// Create the ScreenShakeEntity
	screenShakeEntity = new Entity();
	screenShakeEntity.xPos = 0;
	screenShakeEntity.yPos = 0;
	screenShakeEntity.size = 0;
    }

    void physics(int boardSizeX, int boardSizeY) {

	//================================================================
	//Physics for the target 
	//================================================================

	// If on the left half of the board, push right
	if (target.xPos > boardSizeX / 2) {

	    target.xVelocity -= target.velocityModifier;
	} 
	// If on the left half of the board, push right
	else if (target.xPos < boardSizeX / 2) {

	    target.xVelocity += target.velocityModifier;
	}
	target.xPos += target.xVelocity;

	//================================================================
	//Physics for the Ball 
	//================================================================

	double gravityAcceleration = 0.5;

	// gravity
	mainBall.yVelocity += gravityAcceleration;

	// Air resistance
	mainBall.xVelocity *= 0.996;
	mainBall.yVelocity *= 0.996;

	// collision detection of the four walls
	int leftWall = 0;
	int rightWall = boardSizeX;
	int topWall = 0;
	int bottomWall = boardSizeY;

	int ballRadius = mainBall.size / 2;

	// Used for setting the screenShakeEntity velocity
	float velocityBumpX = 0;
	float velocityBumpY = 0;

	if (mainBall.xPos - ballRadius < leftWall) {

	    mainBall.xVelocity = abs(mainBall.xVelocity) * mainBall.bounciness;
	    mainBall.xPos = leftWall + ballRadius;
	    velocityBumpX = (float) mainBall.xVelocity;
	}
	if (mainBall.xPos + ballRadius > rightWall) {

	    mainBall.xVelocity = -abs(mainBall.xVelocity) * mainBall.bounciness;
	    mainBall.xPos = rightWall - ballRadius;
	    velocityBumpX = (float) mainBall.xVelocity;
	}
	if (mainBall.yPos - ballRadius < topWall) {

	    mainBall.yVelocity = abs(mainBall.yVelocity) * mainBall.bounciness;
	    mainBall.yPos = topWall + ballRadius;
	    velocityBumpY = (float) mainBall.yVelocity;
	}
	if (mainBall.yPos + ballRadius > bottomWall) {

	    mainBall.yVelocity = -abs(mainBall.yVelocity) * mainBall.bounciness;
	    mainBall.yPos = bottomWall - ballRadius;
	    velocityBumpY = (float) mainBall.yVelocity;
	}

	float bumpThreshold = 5;
	if (velocityBumpY > -bumpThreshold && velocityBumpY < bumpThreshold) {
	    velocityBumpY = 0;
	}
	if (velocityBumpX > -bumpThreshold && velocityBumpX < bumpThreshold) {
	    velocityBumpX = 0;
	}

	mainBall.xPos += mainBall.xVelocity;
	mainBall.yPos += mainBall.yVelocity;

	//================================================================
	//Physics for the screenShakeEntity
	//================================================================

	// Add the bump from the ball colliding with the wall
	screenShakeEntity.xVelocity += velocityBumpX / 3;
	screenShakeEntity.yVelocity += velocityBumpY / 3;

	// Add the spring effect that moves the screenShakeEntity back to (0, 0)
	screenShakeEntity.xVelocity -= screenShakeEntity.xPos * (0.5);
	screenShakeEntity.yVelocity -= screenShakeEntity.yPos * (0.5);

	screenShakeEntity.xVelocity *= 0.9;
	screenShakeEntity.yVelocity *= 0.9;

	screenShakeEntity.xPos += screenShakeEntity.xVelocity;
	screenShakeEntity.yPos += screenShakeEntity.yVelocity;
    }

    boolean gameIsOver(){
    
	double distanceFromBallToGoal_X = mainBall.xPos - target.xPos;
	double distanceFromBallToGoal_Y = mainBall.yPos - target.yPos;
	
	double absoluteDistance = Math.sqrt((distanceFromBallToGoal_X * distanceFromBallToGoal_X)
		+ (distanceFromBallToGoal_Y * distanceFromBallToGoal_Y));

	double threshold = (target.size - mainBall.size) / 2;
	
	if (absoluteDistance <= threshold)
	    return true;

	return false;
    }
}	 
	    

While the GameController owns the representing JLabels and associated Images, the LogicHandler owns the actual Entity objects of ball, target, and screenShakeEntity.

Right now, LogicHandler only has two methods: it’s constructor and the physics method. In the constructor, the 3 Entity objects are initialized. The physics method is very similar to the physicsCalculations method from lesson 1.

Your codebase should now look like this: 5 classes and 3 images. It doesn't matter what the name of the package is, as long as all java files start with the same package declaration

Test run

There is only one more thing to do before we give a test run- the GameController's timer must make a call to the LogicHandler’s physics method, and the JLabels representing the Game Objects must then be repositioned to the location of logicHandler.ball and logicHandler.target

Back inside the GameController, there is a method called createTimerTask that is called from the constructor. Go inside that method and replace the TimerTask’s run method with-

replace inside GameController.java

public void run() {

    if (runGame) {

	logicHandler.physics(boardSizeX, boardSizeY);

	setLocationsOfEntityLabels();

	if (logicHandler.gameIsOver()) {
	    runGame = false;
	}
    }
}
	    

This game loop is very similar to part 1’s Game loop. First, the physics are computed. Then, the visuals is updated with a call to setLocationsOfEntityLabels.

Run the program- it should look like this...

Your codebase should now look like this: 5 classes and 3 images. It doesn't matter what the name of the package is, as long as all java files start with the same package declaration

Notice that wobble every time the ball strikes the floor? That is the effect of the screenShakeEntity, defined in the LogicHandler. When the ball collides with a wall in logicHandler.physics, the screenShakeEntity's velocity is modified, which in turn changes the position. The offset of the screenShakeEntity is then added to the positons of the ball and target JLabels.

Adding interaction

The GameController is already to configured to listen to mouse input (with the mousePressed method from MouseListener). But like I said earlier, the GameController is shifting Game Logic-oriented tasks to the LogicHandler. The GameController is just catching mouse input and sending it the logicHandler.throwBall method.
Add a method inside LogicHandler to translate the mouseclick into an action, and a method to reset the game variables too.

add to LogicHandler.java

void throwBall(int clickX, int clickY){

    double distanceX = (float)clickX - mainBall.xPos;
    double distanceY = (float)clickY - mainBall.yPos;

    double pushX = distanceX / 10;
    double pushY = distanceY / 10;

    mainBall.xVelocity += pushX;
    mainBall.yVelocity += pushY;
}

void resetAllGameObjects(){

    mainBall.xPos = 300;
    mainBall.yPos = 300;
    mainBall.size = 50;

    screenShakeEntity.xPos = 0;
    screenShakeEntity.yPos = 0;
    screenShakeEntity.size = 0;
}
	    

Finally, update the mouseListener method to call the throwBall method

replace inside GameController.java

    @Override
    public void mousePressed(MouseEvent e) {

	if (runGame) {

	    int clickX = e.getX() - gameAreaBufferSize;
	    int clickY = e.getY() - gameAreaBufferSize - windowBarHeight;

	    logicHandler.throwBall(clickX, clickY);
	} else {

	    logicHandler.resetAllGameObjects();
	    runGame = true;
	}
    }
	    

...And that's it! run the program, and the game should function exactly like it did in part 1.

by Davis Burnside
Posted Dec 26 2017