2010年4月15日 星期四

Game Loop Compare- DispacterTimer, Storyboard, Composition.Rendering

Game Loop 對寫遊戲來說是一個很重要的步驟, 通常我們會將一些不需長時間且不用大量計算的事情, 放在Game Loop裡面去作, 例如:碰撞偵測, 動畫撥放, 更新繪圖座標, 檢查滑鼠或鍵盤的狀態..等.

WPF/Silverlight中, 可以透過下列方式來實作Game Loop,分別是:

  • DispatcherTimer - 可以在指定的時間間隔內, 處理佇列中的執行緒.
  • Storyboard - 以時間軸控制動畫, 和DispatcherTimer的作法類似, 透過Duration屬性決定時間長短, 然後在Completed事件中再次呼叫StoryboardBegin方法, 就可以達到類似計時器的迴圈.
  • CompositionTarget.Rendering - 每當程式要顯示一個畫面(frame)時, 就會引發Rendering事件

下列程式碼將示範如何使用上述的方法, 我們會讓一個圓球在視窗內作移動, 當碰到邊界時會作反彈, 而圓球的座標是在Game Loop中作更新, 並顯示對應的FPS值, 如下圖所示:

[圖 1]利用Game Loop作動畫

xmal檔如下:

<Window x:Class="GameLoopTesting.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" SizeToContent="WidthAndHeight">
<Canvas x:Name="LayoutRoot" Background="Black" Width="800" Height="600">
<TextBlock x:Name="txtFPS"
Foreground="White"
FontFamily="Arial"
FontSize="24"
Text="FPS:"
Panel.ZIndex="99"/>
<Ellipse x:Name="ball"
Width="50"
Height="50"
Fill="LightBlue"
Stroke="Gray"
Canvas.Left="100"
Canvas.Top="100">
</Ellipse>
</Canvas>
</Window>



cs檔如下:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using System.Windows.Media.Animation;

namespace GameLoopTesting
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public enum GameLoopType
{
DISPATCHERTIMER,
STORYBOARD,
RENDERING
}

/// <summary>
/// For object parameters
/// </summary>
private float radius;
private double dx = 1;
private double dy = 1;
private double x = 0;
private double y = 0;


/// <summary>
/// For FPS property
/// </summary>
private int _frameCounter = 0;
private double _timeSinceLastUpdate = 0.0;
private DateTime _lastUpdateTime = DateTime.MinValue;
private double _fpsValue = 0.0;

private Storyboard _sbTrigger;


public Window1()
{
InitializeComponent();

radius = (float)ball.Width/2;
_lastUpdateTime = DateTime.Now;


///Switch game loop type
SwitchGameLoop(GameLoopType.DISPATCHERTIMER);
}


void SwitchGameLoop(GameLoopType gameLoopType)
{
switch (gameLoopType)
{
case GameLoopType.DISPATCHERTIMER:
{
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(10);
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
break;

case GameLoopType.STORYBOARD:
{
_sbTrigger = new Storyboard();
_sbTrigger.Duration = TimeSpan.FromMilliseconds(10);
_sbTrigger.Completed += new EventHandler(sbTrigger_Completed);
_sbTrigger.Begin();

}
break;


case GameLoopType.RENDERING:
{
CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);

}
break;
}
}

void timer_Tick(object sender, EventArgs e)
{
/// Calculate FPS value
/// -----------------------------------------------------------
_frameCounter++;

if ((DateTime.Now - _lastUpdateTime).Seconds >= 1)
{
txtFPS.Text = "FPS : " + _frameCounter;
_frameCounter = 0;
_lastUpdateTime = DateTime.Now;
}
//-----------------------------------------------------------

UpdateObjectPosition();
}

void sbTrigger_Completed(object sender, EventArgs e)
{
UpdateObjectPosition();
_sbTrigger.Begin();
}

void CompositionTarget_Rendering(object sender, EventArgs e)
{
/// Calculate FPS value
/// -----------------------------------------------------------
TimeSpan elapsedTime = DateTime.Now - _lastUpdateTime;

_frameCounter++;
_timeSinceLastUpdate += elapsedTime.TotalSeconds;

if (_frameCounter >= 100)
{
_fpsValue = (int)(_frameCounter / _timeSinceLastUpdate);
txtFPS.Text = "FPS : " + _fpsValue;

_frameCounter = 0;
_timeSinceLastUpdate = 0;
}
_lastUpdateTime = DateTime.Now;
//----------------------------------------------------------------

UpdateObjectPosition();
}


/// <summary>
/// Update the position of object
/// </summary>
private void UpdateObjectPosition()
{
x = ((double)Canvas.GetLeft(ball));
y = ((double)Canvas.GetTop(ball));

x += dx;
y += dy;

Canvas.SetLeft(ball, x);
Canvas.SetTop(ball, y);

if (x > LayoutRoot.Width - 2 * radius)
{
dx = -Math.Abs(dx);
}
if (x < 0)
{
dx = Math.Abs(dx);
}
if (y > LayoutRoot.Height - 2 * radius)
{
dy = -Math.Abs(dy);
}
if (y < 0)
{
dy = Math.Abs(dy);
}
}
}
}


總 結:
* 在WPF中使用Storyboard作Game Loop時, 會發生畫面更新到一半時, 卻停止更新,但Silverlight上卻不會, 原因目前還不知道.
* 透過上述的3種方式, 都可以作為遊戲的迴圈, 不過穩定度和解析度上有所差別.可參考這篇Game Loop部落格的詳細比較.
就穩定度來說 :
CompositionTarget.Rendering > Storyboard > DispatcherTimer


參考文章:
Creating a Game Loop
Application (Game) Loop, FPS and Sprite like Animation
Game Loop

沒有留言:

張貼留言