怪物们都出现了,如何选中自己心仪的怪是主角目前首要做的事。

为了进行鼠标状态区别,我首先对鼠标变化规则进行约束:当鼠标在屏幕上空旷地图区域移动时,鼠标光标形态表现为默认光标 (0号光标图片),当鼠标经过精灵(悬停于其上方)时则变成发光光标(1号光标图片),如果指向的精灵对象为敌对状态时则鼠标光标变为攻击光标(2号光标图片),当使用魔法快捷键时,鼠标光标变成凝法状态(3号光标图片)。

接下来要做的就是用代码来实现这些规则。要实现鼠标光标的变换,我们首先得将这4个光标加入到系统中,这里我新建一个名为Cursors的文件夹用于保存这4个光标,具体添加方法详见第五节。然后在使用的时候如果该代号光标不存在,则通过数据流将光标添加进系统内存中:

public static Cursor[] GameCursors = new Cursor[4];

/// <summary>

/// 返回指定标号光标

/// </summary>

/// <param name="sign">标号</param>

/// <returns>光标</returns>

public static Cursor getCursor(int sign) {

if (GameCursors[sign] == null) {

GameCursors[sign] = new Cursor(new FileStream(string.Format(@"Cursors\{0}.ani", sign), FileMode.Open, FileAccess.Read, FileShare.Read));

}

return GameCursors[sign];

}

一切就绪,现在正式开始实现游戏窗体的鼠标移动事件。既然是鼠标在地图上滑动时产生的效果,因此我们首先添加游戏窗体鼠标移动事件:MouseMove="Window_MouseMove",然后在后台代码中的Window_MouseMove方法里写入相应内容:

private void Window_MouseMove(object sender, MouseEventArgs e) {

this.Cursor = e.Source is QXSpirit ? Super.getCursor(1) : this.Cursor = Super.getCursor(0);

}

假如鼠标经过的对象是QXSpirit类型,则鼠标的光标变为1号,其他情况时,鼠标光标变为0号。这种效果对于做习惯了.NET网站开发的朋友们来说再熟悉不过了,好比导航栏上的鼠标悬停图片切换CSS或JS效果。

这么短短一句话即实现了最简易的精灵对象捕获,我们先来测试一下程序:

细心的朋友会发现,虽然是勉强实现了但这其实并不准确;因为当鼠标并不在怪物实体上时,鼠标仍然会显示为1号光标(如下图),是代码出问题了吗?

其实问题并非出在代码上,这是因为精灵的图片源是背景透明的PNG或GIF格式图片,就拿上图中的“绝对无敌”来说吧,它的每帧图片为200*200尺寸(如下图),

它的有效实体只是该图片的中间区域,而它的旁边有着比较大面积的透明无效区域,虽然在显示上透明区域是不会显示出来的,但是它整个作为200*200尺寸的Image类型控件而存在。因此当鼠标在游戏窗体上移动时,只要处于这200*200区域内时均会显示为1号光标而并不会理睬它是否停留在精灵的有效实体部分。

精灵的图片源均为位图类型,目前我暂时还未发现在WPF/Silverlight中如何实现将位图转换成矢量图的高效直接方法。因此目前解决这个问题的方式只有两种,第一种为通过对当前拾取对象的图片源进行点对点的颜色拾取,然后判断当前鼠标的位置相对于图片源中的点是否为透明,如果不透明则拾取该精灵,具体方法如下:

/// <summary>

/// 获取图片源某点颜色

/// </summary>

public static Color getImagePointColor(BitmapSource bitmapsource, int x, int y) {

CroppedBitmap crop = new CroppedBitmap(bitmapsource as BitmapSource, new Int32Rect(x, y, 1, 1));

byte[] pixels = new byte[4];

try {

crop.CopyPixels(pixels, 4, 0);

crop = null;

} catch (Exception ee) {

MessageBox.Show(ee.ToString());

}

//蓝pixels[0] 绿pixels[1]  红pixels[2] 透明度pixels[3]

return Color.FromArgb(pixels[3], pixels[2], pixels[1], pixels[0]);

}

此方法的优点是精确,可以定位到精灵有效实体的任一像素角落;而缺点是只能在WPF中使用且性能不好,更麻烦的是必须将之放 Try{}Catch{}块内使用,否则极易出错,因为精灵的图片切换太快了。

解决此问题的另一方式为通过定义精灵实体区域参数public double[] EfficaciousSection来实现,此方法也是我推荐使用的方法,兼顾WPF/Silverlight。

