Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / GDI+

Beginners - Starting a 2D Game with GDI+

4.97/5 (17 votes)
25 Jun 2012CPOL12 min read 53.6K   2.1K  
Basics for setting up a 2D Game using GDI+, Renderloop, and Threading. And a few tips.

Introduction 

This topic is about absolute minimum basics needed to create a renderloop driven game, using basic threading, and a form. We are not creating an actual game.

I am assuming that the reader already has some knowledge of VB.NET. I hope this  is easily understood by both C# and .Net developers. Example is in VB.NET.

Background   

This was inspired by Google (Search Results) and landing up on game development discussions and simple games created by those who took up programming specifically because they wanted to create games.  

However, what you find is often the following: Windows Forms with Pictureboxes acting as sprites or graphics, panels, windows forms buttons and such.  

In honest opinion, developing games using PictureBox-on-Forms approach is one of the ugliest ways to start off in an attempt to build a game or a game prototype.

GDI Isn't particularly fast, but with the right code it can do amazing things. These are realized when you look into the source code for software rasterizers such as the one found in Quake2.

This is also my first ever article after visiting this site over the last couple years - many things I once thought were hard are now easy (or just easier) thanks to the explanations of the fellow coders and their source. Now for once I attempt to return the favor. I will try my best to give a clear explanation.  

Lets get started 

I will begin by explaining the basic needs of a renderloop based GDI+ game.

  • Thread: Your main Renderthread. Separate to the Application Context.
  • A backbuffer: Everything you draw - goes here.
  • A display: Everything shows up here. 

This translates to having the following: 

  • A Bitmap: This is your backbuffer
  • A Graphics Surface: This is what you use to draw to your backbuffer.
  • Another Graphics Surface: This is created from your display window to output your backbuffer.

So when we translate these need into code, we have the following.

VB
' This is your BackBuffer, a Bitmap:
Dim B_BUFFER = new Bitmap(Me.Clientsize.Width,Me.Clientsize.Height)  
 
' This is the surface that allows you to draw on your backbuffer bitmap.
Dim G_BUFFER = new Graphics( B_BUFFER ) 'drawing surface

' This is the surface you will use to draw your backbuffer to your display.
Dim G_TARGET = Me.CreateGraphics ' target surface

The backbuffer (B_BUFFER) is a System.Drawing.Bitmap. It has some similarities to System.Drawing.Image. You can use either a Bitmap or Image as the backbuffer, and both types are accepted by Graphic.New() and Graphics.FromImage(). You only need one of them, not both at the same time. 

The G_BUFFER is object that allows you to draw on a Bitmap, a System.Drawing.Graphics. I have named it G_BUFFER because it is acting as a Graphics Device for my B_BUFFER. Think of it as a Canvas instead. Any drawing routine used here will have their results applied to the B_BUFFER bitmap.

G_TARGET, in the above code's comment is used only for that purpose. Updating the display, which would be the form you will be rendering to. In this case. We are the form. We have asked the form for a surface to draw on which we will use to draw B_BUFFER to.

Tip: You can emulate "Layered Rendering" by using Bitmap.Clone(). Experiment to figure it out how. Hint: You still only draw an updated bitmap only once to the display. Don't make too many clones().

Common Issues: Flickering and Slow Rendering

By no means is GDI+ superfast. But in it's default state the Graphics Object is not setup for the fastest rendering possible. But beyond this article there are ways to do amazing things with it.

I will explain two quick and easy solutions for flickering and slow rendering, they don't do magic

  • Disabling the form's automatic refresh
  • Setting up GDI for optimal performance

Starting with the form, in short - flickering is usually caused by Windows telling the form to clear itself - which often overwrites all immediate mode rendering on the form replacing it with a blank result of whatever color it likes. It don't look good when some other event out there in the shadows of the .NET Framework are trying to replace everything you want to keep in the display.

Solution:   

