在标准的MMORPG中,每个精灵对象都占据着一块区域(脚底的一块小面积),该区域同样是障碍物系统中的一部分,并且它是动态的,随着精灵的移动而时时变化着。实现游戏中动态的障碍物构建使障碍物系统趋向完美是全面进军战斗系统的必要前奏,大家不想看着主角和怪物叠在一起打来打去吧?
之前的章节中障碍物数组只有一个,它只包含地图障碍物信息(地图中固定不动的障碍物),因此要实现动态的障碍物构建,我们还得再增加一个障碍物数组做为当前游戏游戏中相对于主角的动态障碍物数组,以下为这两个障碍物数组的定义:
byte[,] FixedObstruction = new byte[1024, 1024], VaryObstruction;
其中FixedObstruction即为前面章节中的Matrix,这里为了方便理解,我将之重新命了名。
那么如何定义精灵的角底的障碍物区域呢?我们还得为精灵控件添加如下两个属性:
// 获取或设置脚底示为障碍物区域扩展宽度
public int HoldWidth { get; set; }
// 获取或设置脚底示为障碍物区域扩展高度
public int HoldHeight { get; set; }
这两个属性分别代表以精灵脚底坐标(即它的X,Y)为中心,拓展的宽度与高度(见下图)。
在本节实例中我将所有精灵的HoldWidth均设置为1,HoldHeight设置为0;这样所有精灵的脚底障碍物区域即为上图中左数第二个精灵所示区域。
定义好精灵脚底障碍物区域后,我们同样还需要对目前的障碍物预测方法进行相应的调整。此时在预测障碍物的时候就必须首先排除自身精灵占据的障碍物区域,然后以该区域边缘点来取代原先的障碍物预测点(关于障碍物预测点可以参考第二十节)。下面我以精灵向左方向移动为例,该方向的障碍物预测需要进行如下改进:
private bool WillCollide() {
switch ((int)Leader.Direction) {
case 6:
return VaryObstruction[
(int)(Leader.X / GridSizeX) - Leader.HoldWidth - 1,
(int)(Leader.Y / GridSizeY)
]
== 0 ? true : false;
}
}
黄色部分代码为在原先基础上新添加的部分,通过它拓展了障碍物预测区域。下图为该例子演示图:
一切就绪,现在让我们着手构建动态障碍物数组VaryObstruction。
同样的,从实现原理切入。我们首先需要拷贝一份固定障碍物数组(FixedObstruction)的浅表副本赋给动态障碍物数组(VaryObstruction);然后循环遍历索敌区域内的所有精灵对象,将它们占据区域全部示为障碍物区域并更新VaryObstruction;最后间隔一定时间重复以上过程继续更新动态障碍物数组。
原理总是比较简单的,做起来往往并非一帆风顺。大家可以从原理中看出两个重点:1、此过程是一个重复无限循环过程,因此我们可以将此之放在一个计时器中,让Tick事件去处理;2、此过程是绝对的性能消耗,如果将之放在界面线程中,很肯定的将导致游戏刷新率大副下降,这将极其影响游戏的流畅性。聪明的朋友此时一定想到了后台处理;没错,这里必须使用后台处理,而且必须是异步跨线程的(即不占用界面线程,同时又能让结果直接影响到界面)。幸运的是,在WPF/Silverlight中,我们可以轻松的使用BackgroundWorker;在Winform时代用过它的朋友都很清楚,它是一个爽歪歪的东西。呵呵,那么接下来的过程就再简单不过了,且看功能实现的关键步骤及代码:
第一步,创建这两个重要对象:
//设置游戏窗体辅助线程
AuxiliaryThread = new DispatcherTimer(DispatcherPriority.Normal);
AuxiliaryThread.Tick += new EventHandler(AuxiliaryThread_Tick);
AuxiliaryThread.Interval = TimeSpan.FromMilliseconds(1000);
AuxiliaryThread.Start();
//设置后台工作者
BackWorker = new BackgroundWorker();
BackWorker.DoWork += new DoWorkEventHandler(BackWorker_DoWork);
BackWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackWorker_RunWorkerCompleted);
第二步,在辅助线程(AuxiliaryThread)计时器中启动异步后台处理:
private void AuxiliaryThread_Tick(object sender, EventArgs e) {
//异步刷新面板及障碍物
if (!BackWorker.IsBusy) { BackWorker.RunWorkerAsync(); }
}
第三步,定义工作委托:delegate void WorkDelegate();
然后在已经注册的后台处理事件(BackWorker_DoWork)中执行异步委托(RefreshFace),从而实现刷新动态障碍物数组:
private void RefreshFace() {
//将当前动态障碍物重置为原始固定值
VaryObstruction = (byte[,])FixedObstruction.Clone();
//重新填充动态障碍物
for (int i = 0; i < Carrier.Children.Count; i++) {
if (Carrier.Children[i] is QXSpirit) {
QXSpirit spirit = Carrier.Children[i] as QXSpirit;
if (spirit != Leader) {
int x = (int)(spirit.X / GridSizeX);
int y = (int)(spirit.Y / GridSizeY);
for (int m = x - spirit.HoldWidth; m <= x + spirit.HoldWidth; m++) {
for (int n = y - spirit.HoldHeight; n <= y + spirit.HoldHeight; n++) {
VaryObstruction[m, n] = 0;
}
}
}
}
}
}
private void BackWorker_DoWork(object sender, DoWorkEventArgs e) {
//跨线程异步刷新障碍物
this.Dispatcher.BeginInvoke(new WorkDelegate(RefreshFace), DispatcherPriority.Normal, null);
}
黄色代码部分即为通过循环来设定精灵脚底的占据区域。在WPF/Silverlight机制中跨线程调用必须在与Dispatcher关联的线程上执行委托(这与以往有些不同),所以此时要实现跨线程的异步处理我们必须通过this.Dispatcher.BeginInvoke()的来实现。并且此线程同样为比较重要的处理,因此优先级别我设定为DispatcherPriority.Normal。
额外的,如果您想调试障碍物刷新的次数或处理其他的相关数据,您可以在注册的后台工作完成事件中进行计数之类的设定,例如下面方式,这样可以很好的对异步结果进行管理(通过e.Result捕获传递过来的结果参数):
private void BackWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
count +=1;
}
通过以上4个步骤即轻松完成了动态障碍物的构建。BackgroundWorker的强大对于处理这区区一丁点的运算还是绰绰有余的,您还可以将更多的任务托付给它。例如我们同样的可以将上一节中讲到的主角头像面板与监视对象头像面板的刷新方法一起放进RefreshFace()中;这样在游戏运行过程中,我们不再需要去管理它哥两,后台工作者会每间隔1秒左右更新它们一次,就象保姆一样照顾得无微不至且不打扰到您的任何其他工作。
下面让我们来测试一下吧。我在屏幕左下角放了个按钮,每次点击地图最上面的妖精怪物都会向右下方移动50象素,大家不妨走到它旁边,然后在它每次动后在站到它移动之前的位置上,测试一下障碍物此时是否还存在,并且向它新的位置方向移动看看新的障碍物是否已成功设置:
最后总结一下:以1秒为频率刷新动态障碍物在高精度需求的游戏中是不够的,往往需要设置为<=500毫秒;并且也不能太小了,过小不仅会造成过度的性能消耗;同时由于异步的原因也极其容易导致错误的障碍物识别,直接的结果就是穿越。我个人建议是在WPF中间隔设置为500毫秒,在Silverlight中间隔设置为1秒。大家在玩2D-MMORPG游戏时是否有过这样的经历:在城里人多的地方,如果游戏网速突然慢了一下或者游戏突然卡了那么一下,一不小心你的角色精灵就与其他某位玩家的角色精灵重叠在一块了,并且此游戏正常情况下是不允许重叠的(回合制游戏允许重叠,而ARPG类的是绝对不允许的)。根据本节讲解的原理我们可以很快的找出原因:在还未来得及更新出最新的动态障碍物数组前,两位角色已经跑到了当前被定义为非障碍物的坐标上。但是大家不要为此过份担心了,或者说此情况属于完全可以接受的范围中。因为由于障碍物数组的固定值部分(即固定的障碍物) FixedObstruction在游戏地图加载的时候就已经加载并固定了,意味着只要地图不更换它是不变的。所以不管处于异步处理的任何阶段,固定的障碍物如房屋啦、墙壁啦、树木啦等等之类的东西将永远无法穿越,这或许才是我们最终希望达到的目的。
这里还需要说明一下,本节构建的动态障碍物只是相对于主角的,也就是只能为主角所用。在后期加入怪物AI后,可以有两种常见的解决方案:第一种为所有怪物均使用FixedObstruction固定障碍物,这样怪物之间会发生重叠。第二种为给精灵增加一个Obstruction属性来保存相对于它们自身时时的障碍物信息,这种方式实现的结果是完美的,但过程是极其损耗空间与时间的。至于还有第三种更完美的方案吗?这是当然的,大家不妨想想,只有当精灵在移动的时候才会用得上障碍物对吧?那么我们是否只需要在每次发起移动前对全局的VaryObstruction进行拷贝,然后稍微处理一下此副本使之成为此次移动所要面对的障碍物,或许这才是最完美的解决之道。
那么来张测试图吧,此地图上随机布局了20个怪物,大家可以尝试在里面穿梭体验饶过怪物的爽快(由于怪物的图片是提取出来的,只做了稍微的加工调整,脚底并未完美的定位到中心,因此障碍物有时显得并不精确,这不是系统问题,而是图片结构引起的 ^_^):
相对于主角的完美障碍物系统总算构建完成。暴风骤雨来临的前夜,让激情燃烧我们的斗志吧,伟大的勇士!下期敬请期待。