本文版权归 博客园 及其原创作者 深蓝色右手 共同所有。
此处纯粹共享+收藏,如有再转,请按如下方式于醒目位置详细标明原创作者及原文出处,以示尊重!!
作者:深蓝色右手
博客:http://www.cnblogs.com/alamiye010/
原文:C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十五)完美捕捉精灵之神器 -- HitTest

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

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

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

public staticCursor[] GameCursors =newCursor[4];

///<summary>

///返回指定标号光标

///</summary>

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

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

public static Cursor getCursor(int sign) {

if (GameCursors[sign] ==null) {

GameCursors[sign] = newCursor(newFileStream(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.SourceisQXSpirit ?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(bitmapsourceas 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.SourceasQXSpirit;

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 =newList<QXSpirit>();

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

public HitTestFilterBehavior HitFilter(DependencyObject dObject) {

if (dObject is QXSpirit) {

SpiritList.Add(dObject asQXSpirit);

}

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博客团队)
本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面显著位置给出原文连接,否则保留追究法律责任的权利。

标签: WPF/Silverlight动画游戏教程

WPF之完美捕捉精灵神器 -- HitTest相关推荐

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

    怪物们都出现了,如何选中自己心仪的怪是主角目前首要做的事. 为了进行鼠标状态区别,我首先对鼠标变化规则进行约束:当鼠标在屏幕上空旷地图区域移动时,鼠标光标形态表现为默认光标 (0号光标图片),当鼠标经 ...

  2. 个体值0和31差多少攻击_口袋妖怪:攻略篇!个体值有多重要?6V宝可梦才是完美的精灵!...

    爱生活,爱游戏,大家好,我是你们的好朋友汤圆.关注汤圆,收获更多快乐哦! 口袋妖怪到目前为止有近900只精灵,18种属性,每种属性都代表着克制关系,但我们今天不讲这个,我们讲精灵的能力值,看看你心仪的 ...

  3. pd对焦速度_完美捕捉5次爆炸的瞬间,魅蓝Note6的双PD对焦堪称逆天!

    原标题:完美捕捉5次爆炸的瞬间,魅蓝Note6的双PD对焦堪称逆天! 随着手机拍照技术的不断突破,手机拍照效果让我们惊叹不已.全新发布的魅蓝Note6手机之前已经被曝光了拍照样张,同时官网也在发布会前 ...

  4. 手游口袋精灵java_手游口袋精灵2新手攻略 捕捉精灵前的准备

    你的小勇士出发前一定要做好小编提到的以下几件事,正所谓不打无准备之仗,想要捕捉心仪的精灵就更要好好筹备啦!来跟小编一起进入口袋精灵2的梦想世界,学习一下新手攻略吧! 1.商场必备 补给与存储 首先我们 ...

  5. 20张图片完美捕捉数据科学发展的瞬间

    概述 数据不会让过去更好,但是,它确实可以创造一个美妙的未来. 近年来,许多公司已经在数据科学领域投资数百万美元.这显示了对数据科学潜力的巨大信仰,相信它可以创造更好的世界.更好的生活和更好的未来. ...

  6. 经典 :20张图片完美捕捉数据科学发展的瞬间

    概述 数据不会让过去更好,但是,它确实可以创造一个美妙的未来. 近年来,许多公司已经在数据科学领域投资数百万美元.这显示了对数据科学潜力的巨大信仰,相信它可以创造更好的世界.更好的生活和更好的未来. ...

  7. 完美破解下载神器IDM

    首先感谢原创作者:sasalemma 文章发布在百度idm贴吧.此方法不用更新破解补丁,节省了很多麻烦的设置工作.也希望在此抛砖引玉,引来大神把其他软件的此类破解大法贡献出来! 利用系统防火墙的设置破 ...

  8. 友友球捕获率_口袋妖怪精灵球大科普,大师球能捕捉创世神吗?

    精灵球:是口袋妖怪世界中捕捉精灵的重要道具,主要功能是捕捉口袋妖怪,成为己方伙伴:在游戏.动画.漫画等各领域中设定又稍有不同各有不同用途:精灵球的设计基本上是以游戏为原则:即使是体型最大的吼鲸王和最重 ...

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

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

最新文章

  1. 8大趋势已现,未来传感器将彻底改变你的生活!
  2. IE/Firefox中css兼容常见问题
  3. 三十八、商业智能与ETL基础知识
  4. springxml解析
  5. android 设置线程优先级
  6. Navicat连接Mysql 8.0.16报错:Client does not support authentication protocol requested by server?
  7. 别担心!人工智能不会抢你的工作
  8. 判断一个整数的奇偶性php,【算法】- 判断一个整数是否是奇数
  9. java 对象的强制类型转换
  10. C语言的中常用的函数
  11. java实现积分抽奖_java毕业设计_springboot框架的超市消费积分抽奖
  12. INA230测量电压电流功率
  13. ISLR 读书笔记二:评估模型的准确性
  14. 100多个经典常用的PHP功能插件大全实例演示和下载
  15. 固定资产管理系统能给行政和IT人员带来什么?
  16. 扇贝开发:国内NFT平台运营主要合规问题
  17. 【转】strace命令详解
  18. 腾讯汤道生:产业互联网真正目的是降本增效
  19. 当前最流行的 js 工具库
  20. 闽南歌歌词有一句电子计算机,抖音wow you can really dance下一句是什么歌 歌词全文...

热门文章

  1. python不调包实现sobel_python利用百度云接口实现车牌识别的示例
  2. 1470. 重新排列数组
  3. 字符串型String
  4. 旷视科技提出双向网络BiSeNet:实现实时语义分割
  5. 在java中String类为什么要设计成final
  6. Linux文件目录命令,有这些就够了?
  7. DDD(Domain-Driven Design)领域驱动设计-(一)整体概述
  8. WSL2 下的 Docker 配置,使用网易云镜像 + 更改 docker 文件系统(否则无法 apt update)
  9. 未来计算机技术的发展趋势有哪些,计算机技术的未来发展趋势,以及其应用范围...
  10. 数字后端基本概念介绍<IO Cluster>