VB.NET
Me.SetStyle(System.Windows.Forms.ControlStyles.AllPaintingInWmPaint, True) ' True is better
Me.SetStyle(System.Windows.Forms.ControlStyles.OptimizedDoubleBuffer, True) ' True is better
' Disable the on built PAINT event. We dont need it with a renderloop.
' The form will no longer refresh itself
' we will raise the paint event ourselves from our renderloop.
Me.SetStyle(System.Windows.Forms.ControlStyles.UserPaint, FALSE) ' False is better

The first two are self explanatory, the following is sourced from the Object Browser (Shortcut: F2) in Visual Studio:  

System.Windows.Forms.ControlStyles.AllPaintingInWmPaint: If true, the control ignores the window message WM_ERASEBKGND to reduce flicker.

This style should only be applied if the System.Windows.Forms.ControlStyles.UserPaint

bit is set to true.

System.Windows.Forms.ControlStyles.OptimizedDoubleBuffer 

If true, the control is first drawn to a buffer rather than directly to the screen, which can reduce flicker. If you set this property to true, you should also set the System.Windows.Forms.ControlStyles.AllPaintingInWmPaint to true.

Just these two alone make a big difference. The form no longer clears itself when you don't want it to. And everything you draw appears on the target (G_TARGET) surface and becomes persistent. Dragging your form off the edge of the desktop will no longer clear those those areas of the form. If you want to clear it to a specific color (usually a good idea) at least once to clear the rubbish all over the window then us can use Graphics.Clear(). In this case: G_TARGET.clear().

VB.NET
Me.SetStyle(System.Windows.Forms.ControlStyles.UserPaint, FALSE) 

This disables the Paint event in the form, when set to False. This means the Form.Paint event is no longer raised automatically. As mentioned in the comment of the above code, we will call our paint event ourselves. 

However - if you read carefully you would have seen : This style should only be applied if the System.Windows.Forms.ControlStyles.UserPaint bit is set to true.

And then of course is the description of UserPaint:

Summary: If true, the control paints itself rather than the operating system doing so. If false, the System.Windows.Forms.Control.Paint event is not raised. This style only applies to classes derived from System.Windows.Forms.Control. 

Now look up at the code example above again, and then read the description of AllPaintingInWmPaint again  - carefully! You should have noticed it by now.

Now you should have a question like the following in your head:

Question: WHY in the name of Forbidden Cheeseburgers are you setting Userpaint to false? 

 The answer is simple: I want to raise my paint event exactly when I want it to be raised. Because I am raising the paint event myself, I don't need any other paint event invading the control I am supposed to be dominating. I want to eliminate any unnecessary paint events that are not coming from within the renderloop itself when using GDI for games or game prototypes.

The descriptions of those properties are assuming you want a paint event raised for you in some way or another, which is not true in our case. Additionally we are trying to kill as many of these strange things we call middlemen as much as possible.   

So when AllPaintingInWmPaint = true and Userpaint = false then there is nobody, no aliens or other creatures in .NET telling your form to raise the paint event other than your own code.

Especially important because we will be raising a paint event every cycle of a loop - extra unwanted paint events wouldn't really be healthy, and you may even splatter some of the paint, it could get messy! 

Next up - you cannot raise the Paint event directly from within your form class, and Visual Studio will tell you the same as you type it. You will need to replace it with your own as shown in the following example:  

VB.NET
Shadows Event Paint(ByVal G As Graphics)

In this example we will only be passing G - a Graphics object which will actually be the G_BUFFER graphics I mentioned before.

In our version of the Paint event - I have excluded the PaintEventArgs. The above now allows use to raise the Paint event directly from within the Form instance.

Alternatively you could just call the OnPaint() routine directly. I personally don't like doing it that way for some strange and obscure reason. Tomato or Tomaaato - right?

The other problem mentioned was slow rendering. The following would be applied to configure your Graphics objects, for optimal rendering - but they don't do magic.

