有段时间没有搞webgame了,近期看到园子里有些silverlight方面文章,忍不住手闲也写几篇文章。作为silverlight webgame的开篇文章,先给大家普及一下主程序加载器的概念。
什么是加载器/为什么需要加载器
我们想一想,一个webgame做下来大概要多大?即便图片资源和其他一些诸如脚本等数据包是动态下载的,我所经历过的webgame最基本的少说也要500KB。对于国人来讲,500KB对于一个游戏已经算是很小了,但是对于webgame而言用户每次都要通过浏览器下载它,当然你可以通过类似“隔离存储”的东西来减少不必要的下载,但对于网速一般的童鞋们依然需要一个糟糕的等待体验(当玩家们看到一堆篮球球在页面中间绕圈,他们会认为这是什么呢?)。所以,通常的做法是:1.先让用户下载一个简单的silverlight下载器,这个下载器包含友好的界面以及一个下载模块,下载模块会去下载所有sl运行的必要文件,在这个过程中像用户提供友好、有趣的UI来减少等待下载的尴尬。2.尽量减少xap的尺寸。这一方面要从很多处做起,养成好习惯。比如需要在开发过程中尽量不要使用额外的程序集(比如解析xml不要用linq,不要使用庞大的sl toolkit,最好一切自己去做自定义控件等);对于一些资源比如图片要打包好,在运行起来时动态下载;合理使用浏览器缓存和隔离存储等等。
好了,我们来看看市面上的webgame加载器都是怎么个效果。
菜友们熟悉的qq农场加载界面:
这是
MT大冒险的加载界面:(如果您没听说过请不要惊讶。。这个是我们以前用sl做的)
可以看到,其实加载器并没有什么复杂的功能,无非就是只要能把所需的文件下载下来,在UI上给用户一个直观、漂亮的界面就ok(个人认为一个指示进度的UI是非常有用的,当然进度的显示数值该作弊的时候也要作弊~~)。
silverlight中assembly的加载方式及api
.net有两个很重要的概念,一个是assembly(程序集),一个是appdomain(应用程序域),对于silverlight还有一个概念是domain(域)需要注意。assembly(程序集)是一个或多个托管模块,以及一些资源文件的逻辑组合,其实我们可以在其中存放任意格式的资源;appdoman就涉及到.net应用程序体系结构了,appdomain用的最多的还是插件机制的实现,具体您可以参考MSDN。而对于sl中的domain,相信做过sl项目的人都应该了解,假设我们需要sl能够跨域访问服务器,如果是web server的话最省事,只需要放置一个策略文件,如果不是web server的话也没关系,在943端口上监听对策略文件的请求即可并将xml数据传过去即可。还比如sl运行时处于安全考虑,不允许来自两个域的sl程序之间直接进行通信,我们需要通过LocalCommunication来解决sl的跨域通信问题。
好了,上面算是做了个知识普及,其实 silvelright中加载程序集是非常之easy的。我们的程序集当然是从网络读出来的(当然也可能是从IsolatedStorage中读出来),当得到流之后只需调用 Assembly AssemblyPart.Load(Stream stream)即可获得Assembly对象。将所有的程序集载入后就可以通过反射启动我们真正的“程序”了。诸如:
var page = asm.CreateInstance("DemoApp.MainPage") as UserControl;
page.Show()
设计你自己的加载器
我们的加载器要有两个过程,一是把需要的程序集文件下载过来以Assembly对象的形式保存,二是按需加载,通过反射得到我们需要的资源。加载器对外提供必要的接口便于UI使用。
AssemblyLoadManifest:这个类其实存放是需要下载的目标程序集的uri(至于这些uri是什么,你可以将他们放在xml里自己去解析出来)。
AssemblyLoadManifestnamespace Sopaco.Silverlight.Loader
{
/// <summary>
/// 需要加载到应用程序域中的清单
/// the manifest needed to load int the current appdomain
/// </summary>
public class AssemblyLoadManifest
{
#region Fields
/// <summary>
/// Uri列表
/// </summary>
private IList < Uri > dllUris = new List < Uri > ();
/// <summary>
/// 存储对应的程序集
/// </summary>
private Dictionary < Uri, Assembly > _asms = new Dictionary < Uri,Assembly > ();
#endregion
#region Properties
/// <summary>
/// Uri
/// </summary>
public Queue < Uri > Uris
{
get
{
return new Queue < Uri > (dllUris.ToList());
}
}
#endregion
#region Exposed Public Methods
/// <summary>
/// 增加一个目标程序集的Uri
/// </summary>
public void AddUri(Uri uri)
{
if ( ! dllUris.Contains(uri))
{
dllUris.Add(uri);
}
}
/// <summary>
/// 获得Uri对应的程序集,没有则返回null
/// </summary>
public Assembly GetAssembly(Uri key)
{
if ( ! _asms.ContainsKey(key))
return null ;
return _asms[key];
}
/// <summary>
/// 设置Uri对应的程序集,访问限制:internal
/// </summary>
internal void SetAssembly(Uri key, Assembly asm)
{
_asms[key] = asm;
}
#endregion
}
}
得到下载清单我们就开始下载。webcilent就足够了。这里注意因为UI上需要显示进度,所以有必要增加几个用于通知下载状态、进度的事件。
LoadAssemblyTasknamespace Sopaco.Silverlight.Loader
{
/// <summary>
///
/// </summary>
public class LoadAssemblyTask
{
#region Fields
private bool isWorking = false ;
private int oriLength;
private WebClient _webClient;
private Queue < Uri > _uris;
private AssemblyLoadManifest _manifest;
#endregion
#region ctors
public LoadAssemblyTask()
{
}
#endregion
#region progress status events
public event Action < Uri, int > DownloadPartChanged;
public event Action < Exception > DownloadError;
public event Action DownloadCompleted;
public event Action LoadCompleted;
#endregion
public void BeginTask(AssemblyLoadManifest manifest)
{
if ( ! isWorking)
{
_manifest = manifest;
oriLength = 0 ;
_uris = manifest.Uris;
if (_uris.Count == 0 )
return ;
_webClient = new WebClient();
_webClient.OpenReadCompleted += new OpenReadCompletedEventHandler(client_OpenReadCompleted);
_webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(_webClient_DownloadProgressChanged);
var nextUri = _uris.Peek();
DownloadPartChanged.ExecuteSecurity(nextUri, oriLength);
_webClient.OpenReadAsync(nextUri);
isWorking = true ;
}
else
{
EndTask();
if ( ! isWorking)
BeginTask(manifest);
}
}
public event DownloadProgressChangedEventHandler OnPerDownloadProgressChanged;
public void EndTask()
{
if (_webClient != null )
{
_webClient.CancelAsync();
_webClient = null ;
_uris.Clear();
_uris = null ;
oriLength = 0 ;
_manifest = null ;
}
isWorking = false ;
}
private void client_OpenReadCompleted( object sender, OpenReadCompletedEventArgs e)
{
var webClient = sender as WebClient;
if (e.Error != null || e.Result == null )
{
DownloadError.ExecuteSecurity(e.Error);
webClient.OpenReadCompleted -= client_OpenReadCompleted;
return ;
}
oriLength += 1 ;
var part = new AssemblyPart();
var asm = part.Load(e.Result);
_manifest.SetAssembly(_uris.Peek(), asm);
_uris.Dequeue();
if (_uris.Count == 0 )
{
DownloadPartChanged.ExecuteSecurity( null , oriLength);
DownloadCompleted.ExecuteSecurity();
LoadCompleted.ExecuteSecurity();
webClient.OpenReadCompleted -= client_OpenReadCompleted;
return ;
}
var nextUri = _uris.Peek();
DownloadPartChanged.ExecuteSecurity(nextUri, oriLength);
webClient.OpenReadAsync(nextUri);
}
private void _webClient_DownloadProgressChanged( object sender, DownloadProgressChangedEventArgs args)
{
if (OnPerDownloadProgressChanged != null )
OnPerDownloadProgressChanged(sender, args);
}
}
}
最后要测试一下
loaderTestprivate void loaderTest()
{
#region 构建一个清单,填充目标程序集对应的Uri
var manifest = new AssemblyLoadManifest();
manifest.AddUri( new Uri( " DemoTester.dll " , UriKind.Relative));
manifest.AddUri( new Uri( " Sopaco.Silverlight.Insfrastructure.dll " , UriKind.Relative));
manifest.AddUri( new Uri( " Sopaco.Silverlight.Loader.dll " , UriKind.Relative));
manifest.AddUri( new Uri( " a.dll " , UriKind.Relative));
#endregion
#region 注册相关事件(每个下载项的进度事件,下载项变更通知,程序集下载完成事件,程序集加载到域中事件)
var task = new LoadAssemblyTask();
task.OnPerDownloadProgressChanged += (o, e) =>
{
reporter.Text = string .Format( " {0}/{1}:{2} " , e.BytesReceived, e.TotalBytesToReceive, e.ProgressPercentage);
System.Threading.Thread.Sleep( 100 );
};
task.DownloadPartChanged += (uri, i) =>
{
Storyboard sb = new Storyboard();
DoubleAnimation ani = new DoubleAnimation();
ani.From = ( double .IsNaN(progressBar.Width)) ? 0 : progressBar.Width;
ani.To = outterContainer.Width * i / manifest.Uris.Count;
ani.Duration = new Duration(TimeSpan.FromSeconds( 2 ));
Storyboard.SetTarget(ani, progressBar);
Storyboard.SetTargetProperty(ani, new PropertyPath( " Width " ));
sb.Children.Add(ani);
sb.Begin();
// progressBar.Width = progressBarOutter.ActualWidth * i / manifest.Uris.Count;
};
// task.DownloadCompleted += () => MessageBox.Show("download all completed");
task.LoadCompleted += () =>
{
// MessageBox.Show("load all completed");
System.Reflection.Assembly asm = manifest.GetAssembly( new Uri( " a.dll " , UriKind.Relative));
var page = asm.CreateInstance( " DemoApp.MainPage " ) as UserControl;
};
task.DownloadError += (ex) => MessageBox.Show( " occurs error " );
// service.Load();
#endregion
#region 开始下载
task.BeginTask(manifest);
#endregion
#region 解除事件
// 略
#endregion
}
后续
因为对于webgame的主程序加载一般没有特别的需求,所以本篇所写的这个加载器没有涉及一些复杂的功能。有时间会将写一个这个系列的文章,其中会包括一个完整的资源加载管理模块的设计,包括资源的下载、缓存管理、版本判断、动态加载机制等内容,尽请期待。