I have two interfaces. Entity for updates. Drawable for rendering. They both have two interface
methods. One that is called every frame, one every second.
Entity.java
public interface Entity
{
public void updatePerFrame ();
public void updatePerSecond ();
}
Drawable.java
public interface Drawable
{
public void drawPerFrame ();
public void drawPerSecond ();
}
The main component class Game
has two lists, one for Entity
and one for Drawable
.
public class Game {
// if the game is running, default = true
// if the game is paused, default = false
private boolean isRunning;
private boolean isPaused;
// execution time for the last cycle of game loop
private long lastLoopTime;
private long curLoopTime;
private long deltaLoopTime;
private double deltaTime;
// if a second has passed since last execution
private boolean secondPassed;
private long secondTimer;
// nanoseconds spent per frame
private final int OPTIMAL_FPS = 60;
private final int OPTIMAL_TIME = 1000000000 / OPTIMAL_FPS;
// entities
private List<Entity> entities;
// drawable
private List<Drawable> drawables;
// fps tracker
FPSViewer fpsViewer;
/**
* Main Function, instantiating Game
*/
public static void main() {
Game g = new Game();
g.gameLoop();
}
/**
*
*/
public Game () {
// the game is on and not paused
isRunning = true;
isPaused = false;
// a second has not yet passed
secondPassed = false;
secondTimer = System.currentTimeMillis();
// init entities and drawables
entities = new ArrayList<Entity>();
drawables = new ArrayList<Drawable>();
// begin with current time
lastLoopTime = System.nanoTime();
// create the fps viewer
fpsViewer = new FPSViewer();
entities.add(fpsViewer);
drawables.add(fpsViewer);
}
/**
* Game Loop
*/
public void gameLoop () {
while (isRunning == true) {
// if the game is paused
if (isPaused == true) {
// set paused attributes
// to-be-done
// wait for user to unpause
while (isPaused == true) {
}
// reset time synching
lastLoopTime = System.nanoTime();
// reset fps viewer
fpsViewer.reset();
}
// get delta time
curLoopTime = System.nanoTime();
deltaLoopTime = curLoopTime - lastLoopTime;
deltaTime = deltaLoopTime / (double)OPTIMAL_TIME;
// set last to current
lastLoopTime = curLoopTime;
// check if a second has passed
if ((System.currentTimeMillis() - secondTimer) > 1000) {
secondPassed = true;
secondTimer = System.currentTimeMillis();
}
// render everything
render ();
// update game
gameUpdate();
// reset if a second has passed
if (secondPassed == true )
secondPassed = false;
// wait for game to catch up (if loop execution was too fast)
try {
Thread.sleep( (lastLoopTime - System.nanoTime() + OPTIMAL_TIME)/1000000 );
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/**
* Game Updates
*/
private void gameUpdate () {
for (Entity e : entities) {
e.updatePerFrame ();
}
if (secondPassed == true) {
for (Entity e : entities) {
e.updatePerSecond();
}
}
}
/**
* Renders all graphics
*/
private void render () {
for (Drawable d : drawables) {
d.drawPerFrame ();
}
if (secondPassed == true) {
for (Drawable d : drawables) {
d.drawPerSecond();
}
}
}
}
As a test there is the FPSViewer
.
It currently sits at 62 fps. Which will be fixed in the next iteration.
public class FPSViewer implements Drawable, Entity {
private long lastFpsUpdateTime;
private long lastExecutionTime;
private byte fps;
public void drawPerFrame () {
}
public void drawPerSecond () {
System.out.println ("Frame: " + fps);
}
/**
* update fps counter
*/
public void updatePerFrame () {
lastFpsUpdateTime += (System.nanoTime() - lastExecutionTime);
lastExecutionTime = System.nanoTime();
fps++;
}
/**
* Resets frame counter back to 0
*/
public void updatePerSecond () {
lastFpsUpdateTime = 0;
fps = 0;
}
public void reset () {
lastFpsUpdateTime = 0;
lastExecutionTime = 0;
fps = 0;
}
}
Now to the feedback wanted, everything basically. Be merciless. Here are some things to focus on. The game will consist mostly of 2D Buttons with mouse and keyboard input.
2 Answers 2
While it is very interesting to do the timekeeping manually just like you did, I'd like to talk to you about multithreading and scheduled ThreadPoolExecutors.
A ScheduledThreadPoolExecutor can handle all the timing steps for you.. Additionally a gameLoop often just gets a pre-rendered frame and outputs it. the rest will usually be done in separate threads. They will update the games state and render a frame to the buffer for outputting.
In a rough way what happens in most modern game can be described like in this article:
The game state itself doesn’t need to be updated 60 times per second. Player input, AI and the updating of the game state have enough with 25 frames per second. So let’s try to call the update_game() 25 times per second, no more, no less. The rendering, on the other hand, needs to be as fast as the hardware can handle. But a slow frame rate shouldn’t interfere with the updating of the game.
Furthermore there's a few small tricks to have the transitions look even smoother, that are also described in that article. Now to applying this to your current code:
private final ScheduledThreadPoolExecutor gameStateThread = new ScheduledThreadPoolExecutor(2);
/* other fields */
public Game() {
// 1000 (ms / s) / 25 (ticks / s) = 40 ms/tick
gameStateThread.scheduleAtFixedRate(() -> gameUpdateTick(), 1000, 1000 / TICKS_PER_SEC, TimeUnit.MILLISECONDS);
gameStateThread.scheduleAtFixedRate(() -> gameUpdateSecond(), 1, 1, TimeUnit.SECONDS);
}
public void gameLoop() {
//simplifying here, you'd need to handle pausing,
//by checking it in the gameUpdate* calls
while (true) {
render();
}
}
Additionally I'd strongly recommend to eagerly initialize your variables and declare your drawables
and entities
as final. Keep in mind that the default value for booleans is false, the changes proposed above also make the second-timekeeping unnecessary so I removed the stuff for that here:
public class Game {
private boolean isRunning = true;
private boolean isPaused = false;
private long lastLoopTime = System.nanoTime();
private long curLoopTime;
private long deltaLoopTime;
private double deltaTime;
private static final int OPTIMAL_FPS = 60;
private static final int OPTIMAL_TIME = 1000000000 / OPTIMAL_FPS;
private static final int TICKS_PER_SEC = 25;
private final List<Entity> entities = new ArrayList<>();
private final List<Drawable> drawables = new ArrayList<>();
//and here's the rest ;)
-
\$\begingroup\$ Some quick questions, I am fairly new to Java.
final
means it can't be of any other implementation thanArrayList<>
in this case right?gameStateThread()
will in this case be called like a "background-process"? \$\endgroup\$Emz– Emz2014年12月04日 11:54:40 +00:00Commented Dec 4, 2014 at 11:54 -
\$\begingroup\$
private boolean isPaused = false;
that assignment is unnecessary, I know that. However what is the recommended approach? I wanted to make sure that it is implied that it has a default value. That its default value matters. Regarding therender
loop, right now it does not have an fps cap right? So with the simplest of codes (that I have now) an FPS of above thousands is not to be unexpected? Which is a bit overkill for every standard monitor. \$\endgroup\$Emz– Emz2014年12月04日 11:57:00 +00:00Commented Dec 4, 2014 at 11:57 -
\$\begingroup\$ @Emz Concerning FPS-Cap, you're right, but be aware that this is by far not meant as the final version ;) I highly recommend reading the article I linked.
final
means, you can only assign to the reference once. It's fine to explicitly state default values. And yes, the gameStateThread is a so-called background worker. When ending the game you must make sure to shut it down, else it will keep running and hog memory \$\endgroup\$Vogel612– Vogel6122014年12月04日 12:11:14 +00:00Commented Dec 4, 2014 at 12:11 -
\$\begingroup\$ With regards to final, that means I can never call
new ArrayList<>()
or any other implementation later on right? \$\endgroup\$Emz– Emz2014年12月07日 21:56:54 +00:00Commented Dec 7, 2014 at 21:56 -
\$\begingroup\$ Correct. It's not really necessary though.. why would you want to throw away the container after you decided on one... It's not like you're going to change the list's implementation at runtime \$\endgroup\$Vogel612– Vogel6122014年12月07日 22:00:19 +00:00Commented Dec 7, 2014 at 22:00
I just have a couple of superficial points for now. It's been a while since I created a game loop, so I will let someone else comment on that, but at least your busy-loop (while (isPaused == true)
) doesn't seem necessary (it will eat up a lot of resources without actually doing anything).
Boolean Checks
Instead of for example isRunning == true
you can just write isRunning
, which is more readable.
Comments
A lot of your comments are not needed, as the code is already self-explanatory, for example:
// if the game is running
// if the game is paused
// entities
// drawable
// init entities and drawables
// wait for user to unpause
// render everything
// update game
[etc]
These do not add any information, and thus actually make your code harder to read instead of easier.
Misc
- you often (but not always) have a whitespace before
()
. - magic numbers: extract numbers in your code to (static) fields. This tells us what they actually represent (eg
1000
and1000000
). - always use curly brackets, even for one line statements.
-
\$\begingroup\$ the
while (isPaused) {}
will do more stuff later. Right now it is fairly useless, but then again, isPaused can't be set to true! My current IDE is worthless at auto-adding a space between a method and its parentheses. I forget to add them manually from time to time. Thanks so far! \$\endgroup\$Emz– Emz2014年12月03日 22:13:12 +00:00Commented Dec 3, 2014 at 22:13
Entity
s shouldn't know anything about render state, so wouldn't needupdatePerFrame()
. AnddrawPerSecond()
is obviously too slow forDrawable
... why do you have two methods in the first place? \$\endgroup\$drawPerSecond ()
is used currently to render the fps once per second.updatePerFrame ()
is the standard update, called once per execution. TheupdatePerSecond ()
was me trying to get some timings done. If I wanted to time things on second basis instead of per loop execution (currently frame). \$\endgroup\$