VB
' Configure the display (target) graphics for the fastest rendering.
With G_TARGET
    .CompositingMode = Drawing2D.CompositingMode.SourceCopy
    .CompositingQuality = Drawing2D.CompositingQuality.AssumeLinear
    .SmoothingMode = Drawing2D.SmoothingMode.None
    .InterpolationMode = Drawing2D.InterpolationMode.NearestNeighbor
    .TextRenderingHint = Drawing.Text.TextRenderingHint.SystemDefault
    .PixelOffsetMode = Drawing2D.PixelOffsetMode.HighSpeed
End With
 
' Configure the backbuffer's drawing surface for optimal rendering with optional
' antialiasing for Text and Polygon Shapes
With G_BUFFER
 
    'Antialiasing is a boolean that tells us weather to enable antialiasing.
    'It is declared somewhere else
    If Antialiasing then
        .SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
        .TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias
    else
        ' No Text or Polygon smoothing is applied by default
    endif
 
    .CompositingMode = Drawing2D.CompositingMode.SourceOver
    .CompositingQuality = Drawing2D.CompositingQuality.HighSpeed
    .InterpolationMode = Drawing2D.InterpolationMode.Low
    .PixelOffsetMode = Drawing2D.PixelOffsetMode.Half
End With

Aiming for speed with GDI+ is important. Higher quality setting in most cases are hardly noticeable. I have configured G_TARGET with optimal settings. When we are updating the target we will only be using one routine, the fastest way to draw a bitmap in GDI+: DrawImageUnscaled().

For G_BUFFER the configuration is identical, with exception that we have an option to Antialias text and lines or polygon shapes for our Backbuffer bitmap. I also have PixelOffsetMode set to half. The Visual Studio descriptions for each of the properties and values are pretty much self explanatory.

Setting Up

We are controlling our display size and antialiasing switch with:

VB.NET
' some vars containing properties:
Dim DisplaySize As New Size(800, 600)
Dim Antialiasing as Boolean = false; 

Tip: You could store values using MY.Settings instead and have startup screen with options.

VB.NET
' We want our drawing area to be the desired DisplaySize
' - form will(should) grow to accommodate client area.
Me.ClientSize = DisplaySize

' Lock windowsize
Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedSingle

' Resize events are ignored:
Me.SetStyle(ControlStyles.FixedHeight, True)
Me.SetStyle(ControlStyles.FixedWidth, True)

In the above we set our Form.ClientSize (not the form size) to the desired display size. As explained by the comment in code. Naturally the form would resize so that the client area meets our needs.

Following that, we locked the forms ability to resize by setting border style to Windows.Forms.FormBorderStyle.FixedSingle and then using SetStyle to disable any resize events.

If you want to enable resizing, you should also remember to first dispose B_BUFFER and G_BUFFER before re-initializing them to the new client size to ensure they are unloaded first. G_TARGET should not require reinitialization.

In Sub New():

VB.NET
' configure form:
Me.SetStyle(System.Windows.Forms.ControlStyles.AllPaintingInWmPaint, True) ' True is better
Me.SetStyle(System.Windows.Forms.ControlStyles.OptimizedDoubleBuffer, True) ' True is better

' Disables the on built PAINT event. We dont need it with a renderloop.
' we will raise the paint event ourselves
Me.SetStyle(System.Windows.Forms.ControlStyles.UserPaint, False) ' False is better

' We want our drawing area to be the desired DisplaySizesize
' - form will grow to accommodate client area.
Me.ClientSize = DisplaySize

' Lock windowsize
Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedSingle

' Resize events are ignored:
Me.SetStyle(ControlStyles.FixedHeight, True)
Me.SetStyle(ControlStyles.FixedWidth, True)

The complete _renderloop routine - includes initializing and disposing the Bitmap and two graphics surfaces:

