473,388 Members | 1,496 Online
Bytes | Software Development & Data Engineering Community
Post Job

Home Posts Topics Members FAQ

Join Bytes and contribute your articles to a community of 473,388 developers and data experts.

Simple Windows Game - Part 1: A Game Framework

GaryTexmo
1,501 Expert 1GB
I've decided to do a series of insights based on creating a simple windows game using C# and WinForms. There are other methods of approaching this task but I've chosen to try to keep things fairly simple and demonstrate how one might make a game. More experienced developers will note some similarities between my approach here and how Microsoft XNA does things. This is intentional for two reasons...
  1. XNA is an excellent hobbyist framework that makes it quite easy to create simple games. People reading this will hopefully be able to transition easily to it.
  2. I find the XNA class structure fairly natural, so I've tried to emulate it here for ease. I'm also doing this in WinForms as opposed to XNA in order to avoid having readers worry about one more thing. The aim is an introductory article so I'm hoping the things covered here will allow users to learn a few basics and create a few games to get a feel for things.

This series will broken up into three parts, which are...
  • Part 1: A Game Framework - This will take you through the process of creating a set of classes that you can use to build games. It will discuss a simple game loop and game elements, as well as basic input via the mouse.
  • Part 2: A Basic Bricks Game (To Be Written) - Here we will take the Game Framework we created and make a very simple Bricks (or Breakout) type game. This will feature a ball bouncing around the screen and bouncing off, and eliminating bricks.
  • Part 3: A Complete Bricks Game (To Be Written) - Building off our simple Bricks game to provide levels, scoring, lives, and some kind of user interface.

NOTE: These articles have been written using Visual C# Express 2010(free download) and all code is compiled against .NET 3.5. Also, the complete code for each article will be attached at the end. You may wish to download this yourself and read through the article with it open, or you may want to follow along the article writing your own code. Your choice! However, please keep in mind that I'm by no means perfect... I'll likely make a few mistakes. The attached code will run but I may miss a step along the way in the article. If so, please feel free to let me know! Finally, this article also assumes you have a reasonably good understanding of C# programming and high school mathematics. There's nothing terribly tricky in here so it shouldn't be hard to pick up, but I wanted to make sure my target audience was stated :)

With that, lets dive right in, shall we? The very first thing we're going to need is somewhere to draw our game. Almost all visual objects in Windows have a Paint event. This event is triggered when the object's OnPaint method runs. We generally have two options... we can either add an event handler to that object's Paint event and do all our drawing in there, or we can inherit from an object and override it's OnPaint method. The approach you use is up to you; however, as this article is focusing on creating a framework, I'm choosing to create a new class which will inherit from System.Windows.Forms.Panel.

So first thing's first, lets create a new Windows Forms Application in Visual Studio, I've called mine BricksExample but you can call it whatever as we won't get to the Bricks part until the next article. Then, create a new Class Library project for that solution and call it GameLibrary. You can delete the Class1.cs file Visual Studio creates for you, but create a new class and call it GamePanel. We'll want to make this class inherit from Panel, and we'll also make it an abstract class as the intention here is that game classes will inherit from GamePanel and implement the appropriate methods.

You should have the following code in GamePanel.cs...
Expand|Select|Wrap|Line Numbers
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Windows.Forms;
  6. using System.Drawing;
  7.  
  8. namespace GameLibrary
  9. {
  10.     public abstract class GamePanel : Panel
  11.     {
  12.     }
  13. }
