Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

XNA integration inside WPF

0.00/5 (No votes)
20 Jul 2009 1  
Another way to integrate multiple XNA scenes inside WPF.

Introduction

There are many ways to display 3D scenes based on XNA in a WPF environment. Some suffer from problems of speed (with the use of WindowsFormHost), others allow only limited interaction with the WPF controls and interfaces. Implementing multiple displays as seen in programs like Maya becomes problematic:

There is a relatively simple way to do this. This way is simply to give a visual impression to the user of a perfect integration of XNA in a WPF widget when it is not. The key to this lies in the perfect handling of windows.

Theory

We want to integrate our XNA scene in a WPF user interface in the same way that we integrate a canvas or any widget. But the best way to view a 3D scene in XNA is to incorporate it in a window. It is impossible to obtain the handle of any WPF control as can be done with a WinForm. The trick then is to rewrite part of the XNA framework revolving around the Game class. The goal is to inherit a new Game class from Panel (in our case, a Canvas) to be able to include it in the WPF visual tree. The visual bounds of the panel will be the viewing area of the XNA scene. Yet, we have just said that it is not possible to obtain a handle to a visual control that does not inherit from Window. How do we display 3D with XNA then? We will simply display a window without border just above the panel. This window will always be on top when the application has focus and the panel is visible and will be hidden in this case. Similarly, when the panel is not visible, we will halt the activity of the game.

Each change of size or position of the panel will cause a change equivalent to the window above it.

annexe3-2.png

This window is located exactly above the panel and has the same size: the illusion is perfect.

Implementation

The first step is to rewrite part of the classes in the assemblies Microsoft.Xna.Framework and Microsoft.Xna.Framework.Game. The project Arcane.Xna.Presentation has some classes of these assemblies for use with WPF.

annexe3-3.png

Nothing very complicated. Only the Game and GameHost classes are really interesting here.

The Game class, as stated above, is the display of 3D scenes in WPF user interfaces. It inherits from Canvas. Using Canvas meets a particular need that we present below. The Game class is distinguished from the Game class in the Microsoft.Xna.Framework.Game assembly by only a few members. First, it has a member type named GameHost which is the window that is positioned just above. It also has a member _tichGenerator which will update the display at regular intervals.

The constructor initializes its members as follows:

this._window = new GameHost(this);
this._window.Closed += new EventHandler(_window_Closed);
this._tickGenerator = new DispatcherTimer();
this._tickGenerator.Tick += new EventHandler(_tickGenerator_Tick);

It starts by creating the window to be located above it, and registers the Closed event to close the 3D scene. The object DispatcherTimer is used to recreate a loop game by regularly calling the Update and Draw methods. Its velocity depends on the property IsFixedTimeStep.

The last important element, the recording of the event IsVisibleChange:

this.IsVisibleChanged += new 
  DependencyPropertyChangedEventHandler(GameCanvas_IsVisibleChanged);

The activation of DispatcherTimer is based on visibility.

The GameHost class is simple too. It creates a borderless window not visible in the taskbar, and records the SizeChanged event of the panel and the XNA LocationChanged event from the window at the highest level. Both events allow it to always be above the panel XNA by making a call to the method UpdateBounds:

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;
    }
}

This method determines the position of the current window by using the top level window (the window containing the panel of the XNA Game class). It also ensures the same width and height as the panel XNA.

Once again, the window registers on the event IsVisibleChange if it to be visible or not.

First example

We will base our example on the framework AvalonDock (http://www.codeplex.com/AvalonDock), an effective way to create interfaces to be dockable on Visual Studio easily. Also, a way for us to show the power and simplicity of our system in WPF interfaces.

Our solution contains a project called Demo which corresponds to an example of AvanlonDock, nut slightly modified. We added a class inheriting from Game that will display a rotating cube on itself.

This class was simply extracted from a pure XNA application to be added in this project, without any change (or almost):

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);
    }
}

Nothing very complicated here for anyone who knows XNA. We just show here a rotating cube. The first change is to inherit the class RotatingCubeGame from the Game class of our assembly and not the Game class of the Microsoft.Xna.Framework.Game assembly. The second modification is to surround the initializations made in the constructor:

System.ComponentModel.DesignerProperties.GetIsInDesignMode(this);

to ensure that our game will not be created as part of the Visual Studio designer. The rest is very simple. We just replace the contents of each DockablePane in the XAML code in Window1 by:

<Demo:RotatingCubeGame></Demo:RotatingCubeGame>

The result gives us:

Obviously, our system complies with the advantage of allowing AvalonDock docking strong and without disrupting our 3D scene:

annexe3-5.png

Not so bad, but we can do better.

Widgets integration

Why not try to display widgets (Button, Label, Grid, Canvas, ...) in our 3D scene for a perfect integration with WPF?

We might be tempted to add these items directly to the window GameHost. But we would have flicker problem (two different types of displays -3D and vector- on the same clip area is not necessarily good ...). We will simply add a new window above the existing window:

annexe3-6.png

Its content will be directly connected to the content of the panel XNA (the Canvas). The class GameHost will have a new member named _frontWindow (which is a Window). It sets out in the internal property named WPFHost giving access to the content of this window:

internal object WPFHost
{
    get
    {
        return this._frontWindow.Content;
    }
    set
    {
        this._frontWindow.Content = value;
    }
}

The Game class will also expose the content of this window with a property using the same name:

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;
    }
}

This property determines if we are in Design mode (in Visual Studio) or in runtime mode. In Design mode, we use the Canvas class which inherits Game; in runtime mode, we directly target the window. This allows us, in the Visual Studio Designer, to be able to see and modify the UI of our control with the mouse.

In addition, we mark the class attribute of Game:

[System.Windows.Markup. ContentProperty ( "WPFHost" )] 
[System.Windows.Markup. ContentProperty ( "WPFHost")] 

We enable direct content in XAML:

annexe3-7.png

Window1.xaml is modified to add more content to the RotatingCubeGame canvas as shown in the image above. We added pure shapes and paths to reproduce the character orange and yellow, symbolizing XNA, buttons, and labels associated with events, and FlowDocument with scrolling.

Conclusion

The assembly Arcane.Xna.Presentation presents a simple way to integrate professional XNA applications to WPF. The only real flaws that can be found is the code, realized very quickly due to time and the creation of two windows by XNA game classes. The number of displayable windows under Windows is unfortunately limited. The result still works perfectly, and can be used for professional applications:

annexe3-8.png

Screenshot of my editor world

You can download the latest source code here: http://msmvps.com/cfs-file.ashx/__key/CommunityServer.Blogs.Components.WeblogFiles/ valentin.articles.CoursXna.annexe3/7026.XnaInWpf.zip.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here