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

    为了进行鼠标状态区别,我首先对鼠标变化规则进行约束:当鼠标在屏幕上空旷地图区域移动时,鼠标光标形态表现为默认光标 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网站开发的朋友们来说再熟悉不过了,好比导航栏上的鼠标悬停图片切换CSSJS效果。

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

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

    其实问题并非出在代码上,这是因为精灵的图片源是背景透明的PNGGIF格式图片,就拿上图中的“绝对无敌”来说吧,它的每帧图片为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

    EfficaciousSection4个数组成,以上图为例,它的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;

        }

    HitFilterHitResultHitTest中控制流程非常重要的参数,定义完它两后接下来我们在窗体的鼠标移动事件中进行如下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中的位置进行点击测试,通过前面的HitFilterHitResult过滤后得到所有位于鼠标位置的精灵放进容器,然后遍历精灵容器里的所有精灵,只有当该点位于精灵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大基本属性(生命、魔力、经验值)表现形式的实现方法,敬请关注。

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