(You may have different using statements, that's fine... these are the ones we will eventually need to complete this class)

Now the very first thing we're going to want to do is recognize that the drawable area of a windows Form is actually smaller than the Form itself. This makes sense, but it's also a bit of a pain in the butt for us as we typically want to say, "Our game will be X pixels wide by Y pixels high!" If we set our Form to those dimensions, our drawable area will be slightly less than that because things like the height of the title bar and the border will be subtracted from it. Fortunately, we can make a relatively simple method to get what our Form size should be in order to contain a drawable area. We will make use of the SystemInformation class in order to obtain some information about the sizes of our title bar and border. We make the following method...

Expand|Select|Wrap|Line Numbers
  1. public static Size CalculateWindowBounds(Size gameRectangle, FormBorderStyle borderStyle)
  2. {
  3.     Size s = gameRectangle;
  4.  
  5.     switch (borderStyle)
  6.     {
  7.         case FormBorderStyle.Sizable:
  8.             s.Width += SystemInformation.FrameBorderSize.Width * 2;
  9.             s.Height += SystemInformation.CaptionHeight + SystemInformation.FrameBorderSize.Height * 2;
  10.             break;
  11.         case FormBorderStyle.FixedDialog:
  12.             s.Width += SystemInformation.FixedFrameBorderSize.Width * 2;
  13.             s.Height += SystemInformation.CaptionHeight + SystemInformation.FixedFrameBorderSize.Height * 2;
  14.             break;
  15.     }
  16.  
  17.     return s;
  18. }
This code handles two border style cases that we would likely use... if you wish to support more, feel free to add them. The general idea is that we take the desired game size and add to the size and width twice the width of the border for that particular style. We also add the height of the caption to the game height.

Lets quickly test this... in your windows form project, ensure you add a refrence to GameLibrary (right-click References and select Add Reference, then choose GameLibrary from the Projects tab). Don't forget to add a using statement either! Now, in your Form's constructor, below the call to InitializeComponent, you can add the following...

Expand|Select|Wrap|Line Numbers
  1. this.Size = GamePanel.CalculateWindowBounds(new Size(800, 600), this.FormBorderStyle);
  2. Console.WriteLine(this.ClientRectangle);
Run your project. Regardless of what size it was set to in the designer, it will now be sized correctly. Check the output tab in the debugger and ensure the client rectangle is set to 800 by 600. You can remove that WriteLine statement for future runs. Also, using the designer, you may wish to change the FormBorderStyle to FixedDialog and set MaximizeBox to False. Rescaling all our objects to a different window size is a bit of a pain, so we just won't allow the form to be rezied :)

Now, we're going to want to flesh out our GamePanel a bit more... since we're going to be doing drawing on it, we're going to want to disable flicker. We need to do two things here. First, we'll want to override the Panel's CreateParams property to add in the WS_EX_COMPOSITED flag, though through personal experimentation I've found that this causes negative effects on older operating systems such as Windows XP so we'll do a check before we set it. Also, we're going to want to set the GamePanel's DoubleBuffered property to true.

For CreateParams, add the following to GamePanel...
Expand|Select|Wrap|Line Numbers
  1. protected override CreateParams CreateParams
  2. {
  3.     get
  4.     {
  5.         // This stops the control from flickering when it draws
  6.         CreateParams cp = base.CreateParams;
  7.  
  8.         // Only allow this on vista and higher as lower versions seem to not draw lines with this option.
  9.         if (Environment.OSVersion.Version.Major >= 6)
  10.             cp.ExStyle |= 0x02000000; // (this is WS_EX_COMPOSITED)
  11.  
  12.         return cp;
  13.     }
  14. }
Then, create a constructor for GamePanel and in that constructor, set the DoubleBuffered property to true. Since we're creating our constructor, lets go ahead and create them with a few parameters that we'll be needing later. Create a constant in your class and call it DEFAULT_FPS, setting it to 60. This will be the default frames per second our game will run at. This will be explained later. Then we can create two constructors as follows...

Expand|Select|Wrap|Line Numbers
  1. public GamePanel(int initialWidth, int initialHeight)
  2.     : this(DEFAULT_FPS, initialWidth, initialHeight)
  3. {
  4. }
  5.  
  6. public GamePanel(int fps, int gameWidth, int gameHeight)
  7.     : base()
  8. {
  9.     this.DoubleBuffered = true;
  10.     this.FramesPerSecond = fps;
  11.  
  12.     m_initialSize = new SizeF(gameWidth, gameHeight);
  13. }
Note that the constructor that just takes the size parameters will simply call the three parameter constructor using our DEFAULT_FPS value. This allows us to put all our consturctor logic in one place.

We will also need two private members...
Expand|Select|Wrap|Line Numbers
  1. private int m_fps = 0;
  2. private SizeF m_initialSize;
... and a few public properties...
Expand|Select|Wrap|Line Numbers
  1. public int FramesPerSecond
  2. {
  3.     get { return m_fps; }
  4.     set
  5.     {
  6.         m_fps = value;
  7.     }
  8. }
  9.  
  10. public SizeF InitialGameSize
  11. {
  12.     get { return m_initialSize; }
  13. }
I'll explain what FPS does shortly but InitialSize will keep track of what size the game was when it was created. This is useful if our window size changes... and might come in handy for something like scaling. This article doesn't go into it, but you can read another article I wrote on the subject, http://bytes.com/topic/c-sharp/insig...ng-resolutions, if you want more information on how you might use this.

The last thing we'll do to GamePanel before we test it out is we will add a couple of abstract methods to it. The two methods in question will be called OnUpdate and OnDraw. OnUpdate will be where we update all of our game logic. This might be when we move something in our game or when a game should change state. OnDraw is where we simply render our game, in it's current state. I will shortly be discussing a basic game loop but suffice it to say, our update method will need to know how much time has passed since the last update occurred, so it will take a TimeSpan parameter. Our Draw may want to know this, so we'll include it as well, but more importantly we'll also give it a Graphics object that it can use for drawing. A Graphics object can be found in the System.Drawing namespace, which provides excellent drawing tools that we can use for our game.

So to GamePanel, we add the following...
Expand|Select|Wrap|Line Numbers
  1. protected abstract void OnUpdate(TimeSpan elapsedTime);
  2.  
  3. protected abstract void OnDraw(TimeSpan elapsedTime, Graphics g);
Next, we need a way for our game to draw. The way Windows handles it's drawing is that when it detects a window, or portion of a window, needs to be redrawn a WM_PAINT event is triggered. A Form's WndProc method intercepts this and triggers the OnPaint method. This method in turn calls the Paint event as discussed above.

Now, windows doesn't want to be constantly refreshing window drawings so it will only trigger a WM_PAINT event when it needs to. This is typically when a window is rezied or another window is dragged over top and then moved away. This doesn't work at all for our purposes, we're making a game and games animate, so we're going to want our game to be able to redraw itself frequently. To do this, we're going to need to create a game loop.

I'm going to pause here for a second to mention that game loops are a rather technical science, and their application is actually heavily discussed. There are a lot of great articles floating about the internet that go into this in far more detail than I will. If you want to know more about the topic, I suggest you google it. It's definitely good stuff to know about.

Having said that, for the purposes of this article, I'm going to take a very simple approach where our update timer is going to be the same as our draw timer. This keeps things nice and simple but it's important to note that it isn't the greatest approach. Generally you would want your updates to happen as often as possible and your draws slightly less so, but whenever there is time to do so. The Game Loop articles discuss this in detail and there's some lengthy math involved in setting this up. So again, for the purposes of this article we're going to lock our update and draw calls to the same frames per second timer because it's easier.

To accomplish this, we're going to create a System.Windows.Forms.Timer object that will tick at an interval. Every time this timer Ticks, we'll update our game and then draw it. Using this method we will always want to draw after the update so that our game displays the most up to date state.

Lets create a couple of private member variables for GamePanel...

Expand|Select|Wrap|Line Numbers
  1. private Timer m_updateTimer = new Timer();
  2. private DateTime m_lastEngineCycle;
  3. private DateTime m_currentEngineCycle;
m_updateTimer is going to be our timer that will tick on an interval, m_currentEngineCycle will be the time of the current tick and m_lastEngineCycle will be the time of the last tick. This will allow us to generate a TimeSpan, letting our update and draw methods know how much time has passed since they last ran.

In our game's constructor, we will want to add an event handler to our timer's tick event, start the timer, and set the last engine update to right now. This will change our constructor to the following...

Expand|Select|Wrap|Line Numbers
  1. public GamePanel(int fps, int gameWidth, int gameHeight)
  2.     : base()
  3. {
  4.     this.DoubleBuffered = true;
  5.     this.FramesPerSecond = fps;
  6.  
  7.     m_initialSize = new SizeF(gameWidth, gameHeight);
  8.  
  9.     m_updateTimer.Tick += new EventHandler(HandleUpdateTick);
  10.     m_updateTimer.Start();
  11.     m_lastEngineCycle = DateTime.Now;
  12. }
We will also need to update our FramesPerSecond property so that it sets the timer interval according to our frames per second. Now, the timer ticks on an interval that is an integer number of milliseconds. If we have a value that is frames per second, we want to change that to the number of seconds that will pass per frame. Therefore, the timer interval will always be 1 / frames_per_second, seconds. Multiply that by 1000 to get the number of milliseconds.

Expand|Select|Wrap|Line Numbers
  1. public int FramesPerSecond
  2. {
  3.     get { return m_fps; }
  4.     set
  5.     {
  6.         m_fps = value;
  7.  
  8.         m_updateTimer.Interval = (int)Math.Round((1f / (float)m_fps * 1000f));
  9.     }
  10. }
Our event handler for our tick event will be fairly simple. All we will do is call our update method with the time since the last update and then call the Invalidate method, which is a method on a Control object (which Panel inherits from, and we inherit from Panel) that triggers a WM_PAINT event, in turn causing a draw to occur. Our code is as follows...

Expand|Select|Wrap|Line Numbers
  1. private void HandleUpdateTick(object sender, EventArgs e)
  2. {
  3.     m_currentEngineCycle = DateTime.Now;
  4.  
  5.     OnUpdate(m_currentEngineCycle - m_lastEngineCycle);
  6.  
  7.     this.Invalidate();
  8.  
  9.     m_lastEngineCycle = m_currentEngineCycle;
  10. }
Note that we get the current time at the start of the update cycle and use that for our calculations so that update (and draw) will have the correct time span. If we used DateTime.Now for both calculations, we'd lose a tiny bit of time for however long it took to perform the update and for windows to trigger the invalidate call.

Now we need to override the OnPaint method to make sure our draw is triggered. At the same time, lets also override the OnMouseEnter and OnMouseLeave methods in order to hide the Windows cursor. You may wish to omit this part but generally, you don't want the windows arrow pointer over top of your game, you'll likely want to draw your own pointer, if you even want to draw one at all.

Expand|Select|Wrap|Line Numbers
  1. protected override void OnMouseEnter(EventArgs e)
  2. {
  3.     base.OnMouseEnter(e);
  4.  
  5.     Cursor.Hide();
  6. }
  7.  
  8. protected override void OnMouseLeave(EventArgs e)
  9. {
  10.     base.OnMouseLeave(e);
  11.  
  12.     Cursor.Show();
  13. }
  14.  
  15. protected override void OnPaint(PaintEventArgs e)
  16. {
  17.     base.OnPaint(e);
  18.  
  19.     OnDraw(m_currentEngineCycle - m_lastEngineCycle, e.Graphics);
  20. }
Ok, now assuming you've written the code correctly and that I haven't screwed anything up when pasting, we should be ready to do a simple test. In your windows project (ie, not GameLibrary but instead where your main form resides), create a new class and have that class inherit from GamePanel. I called mine TestGame. You will need to create a constructor for it as we haven't allowed a parameterless constructor in our GamePanel (you can if you wish, up to you). We will also need to implement the two abstract methods of GamePanel. Inside the OnDraw implmentation, lets do something simple to make sure it functions correctly.

Expand|Select|Wrap|Line Numbers
  1. public class TestGame : GamePanel
  2. {
  3.     public TestGame(int width, int height)
  4.         : base(width, height)
  5.     {
  6.     }
  7.  
  8.     #region GamePanel Implementation
  9.     protected override void OnUpdate(TimeSpan elapsedTime)
  10.     {
  11.     }
  12.  
  13.     protected override void OnDraw(TimeSpan elapsedTime, System.Drawing.Graphics g)
  14.     {
  15.         g.FillRectangle(Brushes.Black, this.ClientRectangle);
  16.  
  17.         g.FillRectangle(Brushes.Red, 25, 25, 300, 150);
  18.     }
  19.     #endregion
  20. }
Now, in our main form, we'll need to create a new instance of TestGame and give it a FramesPerSecond value. I also like to set the Dock to DockStyle.Fill so it will take up all the drawable area of our form, which we set before. My code for my main form is as follows...

Expand|Select|Wrap|Line Numbers
  1. public partial class Form1 : Form
  2. {
  3.     private const int GAME_WIDTH = 800;
  4.     private const int GAME_HEIGHT = 600;
  5.  
  6.     private TestGame m_game = null;
  7.  
  8.     public Form1()
  9.     {
  10.         InitializeComponent();
  11.  
  12.         this.Size = GamePanel.CalculateWindowBounds(new Size(GAME_WIDTH, GAME_HEIGHT), this.FormBorderStyle);
  13.  
  14.         m_game = new TestGame(GAME_WIDTH, GAME_HEIGHT);
  15.         m_game.FramesPerSecond = 60;
  16.         m_game.Dock = DockStyle.Fill;
  17.  
  18.         this.Controls.Add(m_game);
  19.     }
  20. }
Running this shows me a black window with a red rectangle on it. Lets also do a quick test to make sure animation works correctly. This will also allow me to introduce the concept of how animation will work in our game.

In our TestGame, lets create a member variable for a radius and set it to 25 by default.

Expand|Select|Wrap|Line Numbers
  1. private float m_radius = 25f;
(NOTE: It's generally best to use floats... pixels resolve to integer values but by using floats we can ensure a more precise location. This results in less jumpy animation.)

Now lets change our OnDraw code to the following...

Expand|Select|Wrap|Line Numbers
  1. g.FillRectangle(Brushes.Black, this.ClientRectangle);
  2.  
  3. g.FillEllipse(Brushes.Red, this.ClientRectangle.Width / 2f - m_radius, this.ClientRectangle.Height / 2f - m_radius, m_radius * 2f, m_radius * 2f);
This is going to draw a red circle in the middle of our game area.

Now in the OnUpdate method, we can make changes to our radius so that the resulting drawing will animate. Note, there are two ways we can modify radius... we can simply add to it, or we can add an amount to it based on the amount of time that has passed. The simplest way is to just add to the radius...

Expand|Select|Wrap|Line Numbers
  1. protected override void OnUpdate(TimeSpan elapsedTime)
  2. {
  3.     m_radius += 1;
  4. }
Running this code will show our circle growing ever larger. Lets do a quick change so that it doesn't grow infinitely... we'll make another member variable called m_speed and set it to 1.

Expand|Select|Wrap|Line Numbers
  1. private float m_speed = 1f;
Then we'll add speed to radius instead. We can also do a check to see if radius has grown to a certain amount, at which point we can change speed to negative which will cause the circle to shrink. Again, we can check to see if the radius has shrunk to a certain amount, then change speed to a positive.

Expand|Select|Wrap|Line Numbers
  1. protected override void OnUpdate(TimeSpan elapsedTime)
  2. {
  3.     m_radius += m_speed;
  4.  
  5.     if (m_radius >= this.ClientRectangle.Height / 2f || m_radius <= 25f)
  6.         m_speed *= -1;
  7. }
Running this will show our circle growing until it is as big as the form is high and then shrinking back down to a radius of 25, at which point it will grow again.

Now lets talk about how we're modifying the radius. Run the program and take note of the rate at which the circle changes size. Then lets go to our main form and change where we set the FramesPerSecond to 60 to make it 10 instead. Run the program. Our circle grows much more slowly. Set the FramesPerSecond to 120 and note that the circle grows very quickly. Set it back to 60 before moving on.

So the rate at which our circle changes is directly tied to the amount of frames per second we have, or more specifically, the number of times we update per second. This is generally undesirable as we can't always guarantee that rate... windows might take a little longer to get around to making that call, a higher priority process might bump ours so things take a little longer to run, or our system might be a little bogged down. This is why our update method carries with it a TimeSpan parameter that tells us how long it's been since the last update. We can use this to change the values of our objects based on time, so our objects move at constant rates independent of how many updates per second there are. We can then have our objects move via rates... ie, a number of pixels per second. We can multiply that by the number of seconds that have passed to obtain how many pixels our object moved in that period of time.

Using either of these methods, it's important to note that it's possible for objects to skip past set points because they are moving fast enough that one update they're before the set point and the next they're past. We can account for this and move our objects accordingly. I've changed my OnUpdate to the following...

Expand|Select|Wrap|Line Numbers
  1. protected override void OnUpdate(TimeSpan elapsedTime)
  2. {
  3.     m_radius += m_speed * (float)elapsedTime.TotalSeconds;
  4.  
  5.     if (m_radius > this.ClientRectangle.Height / 2f) m_radius = this.ClientRectangle.Height / 2f;
  6.     else if (m_radius < 25f) m_radius = 25f;
  7.  
  8.     if (m_radius >= this.ClientRectangle.Height / 2f || m_radius <= 25f)
  9.         m_speed *= -1;
  10. }
... and set my m_speed value to 60f, which means we will move 60 pixels in a second. This is roughly equivalent to what we had before. Our speed was one, but we updated 60 times in a second, so we moved at 60 pixels per second. Now we have a speed of 60 pixels per second, we multiply it by the number of seconds that have passed (which generally be close to 1/60, but possibly a little more than that depending on when the OS got around to making our update call), and now we have the amount the radius has changed by.

We check to see if the radius has exceeded our set points and if so, we set then to that point appropriately. This means if, between one tick and the next, the radius becomes bigger than the height of the form, we just set it to the height of the form so it doesn't look funny. Note that this is still slightly off but again, for the purposes of this article I'm taking the easy way out :D That said, I'll briefly discuss the correct way to do it. You would want to calculate how much time it would take for the object to get to the set point, then move the object to that set point. Then you would perform your reflection and move your object the remaining amount of time in the opposite direction. This is difficult though because, in an actual game, you could run into other objects after you change direction so it can create difficulties. So again, for the purposes of this article, we'll take the easy way out and just set the position to the break point and ignore however much farther it would have moved.

Run this code now... you should see your circle growing and shrinking at a rate roughly similar to what you saw before. Now go and play with your FramesPerSecond value. The circle should grow at the same rate, but will be smooth or choppy depending on what you set the FramesPerSecond to. A FramesPerSecond setting of 60 is generally good enough for our purposes, so I generally use that.

Next, we're going to provide a means of letting our GamePanel know where the mouse is. A Panel alredy has a method on it called MousePosition, but this is the position of the mouse relative to the entire screen. We're going to want to make sure the mouse position of our game doesn't go outside the bounds of our game. Our mouse can go anywhere, of course, the the position according to the game should be inside the game's bounds. Also, a Panel has a method called MouseButtons, which gives the mouse buttons that are currently pressed. Again, if the cursor is outside the bounds of of our game, we don't want to show any buttons as being pressed.

In our GameLibrary project, create a file (class) and call it Mouse.cs. Inside this file we will actually create two objects, a struct called MouseState which will hold information about the current state of the mouse, and a class called Mouse which will retrieve the current state for our GamePanel. First, the struct. We want the position, so it will need to have a PointF property, and we also want the buttons pressed, so we need a System.Windows.Forms.MouseButtons property. Then we will also provide methods to see whether or not a button is up or down.

Expand|Select|Wrap|Line Numbers
  1. public struct MouseState
  2. {
  3.     private PointF m_position;
  4.     private MouseButtons m_buttons;
  5.  
  6.     public MouseState(PointF position, MouseButtons buttons)
  7.     {
  8.         m_position = position;
  9.         m_buttons = buttons;
  10.     }
  11.  
  12.     public PointF Position { get { return m_position; } }
  13.     public MouseButtons Buttons { get { return m_buttons; } }
  14.  
  15.     public bool IsButtonDown(MouseButtons button)
  16.     {
  17.         return ((this.Buttons & button) == button);
  18.     }
  19.  
  20.     public bool IsButtonUp(MouseButtons button)
  21.     {
  22.         return !IsButtonDown(button);
  23.     }
  24. }
We use read-only properties because we don't want to allow the changing of the mouse position (which is a whole other process anyway, one which I won't address in this article). The MouseButtons object is simply a state where each mouse button is bit-wise OR'd in, so we can use bit-wise AND to check to see if a button has been pressed.

Next, we'll create the Mouse class. This class will need to keep a reference to the GamePanel that owns it as it will need check to make sure the mouse is inside the bounds of that GamePanel. We'll create a method on Mouse called GetState which will simply return the state of the mouse. We will build that state according to the rules we've defined.

Expand|Select|Wrap|Line Numbers
  1. public class Mouse
  2. {
  3.     private GamePanel m_hostPanel = null;
  4.  
  5.     public Mouse(GamePanel hostPanel)
  6.     {
  7.         if (hostPanel == null)
  8.             throw new ArgumentNullException("hostPanel");
  9.  
  10.         m_hostPanel = hostPanel;
  11.     }
  12.  
  13.     public MouseState GetState()
  14.     {
  15.         PointF hostLoc = m_hostPanel.PointToScreen(m_hostPanel.Location);
  16.         PointF cursorLoc = GamePanel.MousePosition;
  17.  
  18.         PointF mouseLoc = new PointF(cursorLoc.X - hostLoc.X, cursorLoc.Y - hostLoc.Y);
  19.  
  20.         MouseButtons buttons = GamePanel.MouseButtons;
  21.         if (!m_hostPanel.ClientRectangle.Contains((int)mouseLoc.X, (int)mouseLoc.Y))
  22.             buttons = MouseButtons.None;
  23.  
  24.         if (mouseLoc.X < 0) mouseLoc.X = 0;
  25.         else if (mouseLoc.X > m_hostPanel.Width) mouseLoc.X = m_hostPanel.Width;
  26.  
  27.         if (mouseLoc.Y < 0) mouseLoc.Y = 0;
  28.         else if (mouseLoc.Y > m_hostPanel.Height) mouseLoc.Y = m_hostPanel.Height;
  29.  
  30.         return new MouseState(mouseLoc, buttons);
  31.     }
  32. }
Note that a Panel object has a method on it called PointToScreen, which will translate a point on that Panel to where it would be on the screen. In this way, we get the value of the mouse on the screen and subtract where the host is. This tells us where the mosue is relative to its hosting panel. Then we check to see if it is outside the bounds of that panel and if so, lock it to the appropriate value. We also get the buttons and, if the mouse is outside the panel, set the buttons to MouseButtons.None.

Now, in our GamePanel object, we can create a member variable for our mouse, initialize it in our constructor, and expose it via a property.

Expand|Select|Wrap|Line Numbers
  1. private Mouse m_mouse = null;
Expand|Select|Wrap|Line Numbers
  1. public GamePanel(int fps, int gameWidth, int gameHeight)
  2.     : base()
  3. {
  4.     this.DoubleBuffered = true;
  5.     this.FramesPerSecond = fps;
  6.  
  7.     m_initialSize = new SizeF(gameWidth, gameHeight);
  8.  
  9.     m_mouse = new Mouse(this);
  10.  
  11.     m_updateTimer.Tick += new EventHandler(HandleUpdateTick);
  12.     m_updateTimer.Start();
  13.     m_lastEngineCycle = DateTime.Now;
  14. }
Expand|Select|Wrap|Line Numbers
  1. public Mouse Mouse
  2. {
  3.     get { return m_mouse; }
  4. }
Now we can retrieve information about the mouse. In our TestGame object, lets do a few little tests. We can add the following code to the bottom of our OnDraw method...

Expand|Select|Wrap|Line Numbers
  1. MouseState ms = this.Mouse.GetState();
  2. g.DrawString(ms.Buttons.ToString(), this.Font, Brushes.White, 0, 0);
  3.  
  4. g.FillRectangle(Brushes.Pink, ms.Position.X - 1, ms.Position.Y - 1, 2, 2);
This will get the current mouse state, draw the value of Buttons, and draw a small rectangle around the cursor position. Run this and you should see a little pink rectangle move around with your mouse, as well as the text in the upper left corner change as you click the buttons. Note that if you wanted to know if a mouse was clicked or released, you would keep track of the last state of the mouse. A button was just pressed if the current state has that button down and the last state has that button up. Vice versa for a button having just been released.

We're almost done! We're going to create three more objects in our GameLibrary and these will define different kinds of elements that we might want to use in our game, and will serve as a guide. You may wish to create more of these, or change them to suit your purposes.

To start, we're going to create an interface that will tell us what methods an element in our game must have. As our game does two core things, an update and a draw, it's natural that our game element must have this as well. So create a new class in your GameLibrary project and define it as such...

Expand|Select|Wrap|Line Numbers
  1. public interface IGameElement
  2. {
  3.     void Draw(TimeSpan elapsedTime, Graphics g);
  4.     void Update(TimeSpan elapsedTime);
  5. }
Any object inheriting from IGameElement must have these methods, and this means that we can keep lists of IGameElement objects in our game for different things we want. For example, lets say we had a pool game. We might keep a list of IGameElements that were pool balls, these being classes called PoolBall that inherited from IGameElement. We would then simply need to just call the Update method in our OnUpdate and the Draw method in our OnDraw, passing the appropriate parameters.

Note that we could also add a list of IGameElements to our GamePanel itself and have everything operate from there, using GamePanel to automatically update and draw each child object it takes care of. This is definitely viable and XNA takes this very same approach; however, my personal perference is to not use this method. I like to have more control over the draw and update order of my objects, and I also like to be able to process lists of objects separately. For example, in the previously mentioned pool game example we could store all the balls, cues, and table objects in a list that GamePanel owns and will take care of for us automatically. The problem is that if we ever wanted to do a check on just the balls, we would need to process the entire list. That doesn't seem so bad in this example, but what if there were a lot of background objects that we don't even care about? It's just wasted processor time. So to that end, I like to track my object lists separately. You may wish to go with the full list in GamePanel and that's certainly up to you :)

