summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/subconscious/Game.java104
-rw-r--r--src/subconscious/Subconscious.java21
-rw-r--r--src/subconscious/graphics/GameWindow.java9
-rw-r--r--src/subconscious/graphics/Scene.java183
-rw-r--r--src/subconscious/graphics/widget/PerfView.java25
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