So you want to make a game engine? Part 6 – Input! Need input!

Hey everyone!  I really hope you guys are enjoying this series, and as always please feel free to leave feedback and ask questions!

For those following along with development, let’s take a quick look at what we have so far:

  • We’ve discussed the purposes of a game engine and described the things it must be able to do.
  • We’ve implemented a double-buffered drawing system and implemented a Vector class.
  • We’ve written the skeleton framework of a game project created with our engine and looked at how it all works together.
  • We have a way to encapsulate the data of an in-game object and have a method of drawing it.
  • We can check for collisions between objects and simulate bouncing objects.

We’re almost at the point of being able to develop a game.  However, we’ve left out a tiny detail: we don’t have any way to interact with the program, so today, we’ll add handling for keyboard and mouse input, and also one tiny thing to GameObject that will be helpful to have.

Keyboard Input

Handling keyboard input should be simple, right?  We just check what keys are held down.  The only problem is, there’s really not a provided way to do that in Visual Basic.

The way the window reacts to keyboard events is it fires an event called KeyDown, and provides the keycode (a number that is assigned to represent a key) of the key that was pressed.  However, if you hold down the key, it repeatedly fires the event with that keycode.  Notice how if you hold down a key in Word, it prints the character, pauses for a second, and then repeats the character while the key is held down.  That’s not necessarily how we want our game engine to behave, so we’ll need to implement some sort of object that contains the held states of all of the available keys on the keyboard.  We’ll call this KeyboardState, and it’s relatively simple to implement.

KeyboardState

We can think of the KeyboardState class as a container for a data dictionary that tracks which keys are held down.  If we want to update the state of a key, we call the KeyPressed() method with the keycode of the key to change the state of.  Likewise, KeyRelease() ‘releases’ the key.  We can also get the pressed value of any keycode, and set all of the key states to False with a Reset() method.

Create a new class file and call it KeyboardState.vb.  Add it to our Engine namespace.

    Namespace Engine
        Public Class KeyboardState
        
        End Class
    End Namespace

The data structure we’ll use to store the key states is called a Dictionary.  You may recall them from your Java course (although I’m unsure exactly how similar the IST version is to the CMPSC version).  Put simply, a Dictionary stores key-value pairs that we can look up later: for example, if a Dictionary stores a list of CMPSC courses using the course number as the key and the course name as the value for a particular key, a lookup of CMPSCCourses.Item(“CMPSC 360”) will return “Discrete Mathematics in Computer Science”.  The key and value can be of any type and do not have to be the same.

For our key state dictionary, the key type is a enum provided by the .NET Framework called Windows.Forms.Keys, which contains a value for each possible key on a keyboard.  The value type will be a Boolean, indicating whether the key is recorded as pressed (True) or not (False).  Since the integer values for value keycodes is in the range 0-255, we’ll add 255 entries to our dictionary when we create it.

Let’s add the dictionary, the constructor, and the Reset() method:

    Private _heldKeys As Dictionary(Of Windows.Forms.Keys, Boolean)
    
    Public Sub New()
        _heldKeys = New Dictionary(Of Windows.Forms.Keys, Boolean)
        
        Reset()
    End Sub
    
    Public Sub Reset()
        _heldKeys.Clear()
        
        For i As Integer = 0 To 255
            _heldKeys.Add(CType(i, Windows.Forms.Keys), False)
        Next
    End Sub

The declaration of the internal dictionary and the constructor should be fairly intuitive as to what they do, but let’s take a quick moment to mention something about Visual Basic syntax here: you should (hopefully) recall the idea of typecasting in Java, or converting data from one type to another, using a syntax similar to:

    ([new data type]) [variable or value];
    
    //example: char x = (char)25;

In Visual Basic, most typecasting is implicitly done, meaning that you don’t have to worry about having to convert between types most of the time as the compiler can infer what you want to do (most of the time).  If we want to do it explicitly, typecasting is done with a function called CType() that returns an object of the desired type using this syntax:

    CType([variable or value], [new data type])
    
    ‘example: Dim x As Character = CType(25, Character)

