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游戏骨架。
下载链接在这里:Game骨架.rar(如不能打开请右键另存为)
本文就到这里。后续将研究用SharpGL来绘图的相关内容。
微信扫码,自愿捐赠。天涯同道,共谱新篇。
微信捐赠不显示捐赠者个人信息,如需要,请注明联系方式。 |