Anyway, now that we have an interface for IGameElement, lets create a few abstract classes that we can build from later. Generally, games have objects that move and objects that don't move, so lets build from there. We'll create two more objects... StaticElement and DynamicElement. StaticElement will represent an object that only has a position, it doesn't really do anything except hang out. To that end, it will have a RectangleF property that will keep track of it's position and size. We'll also give it a Colour property for funsies. Lastly, we're going to want to make sure that this object has a reference back to the game that created it. The reason for this is that an object might want to know about other objects around it. By giving it access to it's host game, it can look up properties to access information. Again, using the Pool Game example, a Pocket object might want to know if a Ball object has gone over top of it, so the Pocket object's Update method might look at it's host game's Balls property (assuming it exposes one). We also set up some constructors to make object creation easier and, because I'm a nice guy, I created a few extra properties to expose various parts of the Rectangle, such as Position and Size. The code for StaticElement is as follows...

Expand|Select|Wrap|Line Numbers
  1. public abstract class StaticElement : IGameElement
  2. {
  3.     #region protected Members
  4.     private GamePanel m_owningGame = null;
  5.     #endregion
  6.  
  7.     #region ProtectedProperties
  8.     protected GamePanel OwningGame
  9.     {
  10.         get { return m_owningGame; }
  11.     }
  12.     #endregion
  13.  
  14.     #region Public Properties
  15.     public Color Colour { get; set; }
  16.  
  17.     public RectangleF Rectangle { get; set; }
  18.  
  19.     public PointF Position
  20.     { 
  21.         get { return this.Rectangle.Location; }
  22.         set
  23.         {
  24.             this.Rectangle = new RectangleF(value, this.Rectangle.Size);
  25.         }
  26.     }
  27.  
  28.     public SizeF Size
  29.     {
  30.         get { return this.Rectangle.Size; }
  31.         set
  32.         {
  33.             this.Rectangle = new RectangleF(this.Rectangle.Location, value);
  34.         }
  35.     } 
  36.     #endregion
  37.  
  38.     #region Constructor(s)
  39.     public StaticElement(GamePanel owningGame, PointF position, SizeF size)
  40.         : this(owningGame, new RectangleF(position, size))
  41.     {
  42.     }
  43.  
  44.     public StaticElement(GamePanel owningGame, RectangleF rectangle)
  45.     {
  46.         m_owningGame = owningGame;
  47.         this.Rectangle = rectangle;
  48.         this.Colour = Color.Red;
  49.     }
  50.     #endregion
  51.  
  52.     #region IGameElement Implementation
  53.     public abstract void Draw(TimeSpan elapsedTime, Graphics g);
  54.     public abstract void Update(TimeSpan elapsedTime);
  55.     #endregion
  56. }
