From 96377f0c1b9a3e97de767d0ef413fea0a2847a87 Mon Sep 17 00:00:00 2001 From: Nao Pross Date: Wed, 12 Dec 2018 02:55:28 +0100 Subject: Separate UPS from FPS, make thread for GameWindow (graphics thread) The separation of game logic updates from render updates is necessary to proceed on the implementation of sprites. Otherwise the speed of animations of menus and sprites would depend on the game update speed. This commit breaks PerfView. --- src/subconscious/Game.java | 104 ++++++++++++-- src/subconscious/Subconscious.java | 21 ++- src/subconscious/graphics/GameWindow.java | 9 +- src/subconscious/graphics/Scene.java | 183 ++++++++++++++----------- src/subconscious/graphics/widget/PerfView.java | 25 ++-- 5 files changed, 236 insertions(+), 106 deletions(-) diff --git a/src/subconscious/Game.java b/src/subconscious/Game.java index bf1baf6..7c7893a 100644 --- a/src/subconscious/Game.java +++ b/src/subconscious/Game.java @@ -1,12 +1,27 @@ package subconscious; import java.util.ArrayList; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.Condition; /* Game * Contains informations about the current state of the game used in an * Obervable Object pattern. The Game Loop is managed in the graphics. */ public class Game { + + // UpdatesPerSecond indicate the number of game updates (logic) per sec + public static final int DESIRED_UPS = 60; + public static final long DESIRED_DELTA_LOOP_NS = (1000*1000*1000)/ ((long) DESIRED_UPS); + public static final long NO_DELAYS_PER_YELD = 15; + + // thread control + private boolean running = true; + private boolean threadPaused = false; + private Lock pauseLock = new ReentrantLock(); + private Condition threadResumed = pauseLock.newCondition(); + public enum State { // main menu MAIN_MENU, @@ -18,39 +33,110 @@ public class Game { PAUSE, CLOSING, } + // state control private State state = State.MAIN_MENU; private State lastState; - - // private final Lock stateLock = new ReentrantLock(); - // private Condition stateChanged; private boolean stateChanged; - private boolean running = true; + // game stuff private boolean gameOver = false; private ArrayList actors = new ArrayList<>(); private ArrayList maps = new ArrayList<>(); private Map currentMap; - // TODO: load audio? + // resource loaders private MapLoader mapLoader = new MapLoader(); - public Game() { - // set up stateLock - // stateChanged = stateLock.newCondition(); + public Game() {} + public void start() { // TODO: this will be replaced with a dynamic mechanism based // on the progress within the game Map testMap = this.mapLoader.get("testmap.json"); this.currentMap = testMap; this.maps.add(testMap); + + this.running = true; + this.loop(); } - public void start() { + private void loop() { + // measurements (happen in this order) + long beforeTime; + long afterUpdateTime; + long afterSleepTime; + + // differences (deltas) + long updateTimeDiff = 0, sleepTimeDiff = 0; + + // time that the thread will sleep + long sleepTime = 0; + // count how many times the loop had not slept + int skippedDelays = 0; + + + while (this.running) { + beforeTime = System.nanoTime(); + + // update graphics stuff + this.update(updateTimeDiff + sleepTimeDiff); + + afterUpdateTime = System.nanoTime(); + updateTimeDiff = afterUpdateTime - beforeTime; + + sleepTime = (Game.DESIRED_DELTA_LOOP_NS - updateTimeDiff) - (sleepTimeDiff - sleepTime); + + // if sleep is needed (too fast) + if (sleepTime > 0) { + try { + Thread.sleep(sleepTime/(1000*1000)); + } catch (InterruptedException ex) { + // ex.printStackTrace(); + } + + // if sleep is not needed (too slow) + } else { + // if the thread has been late for too much time, give up + // the cpu to other threads + if (++skippedDelays >= Game.NO_DELAYS_PER_YELD) { + Thread.yield(); + skippedDelays = 0; + } + } + + afterSleepTime = System.nanoTime(); + sleepTimeDiff = afterSleepTime - afterUpdateTime; + + if (this.threadPaused) { + this.pauseLock.lock(); + this.threadResumed.awaitUninterruptibly(); + this.pauseLock.unlock(); + } + } + } + /* thread pause controls */ + public void stop() { + // stop thread even if paused + this.resumeThread(); + this.running = false; } + public void pauseThread() { + this.threadPaused = true; + } + + public void resumeThread() { + if (!this.threadPaused) + return; + + this.threadPaused = false; + this.threadResumed.signal(); + } + + public void update(long deltaNanoTime) { // TODO: debug, disable on "release" // System.out.println("Game update from : " diff --git a/src/subconscious/Subconscious.java b/src/subconscious/Subconscious.java index 60b52e5..05ff15e 100644 --- a/src/subconscious/Subconscious.java +++ b/src/subconscious/Subconscious.java @@ -12,11 +12,24 @@ public class Subconscious { // use hw accelleration System.setProperty("sun.java2d.opengl", "true"); - // for debugging - Thread.currentThread().setName("Main"); + + Thread.currentThread().setName("Game (Main)"); // TODO: in the future this will be loaded from a save file - Game g = new Game(); - GameWindow w = new GameWindow(g); + Game game = new Game(); + GameWindow window = new GameWindow(game); + + Thread graphics = new Thread(window); + graphics.setName("Graphics (Window)"); + + graphics.start(); + game.start(); + + // wait for graphics to die + try { + graphics.join(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } } } \ No newline at end of file diff --git a/src/subconscious/graphics/GameWindow.java b/src/subconscious/graphics/GameWindow.java index 12de165..66fe26e 100644 --- a/src/subconscious/graphics/GameWindow.java +++ b/src/subconscious/graphics/GameWindow.java @@ -19,7 +19,7 @@ import java.awt.event.WindowEvent; * unloading scenes, and the window itself */ @SuppressWarnings("serial") -public class GameWindow extends Frame implements WindowListener { +public class GameWindow extends Frame implements Runnable, WindowListener { public static final Dimension WINDOW_SIZE = new Dimension(1280, 720); private Panel root; @@ -51,13 +51,10 @@ public class GameWindow extends Frame implements WindowListener { this.add(this.root, BorderLayout.CENTER); this.pack(); this.setVisible(true); - - // start Window Loop - this.loop(); } - // ovserver of this.game - private void loop() { + @Override + public void run() { // load the first scene this.loadScene(new MainMenuScene(this.game)); diff --git a/src/subconscious/graphics/Scene.java b/src/subconscious/graphics/Scene.java index db035b3..a544b1c 100644 --- a/src/subconscious/graphics/Scene.java +++ b/src/subconscious/graphics/Scene.java @@ -3,6 +3,7 @@ package subconscious.graphics; import subconscious.Game; import subconscious.graphics.widget.Widget; +import subconscious.graphics.widget.PerfView; import subconscious.graphics.widget.Clickable; import subconscious.graphics.widget.Dynamic; @@ -62,9 +63,17 @@ public abstract class Scene extends Panel public final String UNIQUE_NAME; private static int absScenesCount = 0; - public static final long DESIRED_FPS = 80; - public static final long DESIRED_DELTA_LOOP_NS = (1000*1000*1000)/DESIRED_FPS; + // NOTE: the UPS value has to be lower (or equal) than the FPS value: + // it makes no sense to have this type of game update faster than it + // can render. + // + // On the flipside a faster FPS rate can make UI animations smoother. + + // FramesPerSecond indicate the number of frame renders (graphics) per sec + public static final int DESIRED_FPS = 80; + public static final long DESIRED_DELTA_LOOP_NS = (1000*1000*1000)/ ((long) DESIRED_FPS); public static final long NO_DELAYS_PER_YELD = 15; + public static final Map RENDERING_HINTS = Map.of( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON, RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON, @@ -79,17 +88,20 @@ public abstract class Scene extends Panel private ArrayList dynamicWidgetsCache = new ArrayList<>(); private ArrayList clickableWidgetsCache = new ArrayList<>(); + // special widget to show performance infos + private PerfView perfViewWidget; + // Game is never cached in the local thread protected volatile Game game; // TODO: this could become a Queue of scenes to load - protected Scene requestedScene = null; - protected boolean requestedPrevScene = false; + private Scene requestedScene = null; + private boolean requestedPrevScene = false; protected volatile boolean running = true; - protected volatile boolean threadPaused = false; - protected Lock pauseLock = new ReentrantLock(); - protected Condition threadResumed = pauseLock.newCondition(); + private volatile boolean threadPaused = false; + private Lock pauseLock = new ReentrantLock(); + private Condition threadResumed = pauseLock.newCondition(); protected volatile Dimension canvasSize = GameWindow.WINDOW_SIZE; protected Canvas canvas = new Canvas(); @@ -136,29 +148,28 @@ public abstract class Scene extends Panel this.build(); // TODO: this will be controlled in the settings - this.addWidget(new PerfView("default-perfview", 0, 0)); + this.perfViewWidget = new PerfView("default-perfview", 0, 0); + this.addWidget(this.perfViewWidget); } public Scene(Game game) { this(game, ""); } - /* */ - /* abstract methods */ - // runs when the the scene thread starts + // runs once when the the scene thread starts protected abstract void build(); - // runs on each tick protected abstract void update(long deltaNanoTime); protected abstract void render(Graphics2D g); + /* widgets management */ protected void updateWidgets(long deltaNanoTime) { for (Widget w : this.dynamicWidgetsCache) { ((Dynamic) w).update(deltaNanoTime); } } - protected void renderWidgets(Graphics2D g) { + private void renderWidgets(Graphics2D g) { for (Widget w : this.widgets) { Point absPos = new Point(this.widgetAnchors.get(w.getAnchor())); absPos.translate(w.getX(), w.getY()); @@ -172,7 +183,6 @@ public abstract class Scene extends Panel } } - /* widgets management */ protected void addWidget(Widget widget) { this.widgets.add(widget); @@ -190,40 +200,49 @@ public abstract class Scene extends Panel throw new UnsupportedOperationException("TODO"); } - /* request scenes */ - protected synchronized void requestPrevScene() { - this.requestedPrevScene = true; - } - - protected synchronized boolean isRequestingPrevScene() { - return this.requestedPrevScene; - } - - protected synchronized void requestScene(Scene sc) { - if (this.requestedScene != null) - throw new UnsupportedOperationException("only one scene can be requested at once"); + /* call render and updates */ + private void doubleBufferRender(long nanoDeltaTime) { + // render on a double buffer + do { + do { + Graphics2D g = (Graphics2D) this.buffer.getDrawGraphics(); + g.addRenderingHints(Scene.RENDERING_HINTS); + g.setFont(Fonts.DEFAULT); + + this.render(g); + this.renderWidgets(g); + + g.dispose(); + // repeat if the rendering buffer contents were restored + } while (this.buffer.contentsRestored() && running); + // repeat if the drawing buffer contents were lost + } while (this.buffer.contentsLost() && running); + + try { + this.buffer.show(); + } catch (IllegalStateException ex) { + // this happens when the scene is hidden or the frame is disposed + // for example then the thread is stopped, and so this exception + // can be ignored + } - this.requestedScene = sc; } - // TODO: protected synchronized void requestScene(String uniqueName) {} + /* runnable implementation */ + public void run() { + // measurements (happen in this order) + long beforeTime; + long afterRenderTime; + long afterSleepTime; - public synchronized boolean isRequestingScene() { - return this.requestedScene != null; - } + // differences (deltas) + long renderTimeDiff = 0, sleepTimeDiff = 0; - public synchronized Scene getRequestedScene() { - Scene requested = this.requestedScene; - this.requestedScene = null; + // time that the thread will sleep + long sleepTime = 0; - return requested; - } - - /* runnable implementation */ - public void run() { - long beforeTime, afterTime, timeDiff = 0, sleepTime; - long overSleepTime = 0L; - int noDelays = 0; + // count how many times the loop had not slept + int skippedDelays = 0; this.running = true; @@ -243,40 +262,20 @@ public abstract class Scene extends Panel this.canvas.requestFocus(); } - while (running) { + while (this.running) { beforeTime = System.nanoTime(); - // render on a double buffer - do { - do { - Graphics2D g = (Graphics2D) this.buffer.getDrawGraphics(); - g.addRenderingHints(Scene.RENDERING_HINTS); - g.setFont(Fonts.DEFAULT); - this.render(g); - this.renderWidgets(g); - g.dispose(); - // repeat if the rendering buffer contents were restored - } while (this.buffer.contentsRestored() && running); - // repeat if the drawing buffer contents were lost - } while (this.buffer.contentsLost() && running); - - try { - this.buffer.show(); - } catch (IllegalStateException ex) { - // this happens when the scene is hidden or the frame is disposed - // for example then the thread is stopped, and so this exception - // can be ignored - } + // update graphics stuff + this.update(renderTimeDiff + sleepTimeDiff); + this.updateWidgets(renderTimeDiff + sleepTimeDiff); - // update game and widgets - this.update(Scene.DESIRED_DELTA_LOOP_NS - timeDiff); - this.updateWidgets(Scene.DESIRED_DELTA_LOOP_NS - timeDiff); - this.game.update(Scene.DESIRED_DELTA_LOOP_NS - timeDiff); + // render + this.doubleBufferRender(renderTimeDiff + sleepTimeDiff); - afterTime = System.nanoTime(); - timeDiff = afterTime - beforeTime; - sleepTime = (Scene.DESIRED_DELTA_LOOP_NS - timeDiff) - overSleepTime; + afterRenderTime = System.nanoTime(); + renderTimeDiff = afterRenderTime - beforeTime; + sleepTime = (Scene.DESIRED_DELTA_LOOP_NS - renderTimeDiff) - (sleepTimeDiff - sleepTime); // if sleep is needed (too fast) if (sleepTime > 0) { @@ -286,19 +285,19 @@ public abstract class Scene extends Panel // ex.printStackTrace(); } - overSleepTime = (System.nanoTime() - afterTime) - sleepTime; - // if sleep is not needed (too slow) } else { - overSleepTime = 0L; // if the thread has been late for too much time, give up // the cpu to other threads - if (++noDelays >= Scene.NO_DELAYS_PER_YELD) { + if (++skippedDelays >= Scene.NO_DELAYS_PER_YELD) { Thread.yield(); - noDelays = 0; + skippedDelays = 0; } } + afterSleepTime = System.nanoTime(); + sleepTimeDiff = afterSleepTime - afterRenderTime; + if (this.threadPaused) { this.pauseLock.lock(); this.threadResumed.awaitUninterruptibly(); @@ -307,7 +306,7 @@ public abstract class Scene extends Panel } } - /* game pause controls */ + /* thread pause controls */ public void stop() { // stop thread even if paused this.resumeThread(); @@ -326,6 +325,36 @@ public abstract class Scene extends Panel this.threadResumed.signal(); } + /* scenes management */ + protected synchronized void requestPrevScene() { + this.requestedPrevScene = true; + } + + protected synchronized boolean isRequestingPrevScene() { + return this.requestedPrevScene; + } + + protected synchronized void requestScene(Scene sc) { + if (this.requestedScene != null) + throw new UnsupportedOperationException("only one scene can be requested at once"); + + this.requestedScene = sc; + } + + // TODO: protected synchronized void requestScene(String uniqueName) {} + + public synchronized boolean isRequestingScene() { + return this.requestedScene != null; + } + + public synchronized Scene getRequestedScene() { + Scene requested = this.requestedScene; + this.requestedScene = null; + + return requested; + } + + /* canvas size management */ // automagically set the canvas size to the parent's size // WARNING: does not always work diff --git a/src/subconscious/graphics/widget/PerfView.java b/src/subconscious/graphics/widget/PerfView.java index 7ec7372..377d7d4 100644 --- a/src/subconscious/graphics/widget/PerfView.java +++ b/src/subconscious/graphics/widget/PerfView.java @@ -11,8 +11,9 @@ import java.awt.Graphics2D; * it was mainly created for debugging, but could be used to show FPS in the * final game. */ -public class PerfView extends Widget implements Dynamic { - protected long lastDeltaNanoTime = 1; +public class PerfView extends Widget { + protected long updateNanoTimeDiff = 1; + protected long renderNanoTimeDiff = 1; public PerfView(String uniqueName, int x, int y) { // the size depends on the font @@ -23,8 +24,10 @@ public class PerfView extends Widget implements Dynamic { public void render(Graphics2D g) { g.setFont(Fonts.DEFAULT); - String text = "FPS: " - + Long.toString(1000*1000*1000/this.lastDeltaNanoTime); + String text = "UPS: " + + String.format("%3d", 1000*1000*1000/this.updateNanoTimeDiff) + + " FPS: " + + String.format("%3d", 1000*1000*1000/this.renderNanoTimeDiff); this.width = g.getFontMetrics().stringWidth(text); this.height = g.getFontMetrics().getHeight() + 10; @@ -36,11 +39,13 @@ public class PerfView extends Widget implements Dynamic { g.drawString(text, 0, this.height - 10); } - @Override - public void update(long deltaNanoTime) { - if (deltaNanoTime == 0) - deltaNanoTime = 1; - - this.lastDeltaNanoTime = deltaNanoTime; + public void updateTimeDiffs(long updateNanoTimeDiff, long renderNanoTimeDiff) { + if (updateNanoTimeDiff != 0) { + this.updateNanoTimeDiff = updateNanoTimeDiff; + } + + if (renderNanoTimeDiff != 0) { + this.renderNanoTimeDiff = renderNanoTimeDiff; + } } } \ No newline at end of file -- cgit v1.2.1