EfficaciousSection由4个数组成,以上图为例,它的EfficaciousSection = new double []{80,125,50,145},其中第一个数字表示红色区域左边线距离图片左的距离,第二个数字表示红色区域右边距离图片左边距离,第三个数字表示红色区域上边距离图片顶部的距离,第四个数字代表红色区域底边距离图片顶部的距离,上面所说的红色区域即为精灵的有效实体区域,在后面的鼠标点击或移动判断中,只有当鼠标进入精灵的有效实体区域时我们才变换鼠标光标。

精灵获得了有效实体区域,是否代表可以完美准确的捕捉精灵对象了呢?我们将窗体鼠标移动方法进行如下改进:

if (e.Source is QXSpirit) {

QXSpirit Spirit = e.Source as QXSpirit;

Point p = e.GetPosition(Spirit);

if (p.X >= Spirit.EfficaciousSection[0] && p.X <= Spirit.EfficaciousSection[1]

&& p.Y >= Spirit.EfficaciousSection[2] && p.Y <= Spirit.EfficaciousSection[3]) {

this.Cursor = Super.getCursor(1);

} else {

this.Cursor = Super.getCursor(0);

}

}

然后再运行一下游戏,结果更奇怪的事情出现了:

如上图,此时当鼠标停在主角身上时竟然没有变换光标图片,是代码出问题了吗?当然也不是。我们还是得从图片上找原因。此时怪物的图片遮挡住了主角,因此当鼠标悬停在主角身上时,系统却仍然判断当前捕获的是“绝对无敌”,并且鼠标也未进入它的有效实体范围,因此鼠标光标仍然是0号。

怎么办?搞了这么久到头来仍然是一场空。有朋友提出了将图片裁剪成刚好包裹住精灵有效实体区域不就好了。想法是好的,但是将造成每一帧图片都为不同尺寸规格,在动作中如何切换?每张图片都得定义它距离容器Canvas左上角的距离,一个怪物几百张图片,每张都要定义,这将大大增加游戏的开发负担。

难道没有完美的解决方案了吗?WPF/Silverlight中最不起眼但却有着极其重要作用的神器登场了!对,就是它了:HitTest(命中测试)。

称之为命中测试,不如叫它穿透点击来得更形象些。因为它强大到只要游戏窗口中有的东西,它都能抓出来,想抓几个抓几个,想抓到什么深度(Zindex)就抓到什么深度;更甚者,它可以肢解封装的控件直接抓取其内部任意对象控件;完成以上各种任务如若探囊取物搬轻盈且高效,仅仅是通过模拟鼠标点击几乎忽略不计的敏捷捕获。关于HitTest的更多相关知识及原理请大家自行网上查阅,这里不具体讲解了。接下来我们看下图:

在游戏中如何使用HitTest进行对象捕获的原理在上图中已经描述得非常清楚了,接下来看我如何通过代码进行实现:

首先我定义一个精灵容器用于将捕获到的所有精灵进行收容管理:

List<QXSpirit> SpiritList = new List<QXSpirit>();

接下来定义HitTest的过滤器HitFilter,用于筛选HitTest捕获的对象,我们只需要捕获QXSpirit类型对象即可,然后将之添加进精灵容器:

public HitTestFilterBehavior HitFilter(DependencyObject dObject) {

if (dObject is QXSpirit) {

SpiritList.Add(dObject as QXSpirit);

}

return HitTestFilterBehavior.Continue;

}

每执行一次过滤器后,我们必须重复以上过程继续向更深层次进行捕获,因此在HitTest结果HitResult中执行继续操作以供向下个节点轮循:

public HitTestResultBehavior HitResult(HitTestResult result) {

return HitTestResultBehavior.Continue;

}

HitFilter和HitResult是HitTest中控制流程非常重要的参数,定义完它两后接下来我们在窗体的鼠标移动事件中进行如下HitTest命中测试:

private void Window_MouseMove(object sender, MouseEventArgs e) {

SpiritList.Clear();

Point p = e.GetPosition(Carrier);

VisualTreeHelper.HitTest(

Carrier,

new HitTestFilterCallback(HitFilter),

new HitTestResultCallback(HitResult),

new PointHitTestParameters(p));

if (SpiritList.Count > 0) {

for (int i = 0; i < SpiritList.Count; i++) {

if (isEfficaciousSection(SpiritList[i].EfficaciousSection, e.GetPosition(SpiritList[i]))) {

this.Cursor = Super.getCursor(1);

label3.Content = SpiritList[i].Name; //调试用

break;

} else {

this.Cursor = Super.getCursor(0);

}

}

}

}