I just chose red as a default colour, you can do whatever you want. Note, while the name of this object implies that it's static, it can be moved... however, it's not really set up to move in any meaningful way other than simply changing the position manually. An example of how we might use this could be as our mouse cursor... Lets update our TestGame to handle the mouse cursor a little bit better.

First, we need to create a new class in our windows application called MosueCursor. This class will inherit from StaticElement and will this need to implement both abstract methods. We'll also create a couple of constructors. In the Draw method, we can simply do what we were doing before, which is draw a filled rectangle, in pink, using the object's Rectangle property. For Update, we can get the MouseState from the owning game and update the position accordingly. Thus, we have..

Expand|Select|Wrap|Line Numbers
  1. public class MouseCursor : StaticElement
  2. {
  3.     public MouseCursor(GamePanel owningGame, Rectangle rect)
  4.         : base(owningGame, rect)
  5.     {
  6.     }
  7.  
  8.     public MouseCursor(GamePanel owningGame, PointF position, SizeF size)
  9.         : base(owningGame, position, size)
  10.     {
  11.     }
  12.  
  13.     #region IGameElement Implementation
  14.     public override void Draw(TimeSpan elapsedTime, System.Drawing.Graphics g)
  15.     {
  16.         g.FillRectangle(Brushes.Pink, this.Rectangle);
  17.     }
  18.  
  19.     public override void Update(TimeSpan elapsedTime)
  20.     {
  21.         MouseState ms = OwningGame.Mouse.GetState();
  22.         this.Position = ms.Position;
  23.     }
  24.     #endregion
  25. }
