- 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.
- 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
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Windows.Forms;
- using System.Drawing;
- namespace GameLibrary
- {
- public abstract class GamePanel : Panel
- {
- }
- }
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
- public static Size CalculateWindowBounds(Size gameRectangle, FormBorderStyle borderStyle)
- {
- Size s = gameRectangle;
- switch (borderStyle)
- {
- case FormBorderStyle.Sizable:
- s.Width += SystemInformation.FrameBorderSize.Width * 2;
- s.Height += SystemInformation.CaptionHeight + SystemInformation.FrameBorderSize.Height * 2;
- break;
- case FormBorderStyle.FixedDialog:
- s.Width += SystemInformation.FixedFrameBorderSize.Width * 2;
- s.Height += SystemInformation.CaptionHeight + SystemInformation.FixedFrameBorderSize.Height * 2;
- break;
- }
- return s;
- }
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
- this.Size = GamePanel.CalculateWindowBounds(new Size(800, 600), this.FormBorderStyle);
- Console.WriteLine(this.ClientRectangle);
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
- protected override CreateParams CreateParams
- {
- get
- {
- // This stops the control from flickering when it draws
- CreateParams cp = base.CreateParams;
- // Only allow this on vista and higher as lower versions seem to not draw lines with this option.
- if (Environment.OSVersion.Version.Major >= 6)
- cp.ExStyle |= 0x02000000; // (this is WS_EX_COMPOSITED)
- return cp;
- }
- }
Expand|Select|Wrap|Line Numbers
- public GamePanel(int initialWidth, int initialHeight)
- : this(DEFAULT_FPS, initialWidth, initialHeight)
- {
- }
- public GamePanel(int fps, int gameWidth, int gameHeight)
- : base()
- {
- this.DoubleBuffered = true;
- this.FramesPerSecond = fps;
- m_initialSize = new SizeF(gameWidth, gameHeight);
- }
We will also need two private members...
Expand|Select|Wrap|Line Numbers
- private int m_fps = 0;
- private SizeF m_initialSize;
Expand|Select|Wrap|Line Numbers
- public int FramesPerSecond
- {
- get { return m_fps; }
- set
- {
- m_fps = value;
- }
- }
- public SizeF InitialGameSize
- {
- get { return m_initialSize; }
- }
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
- protected abstract void OnUpdate(TimeSpan elapsedTime);
- protected abstract void OnDraw(TimeSpan elapsedTime, Graphics g);
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
- private Timer m_updateTimer = new Timer();
- private DateTime m_lastEngineCycle;
- private DateTime m_currentEngineCycle;
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
- public GamePanel(int fps, int gameWidth, int gameHeight)
- : base()
- {
- this.DoubleBuffered = true;
- this.FramesPerSecond = fps;
- m_initialSize = new SizeF(gameWidth, gameHeight);
- m_updateTimer.Tick += new EventHandler(HandleUpdateTick);
- m_updateTimer.Start();
- m_lastEngineCycle = DateTime.Now;
- }
Expand|Select|Wrap|Line Numbers
- public int FramesPerSecond
- {
- get { return m_fps; }
- set
- {
- m_fps = value;
- m_updateTimer.Interval = (int)Math.Round((1f / (float)m_fps * 1000f));
- }
- }
Expand|Select|Wrap|Line Numbers
- private void HandleUpdateTick(object sender, EventArgs e)
- {
- m_currentEngineCycle = DateTime.Now;
- OnUpdate(m_currentEngineCycle - m_lastEngineCycle);
- this.Invalidate();
- m_lastEngineCycle = m_currentEngineCycle;
- }
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
- protected override void OnMouseEnter(EventArgs e)
- {
- base.OnMouseEnter(e);
- Cursor.Hide();
- }
- protected override void OnMouseLeave(EventArgs e)
- {
- base.OnMouseLeave(e);
- Cursor.Show();
- }
- protected override void OnPaint(PaintEventArgs e)
- {
- base.OnPaint(e);
- OnDraw(m_currentEngineCycle - m_lastEngineCycle, e.Graphics);
- }
Expand|Select|Wrap|Line Numbers
- public class TestGame : GamePanel
- {
- public TestGame(int width, int height)
- : base(width, height)
- {
- }
- #region GamePanel Implementation
- protected override void OnUpdate(TimeSpan elapsedTime)
- {
- }
- protected override void OnDraw(TimeSpan elapsedTime, System.Drawing.Graphics g)
- {
- g.FillRectangle(Brushes.Black, this.ClientRectangle);
- g.FillRectangle(Brushes.Red, 25, 25, 300, 150);
- }
- #endregion
- }
Expand|Select|Wrap|Line Numbers
- public partial class Form1 : Form
- {
- private const int GAME_WIDTH = 800;
- private const int GAME_HEIGHT = 600;
- private TestGame m_game = null;
- public Form1()
- {
- InitializeComponent();
- this.Size = GamePanel.CalculateWindowBounds(new Size(GAME_WIDTH, GAME_HEIGHT), this.FormBorderStyle);
- m_game = new TestGame(GAME_WIDTH, GAME_HEIGHT);
- m_game.FramesPerSecond = 60;
- m_game.Dock = DockStyle.Fill;
- this.Controls.Add(m_game);
- }
- }
In our TestGame, lets create a member variable for a radius and set it to 25 by default.
Expand|Select|Wrap|Line Numbers
- private float m_radius = 25f;
Now lets change our OnDraw code to the following...
Expand|Select|Wrap|Line Numbers
- g.FillRectangle(Brushes.Black, this.ClientRectangle);
- g.FillEllipse(Brushes.Red, this.ClientRectangle.Width / 2f - m_radius, this.ClientRectangle.Height / 2f - m_radius, m_radius * 2f, m_radius * 2f);
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
- protected override void OnUpdate(TimeSpan elapsedTime)
- {
- m_radius += 1;
- }
Expand|Select|Wrap|Line Numbers
- private float m_speed = 1f;
Expand|Select|Wrap|Line Numbers
- protected override void OnUpdate(TimeSpan elapsedTime)
- {
- m_radius += m_speed;
- if (m_radius >= this.ClientRectangle.Height / 2f || m_radius <= 25f)
- m_speed *= -1;
- }
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
- protected override void OnUpdate(TimeSpan elapsedTime)
- {
- m_radius += m_speed * (float)elapsedTime.TotalSeconds;
- if (m_radius > this.ClientRectangle.Height / 2f) m_radius = this.ClientRectangle.Height / 2f;
- else if (m_radius < 25f) m_radius = 25f;
- if (m_radius >= this.ClientRectangle.Height / 2f || m_radius <= 25f)
- m_speed *= -1;
- }
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
- public struct MouseState
- {
- private PointF m_position;
- private MouseButtons m_buttons;
- public MouseState(PointF position, MouseButtons buttons)
- {
- m_position = position;
- m_buttons = buttons;
- }
- public PointF Position { get { return m_position; } }
- public MouseButtons Buttons { get { return m_buttons; } }
- public bool IsButtonDown(MouseButtons button)
- {
- return ((this.Buttons & button) == button);
- }
- public bool IsButtonUp(MouseButtons button)
- {
- return !IsButtonDown(button);
- }
- }
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
- public class Mouse
- {
- private GamePanel m_hostPanel = null;
- public Mouse(GamePanel hostPanel)
- {
- if (hostPanel == null)
- throw new ArgumentNullException("hostPanel");
- m_hostPanel = hostPanel;
- }
- public MouseState GetState()
- {
- PointF hostLoc = m_hostPanel.PointToScreen(m_hostPanel.Location);
- PointF cursorLoc = GamePanel.MousePosition;
- PointF mouseLoc = new PointF(cursorLoc.X - hostLoc.X, cursorLoc.Y - hostLoc.Y);
- MouseButtons buttons = GamePanel.MouseButtons;
- if (!m_hostPanel.ClientRectangle.Contains((int)mouseLoc.X, (int)mouseLoc.Y))
- buttons = MouseButtons.None;
- if (mouseLoc.X < 0) mouseLoc.X = 0;
- else if (mouseLoc.X > m_hostPanel.Width) mouseLoc.X = m_hostPanel.Width;
- if (mouseLoc.Y < 0) mouseLoc.Y = 0;
- else if (mouseLoc.Y > m_hostPanel.Height) mouseLoc.Y = m_hostPanel.Height;
- return new MouseState(mouseLoc, buttons);
- }
- }
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
- private Mouse m_mouse = null;
Expand|Select|Wrap|Line Numbers
- public GamePanel(int fps, int gameWidth, int gameHeight)
- : base()
- {
- this.DoubleBuffered = true;
- this.FramesPerSecond = fps;
- m_initialSize = new SizeF(gameWidth, gameHeight);
- m_mouse = new Mouse(this);
- m_updateTimer.Tick += new EventHandler(HandleUpdateTick);
- m_updateTimer.Start();
- m_lastEngineCycle = DateTime.Now;
- }
Expand|Select|Wrap|Line Numbers
- public Mouse Mouse
- {
- get { return m_mouse; }
- }
Expand|Select|Wrap|Line Numbers
- MouseState ms = this.Mouse.GetState();
- g.DrawString(ms.Buttons.ToString(), this.Font, Brushes.White, 0, 0);
- g.FillRectangle(Brushes.Pink, ms.Position.X - 1, ms.Position.Y - 1, 2, 2);
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
- public interface IGameElement
- {
- void Draw(TimeSpan elapsedTime, Graphics g);
- void Update(TimeSpan elapsedTime);
- }
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
- public abstract class StaticElement : IGameElement
- {
- #region protected Members
- private GamePanel m_owningGame = null;
- #endregion
- #region ProtectedProperties
- protected GamePanel OwningGame
- {
- get { return m_owningGame; }
- }
- #endregion
- #region Public Properties
- public Color Colour { get; set; }
- public RectangleF Rectangle { get; set; }
- public PointF Position
- {
- get { return this.Rectangle.Location; }
- set
- {
- this.Rectangle = new RectangleF(value, this.Rectangle.Size);
- }
- }
- public SizeF Size
- {
- get { return this.Rectangle.Size; }
- set
- {
- this.Rectangle = new RectangleF(this.Rectangle.Location, value);
- }
- }
- #endregion
- #region Constructor(s)
- public StaticElement(GamePanel owningGame, PointF position, SizeF size)
- : this(owningGame, new RectangleF(position, size))
- {
- }
- public StaticElement(GamePanel owningGame, RectangleF rectangle)
- {
- m_owningGame = owningGame;
- this.Rectangle = rectangle;
- this.Colour = Color.Red;
- }
- #endregion
- #region IGameElement Implementation
- public abstract void Draw(TimeSpan elapsedTime, Graphics g);
- public abstract void Update(TimeSpan elapsedTime);
- #endregion
- }
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
- public class MouseCursor : StaticElement
- {
- public MouseCursor(GamePanel owningGame, Rectangle rect)
- : base(owningGame, rect)
- {
- }
- public MouseCursor(GamePanel owningGame, PointF position, SizeF size)
- : base(owningGame, position, size)
- {
- }
- #region IGameElement Implementation
- public override void Draw(TimeSpan elapsedTime, System.Drawing.Graphics g)
- {
- g.FillRectangle(Brushes.Pink, this.Rectangle);
- }
- public override void Update(TimeSpan elapsedTime)
- {
- MouseState ms = OwningGame.Mouse.GetState();
- this.Position = ms.Position;
- }
- #endregion
- }
Expand|Select|Wrap|Line Numbers
- public class TestGame : GamePanel
- {
- private float m_radius = 25f;
- private float m_speed = 60f;
- private MouseCursor m_cursor = null;
- public TestGame(int width, int height)
- : base(width, height)
- {
- m_cursor = new MouseCursor(this, new Rectangle(0, 0, 2, 2));
- }
- #region GamePanel Implementation
- protected override void OnUpdate(TimeSpan elapsedTime)
- {
- m_radius += m_speed * (float)elapsedTime.TotalSeconds;
- if (m_radius > this.ClientRectangle.Height / 4f) m_radius = this.ClientRectangle.Height / 4f;
- else if (m_radius < 25f) m_radius = 25f;
- if (m_radius >= this.ClientRectangle.Height / 4f || m_radius <= 25f)
- m_speed *= -1;
- m_cursor.Update(elapsedTime);
- }
- protected override void OnDraw(TimeSpan elapsedTime, System.Drawing.Graphics g)
- {
- g.FillRectangle(Brushes.Black, this.ClientRectangle);
- g.FillEllipse(Brushes.Red, this.ClientRectangle.Width / 2f - m_radius, this.ClientRectangle.Height / 2f - m_radius, m_radius * 2f, m_radius * 2f);
- MouseState ms = this.Mouse.GetState();
- g.DrawString(ms.Buttons.ToString(), this.Font, Brushes.White, 0, 0);
- m_cursor.Draw(elapsedTime, g);
- }
- #endregion
- }
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
- public abstract class DynamicElement : StaticElement
- {
- #region Public Properties
- public PointF Velocity { get; set; }
- public PointF Acceleration { get; set; }
- #endregion
- #region Constructor(s)
- public DynamicElement(GamePanel owningGame, PointF position, SizeF size, PointF velocity, PointF acceleration)
- : base(owningGame, position, size)
- {
- this.Velocity = velocity;
- this.Acceleration = acceleration;
- }
- public DynamicElement(GamePanel owningGame, RectangleF rectangle, PointF velocity, PointF acceleration)
- : base(owningGame, rectangle)
- {
- this.Velocity = velocity;
- this.Acceleration = acceleration;
- }
- #endregion
- #region Public Methods
- public PointF CalculateNextPosition(TimeSpan elapsedTime)
- {
- return
- new PointF(
- this.Rectangle.X + (float)elapsedTime.TotalSeconds * this.Velocity.X,
- this.Rectangle.Y + (float)elapsedTime.TotalSeconds * this.Velocity.Y
- );
- }
- public PointF CalculateNextVelocity(TimeSpan elapsedTime)
- {
- return
- new PointF(
- this.Velocity.X + (float)elapsedTime.TotalSeconds * this.Acceleration.X,
- this.Velocity.Y + (float)elapsedTime.TotalSeconds * this.Acceleration.Y
- );
- }
- #endregion
- }
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
- public class Ball : DynamicElement
- {
- public Color FillColour
- {
- get { return base.Colour; }
- set { base.Colour = value; }
- }
- public Color OutlineColour { get; set; }
- public Ball(GamePanel owningGame, RectangleF rectangle, PointF velocity, PointF acceleration)
- : base(owningGame, rectangle, velocity, acceleration)
- {
- }
- public Ball(GamePanel owningGame, PointF position, SizeF size, PointF velocity, PointF acceleration)
- : base(owningGame, position, size, velocity, acceleration)
- {
- }
- #region IGameElement Implementation
- public override void Draw(TimeSpan elapsedTime, Graphics g)
- {
- g.FillEllipse(new SolidBrush(this.FillColour), this.Rectangle);
- g.DrawEllipse(new Pen(this.OutlineColour), this.Rectangle);
- }
- public override void Update(TimeSpan elapsedTime)
- {
- this.Velocity = base.CalculateNextVelocity(elapsedTime);
- this.Position = base.CalculateNextPosition(elapsedTime);
- }
- #endregion
- }
Expand|Select|Wrap|Line Numbers
- public class TestGame : GamePanel
- {
- private Ball m_ball = null;
- private MouseCursor m_cursor = null;
- public TestGame(int width, int height)
- : base(width, height)
- {
- m_cursor = new MouseCursor(this, new Rectangle(0, 0, 2, 2));
- m_ball = new Ball(this, new RectangleF(10, 10, 25, 25), new PointF(50, 50), new PointF(0, 0))
- {
- FillColour = Color.DarkRed,
- OutlineColour = Color.Red
- };
- }
- #region GamePanel Implementation
- protected override void OnUpdate(TimeSpan elapsedTime)
- {
- m_ball.Update(elapsedTime);
- m_cursor.Update(elapsedTime);
- }
- protected override void OnDraw(TimeSpan elapsedTime, System.Drawing.Graphics g)
- {
- g.FillRectangle(Brushes.Black, this.ClientRectangle);
- m_ball.Draw(elapsedTime, g);
- m_cursor.Draw(elapsedTime, g);
- }
- #endregion
- }
My Ball code is now...
Expand|Select|Wrap|Line Numbers
- public class Ball : DynamicElement
- {
- public Color FillColour
- {
- get { return base.Colour; }
- set { base.Colour = value; }
- }
- public Color OutlineColour { get; set; }
- public Ball(GamePanel owningGame, RectangleF rectangle, PointF velocity, PointF acceleration)
- : base(owningGame, rectangle, velocity, acceleration)
- {
- }
- public Ball(GamePanel owningGame, PointF position, SizeF size, PointF velocity, PointF acceleration)
- : base(owningGame, position, size, velocity, acceleration)
- {
- }
- #region IGameElement Implementation
- public override void Draw(TimeSpan elapsedTime, Graphics g)
- {
- g.FillEllipse(new SolidBrush(this.FillColour), this.Rectangle);
- g.DrawEllipse(new Pen(this.OutlineColour), this.Rectangle);
- }
- public override void Update(TimeSpan elapsedTime)
- {
- this.Velocity = base.CalculateNextVelocity(elapsedTime);
- PointF nextPosition = base.CalculateNextPosition(elapsedTime);
- PointF newVelocity = this.Velocity;
- // Check X
- if (nextPosition.X < 0)
- {
- nextPosition.X = 0;
- newVelocity.X *= -1;
- }
- else if (nextPosition.X + this.Size.Width > OwningGame.InitialGameSize.Width)
- {
- nextPosition.X = OwningGame.InitialGameSize.Width - this.Size.Width;
- newVelocity.X *= -1;
- }
- // Check Y
- if (nextPosition.Y < 0)
- {
- nextPosition.Y = 0;
- newVelocity.Y *= -1;
- }
- else if (nextPosition.Y + this.Size.Height > OwningGame.InitialGameSize.Height)
- {
- nextPosition.Y = OwningGame.InitialGameSize.Height - this.Size.Height;
- newVelocity.Y *= -1;
- }
- this.Position = nextPosition;
- if (this.Velocity != newVelocity)
- this.Velocity = newVelocity;
- }
- #endregion
- }
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!