The Newtonian Object-oriented Physics Engine -- NOOPE
1. Introduction
NOOPE is an open source physics engine written in Java. A program that allows you to run physical
simulations. Physical laws are not part of the core of the engine, so you can
define your own laws as Java classes and these can be used by NOOPE so long as they follow a certain
format.
NOOPE is based on the following assumptions:
- Independent and universal time
- Euclidean 3-Space
- Newton's 2nd law:
F = ma
where F is the sum of all external forces acting on a body.
- (Objects can also exert an active force on themselves. This and a couple more cheats are
provided to allow simulations that would otherwise be difficult.)
At the time of writing, NOOPE can simulate the motion of particles, and also spheres to some extent,
with elastic collisions, no spin, however.
2. How NOOPE works
This is a short overview of the internal working of NOOPE. For a
reference, refer to the easy-to-browse HTML API documentation generated by javadoc.
A. Basic object hierarchy
NOOPE is built up around two fundamental classes:
noope.entities.Entity: This is the base class for all physical objects simulated.
noope.laws.Law: This is the abstract base class for all physical laws simulated.
The toplevel class in the NOOPE hierarchy is noope.core.Physics. An instance of Physics contains a
list of entities present in the simulation (instance of noope.core.Entities) and a list laws (an
instance of noope.core.Laws). [Both Entities and Laws are subclasses of java.util.Vector - an
object-oriented array.]
An Entity has got a set of fundamental properties:
position, velocity, inertialMass.
What other properties are needed, depends, however, on which laws are being simulated. For this
reason, each Entity holds an array of instances of subclasses of noope.laws.LawPropertyRecord, one
for each Law; where the indeces of a LawPropertyRecord in the Entity and the corresponding Law in
Laws are the same.
The basic NOOPE object hierarchy
B. Running of the engine
A physical simulation is run as a sequence of time-slices. A time-slice can be simulated by calling
Physics.step(dt), which makes a single discrete timestep of length dt.
Then, the forces by all laws on all entities, the corresponding accelerations, and the resulting
new velocities and positions of all entities, are calculated.
For this, Entity provides the methods
- addForce(noope.core.Vector3D force), which adds a force to an internal accumulator and
- step(double dt), which calculates the new velocity and position.
Physics calls the act() method of every Law, which causes the Law to calculate the
resulting force on each Entity and call Entity.addForce(this_law_s_force).
Then, Physics calls the step(dt) method of every Entity to advance it in time by dt.
N.B.: Entity.step(dt) also calls the getActiveForce() method of Entity. The result returned is added
to the total force, before doing the calculations. This returns 0 for Entity,
but subclasses may override it, in order to make simulations, which would otherwise be hard to
describe in terms of physical laws, e.g. the motion of a Rocket. Note that this violates
Newton's 3rd law (action - reaction principle).
C. Construction - noope.input.BlockReader
The constructor of Physics takes an instance of noope.input.BlockReader.
A BlockReader is a generic data source that is used to construct all NOOPE objects. It is
essentially a directory structure with String keys and values, that has to be accessed sequentially.
In more detail, a BlockReader contains
- a header, this is the "title" of the block and is accessed by getHeader(),
- a parameter, accessed by getParameter(),
- a sequence of Entries that are arrays of 2 elements of Strings [(key, entry) pairs], accessed by
getNextEntry(), peekNextEntry(), hasMoreEntries(),
- a sequence of Subblocks accessed by getNextBlock(), peekNextBlock(), hasMoreBlocks(),
that have a structure identical to this BlockReader.
In addition to this, a BlockReader provides a way of identifying errors in the input data, and
giving the user the position of the error.
BlockReader.getEntryLocation() and BlockReader.getBlockLocation() ask the BlockReader to return a
noope.input.BlockReaderLocation, that contains an identification of the position at which the error
has occurred (not necessarily human-readable).
BlockReader.getContext(BlockReaderLocation loc) returns a noope.input.BlockReaderContext that should
be a human-readable representation of the error position stored in loc. A BlockReaderContext can
provide various useful data and methods for presenting the error to the user, these depend on
the particular BlockReader implementation, as do the contents of BlockReaderLocation.
The BlockReader from which Physics will be constructed has the format explained in the
noope.input.BlockReader source file.
Physics uses the BlockReader it is constructed with to dynamically load classes with their names
given in the source file. This means that noope can work with laws implemented later than it was
compiled.
D. Output
There are no specialized output objects in NOOPE, instead these are implemented as laws.
At the time of writing, the most advanced output law is noope.output.OutputProjection,
which draws to the screen (or to
animated gif files or a collection of static gif files, 1 per frame) the projection of the entities
onto a plane.
3. How to write your own law
To write your own subclass of law, you need to do three things:
- Create a constructor for your subclass of law following the agreed format.
- Write the act() method that will calculate the force on each Entity and call
Entity.addForce(force) with it.
- (Optionally) implement a way of storing data for each Entity - even though this is optional, it
will usually be required to store, e.g. the charge of each Entity in the case of the
Electrostatic Law.
The following explains each of these points in more detail:
A. Writing your law constructor
Every law has to have a constructor of the following form for the dynamic loading mechanism to be
able to load it:
public MyLaw(BlockReader br, int lawnumber, Physics phys) throws BRLoadingException
where br is the BlockReader this law should be constructed from, lawnumber is the number of this
law in the collection of laws and phys is a reference to the Physics object this law belongs to.
A minimal implementation of the law constructor that does nothing but calling the inherited
constructor would be:
public MyLaw(BlockReader br, int lawnumber, Physics phys) throws BRLoadingException
{
super(br, lawnumber, phys);
}
B. Writing the act() method
The act() method should calculate the force on each Entity and call Entity.addForce(force) with it.
The abstract class noope.laws.ParticlePairLaw that is used for laws where the effect on each Entity
is the sum of the effects of all other Entitys on that Entity has the following act() method:
public void act()
// for all i != j in e: actOnPair(i, j);
{
Entities e = physics.getEntities();
if (e.size() <= 1) return; // nothing to do
Entity ent, other;
int i, j;
for (i = 0; i < e.size(); i++)
{
ent = (Entity)e.elementAt(i);
for (j = i+1; j < e.size(); j++)
{
other = (Entity)e.elementAt(j);
actOnPair(ent, other);
}
}
}
/** Applies the law to the two parameters. This should be overridden in subclasses. */
abstract public void actOnPair(Entity ent1, Entity ent2);
In an output law, it outputs some sort of data, instead of exerting forces on the Entitys.
C. Storing data for each Entity
If each Entity has a property that it should store for a particular law, it can be held in an
instance of a subclass of LawPropertyRecord.
For this, you need to:
- Create a subclass of LawPropertyRecord that stores that data with its public constructor taking
the BlockReader to get data from and the Entity to construct the LawPropertyRecord for.
- Override the Law's constructNewPropertyRecord(BlockReader, Entity) method that passes on its
arguments to your LawPropertyRecord's constructor and returns the object constructed.
For an example, I recommend you have a look at the source for noope.laws.Gravity and
noope.laws.GravityPropertyRecord.