Framework
Getting Started
You need to have Java 16 or higher. (Contrary to the app, you don’t need Windows for this.)
Add this to your build.gradle:
repositories {
maven { url "https://jitpack.io" }
}
dependencies {
implementation "com.github.tom-mohr:particle-life:v0.2.0"
}
You can now import the framework in Java:
import com.particle_life.*;
Initialization
Instantiate the Physics
class by passing an Accelerator to its constructor.
Example:
Accelerator particleLife = (a, pos) -> {
double rmin = 0.3;
double dist = pos.length();
double force = dist < rmin ?
(dist / rmin - 1) : a * (1 - Math.abs(1 + rmin - 2 * dist) / (1 - rmin));
return pos.mul(force / dist);
};
Physics physics = new Physics(particleLife);
Running the Simulation
That is, compute the new velocity and position of each particle. This is done by a call to Physics.update().
while (true) {
// run the simulaton for one step
physics.update();
}
physics.shutdown(1000); // just call this at the end of your program
Asynchronous Updating
If you want to do something else while the simulation is running. (Such as rendering the particles and providing a UI.)
Loop loop = new Loop();
loop.start(dt -> {
// dt = time passed in loop (in seconds)
physics.settings.dt = dt; // set dt dynamically
physics.update();
});
while (true) { // some GUI loop
if (button.pressed()) {
// physics.update() could be running right now
loop.enqueue(() -> {
// this command should not run while
// physics.update() is running, therefore
// wrap it in a call to loop.enqueue()
physics.settings.wrap = false;
});
}
}
physics.shutdown(1000);
This example shows how to start an asynchronous loop that repeatedly calls the Physics.update() method.
When using this approach, all changes to the state of Physics
(such as changes to Physics.settings
or Physics.particles
) should be wrapped with a call to loop.enqueue()
. You can pause the execution with loop.pause = true
.
Number of Update Threads
Use Physics.preferredNumberOfThreads
to control the number of threads that are used in the Physics.update() method.
Note that this is independent of whether you use synchronous or asynchronous updating.
Particle Coordinates
Particles have a position, a velocity and a type. The coordinates of the position are in the range of [-1.0, 1.0]. Therefore you may need to map them to screen coordinates when drawing the particles.
By default, Physics.settings.wrap
is true. This allows the particles move and interact across the world’s borders. If Physics.settings.wrap
is false, they will collide with the world’s borders.
Accelerators
Accelerators decide how particles behave if they come close to each other.
More precicely, they decide how much an individual particle should accelerate and in what direction.
For that, they are only provided with very limited information:
- the factor between the type of this particle and the type of the neighbor particle
- the position of the neighbor particle, relative to this particle
Accelerators must implement the Accelerator
interface. You can do this with a lambda expression:
Accelerator myAccelerator = (a, pos) -> {
return new Vector3d(0, 0, 0); // no acceleration
};
Pass this to the constructor of the Physics
class:
Physics physics = new Physics(myAccelerator);
To change the accelerator later, simply set the accelerator
attribute of your Physics
instance:
physics.accelerator = myAccelerator;
For example, you could implement the rules of Particle Life like this:
Accelerator particleLife = (a, pos) -> {
double rmin = 0.3;
double dist = pos.length();
double force = dist < rmin ?
(dist / rmin - 1) : a * (1 - Math.abs(1 + rmin - 2 * dist) / (1 - rmin));
return pos.mul(force / dist);
};
But that is just one possible accelerator, and there are many other accelerators that show interesting behaviour.
Settings
Physics.settings
Matrix
Physics.settings.matrix
The values that are passed to the Accelerator when updating the particles’ velocities.
That is: If a particle of type i
meets a particle of type j
, then particle i
is accelerated with value matrix.get(i, j)
and particle j
is accelerated with value matrix.get(j, i)
.
Naturally, this matrix is quadratic. Its size is equivalent to the number of types. That is, if you change the size of the matrix, you need to make sure that all particles in Physics.particles
that have a type
<= Physics.settings.matrix.size()
. (Types start with 0, just like the indices in the matrix.)