I decided to learn about building games, so I picked up C# to use it along with Windows Form - I already have C# experience, so that was the main reason I did so. That said, I tasked myself to build a simple Windows Form app that renders a noise image targeting hopefully at 60 FPS. I'm using the GDI+ APIs to render bitmaps on screen; however, I noticed the app isn't even close to render 60 FPS - it renders around 36-39 FPS on a release build.
I should mention I'm a totally newbie in both Windows Form, and image rendering, so I believe my code isn't optimized at all. I'm sharing below the code snippet the app runs in order to loop infinitely to render the noise images.
private static Bitmap GetStaticNoise(int width, int height)
{
var bitmap = new Bitmap(width, height);
for (int y = 0; y < bitmap.Height; y++)
{
for (int x = 0; x < bitmap.Width; x++)
{
Color color = Color.Black;
var rnd = Random.Shared.Next(0, 2);
if (rnd % 2 == 0)
{
color = Color.White;
}
bitmap.SetPixel(x, y, color);
}
}
return bitmap;
}
private void Form1_Load(object? sender, EventArgs e)
{
Task.Factory.StartNew(() =>
{
var frameStopwatch = new Stopwatch();
var screenGraphics = _screen.CreateGraphics();
var font = new Font(FontFamily.GenericMonospace, 30);
var ct = cts.Token;
while (!ct.IsCancellationRequested)
{
for (int frame = 0; frame < 60; frame++)
{
frameStopwatch.Restart();
// generates "noise"
Bitmap bitmap = GetStaticNoise(_screen.ClientSize.Width, _screen.ClientSize.Height);
screenGraphics.DrawImage(bitmap, new Point(0, 0));
bitmap.Dispose();
frameStopwatch.Stop();
screenGraphics.DrawString(frameStopwatch.ElapsedMilliseconds.ToString(), font, Brushes.Red, new Point(0, 0));
//Thread.Sleep(Math.Max(0, _frameCooldown.Milliseconds - (int)frameStopwatch.ElapsedMilliseconds));
}
}
}, TaskCreationOptions.LongRunning);
}
Any advice about what could be changed here it's appreciated. Also, any resource that could be useful to learn about using GDI+ for game development with Windows Form is also appreciated (or any resource that could help me understanding for instance this situation I'm running into).
Ah! the Windows Form is on .NET 6.0
. I'm also attaching a picture of the bitmap rendered on screen (upper left corner shows the time in MS to render the frame)
EDIT: thank you so much for the comments, etc. I did follow the recommendations, and I realized I wasn't even hitting the FPS I mentioned above :(. I added a fps counter, and I'm seeing 29-30 FPS - I will update my code to use something different than GDI+ - and eventually use something else for developing games. I'm attaching below the updated code.
public partial class Form1 : Form
{
private readonly Bitmap _bitmap;
private readonly Graphics _bitmapGraphics;
private readonly Random _rnd = new Random();
private readonly int _whiteArg = Color.White.ToArgb();
private readonly int _blackArg = Color.Black.ToArgb();
private readonly int[] _noise;
private readonly PictureBox _screen;
private readonly Graphics _screenGraphics;
private readonly CancellationTokenSource cts = new CancellationTokenSource();
public Form1()
{
InitializeComponent();
_screen = new PictureBox
{
ClientSize = this.ClientSize
};
_screenGraphics = _screen.CreateGraphics();
_bitmap = new Bitmap(_screen.ClientSize.Width, _screen.ClientSize.Height);
_bitmapGraphics = Graphics.FromImage(_bitmap);
_noise = new int[_screen.ClientSize.Width * _screen.ClientSize.Height];
Controls.Add(_screen);
this.Load += Form1_Load;
this.FormClosing += Form1_FormClosing;
}
private void Form1_FormClosing(object? sender, FormClosingEventArgs e)
{
cts.Cancel();
}
private void SetNoise()
{
for (int i = 0; i < _noise.Length; i++)
{
int val = _rnd.Next(0, 2);
_noise[i] = val == 1 ? _whiteArg : _blackArg;
}
var bitmapData = _bitmap.LockBits(new Rectangle(0, 0, _bitmap.Width, _bitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
var ptr = bitmapData.Scan0;
System.Runtime.InteropServices.Marshal.Copy(_noise, 0, ptr, _noise.Length);
_bitmap.UnlockBits(bitmapData);
}
private void Form1_Load(object? sender, EventArgs e)
{
Task.Factory.StartNew(() =>
{
var ct = cts.Token;
var font = new Font("Times New Roman", 32, FontStyle.Regular, GraphicsUnit.Pixel);
var sw = new Stopwatch();
int frames = 0;
int lastFps = 0;
long timeSpentInMs = 0;
while (!ct.IsCancellationRequested)
{
sw.Start();
SetNoise();
sw.Stop();
frames++;
timeSpentInMs += sw.ElapsedMilliseconds;
_bitmapGraphics.DrawString(lastFps.ToString(), font, Brushes.Red, 0, 0);
_screenGraphics.DrawImage(_bitmap, new Point(0, 0));
// a second has elapsed
if (timeSpentInMs - 1_000 >= 0)
{
lastFps = frames;
frames = 0;
timeSpentInMs -= 1_000;
sw.Reset();
}
}
}, TaskCreationOptions.LongRunning);
}
}