So you want to make a game engine? Part 9: Shapes and Object-Oriented Programming

[I have no idea why this didn’t post when I scheduled it for Tuesday… also the last post may be a little late due to an insane amount of work for other classes I need to do.]

Hey everyone!  I really hope you guys are enjoying this series, and as always please feel free to leave feedback and ask questions!  This is the last week for these posts, and the last one will be putting everything together to build a game!

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 can detect and handle input from the mouse and keyboard.
  • We can draw objects with rotation applied, as well as check for collisions accurately between objects with rotation applied.
  • We can created animated objects by using multiple images in a Sprite object and using a thread to change the current frame of animation that is drawn.

As mentioned before, the core of our game engine is done, and at this point we’re adding some neat graphics tricks before we start building our game, which is the topic of the next post! 😀

Today we’re going to add some more functionality to our Buffer class, add the ability to use shapes rather than images in our Sprite class, and discuss a couple object-oriented programming concepts that can be used in game development.

Back to the drawing board

Right now, the only thing our Buffer does is draw images.  We provide the drawing function with the image to be drawn, the location at which to draw it, and optionally the size at which we want to draw it.  Drawing a shape is similar, but rather than providing an image, we just give it the colors we want to use when drawing the shape.  We’ll add functions to draw circles (or, more technically, ellipses), rectangles, and text to our Buffer.

Open up the Buffer.vb file and let’s get started!  First we’ll start with a simple rectangle.  To draw a rectangle (most shapes, actually), we’ll need to provide the drawing function with the location, size, the color of the outline, and the color to fill the rectangle with.  We’ll make the fill color optional.

    Public Sub DrawRect(r As RectangleF, outline As Color, Optional fill As Color = Nothing)
        If Not fill = Nothing As Not fill = Color.Transparent Then _drawBuffer.Graphics.FillRectangle(New SolidBrush(fill), r.X, r.Y, r.Width, r.Height)
        _drawBuffer.Graphics.DrawRectangle(New Pen(outline), r.X, r.Y, r.Width, r.Height)
        End Sub

First, if there was a fill color provided and if it’s not the transparent color, then we’ll draw a filled rectangle with that color.  Then we draw an unfilled rectangle (only the outline) using the outline color provided.  We draw the outline after we draw the fill, so that the outline of the shape appears ovr top of the filled portion of the shape.

Drawing an ellipse is very similar to drawing a rectangle; in fact, the only difference is replacing the word Rectangle with the word Ellipse in the Graphics function calls.

    Public Sub DrawEllipse(r As RectangleF, outline As Color, Optional fill As Color = Nothing)
        If Not fill = Nothing As Not fill = Color.Transparent Then _drawBuffer.Graphics.FillEllipse(New SolidBrush(fill), r.X, r.Y, r.Width, r.Height)
        _drawBuffer.Graphics.DrawEllipse(New Pen(outline), r.X, r.Y, r.Width, r.Height)
    End Sub

Finally, we’ll add support for drawing text.  Text works a little bit differently than a shape: the size of the drawn text depends on the length of the string to draw, so we can’t really tell it to draw at a particular size.  The parameters for drawing text are the string to draw, the font to use when drawing it, the location on the screen to draw it at, and the color of the text.

    Public Sub DrawText(t As String, f As Font, c As Color, at As PointF)
        _drawBuffer.Graphics.DrawString(t, f, New SolidBrush(c), at)
    End Sub

Before using this function to draw text, you’ll have to define and initialize a Font object to provide as an argument.  We’ll make things a little more convenient by providing getters for the default system UI fonts.  We’ll provide getters for two common system fonts: the default font that is used in most applications (on Windows 7, this is Segoe UI 11pt) and the font that is used in the title bar of windows (this is a larger size of Segoe UI, but I don’t remember the exact size)

    Public ReadOnly Property SystemUIFont As Font
        Get
            Return SystemFonts.DefaultFont
        End Get
    End Property
    
    Public ReadOnly Property SystemTitleFont As Font
        Get
            Return SystemFonts.CaptionFont
        End Get
    End Property

