Win Forms based "Game"

Started by
11 comments, last by landlocked 12 years, 11 months ago
Hey all,

I just took a few hours to do this in .NET making a WinForms game using just a Timer and the built-in System.Graphics namespace. The only thing it does is "spawn" 200 "balls" and draw them on the screen and handles moving all them at the same time. I just wanted to do this to get a rough idea of game loops and taking input wanted to bring it here for critiques. As written on my machine I get about 60 FPS. Some limitations already obvious to me is the entire scene is redrawn each frame. There is no "state" and therefore this is a huge performance hit. I noticed there was a weird flicker when drawing the circles so I implemented a sort of saved image ala double buffer style since setting the double buffer property in the WinForms designer did NOTHING to mitigate the flicker. Critiques wanted. Challenges sought.

All this code can be copied into a single code file if you want to run it yourself. My VS installation default to .Net 4 but nothing specific to the v4 framework is in here.

Let me know what you think!


using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace Managed
{
public partial class Form1 : Form
{
List<Ball> balls = null;
List<Keys> keysPressed = null;
int fps = 0;
DateTime lastFPSUpdate;
Bitmap savedFrame = null;
bool showSaved = false;

public Form1()
{
InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e)
{
LoadObjects();

keysPressed = new List<Keys>();

this.KeyDown += new KeyEventHandler(Form1_KeyDown);
this.KeyUp += new KeyEventHandler(Form1_KeyUp);

BeginLoop();
}

void Form1_KeyUp(object sender, KeyEventArgs e)
{
lock (keysPressed)
{
while (keysPressed.Contains(e.KeyData))
keysPressed.Remove(e.KeyData);
}
}

void Form1_KeyDown(object sender, KeyEventArgs e)
{
lock (keysPressed)
{
keysPressed.Add(e.KeyData);
}
}

void LoadObjects()
{
Ball[] ballArray = new Ball[200];

for (int i = 0; i < ballArray.Length; i++)
{
ballArray = new Ball(50, 50);
}

balls = new List<Ball>();

for (int i = 0; i < ballArray.Length; i++)
{
balls.Add(ballArray);
}
}

void BeginLoop()
{
Timer t = new Timer();

t.Tick += new EventHandler(ProcessEvents);
t.Interval = 15;
t.Start();
}

void ProcessEvents(object sender, EventArgs e)
{
if (showSaved)
{
this.BackgroundImage = savedFrame;
}

Bitmap b = new Bitmap(this.ClientRectangle.Width, this.ClientRectangle.Height);

Graphics g = null;

try
{
g = Graphics.FromImage(b);

g.FillRectangle(SystemBrushes.Control, this.ClientRectangle);

foreach (Ball ball in balls)
{
HandleInput(ball);

ball.Draw(new PaintEventArgs(g, this.ClientRectangle));
}
}
finally
{
if (g != null)
g.Dispose();
}

savedFrame = b;
showSaved = true;

if (lastFPSUpdate == null)
lastFPSUpdate = DateTime.Now;

TimeSpan ts = new TimeSpan(DateTime.Now.Ticks - lastFPSUpdate.Ticks);

if (ts.TotalMilliseconds > 1000)
{
label1.Text = fps.ToString();
fps = 0;
lastFPSUpdate = DateTime.Now;
}
else
{
fps++;
}
}

private void HandleInput(Ball b)
{
if (IsAllPressed(Keys.Down, Keys.Right))
{
b.yPos += 1;
b.xPos += 1;
}
else if (IsAllPressed(Keys.Down, Keys.Left))
{
b.yPos += 1;
b.xPos -= 1;
}
else if (IsAllPressed(Keys.Up, Keys.Right))
{
b.yPos -= 1;
b.xPos += 1;
}
else if (IsAllPressed(Keys.Up, Keys.Left))
{
b.yPos -= 1;
b.xPos -= 1;
}
else if (IsAllPressed(Keys.Up))
{
b.yPos -= 1;
}
else if (IsAllPressed(Keys.Down))
{
b.yPos += 1;
}
else if (IsAllPressed(Keys.Left))
{
b.xPos -= 1;
}
else if (IsAllPressed(Keys.Right))
{
b.xPos += 1;
}
}

private bool IsAllPressed(params Keys[] k)
{
foreach (Keys key in k)
{
if (!keysPressed.Contains(key))
{
return false;
}
}

return true;
}
}

public class Ball
{
private int width = 0;
private int height = 0;

public int xPos
{
get;
set;
}

public int yPos
{
get;
set;
}

public Ball(int width, int height)
{
this.width = width;
this.height = height;
}

public Ball(int x, int y, int width, int height)
{
this.xPos = x;
this.yPos = y;
this.width = width;
this.height = height;
}

public void Draw(PaintEventArgs e)
{
Graphics g = e.Graphics;

Pen p = Pens.Black;
Rectangle r = new Rectangle(this.xPos, this.yPos, this.width, this.height);

g.DrawEllipse(p, r);
}
}
}

