即开即玩是网页游戏相比传统客户端游戏的最大优势。如果说在每台电脑安装上G的客户端是一种资源浪费及时间污染;那么Silverlight作为RIA界的新宠儿,在继承祖辈优秀血统的前提下拥有更加卓越的性能及更为曼妙的动态表现,势将引领网络未来世界进入那令人神往的低碳空间。
笔者学习Silverlight开发近2年,在写第一部Silverlight游戏系列教程时为了尽快的实现目标而将所有素材资源打包进XAP中。与其他Silverlight初学者一样,这或许是我们所必须会经历的一个过程。QXGameEngine最终完成时,它的体积已经达到了18M有余,功能需求满足了预期,可是却让大量还未接触过Silverlight的朋友产生巨大困惑:难道Silverlight仅仅是镶嵌于网页中的游戏客户端?
QXGameEngine作为教程示例再贴切不过;但如果说要将之商业化,首先就违背了RIA的初衷:即开即用。漫长等待是对用户体验的无情扼杀,不仅随时可能造成用户流失,毫不客气的尊称其为失败品亦不为过。
针对Silverlight资源配置问题,国外很多朋友首先想到且用得最多的莫过于独立存储(Isolated Storage)。比如Dark Ieign -- 最近在网站上看到的Silverlight2D即时战略大作。虽然其等待资源下载过程中我们可以通过欣赏游戏宣传动画短片打发时间,但本质却与QXGameEngine如出一辙,将所有的资源必须性的一次性下载完,不管会不会用到,这样的形式仍旧十分糟糕。是的,Silverlight才刚起步,毕竟Dark Ieign让我们看到的是一款大作风范。历史中新生事物的起源都必然会经历一个适应期,不久的将来一旦Silverlight完美动态技术普及开后,堪比星际争霸2之类大作终有一天会出现在Silverlight平台上,拭目以待!
8个多月过去,第一部游戏教程全部完成了。其后QXSceneEditor在笔者思考如何实现Silverlight游戏快速开发的同时孕育而生。其搭建于一个兼具静态资源及动态资源混合使用的游戏框架下,XAP包存放的不再是一切资源,而仅仅是一些常用的小图片、图标及场景、精灵等配置文件;相对于前作,该场景编辑器动态参数及动态配置的灵活结构可以轻松拓展出任意类型的各式游戏,而不仅仅再局限于RPG。
又是3个月,第二部教程伴随着3个全新Demo的完成落下帷幕。此时再次重温QXSceneEditor,仔细琢磨又一次感到其结构仍不完美:一开始就加载所有场景及所有精灵的xml配置信息,假想一下如果有100个场景,而玩家或许从注册到对游戏失去兴趣也走不到10个场景,那剩下的90个场景的配置文件容量不是白下载了?林林种种……。随后的第三部课程虽然有了结构性的进步,然后更多的与第一部类似,着重在于基础学习。而后,在中游在线的《WOWO世界》的感悟下触使我决定再次去探求Silverlight-WebGame的极至框架,理想中它应该贯穿着“一切动态”,“按需加载”的搭建理念,秉持“体验至上”,“优异性能”的整体特性,于是诞生了想要从头来过的全新思路。
这是一次真正的从零开始,技术的革新让我决心从游戏的开始制作到游戏的结局,不在乎这个结局是喜是悲;于是有了这个全新的第四部作品,它们将倾注更多关于自己领悟的Silverlight-Web游戏设计思想。同以往一样,如果朋友们觉得有不对之处,恳请善意指正。这三个系列的诞生与发展不光是我一个人的努力,没有大家的支持、建议和批评,也不会坚持到今天。
以上抒情。
接下来将进入本节的主要内容:Silverlight WebGame中的动态资源配置。
从Javascript的var到F#的lambda,C#在取之精华,去其糟粕的同时让自身发展得更为完美,趋势中弥漫着“动态”给我们编程带来的无限芳香。“动态”,不论在任何场合都是一种优秀表现;与“动态”相呼应的是“自适应”,从布局的“自适应宽高”到游戏资源的“自适应按需下载”,这些均可以从当下诸多优秀的软件架构中得到充分体现。
在Silverlight学习之初大家已意识到动态下载的重要性,从最初的探讨dll动态下载、xaml动态加载、xap动态获取到数据传输的序列化与反序列化以及资源的压缩与解压。直到今天,笔者在反复尝试下终于完成了个人感觉目前效果还算较好的资源结构布局模式:独立于对象的配置布局体系。
何谓独立于对象的配置布局体系?我们不妨先看张图:
以游戏中的动画为例,素材布局以数字代号顺次标识,与传统不同的关键在于我为每个动画资源都配备了一个描述该动画信息的Info.xml配置文件,以上图0号动画为例,该动画的Info.xml信息如下:
<?xml version="1.0" encoding="utf-8" ?>
<Animation Width="400" Height="400" FrameNum="7" Interval="140" Format="1" Images="Animation/0,0-6,png"/>
当游戏中某个场合需要演示该动画时,我们会首先下载该动画对应的Info.xml并进行解析,再将所有参数赋予自定义的如AnimationButton控件,从而实现动态呈现。
接下去的问题是我们如何下载该Info.xml配置文件,以及解析完成后如何实现队列下载所需的N张图片?另外,对于已经下载好的图象文件及xml配置文件我们该如何区别对待?
大家是否还记得在Silverlight游戏设计(Game Design)这部教程中,我为每个Demo都附加有一个后缀为. Tools的项目,该项目中除了A*寻路的方法类库外还包含一个资源下载用类Downloader。然而此下载器仅仅实现的是单个图象文件下载,为了满足任意文件下载需求,且与接下来的队列下载类所兼容,我这里的将之进行了如下修改:
public sealed class DownloaderEventArgs : EventArgs {
public string uri { get; set; }
public Stream stream { get; set; }
}
public delegate void DownLoaderEventHandler(object sender, DownloaderEventArgs e);
public sealed class Downloader {
/// <summary>
/// 已下载的文件路径字典
/// </summary>
static Dictionary<string, bool> files = new Dictionary<string, bool>();
/// <summary>
/// 资源正在读取中
/// </summary>
public event DownLoaderEventHandler Loading;
/// <summary>
/// 资源下载完成时触发
/// </summary>
public event DownLoaderEventHandler Completed;
DispatcherTimer timer;
/// <summary>
/// 通过WebClient下载资源
/// </summary>
public void GetResource(string uri) {
//假如该路径图片还未下载过
if (!files.ContainsKey(uri)) {
WebClient webClient = new WebClient();
webClient.OpenReadCompleted += (s, e) => {
//该路径图片已下载完成
files[uri] = true;
if (Completed != null) { Completed(this, new DownloaderEventArgs() { uri = uri, stream = e.Result }); }
};
webClient.OpenReadAsync(new Uri(uri, UriKind.Relative), uri);
files.Add(uri, false);
if (Loading != null) { Loading(this, new DownloaderEventArgs() { uri = uri, stream = null }); }
} else {
//假如该路径图片已下载完成
if (files[uri]) {
if (Completed != null) { Completed(this, new DownloaderEventArgs() { uri = uri, stream = null }); }
} else {
if (timer == null) {
//假如该路径图片正在下载,则需要等待,每隔1秒检测一次是否已下载完成
timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };
timer.Tick += (s, e) => {
if (files[uri]) {
if (Completed != null) { Completed(this, new DownloaderEventArgs() { uri = uri, stream = null }); }
DispatcherTimer t = s as DispatcherTimer;
t.Stop();
t = null;
}
};
timer.Start();
}
if (Loading != null) { Loading(this, new DownloaderEventArgs() { uri = uri, stream = null }); }
}
}
}
}
此时该类不仅能下载图象文件,同时还能将数据流中的Stream作为参数附给Completed事件;后面的事情就简单多了,在Completed事件中可以直接通过XElement xelement = XElement.Load(e.stream)对该xml文件进行加载,后续的步骤就不用再多说了吧。
剩下的就是解析完xml配置后该如何队列下载所需的一切图象资源。很多朋友一听到队列、按需就发慌,其实我们只要对上面重新编写的Downloader再进一步逻辑封装就OK了,这个神奇的DownloadManager类其实也不过如此嘛:
public sealed class DownloadManager {
public event EventHandler Completed;
int uriNum, count;
List<string> uris;
/// <summary>
/// 根据资源表下载资源
/// </summary>
/// <param name="uris">资源地址表</param>
public void GetResource(List<string> uris) {
if (uris.Count == 0) {
if (Completed != null) { Completed(this, new EventArgs()); }
} else {
this.uris = uris;
uriNum = uris.Count;
DownloadResource(0);
}
}
private void DownloadResource(int index) {
Downloader downloader = new Downloader();
downloader.Completed += new DownLoaderEventHandler(downloader_Completed);
downloader.GetResource(uris[index]);
}
private void downloader_Completed(object s, DownloaderEventArgs e) {
count++;
if (count < uriNum) {
DownloadResource(count);
} else {
if (Completed != null) { Completed(this, new EventArgs()); }
}
}
}
再回到开头,以自定义呈现动画控件AnimationButton为例,在控件初始化后我们首先下载对应代号的动画xml配置文件:
Downloader downloader = new Downloader();
downloader.GetResource(Global.WebPath(string.Format("Animation/{0}/Info.xml", code)));
然后注册Completed事件,一旦完成后对配置文件进行解析并取值,比如:
downloader.Completed += (s1, e1) => {
string key = string.Format("Animation{0}", code);
if (e1.stream != null) {
Global.PackInfo.Add(key, XElement.Load(e1.stream));
}
XElement config = Global.PackInfo[key].DescendantsAndSelf("Animation").Single();
this.format = Global.FileFormat((Format)((int)config.Attribute("Format")));
this.frameNum = (int)config.Attribute("FrameNum");
this.Width = (double)config.Attribute("Width");
this.Height = (double)config.Attribute("Height");
……
Waiting waiting = new Waiting(this.Width, this.Height) { Z = 999999 };
//下载动画资源
DownloadManager downloadManager = new DownloadManager();
downloadManager.Completed += (s2, e2) => { Heart.Start(); this.Children.Remove(waiting); };
downloadManager.GetResource(Global.GetImageList(config.Attribute("Images").Value));
};
该事件中最后3行即实现了通过DownloadManager来获取配置文件中”Images”节点的属性值(Attribute("Images").Value)后解析并队列下载所需图片,此过程中我们可以先展示一个Waiting动画作为代替,当队列下载完成后(downloadManager.Completed)我们再对实际动画进行播放呈现:
Heart.Tick += (s, e) => {
if (currentFrame == frameNum) {
switch (kind) {
case AnimationKinds.Once:
currentFrame = 0;
Heart.Stop();
break;
case AnimationKinds.OnceToDispose:
Heart.Stop();
(this.Parent as Canvas).Children.Remove(this);
break;
case AnimationKinds.Loop:
currentFrame = 0;
break;
}
}
this.Background = new ImageBrush() { ImageSource = Global.GetWebImage(string.Format(@"Animation/{0}/{1}{2}", code, currentFrame, format)) };
currentFrame++;
};
最后一个问题是我们应该如何处理下载得到的资源才最合理呢?一方面尽量少的占用额外的内存资源;另一方面在下次再请求获取时不用重复执行下载而浪费带宽流量。
针对此问题我的解决思路是:通过静态字典(Dictionary<string, XElement>)缓存xml配置文件,通过延迟加载(BitmapCreateOptions.DelayCreation)与浏览器共用图象缓存,音乐文件则通过MediaElement直接加载,此过程会对加载的视频或音频流进行时时播放,用户体验很好而无须我们任何代码干扰。
到此,从资源配置布局到所有资源按需队列下载并实现缓存,独立于对象的配置布局体系就构建完成了。本文仅以实现自定义动画为例,实际游戏开发中无论精灵、魔法还是场景等均完全可以照般此布局体系。我们的最终目标是让Silverlight的XAP包中不存放除代码外任何的额外资源,而将所有需要的部件布局于Web中,真正实现对一切对象的动态按需下载:
看过本文后,是否还会有朋友再询问如何对xap进行分包?如何对xap进行压缩?如何动态下载xaml?没有必要了吧。本文构建的游戏架构体系实现了代码与素材的完全分离,它将使得就算是一款Silverlight网游巨作其XAP也难超500K,客户端更新后用户就算重新下载也仅仅是数秒内的事,这才是Silverlight开发Web网游的强势所在 – “动态资源”。
在未来商家必争的手机游戏(手机网游)开发领域,动态资源配置或许将成为刻不容缓需要面对的技术问题,根据场景按需加载资源不仅节约带宽流量且能更快速的进入游戏体现着良好的用户体验。希望本文的解决方案能为大家指引一个正确的方向,识时务者为俊杰,让我们携手努力吧,Silverlight网游的蓝天在等待着您去开创!
本节Demo在线演示地址:http://cangod.com