diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b7ab68..69cb1ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Added - Interface `Audio` extrahiert, mit Basisfunktionen von `Sound` und `Music`. - Klasse `Mixer` steuert mehrere Audio-Objekte gleichzeitig. +- Klasse `tasks.RateLimitedTask`, `tasks.FramerateLimitedTask`, `tasks.FrameSynchronizedTask` und `tasks.DelayedTask`. ## Changed - Neue Package-Struktur: diff --git a/src/schule/ngb/zm/Zeichenmaschine.java b/src/schule/ngb/zm/Zeichenmaschine.java index 151de10..93ed677 100644 --- a/src/schule/ngb/zm/Zeichenmaschine.java +++ b/src/schule/ngb/zm/Zeichenmaschine.java @@ -1276,10 +1276,37 @@ public class Zeichenmaschine extends Constants { // Zeichenthread //// + /** + * Globaler Monitor, der einmal pro Frame vom Zeichenthread freigegeben + * wird. Andere Threads können {@link Object#wait()} auf dem Monitor + * aufrufen, um sich mit dem Zeichenthread zu synchronisieren. Der + * {@code wait()} Aufruf sollte sich zur Sicherheit in einer Schleife + * befinden, die prüft, ob sich der Aktuelle {@link #tick} erhöht hat. + *

+	 * int lastTick = Constants.tick;
+	 *
+	 * // Do some work
+	 *
+	 * while( lastTick >= Constants.tick ) {
+	 *     synchronized( Zeichenmaschine.globalSyncLock ) {
+	 *         try {
+	 *             Zeichenmaschine.globalSyncLock.wait();
+	 *         } catch( InterruptedException ex ) {}
+	 *     }
+	 * }
+	 * // Next frame has started
+	 * 
+ *

