diff options
-rw-r--r-- | src/subconscious/Game.java | 104 | ||||
-rw-r--r-- | src/subconscious/Subconscious.java | 21 | ||||
-rw-r--r-- | src/subconscious/graphics/GameWindow.java | 9 | ||||
-rw-r--r-- | src/subconscious/graphics/Scene.java | 183 | ||||
-rw-r--r-- | 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<Actor> actors = new ArrayList<>(); private ArrayList<Map> 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<RenderingHints.Key, ?> 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<Widget> dynamicWidgetsCache = new ArrayList<>(); private ArrayList<Widget> 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 |