紧接着上一节,首先得解释一下为什么需要将这272张图片合成为一张大图。因为如果游戏中还有装备、坐骑等其他设置,那么我们就需要对图片源进行时时的合成;同时对272张甚至更多的图片进行合成效率高还是对2张大图进行合成效率高这是显而易见的。在本节例子中,主角由身体(衣服)及武器两个部分组成;因此,我们还需要定义一个交错数组来保存已经加载的角色装备合成图到内存中:

        /// <summary>

        /// 角色图片缓存

        /// 交错数组示例PartImage[a,b][c,d]

        /// a为衣服代号(在本例中也可以理解为身体代号,因为换衣服就相当于换角色身体)

        /// b为武器代号

        /// c为角色朝向代号

        /// d代表当前动作帧所处于整合图中的列数

        /// 本例中1-5列为站立,6-13列为跑动,14-20列为攻击,21-26列为施法,27-34列为死亡

        /// 本例中PartImage = new BitmapSource[10, 20][,];即初步设定有10个角色,20种武器

        /// </summary>

        public static BitmapSource[,][,] PartImage = new BitmapSource[10, 20][,];

    例如PartImage[0,6]即代表0号角色拿着6号武器8个方向所有动作帧图片源

    PartImage[4,0]则代表4号角色空着手8个方向所有动作帧图片源

    ……依此类推

    如果您的游戏中还有帽子及坐骑,则需要BitmapSource[,][,,,] PartImage这样将第二组定义为4维数组。

    ……依此类推

    当然,你也可以使用Hashtable(哈希表)、Dictionary(字典)等来代替PartImage[,][,]。但是在数字类型键与对象值对应保存的方式中,我更倾向于交错数组,因为它更清晰、优雅且高效。

    有了承接角色的载体,下面就是如何对上一节中合成的角色大图与武器大图(提取及合成方法同上一节相同)进行拼装,最后分帧存储进PartImage嘿嘿,又现精华:

        /// <summary>

        ///  拼装角色+装备后切割成系列帧图片并保存进内存(装备角色)

        /// </summary>

        /// <param name="Equipment">装备代号数组</param>

        /// <param name="rowNum">帧合成图行数</param>

        /// <param name="colNum">帧合成图列数</param>

        /// <param name="totalWidth">帧图合成后总宽</param>

        /// <param name="totalHeight">帧图合成后总高</param>

        /// <param name="singleWidth">单帧图宽</param>

        /// <param name="singleHeight">单帧图高</param>

        /// <returns>如果缓存中有则读取缓存,否则返回合成的图片源</returns>

        public static BitmapSource[,] EquipPart(int[] Equipment, int rowNum, int colNum, int totalWidth, int totalHeight, int singleWidth, int singleHeight) {

            //Equipment[0]为衣服代号,Equipment[1]为武器代号,本例中装备只由衣服+武器组成

            //假如内存中没有该装备的角色现成图片源则进行读取

            if (PartImage[Equipment[0], Equipment[1]] == null) {

                BitmapSource[,] bitmap = new BitmapSource[rowNum, colNum];

                //加载角色衣服(身体)大图

                BitmapSource bitmapSource = new BitmapImage(new Uri(@"Images\Body" + Equipment[0].ToString() + ".gif", UriKind.Relative));

                //假如武器不是0,即如果角色手上有武器而非空手

                if (Equipment[1] != 0) {

                    //加载武器大图,并与衣服大图组装

                    BitmapSource bitmapSource1 = new BitmapImage(new Uri(@"Images\Weapon" + Equipment[1].ToString() + ".gif", UriKind.Relative));

                    DrawingVisual drawingVisual = new DrawingVisual();

                    Rect rect = new Rect(0, 0, totalWidth, totalHeight);

                    DrawingContext drawingContext = drawingVisual.RenderOpen();

                    drawingContext.DrawImage(bitmapSource, rect);

                    drawingContext.DrawImage(bitmapSource1, rect);

                    drawingContext.Close();

                    RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap(totalWidth, totalHeight, 0, 0, PixelFormats.Pbgra32);

                    renderTargetBitmap.Render(drawingVisual);

                    bitmapSource = renderTargetBitmap;

                    //降低图片质量以提高系统性能(由于本身图片已经为低质量的gif类型,因此效果不大)

                    //RenderOptions.SetBitmapScalingMode(bitmapSource, BitmapScalingMode.LowQuality);

                }

                for (int i = 0; i < rowNum; i++) {

                    for (int j = 0; j < colNum; j++) {

                        bitmap[i, j] = new CroppedBitmap(bitmapSource, new Int32Rect(j * singleWidth, i * singleHeight, singleWidth, singleHeight));

                    }

                }

                //将装备合成图放进内存

                PartImage[Equipment[0], Equipment[1]] = bitmap;

                return bitmap;

            } else {

                //如果内存中已存在该装备的角色图片源则从内存中返回合成图,极大提高性能

                return PartImage[Equipment[0], Equipment[1]];

            }

        }

    该方法我已经做了非常详细的注释,大致原理就是将上一节中合成的角色身体大图(5100*1200那张)与一张同样尺寸的武器大图进行合成,组装成一张5100*1200像素的带武器的角色图,最后再将这张图进行所有序列单帧按150*150尺寸进行切割存储进PartImage这个数组中:

    有了EquipPart()方法后还暂时无法使用它,因为精灵控件还缺少一些能与之对接的属性。因此我们首先还得为可爱的精灵控件添加如下属性:

        // 精灵当前调用的图片源(二维数组):第一个表示角色方向,0朝上4朝下,

