Silverlight网游可媲美客户端的核心组件之一便是资源动态下载。Silverlight为我们提供了稳健且丰富的WebClient,虽然它本身无法实现并发操作,然而凭借其强大的特性我们可轻松编写出一款资源下载器,满足网游设计中各式各样的资源获取需求。
如何构建这条资源下载通道是游戏设计之初我们应该考虑的问题。你希望它是一条普普通通的马路人来车往随意穿行游走;还是希望它是一条标示清晰、分道合理、次序井然的高速公路?
当然,你会说越快越好,最好永远不堵车,路窄时就一个挨一个慢慢走;路宽了便分道扬镳。
我们不妨将下载通道比作道路,能并排几辆车通过即为“并行模式”;只能一辆一辆的通过则为“串行模式”。很明显,并行的速度要比串行快得多,不过消耗和设计方面则要比串行多且复杂,各有利弊。另外,下载通道如果仅是单纯的串行,则速度太慢且浪费资源;而如果仅是单纯的并行,过多的并发时快时慢、有大有小极易导致UI卡死以及下载堵塞。
再次回到我们的高速公路上,不知你是否有过这样的体会,行驶时常会见到很多大货车以60公里/小时的速度顺次行驶在慢车道上,而小车们则通常会以80-100公里/小时不等的速度在快车道与超车道上彼此间穿行。
人类自身的活动规律来检验理论的合理性非常具有参考价值,由此可见兼具“串行”与“并行”的游戏资源动态下载器才是我们所追求的设计目标。比如游戏中随时可能大量涌现的动画(animation)资源,我们可将它们放在串行队列中依次获取;而角色装备、地图区块等更迫切需要呈现给玩家的资源则可放在并行通道中并发下载。
依据以上分析,思路清晰后仅仅只需百来行代码便可轻松编写出一套完整的资源动态下载组件- SerialDownloader和ParallelDownloader,它们共用一个完成资源表,且串行下载集成了优先机制(DownloadPriority),并行下载也根据需要封装了并行队列模式(QueueParallelDownloader):
/// 下载器基类
/// </summary>
public class DownloadBase {
protected readonly static List < string > loadedUri = new List < string > ();
/// <summary>
/// 获取已下载完成的地址
/// </summary>
public static List < string > LoadedUri { get { return loadedUri; } }
/// <summary>
/// 下载失败(错误)次数
/// </summary>
public static int Error { get ; protected set ; }
}
/// 下载优先级
/// </summary>
public enum DownloadPriority {
/// <summary>
/// 最高
/// </summary>
Highest = 0 ,
/// <summary>
/// 高
/// </summary>
High = 1 ,
/// <summary>
/// 普通
/// </summary>
Normal = 2 ,
/// <summary>
/// 低
/// </summary>
Low = 3 ,
/// <summary>
/// 最低
/// </summary>
Lowest = 4 ,
}
/// <summary>
/// 串行资源下载器
/// </summary>
public class SerialDownloader : DownloadBase {
/// <summary>
/// 资源下载完成
/// </summary>
public static event OpenReadCompletedEventHandler OpenReadCompleted;
/// <summary>
/// 资源下载进度中触发
/// </summary>
public static event DownloadProgressChangedEventHandler DownloadProgressChanged;
/// <summary>
/// 当前进度百分比
/// </summary>
public static int ProgressPercentage { get ; private set ; }
static WebClient webClient = null ;
readonly static List < string > loadingUri = new List < string > ();
readonly static List < string > waitingUri = new List < string > ();
/// <summary>
/// 获取当前正在下载的地址
/// </summary>
public static List < string > LoadingUri { get { return loadingUri; } }
/// <summary>
/// 获取等待下载地址队列
/// </summary>
public static List < string > WaitingUri { get { return waitingUri; } }
/// <summary>
/// 为Image图片控件设置图像源
/// </summary>
/// <param name="image"> 目标图片 </param>
/// <param name="uri"> 图像源地址 </param>
/// <param name="isWaiting"> 是否等待下载完成后再赋值 </param>
public static void SetImageSource(Image image, string uri, DownloadPriority priority, bool isWaiting) {
if (loadedUri.Contains(uri)) {
image.Source = GlobalMethod.GetWebImage(uri);
} else {
image.Source = null ;
AddUri(uri, priority);
if (isWaiting) {
DispatcherTimer timer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds( 2000 ) };
EventHandler handler = null ;
timer.Tick += handler = (s, e) => {
if (loadedUri.Contains(uri)) {
timer.Stop();
timer.Tick -= handler;
image.Source = GlobalMethod.GetWebImage(uri);
}
};
timer.Start();
}
}
}
/// <summary>
/// 添加预备下载地址
/// </summary>
/// <param name="uri"> 图像源地址 </param>
public static void AddUri( string uri, DownloadPriority priority) {
if ( ! waitingUri.Contains(uri)) { waitingUri.Insert(( int )((( int )priority / 4d) * waitingUri.Count), uri); }
if (loadingUri.Count == 0 ) {
webClient = new WebClient();
webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(webClient_DownloadProgressChanged);
webClient.OpenReadCompleted += new OpenReadCompletedEventHandler(webClient_OpenReadCompleted);
webClient.OpenReadAsync( new Uri(GlobalMethod.WebPath(uri), UriKind.Relative), uri);
loadingUri.Add(uri);
}
}
static void webClient_OpenReadCompleted( object sender, OpenReadCompletedEventArgs e) {
WebClient wc = sender as WebClient;
wc.DownloadProgressChanged -= webClient_DownloadProgressChanged;
wc.OpenReadCompleted -= webClient_OpenReadCompleted;
string uri = e.UserState.ToString();
if (e.Error != null ) {
// 断网处理,5秒后重试
Error ++ ;
GlobalMethod.SetTimeout( delegate {
loadingUri.Remove(uri);
AddUri(uri, DownloadPriority.Highest);
}, 5000 );
} else {
loadingUri.Remove(uri);
waitingUri.Remove(uri);
loadedUri.Add(uri);
if (waitingUri.Count > 0 ) { AddUri(waitingUri[ 0 ], DownloadPriority.Highest); }
if (OpenReadCompleted != null ) { OpenReadCompleted(sender, e); }
}
}
static void webClient_DownloadProgressChanged( object sender, DownloadProgressChangedEventArgs e) {
ProgressPercentage = e.ProgressPercentage;
if (DownloadProgressChanged != null ) { DownloadProgressChanged(sender, e); }
}
}
/// 并行资源下载器
/// </summary>
public sealed class ParallelDownloader : DownloadBase {
/// <summary>
/// 资源下载进度中触发
/// </summary>
public event DownloadProgressChangedEventHandler DownloadProgressChanged;
/// <summary>
/// 资源下载完成
/// </summary>
public event OpenReadCompletedEventHandler OpenReadCompleted;
/// <summary>
/// 当前进度百分比
/// </summary>
public static int ProgressPercentage { get ; private set ; }
readonly static List < string > loadingUri = new List < string > ();
readonly static List < string > waitingUri = new List < string > ();
/// <summary>
/// 获取当前正在下载的地址
/// </summary>
public static List < string > LoadingUri { get { return loadingUri; } }
/// <summary>
/// 获取等待下载地址队列
/// </summary>
public static List < string > WaitingUri { get { return waitingUri; } }
/// <summary>
/// 下载资源文件
/// </summary>
/// <param name="uri"> 资源相对地址 <</param>
/// <param name="userToken"> 资源参数 </param>
/// <param name="waitingTime"> 如果正在被下载,等待检测时间(单位:毫秒) </param>
public void OpenReadAsync( string uri, object userToken, bool isWaiting, int waitingTime) {
if (loadedUri.Contains(uri)) {
Download(uri, userToken);
} else {
if (loadingUri.Contains(uri)) {
// 假如该资源正被下载中,则需要等待,每隔1秒检测一次是否已下载完成
if (isWaiting) {
DispatcherTimer timer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(waitingTime) };
EventHandler handler = null ;
timer.Tick += handler = (s, e) => {
if (loadedUri.Contains(uri)) {
timer.Stop();
timer.Tick -= handler;
Download(uri, userToken);
}
};
timer.Start();
}
} else {
if ( ! waitingUri.Contains(uri)) { waitingUri.Add(uri); }
loadingUri.Add(uri);
Download(uri, userToken);
}
}
}
/// <summary>
/// 开始下载
/// </summary>
/// <param name="uri"> 资源相对地址 </param>
/// <param name="userToken"> 资源参数 </param>
void Download( string uri, object userToken) {
OpenReadCompletedEventHandler openReadCompletedHandler = null ;
DownloadProgressChangedEventHandler progressChangedHandler = null ;
WebClient webClient = new WebClient();
webClient.DownloadProgressChanged += progressChangedHandler = (s, e) => {
ProgressPercentage = e.ProgressPercentage;
if (DownloadProgressChanged != null ) { DownloadProgressChanged( this , e); }
};
webClient.OpenReadCompleted += openReadCompletedHandler = (s, e) => {
WebClient wc = s as WebClient;
wc.DownloadProgressChanged -= progressChangedHandler;
wc.OpenReadCompleted -= openReadCompletedHandler;
if (e.Error != null ) {
// 断网处理,5秒后重试
Error ++ ;
GlobalMethod.SetTimeout( delegate {
Download(uri, userToken);
}, 5000 );
} else {
waitingUri.Remove(uri);
loadingUri.Remove(uri);
if ( ! loadedUri.Contains(uri)) { loadedUri.Add(uri); }
if (OpenReadCompleted != null ) { OpenReadCompleted( this , e); }
}
};
webClient.OpenReadAsync( new Uri(uri, UriKind.Relative), userToken);
}
}
经过测试,它们的运行非常稳健,当然这也少不了WebClient的功劳,尤其体现在其中的抗网速阻塞、抗断网处理机制(5秒重试)。大家不妨在游戏资源下载过程中打开迅雷并全速全线程下载上传一堆东西,或者直接将网卡禁用,此时观察游戏中资源下载状况;然后关闭迅雷并启用网卡再观察所有的一切下载均立刻恢复正常。同时也证明了,规划合理的串行和并行如同设计一条完美的高速公路,它是成就高性能Silverlight网游引擎所不可或缺的功能模块之一。
没错,Silverlight在网游开发中拥有如此优越的特性,不久的将来大家将看到更多的网游奇迹因Silverlight而诞生,一起见证。
本节源码请到目录中下载
在线演示地址:http://cangod.com