zoukankan      html  css  js  c++  java
  • 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来绘图的相关内容。

  • 相关阅读:
    codeforces C. Cows and Sequence 解题报告
    codeforces A. Point on Spiral 解题报告
    codeforces C. New Year Ratings Change 解题报告
    codeforces A. Fox and Box Accumulation 解题报告
    codeforces B. Multitasking 解题报告
    git命令使用
    shell简单使用
    知识束缚
    php 调用系统命令
    数据传输方式(前端与后台 ,后台与后台)
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/winform_game_01_gameloop.html
Copyright © 2011-2022 走看看