在表格关闭之前,计时器不会打勾

时间:2017-08-31 20:47:20

标签: c# forms timer

计时器和表格有一些奇怪的错误。

我正在制作游戏编辑器。编辑器有两种形式 - MainFormPreviewFormPreviewForm仅包含OpenGL输出控件(基于OpenTK的GLControl的自定义控件),名为glSurface

glSurface有两个内联计时器(Windows.Forms.Timer) - 一个用于渲染,另一个用于更新游戏状态。计时器以glSurface方法Run(double updateRate, double frameRate)触发。

所以,我想展示PreviewForm并从MainForm运行更新和渲染。 我的代码是:

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

Run方法的正文:

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;

正在OnVisibleChanged事件中初始化计时器:

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

奇怪的事情从这里开始。

显示PreviewForm时,没有任何反应。但当我关闭那个表格时,两个计时器都会发射它们的事件!我已经测试了可能的跨线程交互,但PreviewForm.InvokeRequiredglSurface.InvokeRequired都是false

请帮我弄清楚到底发生了什么。

2 个答案:

答案 0 :(得分:0)

在这种情况下,在一个代码块中声明并初始化并启动计时器:

{
    .../...

    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();

    .../...
}

如果没有看到程序流程,这是最安全的选择。看来这些变量是OnVisibleChanged事件的本地变量,所以当您从if (Running)调用它们时,我不确定您是如何获得空参考例外的

您可以做的另一件事是让它们成为类变量,并确保在使用它们之前对它们进行初始化。然后在if语句中调用start。

至于表单关闭时开始的问题,无法根据您所显示的代码确定。

答案 1 :(得分:0)

修改:这是一个更深层次的问题。

你真的不应该使用系统计时器来推动游戏更新和渲染。

大多数平台上的系统定时器都具有低精度,不适合高性能多媒体,如音频和大多数游戏。在Windows System.Windows.Forms.Timer上使用精度特别低的Win32计时器,通常会导致间隔时间至少减少15毫秒(请参阅this answer)。有关详细信息,请参阅this technical breakdownthis overview。基本上,即使您的代码正常工作,您的框架也会断断续续。

大多数游戏" tick"通过在主线程中运行无限循环,每次执行以下操作(不一定按此顺序):

  • 回调框架以处理待处理的OS事件。
  • 跟踪自上次" tick"以来的时差。所以与帧无关的时序有效。
  • 更新游戏状态(例如物理和游戏逻辑,可能在另一个线程中)。
  • 根据先前的更新(可能在另一个线程中)渲染场景。

如评论者所述,您的计时器代码中的主要问题是初始化分为RunOnVisibleChanged。我无法重现在子窗体关闭后计时器触发的情况。我怀疑你发布的其他代码是原因。如果您使用OpenTK.GameWindow,您将为自己省去很多麻烦。它为你处理循环饱满,类似于XNA。 This是将其与WinForms集成的一种方法的示例。有关详细信息,请参阅the manual

Run中,您设置Interval并启动每个计时器。未设置Tick个回调。在OnVisibleChanged中,您可以重新创建计时器并指定Tick个回调。没有设置间隔,并且计时器尚未启动。

Run中的计时器初始化代码基本上被忽略,因为没有设置tick回调并且OnVisibleChanged重新创建了计时器。在您致电OnVisibleChanged后不久,Run几乎会立即触发PreviewForm.Show(this)

如果您已经开始使用系统计时器,那么这应该可行:

// 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.
    // ...
}