上一篇文章介绍了用CompositionTarget_Rendering实现固定时间间隔定时器。本篇将继续这个话题,介绍该定时器的一个实际应用:用一个定时器实现多个帧频不同的动画,笔者正在开发的MMORPG游戏中使用了该技术。前文讨论了该定时器的优点之一,即动画帧只在即将提交UI前处理,可以根据silverlight程序的帧频的变化而自适应调整。本文讨论另外一个优点:不同帧频的动画可以共享同一个定时器。而用其他定时器实现这个需求要麻烦很多。
假设要实现两组动画:动画A和动画B。动画A的帧间隔是100毫秒,动画B的帧间隔是120毫秒。如果用dispatcher timer或storyboard实现,需要怎么做呢?我们不得不先取得两个时间间隔的最大公约数20毫秒,作为定时器的时间间隔,然后每隔5次或6次处理动画A或动画B。但是这个方法的前提条件是可以取得一个合理的最大公约数。如果需要实现更多的动画,而且时间间隔各不相同,或者最大公约数为1毫秒,这个方法的代价就太大了。因为间隔1毫秒的定时器意味着定时器事件过于频繁,造成系统负担。如果某个动画的时间间隔为250毫秒,意味着249次定时器事件都是浪费的。显然这个方法的效果不太理想。
用上文介绍定时器,该问题迎刃而解。定时器的时间间隔总是随着程序运行的帧频自动变化,从而保证在不影响动画效果的情况下单位时间内定时器事件次数最少。每个动画的时间间隔则由各自的类来控制。动画类的代码如下。
public
class
Sprite:Canvas
{
int _timerlInterval = 100 ; // ms
int _lastElapsed = 0 ;
double _left;
double _top;
public Sprite( int speed, double left, double top)
{
_timerlInterval = speed;
_left = left;
_top = top;
this .Loaded += new RoutedEventHandler(Sprite_Loaded);
}
void Sprite_Loaded( object sender, RoutedEventArgs e)
{
this .Children.Add(body);
Canvas.SetLeft( this , _left);
Canvas.SetTop( this , _top);
CompositionTargetMainLoop.Instance.Timeout += new MainLoop.UpdateHandler(Instance_Timeout);
}
Image body = new Image()
{
Stretch = Stretch.None
};
int currentFrame = 4 , endFrame = 9 ;
void Instance_Timeout( object sender, int elapsed)
{
_lastElapsed += elapsed;
if (_lastElapsed >= _timerlInterval)
{
while (_lastElapsed >= _timerlInterval)
_lastElapsed -= _timerlInterval;
// display frames
currentFrame ++ ;
if (currentFrame > endFrame) { currentFrame = 4 ; }
string imgPath = string .Format( " images/0-{0}.png " , currentFrame);
body.Source = new BitmapImage( new Uri(imgPath, UriKind.Relative))
{
CreateOptions = BitmapCreateOptions.None
};
}
}
}
{
int _timerlInterval = 100 ; // ms
int _lastElapsed = 0 ;
double _left;
double _top;
public Sprite( int speed, double left, double top)
{
_timerlInterval = speed;
_left = left;
_top = top;
this .Loaded += new RoutedEventHandler(Sprite_Loaded);
}
void Sprite_Loaded( object sender, RoutedEventArgs e)
{
this .Children.Add(body);
Canvas.SetLeft( this , _left);
Canvas.SetTop( this , _top);
CompositionTargetMainLoop.Instance.Timeout += new MainLoop.UpdateHandler(Instance_Timeout);
}
Image body = new Image()
{
Stretch = Stretch.None
};
int currentFrame = 4 , endFrame = 9 ;
void Instance_Timeout( object sender, int elapsed)
{
_lastElapsed += elapsed;
if (_lastElapsed >= _timerlInterval)
{
while (_lastElapsed >= _timerlInterval)
_lastElapsed -= _timerlInterval;
// display frames
currentFrame ++ ;
if (currentFrame > endFrame) { currentFrame = 4 ; }
string imgPath = string .Format( " images/0-{0}.png " , currentFrame);
body.Source = new BitmapImage( new Uri(imgPath, UriKind.Relative))
{
CreateOptions = BitmapCreateOptions.None
};
}
}
}
主程序部分代码如下。
void
MainPage_Loaded(
object
sender, RoutedEventArgs e)
{
// add sprites
this .LayoutRoot.Children.Add( new Sprite( 100 , 100 , 10 ));
this .LayoutRoot.Children.Add( new Sprite( 200 , 300 , 10 ));
// start timer (main game loop)
CompositionTargetMainLoop.Instance.Start();
}
{
// add sprites
this .LayoutRoot.Children.Add( new Sprite( 100 , 100 , 10 ));
this .LayoutRoot.Children.Add( new Sprite( 200 , 300 , 10 ));
// start timer (main game loop)
CompositionTargetMainLoop.Instance.Start();
}
可以看出,通过一个主定时器,理论上可以添加任意多帧间隔不同的动画。当然添加的动画越多,每次定时事件处理的事务就越多,对UI线程的影响就越大。所以原则上不要在定时事件中处理复杂逻辑运算。另外一点需要注意的是每个动画的帧间隔在统计意义上是固定的,但是每次帧间隔并不完全相同,不过在动画效果上与固定帧间隔是完全相同的。这是由CompositionTarget_Rendering的原理决定的。演示如下:
演示项目源码在
这里下载。