Commit a360fd7c authored by Jens Ehlers's avatar Jens Ehlers
Browse files

JavaFX animations and Dyn4j

parent de08481a
Pipeline #5295 passed with stage
in 38 seconds
......@@ -78,7 +78,7 @@ int max = numbers.stream().reduce(Integer.MIN_VALUE, Math::max);</code></pre>
<pre><code class="language-java line-numbers">int max = numbers.stream().parallel().reduce(Integer.MIN_VALUE, Math::max);</code></pre>
<p>Die grundsätzliche Erwartung ist, dass durch die Nutzung mehrerer parallel arbeitender Threads der parallelisierte Stream schneller fertig wird als der sequentielle Stream. Das lässt sich mit einem ausreichend großen Berechnungsaufwand für den Stream und entsprechender Multicore-Hardware auch demonstrieren (<a href="/blob/master/concurrency/src/main/java/streams/ParallelStreamProcessingTime.java" class="repo-link">siehe hier</a>).</p>
<p>Die grundsätzliche Erwartung ist, dass durch die Nutzung mehrerer parallel arbeitender Threads der parallelisierte Stream schneller fertig wird als der sequentielle Stream. Das lässt sich mit einem ausreichend großen Berechnungsaufwand für den Stream und entsprechender Multicore-Hardware auch demonstrieren (<a href="/concurrency/src/main/java/streams/ParallelStreamProcessingTime.java" class="repo-link">siehe hier</a>).</p>
<p>Grundsätzlich ist die Annahme, dass ein sequentieller und ein paralleler Stream stets zum gleichen Ergebnis führen. Es gibt aber Ausnahmefälle, die entsprechend dokumentiert sind, z.B. die häufig genutzte Methode <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html#forEach(java.util.function.Consumer)"><code>forEach</code></a>, mit der über die Elemente eines Streams iteriert werden kann. Die Methode <code>forEach</code> ist wie folgt dokumentiert: <i>"The behavior of this operation is explicitly nondeterministic. For parallel stream pipelines, this operation does not guarantee to respect the encounter order of the stream, as doing so would sacrifice the benefit of parallelism. For any given element, the action may be performed at whatever time and in whatever thread the library chooses. If the action accesses shared state, it is responsible for providing the required synchronization."</i> Die folgende Methode <code>log</code> gibt die Elemente eines parallelen Streams daher nicht unbedingt in der richtigen Reihenfolge aus.</p>
......@@ -102,7 +102,7 @@ Das Fork-Join-Framework agiert nach dem Prinzip des Divide & Conquer-Algorithmus
<p>Beim ersten Fork wird der Indexbereich in die beiden Teilbereiche 0..31 und 32..63 aufgeteilt. Damit sind 2 Teilaufgaben entstanden: (a) Finden der größten Zahl im Indexbereich 0..31 sowie (b) finden der größten Zahl im Indexbereich 32..63.
Diese Teilaufgaben können in der Fork-Phase rekursiv in noch kleinere Teilaufgaben zerlegt werden: Finden der größten Zahl in den Indexbereichen 0..15, 16..31, 32..47 und 48..63. Entscheidend ist, dass jede Teilaufgabe unabhängig von den anderen in einem nebenläufigen Thread gelöst werden kann. Die Methode <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html#reduce(T,java.util.function.BinaryOperator)"><code>reduce</code></a> (s. oben) führt dazu die Elemente eines Streams paarweise zusammen. In diesem Fall wird der Methode <code>reduce</code> dazu die Methode <code>Math.max</code> als <code>BinaryOperator</code> übergeben, um jeweils die größere von zwei Zahlen zurückzugeben. Angenommen es liegt eine Quadcore-Hardware zugrunde, würden in der Fork-Phase 4 Threads genutzt, die unabhängig voneinander die Teilaufgaben bearbeiten. In der anschließenden Join-Phase werden die Ergebnisse der Teilaufgaben aggregiert, indem sie paarweise als Argumente dem <code>BinaryOperator</code> (hier <code>Math:max</code>) übergeben werden, der in der Methode <code>reduce</code> ausgeführt wird.</p>
<p>Wenn N die Anzahl der verfügbaren Prozessorkerne ist, wird das Ausgangsproblem tatsächlich nicht nur in N Teilaufgaben zerlegt sondern in etwa 4·N. Die Anzahl der erzeugten Teilaufgaben entspricht der Zweierpotenz, die größer als 4·(N-1) ist. Auf einer Quadcore-Hardware werden also 16 Teilaufgaben erzeugt, die nacheinander von 4 Threads bearbeitet werden. Dieses Verhalten lässt sich auch beobachten (<a href="/blob/master/concurrency/src/main/java/streams/ParallelStreamCountForks.java" class="repo-link">siehe hier</a>). Der per Default genutzte Thread-Pool namens <i>ForkJoinPool.common</i> lässt sich bei Bedarf verändern oder austauschen. Über die folgende Anweisung wird z.B. die Anzahl der Threads in diesem Thread-Pool auf 8 erhöht.</p>
<p>Wenn N die Anzahl der verfügbaren Prozessorkerne ist, wird das Ausgangsproblem tatsächlich nicht nur in N Teilaufgaben zerlegt sondern in etwa 4·N. Die Anzahl der erzeugten Teilaufgaben entspricht der Zweierpotenz, die größer als 4·(N-1) ist. Auf einer Quadcore-Hardware werden also 16 Teilaufgaben erzeugt, die nacheinander von 4 Threads bearbeitet werden. Dieses Verhalten lässt sich auch beobachten (<a href="/concurrency/src/main/java/streams/ParallelStreamCountForks.java" class="repo-link">siehe hier</a>). Der per Default genutzte Thread-Pool namens <i>ForkJoinPool.common</i> lässt sich bei Bedarf verändern oder austauschen. Über die folgende Anweisung wird z.B. die Anzahl der Threads in diesem Thread-Pool auf 8 erhöht.</p>
<pre><code class="language-java line-numbers">System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "8");</code></pre>
......
......@@ -40,13 +40,19 @@
var filename = $(this).attr('id');
$(this).load(filename + '.html', function() { // try this with $.get
Prism.highlightAll();
$(".navigate").on("click", function() { displayUnit($(this).attr("href")); });
$(".navigate").on("click", function() { displayUnit($(this).attr("href")); });
// add repo links
$(".repo-link").each(function() {
$("#" + filename + " .repo-link").each(function() {
var href = 'https://git.mylab.th-luebeck.de/oncampus/patterns-and-frameworks/blob/master' + $(this).attr('href');
$(this).attr('href', href);
});
// open external links in new tab
$("#" + filename + " a").each(function() {
var href = $(this).attr('href');
if (href.startsWith("http")) $(this).attr('target', '_blank');
});
});
var id = $(this).parent().attr('id').replace('content-', '');
......
<h4>Anforderungen an das Softwareprojekt</h4>
<p>Es soll eine Anwendung erstellt werden, die mind. in Client- und Server-Komponente unterteilt ist. Das Projekt kann im Team von 2-4 Studierenden bearbeitet werden.</p>
<p>Im Fokus des Moduls steht das Kennenlernen und praktische Erproben von bewährten Entwurfsmustern und Frameworks, die auch in professionellen Sofwareprojekten (im Java-Umfeld) eingesetzt werden. Es soll als Prüfungsleistung daher eine Anwendung konstruiert werden, die mind. in Client- und Server-Komponente unterteilt ist. Das Projekt kann im Team von 2-4 Studierenden bearbeitet werden.</p>
<ul>
<li><b>Funktionale Anforderungen</b>: Es soll ein einfaches Multiplayer-Spiel für mind. 2 Spieler realisiert werden. Als Projektidee wird die Implementierung eines bekannten Arcade-Klassikers vorgeschlagen, z.B. <a href="https://de.wikipedia.org/wiki/Tetris">Tetris</a>, <a href="https://de.wikipedia.org/wiki/Tank_(Computerspiel)">Tank</a>, <a href="https://de.wikipedia.org/wiki/Snake_(Computerspiel)">Snake</a>, <a href="https://de.wikipedia.org/wiki/Pac-Man">Pac-Man</a>, <a href="https://de.wikipedia.org/wiki/Dig_Dug">Dig Dug</a> oder <a href="https://de.wikipedia.org/wiki/Blobby_Volley">Blobby Volley</a>. Es kann auch ein Brettspiel oder ein Quizspiel umgesetzt werden. Die Spielregeln des Spiels können gerne individuell angepasst werden. Bezüglich der angestrebten Funktionalität der Anwendung wird dementsprechend Freiraum für kreative Ideen gelassen, die mit dem jeweiligen Modulbetreuer abzustimmen sind. Unabhängig von der gewählten Projektidee sollten folgende Anforderungen berücksichtigt werden:
<li><b>Funktionale Anforderungen</b>: Es soll ein einfaches Multiplayer-Spiel für mind. 2 Spieler realisiert werden. Als Projektidee wird die Implementierung eines bekannten Arcade-Klassikers vorgeschlagen, z.B. <a href="https://de.wikipedia.org/wiki/Tetris">Tetris</a>, <a href="https://de.wikipedia.org/wiki/Tank_(Computerspiel)">Tank</a>, <a href="https://de.wikipedia.org/wiki/Snake_(Computerspiel)">Snake</a>, <a href="https://de.wikipedia.org/wiki/Pac-Man">Pac-Man</a>, <a href="https://de.wikipedia.org/wiki/Dig_Dug">Dig Dug</a> oder <a href="https://de.wikipedia.org/wiki/Blobby_Volley">Blobby Volley</a>. Es kann auch ein Brettspiel oder ein Quizspiel umgesetzt werden. Die Spielregeln des Spiels können gerne individuell angepasst werden. Bezüglich der angestrebten Funktionalität der Anwendung wird dementsprechend Freiraum für kreative Ideen gelassen, die mit dem jeweiligen Modulbetreuer abzustimmen sind. Tatsächlich ist Java keine besonders übliche Sprache für die Implementierung eines Spiels, aber gerade dadurch entsteht Raum für Kreativität im Entwurf. Es darf mit Freude über verschiedene Lösungsansätze diskutiert werden. Zusätzlich kann der Humor, der i.d.R. mit der Entwicklung eines Spiels verbunden ist, die Motivation steigern. Unabhängig von der gewählten Projektidee sollten folgende Anforderungen berücksichtigt werden:
<ul>
<li>Ein Anwender soll sich registrieren, einloggen und ausloggen können.</li>
<li>Für jeden Anwender wird eine Historie seiner gespielten Spiele erfasst und mittels einfacher Auswertungen dargestellt (gewonnene und verlorene Spiele, mittlere Punktzahl, usw.).</li>
......
......@@ -352,4 +352,140 @@ public class PostLoginTask extends Task&lt;User> {
<li>Die Klasse <code>Task&lt;V></code> ist eine generische Klasse, für die ein bestimmter Rückgabetyp <code>V</code> für die Methode <code>call</code> spezifiziert werden kann. Für die Klasse <code>PostLoginTask</code> ist dies im vorliegenden Beispiel die Datenstruktur-Klasse <code>User</code> (vgl. Zeilen 6 und 14).</li>
<li>In Zeile 19 des Task wird ein blockierender HTTP-Request gesendet, der ggf. eine Exception wirft, wenn z.B. der Server nicht zu erreichen ist. Insbesondere wegen dieser Zeile wird überhaupt ein asynchroner Task angelegt. In den Zeilen 20-23 wird die HTTP-Response verarbeitet.</li>
<li>Zum Senden des HTTP-Request wird hier exemplarisch die Bibliothek <a href="http://unirest.io/java.html">Unirest</a> verwendet. Diese würde alternativ auch das Senden eines nicht-blockierenden HTTP-Request ermöglichen, wenn am Ende von Zeile 19 anstatt der Methode <code>asString</code> die Methode <code>asStringAsync</code> aufgerufen wird. Die Rückgabe wäre dann vom Typ <code>Future&lt;HttpResponse&lt;String>></code>. Dadurch würde sich das Auslagern des blockierenden Aufrufs aus dem eigenen Code in den internen Code der Bibliothek verlagern. Ein Task wäre nicht mehr unbedingt erforderlich, falls die Vorbereitung des HTTP-Request und die Verarbeitung der HTTP-Response als nicht sonderlich aufwändig eingestuft werden und es tolerierbar ist, diese im <i>JavaFX Application Thread</i> auszuführen.</li>
</ul>
\ No newline at end of file
</ul>
<h4>Animationen in JavaFX</h4>
<p>In einem Spiel bewegen sich häufig unterschiedliche Objekte über den Bildschirm, die miteinander im Rahmen der Spielregeln interagieren. In JavaFX wird zur Animation von Objekten die abstrakte Klasse <code><a href="https://openjfx.io/javadoc/11/javafx.graphics/javafx/animation/AnimationTimer.html">AnimationTimer</a></code> bereitgestellt. Mit dieser Klasse kann ein Timer erstellt werden, der in jedem Frame aufgerufen wird, während er aktiv ist. Die Frame-Rate ist variabel und hängt von der zugrundeliegenden Ausführungsumgebung und Hardware ab. Grundsätzlich ist es zu empfehlen, die Frame-Rate, in der das UI aktualisiert wird, und die Spielphysik voneinander zu entkoppeln (siehe Diskussion <a href="https://gamedev.stackexchange.com/questions/97933/framerate-is-affecting-speed-of-object">hier</a>). Zur Verwendung eines konkreten <code>AnimationTimer</code> muss die Methode <code>handle</code> sinnvoll überschrieben werden. Die Methoden <code>start</code> und <code>stop</code> erlauben das Starten bzw. Stoppen des Timers. Das folgende Code-Beispiel erzeugt eine Anwendung, in der ein animierter Ball mit fixer Geschwindigkeit an den Kanten des Anwendungsfensters abprallt (siehe <a href="/ui/javafx-animation/src/main/java/animation/SingleBouncingBall.java" class="repo-link">SingleBouncingBall.java</a>).</p>
<ul class="nav nav-tabs" id="javafx3-tabs" role="tablist">
<li class="nav-item"><a href="#javafx3-tabs-bouncingball" class="nav-link active" data-toggle="tab" role="tab">SingleBouncingBallApp</a></li>
<li class="nav-item"><a href="#javafx3-tabs-ball" class="nav-link" data-toggle="tab" role="tab">Ball</a></li>
</ul>
<div class="tab-content" id="javafx3-tabs-content">
<div class="tab-pane show active" id="javafx3-tabs-bouncingball" role="tabpanel">
<pre><code class="language-java line-numbers">import javafx.application.Application;
import javafx.animation.AnimationTimer;
// ...
public class SingleBouncingBall extends Application {
@Override
public void start(Stage stage) {
final Pane ballContainer = new Pane();
final Scene scene = new Scene(ballContainer, 600, 400);
final Ball ball = new Ball(300, 200, 25, Color.DARKGRAY, 300, Math.toRadians(45));
ballContainer.getChildren().add(ball);
stage.setScene(scene);
stage.show();
final LongProperty lastUpdateTime = new SimpleLongProperty(0);
final AnimationTimer timer = new AnimationTimer() {
@Override
public void handle(long now) {
if (lastUpdateTime.get() > 0) {
// change the ball position
double elapsedSeconds = (now - lastUpdateTime.get()) / 1_000_000_000.;
ball.setCenterX(ball.getCenterX() + elapsedSeconds * ball.velocity * Math.cos(ball.angle));
ball.setCenterY(ball.getCenterY() + elapsedSeconds * ball.velocity * Math.sin(ball.angle));
// bounce against a vertical wall
if (ball.getCenterX() &lt;= ball.getRadius()
|| ball.getCenterX() >= (ballContainer.getWidth() - ball.getRadius())) {
ball.angle = Math.PI - ball.angle;
}
// bounce against a horizontal wall
if (ball.getCenterY() &lt;= ball.getRadius()
|| ball.getCenterY() >= (ballContainer.getHeight() - ball.getRadius())) {{
ball.angle = 2 * Math.PI - ball.angle;
}
}
lastUpdateTime.set(now);
}
};
timer.start();
}
public static void main(String[] args) { launch(); }
}</code></pre>
</div>
<div class="tab-pane" id="javafx3-tabs-ball" role="tabpanel">
<pre><code class="language-java line-numbers">import javafx.scene.shape.Circle;
class Ball extends Circle {
double velocity;
double angle;
Ball(double centerX, double centerY, double radius, Paint fill, double velocity, double angle) {
super(centerX, centerY, radius, fill);
this.velocity = velocity;
this.angle = angle;
}
}</code></pre>
</div>
</div>
<p>Die Klasse <code>Ball</code> erweitert die Klasse <a href="https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/shape/Circle.html"><code>javafx.scene.shape.Circle</code></a>. Ein <code>Circle</code> ist ein spezieller <a href="https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/shape/Shape.html"><code>Shape</code></a>, der als <code>Node</code> dem <i>Scene Graph</i> hinzugefügt werden kann. Weitere Shapes sind <code>Rectangle</code>, <code>Polygon</code>, <code>Text</code>, <code>SVGPath</code>, uvm.
Alternativ zur Verwendung von Shapes können animierte Objekte in JavaFX auch auf einer <a href="https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/canvas/Canvas.html">Canvas</a> über deren <a href="https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/canvas/GraphicsContext.html"><code>GraphicsContext</code></a> gezeichnet werden. Die beiden folgenden Code-Beispiele visualisieren jeweils den Text "Hello World!" und einen roten Kreis – im ersten Code-Beispiel werden dazu Shapes angelegt, im zweiten Code-Beispiel wird auf eine Canvas gezeichnet.</p>
<pre><code class="language-java line-numbers">// display text and circle as shapes
Pane shapesContainer = new Pane();
Shape text = new Text(20, 20, "Hello World!"); // x = 20, y = 20
shapesContainer.getChildren().add(text);
Shape circle = new Circle(100, 100, 30, Color.RED); // centerX = 100, centerY = 100, radius = 30
shapesContainer.getChildren().add(circle);</code></pre>
<pre><code class="language-java line-numbers">// draw text and circle on canvas
Canvas canvas = new Canvas(300, 200); // canvas is 300 x 200 pixels
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.fillText("Hello World!", 20, 20); // x = 20, y = 20
gc.setFill(Color.RED);
gc.fillOval(100, 100, 60, 60); // x = 100, y = 100, width = 60, height = 60 => radius = 30</code></pre>
<p>Die alternativen Implementierungen in den Klassen <a href="/ui/javafx-animation/src/main/java/animation/BouncingBallsShapes.java" class="repo-link">BouncingBallsShapes.java</a> und <a href="/ui/javafx-animation/src/main/java/animation/BouncingBallsCanvas.java" class="repo-link">BouncingBallsCanvas.java</a> lassen erkennen, dass im Vergleich von Shapes- und Canvas-Animationen sich keine signifikanten Unterschiede hinsichtlich der Frame-Rate und des Hauptspeicherbedarfs ergeben, aber die Shapes-Animationen eine etwas höhere CPU-Auslastung verursachen. Das folgende Video demonstriert diese Beobachtungen.</p>
<video controls><source src="media/Bouncing_Balls.mp4" type="video/mp4"></video>
<label>Animationen in JavaFX mittels Shapes und Canvas</label>
<h4>Simulation der Spielphysik</h4>
<p>Die Spielphysik kann – wie in den obigen Beispielen – durch stark vereinfachte Annahmen bei der <a href="https://de.wikipedia.org/wiki/Sto%C3%9F_(Physik)">Kollision von Objekten</a> eigenständig implementiert werden. Wenn in der Spielphysik allerdings auch spezielle Effekte und Kräfte wie Reibung, Dichte oder Rotationsgeschwindigkeit berücksichtigt werden sollen, bietet sich die Verwendung einer Physik-Engine wie z.B. <a href="http://www.dyn4j.org/">dyn4j</a> an. Die Physik-Engine ist unabhängig von JavaFX und kann gleichermaßen mit jedem anderen UI-Framework verwendet werden.</p>
<p>In dyn4j wird eine 2D-Welt simuliert. Die Welt (Klasse <a href="http://docs.dyn4j.org/v3.3.0/org/dyn4j/dynamics/World.html"><code>World</code></a>) kann diverse Körper (Klasse <a href="http://docs.dyn4j.org/v3.3.0/org/dyn4j/dynamics/Body.html"><code>Body</code></a>) enthalten, die ihrerseits aus mehreren miteinander verbundenen Körperteilen (Klasse <code>BodyFixture</code>) bestehen (s. <a href="http://www.dyn4j.org/documentation/getting-started/">Getting Started zu dyn4j</a>). Auf die Körper können Kräfte und Impulse wirken. Das folgende Code-Beispiel zeigt wie einer dyn4j-Welt ein rechteckiger und ein runder Körper hinzugefügt werden.</p>
<pre><code class="language-java line-numbers">World world = new World();
Body rect = new Body();
rect.addFixture(Geometry.createRectangle(width, height)); // 1st body fixture is a rectangle
rect.setMass(MassType.INFINITY);
body.translate(0, 0); // sets the rectangle's center (x, y)
world.addBody(rectangle);
Body circle = new Body();
circle.addFixture(Geometry.createCircle(radius), density, friction, restitution);
circle.setMass(MassType.NORMAL);
circle.translate(0, 10); // sets the circle's center (x, y)
world.addBody(circle);
world.setGravity(World.EARTH_GRAVITY); // the circle will fall down, the rectangle is fixed due to an infinite mass
circle.applyImpulse(new Vector2(x, y)); // applies an impulse on the circle in the direction of (x, y)</code></pre>
<p>Mittels der Methode <code>update</code> auf der Klasse <code>World</code> kann die Simulation der Welt in flexiblen zeitlichen Intervallen fortgeführt werden.</p>
<pre><code class="language-java line-numbers">ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
scheduler.scheduleAtFixedRate(() -> {
world.update(interval); // simulation of the world model is updated here
}, 0, interval, TimeUnit.MILLISECONDS);</code></pre>
<p>Das folgende Video zeigt eine Beispielanwendung, in der dyn4j zur Kollisionserkennung der animierten Objekte eingesetzt wird. Der Ball kann über die Pfeiltasten gesteuert werden. Durch Mausklicks können herabfallende Rechtecke erzeugt werden. Der vollständige Code der Beispielanwendung findet sich in der Klasse <a href="/ui/javafx-animation/src/main/java/animation/BouncingBallDyn4j.java" class="repo-link">BouncingBallDyn4j</a> im Verzeichnis <a href="/ui/javafx-animation" class="repo-link">/ui/javafx-animation</a> des Modul-Repository.</p>
<video controls><source src="media/Bouncing_Ball_dyn4j.mp4" type="video/mp4"></video>
<label>Spielphysik in JavaFX mittels dyn4j</label>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.oncampus.patterns</groupId>
<artifactId>javafx-animation</artifactId>
<version>1</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>11.0.1</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>11.0.1</version>
</dependency>
<dependency>
<groupId>org.dyn4j</groupId>
<artifactId>dyn4j</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
</project>
// adapted from https://gist.github.com/james-d/8327842
package animation;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Random;
import static java.lang.Math.PI;
import static java.lang.Math.cos;
import static java.lang.Math.sin;
import static java.lang.Math.sqrt;
import static javafx.scene.paint.Color.*;
public abstract class AbstractBouncingBalls extends Application {
protected List<Ball> balls = new ArrayList<>();
private static final int NUM_BALLS = 400;
private static final double MIN_RADIUS = 5;
private static final double MAX_RADIUS = 15;
private static final double MIN_SPEED = 50;
private static final double MAX_SPEED = 250;
private static final Color[] COLORS = new Color[]{RED, YELLOW, GREEN, BROWN, BLUE, PINK, BLACK};
private final FrameStats frameStats = new FrameStats();
protected final Pane ballContainer = createBallContainer();
protected abstract Pane createBallContainer();
protected abstract void updateWorld(long elapsedTime);
protected abstract String getTitle();
@Override
public void start(Stage primaryStage) {
constrainBallsOnResize(ballContainer);
ballContainer.addEventHandler(MouseEvent.MOUSE_CLICKED, (event) -> {
if (event.getClickCount() == 2) { // double-click event
balls.clear();
createBalls(NUM_BALLS, MIN_RADIUS, MAX_RADIUS, MIN_SPEED, MAX_SPEED, ballContainer.getWidth() / 2, ballContainer.getHeight() / 2);
}
});
createBalls(NUM_BALLS, MIN_RADIUS, MAX_RADIUS, MIN_SPEED, MAX_SPEED, 400, 300);
final BorderPane root = new BorderPane();
final Label stats = new Label();
stats.textProperty().bind(frameStats.textProperty());
root.setCenter(ballContainer);
root.setBottom(stats);
final Scene scene = new Scene(root, 800, 600);
primaryStage.setScene(scene);
primaryStage.setTitle(getTitle());
primaryStage.show();
startAnimation(ballContainer);
}
private void startAnimation(final Pane ballContainer) {
final LongProperty lastUpdateTime = new SimpleLongProperty(0);
final AnimationTimer timer = new AnimationTimer() {
@Override
public void handle(long timestamp) {
if (lastUpdateTime.get() > 0) {
long elapsedTime = timestamp - lastUpdateTime.get();
checkCollisions(ballContainer.getWidth(), ballContainer.getHeight());
updateWorld(elapsedTime);
frameStats.addFrame(elapsedTime);
}
lastUpdateTime.set(timestamp);
}
};
timer.start();
}
private void checkCollisions(double maxX, double maxY) {
for (ListIterator<Ball> slowIt = balls.listIterator(); slowIt.hasNext(); ) {
Ball b1 = slowIt.next();
// check wall collisions
double xVel = b1.xVelocity.get();
double yVel = b1.yVelocity.get();
if ((b1.centerX - b1.radius <= 0 && xVel < 0) || (b1.centerX + b1.radius >= maxX && xVel > 0)) {
b1.xVelocity.set(-xVel);
}
if ((b1.centerY - b1.radius <= 0 && yVel < 0) || (b1.centerY + b1.radius >= maxY && yVel > 0)) {
b1.yVelocity.set(-yVel);
}
for (ListIterator<Ball> fastIt = balls.listIterator(slowIt.nextIndex()); fastIt.hasNext(); ) {
Ball b2 = fastIt.next();
// both colliding() and bounce() need deltaX and deltaY, so compute them once here
final double deltaX = b2.centerX - b1.centerX;
final double deltaY = b2.centerY - b1.centerY;
if (colliding(b1, b2, deltaX, deltaY)) {
bounce(b1, b2, deltaX, deltaY);
}
}
}
}
public boolean colliding(final Ball b1, final Ball b2, final double deltaX, final double deltaY) {
// square of distance between balls is s^2 = (x2-x1)^2 + (y2-y1)^2
// balls are "overlapping" if s^2 < (r1 + r2)^2
// also check that distance is decreasing, i.e. d/dt(s^2) < 0: 2(x2-x1)(x2'-x1') + 2(y2-y1)(y2'-y1') < 0
final double radiusSum = b1.radius + b2.radius;
if (deltaX * deltaX + deltaY * deltaY <= radiusSum * radiusSum) {
if (deltaX * (b2.xVelocity.get() - b1.xVelocity.get()) + deltaY * (b2.yVelocity.get() - b1.yVelocity.get()) < 0) {
return true;
}
}
return false;
}
private void bounce(final Ball b1, final Ball b2, final double deltaX, final double deltaY) {
final double distance = sqrt(deltaX * deltaX + deltaY * deltaY);
final double unitContactX = deltaX / distance;
final double unitContactY = deltaY / distance;
final double xVelocity1 = b1.xVelocity.get();
final double yVelocity1 = b1.yVelocity.get();
final double xVelocity2 = b2.xVelocity.get();
final double yVelocity2 = b2.yVelocity.get();
final double u1 = xVelocity1 * unitContactX + yVelocity1 * unitContactY; // velocity of ball 1 parallel to contact vector
final double u2 = xVelocity2 * unitContactX + yVelocity2 * unitContactY; // same for ball 2
final double massSum = b1.mass + b2.mass;
final double massDiff = b1.mass - b2.mass;
// These equations are derived for one-dimensional collision by solving equations for conservation of momentum and conservation of energy.
final double v1 = (2 * b2.mass * u2 + u1 * massDiff) / massSum;
final double v2 = (2 * b1.mass * u1 - u2 * massDiff) / massSum;
final double u1PerpX = xVelocity1 - u1 * unitContactX; // Components of ball 1 velocity in direction perpendicular to contact vector. This doesn't change with collision.
final double u1PerpY = yVelocity1 - u1 * unitContactY;
final double u2PerpX = xVelocity2 - u2 * unitContactX; // same for ball 2
final double u2PerpY = yVelocity2 - u2 * unitContactY;
b1.xVelocity.set(v1 * unitContactX + u1PerpX);
b1.yVelocity.set(v1 * unitContactY + u1PerpY);
b2.xVelocity.set(v2 * unitContactX + u2PerpX);
b2.yVelocity.set(v2 * unitContactY + u2PerpY);
}
protected void createBalls(int numBalls, double minRadius, double maxRadius, double minSpeed, double maxSpeed, double initialX, double initialY) {
final Random rng = new Random();
for (int i = 0; i < numBalls; i++) {
double radius = minRadius + (maxRadius - minRadius) * rng.nextDouble();
double mass = Math.pow((radius / 40), 3);
final double speed = minSpeed + (maxSpeed - minSpeed) * rng.nextDouble();
final double angle = 2 * PI * rng.nextDouble();
Ball ball = new Ball(initialX, initialY, radius, speed * cos(angle), speed * sin(angle), mass, COLORS[i % COLORS.length]);
balls.add(ball);
}
}
private void constrainBallsOnResize(final Pane ballContainer) {
ballContainer.widthProperty().addListener((observable, newValue, oldValue) -> {
if (newValue.doubleValue() < oldValue.doubleValue()) {
for (Ball b : balls) {
double max = newValue.doubleValue() - b.radius;
if (b.centerX > max) {
b.centerX = max;
}
}
}
});
ballContainer.heightProperty().addListener((observable, newValue, oldValue) -> {
if (newValue.doubleValue() < oldValue.doubleValue()) {
for (Ball b : balls) {
double max = newValue.doubleValue() - b.radius;
if (b.centerY > max) {
b.centerY = max;
}
}
}
});
}
protected static class Ball {
double centerX;
double centerY;
final DoubleProperty xVelocity; // pixels per second
final DoubleProperty yVelocity;
final ReadOnlyDoubleWrapper speed;
final double mass; // arbitrary units
final double radius; // pixels
final Color color;
public Ball(double centerX, double centerY, double radius, double xVelocity, double yVelocity, double mass, Color color) {
this.centerX = centerX;
this.centerY = centerY;
this.xVelocity = new SimpleDoubleProperty(this, "xVelocity", xVelocity);
this.yVelocity = new SimpleDoubleProperty(this, "yVelocity", yVelocity);
this.speed = new ReadOnlyDoubleWrapper(this, "speed");
speed.bind(Bindings.createDoubleBinding(() -> {
final double xVel = this.xVelocity.get();
final double yVel = this.yVelocity.get();
return sqrt(xVel * xVel + yVel * yVel);
}, this.xVelocity, this.yVelocity));
this.mass = mass;
this.radius = radius;
this.color = color;
}
}
private static class FrameStats {
private long frameCount;
private double meanFrameInterval; // millis
private final ReadOnlyStringWrapper text = new ReadOnlyStringWrapper(this, "text", "Frame count: 0 Average frame interval: N/A");
public long getFrameCount() {
return frameCount;
}
public double getMeanFrameInterval() {
return meanFrameInterval;
}
public void addFrame(long frameDurationNanos) {
meanFrameInterval = (meanFrameInterval * frameCount + frameDurationNanos / 1_000_000.0) / (frameCount + 1);
frameCount++;
text.set(toString());
}
public ReadOnlyStringProperty textProperty() {
return text.getReadOnlyProperty();
}
@Override
public String toString() {
return String.format("Frame count: %,d Average frame interval: %.3f milliseconds", getFrameCount(), getMeanFrameInterval());
}
}
public static void main(String[] args) {
launch(args);
}
}
package animation;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import org.dyn4j.dynamics.Body;
import org.dyn4j.dynamics.World;
import org.dyn4j.dynamics.contact.ContactPoint;
import org.dyn4j.geometry.AABB;
import org.dyn4j.geometry.Geometry;
import org.dyn4j.geometry.MassType;
import org.dyn4j.geometry.Vector2;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class BouncingBallDyn4j extends Application {
final static ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
final static Color[] colors = {Color.DARKGRAY, Color.FIREBRICK, Color.GREEN, Color.ORANGE, Color.DARKBLUE, Color.BLUEVIOLET};
final static Random rand = new Random();
final int simulationInterval = 20; // simulation interval is 20 ms
final World world = new World(); // world model of the physics engine
final Map<Shape, Body> shapesAndBodies = new ConcurrentHashMap<>(); // map of JavaFX shapes and corresponding dyn4j bodies
final Pane ballContainer = new Pane();
final Body ball = createCircle();
final Label status = new Label("Control the ball with the arrow keys. Spawn a new brick by a mouse click.");
boolean up, left, right;
@Override
public void start(Stage stage) {
final BorderPane root = new BorderPane();
root.setCenter(ballContainer);
root.setBottom(status);
final int initialWidth = 900;
final int initialHeight = 450;
final Scene scene = new Scene(root, initialWidth, initialHeight);
stage.setScene(scene);
stage.setTitle("Bouncing Ball based on Dyn4j");
stage.show();
stage.setOnCloseRequest(event -> scheduler.shutdown());
// transform y-axis and scaling
ballContainer.getTransforms().add(new Translate(0, initialHeight));
final int scale = 30; // fixed scale factor between JavaFX UI and the world model
ballContainer.getTransforms().add(new Scale(scale, -scale));
// register key listeners
scene.setOnKeyPressed((event) -> {
switch (event.getCode()) {