// 顺时针依次为0,1,2,3,4,5,6,7;第二个表示该方向帧数

        public BitmapSource[,] Source { get; set; }

        // 精灵方向数量,默认为8个方向

        public int DirectionNum { get; set; }

        // 精灵当前动作状态

        public Actions Action { get; set; }

        // 精灵之前动作状态

        public Actions OldAction { get; set; }

        // 精灵各动作对应的帧列范围(暂时只有5个动作)

        public int[] EachActionFrameRange { get; set; }

        // 精灵每方向总列数

        public int DirectionFrameNum { get; set; }

        // 精灵当前动作开始图片列号

        public int CurrentStartFrame { get; set; }

        // 精灵当前动作结束图片列号

        public int CurrentEndFrame { get; set; }

        // 每张精灵合成大图总宽

        public int TotalWidth { get; set; }

        // 每张精灵合成大图总高

        public int TotalHeight { get; set; }

        // 精灵单张图片宽,默认150

        public int SingleWidth { get; set; }

        // 精灵单张图片高,默认150

        public int SingleHeight { get; set; }

        /// <summary>

        /// 精灵装备代码(目前只有前2)

        /// [0],衣服

        /// [1],武器

        /// [2],头盔

        /// [3],腰带

        /// [4],护手

        /// [5],鞋子

        /// [6],项链

        /// [7],戒指1

        /// [8],戒指2

        /// ……

        /// </summary>

        public int[] Equipment { get; set; }

    看到这些关键属性后是否已经激动不己了?这就对啦,说明你已经进入状态。这里需要对几个特别的属性进行些说明:BitmapSource[,] Source是我们可以通过EquipPart()方法获取的图片源,在精灵生命线程中调用以显示对应的精灵图片; Actions ActionActions OldAction是两个精灵动作的枚举属性,该枚举构造如下:

    public enum Actions {

        /// <summary>

        /// 停止

        /// </summary>

        Stop = 0,

        /// <summary>

        /// 跑动

        /// </summary>

        Run = 1,

        /// <summary>

        /// 战斗

        /// </summary>

        Attack = 2,

        /// <summary>

        /// 施法

        /// </summary>

        Magic = 3,

        /// <summary>

        /// 死亡

        /// </summary>

        Death = 4,

    }

    这两个属性将在游戏中对精灵起到非常关键的调控作用。属性定义完后,我们还得将上一节中制作的角色身体(我制作了两张分别为Body0.gifBody1.gif)及武器(同样也制作了两把剑:Weapon1.gifWeapon2.gif)大图加载进项目中(加载方式请看第五节)

    素材准备就绪,接着需要对这些属性进行设置来初始化主角精灵:

        QXSpirit Spirit = new QXSpirit();

        private void InitSpirit() {

            Spirit.X = 300;

            Spirit.Y = 400;

            Spirit.Timer.Interval = TimeSpan.FromMilliseconds(150);

            //设置角色身体及装备

            Spirit.Equipment[0] = 0;

            Spirit.Equipment[1] = 1;

            Spirit.EachActionFrameRange = new int[] { 5, 8, 7, 6, 8 }; //5个数字在第十七节中讲了多次

            Spirit.DirectionFrameNum = 34; //每个方向行上有34(第十七节中有说明)

            Spirit.TotalWidth = 5100;

            Spirit.TotalHeight = 1200;

            //加载角色图片源(具体参数意思可以到QXSpirit中了解)

            Spirit.Source = Super.EquipPart(Spirit.Equipment,

            Spirit.DirectionNum, Spirit.DirectionFrameNum,

            Spirit.TotalWidth, Spirit.TotalHeight,

            Spirit.SingleWidth, Spirit.SingleHeight

            );

            Carrier.Children.Add(Spirit);

        }

    该方法很简单,注释描述得较清楚了,大家也可以将之与前面章节中的InitSpirit()进行比较来理解。接下来该让精灵动一下了,我们可以将精灵的生命线程进行如下改进:

        //帧推进器

        public int FrameCounter { get; set; }

        //精灵线程间隔事件

        private void Timer_Tick(object sender, EventArgs e) {

            //假如精灵动作发生改变,则调用ChangeAction()方法进行相关参数设置

            if (OldAction != Action) {

                ChangeAction();

            }

            //动态更改精灵图片源以形成精灵连续动作

            Body.Source = Source[(int)Direction, FrameCounter];

            FrameCounter = FrameCounter == CurrentEndFrame ? CurrentStartFrame : FrameCounter + 1;

        }

    这里我将前面章节中的count改成了FrameCounter(即帧推进器,意义差不多,但是在此处效果不同,它更加动态,大家需要承上启下的分析后比较容易理解),然后在生命线程事件中首先判断主角当前的动作状态是否改变(例如主角默认是站立的,当在地图上点击了一下后动作即变成跑动状态),如果改变则调用ChangeAction()方法,该方法完整代码如下:

        /// <summary>

        /// 改变精灵动作状态后激发的属性及线程改变

        /// </summary>

        private void ChangeAction() {

            switch (Action) {

                case Actions.Stop:

                    Timer.Interval = TimeSpan.FromMilliseconds(150); //动作图片切换间隔

                    CurrentStartFrame = 0; //该动作在合成大图中的开始列

                    CurrentEndFrame = EachActionFrameRange[0] - 1; //该动作在合成大图中的结束列

                    OldAction = Actions.Stop; //将当前动作记录进OldAction

                    break;

                case Actions.Run:

                    Timer.Interval = TimeSpan.FromMilliseconds(150);

                    CurrentStartFrame = EachActionFrameRange[0];

                    CurrentEndFrame = EachActionFrameRange[0] + EachActionFrameRange[1] - 1;

                    OldAction = Actions.Run;

                    break;

                case Actions.Attack:

                    Timer.Interval = TimeSpan.FromMilliseconds(120);

                    CurrentStartFrame = EachActionFrameRange[0] + EachActionFrameRange[1];

                    CurrentEndFrame = EachActionFrameRange[0] + EachActionFrameRange[1] + EachActionFrameRange[2] - 1;

                    OldAction = Actions.Attack;

                    break;

                case Actions.Magic:

                    Timer.Interval = TimeSpan.FromMilliseconds(100);

                    CurrentStartFrame = EachActionFrameRange[0] + EachActionFrameRange[1] + EachActionFrameRange[2];

                    CurrentEndFrame = EachActionFrameRange[0] + EachActionFrameRange[1] + EachActionFrameRange[2] + EachActionFrameRange[3] - 1;

                    OldAction = Actions.Magic;

                    break;

                case Actions.Death:

                    Timer.Interval = TimeSpan.FromMilliseconds(150);

                    CurrentStartFrame = EachActionFrameRange[0] + EachActionFrameRange[1] + EachActionFrameRange[2] + EachActionFrameRange[3];

                    CurrentEndFrame = EachActionFrameRange[0] + EachActionFrameRange[1] + EachActionFrameRange[2] + EachActionFrameRange[3] + EachActionFrameRange[4] - 1;

                    OldAction = Actions.Death;

                    break;

            }

            FrameCounter = CurrentStartFrame;

        }

    该方法根据精灵当前的动作状态是5大动作中的哪个进而对精灵的切图参数进行修改,从而达到改变窗口中显示精灵相应动作动画的效果。

    Timer_Tick()事件中判断完精灵动作状态后,就需要动态的配置精灵的图片源了:

    Body.Source = Source[(int)Direction, FrameCounter];

    Source的第一个参数为精灵当前的朝向,第二个参数为帧推进器。有的朋友就问了:前面增加的属性中并没有Direction这个属性呀?是的,我就是为了突出该属性的重要所以特别在此再申明,具体如下:

        //精灵当前朝向:0朝上4朝下,顺时针依次为0,1,2,3,4,5,6,7(关联属性)

        public double Direction {

            get { return (double)GetValue(DirectionProperty); }

            set { SetValue(DirectionProperty, value); }

        }

        public static readonly DependencyProperty DirectionProperty = DependencyProperty.Register(

            "Direction",

            typeof(double),

            typeof(QXSpirit)

        );

    跟着我教程学习的朋友一看就知道它是一个关联属性(参考第十五节),为什么需要将精灵的朝向单独作为一个关联属性来定义?因为我将在主角的Storyboard移动动画中对精灵的方向进行时时修改,以使得寻路移动动画更加平滑(本例中的Storyboard仍然沿用DoubleAnimation类型逐帧动画,而不是objectAnimation类型;因此为了与前面章节更好的兼容,Direction在此设置为double类型。)。

    OK,至此已经写了那么多属性和方法,休息休息看一下我们的成果吧:

终于看到了久违的主角站立动作,是否有种感动得想要流涕的冲动?再看一张

    虽然我们可以通过点击地图上的点进行移动,但是无论如何移动,主角的方向始终都是朝着0(即北)这个方向的。那么如何利Direction这个关联属性让主角在任何动作中均可以显示正确的朝向?请听下回分解。

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