In the Reset() method, we cast the value of our counter variable i to type Windows.Forms.Keys.  Recall that this is a enum that contain each possible key on a keyboard, and each key has a unique integer value.  This typecast converts the integer code of the key into its equivalent Windows.Forms.Keys representation, and stores this in the dictionary with an initial pressed value of False.  Storing the key states with a key type of Windows.Forms.Keys is extremely helpful, since we now don’t have to worry about remembering the integer keycodes for each key.

What we’re doing here is populating the dictionary of key states: we add an entry for each possible key, and set the initial value of each one to False.  This way, we don’t have to worry about exceptions being thrown if an entry for a particular key doesn’t exist.

The remaining three methods for this class change the state of entries in this dictionary and look up values.  They’re each one-liners and are straightforward enough that they don’t really need explained, as the name of each one pretty much indicates what it does.

    Public Function GetKeyState(key As Windows.Forms.Keys) As Boolean
        Return _heldKeys.Item(key)
    End Function
    
    Public Sub KeyPressed(key As Windows.Forms.Key)
        _heldKeys.Item(key) = True
    End Sub
    
    Public Sub KeyReleased(key As Windows.Forms.Key)
        _heldKeys.Item(key) = False
    End Sub

KeyboardState is done!  Let’s add this to our game window’s skeleton code and incorporate it into the rest of the engine!

Adding Keyboard Handling to the Game Engine

Open up your main window’s code view (Form1.vb, unless you renamed it).  Right underneath our engine variable declarations at the top (for the buffer, stopwatch, etc), add one for the keyboard state, and another that we’ll explain in a bit.

    Private _keyboard As KeyboardState
    Private _useESCToExitGame As Boolean

We initialize this at the end of our Form1_Load() method, but before the call to Initialize():

        ...
        
        _keyboard = New KeyboardState
        _useESCToExitGame = True
        
        Initialize()
    End Sub

Next, we need to add event handlers for keyboard events to our window.  Recall in part 3 that in our Form1_Load(), we include the statement Me.KeyPreview = True.  This allows the window to handle keyboard events while the game window is the active window.  However, without event handlers, it still won’t react to them.  So let’s fix that!

Open the event list (with the lightning bolt icon on it) and choose KeyDown.  A new method called Form1_KeyDown() is generated.  Repeat the process for the KeyUp event.  They may be created near the end of the file, so if you want to group all the engine stuff together feel free to move those methods around.

Let’s start with KeyDown.  Many games allow the user to exit when ESC is pressed.  Sometimes, the developer may not want this behavior, or may want to disable or re-enable it depending on the context of the game; for example, while in an inventory screen, it makes more sense for ESC to close the inventory rather than closing the game itself.

    If e.KeyCode = Keys.Escape And _useESCToExitGame Then
        If MessageBox.Show("Are you sure you want to exit?", "Confirm Exit", MessageBoxButtons.YesNo, MessageBoxIcon.Question) = Windows.Forms.DialogResult.No Then
            'user cancelled
        Else
            'user did not cancel
            isGameRunning = False
        End If
    Else
        _keyboard.KeyPressed(e.KeyCode)
    End If

When the use-ESC flag is True, pressing ESC will show the ‘confirm exit’ dialog.  All other keys, including ESC if the flag is False, will have their pressed state changed to True in the keyboard state object.

KeyUp does the opposite: it sets the pressed state of the key pressed to False and doesn’t do anything special for certain keys.

    _keyboard.KeyReleased(e.KeyCode)

One more thing before we can handle keypresses: we need to create a method that handles user input and add a call to it within our game loop.  This function will be called ProcessInput(), and must get called before the GameLogic() function.