Adding shape support to GameObject and Sprite

Now that we have shape support added to our buffer, we can implement the shapes into our Sprite and GameObject classes.  We’ll need to add a new constructor to both Sprite and GameObject to allow for the creation of an object using a shape-based sprite, and we’ll have to modify the drawing methods in Sprite to draw the appropriate type of object to the buffer depending on the type of sprite we’re dealing with.

First, remember that we need to provide two colors to the shape-drawing functions: an outline color and a fill color.  We currently don’t have a way to store those values in either our Sprite or GameObject class, so we’ll create a new class called ShapeStyle, which stores the color values to use for drawing shapes.  It’s not very long so we’ll just add it to the end of the Sprite.vb file, but still inside the namespace.

            ...
        
        End Class 'end class Sprite
        
        Public Class ShapeStyle
            Public Property Outline As Color
            Public Property Fill As Color
            
            Public Sub New()
                Me.Outine = Color.Black
                Me.Fill = Color.Transparent
            End Sub
            
            Public Sub New(outline As Color)
                Me.Outine = outline
                Me.Fill = Color.Transparent
            End Sub
            
            Public Sub New(outline As Color, fill As Color)
                Me.Outine = outline
                Me.Fill = fill
            End Sub
        End Class
    End Namespace

We have three constructors for ShapeStyle: we can create one with no arguments and get a style that defines a shape with a simple black outline, or we can change the color of that outline, or change both the fill and the outline.

Now, we can add a property of type ShapeStyle to our Sprite class, but one small issue: ShapeStyle alone isn’t enough to tell the Sprite what kind of shape though; ShapeStyle only tells Sprite how to draw it.  So we’ll also need a way to store the kind of Sprite which we can define in a enum (if you don’t recall what a enum is, it’s a collection of named values).  We’ll add these with all of the other properties at the top of the Sprite class:

    Public Property Style As ShapeStyle
    
    Public Enum SprType
        Image
        Rectangle
        Ellipse
    End Enum
    
    Public Property SpriteType As SprType

Now we can write the new constructor to create shape-based Sprites.  Since Image is also a sprite type, we’ll also need to account for the case where a user would pass the Image enum value to this new constructor:

    Public Sub New(type As SprType, sprData As Object)

Notice the type of the sprite data parameter: we leave it Object, the root of the class hierarchy.  We need to be able to pass in either an Image-type or a ShapeStyle-type argument.  The value of the type parameter determines which one the sprite data gets typecasted to.

        ...
        
        If type = SprType.Image Then
            If TypeOf sprData Is Image() Then

One nice thing about Visual Basic is the TypeOf keyword: it allows us to determine if we can cast a variable to a particular type.  Since an image-based sprite can be either an Image or an array of Images, we can check which one it is.

                ...
                
                _curFrame = 0
                _imgs = CType(sprData, Image())
            ElseIf TypeOf sprData Is Image Then
                _curFrame = 0
                _imgs = New Image() {CType(sprData, Image)}
            Else
                Throw New ArgumentException("The provided sprite data is not valid for type SprType.Image; must be type Image or Image()", "sprData")
            End If

We also have to have an Else block in here in case we don’t get an Image or Image array as the sprite data.  In this case we just throw an invalid argument exception, since there’s nothing else you can really do to get around the problem of invalid sprite data.  Of course we could just default to a rectangle or something, but that would likely go against the intent of the programmer, and if something is wrong where we get invalid data it’s probably not the best idea to silently let it happen and move on.

            ...
            
        Else
            _curFrame = -1

