紧接着上一节,首先得解释一下为什么需要将这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 Action和Actions 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.gif和Body1.gif)及武器(同样也制作了两把剑:Weapon1.gif和Weapon2.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这个关联属性让主角在任何动作中均可以显示正确的朝向?请听下回分解。

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

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

  1. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(六)完美移动

    经过前面的介绍和学习,我们分别掌握了如何点击鼠标让对象移动,并且实现2D人物的动作动画.那么,如何将两者完美的进行融合呢?这一节的内容将涉及到很多重要的技术及技巧,很关键哦. 那么同样的,前台xaml ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

最新文章

  1. python隐式调用方法_Python为什么不隐式实现self
  2. 定义进项税代码缺省值
  3. el表达式与jstl的用法
  4. 吴恩达神经网络和深度学习-学习笔记-30-相关符号和计算+单层卷积网络+简单卷积网络示例
  5. 解决64位进程调用32位库文件报错问题
  6. 《非对称风险》读书笔记(一)
  7. qq linux五笔输入法下载软件,qq五笔输入法
  8. 编译原理——词法分析程序
  9. 酒店返现应用评测: 企鹅竟然没有模仿?
  10. 苹果内存不够怎么办_内存硬盘不够用怎么办?手把手教你给自己的笔记本更换,超实用!...
  11. The simplest Singleton
  12. 剑指offe系列之6:旋转数组的最小值
  13. 媒体邀约得3个步骤和5个注意事项
  14. 实现labelme批量json_to_dataset方法(anaconda)
  15. Android studio 启动模拟器出现 VT-x is disabled in BIOS 以及 /dev/kvm is not found
  16. RJ45水晶头接线和网线测试仪如何使用
  17. 数据分析师是青春饭吗,前景如何?
  18. canvas绘制象棋谱
  19. 分享一下自己做电影解说的步骤流程和经验,小白必看!
  20. Docker技术入门与实战 第2版

热门文章

  1. GitHub从入门到精通
  2. mysql主从复制简单配置
  3. 胡总和老朱说的一个小技巧
  4. 维基链(WICC)当前币值应该还远远没有达到它本身应有的高度
  5. 004-什么是软件测试?软件测试的目的与原则
  6. yslow前端性能测试工具
  7. linux桌面创建快捷方式
  8. rtems线程管理与调度(一)
  9. IOS 百度地图获取当前屏幕的经纬度
  10. Linux中文件系统简介