Create an empty method called ProcessInput() underneath Initialize() and before GameLogic(), and add the call to the game loop right before the call to GameLogic():

    ...
    
    ProcessInput()
    GameLogic()
    Render()
    
    ...

There we have it: keyboard input is now implemented and ready to use in our engine!  At this point, we have a very basic engine that can actually be used in its current state for simple games: some examples I can think of include Pong, Snake, and (if you’re really clever) a simple platformer.

However, many games also include support for input using the mouse.  This isn’t as involved as keyboard input, but there’s a couple subtle things we should consider.

Handling Mouse Input

Luckily for us, the .NET Framework provides us with convenient ways to access the current state of the mouse.  The Windows.Forms.Form class, which is the base class of all windows (including our game window) includes two helpful mouse-related properties: MousePosition and MouseButtons.  MouseButtons’s value is the ID of the button that is pressed, and valid IDs are listed in the enum Windows.Forms.MouseButtons.  MousePosition is the location of the mouse pointer on the screen.

Pay careful attention to the wording here: MousePosition is not the location of the pointer within the drawing area of our window.  We can account for this and calculate the position of the pointer relative to the upper-left corner of the drawing area of our window.  This involves a bit of trickery and I actually had to look up a better way to do this as I was writing this up.  My previous implementation of this, which was done separately from the engine as part of a game, was to use hard-coded values to correct the offset from the size of the window borders.  The problem there was that those sizes can differ depending on the user’s Windows theme and configuration.

Here’s the function that calculates the relative position of the pointer, adapted from here (I’m inserting this right after GameLoop()): https://ivision.wordpress.com/2007/01/05/title-bar-height-and-form-border-width-of-net-form.

    Public Function GetPointerLocation() As Point
        Dim BorderWidth As Integer = (Me.Width – Me.ClientSize.Width) / 2
        Dim TitlebarHeight As Integer = SystemInformation.CaptionHeight + BorderWidth
        Return New Point(MousePosition.X – Location.X – Cursor.HotSpot.X – BorderWidth, MousePosition.Y – Location.Y – Cursor.HotSpot.Y – TitlebarHeight)
    End Function

Another thing to consider is that in some games, the mouse pointer itself should not be visible; for example, in a shooting game you may want to draw a crosshair at the mouse location instead of the default pointer.  Thus, we should add a way to set the visibility state of our pointer.

Again, the .NET Framework comes to the rescue: the window contains a property called Cursor, which includes the methods Show() and Hide().  However, it doesn’t contain a flag to tell us whether it is visible or not, which may be helpful to know.  We can add that flag ourselves, and add a small bit of logic to the game loop that checks when the value of our flag is changed.

At the top of the file near our other engine variables, add a flag variable:

    Private _hidePointer As Boolean

We can check when the value of this changes by adding another Boolean to GameLoop() that is set to the value of _hidePointer when the game starts.  Above the call to ProcessInput(), we simply need to check whether the current state matches the previous state; if it doesn’t, we need to show or hide the pointer depending on the value.

    ...
    
    If _hidePointer Then Cursor.Hide()
    Dim prevMouseHiddenState As Boolean = _hidePointer
    
    While isGameRunning
        sTime = _stopwatch.ElapsedMilliseconds
        
        If Not _hidePointer = prevMouseHiddenState Then
            prevMouseHiddenState = _hidePointer
            If _hidePointer Then Cursor.Hide() Else Cursor.Show()
        End If
        
        ProcessInput()
        
        ...

That was simple enough!  Now, we can check whether the pointer is visible or not by checking the value of _hidePointer, and we can change that value to show or hide the cursor.  With that, input handling is complete, and the addition of mouse input opens up a lot more possibilities of possible games that can be created with our simple engine, such as a pop-the-bubbles sort of game (or other types of the sort of game that moms play on Facebook), an Angry Birds clone, or even a tanks game (in which the player controls a tank with the keyboard and clicks to shoot at other tanks).  Also, making something that follows your mouse can be kind of neat, which could be clicking and dragging a piece of cheese to lead a mouse through a dangerous maze.