每次鼠标移动的时候我们必须清空精灵容器,然后对鼠标当前的点在Carrier中的位置进行点击测试,通过前面的HitFilter和HitResult过滤后得到所有位于鼠标位置的精灵放进容器,然后遍历精灵容器里的所有精灵,只有当该点位于精灵Canvas里的位置处于精灵的有效实体区域时,才算真正的捕获到了精灵。一旦捕获到了精灵则同时更改鼠标光标为1号光标然后退出循环;这里我为了测试是否精确的捕获了精灵对象,设置了名叫label3的文本来显示抓取到的精灵名字。

到此就完成了整个HitTest精确捕获精灵流程,下面我在地图密集的区域内添加30个拥有不同的名字的怪物精灵,然后尝试移动鼠标去分别捕获,通过label3中的名字显示该方法实现起来是极其准确的,比卫星定位还要精确与高效^_^||:

已经能完美捕捉想要的精灵了,但是如何让被捕获的精灵进行特效显示呢?目前的网络游戏中最常用的方式有两种:1、对被捕获的精灵进行描边;2、让被捕获的精灵半透明化。

第一种方法的实现需要首先为精灵控件中的身体部分控件添加一个WPF专有的OuterGlowBitmapEffect效果:

<Image x:Name="Body" Stretch="Fill">

<Image.BitmapEffect>

<OuterGlowBitmapEffect GlowColor="Blue" GlowSize="5" Noise="0" Opacity="1" />

</Image.BitmapEffect>

</Image>

具体意思就是在精灵身体图片不透明区域进行外发光:蓝色,5像素宽,无噪音,完整透明度。其运行效果如下图:

看到这张图的时候或许大家开始有些欣喜若狂了,但是我想告诉大家:此方法绝对的行不通,为什么?一方面此方法只能在WPF中使用,它的原理是时时动态查找图片不透明区域的边缘,然后对边缘路径进行发光滤镜处理;而另一方面由于它是对图片源不透明区域进行时时的边缘查找,将极大的占用游戏的界面线程资源,是极其不友好的表现方式。

因此,为了同时适应WPF/Silverlight,我使用第二种方法作为最终解决方案。这种方法实现起来简单多了,只需要在前面代码的基础上加进行如下更改:

private void Window_MouseMove(object sender, MouseEventArgs e) {

……

if (SpiritList.Count > 0) {

bool targetIsFound = false;

for (int i = 0; i < SpiritList.Count; i++) {

if (!targetIsFound && isEfficaciousSection(SpiritList[i].EfficaciousSection, e.GetPosition(SpiritList[i]))) {

this.Cursor = Super.getCursor(1);

SpiritList[i].Opacity = 0.6;

targetIsFound = true;

label3.Content = SpiritList[i].Name;

} else {

if (!targetIsFound) { this.Cursor = Super.getCursor(0); }

SpiritList[i].Opacity = 1;

}

}

}

}

在鼠标移动事件中仅仅增改6行代码即可以轻松的实现,运行效果如下:

到此为止即完美实现了对精灵的精确捕获。忽忽,是不是感觉向完整的游戏框架目标又迈出了一大步?

在此,我还想对那些极端的朋友说一下:由于目前暂时采用多线程结构,在单核CPU电脑以及Win2003以前的操作系统上运行时,怪物密集的地方会有些卡。但是这根本代表不了游戏引擎的最终性能,教程还有非常非常多的内容没有讲到,优化的技术还在后面呢,太多了就不一一罗列了,大家应该都明白本系列既然取名为教程,代表的就是一个由浅入深的过程,很多人连基础原理都没弄清楚,源码对你有何意义?

小结:HitTest功能强大到几乎无所不能,它是我们实现打怪与施放魔法的前提条件。下一节我将讲解精灵面板界面,以及精灵3大基本属性(生命、魔力、经验值)表现形式的实现方法,敬请关注。

作者:深蓝色右手
出处:http://alamiye010.cnblogs.com/
教程目录及源码下载:点击进入(欢迎加入WPF/Silverlight小组 WPF/Silverlight博客团队)
本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面显著位置给出原文连接,否则保留追究法律责任的权利。

转载于:https://www.cnblogs.com/alamiye010/archive/2009/07/11/1521418.html

