The game physics demo application is a JavaFX application that runs a simple simulation of a ball bouncing around in a box.
The application responds to key events, and allows the player to move the inner box around using the arrow keys. Whenever the ball strikes the inner box or the outer walls it will bounce off in a realistic way.
The main class in this application that implements the simulation of the ball bouncing around is the simulation class.
public class Simulation { private Box outer; private Ball ball; private Box inner; private Lock lock; public Simulation(int width,int height,int dX,int dY) { outer = new Box(0,0,width,height,false); ball = new Ball(width/2,height/2,dX,dY); inner = new Box(width - 60,height - 40, 40, 20,true); lock = new ReentrantLock(); } // Evolve the simulation through one round, advancing simulation // time by time units. public void evolve(double time) {} // Move the inner box by the indicated amount public void moveInner(int deltaX,int deltaY) {} // Set up the shapes to be displayed in the GUI public List<Shape> setUpShapes() { ArrayList<Shape> newShapes = new ArrayList<Shape>(); newShapes.add(outer.getShape()); newShapes.add(inner.getShape()); newShapes.add(ball.getShape()); return newShapes; } // Update the GUI shapes by moving things to their correct positions public void updateShapes() { inner.updateShape(); ball.updateShape(); } }
The Simulation container is first and foremost a container for a set of simulation objects: the outer box (which forms the walls of the simulation), the inner box (that the user can move around by pressing the arrow keys), and the ball (which will move around in the simulated world).
The two boxes and the ball are implemented in the following two classes.
public class Box { private ArrayList<LineSegment> walls; private Rectangle r; public int x; public int y; public int width; public int height; // Set outward to true if you want a box with outward pointed normals public Box(int x,int y,int width,int height,boolean outward) {} public Ray bounceRay(Ray in,double time) {} public void move(int deltaX,int deltaY) {} public boolean contains(Point p) {} public Shape getShape() { r = new Rectangle(x, y, width, height); r.setFill(Color.WHITE); r.setStroke(Color.BLACK); return r; } public void updateShape() { r.setX(x); r.setY(y); } } public class Ball { private Ray r; private Circle c; public Ball(int startX,int startY,int dX,int dY) {} public Ray getRay() { return r; } public void setRay(Ray r) { this.r = r; } public void move(double time) { r = new Ray(r.endPoint(time),r.v,r.speed); } public Shape getShape() { c = new Circle(r.origin.x,r.origin.y,4); c.setFill(Color.RED); return c; } public void updateShape() { c.setCenterX(r.origin.x); c.setCenterY(r.origin.y); } }
Both of these classes contain two representations. The first of these is an internal representation that will be used by the physics simulation code to run the simulation. In the case of the Box that internal representation is a list of LineSegment objects. In the case of the Ball, that internal representation is a Ray object. The second representation these classes contain is an external representation that the GUI will use to represent the object on the screen. These representations are simply JavaFX shape objects. In the case of the Box that shape is a Rectangle. In the case of the Ball that shape is a Circle.
Both classes feature a getShape()
method that constructs the external representation from the data in the internal representation. Both classes also contain an updateShape()
method that reads data from the internal representation to update the external representation. This method will get called on each round of the simulation.
To implement the physics of motion for the simulation that program will use a set of four classes that represent key concepts in the simulation.
The first of these is a simple Point class:
public class Point { public double x; public double y; public Point(double x,double y) { this.x = x; this.y = y; } }
The second of these is a Vector class, which contains methods to implement common vector operations:
public class Vector { public double dX; public double dY; public Vector(double dX,double dY) { this.dX = dX; this.dY = dY; } public void normalize() { double length = this.length(); dX /= length; dY /= length; } public double length() { return Math.sqrt(dotProduct(this,this)); } static public double crossProduct(Vector one,Vector two) { return one.dX*two.dY-two.dX*one.dY; } static public double dotProduct(Vector one,Vector two) { return one.dX*two.dX + one.dY*two.dY; } }
The third physics class is a Ray class. A Ray is a combination of an origin point and a direction vector. The direction vector is a combination of two elements: a unit Vector v that specifies the direction of the Ray and a speed value that gives the length of the direction vector.
public class Ray { public Point origin; public Vector v; public double speed; public Ray(Point origin,Vector v,double speed) { this.origin = origin; this.v = v; this.v.normalize(); this.speed = speed; } // Construct and return a line segment representing the path // the object would take over the given span of time. public LineSegment toSegment(double time) { return new LineSegment(origin,this.endPoint(time)); } // Compute the location after the given time span. public Point endPoint(double time) { double destX = origin.x + v.dX*time*speed; double destY = origin.y + v.dY*time*speed; return new Point(destX,destY); } // Compute and return the time at which this ray will // be closest to the given point. public double getTime(Point p) { if(Math.abs(v.dX) > Math.abs(v.dY)) return (p.x - origin.x)/(v.dX*speed); return (p.y - origin.y)/(v.dY*speed); } }
The final physics class is the LineSegment, which represents a simple line segment with two endpoints. The LineSegment class contains useful computational methods to answer key questions involving line segments, such as whether or not two line segments intersect, and what happens when a Ray collides with a line segment and bounces off of it.
/* The LineSegment class represent a directed line segment with an attached * normal. The direction of the line segment is a vector running from point * a to point b. The normal is a unit vector rotated 90 degrees clockwise * from the direction vector in pixel coordinate space. */ public class LineSegment { public Point a; public Point b; public LineSegment(Point a,Point b) { this.a = a; this.b = b; } public void move(int deltaX,int deltaY) { a.x += deltaX; a.y += deltaY; b.x += deltaX; b.y += deltaY; } public Vector toVector() { return new Vector(b.x - a.x,b.y - a.y); } // Return the point at which these two line segments intersect, // or null if they do not. public Point intersection(LineSegment other) { // The technique used to compute the intersection is described at // http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect LineSegment connector = new LineSegment(this.a,other.a); Vector me = this.toVector(); Vector them = other.toVector(); Vector us = connector.toVector(); double common = Vector.crossProduct(me,them); // If the cross product is positive, the other line segment is coming // from our right side, and we should not allow it to intersect us. if(common >= 0.0) return null; double t = Vector.crossProduct(us,them)/common; double u = Vector.crossProduct(us,me)/common; if(0 <= t && t <= 1 && 0 <= u && u <= 1) { return new Point(a.x + me.dX*t,a.y + me.dY*t); } return null; } // Return a Ray that represents the reflection of the // other line segment. For this to work correctly, // the other line segment's vector needs to be // oriented in the opposite direction of our normal. // The returned Ray is positioned at the point of // intersection and is oriented in the reflected direction. public Ray reflect(LineSegment other,double speed) { // Compute the normal to this segment Vector N = new Vector(a.y-b.y,b.x-a.x); N.normalize(); // Compute the relected direction Vector V = other.toVector(); double dot = Vector.dotProduct(N,V); if(dot > 0) dot = -dot; Vector R = new Vector(V.dX - 2*dot*N.dX,V.dY - 2*dot*N.dY); // Construct and return the result Ray Point origin = this.intersection(other); return new Ray(origin,R,speed); } }
The most important job these four classes have to perform is simulating the motion of the ball in the imaginary space. On each round of the simulation we have to start by computing a trajectory for the ball. Given the Ray that describes the ball's motion we can ask where the origin point of the Ray would move after a short span of time deltaT. The Ray class's toSegment()
method does this computation and returns the trajectory in the form of a LineSegment with one endpoint where the object started and another endpoint where the object ended.
The next step is to determine whether or not that trajectory would intersect at any point with one of the line segments that makes up either the outer box or the inner box. The LineSegment class has an intersection()
method that can answer this question. If the ball's trajectory would intersect the LineSegment, intersection()
will return the exact Point where the intersection would take place. If there is a collision, the LineSegment's reflect()
method will compute the new Ray that results from the collistion.
Now that we have a collection of classes in place that can implement the simulation for us, all we need is a thread that actually runs the simulation.
That thread gets set up in the JavaFX application's start method. Here is the code that sets up and starts the thread:
new Thread(() -> { while (true) { sim.evolve(1.0); Platform.runLater(()->sim.updateShapes()); try { Thread.sleep(50); } catch (InterruptedException ex) {} } }).start();
The physics demo application also responds to key events. The user can move the inner box as the simulation is running by pressing the arrow keys.
The code that sets up key handling also appears in the application's start method as part of the code that sets up both the simulation and the pane used to display the simulation:
GamePane root = new GamePane(); Simulation sim = new Simulation(300, 250, 2, 2); root.setShapes(sim.setUpShapes()); Scene scene = new Scene(root, 300, 250); root.setOnKeyPressed(e -> { switch (e.getCode()) { case DOWN: sim.moveInner(0, 3); break; case UP: sim.moveInner(0, -3); break; case LEFT: sim.moveInner(-3, 0); break; case RIGHT: sim.moveInner(3, 0); break; } }); root.requestFocus();