Let’s add a tiny thing to GameObject while we’re speaking of things following the mouse.

Calculating the CenterPoint of a GameObject

Being able to access and change the center point of an object can be pretty useful; for example, the tiny + at the center of a crosshair should be placed at the mouse position.  While we can do that without a CenterPoint property with a bit of math instead, remember: I hate math and you probably do too, so let’s just do it once and get it over with 🙂

    Public Property CenterPoint As PointF
        Get
            Return New PointF(X + ((w * scaleW) / 2), Y + ((h * scaleH) / 2))
        End Get
        Set(value As PointF)
            X = value.X – ((w * scaleW) / 2)
            Y = value.Y – ((h * scaleH) / 2)
        End Set
    End Property

The x-coordinate of the centerpoint of the object is calculated by its x-location, plus half of its width, taking into account the scaling factor for the width.  Same idea for the Y-coordinate, replacing width with height.  Don’t forget that we need to consider the scaling factor every time we want to do something involving the size of the object!

With that, let’s move on to our example project.  We don’t have enough time to make one of the games listed above, but if somehow you have no homework or anything to study for, feel free to experiment on your own! (or possibly switch lives with me and do my mountain of homework)

Example Project 6: Input Test

Goal: Move an object using the arrow keys, and make a crosshair image follow the mouse pointer.  Display a message box when the left mouse button is clicked.

We’ll run through this quickly as this post is getting pretty long, but at the same time I want to introduce using something that we implemented previously, but never actually used yet: multiple frames in a single Sprite.

Most games switch the image used to display a character when they change the direction in which they are moving, so we can create and use four images for our simple object: one for looking up, one for looking down, one for looking left, and one for looking right.  We’ll put them in the same Sprite and change which one is displayed by adjusting the value of Sprite.CurrentFrame.  We’ll revisit frame-based Sprites and actual animation in part 8; this is mainly to get us thinking about frames and graphics topics.

Download the example images here and add them to your project: (for info on adding them, see the previous post)

player_up.png player_down.png player_left.png player_right.png crosshair.png

We’ll need three game variables declared before the Initialize() method: one for the crosshair, one for the player, and an integer for the player’s movement speed.

    Dim crosshair As GameObject
    Dim player As GameObject
    Dim moveSpeed As Integer = 4

Let’s start initializing things!

    Private Sub Initialize()
        player = New GameObject(Image.FromFile(Application.StartupPath & "\gfx\player_up.png"), New PointF(100, 100))
        
        player.Sprite = New Sprite(New Image() {
            Image.FromFile(Application.StartupPath & "\gfx\player_up.png"),
            Image.FromFile(Application.StartupPath & "\gfx\player_down.png"),
            Image.FromFile(Application.StartupPath & "\gfx\player_left.png"),
            Image.FromFile(Application.StartupPath & "\gfx\player_right.png")})

We initialize the player object with a single image, only because we don’t yet have a constructor for GameObject accepting an image array as a parameter.  We’ll add that in part 8, but for now, we can simply replace the sprite with a new Sprite object, which does accept image arrays.  Our sprite has four frames, with frame 0 representing the up sprite, 1 = down, 2 = left, and 3 = right.

        ...
        
        player.Velocity = New Vector(0, 0)
        player.IsVelocityOn = True
        
        crosshair = New GameObject(Image.FromFile(Application.StartupPath & "\gfx\crosshair.png"), New PointF(0, 0))
        crosshair.CenterPoint = GetPointerLocation()
        _hidePointer = True
    End Sub

The rest of Initialize() is nothing we haven’t covered already so I won’t go too far into detail.  Notice that we’re setting our _hidePointer flag to True: we want to see a crosshair, not an arrow.

