So you want to make a game engine? Part 4 – Let’s get the ball rolling

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

Hey everyone!  Hope you guys are ready for more engine development, there’s a lot of important stuff to cover today!

Before we get into it, let’s recap what we’ve already done up to this point:

  • 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’ve drawn an image!

So quite a lot so far!  However, it’s still missing the core ability of a game engine: a mechanism to attach object properties, such as location, size, speed, etc. to visuals, and also to manage these object properties.  We’ll call this a GameObject, and we’ll call their visual representations Sprites.

GameObjects and Sprites

We discussed briefly in part 2 the role of a GameObject.  The engine has to manage potentially many objects at one time, each of which has their own location within the world, size, velocity, acceleration, sprites, and other properties.  We can encapsulate all of this information into a GameObject class, give each of them a Sprite that represents them, and we’ll have a nice way to handle things in our engine.

There’s two ways we could go about doing this.  One way would be to create a GameObject class that includes all of the relevant properties for object information, including an image.  That is, we bake the visual side of the object directly into the thing so we have a single class.  We can do it this way, but it could get messy.  We’re going to be adding on to this a lot over the next couple posts, and if we cram the visual and ‘physical’ properties into the same codebase, it could get a little cumbersome to modify one part of it without breaking another part.

This leads us to the other way of doing it: splitting up the ‘physical’ properties – the location, size, and everything else – from the visual property – the sprite.  We make two classes, the GameObject and the Sprite; the GameObject will then have a property of type Sprite.  We’re going to be adding additional sprite-types besides image-based, so if we split the classes this way it’s easier to expand on both.

A question some of you may have is, can you really separate the two like that?  Don’t you need location and size information to draw the sprite for something?  The answer is yes you do, but you don’t have to store this in the same class as the Sprite.  This goes back to an older version of my game engine, where I actually did keep location and size information both in the GameObject and in the Sprite classes.  It was redundant and trying to keep both copies of the same data in sync with each other was pretty bug-prone and pointless.

We can overcome this and still provide the Sprite with the information it needs to draw itself, by only providing the information when it needs drawn.  Basically, we call the GameObject’s Draw() method.  This calls its Sprite’s Draw() method, passing in its (the GameObject’s) location and size as parameters.  From there, based on the type of Sprite we’re dealing with, we draw the thing to the buffer.

The benefits of this approach may become clearer in future posts as we further develop the Sprite class and discuss more graphics topics, but for now, all we’re going to worry about implementing today is the core parts of the GameObject and the Sprite; we’ll add just enough to make a moving object with an image-based sprite.

Let’s start with the Sprite class.  The part we’re implementing today is actually really short so let’s get it out of the way before we jump into the GameObject.

Sprite

Open up your previous project and add a new class called Sprite.vb.  As usual, make it a part of the Engine namespace we’ve been working on, and import the System.Drawing namespace too.

    Imports System.Drawing
    
    Namespace Engine
        Public Class Sprite
        
        End Class
    End Namespace

We’re only going to worry about an image-based sprite for now.  Later in this series, we’ll implement support for animated sprites and using shapes in place of images, but for now let’s get the basics down.

Obviously, we’re going to need to store the image that is to be drawn.  We’ll implement it as an image array rather than a single image.  One motivation for doing it like this is so that animation can be implemented later on.  Another that you may not have thought about is using separate images to represent an object’s state: we could load in an array of four images for a human character, facing each of the four cardinal directions, and switch between them depending on which way the character is moving.

Since we’re dealing with an image array here, we’ll also need an index variable to tell the drawing function which of the elements of the array it should be drawing.  We’ll refer to this as the current frame of animation.

So, we’ll need two class members to store this data, plus two constructors: one that creates the sprite from one image, and another that creates the sprite from an array of images.

    Private _imgs() As Image
    Public Property Images As Image()
        Get
            Return _imgs
        End Get
        Set(value As Image())
            _imgs = value
            _curFrame = 0
        End Set
    End Property
    
    Private _curFrame As Integer
    Public Property CurrentFrame As Integer
        Get
            Return _curFrame
        End Get
        Set(value As Integer)
            _curFrame = Math.Max(value, 0)
        End Set
    End Property

Some minor points to consider for the setters of each of these properties:

  • If we decide to swap out the image(s) used for the sprite, we need to reset the current frame index. Suppose we load in an image array with fewer frames than the previous image array, but don’t reset this index.  What could happen?
  • It doesn’t make sense for the current frame index to be negative. It also doesn’t make sense for it to be larger than the size of the array, but we’ll save that for the animation section.

    Public Sub New(img As Image)
        _curFrame = 0
        _imgs = New Image() {img}
    End Sub
    
    Public Sub New(imgs() As Image)
        _curFrame = 0
        _imgs = imgs
    End Sub