Always strive to be better than yourself.
Advertisement
Got an interesting problem that isn't obvious. I implemented a line in the circle that but the end goal of that line is to always point at the cursor. I've tied in the mouse location easily enough and can draw a line from the pointer to the center of the circle but I'm having problems figuring how to "clip" the line at the border of the circle. How do I detect where that is? Image is displaying what the code currently does. Also, here is a snippet of code from the Draw method in the circle.

g.DrawLine(p, Direction.X, Direction.Y, this.xPos + (this.width / 2), this.yPos + (this.height / 2));

Direction is the Point of the cursor.

[media]http://img153.imageshack.us/i/samplef.gif/[/media]
Always strive to be better than yourself.
You need to calculate the dot over the circle that belongs to the line you are tracing. I think you can do this easily following the next steps.


First normalize the vector circle_center -> [font="Arial"]cursor_position[/font]:

tempX = [font="Arial"]Direction.X - (this.xPos + (this.width / 2));[/font]
tempY = Direction.Y - (this.yPos + (this.height / 2));

norm = sqrt(tempX * tempX + tempY * tempY);

xval = tempX/norm;
yval = tempY/norm;


And then multiply by the circle radius:

xval = xval * radius;
yval = yval * radius;

This will give you the dot over the circle that points at the cursor.
Finally draw the line with the new point coordinates instead of the mouse position:

g.DrawLine(p, xval, yval, this.xPos + (this.width / 2), this.yPos + (this.height / 2));


I hope my math is correct smile.gif
Not really sure what to say on this one as it depends on where you are taking it.
Maybe down the line if your 'game loop' requires some significant cpu time your UI (if you have any?) may become less/un-responsive if this is all happening on the UI thread which I am guessing it is, so perhaps need to consider a thread for the loop. Though this may in itself require alot of invokes to the UI thread since you are drawing objects it has created.

You need to calculate the dot over the circle that belongs to the line you are tracing. I think you can do this easily following the next steps.


First normalize the vector circle_center -> [font="Arial"]cursor_position[/font]:

tempX = [font="Arial"]Direction.X - (this.xPos + (this.width / 2));[/font]
tempY = Direction.Y - (this.yPos + (this.height / 2));

norm = sqrt(tempX * tempX + tempY * tempY);

xval = tempX/norm;
yval = tempY/norm;


And then multiply by the circle radius:

xval = xval * radius;
yval = yval * radius;

This will give you the dot over the circle that points at the cursor.
Finally draw the line with the new point coordinates instead of the mouse position:

g.DrawLine(p, xval, yval, this.xPos + (this.width / 2), this.yPos + (this.height / 2));


I hope my math is correct smile.gif

Thanks! I'll see if this works.


Not really sure what to say on this one as it depends on where you are taking it.
Maybe down the line if your 'game loop' requires some significant cpu time your UI (if you have any?) may become less/un-responsive if this is all happening on the UI thread which I am guessing it is, so perhaps need to consider a thread for the loop. Though this may in itself require alot of invokes to the UI thread since you are drawing objects it has created.

I'm not really going anywhere with this, per se. I just wanted to try, from scratch, to create a program that draws to the screen constantly and takes input that modifies what is drawn, the most basic interpretation of a modern game application, using something I already knew which happens to include C# WinForms. I wanted to present it here hoping to have responses like "hey doing this is dumb do it this way instead" trying to crowdsource some of my education. :P I am trying to include some multi-threading like you mentioned. Ideally I'd like to get the entire "game loop" on a separate thread. Something interesting I saw is that when I was using my timer-based approached changing the tick number from 15 to 20 cut my FPS in half. Then again, my FPS tracking mechanism may be faulty or not as accurate as it could be.
Always strive to be better than yourself.
Minor point....

norm = sqrt(tempX * tempX + tempY * tempY);

xval = tempX/norm;
yval = tempY/norm;

Just check that norm is not zero....before doing the division.
Okay, some good news. I successfully paralleled the game loop away from the UI thread and am seeing framerates of upwards to 200+. The bad thing is the math for drawing the line that's supposed to point at the cursor is wrong. By wrong it points towards the upper left (0,0 coords) always. Attached is another screen shot. For the screen shot I had the mouse pointer almost directly opposite the point where the line intersects the circle and had the pointer very close to the border of the circle (>50 pixels). Any clues? Also pasted in is the code I've used. If you can spot something that just acts backwards or can be done better I'm open to suggestions!

