So you want to make a game engine? Part 3 – The brains of the game

[Sorry, started posting a week before the third blogging period began ._.]

Welcome back everyone!  Today’s going to be exciting: we’re going to draw something!

By now, if you’ve been following along, you should have a buffer system implemented, and a Vector class (I promise there will be less math today).  So now it’s time to actually do something with our buffer!

Well, later we will.  Before we can draw anything, we need to consider how a game program runs.

There’s a lot of stuff that gets done before you can start playing a game: for example, the game has to prepare the game window and initialize objects and resources used by the game.  The engine is responsible for setting up the window and initializing the graphics buffer, and then it calls any game-specific code to set up everything else before transferring control to the game loop.  We’re going to outline the control flow of the program from start to finish and create a simple project that displays the classic “Hello world!” text in the window.  This will cover setting up the window, loading image resources from files, a basic game loop, and how to draw images using our buffer.

When a game starts, one of the first things that gets done is creating and setting up the window and sets up other internal things used by the game engine.  It then calls any game-specific initialization code, which usually prepares elements of the game such as textures, entities, etc.  Once everything is initialized and ready, it transfers control to the game loop, which runs until the program is directed to exit.  From there, it calls any game code that disposes of memory resources used by the game (closing files, unloading textures from memory, etc.), and then finally stops.  We’ll run through each of these steps in this post.

Preparing the game window

In the last post, we ignored the Windows Form that was created with the project, and instead moved on and created new class files.  Let’s revisit that form, but we don’t need to add any controls to it: just go straight into the code editor by double-clicking anywhere inside the form.

The event handler created for the window's loading event

The event handler created for the window’s loading event

A Form1_Load() method is generated for us: this handles the form’s load event, so anything placed in here is executed while the form is loading, but before it is made visible to the user.  This seems like a good place to set everything up!

By personal preference and experimentation, I’ve found that a window size of 800 by 480 is a decent size.  Most windowed games typically are placed in the center of the screen, and do not allow the user to resize or maximize the window.  We could easily take care of all of these by setting the values in the Properties window in Design View, but where’s the fun in that?

    Me.Width = 800
    Me.Height = 480
    
    Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedSingle
    Me.MaximizeBox = False
    
    Me.StartPosition = FormStartPosition.Manual
    Me.Left = (Screen.PrimaryScreen.WorkingArea.Width / 2) – (Me.Width / 2)
    Me.Top = (Screen.PrimaryScreen.WorkingArea.Height / 2) – (Me.Height / 2)

Here, we set the window’s size, set its border style to a fixed-size style, removed the maximize button, and centered the window on the screen.

Before we move on, let’s take a look at setting the window size.  Notice that on your computer, each window has a border around it.  When we set the size of our window in the code above, it’s resizing the entire window, which includes space taken up by the borders.  This means the actual drawing area inside the window is slightly smaller than our specified size of 800×480.  We can fix that by first setting the size as we did above, then checking the difference between the desired size and the actual size of the drawing area, and adjusting accordingly.

    Me.Width = 800
    Me.Height = 480
    
    Dim dWidth As Integer = 800 – Me.DisplayRectangle.Width
    Dim dHeight As Integer = 480 – Me.DisplayRectangle.Height
    
    Me.Width = 800 + dWidth
    Me.Height = 480 + dHeight
    
    Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedSingle
    ...

Now, our drawing area has the desired size of 800×480.  Perfect!  Just three more tiny things before we move on:

    ...
    Me.KeyPreview = True
    Me.DoubleBuffered = True
    
    Me.Text = "Hello World!"

We set the value of KeyPreview to True: this is what will allow the window to capture and react to keyboard events.  This is critical in games requiring keyboard input, but we won’t be worrying about handling that input just yet.  We set the window’s DoubleBuffered property to True to optimize the drawing behavior of the window for double buffering, and then we set the text of the window title bar.  You can make this whatever you want.

After the window is sized and adjusted properly, we can initialize our drawing buffer, as well as a couple of other things that will be used in the game loop.  At the top of the file just inside the Form1 class declaration, add these declarations:

    Private _buffer As Engine.Buffer
    Private _stopwatch As Stopwatch
    Private _sFPS As Integer = 60
    Private isGameRunning As Boolean = False
    Private isShuttingDown As Boolean = False

The buffer, as we discussed more in-depth in the previous post, is what we’ll be doing all of our drawing with.