VB.NET
' In this routine
' -- we configure the form, the buffer, drawing and target surface 
' -- loop until form closes
' -- raise our paint event were the actual painting of game graphics will take place
' -- disposes our surfaces and buffer when we are done with them
' All in one function
' Does not allow dynamic resizing. That you can experiment with.
Private Sub _renderloop()

    ' Create a backwash - er Backbuffer and some surfaces:
    Dim B_BUFFER = New Bitmap(Me.Clientsize.Width, Me.Clientsize.Height) ' backbuffer
    Dim G_BUFFER = New Graphics(B_BUFFER) 'drawing surface
    Dim G_TARGET = Me.CreateGraphics ' target surface

    ' Clear the random gibberish that would have been behind (and now imprinted in) the form away. 
    G_TARGET.Clear(Color.SlateGray) 
 
    ' Configure Surfaces for optimal rendering:
    With G_TARGET ' Display 
        .CompositingMode = Drawing2D.CompositingMode.SourceCopy
        .CompositingQuality = Drawing2D.CompositingQuality.AssumeLinear
        .SmoothingMode = Drawing2D.SmoothingMode.None
        .InterpolationMode = Drawing2D.InterpolationMode.NearestNeighbor
        .TextRenderingHint = Drawing.Text.TextRenderingHint.SystemDefault
        .PixelOffsetMode = Drawing2D.PixelOffsetMode.HighSpeed
    End With
 
    With G_BUFFER ' Backbuffer

        ' Antialiased Polygons and Text?
        If AntiAliasing Then
            .SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
            .TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias
        Else
            ' defaults will not smooth
        End If
 
        .CompositingMode = Drawing2D.CompositingMode.SourceOver
        .CompositingQuality = Drawing2D.CompositingQuality.HighSpeed
        .InterpolationMode = Drawing2D.InterpolationMode.Low
        .PixelOffsetMode = Drawing2D.PixelOffsetMode.Half
    End With
 
    ' The Loop, terminates automatically when the window is closing.
    While Me.Disposing = False And Me.IsDisposed = False And Me.Visible = True
 
        ' we use an exception handler because sometimes the form may be
        ' - beginning to unload after the above checks. 
        ' - Most exceptions I get here are during the unloading stage.
        ' - Or attempting to draw to a window that has probably begun unloading already .
        ' - also any errors within OnPaint are ignored. 
        ' - Use Exception handlers within OnPaint()
        Try
 
            ' Raise the Paint Event - were the drawing code will go
            RaiseEvent Paint(G_BUFFER)
             
            ' Update Window using the fastest available GDI function to do it with.
            With G_TARGET
                .DrawImageUnscaled(B_BUFFER, 0, 0)
            End With
 
        Catch E As exception
            ' Show me what happened in the debugger
            ' Note: Too Many exception handlers can cause JIT to slow down your renderloop. 
            ' - One should be enough. Stack Trace (usually) tells all!
#If DEBUG then
            debug.print(E.tostring)
#End If
        End Try
 
    End While
 
    ' If we are here then the window is closing or has closed. 
    ' - Causing the loop to end

    ' Clean up:
    G_TARGET.Dispose()
    G_BUFFER.Dispose()
    B_BUFFER.Dispose()
 
 
    ' Routine is done. K THX BYE
End Sub

The Loop itself without comments to see the event being raised and target update without comment clutter:

VB.NET
While Me.Disposing = False And Me.IsDisposed = False And Me.Visible = True
    Try
        RaiseEvent Paint(G_BUFFER)
        With G_TARGET
           .DrawImageUnscaled(B_BUFFER, 0, 0)
        End With
    Catch E As exception

#If DEBUG then
        debug.print(E.tostring)
#End If
    End Try

End While

In my own experience I usually only get an exception now and then after the form begins to unload. This doesn't always happen. Just catch a thread trying to do something at the wrong time - it will happen eventually.

The above, pretty simple - does the following every cycle:

  • Check if we are unloading. If we are - exit loop!
  • Raises the paint event.
  • Catches any exceptions and sends them to the debugger.