Now in TestGame, we create a new private member variable for our cursor, initialize it in our constructor, then call Draw and Update in the appropriate places. My TestGame code now looks like this...

Expand|Select|Wrap|Line Numbers
  1. public class TestGame : GamePanel
  2. {
  3.     private float m_radius = 25f;
  4.     private float m_speed = 60f;
  5.     private MouseCursor m_cursor = null;
  6.  
  7.     public TestGame(int width, int height)
  8.         : base(width, height)
  9.     {
  10.         m_cursor = new MouseCursor(this, new Rectangle(0, 0, 2, 2));
  11.     }
  12.  
  13.     #region GamePanel Implementation
  14.     protected override void OnUpdate(TimeSpan elapsedTime)
  15.     {
  16.         m_radius += m_speed * (float)elapsedTime.TotalSeconds;
  17.  
  18.         if (m_radius > this.ClientRectangle.Height / 4f) m_radius = this.ClientRectangle.Height / 4f;
  19.         else if (m_radius < 25f) m_radius = 25f;
  20.  
  21.         if (m_radius >= this.ClientRectangle.Height / 4f || m_radius <= 25f)
  22.             m_speed *= -1;
  23.  
  24.         m_cursor.Update(elapsedTime);
  25.     }
  26.  
  27.     protected override void OnDraw(TimeSpan elapsedTime, System.Drawing.Graphics g)
  28.     {
  29.         g.FillRectangle(Brushes.Black, this.ClientRectangle);
  30.  
  31.         g.FillEllipse(Brushes.Red, this.ClientRectangle.Width / 2f - m_radius, this.ClientRectangle.Height / 2f - m_radius, m_radius * 2f, m_radius * 2f);
  32.  
  33.         MouseState ms = this.Mouse.GetState();
  34.         g.DrawString(ms.Buttons.ToString(), this.Font, Brushes.White, 0, 0);
  35.  
  36.         m_cursor.Draw(elapsedTime, g);
  37.     }
  38.     #endregion
  39. }
