1

There's some strange mistake with timer and forms.

I am making editor for game. Editor has two forms - MainForm and PreviewForm. PreviewForm contains only control for OpenGL output (Custom control based on GLControl from OpenTK), named glSurface.

glSurface has two inline timers (Windows.Forms.Timer) - one for rendering, and one for updating game state. Timers fires in glSurface method Run(double updateRate, double frameRate).

So, I want to show PreviewForm and run updating and rendering from MainForm. My code is:

PreviewForm = new PreviewForm();
PreviewForm.glSurface.Run(60d, 60d);
PreviewForm.Show(this); //Form is "modal"

Body of Run method:

if (Running)
    throw new Exception("Already run");
_updateRate = updateRate;
_renderRate = frameRate;

var renderFrames = Convert.ToInt32(1000/frameRate);
var updateFrames = Convert.ToInt32(1000/updateRate);
RenderTimer.Interval = renderFrames;
UpdateTimer.Interval = updateFrames;
RenderTimer.Start();
UpdateTimer.Start();
Running = true;

Timers is being initialized in OnVisibleChanged event:

protected override void OnVisibleChanged(EventArgs e)
{
    ...
    RenderTimer = new Timer();
    UpdateTimer = new Timer();
    RenderTimer.Tick += RenderTick;
    UpdateTimer.Tick += UpdateTick;
    ...
}

Weird things start here.

When PreviewForm is showing, nothing happens. BUT when I close that form, both timers fire their events! I have tested for possible cross-thread interaction, but PreviewForm.InvokeRequired and glSurface.InvokeRequired are both false.

Please help me find out what the hell is going on.

V. Pupkin
  • 13
  • 2
  • `PreviewForm.Show(this); //Form is "modal"` Form is "not" modal. ShowDialog would make it modal. – LarsTech Aug 31 '17 at 20:54
  • 3
    It looks like you start the timers before you create them. And then when you create them, you don't start them. – LarsTech Aug 31 '17 at 21:02
  • 'I have tested for possible cross-thread interaction': Are you using multithreading? Windows.Forms.Timers are designed for single threaded applications. – Johan Donne Aug 31 '17 at 21:05
  • do you have code elsewhere that also sets the timers to `new Timer()` (like in the constructor)? because otherwise it seems like you'd have an exception in your `Run` method when you try to access the `.Interval` property of a null object, since that will be called before `VisibleChanged`. – Rufus L Aug 31 '17 at 21:22
  • Thanks for comments, but actual reason is another else. Strangely, timers work fine, UNTIL I call `Show()` on `PreviewForm`. Editor application is not multithreading, but engine library is, and I guess OpenTK library too. I moved all the code into `Run` function, but nothing changed. – V. Pupkin Sep 01 '17 at 18:39
  • Rather than doing those on `OnVisibleChanged` event, please try to move them to the Form's constructor. – Thariq Nugrohotomo Sep 01 '17 at 23:07

2 Answers2

0

In this case declare and initialise and start your timers all within the one code block:

{
    .../...

    RenderTimer = new Timer();
    UpdateTimer = new Timer();
    RenderTimer.Tick += RenderTick;
    UpdateTimer.Tick += UpdateTick;
    var renderFrames = Convert.ToInt32(1000/frameRate);
    var updateFrames = Convert.ToInt32(1000/updateRate);
    RenderTimer.Interval = renderFrames;
    UpdateTimer.Interval = updateFrames;
    RenderTimer.Start();
    UpdateTimer.Start();

    .../...
}

Without seeing the program flow this is the safest option. It appears the variables are local to the OnVisibleChanged event, so I'm not sure how you're not getting a null refernce exception when you're calling them from your if (Running).

The other thing you could do is make them class variables and ensure they are initialised before you use them. Then call start within the if statement.

As for the issue of them starting when the form closes, it's impossible to determine from the code you've shown.

0

Edit: There's a deeper problem.

You really shouldn't be using system timers to drive your game updates and rendering.

System timers on most platforms have low accuracy that's inadequate for high performance multimedia such as audio and most games. On Windows System.Windows.Forms.Timer uses Win32 timers which have particularly low accuracy, typically resulting in intervals at least 15ms off(see this answer). See this technical breakdown and this overview for more information. Basically, even if your code worked correctly your frames would stutter.

Most games "tick" by running an infinite loop in the main thread, doing the following each time(not necessarily in this order):

  • Call back into the framework to handle pending OS events.
  • Track the time difference since the last "tick" so frame-independent timing works.
  • Update the state of the game(such as physics and game logic, possibly in another thread).
  • Render the scene based on a previous update(possibly in another thread).

As noted by commenters, the main problem in your timer code is that initialization is split between Run and OnVisibleChanged. I was unable to reproduce the case where the timer fires after a sub form is closed. I suspect some other code you haven't posted is the cause. You'll save yourself a great deal of trouble if you use OpenTK.GameWindow. It handles the loop plumping for you, similar to XNA. This is an example of one way to integrate it with WinForms. See the manual for more information.

In Run, you set Interval and start each timer. No Tick callbacks are set. In OnVisibleChanged, you recreate the timers and assign Tick callbacks. No intervals are set, and the timer's haven't been started.

The timer initialization code in Run is essentially ignored because no tick callbacks are set and OnVisibleChanged recreates the timers. OnVisibleChanged triggers almost immediately after Run, shortly after you call PreviewForm.Show(this).

If you're dead set on using system timers, this should work:

// somewhere before Run(ideally in the initialization of the main form).
RenderTimer.Interval = Convert.ToInt32(1000 / frameRate);
RenderTimer.Tick += RenderTick;
UpdateTimer.Interval = Convert.ToInt32(1000 / updateRate);
UpdateTimer.Tick += UpdateTick;

void Run(double frameRate, double updateRate)
{
    // ...
    RenderTimer.Start();
    UpdateTimer.Start();
    // ...
    Running = true;
}
// ...
protected override void OnVisibleChanged(EventArgs e)
{
    // ...
    // Don't initialize timers here.
    // ...
}
Koby Duck
  • 1,118
  • 7
  • 15