In the last post when we wrote the animation methods, each one included a conditional check to see if the current frame index is -1: if so, the method exits and does nothing more.  The only way we can set the frame index to -1 is in this constructor, which happens if the Sprite is to be a shape.

            ...
        
            If TypeOf sprData Is ShapeStyle Then
                Style = CType(sprData, ShapeStyle)
            Else
                Style = New ShapeStyle()
            End If
        End If
    End Sub

Notice that here if we get something other than a ShapeStyle data parameter, we call the default ShapeStyle constructor.  It makes sense to do this here since we already know that the Sprite is going to be a shape, so this won’t cause any weird errors later.

We have a constructor now for a shape-based Sprite, so now we need to modify the Draw() and DrawRotated() methods to actually draw them.  The idea is pretty simple: replace the image drawing code with a Select Case block (switch block in Java) to check the type of the Sprite and do whatever is needed to draw it.  Luckily, shapes are easy and only take one line of code to draw: the call to the corresponding Buffer function.

Our new Draw() method can be rewritten as:

    Public Sub Draw(ByRef b As Buffer, bounds As RectangleF)
        Select Case SpriteType
            Case SprType.Image

                'draws the current frame to the provided buffer within the given bounds
                b.DrawImage(_imgs(_curFrame), bounds.Location, bounds.Size)
            Case SprType.Rectangle
                'remember: the only thing we need to draw the sprite that is not included within the sprite, is the bounding box in which to draw it
                'we know the sprite type, so we just need to draw the correct shape
                b.DrawRect(bounds, Style.Outline, Style.Fill)
            Case SprType.Ellipse
                    b.DrawEllipse(bounds, Style.Outline, Style.Fill)
        End Select

    End Sub

The DrawRotated() method may look a bit more daunting to modify, but the same idea applies.  We don’t need to modify any of the code that handles the rotation!

    ...
    
    'apply transformations
    g.TranslateTransform(bounds.X + offset.X, bounds.Y + offset.Y)
    g.RotateTransform(rotation)
    g.TranslateTransform(-(bounds.X + offset.X), -(bounds.Y + offset.Y))
    
    Select Case SpriteType
        Case SprType.Image

            'draw the image to rotatedBitmap
            g.DrawImage(_imgs(_curFrame), bounds.X, bounds.Y, bounds.Width, bounds.Height)
        Case SprType.Ellipse
            'draw the ellipse
            g.FillEllipse(New SolidBrush(Style.Fill), bounds)
            g.DrawEllipse(New Pen(Style.Outline), bounds.X, bounds.Y, bounds.Width, bounds.Height)
        Case SprType.Rectangle
            'draw the rectangle
            g.FillRectangle(New SolidBrush(Style.Fill), bounds)
            g.DrawRectangle(New Pen(Style.Outline), bounds.X, bounds.Y, bounds.Width, bounds.Height)
    End Select

    
    'draw rotatedBitmap to the buffer
    b.DrawImage(rotatedBitmap, New Point(0, 0), b.BufferSize)
    
    ...

See?  That wasn’t bad!  Notice that we’re not calling our new Buffer functions here; we’re drawing to the Graphics object g that is drawing onto our already-rotated surface, which then is what gets drawn to the Buffer.

One last thing: we need to add a new constructor to GameObject to handle the creation of GameObjects with shape-based Sprites.  Our previous constructors take in an image and generate the Sprite object in the constructor; here, we can write a constructor that takes in an entire Sprite object.  Under all of our other constructors in GameObject, we’ll add the new one:

    Public Sub New(sprite As Sprite, location As PointF, size As SizeF)
        Me.Sprite = sprite
        Me.Location = location
        initialPosition = location
        
        Me.Size = size
        
        If Me.Sprite.SpriteType = Engine.Sprite.SprType.Image Then
            scaleW = size.Width / Me.Sprite.Images(0).Width
            scaleH = size.Height / Me.Sprite.Images(0).Height
        Else
            scaleW = 1.0
            scaleH = 1.0
        End If
        
        Velocity = New Vector(0, 0)
        IsVelocityOn = False
        
        Acceleration = New Vector(0, 0)
        IsAccelerationOn = False
        
        Rotation = New Angle(0)
        RotationAnchor = New PointF((w * scaleW) / 2, (h * scaleH) / 2)
    End Sub

