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.
Dim B_BUFFER = new Bitmap(Me.Clientsize.Width,Me.Clientsize.Height)
Dim G_BUFFER = new Graphics( B_BUFFER )
Dim G_TARGET = Me.CreateGraphics
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:
Me.SetStyle(System.Windows.Forms.ControlStyles.AllPaintingInWmPaint, True)
Me.SetStyle(System.Windows.Forms.ControlStyles.OptimizedDoubleBuffer, True)
Me.SetStyle(System.Windows.Forms.ControlStyles.UserPaint, FALSE)
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()
.
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:
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.
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
With G_BUFFER
If Antialiasing then
.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias
else
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:
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.
Me.ClientSize = DisplaySize
Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedSingle
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():
Me.SetStyle(System.Windows.Forms.ControlStyles.AllPaintingInWmPaint, True)
Me.SetStyle(System.Windows.Forms.ControlStyles.OptimizedDoubleBuffer, True)
Me.SetStyle(System.Windows.Forms.ControlStyles.UserPaint, False)
Me.ClientSize = DisplaySize
Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedSingle
Me.SetStyle(ControlStyles.FixedHeight, True)
Me.SetStyle(ControlStyles.FixedWidth, True)
The complete _renderloop
routine - includes initializing and disposing the Bitmap and two graphics surfaces:
Private Sub _renderloop()
Dim B_BUFFER = New Bitmap(Me.Clientsize.Width, Me.Clientsize.Height)
Dim G_BUFFER = New Graphics(B_BUFFER)
Dim G_TARGET = Me.CreateGraphics
G_TARGET.Clear(Color.SlateGray)
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
With G_BUFFER
If AntiAliasing Then
.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias
Else
End If
.CompositingMode = Drawing2D.CompositingMode.SourceOver
.CompositingQuality = Drawing2D.CompositingQuality.HighSpeed
.InterpolationMode = Drawing2D.InterpolationMode.Low
.PixelOffsetMode = Drawing2D.PixelOffsetMode.Half
End With
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
G_TARGET.Dispose()
G_BUFFER.Dispose()
B_BUFFER.Dispose()
End Sub
The Loop itself without comments to see the event being raised and target update without comment clutter:
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:
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:
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.