在Browser Application中使用XNA
在WPF中,我们使用Mode3D等API来绘制三维场景,当期间的“三角形”超过一定数量时,整个场景的渲染速率直线下降,无论显卡的运行速度有多快,帧率都维持在3、5帧每秒。
XNA是微软推出的一套游戏开发API,作为Managed DirectX的进化版,XNA同样封装了DirectX的底层API,此外还提供了一系列和游戏生命周期相关的类,大大减轻了传统Win32下DirectX开发的烦琐。
本文试图让XNA“嵌入”在WPF的Browser Application中,使用XNA来渲染场景,并以XBAP的方式在互联网上发布,集XNA的高效和WPF的部署方便为一生,免除了开发人员在部署、安装、升级应用程序的困扰。
建立应用程序
首先建立一个空白解决方案:
为它添加一个XNA应用程序,一个WPF Webbrowser 应用程序:
在XNA上画一个人物模型:
在Browser Application中使用XNA
为Browser Application添加相关引用:
使用Winform的Panel
修改页面文件,在上面添加一个和一个Winform的Panel:
<Page x:Class="Newinfosoft.Test.Browser.XNAPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:form="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms" Title="XNA Page" Loaded="Page_Loaded"> <Grid> <WindowsFormsHost Margin="0,0,0,30"> <form:Panel x:Name="drawPanel" /> </WindowsFormsHost> </Grid> </Page>
由于我们在Browser Application中使用了Win32的资源,因此,需要修改其安全性,最简答的方式是将安全性设置为完全信任(Full Trust)
接下来,修改XNA应用程序的构造函数,传入一个IntPtr类型(实际上是窗体指针),并把渲染的Handle设置为该地址:
public XNAGame(IntPtr handle) { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; graphics.PreferredBackBufferWidth = 480; graphics.PreferredBackBufferHeight = 320; graphics.PreparingDeviceSettings += (sender, e) => { e.GraphicsDeviceInformation.PresentationParameters.DeviceWindowHandle = handle; }; }
修改Browser Application,在Page加载时运行游戏:
private void Page_Loaded(object sender, RoutedEventArgs e) { XNAGame game = new XNAGame(drawPanel.Handle); game.Run(); }
效果如下:
如果你不想见到那个游戏的原生窗口,可以在XNAGame的构造函数中把原生窗口隐藏起来:
public XNAGame(IntPtr handle) { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; graphics.PreferredBackBufferWidth = 480; graphics.PreferredBackBufferHeight = 320; graphics.PreparingDeviceSettings += (sender, e) => { e.GraphicsDeviceInformation.PresentationParameters.DeviceWindowHandle = handle; }; System.Windows.Forms.Form form = (System.Windows.Forms.Form) System.Windows.Forms.Control.FromHandle((this.Window.Handle)); form.Visible = false; form.VisibleChanged += (sender, e) => { (sender as System.Windows.Forms.Form).Visible = false; }; }
缺点
使用Win32原生窗口,好处在于简单、高效。但缺点是,原生窗口打破了WPF的Z-Order,往往会伏在WPF控件上方,例如,在上面添加一个按钮,设计效果如下:
运行时,发现Panel反而跑到按钮上面去了:
这是由于二者采用不同的渲染模式,具体参见:The WPF Interoperation: “Airspace” and Windows Regions Overview
使用D3DImage
所幸WPF提供了一个D3DImage的ImageSource,负责在WPF中承载d3d,既然XNA底层封装了d3d,那么,是不是WPF也能通过D3DImage承载XNA?答案是肯定的。
修改XNAGame,为它添加两个事件,Init和DrawEnd:
public event EventHandler Init; public event EventHandler DrawEnd;
添加一个RenderTarget2D对象,让XNA把渲染的结果保存在其中,而不是back buffer里。
public RenderTarget2D RenderTarget { get;protected set; }
在Initialize函数中初始化这个RenderTarget2D对象,并使用事件Init通知其他程序。
protected override void Initialize() { // TODO: Add your initialization logic here base.Initialize(); RenderTarget = new RenderTarget2D(graphics.GraphicsDevice, graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height, 1, SurfaceFormat.Color ); GraphicsDevice.SetRenderTarget(0, RenderTarget); if (Init != null) { Init(this, new EventArgs()); } }
在Draw函数的最后,使用事件DrawEnd来通知其他程序,完成一帧的渲染:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); // …?…?…?…? base.Draw(gameTime); if (this.DrawEnd != null) { DrawEnd(this, new EventArgs()); } }
修改Page,使用Image控件代替WindowsFormsHost:
<Page x:Class="Newinfosoft.Test.Browser.XNAPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="XNA Page"> <Grid> <Image x:Name="image" Margin="0,0,0,30" Source="start.png" MouseLeftButtonUp="image_MouseLeftButtonUp" /> <Button Content="Click Me" VerticalAlignment="Bottom" Margin="0,0,0,10" HorizontalAlignment="Center" Padding="10"></Button> </Grid> </Page>
以上代码中,用了一个hack,即首先为该Image控件设定一个ImageSource,同样的,把创建XNAGame的步骤从Page.Load中转移到鼠标点击该Image上,由于不再使用WinForm的Panel,因此,使用一个HwndSource对象作为Game的载体:
private void image_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { image.MouseLeftButtonUp-=new MouseButtonEventHandler(image_MouseLeftButtonUp); HwndSource hwnd = new HwndSource(0, 0, 0, 0, 0, "test", IntPtr.Zero); XNAGame game = new XNAGame(hwnd.Handle); game.Init += new EventHandler(game_Init); game.Run(); }
添加一个D3DImage对象:
D3DImage d3dimage = new D3DImage();
在game_Init中初始化该d3dimage的后台缓存:
void game_Init(object sender, EventArgs e) { XNAGame game = sender as XNAGame; if (d3dimage.IsFrontBufferAvailable) { d3dimage.Lock(); d3dimage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, GetRenderTargetPointer(game.RenderTarget)); d3dimage.Unlock(); image.Source = d3dimage; } image.Source = d3dimage; game.DrawEnd += new EventHandler(game_DrawEnd); }
GetRenderTargetPointer定义如下,利用反射的机制取得RenderTarget2D的内存地址:
public unsafe IntPtr GetRenderTargetPointer(RenderTarget2D renderTarget) { FieldInfo comPtr = renderTarget.GetType().GetField( "pComPtr", BindingFlags.NonPublic | BindingFlags.Instance ); return new IntPtr(Pointer.Unbox(comPtr.GetValue(renderTarget))); }
最后,在game_DrawEnd函数中更新d3dimage:
void game_DrawEnd(object sender, EventArgs e) { if (d3dimage.IsFrontBufferAvailable) { d3dimage.Lock(); d3dimage.AddDirtyRect(new Int32Rect(0, 0, d3dimage.PixelWidth, d3dimage.PixelHeight)); d3dimage.Unlock(); } }