Note the bolded sections. In this way, MouseCursor now takes care of itself. If we want to change anything about the way our cursor behaves, we can do it in the MouseCursor class and our TestGame code won't change.

The second object we identified as needing was DynamicElement. This will handle our objects that are intended to move. To facilitate this, we will give this object a PointF velocity and a PointF acceleration. These will govern how our element's position updates. Our DynamicElement is going to need a Position and Size as well, but we've already written that in StaticElement so lets just have DynamicElement inherit from that. The Update and Draw methods are already defined in StaticElement so we don't need to redefine them, but we can create a couple helper methods to give us the next velocity and the next position. My DynamicElement code is as follows...

Expand|Select|Wrap|Line Numbers
  1. public abstract class DynamicElement : StaticElement
  2. {
  3.     #region Public Properties
  4.     public PointF Velocity { get; set; }
  5.     public PointF Acceleration { get; set; }
  6.     #endregion
  7.  
  8.     #region Constructor(s)
  9.     public DynamicElement(GamePanel owningGame, PointF position, SizeF size, PointF velocity, PointF acceleration)
  10.         : base(owningGame, position, size)
  11.     {
  12.         this.Velocity = velocity;
  13.         this.Acceleration = acceleration;
  14.     }
  15.  
  16.     public DynamicElement(GamePanel owningGame, RectangleF rectangle, PointF velocity, PointF acceleration)
  17.         : base(owningGame, rectangle)
  18.     {
  19.         this.Velocity = velocity;
  20.         this.Acceleration = acceleration;
  21.     }
  22.     #endregion
  23.  
  24.     #region Public Methods
  25.     public PointF CalculateNextPosition(TimeSpan elapsedTime)
  26.     {
  27.         return 
  28.             new PointF(
  29.                 this.Rectangle.X + (float)elapsedTime.TotalSeconds * this.Velocity.X,
  30.                 this.Rectangle.Y + (float)elapsedTime.TotalSeconds * this.Velocity.Y
  31.             );
  32.     }
  33.  
  34.     public PointF CalculateNextVelocity(TimeSpan elapsedTime)
  35.     {
  36.         return
  37.             new PointF(
  38.                 this.Velocity.X + (float)elapsedTime.TotalSeconds * this.Acceleration.X,
  39.                 this.Velocity.Y + (float)elapsedTime.TotalSeconds * this.Acceleration.Y
  40.             );
  41.     }
  42.     #endregion
  43. }