The stopwatch is used for timing to control the speed of the game loop.  We also set the game’s FPS (frames per second) here, which controls how many times the game loop should run per second (sometimes referred to as a game ticks).  We’ll revisit these momentarily.

Finally, we have some state variables that tell us if the game is running or if the game is preparing to exit.  The isGameRunning variable is our condition for the game loop, which exits once this becomes False.

Let’s return to our form’s load event handler so we can prepare our buffer and stopwatch for use.

    ...
    Me.Text = "Hello World!"
    
    _buffer = New Buffer(Me)
    _stopwatch = New StopWatch()

We create our buffer and link it to our window, so that when we call the buffer’s Render() method, its contents are drawn to our window.

So our window is ready to go!  It’s all set up and ready for action.  A couple more quick things to lay the groundwork for our game loop discussion.

After the form load event handler, add four empty methods: Initialize(), GameLoop(), GameLogic(), and Render().  You can declare them all as Private Sub, there’s no reason to make them public.

    Private Sub Initialize()
    
    End Sub
    
    Private Sub GameLoop()
    
    End Sub
    
    Private Sub GameLogic()
    
    End Sub
    
    Private Sub Render()
    
    End Sub

Finally, create an event handler for the window’s Shown event:

Adding an event handler for the Shown event

Adding an event handler for the Shown event

Just add the single statement below to this handler:

    GameLoop()

Now, after our window is loaded, sized, positioned, and shown, it’s ready to go.  Now the fun part: how do we make it do something?

The Game Loop

