Winform游戏编程入门1:游戏循环的演化

本文来自codeprojct上一篇文章http://www.codeproject.com/Articles/25909/Game-Programming-One,可以说是翻译,但是只保留精髓部分。

Winform窗体是事件驱动的,但游戏不是。所以我们需要为游戏设计一个循环体(俗称游戏循环?)

/// <summary>
/// 游戏通常不是事件驱动的。
/// <para>所以我们设计一个循环,在循环里面进行“获取输入”、“逻辑处理”和“绘图”操作。</para>
/// <para>缺点:电脑配置不同,场景复杂程度不同等都会导致游戏更新速度不同。</para>
/// <para>实际上,游戏通常会运行的过快,使得玩家反应不过来。</para>
/// </summary>
private static void GameLoop1()
{
    bool runGame = true;
    while (runGame)
    {
        GetInput();
        PerformLogic();
        DrawGraphics();
    }
}

如注释所说,GameLoop1实现了获取输入、逻辑处理和绘图这三项基本功能,算是游戏的骨架。但是这个循环在99%的情况下会因为速度太快使得玩家无法反应过来。

于是出现了下面的改进版。

static bool doStuff = false;
/// <summary>
/// 用计时器控制游戏更新的速度。客服了GameLoop1的缺点。
/// <para>实际上从这一版的游戏开始才是真正能玩的。</para>
/// <para>缺点:通常DrawGraphics是最慢的部分。若这部分太慢,整个游戏速度就会下降。</para>
/// <para>你可以想象DrawGraphics慢慢悠悠的进行着,而mainTimer已经滴答了好多次,doStuff已经多次被置为true,游戏输入和逻辑却无法更新。</para>
/// </summary>
private static void GameLoop2()
{
    Timer mainTimer = new Timer();
    mainTimer.Interval = 1000 / 60;
    mainTimer.Elapsed += new ElapsedEventHandler(mainTimer_Elapsed);
    bool runGame = true;
    while (runGame)
    {
        if (doStuff)
        {
            GetInput();
            PerformLogic();
            DrawGraphics();
            doStuff = false;
        }
    }
}

static void mainTimer_Elapsed(object sender, ElapsedEventArgs e)
{
    doStuff = true;
}

GameLoop2用计时器控制游戏更新的速度。理论上是解决了GameLoop1的问题。

但实际上,一个游戏最耗时的部分是绘图。你可以想象DrawGraphics慢慢悠悠的进行着,而mainTimer已经滴答了好多次,doStuff已经多次被置为true,游戏输入和逻辑却无法更新。

于是又出现了下面的改进版。

static uint speedCounter = 0;
//static bool doStuff = false;
/// <summary>
/// 若DrawGraphics太慢,会导致speedCounter超过1,这样,下次就只进行输入、逻辑处理,省略了绘制画面。
/// <para>克服了GameLoop2的缺点。</para>
/// </summary>
private static void GameLoop3()
{
    Timer mainTimer = new Timer();
    mainTimer.Interval = 1000 / 60;
    mainTimer.Elapsed += new ElapsedEventHandler(mainTimer_Elapsed);
    bool runGame = true;
    while (runGame)
    {
        if (speedCounter > 0)
        {
            GetInput();
            PerformLogic();
            speedCounter--;
            if (speedCounter == 0)
            {
                DrawGraphics();
            }
        }
    }
}

static void mainTimer_Elapsed(object sender, ElapsedEventArgs e)
{
    speedCounter++;
    //doStuff = true;
}

你可以想象,当绘图部分超过一帧(mainTimer的一个Interval),speedCounter会超过1,这样就省略一次绘图操作。解决了GameLoop2的问题。

演化到这里就算是理论可行了。不过要放到Winform程序中,需要形式上做一点改变,本质是不变的。

步骤如下:

1. 创建Winform程序,为主窗体Form1添加一个Timer控件timer1,设置timer1.Enabled属性为true。

2. 为Form1添加两个成员变量。

uint speedCounter = 0;
bool drawGraphics = false;

3. 为timer添加Tick事件。

private void timer1_Tick(object sender, EventArgs e)
{
    speedCounter++;

    PerformGameLogic();

    speedCounter--;
    if (speedCounter == 0)
    {
        drawGraphics = true;
    }
    this.Invalidate();
}

4. 覆盖窗体的OnPaint事件和OnPaintBackground事件

protected override void OnPaint(PaintEventArgs e)
{
    //base.OnPaint(e);
    if (drawGraphics)
    {
        Brush myBrush = new SolidBrush(Color.Black);
        e.Graphics.FillRectangle(myBrush, 0, 0, this.Width, this.Height);
        myBrush = new SolidBrush(Color.Green);
        e.Graphics.FillPie(myBrush, 100, 100, 200, 200, 0, 360);
        myBrush.Dispose();

        drawGraphics = false;
    }
}

protected override void OnPaintBackground(PaintEventArgs e)
{
    //base.OnPaintBackground(e);
    // Nothing to do.
}

大功告成,运行结果如下:

未命名

刚刚说了形式上的变化在步骤中已经看到了:逻辑处理放到了timer事件里(输入部分由Winform的各种鼠标键盘事件完成)。

如果你想问timer1_Tick里面的

if (speedCounter == 0)

是不是始终都是true?有什么意义?

这就是改到Winform后的又一个改进了。如果timer1_Tick的执行时间超过了timer1.Interval,speedCounter == 0可能就不是true了!

所以,这个改进就是,当游戏逻辑的执行时间超过一帧的时候,只有最后一次的超长时间计算后才更新绘图。

这个版本还有一个潜在“问题”,若绘图部分速度太慢,timer1的Tick事件里不停的调用this.Invalidate();,会不会导致OnPaint()事件在一次执行尚未完毕的时候就开始了下一次的执行?

答案是不会。原因嘛,不知道……我只是通过试验发现,即使Invalidate()函数比OnPaint()执行的频率快,也不会引起OnPaint()事件发生那种情况。最多是一次执行完毕的瞬间立即开始执行下一次。我猜想这是windows底层的消息队列机制在起作用吧。有高手懂的话请多多指点哈。

而且我又通过试验发现,即使把timer1的Interval设定为很小(比如10毫秒),若OnPaint执行时间很长,timer1的下一次Tick也会被顺延到OnPaint执行完之后才发生。就是说,这个版本不能保证游戏每一帧的等时性。

好吧, 问题太多了。原作者本来很好的思路,到最后弄的什么都不是。Timer只应该用来计时,GameLogic和绘图分别用两个线程完成,输入应该保存到一个队列里,在Tick时统一处理。这才对。

我只好自己整理了一个新版本。算是吸收了原作者的精华,应用到Winform上面来了。

所以我自己创建了新的版本。

用SharpGL做绘图,后台线程做GameLogic,System.Timers.Timer做定时器的3D游戏骨架。

image

下载链接在这里:Game骨架.rar(如不能打开请右键另存为)

本文就到这里。后续将研究用SharpGL来绘图的相关内容。

原文地址:https://www.cnblogs.com/bitzhuwei/p/winform_game_01_gameloop.html