Velocity is a change in position over time, so we can calculate the next position by multiplying the velocity by the total seconds that have elapsed, then adding that to the current location. Likewise, acceleration is the change in velocity over time so we can perform a similar calculation to obtain the next velocity, based on how much time has passed. Lets see an example of this in our TestGame.

First, remove everything to do with the expanding/contracting circle, but you can leave the cursor in place, as well as the draw call to set the background to black. Now, create a new class in your windows application project and call it Ball. I added a couple of Color properties to give a fill colour and an outline colour, then drew it appropraitely in Draw and updated the Velocity and Position in Update. Note that I always update Velocity first as Position updates based on that.

Expand|Select|Wrap|Line Numbers
  1. public class Ball : DynamicElement
  2. {
  3.     public Color FillColour
  4.     {
  5.         get { return base.Colour; }
  6.         set { base.Colour = value; }
  7.     }
  8.  
  9.     public Color OutlineColour { get; set; }
  10.  
  11.     public Ball(GamePanel owningGame, RectangleF rectangle, PointF velocity, PointF acceleration)
  12.         : base(owningGame, rectangle, velocity, acceleration)
  13.     {
  14.     }
  15.  
  16.     public Ball(GamePanel owningGame, PointF position, SizeF size, PointF velocity, PointF acceleration)
  17.         : base(owningGame, position, size, velocity, acceleration)
  18.     {
  19.     }
  20.  
  21.     #region IGameElement Implementation
  22.     public override void Draw(TimeSpan elapsedTime, Graphics g)
  23.     {
  24.         g.FillEllipse(new SolidBrush(this.FillColour), this.Rectangle);
  25.         g.DrawEllipse(new Pen(this.OutlineColour), this.Rectangle);
  26.     }
  27.  
  28.     public override void Update(TimeSpan elapsedTime)
  29.     {
  30.         this.Velocity = base.CalculateNextVelocity(elapsedTime);
  31.         this.Position = base.CalculateNextPosition(elapsedTime);
  32.     }
  33.     #endregion
  34. }
Now, in TestGame, we can create a ball object, initialize it in our constructor, then call draw and update appropriately. My TestGame now looks like...

Expand|Select|Wrap|Line Numbers
  1. public class TestGame : GamePanel
  2. {
  3.     private Ball m_ball = null;
  4.     private MouseCursor m_cursor = null;
  5.  
  6.     public TestGame(int width, int height)
  7.         : base(width, height)
  8.     {
  9.         m_cursor = new MouseCursor(this, new Rectangle(0, 0, 2, 2));
  10.         m_ball = new Ball(this, new RectangleF(10, 10, 25, 25), new PointF(50, 50), new PointF(0, 0))
  11.         {
  12.             FillColour = Color.DarkRed,
  13.             OutlineColour = Color.Red
  14.         };
  15.     }
  16.  
  17.     #region GamePanel Implementation
  18.     protected override void OnUpdate(TimeSpan elapsedTime)
  19.     {
  20.         m_ball.Update(elapsedTime);
  21.         m_cursor.Update(elapsedTime);
  22.     }
  23.  
  24.     protected override void OnDraw(TimeSpan elapsedTime, System.Drawing.Graphics g)
  25.     {
  26.         g.FillRectangle(Brushes.Black, this.ClientRectangle);
  27.  
  28.         m_ball.Draw(elapsedTime, g);
  29.         m_cursor.Draw(elapsedTime, g);
  30.     }
  31.     #endregion
  32. }
Running this will show a circle moving down and to the right until it goes off the screen. That's kind of boring, so lets make it bounce. Again, the benefit of writing our code like this is that we don't need to touch TestGame in order to change the behavour of our ball. We can do all our updates in the Ball class itself. Lets have the ball check to see if its new position has hit the edge of our game. If so, we will set the position to place the ball adjacent to the wall and change the direction appropriately.

