在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的部署方便为一生,免除了开发人员在部署、安装、升级应用程序的困扰。

建立应用程序

首先建立一个空白解决方案:

image

为它添加一个XNA应用程序,一个WPF Webbrowser 应用程序:

image

在XNA上画一个人物模型:

image

在Browser Application中使用XNA

为Browser Application添加相关引用:

image

使用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)

image

接下来,修改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();
}

效果如下:

image

如果你不想见到那个游戏的原生窗口,可以在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控件上方,例如,在上面添加一个按钮,设计效果如下:

image

运行时,发现Panel反而跑到按钮上面去了:

image

这是由于二者采用不同的渲染模式,具体参见: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();
    }
}

运行结果,我们发现游戏画面不再阻挡Button了:

image

posted on 2011-05-06 17:03  carekee  阅读(669)  评论(0)    收藏  举报