There’s nothing really special about the constructors here: they take in either a single image or an image array and set the current frame index to 0.  All that remains to do for our simple Sprite class is the Draw() method.  We discussed before that the Sprite does not internally store any kind of location or size info, so we’ll need to make these parameters to the drawing method.

    Public Sub Draw(ByRef b As Buffer, bounds As RectangleF)
        b.DrawImage(_imgs(_curFrame), bounds.Location, bounds.Size)
    End Sub

Notice that we’re passing the buffer by reference here, as opposed to the default of by value.  Passing an argument by reference allows the method to make changes to the value of that argument that persist after the method finishes; since we’re drawing an image onto the buffer, it would definitely be nice for that change to the buffer to stick around.

That’s it for Sprite for now!  We’ll be adding a lot more in later posts.

GameObject

This is, without a doubt, the most fundamental piece of the engine.  Most, if not all, of the objects in your game will be an instance of this class.  You may even want to write a class that inherits the GameObject class (we’ll look more at this technique and object-oriented concepts later on).  It has three main tasks: keep track of the properties of an object, update itself if it should be moving, and provide the relevant information to its Sprite when asked to draw itself.

First, if you haven’t already, make a new class file in your project and call it GameObject.  Add it to the Engine namespace as we have been doing.

    Namespace Engine
        Public Class GameObject
        
        End Class
    End Namespace

We’ll start by adding getters and setters for the location.

    Public Property X As Single
    Public Property Y As Single
    
    Public Property Location As PointF
        Get
            Return New PointF(X, Y)
        End Get
        Set(value As PointF)
            X = value.X
            Y = value.Y
        End Set
    End Property

The Location property is mainly for convenience if we want to deal with the location as a Point rather than get the coordinates separately.  A game engine is a piece of software that other developers can use on their own to make their own games, so it’s always nice to include things for convenience (however, overdoing it can be counterproductive).  A lot of game engines have a relatively steep learning associated with using them, whereas we’re focusing on simplicity and ease of use with ours.

Let’s move on to size.  We’ll handle it slightly differently.

    Private w As Single, h As Single
    
    Public Property Width As Single
        Get
            Return w
        End Get
        Set(value As Single)
            If value >= 0 Then w = value
        End Set
    End Property
    
    Public Property Height As Single
        Get
            Return h
        End Get
        Set(value As Single)
            If value >= 0 Then h = value
        End Set
    End Property

Notice the conditional check in the setters here.  It doesn’t make sense for something to have a negative size, so we don’t change the internal size variables if the game tries to give something a negative size.

We’ll also make a convenience property to get and set the size as a Size object:

    Public Property Size As SizeF
        Get
            Return New SizeF(w, h)
        End Get
        Set(value As SizeF)
            w = Math.Max(value.Width, 0)
            h = Math.Max(value.Height, 0)
        End Set
    End Property

Related to size, we can also add the concept of scaling here.  Scaling simply allows us to draw something larger or smaller by stretching its sprite.  We already have that to draw the sprite, we simply need to give it a location and size, so we can easily implement scaling at this stage.  It’s better to add it now rather than work it in to everything we add later on.

    Private scaleW As Single, scaleH As Single
    
    Public Property ScaleWidth As Single
        Get
            Return scaleW
        End Get
        Set(value As Single)
            scaleW = Math.Max(value, 0)
        End Set
    End Property
    
    Public Property ScaleHeight As Single
        Get
            Return scaleH
        End Get
        Set(value As Single)
            scaleH = Math.Max(value, 0)
        End Set
    End Property

Finally, it may be useful to remember the location at which the object was initialized, such as if a character dies and must respawn at its start location.

    Private initialPosition As PointF
    Public ReadOnly Property InitialLocation As PointF
        Get
            Return initialPosition
        End Get
    End Property

We can make this property ReadOnly to prevent it from being externally changed later on. That’s basically it for the ‘physical’ properties.  We just have a few more to add.

    Public Property Velocity As Vector
    Public Property IsVelocityOn As Boolean
    
    Public Property Name As String
    
    Public Property Sprite As Sprite

Here’s our Vector class we developed earlier!  We also have a toggle that determines whether the object should be moving according to velocity or not.  We also have a Name property that can be useful when dealing with lists or arrays of GameObjects, and of course the Sprite for the GameObject.