C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十五)完美捕捉精灵之神器 -- HitTest...相关推荐

  1. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十八) 完美精灵之八面玲珑(WPF Only)②...

    紧接着上一节,首先得解释一下为什么需要将这272张图片合成为一张大图.因为如果游戏中还有装备.坐骑等其他设置,那么我们就需要对图片源进行时时的合成:同时对272张甚至更多的图片进行合成效率高还是对2张 ...

  2. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十)斜度α地图的构造及算法...

    在当前的网络游戏中,地图基本都是采取一定斜度的拼装地图,这其中存在两种斜度地图的构造方式: 第一种我称之为伪斜度地图:该类型地图表现层图片为斜度的,但地图基底障碍物等的构造则实为正方形,如下图: 其实 ...

  3. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十四) 精灵控件横空出世!①

    在上一节中,我们实现了地图牵引式移动,同时还遗留着一个小尾巴:主角和障碍物该如何跟随着地图的移动而移动? 上节中有点到,只要在地图移动的同时,时时根据主角等对象物体的X,Y坐标进行相对于地图的X,Y坐 ...

  4. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):目录

    本系列教程的示例代码下载(感谢 银光中国 提供资源分流): 第一部分源码:WPFGameTutorial_PartI(1-20节) 第二部分源码:WPFGameTutorial_PartII(21-2 ...

  5. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二)让物体动起来②

    第二种方法,CompositionTarget动画,官方描述为:CompositionTarget对象可以根据每个帧回调来创建自定义动画.其实直接点,CompositionTarget创建的动画是基于 ...

  6. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(一)让物体动起来①

    序:自从QXGame(WPF GAME ENGINE)游戏引擎公布以来,受到很多朋友的热切关注,于是乎有了写教程的想法.那么从今天开始,我将带领大家一步一步的学会如何使用纯C#开发WPF/Silver ...

  7. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(四)实现2D人物动画①

    通过前面的学习,我们掌握了如何动态创建物体移动动画,那么接下来我将介绍WPF中如何将物体换成2D游戏角色,并通过使用前面所讲的DispatcherTimer计时器来实现2D人物角色的各种动作动画. 动 ...

  8. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(四十九) 落雷!治疗!陷阱!连锁闪电!多段群伤!魔法之终极五重奏②...

    本节,我将完成本教程示例游戏的最终两个魔法:传说中的连锁闪电与暴风雪.如此经典与华丽的家伙无论在哪款好游戏中都少不了它们的踪影. 首先是连锁闪电,在<英雄无敌>中体现得尤为出色,击中一个怪 ...

  9. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十六)通用型角色头像面板...

    目前游戏的开发进度已经基本实现了精灵对象之间的普通交互,接下来我们需要朝着实现战斗系统的目标前行.而实现它的前提是必须完善精灵控件的基本属性,如添加生命值.魔法值.活力值.经验值等基本属性并通过窗体界 ...

最新文章

  1. 到底什么是hash?它起什么作用?
  2. Kosaraju 算法查找强连通分支
  3. EXCEL导入导出使用的框架
  4. numpy矩阵运算和常用函数
  5. ubuntu清除无效的右键打开方式
  6. Java面向对象(11)--多态性
  7. [导入]一再的变故,终于决定何去何从.
  8. mysql+e+文件+xls_TP5+PHPexcel导入xls,xlsx文件读取数据
  9. 完全卸载HDP和Ambari
  10. 精通Android自定义View(十三)事件分发简述
  11. 8、TypeScript-解构赋值
  12. exec是不是python的内置函数_Python内置函数(62)——exec
  13. 学习Java必须避开的十大致命雷区,新手入门千万不要踩!
  14. 神经网络中的分类器该如何改成生成器?
  15. pfSense添加子网的几种方式
  16. HarmonyOS竞赛,2021全国大学生物联网设计竞赛正式开赛,全新HarmonyOS赛题引关注...
  17. Win11如何调整鼠标dpi?Win11调整鼠标dpi的方法
  18. 我们整理了20个Python项目,送给正在求职的你
  19. DBeaver21.1.5如何迁移已有数据库连接
  20. BZOJ4987 Tree

热门文章

  1. 求连通块个数(使用并查集)
  2. 怎么添加项目到SVN上面
  3. 11月27号例会记录
  4. mysql 主从单库单表同步 binlog-do-db replicate-do-db
  5. ASP.NET 之异步处理一(Session处理)
  6. MyBatis简介与配置MyBatis+Spring+MySql
  7. Customization larbin
  8. 概率论与数理统计中的算子半群 第一讲 Banach-Steinhaus定理1 Baire‘s Category与Banach-Steinhaus定理的证明
  9. UA MATH636 信息论9 有限域简介
  10. MySQL存储引擎和外键学习