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来绘图的相关内容。

  • 相关阅读:
    BZOJ 1609: [Usaco2008 Feb]Eating Together麻烦的聚餐( LIS )
    BZOJ 1660: [Usaco2006 Nov]Bad Hair Day 乱发节( 单调栈 )
    BZOJ 1620: [Usaco2008 Nov]Time Management 时间管理( 二分答案 )
    BZOJ 1639: [Usaco2007 Mar]Monthly Expense 月度开支( 二分答案 )
    JAVA
    CodeForces-327A-Flipping Game
    Python学习笔记(九)- 变量进阶、函数进阶
    HDU6480-A Count Task-字符串+公式
    JQuery学习笔记(一)
    JAVA
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/winform_game_01_gameloop.html
Copyright © 2011-2022 走看看