Next up is our new friend ProcessInput().   Here, we check whether the arrow keys are pressed and adjust the velocity vector of the player accordingly.  We also test if the mouse button is pressed.

    Private Sub ProcessInput()
        If _keyboard.GetKeyState(Keys.Up) Then
            player.Velocity.Y = -moveSpeed
            player.Sprite.CurrentFrame = 0
        ElseIf _keyboard.GetKeyState(Keys.Down) Then
            player.Velocity.Y = moveSpeed
            player.Sprite.CurrentFrame = 1
        Else
            player.Velocity.Y = 0
        End If

First, we check whether either the Up or the Down key is pressed.  We adjust the velocity of the player object and the frame of the sprite that is shown, depending on whether the player is moving up or down.  If neither is pressed, we set the Y-velocity to 0.

        ...
        
        If _keyboard.GetKeyState(Keys.Left) Then
            player.Velocity.X = -moveSpeed
            player.Sprite.CurrentFrame = 2
        ElseIf _keyboard.GetKeyState(Keys.Right) Then
            player.Velocity.X = -moveSpeed
            player.Sprite.CurrentFrame = 3
        Else
            player.Velocity.X = 0
        End If

The same concept applies for the Left and Right keys.

        ...
        
        If MouseButtons = Windows.Forms.MouseButtons.Left Then
            Cursor.Show()
            MsgBox("Bang!")
            Cursor.Hide()
        End If
    End Sub

Finally, we check whether the left mouse button has been pressed.  If it has been pressed, we show the cursor, pop up a message box, and then hide the cursor when the message box is closed.

The GameLogic() function is next.  One thing we’ll do this time is keep the player object completely within the window by preventing it from being able to move off-screen.

    Private Sub GameLogic()
        player.Update()
        
        If player.X <= 0 Then player.X = 0
        If player.Right >= DisplayRectangle.Width Then player.Right = DisplayRectangle.Width
        
        If player.Y <= 0 Then player.Y = 0
        If player.Bottom >= DisplayRectangle.Height Then player.Bottom = DisplayRectangle.Height

As usual, we update the location of the player object.  We then check whether it’s past any of the boundaries of the window, moving it back within bounds if it is.

        ...
        
        crosshair.CenterPoint = GetPointerLocation()
    End Sub

Finally, we center the crosshair at the mouse pointer location.

One more function to go: the Render() method.  Just add draw statements for player and crosshair here (and of course the ClearBuffer() call at the beginning and the Render() call at the end) and we’re done!

Run the program.  You should be able to move an object with the arrow keys, which should change its image depending on the direction it’s moving.  Finally, clicking with the left mouse button should pop up a message.

If you’ve followed the coding adventure for this long, congratulations!  You have a simple 2D game engine that you wrote and that you can make games with!  The rest of the posts will be improving on this, since there’s a lot more we can do with it.  If you decide to stick around, we’ll cover some additional graphics topics that have been repeatedly foreshadowed 😉 And, at the very end we’ll make a full game!  I’m still not going to spoil that one yet though.

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

Question: When moving up/down and left/right (in other words, moving diagonally), we see the sprite as either facing left or right, not up or down. Can you figure out why? Is there a solution to this?

Challenge: Make an object that moves toward the mouse pointer.  Hint: use comparisons to test where the object is relative to the mouse location.

Previous Question Answer:
Right now, our engine only has support for sprites that are made up of images.  A bit of foreshadowing here: we’ll be adding support for using primitive shapes instead (circles and rectangles) later on.  Can you think of how this might be done?  Think about how we go about drawing the sprite currently, that should provide a hint 🙂

When the Draw() method of a GameObject is called, it internally calls the Draw() method of the Sprite associated with it.  Recall that the Sprite has no knowledge of the object’s location and size; this information is given directly to the Sprite.Draw() method.  Thus, we could introduce a mechanism to change the type of sprite to be either an image, a circle, or a rectangle, and revise the Sprite.Draw() method to account for the sprite to be drawn.  Stay tuned for part 9!

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

Leave a Reply