目录
介绍
2个Ticks之间WPF需要的最短时间
增加用于DispatcherTimer的DispatcherPriority
为间隔选择一个现实的持续时间
使tick每100毫秒可靠地增加
- 下载源 - 3.2 KB
我写了一个开源的WPF游戏MasterGrab,它已经成功运行了6年。在这个游戏中,人类玩家与多个机器人对战。最近,我想扩展游戏,让机器人可以相互玩耍。用户可以选择机器人每秒应该移动多少次,每次移动都显示在屏幕上。
我认为使用DispatcherTimer会很容易实现,它每x秒在WPF线程上引发一个Tick事件。此持续时间由DispatcherTimer.Interval控制。当我将它设置为100毫秒时,我注意到我在不规则的时间每秒只看到大约4次移动。因此,我开始研究其DispatcherTimer行为方式以及如何改进。
2个Ticks之间WPF需要的最短时间那么,第一个问题是:DispatcherTimer能跑多快?为了在我的PC上测量它,我编写了一个WPF应用程序,它仅仅运行DispatcherTimer。XAML窗口仅包含一个名称为MainTextBlock的TextBlock:
using System;
using System.Text;
using System.Windows;
using System.Windows.Threading;
namespace WpfTimer {
public partial class MainWindow: Window {
DispatcherTimer timer;
public MainWindow() {
InitializeComponent();
timer = new();
timer.Interval = TimeSpan.FromMilliseconds(0);
timer.Tick += Timer_Tick;
timer.Start();
}
const int timesCount = 20;
DateTime[] times = new DateTime[timesCount];
int timesIndex;
private void Timer_Tick(object? sender, EventArgs e) {
times[timesIndex] = DateTime.Now;
if (++timesIndex>=timesCount) {
timer.Stop();
var sb = new StringBuilder();
var startTime = times[0];
for (int i = 1; i < timesCount; i++) {
var time = times[i];
sb.AppendLine($"{(time - startTime):ss\\.fff} |
{(int)(time - times[i-1]).TotalMilliseconds, 3:##0}");
}
MainTextBox.Text = sb.ToString();
}
}
}
}
输出是:
00.021 | 21
00.021 | 0
00.021 | 0
00.021 | 0
...
对于每个tick,有一行。第一列显示这个tick发生后有多少seconds.milliseconds。第二列显示了此tick和上一个tick之间经过了多长时间。
由于我将Interval设置为0,因此在tick之间没有时间损失,除了第一个tick和第二个tick之间。显然,将Interval设置为0没有意义,我本以为会出现异常。
这是结果Interval = 1 millisecond:
00.192 | 192
00.215 | 22
00.219 | 3
00.235 | 15
00.471 | 236
00.600 | 128
00.743 | 142
00.764 | 21
00.935 | 170
01.239 | 303
01.326 | 87
01.628 | 302
01.894 | 266
02.210 | 316
02.375 | 164
02.435 | 60
02.527 | 92
02.658 | 131
02.685 | 26
我没想到Tick会每毫秒提高一次,但我惊讶地发现2个tick之间可能会经过300多毫秒,考虑到我的应用程序除了运行计时器之外什么都不做。
然后我注意到DispatcherTimer构造函数可以带一个DispatcherPriority参数,该参数似乎设置为Background,这意味着计时器只会运行一次“所有其他非空闲操作都完成”。
Name Priority Description
Invalid -1 This is an invalid priority.
Inactive 0 Operations are not processed.
SystemIdle 1 Operations are processed when the system is idle.
ApplicationIdle 2 Operations are processed when the application is idle.
ContextIdle 3 Operations are processed after background operations have completed.
Background 4 Operations are processed after all other non-idle operations are completed.
Input 5 Operations are processed at the same priority as input.
Loaded 6 Operations are processed when layout and render has finished but just
before items at input priority are serviced. Specifically this is used
when raising the Loaded event.
Render 7 Operations processed at the same priority as rendering.
DataBind 8 Operations are processed at the same priority as data binding.
Normal 9 Operations are processed at normal priority. This is the typical
application priority.
Send 1o Operations are processed before other asynchronous operations. This is
the highest priority.
我在我的游戏应用程序中尝试了我可以使用的最高优先级。显然,必须在显示下一步之前完成渲染。因此,让我们看看Tick使用DispatcherPriority.Input时提升的速度有多快:
00.014 | 14
00.024 | 9
00.091 | 67
00.202 | 111
00.221 | 19
00.226 | 4
00.242 | 16
00.272 | 30
00.307 | 34
00.369 | 61
00.460 | 91
00.493 | 33
00.524 | 30
00.555 | 31
00.586 | 30
00.712 | 125
00.745 | 33
00.761 | 15
00.788 | 27
为间隔选择一个现实的持续时间
我想说它现在可以快近3倍的速度运行。显然,Interval=1millisecond没有真正的意义。那么Interval=100 msec怎么样?
00.193 | 193
00.292 | 98
00.417 | 124
00.592 | 174
00.718 | 126
00.876 | 157
01.001 | 125
01.142 | 141
01.263 | 120
01.392 | 129
01.559 | 166
01.677 | 117
01.872 | 195
02.010 | 137
02.143 | 133
02.256 | 113
02.358 | 101
02.472 | 114
02.589 | 116
糟糕,现在2个tick之间所需的时间几乎总是显着超过100毫秒,而我每秒只得到大约7个而不是10个tick。:-( 问题似乎是在x毫秒的随机延迟后,计时器再次等待100毫秒,而不是100-x毫秒。
所以基本上,我们必须告诉在每个tick事件期间,Interval应该有多少毫秒长,直到下一个tick:
const int constantInterval = 100;//milliseconds
private void Timer_Tick(object? sender, EventArgs e) {
var now = DateTime.Now;
var nowMilliseconds = (int)now.TimeOfDay.TotalMilliseconds;
var timerInterval = constantInterval -
nowMilliseconds%constantInterval + 5;//5: sometimes the tick comes few millisecs early
timer.Interval = TimeSpan.FromMilliseconds(timerInterval);
代码是这样工作的。它试图每0.1秒准确地进行一次tick。因此,如果第一个tick发生在142毫秒之后,则Interval设置为58毫秒而不是100:
00.093 | 93
00.216 | 122
00.311 | 95
00.408 | 96
00.515 | 106
00.611 | 96
00.730 | 119
00.859 | 128
00.929 | 70
00.995 | 65
01.147 | 152
01.209 | 62
01.314 | 104
01.402 | 87
01.496 | 94
01.621 | 125
01.731 | 109
01.794 | 63
01.936 | 141
最后!现在我每秒有10个tick。当然,也不完全是每100毫秒,因为有时,WPF线程仍然需要太多时间来进行其他活动。但是当这种情况发生时,计时器至少会尝试更快地提高下一个tick。
- https://github.com/PeterHuberSg/MasterGrab
https://www.codeproject.com/Articles/5323994/Improving-the-WPF-DispatcherTimer-Precision