Robert Nystrom, former software engineer at Electronic Arts and author of the wonderful game programming resource, Game Programming Patterns, sums up what a game loop is: “A game loop runs continuously during gameplay. Each turn of the loop, it processes user input without blocking, updates the game state, and renders the game. It tracks the passage of time to control the rate of gameplay.” [http://gameprogrammingpatterns.com/game-loop.html]

The ability to process input “without blocking” is critical.  Many computer programs wait for input before continuing execution, or simply ‘listen’ and wait for events which it can react to.  A game, however, is always running.  There’s always something happening within the world that needs to continue running whether the player is doing anything or not.  Thus, the act of waiting for user input should not block the rest of the game from executing.  At the same time, the game loop itself shouldn’t block the main application thread from handling messages and events from the operating system – if that happens, the application usually gets flagged as “not responding” and it doesn’t end well for anyone involved.

Nystrom discusses a couple of possible ways to implement a game loop, but I’ll cover the first two here.  The one I use and prefer I learned about before I discovered the Game Programming Patterns book, and while it may have its flaws it’s always worked well enough for me.

The first one involves no time control.  Basically: here’s the code, run it as fast as possible.  Let’s take a look at that kind of implementation:

    While isGameRunning
        ProcessInput()
        GameLogic()
        Render()
    End While

It’s pretty simplistic, but there’s something wrong here.  The fact that there is no timing control here means that it’s entirely likely the game will run too fast or too slow.  A user playing this game on grandma’s old computer is going to see performance suffer, while a gamer who just built himself a new high-end computer will see the game running so fast that he or she can’t properly play it.

In the old days of video games, back in the NES era, games were written like this.  More accurately, the developers knew the speed of the processor and programmed accordingly.  They had to be very careful to make sure they didn’t try to do too much or too little in a single game tick, which could throw off the game’s performance.

Now, we have computers that have clocks for keeping track of time.  The ability to track time can help ensure that a game loop tick takes the same amount of time on grandma’s computer as it does on a high-end gaming computer.

So with that, let’s write our game loop.  First things first, we need to calculate our timing interval for the game loop.  We defined the game’s FPS to be 60fps; we can calculate the length of time for one frame, which is (1000 / FPS) = (1000 / 60) = 16.67ms.  We’ll also need to store the current time at the start of the game tick so we can determine how long we need to wait after the game’s logic and rendering functions are done before we go to the next tick of the game loop.  Additionally, we need to make sure that the loop allows for handling events from the operating system.

So with all that in mind, here’s the contents of GameLoop():

    Dim sInterval As Integer = 1000 / _sFPS
    Dim sTime As Long = 0
    
    isGameRunning = True
    _stopwatch.Start()
    
    While isGameRunning
        sTime = _stopwatch.ElapsedMilliseconds
        ‘ProcessInput() – commented out since we aren’t dealing with input yet
        GameLogic()
        Render()
        Application.DoEvents()
        Do While _stopwatch.ElapsedMilliseconds – sTime < sInterval
            If Not isGameRunning Then Exit Do
        Loop
    End While
    
    isShuttingDown = True
    Shutdown()
    
    Close()

At the start of each tick of the game loop, we capture the time that has elapsed since starting the stopwatch.  After the logic and rendering functions complete, we allow the application to handle messages from the operating system (that’s what the Application.DoEvents call is for), and then wait until the desired interval (16ms) has passed since the start of the tick.  The exit condition for the game loop is when isGameRunning becomes False.  When the game loop does exit, it sets the isShuttingDown flag to True, calls any shutdown code needed, and finally closes the window.

Already, we see a problem: nothing sets isGameRunning to False!  Even if you close the window with the X button, the game loop will continue to run and the program will need killed with Task Manager.  Infinite loops are no bueno.

Fixing this requires us to add one more event handler: we need to handle the window’s FormClosing event.  Add it in the same way we added the Shown event handler.

Many computer games display some sort of prompt when you try to exit, asking if you’re sure you really want to do that.  We can add that easily using the MessageBox.Show() method.

    If Not isShuttingDown
        If MessageBox.Show("Are you sure you want to exit?", "Confirm Exit", MessageBoxButtons.YesNo, MessageBoxIcon.Question) = Windows.Forms.DialogResult.No Then
            e.Cancel = True
        Else
            isGameRunning = False
            e.Cancel = True
        End If
    End If

The check on the value of isShuttingDown is important: this ensures that the exit prompt is not shown while the game is already shutting down, which would result in another fun infinite loop where it asks if you want to exit every time it tries to close the window.

Note that both responses to the message box include the statement e.Cancel = True.  This prevents the window from closing when the event handler finishes running, and allows the game to either continue running or run its shutdown functions before closing, depending on the choice made.

Hello, world!

At this point, we’ve done a lot.  We have the core framework of our game project made, and a game loop written.  Granted, the only thing it can do still is draw images, so let’s go ahead and do that before wrapping things up, just to make sure we’ve got everything working right.

Go ahead and find an image file on your computer, or download one.  I’m using this fancy one here
dang, that is a sweet world.

Add this file to your Visual Studio project: right-click the project in the Solution Explorer (it’s the item at the top), go to Add, and choose Existing Item.  Be sure to change the file type to Image Files.

Adding a file to a project

Adding a file to a project

Find your image and add it to your project.  Under the Properties panel, change Copy To Output Directory to Copy Always.  This ensures that the image file will always be copied to the build directory when we compile the program.

This ensures that the file is copied to the output directory when we build the project

This ensures that the file is copied to the output directory when we build the project

Add a class variable declaration above our Initialize() method from earlier.  Call it whatever you’d like.  Then, inside the Initialize() method, we’ll go ahead and load our image resource.

    ...
    
    Private world As Image
    
    Private Sub Initialize()
        world = Image.FromFile(Application.StartupPath & "\earth-with-shimmer.jpg")
    End Sub
    
    ...

Finally, in our Render() function, add the following:

    _buffer.ClearBuffer()
    _buffer.DrawImage(world, New Point(0, 0), DisplayRectangle.Size)
    _buffer.Render()

We clear the buffer, and draw our image to the buffer with the upper-left corner of the image at point (0, 0).  We draw it with the same size as the DisplayRectangle of the window, so that it perfectly fits inside the form.  Finally, it is rendered to the window and we get the following result:

Hello, world!

Hello, world!

Now that, my friends, is far better than your average System.out.println(“Hello World!”) example.

That’s it for today!  We’ve gotten through a lot of stuff and we now have the skeleton code that we’ll be using in future projects.  Next time, we’ll add objects to our engine!  Objects that can move on their own!  Woah!

You can download the code from today’s work from here: Dropbox

Question: To keep things simple, we’ve prevented the user from being able to resize the window. What would happen if we left the window resizable and made it bigger? Would the Render() method still draw our image correctly?

Challenge: No challenge here!

Previous Challenge Answer: Last time, I asked you to add conversion functions to the Vector class for converting between radians and degrees.  Here they are!

    Public Shared Function RadToDeg(radians As Double) As Double
        Return radians * (180 / Math.PI)
    End Function
    
    Public Shared Function DegToRad(degrees As Double) As Double
        Return degrees * (Math.PI / 180)
    End Function

This entry was posted in Game Designers and Developers. Bookmark the permalink.

Leave a Reply