上篇文章的访问量竟然超过了1000,哈哈,还从来没有这么多人看过我的文章呢。虽然在公司内部,我十分看重交流,互相汲取经验,避免重复造轮子的事情发生,但在这之外,还没有过如此多的人来看自己的东西。
我仍然要强调一个事情,虽然已经强调多遍:朋友们,如果你打算腾出你宝贵的时间来看我写的文章,那么请你一定腾出更多的时间去实践一下,用代码来实践这一切。这要比阅读困难的多,我只花了个把小时就读完了调度器这个章节,但我却花了整整一个月来实现它。但也只有在这样的过程中,才能够学习到属于自己的东西——“实践出真知”。
I : 调度器的扩展
在上篇文章里,我向大家展示了一个简单调度器的结构。在这个基础上,能够做的事情还很多。其中性能的均衡是很重要的一点。建议大家看看这篇文章《游戏里的时钟》。里面那张消去波峰的图很有意思,当然那是一种理想情况,但并非没有可能做到。首先我们要得到系统的负载情况;在Silverlight 中这一点很容易做到;如果您对Silverlight 的API很熟悉,就能够想起来这个 Analytics 类,其实即便不熟悉也没有关系,随时可以查阅Silverlight文档----“Manual is your friend .” 更好的办法就是写成一个工具类,放在那里随时,可以调用。一旦得到CPU的负载,接下来的事情就好办多了,这时候就体现了调度器的强大,在调度器的结构里,需要执行的是一个个在队列里的时间任务和帧任务,还有每帧都执行一次,永不停息的渲染任务。(在实际的开发中,如果有必要,你也可以定义更多的任务类型。)或许某一段特殊的时间内,CPU的负载极高,达到你设置的警戒线了,比如60%,游戏里的人物变得很卡,这是非常糟糕的事情,这个时候就有必要进行一下时钟调节。
在简单调度器里,任务队列的任务是无条件得到执行的,那么现在,可能只有在CPU负载不超过警戒线的情况下得以执行,否则就会进行调节:调节的手段是多样的;调节的策略根据具体的情况而来。你可以简单的在高于警戒线的时候就略过一些任务;也可以把任务的帧间隔/时间间隔拉的更长,比如2帧执行一次的任务,可以是3帧执行一次,甚至你可以把那个永不停息的渲染任务停止,这都取决于你的具体情况。应该说,这里不仅仅是调度器的问题了,还需要很多其他的模块相配合,但最终是在调度器这里得到解决的。或许现在我要赞美一下调度器了,在开发过程中有这样一个单独的出口是多么美妙的事情呀。
据一个例子说明在游戏里调度器的性能调节:在游戏里新登录的时候,在出生地那里总是聚集着很多的人,这时候游戏会变得很卡,虽然你会很快离开这里到其他地方去冒险,但很卡,运动的很慢,离开这个地方都变得困难。这时候我们可以执行一个“视觉重点”的调节策略,假设你的注意力都集中在以你为中心的一小块区域上,对于其他区域你是不那么关心的,虽然随着你四处走动,这个区域在变化,就像你在舞台上被一盏灯照着一样。那么我们就可以调节调度器,把这个区域内的任务都照常执行,但在这个区域外的任务进行调节,注意,这个时候的调节也是要非常小心的,并不是越多越好,其实应该说是越少越好,比如你可以把其他人物的动画渲染帧频放慢,可以取消一些场景特效,但你无论如何都要保证你的网络数据包如期发送。我们需要找到负载最大的任务,但并不是说就要调节它,如果这个任务是非常的重要,那么可怕就要在其他地方想办法了。这都取决于你的游戏具体情况。应该说,设计一套良好的调节策略是不容易的。
在进行你的调节策略时,还需要防止一点,就是调节震荡的产生;比如你现在的CPU负载超过了警戒线,决定把A任务忽略执行,A任务被忽略后,CPU负载很快下降,此时系统觉得一切正常了,于是把一切都恢复了,A任务同样也得以恢复;结果CPU又马上超过负载,于是系统就在警戒线的两侧开始震荡,这不是我们希望的,要避免这些,要把调整量限制的更为合适,或者改进效果统计分析。
II:多线程
摩尔定律不再起作用后,并行编程的重要性怎么提都为过。
我要说一个令人沮丧的事情,在目前的结构和经验中,整体都是基于单线程的。都在探讨高效的算法,精巧的结构。但对于并行,提的不多,经验也少。我也尝试在游戏里引入并行,我必须要承认,这很难。当然了,对于我困难的事情,对于各位来说,或许很容易,毕竟我不是一个“Smart Guy”。调度器基于任务系统的结构,还是给我们使用多线程提供了方便的。首先,需要拆分代码中过于庞杂的函数。 比如在这个方法里 public void ExecuteFrame(),执行力帧任务队列,时间任务队列,还有渲染任务。如果把帧任务队列和时间任务队列拆分出来,就可以建立两个线程来分别执行他们。我要提到的一点就是,避免在不同的线程里执行和UI相关联的事情,即便使用异步委托取得了跨线程的操作权限,得到的结果仍然是阻塞的,串行的,这并没有多少意义。在设计的过程中,应该小心的辨别,分拆得到任务的执行顺序和关联关系,尽量避免竞争资源,逻辑处理放在前面(只要不涉及UI的时候,Silverlight还是可以真正多线程并行处理的),UI的渲染可以统一执行。
这里涉及的种种问题,恐怕只有在实践中慢慢体会了,我希望自己这块砖抛出来,能吸引片片美玉飞来,大家共同探讨探讨。
在这里我把自己写的一个CPU统计的工具类分享给大家。
public class CPUCounter
{
private static TextBlock txtCpuLoader;
private static Canvas _canvas;
private static System.Windows.Threading.DispatcherTimer dt;
private static Analytics analytics;
//此方法使IE的状态栏上显示FPS
//大部分教程提到要在Html里面添加 <param name="EnableFrameRateCounter" value="true"/>,
//但我的实验结果是添加与否都会显示FPS,但IE的安全设置要修改
public static void ShowFrameFrequency(bool _showFrequency, int _maxFrameRate)
{
System.Windows.Interop.SilverlightHost host = Application.Current.Host;
System.Windows.Interop.Settings settings = host.Settings;
settings.EnableFrameRateCounter = _showFrequency;
settings.MaxFrameRate = _maxFrameRate;
}
//添加一个CPU统计到指定的Canvas上面,可以指定颜色大小和位置
//这个统计器是自启动的
public static void AddCPULoader(Canvas _rootCanvas, int _fontSize, Color _color, Point _pos)
{
_canvas = _rootCanvas;
txtCpuLoader = new TextBlock()
{
FontSize = _fontSize,
Foreground = new SolidColorBrush(_color),
FontWeight = FontWeights.Bold
};
_canvas.Children.Add(txtCpuLoader);
Canvas.SetLeft(txtCpuLoader, _pos.X);
Canvas.SetTop(txtCpuLoader, _pos.Y);
dt = new System.Windows.Threading.DispatcherTimer();
dt.Interval = new TimeSpan(0, 0, 0, 0, 1);
analytics = new Analytics();
dt.Tick += new EventHandler(PrintTxt);
dt.Start();
}
static void PrintTxt(object sender, EventArgs e)
{
txtCpuLoader.Text = "CourseCPU:" + GetSuitableDouble(analytics.AverageProcessLoad, 2) +
"% AllCPU:" + GetSuitableDouble(analytics.AverageProcessorLoad, 2) + "%";
}
//卸载CPU统计器
public static void RemoveCPULoader()
{
if (_canvas != null)
{
_canvas.Children.Remove(txtCpuLoader);
if (dt != null)
{
dt.Tick -= new EventHandler(PrintTxt);
dt.Stop();
}
}
}
public static string GetSuitableDouble(double _d, int _digit)
{
string result = string.Empty;
int d = (int)(_d * Math.Pow(10, _digit));
double dd = d / Math.Pow(10, _digit);
if (dd == 0)
{
for (int i = 0; i < _digit; i++)
{
result += "0";
}
return ("0." + result);
}
if (dd.ToString().Length != d.ToString().Length + 1)
{
result = dd.ToString().PadRight(d.ToString().Length + 1, '0');
return result;
}
result = dd.ToString();
return result;
}
}