My program is meant to simulate 2D physics(hopefully 3D as well in the future), namely classical mechanics and eventually electromagnetism. Currently it simulates projectile motion by firing a cannonball(which is plainly a black ellipse) from an invisible cannon. It simply spawns an ellipse with an initial velocity and draws its motion.
My concern is that I have designed the program poorly in terms of organization and refactorability, if the latter is a word. Basically, I want to be able to build upon this model and add more simulations to it, so I want it to be designed well.
I used a GUI Form in IntelliJ IDEA to layout the JPanels, JLabels, etc.
If I want to eventually add collisions, explosions, magnetic fields, etc etc(perhaps in a tabbed pane that holds each type of simulation), am I moving in the correct direction in terms of design?
Edit: Any opinions or tips would answer this question.
Here is a screenshot(balls spawned in bottom-left corner):
Here is my code:
ProjectileMotionApp.java
import javax.swing.*;
import java.util.Timer;
import java.util.TimerTask;
/*
* Creates JFrame and runs the simulation
*/
public class ProjectileMotionApp {
public static void main(String[] args) {
// The main panel that has embedded panels for the UI and the scene
GUI gui = new GUI();
// 60 fps (assuming computer can keep up)
int DELAY = 1000 / 60;
Timer timer;
JFrame frame = new JFrame("Projectile Motion");
frame.setContentPane(gui.getRootPanel());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setSize(900, 600);
frame.setVisible(true);
timer = new java.util.Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// has moving objects
if(gui.getScene().inFlux()) {
gui.getScene().updateBalls();
gui.getScene().repaint();
}
}
}, 0, DELAY);
}
}
GUI.java
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/*
* Creates GUI. Adds a UI and "scene"
*/
public class GUI {
private JPanel rootPanel;
private JPanel userInputPanel;
private JButton FIREButton;
private JTextField ballMassTextField;
private JTextField ballFireVelocityTextField;
private JTextField gravTextField;
private JTextField airResistanceTextField;
private JLabel ballMassLabel;
private JLabel ballFireVelLabel;
private JLabel gravityConstantLabel;
private JLabel airResistanceLabel;
private JLabel massUnitLabel;
private JLabel gravUnitLabel;
private JLabel velUnitLabel;
private JLabel forceUnitLabel;
private JTextField dirTextField;
private Scene scene;
// convert to seconds
private final double deltaTime = 1 / (double)60;
public GUI() {
rootPanel = new JPanel();
// tried BorderLayout to make scene as big as possible in JFrame, didn't work
rootPanel.setLayout(new GridLayout(2, 1));
// Instantiates scene with Earth-like gravity(9.8 m/s^2) and zero air resistance(0 N)
scene = new Scene(9.8, 0, deltaTime);
rootPanel.add(userInputPanel);
rootPanel.add(scene);
FIREButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
//15 kg
double mass = 15;
// 150 m/s
double velocity = 150;
// 45 degrees
double dir = 45;
/* Replace default values */
if(!ballMassTextField.getText().equals("")) {
mass = Double.parseDouble(ballMassTextField.getText());
}
if(!ballFireVelocityTextField.getText().equals("")) {
velocity = Double.parseDouble(ballFireVelocityTextField.getText());
}
if(!gravTextField.getText().equals("")) {
scene.setGravity(Double.parseDouble(gravTextField.getText()));
}
else {
scene.setGravity(9.8);
}
if(!airResistanceTextField.getText().equals("")) {
scene.setDrag(Double.parseDouble(airResistanceTextField.getText()));
}
else {
scene.setDrag(0);
}
if(!dirTextField.getText().equals("")) {
dir = Double.parseDouble(dirTextField.getText());
}
// makes angle with horizontal axis
dir = Math.toRadians(360 - dir);
Projectile ball = new Projectile(10, scene.getHeight() - (scene.getHeight() / 16), mass, velocity, dir);
scene.addBall(ball);
}
});
}
public JPanel getRootPanel() {
return rootPanel;
}
public Scene getScene() {
return scene;
}
}
Scene.java
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.util.ArrayList;
/*
* The "scene" of the simulation. JPanel that draws balls
*/
public class Scene extends JPanel {
private ArrayList<Projectile> balls;
private ArrayList<Projectile> explosionBits;
private ArrayList<Integer> locs;
private double g;
private double d;
private double dt;
public Scene(double gravity, double airResistance, double deltaTime) {
this.setBackground(Color.WHITE);
this.setSize(500, 800);
balls = new ArrayList<>();
explosionBits = new ArrayList<>();
locs = new ArrayList<>();
g = gravity;
d = airResistance;
dt = deltaTime;
}
public void addBall(Projectile ball) {
balls.add(ball);
}
// adds exploded bits to scene on impact
public void addExplosiveBits() {
}
/*
* Updates coordinates according to kinematic equations
*/
public void updateBalls() {
for(int i = 0; i < balls.size(); i++) {
balls.get(i).updatePosition(dt, g, d);
balls.get(i).updateVelocity(dt, g, d);
if(balls.get(i).getXPos() < 0 || balls.get(i).getYPos() > this.getHeight()) {
balls.remove(i);
//addExplosiveBits();
i--;
}
}
}
public void setGravity(double gravity) {
g = gravity;
}
public void setDrag(double airResistance) {
d = airResistance;
}
public ArrayList<Projectile> getBalls() {
return balls;
}
public boolean inFlux() {
if(balls.isEmpty())
return false;
return true;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
/* Cast it to Graphics2D */
Graphics2D g2 = (Graphics2D) g;
g2.setColor(Color.BLACK);
/* Enable anti-aliasing and pure stroke */
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
for(Projectile ball : balls) {
/* Construct a shape and draw it */
Ellipse2D.Double shape = new Ellipse2D.Double(ball.getXPos(), ball.getYPos(), 5, 5);
g2.fill(shape);
}
}
}
Projectile.java
/*
* Projectile with properties
*/
public class Projectile {
private double xPos; // x-coordinate
private double yPos; // y-coordinate
private double xVel; // x component velocity magnitude
private double yVel; // y component velocity magnitude
private double dir; // angle ball is shot from
// used to instantiate component velocities
private double m; // mass
public Projectile(double x, double y, double mass, double velocity, double direction) {
xPos = x;
yPos = y;
m = mass;
dir = direction;
xVel = velocity * Math.cos(dir);
yVel = velocity * Math.sin(dir);
}
/*
* Updates coordinates according Kinematic Equations(metric system)
*
* x = xo + vo t + ½ a t2
*/
void updatePosition(double dt, double g, double d) {
xPos = xPos + (xVel * dt) - ((d/m) * Math.pow(dt, 2) * 0.5);
yPos = yPos + (yVel * dt) - (g * Math.pow(dt, 2) * 0.5);
}
/*
* Updates component velocities according Kinematic Equations(metric system)
*
* v = vo + at
*/
void updateVelocity(double dt, double g, double d) {
xVel = xVel - dt * d;
yVel = yVel + dt * g;
}
public double getXPos() {
return xPos;
}
public double getYPos() {
return yPos;
}
}
Projectile
class, when you simulate things that are not spheres you will need to also track rotational inertia and rotational velocity. It may be useful to also track the number of forces on an object too. \$\endgroup\$