4

I'm a beginner and doing some C# exercises. I found out about Forest Fire Model and tried to do this with WPF and for drawing, I'm using canvas by creating a rectangle for every pixel. The problem iam getting is that the Program freezes and canvas doesn't draw anything (with the while(true) loop). Also, I'm deleting all children after an iteration, still the program is collecting GBs of RAM.

Simplified Code for testing:

public partial class TestDrawing : Window
{
    public TestDrawing()
    {
        InitializeComponent();
    }
    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
        DrawForestFire();
    }
    private void DrawForestFire()
    {

        Random rand = new Random();

        while (true)
        {
            for (int y = 0; y < 100; y++)
            {
                for (int x = 0; x < 100; x++)
                {
                    Rectangle rectangle = new Rectangle();

                    Color color = Color.FromRgb((byte)rand.Next(200), 
                              (byte)rand.Next(200), (byte)rand.Next(200));

                    rectangle.Fill = new SolidColorBrush(color);
                    rectangle.Width = 4;
                    rectangle.Height = 4;

                    Canvas.SetTop(rectangle, y * 4);
                    Canvas.SetLeft(rectangle, x * 4);

                    canvas.Children.Add(rectangle);
                }
            }
            canvas.Children.Clear();
        }
    }
}

I also tried to draw run the "DrawForestFire()" in a Thread, with the canvas Object in a "this.Dispatcher.Invoke(() => { ... });" but it didn't made any difference for me. What is going wrong?

And, is there something better than Canvas for this kind of operations?

Tiago Mussi
  • 801
  • 3
  • 13
  • 20
SergSam
  • 365
  • 1
  • 4
  • 16
  • 7
    Never do `while (true)` in the UI thread of an application. Use a timer, e.g. DispatcherTimer, instead. – Clemens Jun 19 '18 at 12:29
  • 1
    You are doing 10000 black rectangles in a continuous loop. No wonder it becomes unresponsive. – Palle Due Jun 19 '18 at 12:31
  • And instead of drawing 10000 Rectangles, consider using WriteableBitmap. – Clemens Jun 19 '18 at 12:31
  • Have you seen ? – NoRegrets Jun 19 '18 at 12:31
  • I tried to create an DispatcherTimer with an Interval of 2 seconds, I did let DrawForestFire() get run each Tick but it only frezzed for a second and nothing happened. I would like to get it work in canvas, after that i would try it out with a Bitmap. – SergSam Jun 19 '18 at 12:49
  • "I would like to get it work in canvas" - you should at least try to *reuse* the Rectangles that are already there. Create them once, and access them later by iterating over the Children collection. – Clemens Jun 19 '18 at 13:17

2 Answers2

4

Instead of adding 10000 Rectangle elements to a Canvas, better draw into a single WriteableBitmap.

Declare an Image element in XAML

<Image x:Name="image"/>

and assign a WriteableBitmap to its Source property. Then use a DispatcherTimer to update the bitmap pixels:

public partial class MainWindow : Window
{
    private const int width = 100;
    private const int height = 100;

    private readonly Random random = new Random();
    private readonly byte[] buffer = new byte[3 * width * height];
    private readonly WriteableBitmap bitmap =
        new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgr24, null);

    public MainWindow()
    {
        InitializeComponent();

        image.Source = bitmap;

        var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
        timer.Tick += OnTimerTick;
        timer.Start();
    }

    private void UpdateBuffer()
    {
        for (var y = 0; y < height; y++)
        {
            for (var x = 0; x < width; x++)
            {
                var i = 3 * (width * y + x);
                buffer[i++] = (byte)random.Next(200);
                buffer[i++] = (byte)random.Next(200);
                buffer[i++] = (byte)random.Next(200);
            }
        }
    }

    private async void OnTimerTick(object sender, EventArgs e)
    {
        await Task.Run(() => UpdateBuffer());

        bitmap.WritePixels(new Int32Rect(0, 0, width, height), buffer, 3 * width, 0);
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • Thanks for the help, i implemented this with the Bitmap because i couldnt get it work with the Canvas. Anyway, I have a question about your solution each pixel isnt drawn one after another but all are drawn after an iteration of both loops. In the Wikipedia page: https://en.wikipedia.org/wiki/Forest-fire_model you can see that each pixel is drawn after another, should i use WritePixels inside the second for loop? If so, how exactly? – SergSam Jun 19 '18 at 14:33
  • The model rules are applied to all pixels of the grid simultaneously. So you should calculate a new value for each pixel on every timer tick. Replace random colors by one of three colors that represent the actual state - empty, occupied by a tree, or burning. – Clemens Jun 21 '18 at 08:49
1

Just for fun, here is a working forest fire implementation. I enjoyed to play around with the self-ignitition and new-tree probabilities.

public partial class MainWindow : Window
{
    private enum CellState
    {
        Empty, Tree, Burning
    }

    private const int width = 400;
    private const int height = 400;

    private readonly WriteableBitmap bitmap =
        new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgr24, null);

    private readonly byte[] buffer = new byte[3 * width * height];
    private readonly Random random = new Random();

    private readonly Dictionary<CellState, Color> stateColors =
        new Dictionary<CellState, Color>
        {
            { CellState.Empty, Colors.Black },
            { CellState.Tree, Colors.Green },
            { CellState.Burning, Colors.Yellow }
        };

    private CellState[,] cells = new CellState[height, width];

    private double ignitionProbability = 0.0001;
    private double newTreeProbability = 0.01;

    public MainWindow()
    {
        InitializeComponent();

        image.Source = bitmap;

        var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
        timer.Tick += OnTimerTick;
        timer.Start();
    }

    private async void OnTimerTick(object sender, EventArgs e)
    {
        await Task.Run(() => UpdateCells());

        bitmap.WritePixels(new Int32Rect(0, 0, width, height), buffer, 3 * width, 0);
    }

    private bool IsBurning(int y, int x)
    {
        return x >= 0 && x < width && y >= 0 && y < height
            && cells[y, x] == CellState.Burning;
    }

    private bool StartsBurning(int y, int x)
    {
        return IsBurning(y - 1, x - 1)
            || IsBurning(y - 1, x)
            || IsBurning(y - 1, x + 1)
            || IsBurning(y, x - 1)
            || IsBurning(y, x + 1)
            || IsBurning(y + 1, x - 1)
            || IsBurning(y + 1, x)
            || IsBurning(y + 1, x + 1)
            || random.NextDouble() <= ignitionProbability;
    }

    private CellState GetNewState(int y, int x)
    {
        var state = cells[y, x];

        switch (state)
        {
            case CellState.Burning:
                state = CellState.Empty;
                break;

            case CellState.Empty:
                if (random.NextDouble() <= newTreeProbability)
                {
                    state = CellState.Tree;
                }
                break;

            case CellState.Tree:
                if (StartsBurning(y, x))
                {
                    state = CellState.Burning;
                }
                break;
        }

        return state;
    }

    private void UpdateCells()
    {
        var newCells = new CellState[height, width];

        for (var y = 0; y < height; y++)
        {
            for (var x = 0; x < width; x++)
            {
                newCells[y, x] = GetNewState(y, x);

                var color = stateColors[newCells[y, x]];
                var i = 3 * (width * y + x);

                buffer[i++] = color.B;
                buffer[i++] = color.G;
                buffer[i++] = color.R;
            }
        }

        cells = newCells;
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268