Skip to content

Commit

Permalink
Consolidate the RenderLoop with the GameLoop
Browse files Browse the repository at this point in the history
The concept of separated loops for gamelogic and rendering is not really of much use for the kind of games that are/can be created with the litiengine. It causes a lot of concurrency issues that can be simply resolved by performing the game logic and rendering sequentially. e.g. Position updates in the game loop could cause visual stuttering in movement and positions of entities could thereby change inbetween a frame which caused inconsistent behavior.

Moreover this simplifies the general concept of loops for the LITIengine and the developer doesn't always have to ask himself which loop should be used for a task.
Overall, its a more straightforward and less error-prone approach which should also be easier to understand.
  • Loading branch information
steffen-wilke committed Dec 22, 2019
1 parent 392bcc5 commit ad5a0a4
Show file tree
Hide file tree
Showing 14 changed files with 39 additions and 163 deletions.
46 changes: 7 additions & 39 deletions src/de/gurkenlabs/litiengine/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ public final class Game {
private static GameInfo gameInfo = new GameInfo();

private static GameLoop gameLoop;
private static RenderLoop renderLoop;
private static UpdateLoop inputLoop;
private static ScreenManager screenManager;
private static GameWindow gameWindow;
Expand Down Expand Up @@ -150,7 +149,6 @@ public static void allowDebug(boolean allow) {
* If set to true, the GUI will be hidden.
* @see GameWindow
* @see Game#init(String...)
* @see RenderLoop
* @see Camera
*/
public static void hideGUI(boolean noGui) {
Expand Down Expand Up @@ -319,10 +317,13 @@ public static RenderEngine graphics() {
}

/**
* Gets the game's main loop that is used to execute and manage all game logic apart from rendering and input processing.<br>
* Gets the game's main loop that is used to execute and manage all game logic apart from input processing.<br>
* You can attach any <code>Updatable</code> instance to this loop if you want to execute custom game logic that is executed at the configured
* updaterate.
* max fps.
*
* The game's loop also executes the rendering process on the GameFrame's <code>RenderComponent</code>.<br>
* This internally renders the currently active screen which passes the <code>Graphics2D</code> object to all <code>GuiComponents</code> and the
* Environment for rendering.
* <p>
* <i>The LITIengine has separate loops for game logic, rendering and input processing. <br>
* This prevents them from interfering with each other and also properly separates tasks by their category.</i>
Expand All @@ -331,12 +332,11 @@ public static RenderEngine graphics() {
*
* @return The game's main loop.
*
* @see ClientConfiguration#getUpdaterate()
* @see ClientConfiguration#getMaxFps()
* @see IUpdateable
* @see ILoop#attach(IUpdateable)
* @see ILoop#detach(IUpdateable)
* @see Game#inputLoop()
* @see Game#renderLoop()
*/
public static IGameLoop loop() {
return gameLoop;
Expand All @@ -355,29 +355,6 @@ public static ILoop inputLoop() {
return inputLoop;
}

/**
* Gets the game's loop that executes the rendering process on the GameFrame's <code>RenderComponent</code>.<br>
* This internally renders the currently active screen which passes the <code>Graphics2D</code> object to all <code>GuiComponents</code> and the
* Environment for rendering. This loop will try to execute at the configured frames-per-second and limit the frames to this value.
*
* <p>
* <i>It's also possible to register <code>Updatable</code> instances to this loop which is useful if you want to execute something that is directly
* related to
* the rendering process and needs to be executed right before the game's rendering starts.</i>
* </p>
*
* @return The game's render loop.
*
* @see ClientConfiguration#getMaxFps()
* @see RenderComponent#render()
* @see Screen#render(java.awt.Graphics2D)
* @see GuiComponent#render(java.awt.Graphics2D)
* @see Environment#render(java.awt.Graphics2D)
*/
public static RenderLoop renderLoop() {
return renderLoop;
}

/**
* Gets the game's <code>ScreenManager</code> that is responsible for organizing all <code>Screens</code> of your game and providing the currently
* active <code>Screen</code> that is used to render the current <code>Environment</code>.<br>
Expand Down Expand Up @@ -457,14 +434,13 @@ public static synchronized void init(String... args) {
config().load();
Locale.setDefault(new Locale(config().client().getCountry(), config().client().getLanguage()));

gameLoop = new GameLoop("Main Update Loop", config().client().getUpdaterate());
gameLoop = new GameLoop("Main Update Loop", config().client().getMaxFps());
loop().attach(physics());
loop().attach(world());

final ScreenManager scrMgr = new ScreenManager();

// setup default exception handling for render and update loop
renderLoop = new RenderLoop("Render Loop");
inputLoop = new UpdateLoop("Input Loop", loop().getUpdateRate());

setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(config().client().exitOnError()));
Expand Down Expand Up @@ -525,7 +501,6 @@ public static synchronized void init(String... args) {

public static void setUncaughtExceptionHandler(UncaughtExceptionHandler uncaughtExceptionHandler) {
gameLoop.setUncaughtExceptionHandler(uncaughtExceptionHandler);
renderLoop.setUncaughtExceptionHandler(uncaughtExceptionHandler);
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
}

Expand Down Expand Up @@ -553,10 +528,6 @@ public static synchronized void start() {
gameLoop.start();
inputLoop.start();

if (!isInNoGUIMode()) {
renderLoop.start();
}

soundEngine.start();

for (final GameListener listener : gameListeners) {
Expand Down Expand Up @@ -594,9 +565,6 @@ static synchronized void terminate() {
soundEngine.terminate();

world().clear();
if (!isInNoGUIMode()) {
renderLoop.terminate();
}

for (final GameListener listener : gameListeners) {
try {
Expand Down
31 changes: 11 additions & 20 deletions src/de/gurkenlabs/litiengine/GameLoop.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,8 @@ public final class GameLoop extends UpdateLoop implements IGameLoop {

private final List<TimedAction> actions;

private long lastUpsTime;

private float timeScale;

private int updateCount;

protected GameLoop(String name, final int updateRate) {
super(name, updateRate);
Expand Down Expand Up @@ -72,25 +69,23 @@ public void updateExecutionTime(int index, long ticks) {
}
}

@Override
public Lock getLock() {
// make sure so synchronize the game logic with the render loop such that the update of gamelogic (e.g. positions)
// doesn't happen while a frame is being rendered
return RenderLoop.RenderLock;
}

/**
* In addition to the normal base implementation, the <code>GameLoop</code> performs registered action at the required
* time and tracks some detailed metrics.
*/
@Override
protected void process() {
Game.world().camera().updateFocus();
if (this.getTimeScale() > 0) {
super.process();
this.executeTimedActions();
}

if(!Game.isInNoGUIMode()) {
Game.window().getRenderComponent().render();
}

this.trackUpdateRate();
this.trackRenderMetric();
}

@Override
Expand All @@ -111,15 +106,11 @@ private void executeTimedActions() {

this.actions.removeAll(executed);
}

private void trackUpdateRate() {
++this.updateCount;

long currentMillis = System.currentTimeMillis();
if (currentMillis - this.lastUpsTime >= 1000) {
this.lastUpsTime = currentMillis;
Game.metrics().setUpdatesPerSecond(this.updateCount);
this.updateCount = 0;

private void trackRenderMetric() {
Game.metrics().setEstimatedMaxFramesPerSecond((int) (1000.0 / this.getProcessTime()));
if (Game.config().debug().trackRenderTimes()) {
Game.metrics().trackRenderTime("total", this.getProcessTime());
}
}

Expand Down
10 changes: 0 additions & 10 deletions src/de/gurkenlabs/litiengine/GameMetrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ public final class GameMetrics implements IRenderable {
private long upStreamInBytes;

private int framesPerSecond;
private int updatesPerSecond;
private int maxFramesPerSecond;

private float usedMemory;
Expand Down Expand Up @@ -68,10 +67,6 @@ public long getPing() {
return this.ping;
}

public long getUpdatesPerSecond() {
return this.updatesPerSecond;
}

public float getUpStreamInBytes() {
return this.upStreamInBytes;
}
Expand Down Expand Up @@ -107,7 +102,6 @@ public void render(final Graphics2D g) {
// render client metrics
this.drawTitle(g, "[client]");
this.drawMetric(g, "fps : " + this.getFramesPerSecond());
this.drawMetric(g, "ups : " + this.getUpdatesPerSecond());
this.drawMetric(g, "max fps : " + this.maxFramesPerSecond);

// render jvm metrics if debug is enabled
Expand Down Expand Up @@ -151,10 +145,6 @@ public void setPing(final long ping) {
this.ping = ping;
}

public void setUpdatesPerSecond(final int updatesPerSecond) {
this.updatesPerSecond = updatesPerSecond;
}

/**
* Sets the color that is used when rendering the metrics if <code>cl_showGameMetrics = true</code>.
*
Expand Down
8 changes: 4 additions & 4 deletions src/de/gurkenlabs/litiengine/GameWindow.java
Original file line number Diff line number Diff line change
Expand Up @@ -234,23 +234,23 @@ private static void initializeWindowEventListeners(Window window) {

window.addWindowStateListener(e -> {
if (e.getNewState() == Frame.ICONIFIED) {
Game.renderLoop().setMaxFps(ICONIFIED_MAX_FPS);
Game.loop().setTickRate(ICONIFIED_MAX_FPS);
} else {
Game.renderLoop().setMaxFps(Game.config().client().getMaxFps());
Game.loop().setTickRate(Game.config().client().getMaxFps());
}
});

window.addWindowFocusListener(new WindowFocusListener() {
@Override
public void windowLostFocus(WindowEvent e) {
if (Game.config().graphics().reduceFramesWhenNotFocused()) {
Game.renderLoop().setMaxFps(NONE_FOCUS_MAX_FPS);
Game.loop().setTickRate(NONE_FOCUS_MAX_FPS);
}
}

@Override
public void windowGainedFocus(WindowEvent e) {
Game.renderLoop().setMaxFps(Game.config().client().getMaxFps());
Game.loop().setTickRate(Game.config().client().getMaxFps());
}
});

Expand Down
6 changes: 6 additions & 0 deletions src/de/gurkenlabs/litiengine/ILoop.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,10 @@ public interface ILoop extends ILaunchable {
* @return A {@code Lock} for this loop.
*/
public Lock getLock();

/**
* Sets the tickrate at which the loop performs its updates.
* @param tickRate The tickrate of the loop.
*/
public void setTickRate(int tickRate);
}
2 changes: 1 addition & 1 deletion src/de/gurkenlabs/litiengine/IUpdateable.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public interface IUpdateable {
* This method is called by the game loop on all objects that are attached to the loop.
* It's called on every tick of the loop and the frequency can be configured using the <code>ClientConfiguration</code>.
*
* @see ClientConfiguration#setUpdaterate(int)
* @see ClientConfiguration#setMaxFps(int)
*/
public void update();

Expand Down
46 changes: 0 additions & 46 deletions src/de/gurkenlabs/litiengine/RenderLoop.java

This file was deleted.

8 changes: 4 additions & 4 deletions src/de/gurkenlabs/litiengine/UpdateLoop.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ public double getProcessTime() {
return this.processTime;
}

public void setTickRate(int tickRate) {
this.tickRate = tickRate;
}

protected Set<IUpdateable> getUpdatables() {
return this.updatables;
}
Expand Down Expand Up @@ -170,10 +174,6 @@ protected double delay() throws InterruptedException {
return delay;
}

protected void setTickRate(int tickRate) {
this.tickRate = tickRate;
}

@Override
public Lock getLock() {
return this.lock;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,13 @@ public class ClientConfiguration extends ConfigurationGroup {

private boolean showGameMetrics;

private int updaterate;

private boolean exitOnError;

/**
* Instantiates a new client configuration.
*/
public ClientConfiguration() {
super();
this.setUpdaterate(60);
this.setMaxFps(60);
this.setShowGameMetrics(false);
this.setExitOnError(false);
Expand Down Expand Up @@ -58,15 +55,6 @@ public int getMaxFps() {
return this.maxFps;
}

/**
* Gets the updaterate.
*
* @return the updaterate
*/
public int getUpdaterate() {
return this.updaterate;
}

public void setCountry(final String country) {
this.country = country;
}
Expand All @@ -89,25 +77,6 @@ public void setShowGameMetrics(final boolean showGameMetrics) {
this.showGameMetrics = showGameMetrics;
}

/**
* Sets the updaterate. On a very good machine the max update rate is sth.
* around 500 but such a high value will never be beneficial for the player.
*
* <p>
* This defaults to a value of 60.
* </p>
*
* @param updaterate
* the new updaterate
*/
public void setUpdaterate(final int updaterate) {
if (updaterate < 1 || updaterate > 500) {
return;
}

this.updaterate = updaterate;
}

public void setExitOnError(boolean exit) {
this.exitOnError = exit;
}
Expand Down
Loading

0 comments on commit ad5a0a4

Please sign in to comment.