C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十五)完美捕捉精灵之神器 -- HitTest...
怪物们都出现了,如何选中自己心仪的怪是主角目前首要做的事。
为了进行鼠标状态区别,我首先对鼠标变化规则进行约束:当鼠标在屏幕上空旷地图区域移动时,鼠标光标形态表现为默认光标 (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...相关推荐
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十八) 完美精灵之八面玲珑(WPF Only)②...
紧接着上一节,首先得解释一下为什么需要将这272张图片合成为一张大图.因为如果游戏中还有装备.坐骑等其他设置,那么我们就需要对图片源进行时时的合成:同时对272张甚至更多的图片进行合成效率高还是对2张 ...
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十)斜度α地图的构造及算法...
在当前的网络游戏中,地图基本都是采取一定斜度的拼装地图,这其中存在两种斜度地图的构造方式: 第一种我称之为伪斜度地图:该类型地图表现层图片为斜度的,但地图基底障碍物等的构造则实为正方形,如下图: 其实 ...
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十四) 精灵控件横空出世!①
在上一节中,我们实现了地图牵引式移动,同时还遗留着一个小尾巴:主角和障碍物该如何跟随着地图的移动而移动? 上节中有点到,只要在地图移动的同时,时时根据主角等对象物体的X,Y坐标进行相对于地图的X,Y坐 ...
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):目录
本系列教程的示例代码下载(感谢 银光中国 提供资源分流): 第一部分源码:WPFGameTutorial_PartI(1-20节) 第二部分源码:WPFGameTutorial_PartII(21-2 ...
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二)让物体动起来②
第二种方法,CompositionTarget动画,官方描述为:CompositionTarget对象可以根据每个帧回调来创建自定义动画.其实直接点,CompositionTarget创建的动画是基于 ...
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(一)让物体动起来①
序:自从QXGame(WPF GAME ENGINE)游戏引擎公布以来,受到很多朋友的热切关注,于是乎有了写教程的想法.那么从今天开始,我将带领大家一步一步的学会如何使用纯C#开发WPF/Silver ...
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(四)实现2D人物动画①
通过前面的学习,我们掌握了如何动态创建物体移动动画,那么接下来我将介绍WPF中如何将物体换成2D游戏角色,并通过使用前面所讲的DispatcherTimer计时器来实现2D人物角色的各种动作动画. 动 ...
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(四十九) 落雷!治疗!陷阱!连锁闪电!多段群伤!魔法之终极五重奏②...
本节,我将完成本教程示例游戏的最终两个魔法:传说中的连锁闪电与暴风雪.如此经典与华丽的家伙无论在哪款好游戏中都少不了它们的踪影. 首先是连锁闪电,在<英雄无敌>中体现得尤为出色,击中一个怪 ...
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十六)通用型角色头像面板...
目前游戏的开发进度已经基本实现了精灵对象之间的普通交互,接下来我们需要朝着实现战斗系统的目标前行.而实现它的前提是必须完善精灵控件的基本属性,如添加生命值.魔法值.活力值.经验值等基本属性并通过窗体界 ...
最新文章
- 到底什么是hash?它起什么作用?
- Kosaraju 算法查找强连通分支
- EXCEL导入导出使用的框架
- numpy矩阵运算和常用函数
- ubuntu清除无效的右键打开方式
- Java面向对象(11)--多态性
- [导入]一再的变故,终于决定何去何从.
- mysql+e+文件+xls_TP5+PHPexcel导入xls,xlsx文件读取数据
- 完全卸载HDP和Ambari
- 精通Android自定义View(十三)事件分发简述
- 8、TypeScript-解构赋值
- exec是不是python的内置函数_Python内置函数(62)——exec
- 学习Java必须避开的十大致命雷区,新手入门千万不要踩!
- 神经网络中的分类器该如何改成生成器?
- pfSense添加子网的几种方式
- HarmonyOS竞赛,2021全国大学生物联网设计竞赛正式开赛,全新HarmonyOS赛题引关注...
- Win11如何调整鼠标dpi?Win11调整鼠标dpi的方法
- 我们整理了20个Python项目,送给正在求职的你
- DBeaver21.1.5如何迁移已有数据库连接
- BZOJ4987 Tree
热门文章
- 求连通块个数(使用并查集)
- 怎么添加项目到SVN上面
- 11月27号例会记录
- mysql 主从单库单表同步 binlog-do-db replicate-do-db
- ASP.NET 之异步处理一(Session处理)
- MyBatis简介与配置MyBatis+Spring+MySql
- Customization larbin
- 概率论与数理统计中的算子半群 第一讲 Banach-Steinhaus定理1 Baire‘s Category与Banach-Steinhaus定理的证明
- UA MATH636 信息论9 有限域简介
- MySQL存储引擎和外键学习