[media]http://img64.imageshack.us/i/sampleu.gif/[/media]


using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Managed
{
public partial class Form1 : Form
{
List<Ball> balls = null;
List<Keys> keysPressed = null;
int fps = 0;
DateTime lastFPSUpdate;
Bitmap savedFrame = null;
bool showSaved = false;
Point mouseLocation;
Task GameTask;
Rectangle window;

private delegate void UpdateFPS(int i);
private delegate void UpdateImage(Bitmap b);
private UpdateFPS doUpdate;
private UpdateImage drawImage;

public Form1()
{
InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e)
{
LoadObjects();

keysPressed = new List<Keys>();

this.KeyDown += new KeyEventHandler(Form1_KeyDown);
this.KeyUp += new KeyEventHandler(Form1_KeyUp);
this.MouseMove += new MouseEventHandler(Form1_MouseMove);

doUpdate = new UpdateFPS(ChangeFPS);
drawImage = new UpdateImage(DrawImage);

window = this.DisplayRectangle;

GameTask = Task.Factory.StartNew(new Action(ProcessEvents), TaskCreationOptions.LongRunning | TaskCreationOptions.AttachedToParent);
}

void DrawImage(Bitmap b)
{
this.BackgroundImage = b;
}

void Form1_MouseMove(object sender, MouseEventArgs e)
{
Rectangle r = this.RectangleToScreen(this.DisplayRectangle);

mouseLocation = new Point(MousePosition.X - r.X, MousePosition.Y - r.Y);
}

void Form1_KeyUp(object sender, KeyEventArgs e)
{
lock (keysPressed)
{
while (keysPressed.Contains(e.KeyData))
keysPressed.Remove(e.KeyData);
}
}

void Form1_KeyDown(object sender, KeyEventArgs e)
{
lock (keysPressed)
{
keysPressed.Add(e.KeyData);
}
}

void LoadObjects()
{
Ball[] ballArray = new Ball[1];

for (int i = 0; i < ballArray.Length; i++)
{
ballArray = new Ball(50, 50);
}

balls = new List<Ball>();

for (int i = 0; i < ballArray.Length; i++)
{
balls.Add(ballArray);
}
}

void ProcessEvents()
{
if (showSaved)
{
if (this.InvokeRequired)
{
this.BeginInvoke(drawImage, savedFrame);
}
else
{
this.BackgroundImage = savedFrame;
}
}

Bitmap b = new Bitmap(window.Width, window.Height);

Graphics g = null;

try
{
g = Graphics.FromImage(b);

g.FillRectangle(SystemBrushes.Control, window);

foreach (Ball ball in balls)
{
HandleInput(ball);

ball.Direction = mouseLocation;
ball.Draw(new PaintEventArgs(g, window));
}
}
finally
{
if (g != null)
g.Dispose();
}

savedFrame = b;
showSaved = true;

if (lastFPSUpdate == null)
lastFPSUpdate = DateTime.Now;

TimeSpan ts = new TimeSpan(DateTime.Now.Ticks - lastFPSUpdate.Ticks);

if (ts.TotalMilliseconds > 1000)
{
if (label1.InvokeRequired)
{
label1.Invoke(doUpdate, fps);
}
else
{
label1.Text = fps.ToString();
}
fps = 0;
lastFPSUpdate = DateTime.Now;
}
else
{
fps++;
}

GameTask.ContinueWith(delegate
{
ProcessEvents();
}, TaskContinuationOptions.None);
}

private void ChangeFPS(int i)
{
label1.Text = i.ToString();
}

private void HandleInput(Ball b)
{
if (IsAllPressed(Keys.Down, Keys.Right))
{
b.yPos += 1;
b.xPos += 1;
}
else if (IsAllPressed(Keys.Down, Keys.Left))
{
b.yPos += 1;
b.xPos -= 1;
}
else if (IsAllPressed(Keys.Up, Keys.Right))
{
b.yPos -= 1;
b.xPos += 1;
}
else if (IsAllPressed(Keys.Up, Keys.Left))
{
b.yPos -= 1;
b.xPos -= 1;
}
else if (IsAllPressed(Keys.Up))
{
b.yPos -= 1;
}
else if (IsAllPressed(Keys.Down))
{
b.yPos += 1;
}
else if (IsAllPressed(Keys.Left))
{
b.xPos -= 1;
}
else if (IsAllPressed(Keys.Right))
{
b.xPos += 1;
}

if (IsAllPressed(Keys.Space))
{
b.Jump = true;
}
else
{
b.Jump = false;
}
}

private bool IsAllPressed(params Keys[] k)
{
foreach (Keys key in k)
{
if (!keysPressed.Contains(key))
{
return false;
}
}

return true;
}
}

public class Ball
{
private int width = 0;
private int height = 0;

public int xPos
{
get;
set;
}

public int yPos
{
get;
set;
}

public bool Jump
{
get;
set;
}

public Point Direction
{
get;
set;
}

public float Radius
{
get
{
return this.width / 2;
}
}

public Ball(int width, int height)
{
this.width = width;
this.height = height;
}

public Ball(int x, int y, int width, int height)
{
this.xPos = x;
this.yPos = y;
this.width = width;
this.height = height;
}

public void Draw(PaintEventArgs e)
{
Graphics g = e.Graphics;

Pen p = null;

if (Jump)
{
p = new Pen(Color.Black, 5);
}
else
{
p = new Pen(Color.Black, 1);
}

Rectangle r = new Rectangle(this.xPos, this.yPos, this.width, this.height);

float tempX, tempY, xval, yval, norm = 0.0f;

tempX = Direction.X - (this.xPos + (this.width / 2));
tempY = Direction.Y - (this.yPos + (this.height / 2));

norm = (float)Math.Sqrt(tempX * tempX + tempY * tempY);

if (norm > 0)
{
xval = tempX / norm;
yval = tempY / norm;

xval = xval * Radius;
yval = yval * Radius;

g.DrawLine(p, xval, yval, this.xPos + (this.width / 2), this.yPos + (this.height / 2));
}

//double sides = ((this.xPos + (this.width / 2)) - (this.yPos + (this.height / 2))) ;

//double angle = Math.Sqrt(

//double x = (this.yPos + (this.height / 2)) * Math.Cos(

g.DrawEllipse(p, r);
//g.DrawLine(p, Direction.X, Direction.Y, this.xPos + (this.width / 2), this.yPos + (this.height / 2));
}
}
}