It looks largely the same as our previous constructors, except we need both a location and a size to be provided in addition to the Sprite.  Recall that we can’t draw a shape without both of those, so we need that information to come from somewhere.  If the Sprite is image-based, we calculate the GameObject’s scaling factor from the size of the image and the provided size, otherwise we just leave the scaling factors 1.0.

Now, our game engine (finally) supports shapes!  The Graphics object provides plenty more shape-drawing methods and there’s a few more that could be added to our engine similarly; those are other things you can experiment with on your own, but at this point we can say the major engine work is done!  We set out to create a simple 2D engine and now here we are 🙂

There’s an example project linked at the end of this post that demonstrates the new shapes in action, but I want to discuss (or review, rather) some important object-oriented programming concepts that you should know.  It always helps me to see concepts in practical use in order to fully understand how they work, so I hope some of this helps not only in game development but in future software development work!

Object-Oriented Programming – Review

Inheritance

One of the most useful concepts, if not the most useful, is the idea of class inheritance: one class, called the child class, can inherit the exposed members and functions of a parent class without having to rewrite them.

An example: Suppose you’re developing a platformer game that awards points when you collect coins or stomp on strange walking mushroom creatures that can kill you (I know right, why would anyone want to play a game like that? 😉 ).  The game needs to keep track then of the score, the amount of coins held by the player, and the player’s health, as well as the player object itself.  One way of doing this would be a separate variable for all of them.  The issues is that they’re all related, so why worry about managing them separately?  Code can get messy like that, and it is far easier (in most cases) to manage everything related to the player, from the player object itself.

Thus, we could create a Player class that includes variables for these values like so:

    Public Class Player
        Inherits GameObject
        
        Public Coins As Integer = 0
        Public Score As Integer = 0
        
        ...

Then any time we wanted to access the coin count, we use Player.Coins, and the Player class still has everything that the GameObject class provides.  Intuitively it’s easier to group related things together in code, and it helps to keep code organized.

This isn’t really a big deal, since it’s no more complex to manage things in separate variables like PlayerObject, PlayerCoins, PlayerScore, etc.  The real power of class inheritance comes in overriding and overloading methods and functions.

The Player object is likely to have a lot of logic associated with it: collision checking with walls, with monsters, checking if it fell out of the world, etc.  If we cram every piece of logic for every entity in the game into the same GameLogic() function, it’s going to get really big, really fast, and is going to be a pain in the ass to modify later.  Again, we can group the Player logic code into the Player class, the monster logic into a Monster class, etc.

We can easily overload the Player’s Update() method which is currently provided by GameObject.  All the GameObject.Update() method does is update the location and velocity of the object, which is common logic for all game objects, but if we overload it in the Player class, it won’t do that.  That doesn’t mean we have to rewrite that part: we simply call the Update() method of the base class of Player, which is GameObject!

    Public Overloads Sub Update()
        MyBase.Update()
        
        'player logic here
        
        ...

Now, instead of cramming the Player logic into the main GameLogic() method, we just include a call to Player.Update() which will move it if necessary and then do whatever logic is required for the player.

One small note about inheritance worth mentioning is the idea of scope and access modifiers. An inheriting child class can only access the members of the parent that are declared as Public or Protected. As a review, Public allows access to a member from any part of the program’s code, while Protected only allows access from a child class. Private, of course, restricts a member to only be accessible within the class it is defined in.