Let’s look at the constructors.  We’re only going to worry about two for now.  One will initialize the object with an image and a location, and the other will initialize the object with an image, location, and a size.  We’ll see how we can use the scaling factor in the second constructor.

    Public Sub New(img As Image, location As PointF)
        Sprite = New Sprite(img)
        
        X = location.X
        Y = location.Y
        initialPosition = location
        
        Size = img.Size
        scaleW = 1.0
        scaleH = 1.0
        
        Velocity = New Vector(0, 0)
        IsVelocityOn = False
    End Sub
    
    Public Sub New(img As Image, location As PointF, newSize As SizeF)
        Sprite = New Sprite(img)
        
        X = location.X
        Y = location.Y
        initialPosition = location
        
        Size = img.Size
        scaleW = newSize.Width / img.Width
        scaleH = newSize.Height / img.Height
        
        Velocity = New Vector(0, 0)
        IsVelocityOn = False
    End Sub

Note that in both constructors, the object’s size is set to the same size as the image.  The only difference in these constructors is the scaling that is applied to the object.  You may notice at this point that the Size property doesn’t account for the scaling factor; we’ll add a bounding box property in the next post that accounts for this when we discuss collision checking.

At this point we only need two more things to finish up our simple GameObject implementation: a method that moves it, and a method that draws it.  Fortunately, both are pretty simplistic.

    Public Sub Update()
        If IsVelocityOn Then
            X += Velocity.X
            Y += Velocity.Y
        End If
    End Sub

The Update() method changes the location of the object every time it is called if velocity is to be applied to the object.  We’ll add acceleration, which changes the velocity of the object, in the next post.

    Public Sub Draw(ByRef b As Buffer)
        Sprite.Draw(b, New RectangleF(Location, New SizeF(w * scaleW, h * scaleH)))
    End Sub

Note that we can’t just use the object’s Size property here, because as mentioned before it doesn’t account for the scaling factor.

So with the core of our GameObject and Sprite complete, let’s try them out!  Example projects from here on out are assuming you have the skeleton framework of a game project that we developed in post 3 (the game loop, window setup, Initialize(), GameLogic(), Render(), and Shutdown()).

Moving Ball Example

First, find a small image of some sort or make one in Paint.  I’m using this one here: i'm not known for my artistic ability

Add it to your project as we did in the last post: for a recap, right-click the top item in the Solution Explorer, go to Add, and choose Existing Item.  Make sure you change the file type to Image Files.  When you add the file, right-click it, choose Properties, and change the Copy To Output Directory property to Copy Always.

Right above the Initialize() method, define a GameObject variable and give it a name.  Mine is called ‘ball’ for obvious reasons.

Yep, it's definitely a ball.

Yep, it’s definitely a ball.

In Initialize(), we’re going to set it up by assigning it our image and giving it a location and velocity.

    ball = New GameObject(Image.FromFile(Application.StartupPath & "\ball.png"), New Point(400, 200))
    
    ball.IsVelocityOn = True
    ball.Velocity = New Vector(-3, 0)

To make it move, we need to add a call to its Update() method in the GameLogic() method.  Since it has a negative X-velocity, it will move left.  Eventually, it will move off the window, so let’s make it wrap around to the other side when it does.

    ball.Update()
    If ball.X + (ball.Width * ball.ScaleWidth) <= 0 Then ball.X = 800

Remember that the X and Y properties are the coordinates of the upper-left point of the object.  To check whether it’s off the left side of the screen, we need to check whether the right edge of the object has an X-value of less than or equal to 0.  The right edge’s X-value of the object is equal to the X-coordinate plus the scaled width of the object.

Finally, in Draw(), we need to clear the buffer at the start of drawing each frame, draw the thing, and render it to the window.  We’ll make the background of the window blue this time.

    _buffer.ClearBuffer(Color.CornflowerBlue)
    ball.Draw(_buffer)
    _buffer.Render()

Run the program.  If everything was done right, your object should move off screen and reappear on the other side!  Cool!

Our example of a moving object (not pictured: the movement)

Our example of a moving object (not pictured: the movement)

That’s all for today!  We can draw moving objects now and we have a way to manage their properties, so we’re making progress!  Next time, we’ll discuss collision checking and acceleration.

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

Question: This example project is a great example of why the call to ClearBuffer() is needed in our game’s Render() method.  Comment it out and run it.  What happens?

Challenge: Let’s revisit our conditional check in the GameLogic() method:

    If ball.X + (ball.Width * ball.ScaleWidth) <= 0 Then ball.X = 800

Often it can be very useful to be able to check the right edge’s X-value, and the bottom edge’s Y-value; it’s also very useful to be able to set them directly.  Try implementing getters and setters for Right and Bottom.  The formula for getting the right edge is provided in the statement above.  The answers will be included in the next post’s sample code.

Previous Question Answer:
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?
If we made the window bigger, the size stored in the Buffer is not changed. So the image would still draw at a size of 800×480 and there would be empty space left in the window.

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

Leave a Reply