Saturday, December 7, 2013

Conway's Game of Life in a C# Windows Form Application

I'm starting a programming blog here, for sharing anything I've made that I've found useful myself. Whether or not anyone else sees this doesn't really matter to me, it's just nice to have my progress/projects here for sentimental reasons. Also, due to medication I've started to get out of that depressive state I've been in for a month or two and actually want to do things again, so I figured this would make a nice Saturday project to get my mind going and practice extending my programming techniques, as I used to do often.

So to begin, the intent was to see if I could have efficient pixel writing without using the Get and Set Pixel methods, instead by using an int[] array, and it worked out fairly well.
  1. Open up Visual Studio 2010 (maybe?) or 2012.
  2. Make a new Windows Form Application
  3. Navigate to your Form (probably called Form1 on the right next to your code)
  4. Add a Picture Box by dragging from the toolbox (View -> Toolbox if it's not already somewhere visible) and dropping onto to your form.
  5. Stretch this Picture Box to whatever dimensions you want Conway's Game of Life to run in - feel free to test the limits, it's pretty efficient I hope.
  6. In the Properties window (View -> Properties like the toolbox if it's not already visible), rename it to GraphicsBox or another name of your choice (you'll just have to edit the given code).
  7. Right click anywhere in the form editor and click "Show Code," then paste in the code below.
  8. Build and run, and you have Conway's Game of Life - enjoy - and the reader's challenge would be to make a rainbow version that propagates colors around when duplication occurs according to the most dominant color around.
  9. (optional) If you want even more efficient access than what is given here, you'll need to right click on your project, click on Properties (at the bottom), Build, then check "Allow unsafe code."
    1. This allows the use of pointers, which using graphicsData.Scan0 as your initial position, let you access the memory in the bitmap directly. I avoid this because the array copy method works about as well, and allows me to separate out the code into an object much more easily. Plus unsafe code is scary and bad.

Essentially, the idea here was to be able to write to bitmap data directly. This is done by using the RGBA 32-bit color format in the Bitmaps, which lines up nicely with 32-bit integers, the default integer size (since there's 8 bits/1 byte per value).

This is my simple function to convert from RGBA values to the equivalent integer, essentially just by "packing" the bytes into a single integer using bit shifts, I'm pretty sure this is the standard way to do it. The ()'s are needed here, bit-wise precedence is even lower than boolean operators like == and > I think. That's got me too many times.
public int rgbaToColor(byte r, byte g, byte b, byte a)
{
    return r + (g << 8) + (b << 16) + (a << 24);
}
Now, I just wanted an int[] array that I could address pixels in, where

index = x + y * width

is the formula for converting between (x,y) coordinates of a pixel and positions in the array.

The CustomBitmap class I made does this well, by "locking the bits" - essentially just assigning the memory of the Bitmap to a specific location. I can write to this location by using a Marshal class, which just is a convenient way of copying large arrays.

I double buffered (two bitmaps) everything as well, just to ensure that the bitmap being displayed never had it's bit's locked, since for some reason if you try and display a locked bitmap all you see is a big red X.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Forms;


namespace Conway_Test
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            randomGen = new Random();

            black = rgbaToColor(0, 0, 0, 255);
            white = rgbaToColor(255, 255, 255, 255);

            this.Load += FormLoaded;
            
        }

        public void FormLoaded(object sender, System.EventArgs e)
        {
            InitializeGraphics((Form1)sender);
        }

        CustomBitmap graphics;

        public Random randomGen;

        public Form1 formHolding;

        private int[] newArray;
        private int[] oldArray;

        int black;
        int white;

        /// 
        /// This initializes front and back buffer
        /// The idea here is that one of these can be the old array,
        /// which is the one that you read from when making changes.
        /// These changes then occur by writing to the new array,
        /// then that new array is passed into the CustomBitmap (graphics)
        /// object above to actually update the graphics.
        /// 
        /// You can see an example of this in the DrawStuff method below.
        /// 
        public void InitializeGraphics(Form1 formHolding)
        {
            graphics = new CustomBitmap(GraphicsBox, formHolding);

            int graphicsSize = graphics.GetDataSize();

            newArray = new int[graphicsSize];
            oldArray = new int[graphicsSize];

            // Fill buffers initially with random data
            for (int i = 0; i < newArray.Length; i++)
            {
                if ((randomGen.Next() & 1) == 0)
                {
                    newArray[i] = black;
                    oldArray[i] = black;
                }
                else
                {
                    newArray[i] = white;
                    oldArray[i] = white;
                }
            }

            // Assign data to buffers (and flip the buffers to display data)
            graphics.SetBitmapData(newArray);

            // Create a separate thread to do the heavy processing
            // This allows other non-existent GUI functions to run as needed
            Thread frameThread = new Thread(() => EveryFrame());
            frameThread.IsBackground = true;
            frameThread.Start();
        }

        /// 
        /// This method is intended to be run as a seperate thread,
        /// and updates graphics (and other game logic) repeatedly,
        /// computing the FPS every second as well.
        /// 
        private void EveryFrame()
        {
            long curSecond = DateTime.Now.Second;
            int numPerSecond = 0;
            while (true)
            {
                if (curSecond != DateTime.Now.Second)
                {
                    // This value can be displayed as needed
                    int FPS = numPerSecond;

                    numPerSecond = 0;
                    curSecond = DateTime.Now.Second;
                }
                numPerSecond++;

                DrawStuff();
            }
        }

        /// 
        /// Does one cycle of rules for Conway's Game of Life,
        /// and updates the graphics accordingly.
        /// 
        /// As explained above, after we swap them (so old array is the
        /// previous new array), we read from old array and never write to it,
        /// and write to new array but never read from it (since it's values
        /// before they are assigned are old old - I mean you could read from it,
        /// it just wouldn't help).
        /// 
        /// Then once we have the new generation values, we pass them into
        /// our graphics to be stored in a Bitmap and displayed.
        /// 
        public void DrawStuff()
        {
            // Swap the two byte buffers (so the new is now old and we can modify other)
            int[] tempArray = oldArray;
            oldArray = newArray;
            newArray = tempArray;

            int width = GraphicsBox.Width;
            int height = GraphicsBox.Height;

            // oldArray is intended to be read from for data,
            // and newArray is intended to be written to based on that data.
            for (int x = 1; x < width - 1; x++)
            {
                for (int y = 1; y < height - 1; y++)
                {
                    int neighbors = 0;
                    int curPos = x + y * width;
                    if (oldArray[curPos - 1 - width] == white) neighbors++;
                    if (oldArray[curPos - width] == white) neighbors++;
                    if (oldArray[curPos + 1 - width] == white) neighbors++;
                    if (oldArray[curPos - 1] == white) neighbors++;
                    if (oldArray[curPos + 1] == white) neighbors++;
                    if (oldArray[curPos - 1 + width] == white) neighbors++;
                    if (oldArray[curPos + width] == white) neighbors++;
                    if (oldArray[curPos + 1 + width] == white) neighbors++;

                    if (oldArray[curPos] == white && (neighbors < 2 || neighbors > 3))
                        newArray[curPos] = black;
                    else if (oldArray[curPos] == black && neighbors == 3)
                        newArray[curPos] = white;
                    else
                        newArray[curPos] = oldArray[curPos];

                }
            }
            graphics.SetBitmapData(newArray);
        }

        /// 
        /// "Packs" the given rgba byte values (0-255) into 
        /// the corresponding integer value, for use in the array
        /// above.
        /// 
        /// Technically a byte array could be used throughout
        /// this code and then this method wouldn't be needed, but I
        /// think an integer array is more efficient and actually makes
        /// the code cleaner in the long run.
        /// 
        public int rgbaToColor(byte r, byte g, byte b, byte a)
        {
            return r + (g << 8) + (b << 16) + (a << 24);
        }
    }

    /// 
    /// Contains a Bitmap to be written to as needed.
    /// The idea here is that a PictureBox is provided,
    /// and a Bitmap of that same size is created.
    /// 
    /// GetDataSize() will then return the size of the int[]
    /// array that needs to be created, and an int[] array of that
    /// size with colors created by the rgbaToColor method above.
    /// That int[] array can be passed into the SetBitmapData method
    /// which will display the corresponding graphics on the screen.
    /// 
    public class CustomBitmap
    {
        private PictureBox holder;
        private Bitmap frontBitmap;
        private Bitmap backBitmap;
        private Rectangle bitmapRectangle;
        private Form1 formHolding;

        private bool isFormClosed;

        public CustomBitmap(PictureBox holder, Form1 formHolding)
        {
            this.holder = holder;
            this.formHolding = formHolding;

            this.formHolding.FormClosed += FormHoldingCloseCallback;
            isFormClosed = false;
            
            frontBitmap = new System.Drawing.Bitmap(holder.Width, holder.Height, PixelFormat.Format32bppArgb);
            backBitmap = new System.Drawing.Bitmap(holder.Width, holder.Height, PixelFormat.Format32bppArgb);
            bitmapRectangle = new Rectangle(0, 0, holder.Width, holder.Height);
        }

        public void FormHoldingCloseCallback(object sender, System.EventArgs e)
        {
            this.isFormClosed = true;
        }

        public int GetDataSize()
        {
            return holder.Width * holder.Height;
        }

        public void SetBitmapData(int[] buffer)
        {
            // Lock the data bits of the back bitmap
            BitmapData graphicsData =
                backBitmap.LockBits(bitmapRectangle, System.Drawing.Imaging.ImageLockMode.ReadWrite,
                backBitmap.PixelFormat);

            // Copy the RGBA values of the given buffer to the back bitmap's data
            System.Runtime.InteropServices.Marshal.Copy(buffer, 0, graphicsData.Scan0, buffer.Length);

            // Unlock the data bits of the back bitmap
            backBitmap.UnlockBits(graphicsData);

            // Swap the front and back bitmaps
            Bitmap tempBitmap = backBitmap;
            backBitmap = frontBitmap;
            frontBitmap = tempBitmap;

            // Display the new front bitmap (the one we just drew on)
            if (!this.isFormClosed)
            {
                formHolding.BeginInvoke(new Action(() => { holder.Image = (Bitmap)frontBitmap; }));
            }
        }
    }
}

Let me know if you have any further questions by email (DaniPhye@Gmail.com) or by commenting below. I place this code in the public domain for anyone's use, have fun.

Edit: The multi-threading (which isn't actually needed in this example) was being done on a non-background thread, meaning that closing out of the program didn't terminate the process since there was still an invisible background thread running. Fixed now.

Edit edit: Fixed a race condition upon opening and closing the form. Technically there is still a slight race condition in that the form can be closed after the test at the bottom but before calling formHolding.BeginInvoke(...), however I'll leave it to the reader to fix this if desired, because it just made the code too gross to provide as a helpful example.

1 comment:

  1. Hey thank a lot I was messing aroudn with SetPixel this really cranked up the framerate!

    ReplyDelete