In this chapter we will start developing our game engine by creating the game loop. The game loop is the core component of every game. It is basically an endless loop which is responsible for periodically handling user input, updating game state and rendering to the screen.
You can find the complete source code for this chapter here.
The basis
The following snippet shows the structure of a game loop:
while (keepOnRunning) {input();update();render();}
The input method is responsible of handling user input (key strokes, mouse movements, etc.). The update method is responsible of updating game state (enemy positions, AI, etc..) and, finally, theSo, is that all? Are we finished with game loops? Well, not yet. The above snippet has many pitfalls. First of all the speed that the game loop runs at will be different depending on the machine it runs on. If the machine is fast enough the user will not even be able to see what is happening in the game. Moreover, that game loop will consume all the machine resources.
First of all we may want to control separately the period at which the game state is updated and the period at which the game is rendered to the screen. Why do we do this? Well, updating our game state at a constant rate is more important, especially if we use some physics engine. On the contrary, if our rendering is not done in time it makes no sense to render old frames while processing our game loop. We have the flexibility to skip some frames.
Implementation
Prior to examining the game loop, let's create the supporting classes that will form the core of the engine. We will first create an interface that will encapsulate the game logic. By doing this we will make our game engine reusable across the different chapters. This interface will have methods to initialize the game assets (init), handle user input (input), update game state (update) and clean up the resources (cleanup).
As you can see, there are some classes instances which we have not defined yet (Window, Scene and Render) and a parameter named diffTimeMillis which holds the milliseconds passed between invocations of those methods.
Let's start with the Window class. We will encapsulate in this class all the invocations to GLFW library to create and manage a window, and its structure is like this:
As you can see, it defines some attributes to store the window handle, its width and height and a callback function which will be invoked nay time the window is resized. It also defines an inner class to set up some options to control window creation:
compatibleProfile: This controls wether we want to use old functions from previous versions (deprecated functions) or not.
fps: Defines the target frames per second (FPS). If it has a value equal os less than zero it will mean that we do not want to set up a target but either use monitor refresh that as target FPS. In order to do so, we will use v-sync (that is the number of screen updates to wait from the time glfwSwapBuffers was called before swapping the buffers and returning).
height: Desired window height.
width: Desired window width:
ups: Defines the target number of updates per second (initialized to a default value).
Let's examine the constructor of the Window class:
We start by setting some window hints to hide the window and set it resizable. After that, we set OpenGL version and set either core or compatible profile depending on window options. Then, if we have not set a preferred width and height we get the primary monitor dimensions to set window size. We then create the window by calling the glfwCreateWindow and set some callbacks when window is resized or to detect window termination (when ESC key is pressed). If we want to manually set a target FPS, we invoke glfwSwapInterval(0) to disable v-sync and finally, we show the window and get the frame buffer size (the portion of the window used to render()).
The rest of the methods of the Window class are for cleaning up resources, the resize callback, some getters for window size and methods to poll events and to check if the window should be closed.
The Engine class, receives in the constructor the title of the window, the window options and a reference to the implementation of the IAppLogic interface. In the constructor it creates instance of the Window, Render and Scene classes. The cleanup method just invokes the other classes cleanup resources. The game loop is defined in the run method which is defined like this:
The loop starts by calculating two parameters: timeU and timeR which control the maximum elapsed time between updates (timeU) and render calls (timeR) in milliseconds. If those periods are consumed we need either to update game state or to render. In the later case, if the target FPS is set to 0 we will rely on v-sync refresh rate so we just set tha value to 0. The loop starts by polling the events over the window, after that, we get current time in milliseconds. After that we get the elapsed time between update and render calls. If we have passed the maximum elapsed time for render (or relay in v-sync), we process user input by calling appLogic.input. If we have surpassed maximum update elapsed time we update game state by calling appLogic.update. we have passed the maximum elapsed time for render (or relay in v-sync), we trigger render calls by calling render.render.
At the end of the loop we call the cleanup method to free resources.
A little bit note on threading. GLFW requires to be initialized from the main thread. Polling of events should also be done in that thread. Therefore, instead of creating a separate thread for the game loop, which is what you would see commonly in games, we will execute everything from the main thread. This is whey we do not create new Thread in the start method.
Finally, we just simplify the Main class to this:
packageorg.lwjglb.game;importorg.lwjglb.engine.*;importorg.lwjglb.engine.graph.Render;importorg.lwjglb.engine.scene.Scene;publicclassMainimplementsIAppLogic {publicstaticvoidmain(String[] args) {Main main =newMain();Engine gameEng =newEngine("chapter-02",new Window.WindowOptions(), main);gameEng.start(); } @Overridepublicvoidcleanup() {// Nothing to be done yet } @Overridepublicvoidinit(Window window,Scene scene,Render render) {// Nothing to be done yet } @Overridepublicvoidinput(Window window,Scene scene,long diffTimeMillis) {// Nothing to be done yet } @Overridepublicvoidupdate(Window window,Scene scene,long diffTimeMillis) {// Nothing to be done yet }}
We just create the Engine instance and start it up in the main method. The Main class also implements the IAppLogic interface which by now is just empty.