+ * Die {@link schule.ngb.zm.tasks.FrameSynchronizedTask} implementiert eine + * {@link schule.ngb.zm.tasks.Task}, die sich automatisch auf diese Wiese + * mit dem Zeichenthread synchronisiert. + */ + public static final Object globalSyncLock = new Object[0]; + class Zeichenthread extends Thread { public Zeichenthread() { - super(Zeichenthread.class.getSimpleName()); + super(APP_NAME); //super(APP_NAME + " " + APP_VERSION); } @@ -1328,6 +1355,9 @@ public class Zeichenmaschine extends Constants { } runTasks(); + synchronized( globalSyncLock ) { + globalSyncLock.notifyAll(); + } // delta time in ns long afterTime = System.nanoTime(); @@ -1413,7 +1443,8 @@ public class Zeichenmaschine extends Constants { } - class InputListener implements MouseInputListener, MouseMotionListener, MouseWheelListener, KeyListener{ + class InputListener implements MouseInputListener, MouseMotionListener, MouseWheelListener, KeyListener { + @Override public void mouseClicked( MouseEvent e ) { enqueueEvent(e); diff --git a/src/schule/ngb/zm/tasks/DelayedTask.java b/src/schule/ngb/zm/tasks/DelayedTask.java new file mode 100644 index 0000000..7597ab3 --- /dev/null +++ b/src/schule/ngb/zm/tasks/DelayedTask.java @@ -0,0 +1,65 @@ +package schule.ngb.zm.tasks; + +import schule.ngb.zm.Zeichenmaschine; + +import java.util.concurrent.Delayed; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; + +public abstract class DelayedTask extends Task implements Delayed { + + protected long startTime = System.currentTimeMillis(); // in ms + + /** + * Gibt die absolute Verzögerung der Task zurück. Im Gegensatz zu + * {@link #getDelay(TimeUnit)} sollte das Ergebnis von {@code getDelay()} + * bei mehrmaligem Aufruf konstant bleiben. + * + * @return Die ursprüngliche Verzögerung in Millisekunden + */ + public abstract int getDelay(); + + public long getStartTime() { + return startTime + getDelay(); + } + + /** + * Gibt die verbleibende Verzögerung bis zur Ausführung der Task zurück. Im + * Gegensatz zu {@link #getDelay()} sollte für mehrere Aufrufe von + * {@code getDelay(TimeUnit)} gelten, dass der zeitlich spätere Aufruf einen + * kleineren Wert zurückgibt, als der Frühere (abhängig von der gewählten + * {@link TimeUnit}). + * + * @param unit Die Zeiteinheit für die Verzögerung. + * @return Die verbleibende Verzögerung in der angegebenen Zeiteinheit. + */ + @Override + public long getDelay( TimeUnit unit ) { + int diff = (int) (getStartTime() - System.currentTimeMillis()); + return unit.convert(diff, TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo( Delayed o ) { + return (int) (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } + + @Override + public void run() { + while( getDelay(TimeUnit.MILLISECONDS) > 0 ) { + try { + System.out.println("Waiting for " + getDelay(TimeUnit.MILLISECONDS) + " ms"); + Thread.sleep(getDelay(TimeUnit.MILLISECONDS)); + //wait(getDelay(TimeUnit.MILLISECONDS)); + } catch( InterruptedException e ) { + // Keep waiting + } + } + + running = true; + this.update(0.0); + running = false; + done = true; + } + +} diff --git a/src/schule/ngb/zm/tasks/FrameSynchronizedTask.java b/src/schule/ngb/zm/tasks/FrameSynchronizedTask.java new file mode 100644 index 0000000..416b9db --- /dev/null +++ b/src/schule/ngb/zm/tasks/FrameSynchronizedTask.java @@ -0,0 +1,50 @@ +package schule.ngb.zm.tasks; + +import schule.ngb.zm.Constants; +import schule.ngb.zm.Zeichenmaschine; + +public abstract class FrameSynchronizedTask extends Task { + + private static Thread mainThread; + + private static final Thread getMainThread() { + if( mainThread == null ) { + mainThread = Thread.currentThread(); + if( !mainThread.getName().equals(Constants.APP_NAME) ) { + // Need to search for main Zeichenthread ... + } + } + return mainThread; + } + + @Override + public void run() { + running = true; + int lastTick = 0; + Object lock = Zeichenmaschine.globalSyncLock; + + while( running ) { + lastTick = Constants.tick; + this.update(lastTick); + + synchronized( lock ) { + while( lastTick >= Constants.tick ) { + try { + lock.wait(); + } catch( InterruptedException e ) { + // We got interrupted ... + } + } + } + } + + running = false; + done = true; + } + + @Override + public boolean isActive() { + return false; + } + +} diff --git a/src/schule/ngb/zm/tasks/FramerateLimitedTask.java b/src/schule/ngb/zm/tasks/FramerateLimitedTask.java new file mode 100644 index 0000000..8fab4f6 --- /dev/null +++ b/src/schule/ngb/zm/tasks/FramerateLimitedTask.java @@ -0,0 +1,11 @@ +package schule.ngb.zm.tasks; + +import schule.ngb.zm.Constants; + +public abstract class FramerateLimitedTask extends RateLimitedTask { + + public int getRate() { + return Constants.framesPerSecond; + } + +} diff --git a/src/schule/ngb/zm/tasks/RateLimitedTask.java b/src/schule/ngb/zm/tasks/RateLimitedTask.java new file mode 100644 index 0000000..aadc06d --- /dev/null +++ b/src/schule/ngb/zm/tasks/RateLimitedTask.java @@ -0,0 +1,53 @@ +package schule.ngb.zm.tasks; + +public abstract class RateLimitedTask extends Task { + + public abstract int getRate(); + + @Override + public final void run() { + if( running || done ) { + return; + } + + // current time in ns + long beforeTime = System.nanoTime(); + // store for deltas + long overslept = 0L; + // delta in ms + double delta = 0; + + running = true; + while( running ) { + // delta in seconds + delta = (System.nanoTime() - beforeTime) / 1000000000.0; + beforeTime = System.nanoTime(); + + this.update(delta); + + // delta time in ns + long afterTime = System.nanoTime(); + long dt = afterTime - beforeTime; + long sleep = 0; + if( getRate() > 0 ) { + sleep = ((1000000000L / getRate()) - dt) - overslept; + } + + if( sleep > 0 ) { + try { + Thread.sleep(sleep / 1000000L, (int) (sleep % 1000000L)); + } catch( InterruptedException e ) { + // Interrupt not relevant + } + } else { + Thread.yield(); + } + // Did we sleep to long? + overslept = (System.nanoTime() - afterTime) - sleep; + } + + running = false; + done = true; + } + +} diff --git a/src/schule/ngb/zm/tasks/Task.java b/src/schule/ngb/zm/tasks/Task.java new file mode 100644 index 0000000..a8ae158 --- /dev/null +++ b/src/schule/ngb/zm/tasks/Task.java @@ -0,0 +1,24 @@ +package schule.ngb.zm.tasks; + +import schule.ngb.zm.Updatable; + +public abstract class Task implements Runnable, Updatable { + + protected boolean running = false; + + protected boolean done = false; + + @Override + public boolean isActive() { + return running; + } + + public boolean isDone() { + return !running & done; + } + + public void stop() { + running = false; + } + +} diff --git a/src/schule/ngb/zm/tasks/TaskRunner.java b/src/schule/ngb/zm/tasks/TaskRunner.java index 5bd20da..0173fca 100644 --- a/src/schule/ngb/zm/tasks/TaskRunner.java +++ b/src/schule/ngb/zm/tasks/TaskRunner.java @@ -25,6 +25,11 @@ public class TaskRunner { return runner; } + public static Future run( Task task ) { + TaskRunner r = getTaskRunner(); + return r.pool.submit(task); + } + public static Future run( Runnable task ) { TaskRunner r = getTaskRunner(); return r.pool.submit(task); @@ -35,18 +40,13 @@ public class TaskRunner { return r.pool.submit(task, result); } - public static Future schedule( Runnable task, int ms ) { - TaskRunner r = getTaskRunner(); - return r.pool.schedule(task, ms, TimeUnit.MILLISECONDS); - } - public static void invokeLater( Runnable task ) { SwingUtilities.invokeLater(task); } public static void shutdown() { if( runner != null ) { - runner.pool.shutdown(); + /*runner.pool.shutdown(); try { runner.pool.awaitTermination(SHUTDOWN_TIME, TimeUnit.MILLISECONDS); } catch( InterruptedException ex ) { @@ -55,15 +55,27 @@ public class TaskRunner { if( !runner.pool.isTerminated() ) { runner.pool.shutdownNow(); } - } + }*/ + runner.pool.shutdownNow(); } } - ScheduledExecutorService pool; + ExecutorService pool; private TaskRunner() { //pool = new ScheduledThreadPoolExecutor(4); - pool = Executors.newScheduledThreadPool(POOL_SIZE, new ThreadFactory() { + /*pool = Executors.newScheduledThreadPool(POOL_SIZE, new ThreadFactory() { + private final ThreadFactory threadFactory = Executors.defaultThreadFactory(); + + @Override + public Thread newThread( Runnable r ) { + Thread t = threadFactory.newThread(r); + t.setName("TaskRunner-" + t.getName()); + t.setDaemon(true); + return t; + } + });*/ + pool = Executors.newCachedThreadPool(new ThreadFactory() { private final ThreadFactory threadFactory = Executors.defaultThreadFactory(); @Override