Always strive to be better than yourself.
So, I took the Task library a bit further and used a Parallel.ForEach to parallel-ize the actual drawing of each circle. I noticed the following was actually a performance hit:


try
{
g = Graphics.FromImage(b);

g.FillRectangle(SystemBrushes.Control, window);

foreach (Ball ball in balls)
{
HandleInput(ball);
ball.Direction = mouseLocation;
}

Parallel.ForEach<Ball>(balls, bll =>
{
lock (g)
{
bll.Draw(new PaintEventArgs(g, window));
}
});
}
finally
{
if (g != null)
g.Dispose();
}


The hit is for two reasons. 1) I'm iterating a loop twice and I'm locking on g (the Graphics object) so regardless of how many threads is trying to "draw" g is only accessed once and because of the splitting of the threads the overhead from so much context switching in the Kernel that it's actually slowing me down. I figured I'd give this a shot because given this context it didn't matter what was drawn when since they're all on "top" of each other anyway. I think I'm going to try to do some detection and see if a point is occupied by all the objects to only draw any certain pixel once. This will bottleneck me somewhat but I suspect I could have this application generate several thousand circles and have the same performance of my current cap which seems to be about 200 or so, if done right. So, I'll be trading initial speed for scalability of how much this app can handle.
Always strive to be better than yourself.
My bad, the DrawLine has a mistake, the calculated point has to be measured from the center of the circle so:

g.DrawLine(p, this.xPos + (this.width / 2) + xval, this.yPos + (this.height / 2) + yval, this.xPos + (this.width / 2), this.yPos + (this.height / 2));


Note: You may need to adjust some signals because c#'s coordinate system is not the 'standard' one but now the line might not cross the circle. I'm almost certain that you have to flip Y but try it and let us know how it goes

Try this if it doesn't work

g.DrawLine(p, this.xPos + (this.width / 2) + xval, this.yPos + (this.height / 2) - yval, this.xPos + (this.width / 2), this.yPos + (this.height / 2));

My bad, the DrawLine has a mistake, the calculated point has to be measured from the center of the circle so:

g.DrawLine(p, this.xPos + (this.width / 2) + xval, this.yPos + (this.height / 2) + yval, this.xPos + (this.width / 2), this.yPos + (this.height / 2));


Note: You may need to adjust some signals because c#'s coordinate system is not the 'standard' one but now the line might not cross the circle. I'm almost certain that you have to flip Y but try it and let us know how it goes

Try this if it doesn't work

g.DrawLine(p, this.xPos + (this.width / 2) + xval, this.yPos + (this.height / 2) - yval, this.xPos + (this.width / 2), this.yPos + (this.height / 2));

The first one worked, thanks!
Always strive to be better than yourself.

This topic is closed to new replies.

Advertisement