WPF 中的 XNA 集成






4.96/5 (20投票s)
在 WPF 中集成多个 XNA 场景的另一种方法。
引言
在 WPF 环境中显示基于 XNA 的 3D 场景有多种方法。一些方法存在速度问题(使用 WindowsFormHost
),另一些则只允许与 WPF 控件和界面进行有限的交互。实现像 Maya 这样的程序中的多显示屏会变得很麻烦。
有一种相对简单的方法可以做到这一点。这种方法就是通过不真正地集成 XNA 来给用户一个完美的 XNA 集成在 WPF 小部件中的视觉印象。关键在于完美地处理窗口。
理论
我们希望像集成画布或任何小部件一样,将 XNA 场景集成到 WPF 用户界面中。但查看 XNA 中 3D 场景的最佳方式是将其嵌入到一个窗口中。与 WinForm 不同,我们无法获取任何 WPF 控件的句柄。诀窍就是重写 XNA 框架中围绕 Game
类的一部分。目标是让新的 Game
类继承自 Panel
(在本例中是 Canvas
),以便将其包含在 WPF 的视觉树中。面板的视觉边界将是 XNA 场景的视口。然而,我们刚刚说过,无法获取不继承自 Window
的视觉控件的句柄。那么我们如何用 XNA 显示 3D 呢?我们将简单地显示一个无边框的窗口,正好位于面板上方。当应用程序获得焦点且面板可见时,此窗口将始终保持在最前面,否则将隐藏。同样,当面板不可见时,我们将暂停游戏的活动。
面板的每一次大小或位置的变化都会导致其上方的窗口发生等效的变化。
这个窗口位于面板的正上方,并且大小相同:这种错觉是完美的。
实现
第一步是重写程序集 Microsoft.Xna.Framework 和 Microsoft.Xna.Framework.Game 中的部分类。Arcane.Xna.Presentation 项目包含这些程序集中的一些类,用于与 WPF 一起使用。
没有什么太复杂的。只有 Game
和 GameHost
类在这里真正有趣。
如上所述,Game
类是在 WPF 用户界面中显示 3D 场景的。它继承自 Canvas
。使用 Canvas
满足了我们下面将介绍的一个特定需求。Game
类仅通过少数几个成员就与 Microsoft.Xna.Framework.Game 程序集中的 Game
类有所不同。首先,它有一个名为 GameHost
的成员,该成员就是位于其正上方的窗口。它还有一个名为 _tichGenerator
的成员,该成员将定期更新显示。
构造函数按如下方式初始化其成员。
this._window = new GameHost(this);
this._window.Closed += new EventHandler(_window_Closed);
this._tickGenerator = new DispatcherTimer();
this._tickGenerator.Tick += new EventHandler(_tickGenerator_Tick);
它首先创建要位于其上方的窗口,并注册 Closed
事件以关闭 3D 场景。DispatcherTimer
对象用于通过定期调用 Update
和 Draw
方法来重新创建游戏循环。其速度取决于 IsFixedTimeStep
属性。
最后一个重要元素,事件 IsVisibleChange
的注册。
this.IsVisibleChanged += new
DependencyPropertyChangedEventHandler(GameCanvas_IsVisibleChanged);
DispatcherTimer
的激活基于可见性。
GameHost
类也很简单。它创建一个无边框的窗口,不显示在任务栏中,并注册面板的 SizeChanged
事件以及顶层窗口的 XNA LocationChanged
事件。这两个事件都允许它通过调用 UpdateBounds
方法始终位于 XNA 面板的上方。
public void UpdateBounds()
{
if (this.IsVisible)
{
GeneralTransform gt = this.game.TransformToVisual(this.TopLevelWindow);
this.Width = this.game.ActualWidth;
this.Height = this.game.ActualHeight;
this.Left = this.TopLevelWindow.Left + gt.Transform(new Point(0, 0)).X;
this.Top = this.TopLevelWindow.Top + gt.Transform(new Point(0, 0)).Y;
}
}
此方法通过使用顶层窗口(包含 XNA Game
类面板的窗口)来确定当前窗口的位置。它还确保与 XNA 面板具有相同的宽度和高度。
再次,窗口注册 IsVisibleChange
事件,以确定其是否可见。
第一个示例
我们将基于 AvalonDock 框架 (http://www.codeplex.com/AvalonDock),这是一种创建可轻松停靠在 Visual Studio 中的界面的有效方法。此外,这也是我们展示系统在 WPF 界面中的强大功能和简单性的一个方法。
我们的解决方案包含一个名为 Demo 的项目,该项目对应于 AvanlonDock 的一个示例,但略有修改。我们添加了一个继承自 Game
的类,该类将显示一个在其自身上旋转的立方体。
这个类只是从纯 XNA 应用程序中提取出来的,以便添加到这个项目中,几乎没有进行任何更改。
public class RotatingCubeGame : Arcane.Xna.Presentation.Game
{
#region Fields
Arcane.Xna.Presentation.GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
BasicEffect effect;
VertexPositionColor[] vertices;
Vector3 position = Vector3.Zero;
Vector3 size = Vector3.One;
VertexBuffer vertexBuffer;
IndexBuffer indexBuffer;
#endregion
#region Constructors
public RotatingCubeGame()
{
if (!(System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)))
{
graphics = new Arcane.Xna.Presentation.GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
}
#endregion
/// <summary>
/// Allows the game to perform any initialization it needs to before starting to run.
/// This is where it can query for any required services and load any non-graphic
/// related content. Calling base.Initialize will enumerate through any components
/// and initialize them as well.
/// </summary>
protected override void Initialize()
{
base.Initialize();
// TODO: Add your initialization logic here
this.graphics.IsFullScreen = false;
this.graphics.PreferredBackBufferWidth = 800;
this.graphics.PreferredBackBufferHeight = 600;
this.graphics.ApplyChanges();
this.Window.Title = "";
this.InitializeVertices();
this.InitializeIndices();
}
private void InitializeVertices()
{
vertices = new VertexPositionColor[8];
vertices[0].Position = new Vector3(-10f, -10f, 10f);
vertices[0].Color = Color.Yellow;
vertices[1].Position = new Vector3(-10f, 10f, 10f);
vertices[1].Color = Color.Green;
vertices[2].Position = new Vector3(10f, 10f, 10f);
vertices[2].Color = Color.Blue;
vertices[3].Position = new Vector3(10f, -10f, 10f);
vertices[3].Color = Color.Black;
vertices[4].Position = new Vector3(10f, 10f, -10f);
vertices[4].Color = Color.Red;
vertices[5].Position = new Vector3(10f, -10f, -10f);
vertices[5].Color = Color.Violet;
vertices[6].Position = new Vector3(-10f, -10f, -10f);
vertices[6].Color = Color.Orange;
vertices[7].Position = new Vector3(-10f, 10f, -10f);
vertices[7].Color = Color.Gray;
this.vertexBuffer = new VertexBuffer(this.graphics.GraphicsDevice,
typeof(VertexPositionColor), 8, BufferUsage.WriteOnly);
this.vertexBuffer.SetData(vertices);
}
private void InitializeIndices()
{
short[] indices = new short[36]{
0,1,2, //face devant
0,2,3,
3,2,4, //face droite
3,4,5,
5,4,7, //face arrière
5,7,6,
6,7,1, //face gauche
6,1,0,
6,0,3, //face bas
6,3,5,
1,7,4, //face haut
1,4,2};
this.indexBuffer = new IndexBuffer(this.graphics.GraphicsDevice,
typeof(short), 36, BufferUsage.WriteOnly);
this.indexBuffer.SetData(indices);
}
/// <summary>
/// LoadContent will be called once per game and is the place to load
/// all of your content.
/// </summary>
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
this.effect = new BasicEffect(graphics.GraphicsDevice, null);
this.effect.View = (Matrix.CreateLookAt(new Vector3(20, 30, -50),
Vector3.Zero, Vector3.Up));
this.effect.Projection = (Matrix.CreatePerspectiveFieldOfView(
MathHelper.PiOver4, this.GraphicsDevice.Viewport.AspectRatio, 0.1f, 100f));
// this.effect.EnableDefaultLighting();
// this.effect.LightingEnabled = true;
this.effect.VertexColorEnabled = true;
}
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot
/// of timing values.</param>
protected override void Update(GameTime gameTime)
{
if (Keyboard.GetState()[Keys.Up] == KeyState.Down)
position += Vector3.Up;
if (Keyboard.GetState()[Keys.Down] == KeyState.Down)
position += Vector3.Down;
if (Keyboard.GetState()[Keys.Left] == KeyState.Down)
position += Vector3.Left;
if (Keyboard.GetState()[Keys.Right] == KeyState.Down)
position += Vector3.Right;
if (Keyboard.GetState()[Keys.PageUp] == KeyState.Down)
size += new Vector3(0.1f, 0.1f, 0.1f);
if (Keyboard.GetState()[Keys.PageDown] == KeyState.Down)
size -= new Vector3(0.1f, 0.1f, 0.1f);
// Allows the default game to exit on Xbox 360 and Windows
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
float fAngle = (float)gameTime.TotalGameTime.TotalSeconds;
//la transformation en elle même
Matrix world = Matrix.CreateRotationY(fAngle) * Matrix.CreateRotationX(fAngle)
* Matrix.CreateScale(size)
* Matrix.CreateTranslation(position);
this.effect.World = (world);
base.Update(gameTime);
}
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime)
{
this.graphics.GraphicsDevice.Vertices[0].SetSource(this.vertexBuffer, 0,
VertexPositionColor.SizeInBytes);
this.graphics.GraphicsDevice.Indices = this.indexBuffer;
this.graphics.GraphicsDevice.VertexDeclaration =
new VertexDeclaration(this.graphics.GraphicsDevice,
VertexPositionColor.VertexElements);
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
this.effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
this.graphics.GraphicsDevice.DrawIndexedPrimitives(
PrimitiveType.TriangleList, 0, 0, 8, 0, 12);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
对于熟悉 XNA 的人来说,这里没有什么太复杂的。我们只是在这里展示一个旋转的立方体。第一个变化是将 RotatingCubeGame
类继承自我们程序集的 Game
类,而不是 Microsoft.Xna.Framework.Game 程序集的 Game
类。第二个修改是将构造函数中进行的初始化包装起来
System.ComponentModel.DesignerProperties.GetIsInDesignMode(this);
以确保我们的游戏不会作为 Visual Studio 设计器的一部分被创建。其余的很简单。我们只是将 Window1
中的 XAML 代码中每个 DockablePane
的内容替换为
<Demo:RotatingCubeGame></Demo:RotatingCubeGame>
结果如下:
显然,我们的系统具有允许 AvalonDock 强大地停靠并且不干扰我们 3D 场景的优势。
不算太糟,但我们可以做得更好。
小部件集成
为什么不尝试在我们的 3D 场景中显示小部件(Button
、Label
、Grid
、Canvas
等)以实现与 WPF 的完美集成?
我们可能会倾向于将这些项直接添加到 GameHost
窗口中。但我们会遇到闪烁问题(两个不同类型的显示 - 3D 和矢量 - 在同一个裁剪区域上并不一定好……)。我们将简单地添加一个新窗口来覆盖现有窗口。
它的内容将直接连接到 XNA 面板(Canvas
)的内容。GameHost
类将有一个名为 _frontWindow
的新成员(它是一个 Window
)。它在名为 WPFHost
的内部属性中进行设置,该属性允许访问此窗口的内容。
internal object WPFHost
{
get
{
return this._frontWindow.Content;
}
set
{
this._frontWindow.Content = value;
}
}
Game
类还将通过同名属性公开此窗口的内容。
public object WPFHost
{
get
{
if (!(System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)))
return this.Window.WPFHost;
else
return (base.Children[0] as ContentControl).Content;
}
set
{
if (!(System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)))
this.Window.WPFHost = value;
else
(base.Children[0] as ContentControl).Content = value;
}
}
此属性确定我们是在设计模式(在 Visual Studio 中)还是在运行时模式。在设计模式下,我们使用继承自 Game
的 Canvas
类;在运行时模式下,我们直接定位到窗口。这使得我们在 Visual Studio 设计器中能够通过鼠标查看和修改我们控件的 UI。
此外,我们标记了 Game
类的类属性。
[System.Windows.Markup. ContentProperty ( "WPFHost" )]
[System.Windows.Markup. ContentProperty ( "WPFHost")]
我们在 XAML 中启用了直接内容。
Window1.xaml 被修改以向 RotatingCubeGame
画布添加更多内容,如上图所示。我们添加了纯形状和路径来重现象征 XNA 的橙色和黄色字符、按钮和与事件关联的标签,以及带滚动条的 FlowDocument
。
结论
Arcane.Xna.Presentation 程序集提供了一种将专业 XNA 应用程序集成到 WPF 的简单方法。唯一可以找到的真正缺陷是代码,由于时间和创建两个窗口而每个 XNA 游戏类都很快完成。Windows 下可显示的窗口数量非常有限。结果仍然完美工作,并且可以用于专业应用程序。
您可以在此处下载最新的源代码:http://msmvps.com/cfs-file.ashx/__key/CommunityServer.Blogs.Components.WeblogFiles/ valentin.articles.CoursXna.annexe3/7026.XnaInWpf.zip。