Just like the GameObject provides common update logic for any class that inherits from it, we can go further and make a class that inherits from GameObject, which provides additional logic for any other class that inherits from it which must provide its own logic as well.  It sounds tricky and that was probably a really bad explanation, but consider the monsters from our hypothetical game mentioned above.  Suppose now we want to add turtles and spiny-shelled turtles to our game.  All three of these monsters walk, all of them turn around if they hit a wall, but they behave differently when stomped on: one dies, one retracts into its shell and re-emerges, and one damages the player.  There’s common logic here that would be wasteful to write every time we add a new monster.  This is where the beautiful idea of abstract classes comes in.

Abstract Classes

An abstract class is one that provides declarations of methods or functions and may contain some definitions of methods or functions, but cannot be instantiated themselves.  Instead, you must write a class that inherits from it and implement the undefined methods and functions in the new subclass.

If that’s confusing, let’s go back to our monsters: they all same the same logic for moving, so we can wrap that in an abstract class like this (in Visual Basic, these are called MustInherit classes but the idea is the same):

    Public MustInherit Class Monster
        Inherits GameObject
        
        Public dead As Boolean = False
        
        Public Sub BaseUpdate()
            MyBase.Update()
            
            If Me.IsCollidingWith(wall) Then
                'turn around
                Velocity.X = -Velocity.X
            End If
            
            'other monster logic...
        End Sub
        
        Public MustOverride Overloads Sub Update()
        
    End Class

The BaseUpdate() method calls the GameObject’s Update() method to update the object’s location and velocity, then handles the common monster logic.  The overloaded Update() method is declared as MustOverrides (abstract in Java), so any class that inherits from this Monster class must provide its own implementation of Update(). Now we can write a WalkingMushroom class that inherits from it and define its logic:

    Public Class WalkingMushroom
        Inherits Monster
        
        Public Overrides Sub Update()
            BaseUpdate()
            
            If Me.IsCollidingWith(Player) Then
                If Me.IsBelow(Player) Then
                    dead = True
                Else
                    Player.Health -= 1
            End If
            End If
        End Sub
    End Class

This kind of implementation allows us now to manage the monsters as a list: we can add monsters to a list when created and remove them when they die, which is far easier than creating a separate variable for each instance monster in the map.  The list just needs to be a list of objects of type Monster; since Monster is an abstract class it doesn’t matter what kind of monster we’re dealing with when we go to update each one in the list, as each possible one has its own Update() method that gets called.

The dead variable allows us to check when a monster is killed and remove it from the list when it dies.  Suppose our list is called MonstersList; the update code in GameLogic() then looks like this:

    ...
    
    Dim i As Integer = 0
    Do Until i = MonstersList.Count – 1
        If MonstersList(i).dead Then
            MonstersList(i).Dispose()
            MonstersList.RemoveAt(i)
        Else
            MonstersList(i).Update()
            i += 1
        End If
    Loop
    
    ...

This simply loops through the list of monsters, removing the dead ones and updating the rest of them. This is much nicer than cramming the monster logic for each possible type of monster in the same method, and as a result things are far easier to debug and adding new monster types doesn’t require any changes to the main GameLogic() method.

There are many more OOP concepts but I think these two are definitely important for any programmer to know, and we’ll make use of them in the last and final post when we build our game!

Wrap up

Well, if you made it this far, congrats!  You now have a functional and extendible 2D game engine!  These many more things that can be added to it, but it’s a good start and it’s easy to use.

I really hope this has provided some insight on the internal work that game engines do and on game development in general.  Though ours is very simplistic, many modern game engines have to worry about the same kinds of tasks that we’ve covered over the course of these posts.

Next time we’ll put it to the test and make a playable game with it!  Get ready!

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

Question: No question today!

Previous Question Answer: There are a lot of different answers, but one example I’m using in my group project is providing immunity to attacks after taking damage.  When the player collides with a monster, it sets an immunity flag to True and starts a thread that simply waits for two seconds and then sets the immunity flag back to False.  This allows the player some time to get away from the monster.

Challenge: No challenge today!

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

Leave a Reply