com.kauridev.lunarbase.Game Maven / Gradle / Ivy
Show all versions of lunar-base Show documentation
/*
* This file is part of the lunar-base package.
*
* Copyright (c) 2014 Eric Fritz
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.kauridev.lunarbase;
/**
* A basic game utility that provides timing mechanisms including an update/render loop. The loop
* can be either fixed or variable timestep (fixed timestep by default). The type of step determines
* how often {@link #update(double)} and {@link #render(double)} will be called and affects how you
* need to represent time-based procedures such as movement and animation.
*
* When using a variable timestep game loop you should express rates - such as the distance a sprite
* moves - in game units per millisecond (ms). The amount a sprite moves in any given update can
* then be calculated as the product of the rate of the sprite and the elapsed time. Using this
* approach to calculate the distance the sprite moved ensures that the sprite will move
* consistently if the speed of the game or computer varies.
*
* @author Eric Fritz
*/
abstract public class Game
{
/**
* The number of slow updates before {@link #isRunningSlowly} is set to true.
*/
private final int slowUpdateThreshold = 20;
/**
* Whether the game loop has started.
*/
private boolean isStarted = false;
/**
* Whether the game loop is running.
*/
private boolean isRunning = false;
/**
* Whether the game loop has started.
*/
private boolean isRunningSlowly = false;
/**
* Whether the game loop runs in fixed or variable timestep.
*/
private boolean isFixedTimeStep = true;
/**
* The number of millisecond calls to update should be padded if isFixedTimeStep is true.
*/
private double targetElapsedTime = 1000.0 / 60;
/**
* Whether update should be called if the game is inactive.
*/
private boolean alwaysUpdate = true;
/**
* Whether render should be called if the game is inactive.
*/
private boolean alwaysRender = true;
/**
* Whether render should be skipped during the next tick.
*/
private boolean isRenderSuppressed = false;
/**
* Whether update has been called at least once.
*/
private boolean hasCalledUpdate = false;
/**
* Whether render has been called at least once.
*/
private boolean hasCalledRender = false;
/**
* The relative time (in milliseconds) of when the game loop started.
*/
private long startTime;
/**
* The relative time (in nanoseconds) of the last call to update.
*/
private long lastUpdate;
/**
* The relative time (in nanoseconds) of the last call to render.
*/
private long lastRender;
/**
* The number of consecutive updates that took longer than targetElapsedTime.
*/
private int slowUpdates;
/**
* Initializes the game and begins running the game loop.
*
* @throws IllegalStateException If the game has already been started.
*/
final public void start() {
if (isStarted) {
throw new IllegalStateException();
}
run();
}
/**
* Stops the game loop.
*
* @throws IllegalStateException If the game is not running.
*/
final public void stop() {
if (!isRunning) {
throw new IllegalStateException();
}
isRunning = false;
}
/**
* Runs the game.
*/
private void run() {
isStarted = true;
isRunning = true;
startTime = System.currentTimeMillis();
init();
while (isRunning) {
tick();
}
exit();
}
/**
* Calls {@link #update(double)} (if applicable) and {@link #render(double)} (if applicable).
*/
private void tick() {
// If the loop is running in fixed timestep and at least one update has already been
// called, wait until targetElapsedTime has passed before calling the next update.
if (isFixedTimeStep && hasCalledUpdate) {
double elapsed = getTimeSinceLastUpdate();
if (elapsed < targetElapsedTime) {
try {
Thread.sleep((long) (targetElapsedTime - elapsed));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// Call update.
if ((isActive() || alwaysUpdate)) {
//
// TODO - if inactive, should the elapsed time reset?
double elapsed = getTimeSinceLastUpdate();
lastUpdate = System.nanoTime();
update(elapsed);
hasCalledUpdate = true;
if (!isRunning) {
return;
}
}
// Determine if the game loop is running slowly. Maintain a counter for the number of
// consecutive updates that took longer than targetElapsedTime. If this value becomes
// greater than slowUpdateThreshold, the loop is running consistently slower than the
// target elapsed time.
isRunningSlowly = false;
if (isFixedTimeStep && getTimeSinceLastUpdate() > targetElapsedTime) {
if (slowUpdates++ >= slowUpdateThreshold) {
isRunningSlowly = true;
}
} else {
slowUpdates = 0;
}
// Call render if the game loop is not running slowly and the last call to update did
// not suppress rendering for this tick.
if ((isActive() || alwaysRender) && !isRenderSuppressed && !isRunningSlowly) {
//
// TODO - if inactive, suppressed, or skipped, should the elapsed time reset?
double elapsed = getTimeSinceLastRender();
lastRender = System.nanoTime();
render(elapsed);
hasCalledRender = true;
}
isRenderSuppressed = false;
}
/**
* Returns the total elapsed time (in milliseconds) that the game loop has been running. This
* also includes the time it took to initialize the game, not just the time since the first
* update.
*
* @return The total elapsed time (in milliseconds) that the game loop has been running.
*/
final public long getTotalElapsedTime() {
return System.currentTimeMillis() - startTime;
}
/**
* Returns the time elapsed (in fractional milliseconds) since the last call to
* {@link #update(double)}. If update has not yet been called since the start of the
* game loop, this method returns 0.0.
*
* @return The time elapsed (in fractional milliseconds) since the last call to update.
*/
final public double getTimeSinceLastUpdate() {
if (!hasCalledUpdate) {
return 0;
}
return (System.nanoTime() - lastUpdate) / 1000000.0;
}
/**
* Returns the time elapsed (in fractional milliseconds) since the last call to
* {@link #render(double)}. If render has not yet been called since the start of the
* game loop, this method returns 0.0.
*
* @return The time elapsed (in fractional milliseconds) since the last call to render.
*/
final public double getTimeSinceLastRender() {
if (!hasCalledRender) {
return 0;
}
return (System.nanoTime() - lastRender) / 1000000.0;
}
/**
* Returns true if the game is running, false otherwise.
*
* @return true if the game is running, false otherwise.
*/
final public boolean isRunning() {
return isRunning;
}
/**
* Returns true if the game loop is taking too long, false otherwise.
*
* During fixed timestep, if calls to {@link #update(double)} are taking consistently longer
* than the target elapsed time to complete, the game loop can be considered to be running too
* slowly and should do something to "catch up".
*
* @return true if the game loop is taking too long, false otherwise.
*
* @see #setTargetElapsedTime(double)
* @see #setTargetUpdatesPerSecond(int)
*/
final public boolean isRunningSlowly() {
return isRunningSlowly;
}
/**
* Sets the game loop as either fixed timestep or variable timestep.
*
* If isFixedTimeStep is true, the game loop will run in fixed timestep. The
* game loop will call {@link #update(double)} after the target elapsed time has passed since
* the last call to update and then attempt to call {@link #render(double)} if the game
* loop is not running too slowly.
*
* If isFixedTimeStep is false, the game loop will run in variable timestep.
* The game loop will call update and render continuously.
*
* @param isFixedTimeStep true for fixed timestep, false for variable timestep.
*
* @see #isRunningSlowly()
* @see #setTargetElapsedTime(double)
* @see #setTargetUpdatesPerSecond(int)
*/
final public void setFixedTimeStep(boolean isFixedTimeStep) {
this.isFixedTimeStep = isFixedTimeStep;
}
/**
* Sets the target number of calls to {@link #update(double)} per second when the game loop runs
* in fixed timestep.
*
* @param numUpdates The target number of calls to update per second.
*
* @throws IllegalArgumentException If targetElapsedTime is not greater than zero.
* @see #setFixedTimeStep(boolean)
* @see #setTargetElapsedTime(double)
*/
final public void setTargetUpdatesPerSecond(int numUpdates) {
if (targetElapsedTime <= 0) {
throw new IllegalArgumentException();
}
setTargetElapsedTime(1000.0 / numUpdates);
}
/**
* Sets the target time between calls to {@link #update(double)} in milliseconds when the game
* loop runs in fixed timestep.
*
* @param targetElapsedTime The target time between calls to update in milliseconds.
*
* @throws IllegalArgumentException If targetElapsedTime is not greater than zero.
* @see #setFixedTimeStep(boolean)
* @see #setTargetUpdatesPerSecond(int)
*/
final public void setTargetElapsedTime(double targetElapsedTime) {
if (targetElapsedTime <= 0) {
throw new IllegalArgumentException();
}
this.targetElapsedTime = targetElapsedTime;
}
/**
* Prevents calls to {@link #render(double)} until after the next call to
* {@link #update(double)}.
*/
final public void suppressRender() {
isRenderSuppressed = true;
}
/**
* Returns true if the game is active, false otherwise.
*
* The base implementation always returns true. Override this method to return
* false when, for example, the window is minimized or does not have focus.
*
* @return true if the game is active, false otherwise.
*/
public boolean isActive() {
return true;
}
/**
* Sets whether {@link #update(double)} should be called when the game is not active.
*
* @param alwaysUpdate true if update should be called when the game is not active.
*
* @see #isActive()
*/
final public void setAlwaysUpdate(boolean alwaysUpdate) {
this.alwaysUpdate = alwaysUpdate;
}
/**
* Sets whether {@link #render(double)} should be called when the game is not active.
*
* @param alwaysRender true if render should be called when the game is not active.
*
* @see #isActive()
*/
final public void setAlwaysRender(boolean alwaysRender) {
this.alwaysRender = alwaysRender;
}
/**
* Initializes the game.
*/
abstract public void init();
/**
* Called after the game has stopped updating. This is where allocated resources should be
* closed or released.
*/
abstract public void exit();
/**
* Called when the game has determined that game logic needs to be processed. This might include
* the management of the game state, the processing of user input, or the updating of simulation
* data. Override this method with game-specific logic.
*
* @param elapsed The number of milliseconds elapsed since the last call to update.
*/
abstract public void update(double elapsed);
/**
* Called when the game determines it is time to render a frame. Override this method with
* game-specific rendering code.
*
* @param elapsed The number of milliseconds elapsed since the last call to render.
*/
abstract public void render(double elapsed);
}