My Ball code is now...
Expand|Select|Wrap|Line Numbers
  1. public class Ball : DynamicElement
  2. {
  3.     public Color FillColour
  4.     {
  5.         get { return base.Colour; }
  6.         set { base.Colour = value; }
  7.     }
  8.  
  9.     public Color OutlineColour { get; set; }
  10.  
  11.     public Ball(GamePanel owningGame, RectangleF rectangle, PointF velocity, PointF acceleration)
  12.         : base(owningGame, rectangle, velocity, acceleration)
  13.     {
  14.     }
  15.  
  16.     public Ball(GamePanel owningGame, PointF position, SizeF size, PointF velocity, PointF acceleration)
  17.         : base(owningGame, position, size, velocity, acceleration)
  18.     {
  19.     }
  20.  
  21.     #region IGameElement Implementation
  22.     public override void Draw(TimeSpan elapsedTime, Graphics g)
  23.     {
  24.         g.FillEllipse(new SolidBrush(this.FillColour), this.Rectangle);
  25.         g.DrawEllipse(new Pen(this.OutlineColour), this.Rectangle);
  26.     }
  27.  
  28.     public override void Update(TimeSpan elapsedTime)
  29.     {
  30.         this.Velocity = base.CalculateNextVelocity(elapsedTime);
  31.  
  32.         PointF nextPosition = base.CalculateNextPosition(elapsedTime);
  33.         PointF newVelocity = this.Velocity;
  34.  
  35.         // Check X
  36.         if (nextPosition.X < 0)
  37.         {
  38.             nextPosition.X = 0;
  39.             newVelocity.X *= -1;
  40.         }
  41.         else if (nextPosition.X + this.Size.Width > OwningGame.InitialGameSize.Width)
  42.         {
  43.             nextPosition.X = OwningGame.InitialGameSize.Width - this.Size.Width;
  44.             newVelocity.X *= -1;
  45.         }
  46.  
  47.         // Check Y
  48.         if (nextPosition.Y < 0)
  49.         {
  50.             nextPosition.Y = 0;
  51.             newVelocity.Y *= -1;
  52.         }
  53.         else if (nextPosition.Y + this.Size.Height > OwningGame.InitialGameSize.Height)
  54.         {
  55.             nextPosition.Y = OwningGame.InitialGameSize.Height - this.Size.Height;
  56.             newVelocity.Y *= -1;
  57.         }
  58.  
  59.         this.Position = nextPosition;
  60.  
  61.         if (this.Velocity != newVelocity)
  62.             this.Velocity = newVelocity;
  63.     }
  64.     #endregion
  65. }
(NOTE: I also changed my velocity in TestGame to be 200 pixels per second as it took forever for the damn ball to get to the edge at 50 pps. Use whatever value you like though, not everybody is as impatient as I am :P)

That's basically it. You now have a basic library for simple game development and hopefully an understanding on how to use it. I will attach two files to this article, the complete code for GameLibrary, as well as the complete code for my TestGame windows application (at the point of the end of this article). These will only be the projects themselves, you'll have to add them to a solution file on your own if you want to run them.

Please feel free to leave comments/suggestions. I will likely write the next article sometime in the next month. That article will discuss using this GameLibrary to make a simple Bricks game, so keep an eye out :)

Thanks for reading!
Attached Files
File Type: zip GameLibrary.zip (5.6 KB, 498 views)
File Type: zip TestArea.zip (10.7 KB, 460 views)
Feb 17 '12 #1
0 10401

Sign in to post your reply or Sign up for a free account.

Similar topics

2
by: Maarten | last post by:
Does anyone can help with this? For our goose game, we have to create the game board in a spiral. It may be a square instead of an oval or circle. Does anyone know a simple formula to do this...
3
by: Jean Stax | last post by:
Hi ! I try to make pretty straight forward operation, but it fails... I have .NET Framework 1.1 Configuration (version 1.1.4322.573) with VS 2003, where I developed a sample application. Now I...
3
by: J. Muenchbourg | last post by:
I'm using Windows XP Pro, and i want to install the .net framework, and use Asp.Net's Web Matrix Project as my editor. How would I configure my machine to see local .aspx webpages thru my...
1
by: Jeff Yang | last post by:
These days,I have read the artile "Safe, Simple Multithreading in Windows Forms, Part 1" on MSDN,and I also runed the example of it....
3
by: Sam | last post by:
I wonder whether I should install .net framework 1.1 with service pack 1 into MS Windows 2003 Server? My inquiries Application using ASP.Net 1.1 - Will it run on MS Windows 2003 Server without...
4
by: bob lambert | last post by:
Help I am trying to deploy to another pc a vb.net std 2002 windows form application. I am confused. I created a project - windows form I built form, compiled and debugged. I created a...
1
by: VictorT | last post by:
Hi All, I am trying to create a simple Windows form that lists a users' data one user at a time with the usual "Next" & "Previous" buttons. Upon loading the form, I am able to populate all...
0
by: Reidar | last post by:
I have made a simple windows form application and want to publish it on the web. This deployment package contains 3 files and a folder. When 'the installer' downloads the setup-file and run it, I...
0
by: Vishal Patil | last post by:
Check out a new and simple Python base genetic algorithm framework at http://vishpat.googlepages.com/pgap -- Motivation will almost always beat mere talent.
5
by: geoffbache | last post by:
Hi all, I have a small python script that doesn't depend on anything except the standard interpreter. I would like to convert it to a small .exe file on Windows that can distributed alone...
0
by: taylorcarr | last post by:
A Canon printer is a smart device known for being advanced, efficient, and reliable. It is designed for home, office, and hybrid workspace use and can also be used for a variety of purposes. However,...
0
by: Charles Arthur | last post by:
How do i turn on java script on a villaon, callus and itel keypad mobile phone
0
by: ryjfgjl | last post by:
If we have dozens or hundreds of excel to import into the database, if we use the excel import function provided by database editors such as navicat, it will be extremely tedious and time-consuming...
0
by: ryjfgjl | last post by:
In our work, we often receive Excel tables with data in the same format. If we want to analyze these data, it can be difficult to analyze them because the data is spread across multiple Excel files...
1
by: nemocccc | last post by:
hello, everyone, I want to develop a software for my android phone for daily needs, any suggestions?
0
by: Hystou | last post by:
There are some requirements for setting up RAID: 1. The motherboard and BIOS support RAID configuration. 2. The motherboard has 2 or more available SATA protocol SSD/HDD slots (including MSATA, M.2...
0
marktang
by: marktang | last post by:
ONU (Optical Network Unit) is one of the key components for providing high-speed Internet services. Its primary function is to act as an endpoint device located at the user's premises. However,...
0
by: Hystou | last post by:
Most computers default to English, but sometimes we require a different language, especially when relocating. Forgot to request a specific language before your computer shipped? No problem! You can...
0
Oralloy
by: Oralloy | last post by:
Hello folks, I am unable to find appropriate documentation on the type promotion of bit-fields when using the generalised comparison operator "<=>". The problem is that using the GNU compilers,...

By using Bytes.com and it's services, you agree to our Privacy Policy and Terms of Use.

To disable or enable advertisements and analytics tracking please visit the manage ads & tracking page.