Author: Brian A. Ree
1: Welcome Back
If you've gone through the previous two tutorials you've got a pretty solid foundation to build your game on.
Oh, btw, welcome back! Now let's get into some low level game code, finally, lol. You're NetBeans environment should
look something like this.
You can kind of see how the project has slowly grown at each iteration. New files and new functionality are added to the project.
While the projects are separated based on the tutorial the are associated with, this concept is something I'd like to reflect upon for
a little bit. Generally speaking when you're writing code it's a great approach to add new code to rock solid existing code.
This can be done by utilizing encapsulation of code properly and or by focusing on specific code goals and making sure they
are functioning properly before going on to the next task or goal. I know this sounds like something that's obvious but you have no idea
how many coders would have jumped into the game graphics by now and then gone back to optimize their drawing routines or configuration subsystem.
We've taken our time and setup a rock solid foundation that we can use again and again for other games. Once we're done with this tutorial
you will have your own base foundation that can be extended for any game you want to write... that's 2D. Ok, enough lecturing. For a quick dive into
encapsulation go here.
Let's take a second to look at where the last tutorial got us. You should have something like this visible when you run your
project in NetBeans.
So get your coding helmets on, we're going to take this simple GUI window implementation to the next level by adding
threading to initiate update, draw calls at a set interval, thereby establishing our game loop and a target frames per second.
I don't know why I said that about the coding helmets. If you need to refresh what we've done so far, go back to
tutorial one and review the Java Swing GUI implementation until you're comfortable
moving forward.
Let's start off by taking a look at the class that holds our static main. If you open up the Tutorial2Page0App.java file you should
see some familiar code from the last tutorial. However, now there are some lines that are now uncommented out. The first line creates a new instance of our
RunFrameRate class. You can see that the constructor for this class takes a reference to our MainFrame instance mf and our
FPS local static variable. You can kind of see from the constructor that the RunFrameRate class is going to drive the drawing loop in
our MainFrame instance.
MmgApiUtils.wr("Window Width: " + WIN_WIDTH);
MmgApiUtils.wr("Window Height: " + WIN_HEIGHT);
MmgApiUtils.wr("Panel Width: " + PANEL_WIDTH);
MmgApiUtils.wr("Panel Height: " + PANEL_HEIGHT);
mf = new MainFrame(Tutorial2Page0App.WIN_WIDTH, Tutorial2Page0App.WIN_HEIGHT, Tutorial2Page0App.PANEL_WIDTH, Tutorial2Page0App.PANEL_HEIGHT);
fr = new RunFrameRate(mf, FPS);
mf.setSize(Tutorial2Page0App.WIN_WIDTH, Tutorial2Page0App.WIN_HEIGHT);
mf.setResizable(false);
mf.setVisible(true);
mf.setName("Tutorial1Page0");
mf.setTitle("Tutorial1Page0");
mf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mf.GetGamePanel().PrepBuffers();
t = new Thread(fr);
t.start();
Before we take a look into our RunFrameRate class, there were some more new lines of code I wanted to go over.
At the end of our static main method you can see the lines t = new Thread(fr); t.start();, these calls setup a thread
that drives the drawing loop in our main drawing class. The creation of a Thread instance let's us know that RunFrameRate
is Runnable and can be run in a separate thread than our main execution thread. So why would we want to run some code on a different thread?
The reason why we want to do this is because the code on the separate thread can execute asynchronously with regard to the main execution thread.
Threading is a really important subject so we're not going to just glance over it. Let's take a look at the RunFrameRate class first though.
This will give us a better idea of how using threads is actually going to help us out a lot.
package com.middlemindgames.Tutorial2Page0;
import net.middlemind.MmgGameApiJava.MmgBase.MmgApiUtils;
/**
* The frame rate worker thread, runs the main loop while tracking frame rate
* information. Created on August 1, 2015, 10:57 PM by Middlemind Games Created
* by Middlemind Games
*
* @author Victor G. Brusca
*/
public final class RunFrameRate implements Runnable {
/**
* The MainFrame that houses the game, connection between JFrame, JPanel and
* the game.
*/
private final MainFrame mf;
/**
* Target frames per second.
*/
private final long tFps;
/**
* Target frame time.
*/
private final long tFrameTime;
/**
* Actual frames per second.
*/
private long aFps;
private long rFps;
/**
* Last frame stop time.
*/
private long frameStart;
/**
* Last frame start time.
*/
private long frameStop;
/**
* Frame time.
*/
private long frameTime;
/**
* Frame time difference from actual time to target time. Used to sleep the
* few milliseconds between the target time and the actual time if the
* actual time is less than the target time.
*/
private long frameTimeDiff;
/**
* Pauses the current render loop.
*/
public static boolean PAUSE = false;
/**
* Exits the current render loop.
*/
public static boolean RUNNING = true;
/**
* Constructor, sets the MainFrame, JFrame, and the target frames per
* second.
*
* @param Mf The MainFrame, JFrame for this game.
* @param Fps The target frames per second to use for the main loop.
*/
public RunFrameRate(MainFrame Mf, long Fps) {
mf = Mf;
tFps = Fps;
tFrameTime = (1000 / tFps);
MmgApiUtils.wr("Target Frame Rate: " + tFps);
MmgApiUtils.wr("Target Frame Time: " + tFrameTime);
}
/**
* Pauses the main loop.
*/
public final static void Pause() {
PAUSE = true;
}
/**
* Gets the pause status of the main loop.
*
* @return The pause status of the main loop.
*/
public final static boolean IsPaused() {
return PAUSE;
}
/**
* UnPauses the main loop.
*/
public final static void UnPause() {
PAUSE = false;
}
/**
* Stops the main loop from running.
*/
public final static void StopRunning() {
RUNNING = false;
}
/**
* Gets the running status of the main loop.
*
* @return True if running, false otherwise.
*/
public final static boolean IsRunning() {
return RUNNING;
}
/**
* Starts running the main loop but only if run is called again. Once the
* main loop exits the run method returns.
*/
public final static void StartRunning() {
RUNNING = true;
}
/**
* Gets the actual frame rate of the game.
*
* @return The game's frame rate.
*/
public final long GetActualFrameRate() {
return aFps;
}
/**
* Gets the target frame rate of the game.
*
* @return The game's target frame rate.
*/
public final long GetTargetFrameRate() {
return tFps;
}
/**
* The main drawing loop of the game.
*/
@Override
@SuppressWarnings("SleepWhileInLoop")
public final void run() {
while (RunFrameRate.RUNNING == true) {
frameStart = System.currentTimeMillis();
if (RunFrameRate.PAUSE == false) {
mf.Redraw();
}
frameStop = System.currentTimeMillis();
frameTime = (frameStop - frameStart) + 1;
aFps = (1000 / frameTime);
frameTimeDiff = tFrameTime - frameTime;
if (frameTimeDiff > 0) {
try {
Thread.sleep((int) frameTimeDiff);
} catch (Exception e) {
MmgApiUtils.wrErr(e);
}
}
frameStop = System.currentTimeMillis();
frameTime = (frameStop - frameStart) + 1;
rFps = (1000 / frameTime);
mf.SetFrameRate(aFps, rFps);
}
}
}
If you noticed this line right away, public final class RunFrameRate implements Runnable {, pat yourself on the back.
So we were right our RunFrameRate class implements a Runnable. This means that our local project class implements
the methods necessary to be used as a Runnable type of class. For more information on what implementing an interface means go
here. You can think of it as agreeing to implement
a set of methods that the interface class requires. So if you are trying to implement the Runnable interface then you must have
an implementation of the run method. Scroll down to the bottom of the RunFrameRate class and bam you can see our method implementation.
Let's take a look at the variables we are declaring in the RunFrameRate class. You can see that they all mostly have to do with measuring the current frame rate,
or keeping track of frame start and stop times. You'll notice that the loop control variables are static members RUNNING, and PAUSE. We can learn a little bit about the design
of this class by the use of static variables to control the loop. It tells us that this is a main piece of code that is important for us to be able to turn on or off without having a reference to an instance
of this class. This kind of makes sense though, our game has one heart beat - the drawing loop - and this class provides that pulse of that heart beat.
So we can see why we want some of the main control variables to be simple static variables we can access from anywhere in the game.
/**
* The MainFrame that houses the game, connection between JFrame, JPanel and
* the game.
*/
private final MainFrame mf;
/**
* Target frames per second.
*/
private final long tFps;
/**
* Target frame time.
*/
private final long tFrameTime;
/**
* Actual frames per second.
*/
private long aFps;
private long rFps;
/**
* Last frame stop time.
*/
private long frameStart;
/**
* Last frame start time.
*/
private long frameStop;
/**
* Frame time.
*/
private long frameTime;
/**
* Frame time difference from actual time to target time. Used to sleep the
* few milliseconds between the target time and the actual time if the
* actual time is less than the target time.
*/
private long frameTimeDiff;
/**
* Pauses the current render loop.
*/
public static boolean PAUSE = false;
/**
* Exits the current render loop.
*/
public static boolean RUNNING = true;
The majority of methods in the class are convenience methods for getting or setting class members so we can kind breeze over them quickly.
Not much is going on in those methods. That leaves us with the constructor and the run method. We'll talk about the constructor next.
/**
* Constructor, sets the MainFrame, JFrame, and the target frames per
* second.
*
* @param Mf The MainFrame, JFrame for this game.
* @param Fps The target frames per second to use for the main loop.
*/
public RunFrameRate(MainFrame Mf, long Fps) {
mf = Mf;
tFps = Fps;
tFrameTime = (1000 / tFps);
MmgApiUtils.wr("Target Frame Rate: " + tFps);
MmgApiUtils.wr("Target Frame Time: " + tFrameTime);
}
The constructor code is very straight forward so we'll go over it quickly. A reference to a MainFrame instance and a target frame rate are passed in.
Then a local variable is set to, 1000 / tFps, which may seem confusing. So we're storing the passed in frames per second information to a local variable tFps, or target frames per second.
If we know how many frames per second we want then we can calculate how many parts of a second each frame should be.
In this case we are using milliseconds, so we divide 1 second by the target frames per second. But remember we're working with milliseconds so the value 1 second becomes 1000 milliseconds.
Now we know exactly how long the maximum amount of milliseconds each frame can take. Any longer and the frame rate will drop below our target.
This information is important in tracking the performance of our drawing loop. You'll see a bit more of the picture when we go over the run method next.
/**
* The main drawing loop of the game.
*/
@Override
@SuppressWarnings("SleepWhileInLoop")
public final void run() {
while (RunFrameRate.RUNNING == true) {
frameStart = System.currentTimeMillis();
if (RunFrameRate.PAUSE == false) {
mf.Redraw();
}
frameStop = System.currentTimeMillis();
frameTime = (frameStop - frameStart) + 1;
aFps = (1000 / frameTime);
frameTimeDiff = tFrameTime - frameTime;
if (frameTimeDiff > 0) {
try {
Thread.sleep((int) frameTimeDiff);
} catch (Exception e) {
MmgApiUtils.wrErr(e);
}
}
frameStop = System.currentTimeMillis();
frameTime = (frameStop - frameStart) + 1;
rFps = (1000 / frameTime);
mf.SetFrameRate(aFps, rFps);
}
}
Ok, now we've gotten to the core functionality of the RunFrameRate class. As you can see the main while loop is protected by the static
loop control variable we looked at ealier. This gives us control over our game loop and lets us exit out of it at any point by setting the
RUNNING variable to false. Let's assume that the variable is set to true and our loop is working normally.
The next piece of code sets the frameStart variable to the current time in milliseconds. This gives us a reference point to figure out
how long our game loop took to complete. I want to quickly explain what a game loop is, basically it's the work that has to be done to redraw one frame
of your game. So one execution of the game loop will cause one call of your game update code, and one call of your game drawing code.
And that code has to be designed well and written well so that it can complete in a few milliseconds and run many times a second without causing
garbage collection.
So once the frameStart is recorded, we then check to see if the game loop is in a pause state. You can have different levels of control over your game drawing but
in the RunFrameRate class we're talking about the lowest level pulse of the game. So if you set RUNNING to false, your game will no longer have a drawing loop.
If you set PAUSE to true, the game will not perform it's update, redraw steps but your pulse will still be running. So the next few lines of code should make good sense.
If the frame rate is not paused, we call the Redraw method of our MainFrame class. Remember our MainFrame class is the foundation for the custom drawing
we do during a game. So it makes sense that the pulse of our game asks the main frame to redraw itself a few times a second. We're very close to having our game loop in place.
The next few lines of code is subtle so we'll go over it carefully. You can see the frameStop time is recorded and a calculation
is done that tells use how long it took to redraw the game, frameTime = (frameStop - frameStart) + 1. One thing we do here is increment the frame time by 1 millisecond.
We do this because we don't want a zero frame time so we'll make it so that the minimum frame time is at least one millisecond.
Next we calculate and store the actual frames per second, aFps = (1000 / frameTime), this tells us what our actual frame execution time was.
So what's the big deal you say? Well we want our game to run the same in different environments so we need to be able to control the speed at which the game runs, we do this by tying
the game to a set frame rate. So what happens when your game runs too fast? What happens when it runs too slow? We'll go over that next.
So what do we do when our game redraw code runs too fast and we have extra time left over in our frame? Well if we let the game speed up then it will
run differently on faster computers and we won't be able to control the user experience very well. To solve this problem we sleep the game drawing loop for the extra time
this way we keep the game loop execution fairly normalized across different computers. You can see that the difference between the maximum alotted execution time and the actual
execution time is stored here, frameTimeDiff = tFrameTime - frameTime.
if (frameTimeDiff > 0) {
try {
Thread.sleep((int) frameTimeDiff);
} catch (Exception e) {
MmgApiUtils.wrErr(e);
}
}
If we detect that the actual frame execution is less than the alooted time for one frame to execute, we then sleep for the time difference.
This is not an exact science and there will be some fluctution in the time it takes to sleep and wake the drawing thread. But it's ok, the fluctuation
in time is small and our code can now run on faster machines while maintaining the smae user experience from machine to machine.
So what happens if our redraw code is too slow? This means that each frame is taking longer than expected and the frame rate or FPS (frames per second) will now drop.
This will cause the user experience to change making their interaction with your game much slower then you anticipated. This is a more difficult situation to handle.
In order to get our frame rate back to what we want we have to do less work each frame. While you can put some logic in your code to switch to a more efficient use of
objects, memory, etc in order to restore the frame rate, you would generally design your game to run on a lowest set of hardware specifications. This sets a baseline
on how your game is going to perform, and any faster computers will be throttled to the desired frame rate by our code! Magical, we just (well almost) implemented a game loop
that can automatically handle keeping the game experience intact when it's being executed on machines must faster then we anticipated. There is actually a little bit more cool stuff
going on with the frame rates, we'll cover that next. I know how much frame rate stuff can one person take in a tutorial. It's ok though, we'll be done shortly and we'll have a rock solid
foundation to build our game on. See if you can figure our what the next snippet of code does.
frameStop = System.currentTimeMillis();
frameTime = (frameStop - frameStart) + 1;
rFps = (1000 / frameTime);
mf.SetFrameRate(aFps, rFps);
Figured it out yet? Well we can see that the frame stop time os being recorded. We can also see that some frame time is being calculated.
But why do we add a + 1 to our frame time? Well it's measured in miliseconds, and we use the frame time to determine the number of frames per
second so we don't want frameTime to ever be zero, so we say the minimum frame time will be 1 milisecond, now we never have to worry about a division
by zero edge case, cool!