Also - don't try to make unsafe cross thread calls to controls. You will know when your doing it - an exception will be raised telling you that you are doing it. But try to keep WinForms control related stuff outside of the renderloop thread anyhow. But If you really have to: You can read here, and here. There are also alot of examples on this site.

And finally - importantly: 

VB.NET
' Clean up:
G_TARGET.Dispose()
G_BUFFER.Dispose()
B_BUFFER.Dispose()

Telling an object to unload is purely optional. However - why make someone else clean up the stuff you leave in memory?

Just because Mr. Garbage Collector specializes in collecting trash and stuff off the sidewalks of an application doesn't mean he likes it. Sometimes he will even get in the way.

It's always better to recycle those that will be reused, by trashing them first. If you don't plan to use them again - trash them.  Additionally in Games, you don't want the GC firing too often while your renderloop is running, it can impact performance (even if minimal for small apps) . Clean up everything you don't need anymore.

I think this nothing more than my own opinion however, but I have seen XNA developers complaining and looking for ways to get the Garbage Collector out of the way.

In the source

The source provided , when Run should look like this following: 

Image 1

There is more code in the source than what was described here. Built in VS2008.

Points of interest 

  • A fully 3D Software Rasterizer is possible with some clever programming tricks. See the Software Rasterizer in Quake 2 (It is even in a C# Port of it ). The comments in the code explain all. See C++ version: Quake 2 Sourcecode Review. However on .NET it may be too slow using only managed API. Additionally you can get away from JIT  by compiling to Native using NGEN to kill the middleman named CIL. (Learn this some other time).

Tips and challenges for Aspiring .NET Game Engine Developers

These are things you could try implementing eventually:  

  • Rendering too many bitmaps at once can slow you down. Keep track of what is within and not within the display area.
  • I found creating too many bitmaps too fast using threads sometimes tries to create them faster than the memory can be allocated. Resulting in a premature "Out of Memory Exception". - Use a single resource list (per level?) to load all image resources only once. All sprites/textures should be used from there. Loading could be done using a BackgroundWorker.
  • Learn how to create a Marshal Thread for your windows GUI.
  • Learn how to do Parallel Processing in .NET4
  • Blit by making direct calls to GDI32.DLL. You kill more middleman this way. There is no law against killing middlemen - think of them as Garden Gnomes. You want to kick them over eventually...
  • Do not try to place every single call to a GDI routine in their own microthreads. It doesn't make it faster. GDI will assault you with a wormhole manufactured icecream cone with the intention of impersonating Jack The Ripper after your demise. Trust me - I tried it once. I try everything  at least once...
  • Something I do: Replace the use of the GDI Bitmap by creating your own Bitmap class with direct access to the pixel arrays in memory. This would be for faster manipulation of pixel data instead of using a wrapped API methods. Try to be optimal.
  • All non-interactive graphics are better rendered to a single resource (during loading screens), E.g: A Map with trees that cant be interacted with - render to map - use one map. Sample only Visible Area - render more dynamic stuff on top of the sampled area.
  • Keep track of objects that move. Tree comparisons - Dont update or redraw objects that don't move unless they are animating. Re-Render only regions for objects that are moving - and those that are "touching" those objects.
  • Create your own math library directly in your own game engine. Calculating complex math directly is a little faster than using an API Call/Wrapper (garden gnomes). You can also merge math source code from other projects into yours if their license permits - follow their rules or pick code that matches your license for release.
  • Implement a scripting engine. (Avoid using wrappers - middemant API libraries.)
  • Object Collision using non rectangular hitboxes, cached Bounding Boxes, Object Avoidance (Anticollision).
  • Eventually switch to XNA, or DirectX/Direct2D. DirectX and Direct2D wrappers are available as SlimDX and SharpDX. Both APIs are mostly identical to each other. Or you could try using OpenGL.
  • Take things one step at a time. Be Optimal.

History

  • June 25 , 2012: Original version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)