5

Alright, so I've done some research into this topic and most of the solutions I've found claim to fix the problem but I am finding that they aren't quite working right. I'm in the early stages of implementing just a simple little particle engine, nothing crazy I'm just doing it out of boredom. I have not done anything like this with WinForms before, I have certainly with C/C++ but this is a new thing for me. The following is the code I am using to draw the particles to the screen, the boiler plate code for the particles is not relevant as it works fine, I am more curious about my actual game loop.

Here is the main code for updates and redraws

public MainWindow()
    {
        this.DoubleBuffered = true;
        InitializeComponent();
        Application.Idle += HandleApplicationIdle;
    }

    void HandleApplicationIdle(object sender, EventArgs e)
    {
        Graphics g = CreateGraphics();

        while (IsApplicationIdle())
        {
            UpdateParticles();
            RenderParticles(g);
            g.Dispose();
        }
    }

    //Variables for drawing the particle
    Pen pen = new Pen(Color.Black, 5);
    Brush brush = new SolidBrush(Color.Blue);
    public bool emmiter = false;

    private void EmitterBtn_Click(object sender, EventArgs e)
    {
        //Determine which emitter to use
        if (emmiter == true)
        {
            //Creates a new particle
            Particle particle = new Particle(EmitterOne.Left, EmitterOne.Top, .5f, .5f, 20, 20);
            emmiter = false;
        }
        else if(emmiter == false)
        {
            Particle particle = new Particle(EmitterTwo.Left, EmitterTwo.Top, -.5f, .5f, 20, 20);
            emmiter = true;
        }
    }

    public void RenderParticles(Graphics renderer)
    {


        Invalidate();
        Thread.Sleep(0);

        //Iterate though the static list of particles
        for (int i = 0; i < Particle.activeParticles.Count; i++)
        {


            //Draw Particles
            renderer.DrawRectangle(pen, Particle.activeParticles[i].x,
                                 Particle.activeParticles[i].y,
                                 Particle.activeParticles[i].w,
                                 Particle.activeParticles[i].h);
        }
    }

    public void UpdateParticles()
    {
        for (int i = 0; i < Particle.activeParticles.Count; i++)
        {
            //Move particles
            Particle.activeParticles[i].MoveParticle();
        }
    }

The issue I am running into is that anytime the screen is getting cleared and updated, it gets this awful flickering, and not only that but it sometimes won't whenever I emit a particle.

The form is basically just using labels as invisible locations on the screen to say where to render each particle.

Anyway, I've seen this topic before but nothing has fixed anything, the current implementation is the least flickery/laggy but is not solving the issue.

Any help is appreciated, thanks!

EDIT* I realized I was never deallocating the graphics object each loop so I did that and there is no more delay whenever I click the emitter button, however the flicker is still there, I updated the code accordingly.

Trevor Hart
  • 993
  • 7
  • 26
  • `Invalidate();` will cause the entire control area to be wiped clean, hence the flicker. Normally one would draw to an offscreen buffer but since you are merely drawing dots, you could remember the old dot locations and erase them prior to drawing the new location. It works to a point. As I said offscreen buffer is preferred. The speed of your animation will fluctuate badly based on `Application.Idle`. You may want to consider using a timer. –  Dec 14 '15 at 05:17
  • Interesting, the other stuff I've read says that the timer is not as reliable although I was thinking the same thing, I may have to give that a try. – Trevor Hart Dec 14 '15 at 05:22
  • The `System.Windows.Forms.Timer` is fine for this sort of thing particularly if you want to target say 30 FPS. The `Timer` is course enough to handle that (we're not after a super precision timer). The problem of having it sit on application idle instead of a timer, is that if you move the mouse about, Windows will actually cutdown on the amount of app idle messages that you and I probably saw in our c++ MFC days and follows on into c# :) –  Dec 14 '15 at 05:25
  • ...That makes sense, I was thinking about that too that maybe it was processing mouse movements in the background so it wasn't going idle but I was like no.....why would it be looking for those events? Okay yeah that definitely makes sense now. Thanks for your answer! – Trevor Hart Dec 14 '15 at 05:29
  • Check out my fully working example of creating a `UserControl` that renders offscreen and flickerfree in my answer here - _[Avoiding creating PictureBoxes again and again](http://stackoverflow.com/questions/28510262/avoiding-creating-pictureboxes-again-and-again)_. Hope this helps good sir :) (oh it will also do desired FPS) –  Dec 14 '15 at 05:29

1 Answers1

5

Getting rid of the visible paint artifacts requires double-buffering. In other words, render the scene into a back-buffer that, when ready, gets quickly blitted to the screen surface in a single step. That's a built-in feature in Winforms, simply set the DoubleBuffered property to true in the form constructor. You must use the Paint event to take advantage of that. Override OnPaint() and call RenderParticles(e.Graphics).

You need to take care of timing, right now your UI thread is burning 100% core and animation speed completely depends on the number of particles and the speed of the machine. Instead of Application.Idle, drop a Timer from the toolbox onto your form. In the Tick event handler, call UpdateParticles() and this.Invalidate() to get the Paint event to fire again. The timer's Interval property value is critical, you get the most reproducible update rate by picking 15 or 31 msec (64 or 32 FPS).

You are not always going to get the desired FPS rate, the timer will simply delay or skip a Tick event if the machine gets busy or is too slow or other code on the UI thread needs to run. To make sure that doesn't affect the animation, you must measure actual elapsed time instead of moving the particles by a fixed amount. Either Environment.TickCount, DateTime.UtcNow or Stopwatch are suitable ways to measure true elapsed time.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536