ProfilProfil
 Registrieren
 Login
Bild der WocheBild der Woche

(von Backslider)
Kommentare (0)
****

Weitere
User onlineBenutzer online
Gäste online: 6
Mitglieder online: Keine
Registrierte Mitglieder: 2116
Neustes Mitglied: onkel_keks

XNA und WinForms

Autor: SteveKr

Inhaltsverzeichnis

  1. Einleitung
  2. Die Klassen XnaForms und DeviceManager
  3. Erster Einsatz der XnaForms-Klasse
  4. GameLoops - Rendern
  5. GameLoops - Die gute alte Zeit

^ Einleitung


Manchmal, zum Beispiel für einen Editor, benötigt man XNA nicht in seiner gewohnten Umgebung, sondern im Zusammenspiel mit WinForms. Da das XNA Framework von Hause aus keine Klassen mitbringt, die eine unkomplizierte Verwendung ermöglichen würden, soll im Folgenden eine Klasse erstellt werden, die den Umgang mit XNA unter WinForms erleichtern soll.
Ausgangsbasis ist ein neu erstelltes "Windows-Anwendung"-Projekt.

XNA Framework einbinden


Um das XNA-Framework nutzen zu können, werden die folgenden Assemblys benötigt:
  • Microsoft.Xna.Framework
  • Microsoft.Xna.Framework.Game

Anschließend erstellen wir auf der Form ein Panel und geben ihm den Namen pnlOutput. In dieses Panel werden wir später mittels XNA rendern.

^ Die Klassen XnaForms und DeviceManager


Die Klasse XnaForms wird für eine einfache Handhabung von XNA in WinForms-Projekten sorgen. Wir spendieren ihr zunächst einen Konstruktor, der Handle, Breite sowie Höhe des gewünschten Ausgabebereichs erhält.
public class XnaForms
{
    public XnaForms(IntPtr handle, int width, int height)
    {
    }
   
}

Danach wechseln wir in die Form-Klasse. Hier deklarieren wir ein XnaForms-Objekt und übergeben dem Konstruktor die Werte des Panels.
public partial class Form1 : Form
{
    private XnaForms xna;

    public Form1()
    {
        InitializeComponent();

        xna = new XnaForms(pnlOutput.Handle, pnlOutput.Width, pnlOutput.Height);
    }
}

Da wir später auch einen ContentManager verwenden möchten, benötigen wir einen GraphicsDeviceService. Der GraphicsDeviceManager des XNA-Frameworks wäre ein möglicher Kandidat, allerdings verlangt dessen Konstruktor ein Game-Objekt. Wir verwenden allerdings kein solches Game-Objekt, weshalb wir unseren eigenen Manager erstellen müssen.
Wir nenne unsere Manager-Klasse DeviceManager. Diese leiten wir vom IGraphicsDeviceService-Interface, welches der ContentManager später verlangen wird.
public class DeviceManager : IGraphicsDeviceService
{
    private GraphicsDevice graphicsDevice;

    public event EventHandler DeviceCreated;
    public event EventHandler DeviceDisposing;
    public event EventHandler DeviceReset;
    public event EventHandler DeviceResetting;
    public GraphicsDevice GraphicsDevice
    {
        get { return graphicsDevice; }
    }

    public DeviceManager(IntPtr handle, int backBufferWidth, int backBufferHeight)
    {
        PresentationParameters pp = new PresentationParameters();
        pp.IsFullScreen = false;
        pp.SwapEffect = SwapEffect.Discard;
        pp.BackBufferWidth = backBufferWidth;
        pp.BackBufferHeight = backBufferHeight;

        graphicsDevice = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware, handle, pp);
        graphicsDevice.Disposing += new EventHandler(graphicsDevice_Disposing);
        graphicsDevice.DeviceReset += new EventHandler(graphicsDevice_DeviceReset);
        graphicsDevice.DeviceResetting += new EventHandler(graphicsDevice_DeviceResetting);

        OnDeviceCreated(this, EventArgs.Empty);
    }

    private void OnDeviceCreated(object sender, EventArgs e)
    {
        if (DeviceCreated != null)
        {
            DeviceCreated(sender, e);
        }
    }

    private void graphicsDevice_Disposing(object sender, EventArgs e)
    {
        if (DeviceDisposing != null)
        {
            DeviceDisposing(sender, e);
        }
    }

    private void graphicsDevice_DeviceReset(object sender, EventArgs e)
    {
        if (DeviceReset != null)
        {
            DeviceReset(sender, e);
        }
    }

    private void graphicsDevice_DeviceResetting(object sender, EventArgs e)
    {
        if (DeviceResetting != null)
        {
            DeviceResetting(sender, e);
        }
    }
}

Im Konstruktor erstellen wir anhand der übergebenen Parameter einen neuen GraphicsDevice. Auf Grund der Implementierung des IGraphicsDeviceService-Interfaces haben zudem einige Events in die Klasse Einzug gehalten, die wir allerdings in diesem Tutorial nicht benötigen.

Nun, da der DeviceManager fertig gestellt wurde, wechseln wir zur XnaForms-Klasse, um ihn dort zu integrieren.
public class XnaForms
{
    private DeviceManager deviceManager;

    public GraphicsDevice GraphicsDevice
    {
        get { return deviceManager.GraphicsDevice; }
    }

    public XnaForms(IntPtr handle, int width, int height)
    {
        deviceManager = new DeviceManager(handle, width, height);
    }
}

Als Letztes spendieren wir der Klasse noch einen ContentManager. Dessen Konstruktor benötigt ein GameServiceContainer-Objekt, das wir aus diesem Grund ebenfalls in unsere XnaForms-Klasse einbauen müssen. Darüber hinaus benötigt der ContentManager einen GraphicsDeviceService, der sich in Form unseres DeviceManagers wiederfindet.
public class XnaForms
{
    private DeviceManager deviceManager;
    private ContentManager contentManager;
    private GameServiceContainer services;

    public GraphicsDevice GraphicsDevice
    {
        get { return deviceManager.GraphicsDevice; }
    }
    public ContentManager Content
    {
        get { return contentManager; }
    }
    public GameServiceContainer Services
    {
        get { return services; }
    }

    public XnaForms(IntPtr handle, int width, int height)
    {
        deviceManager = new DeviceManager(handle, width, height);

        services = new GameServiceContainer();
        services.AddService(typeof(IGraphicsDeviceService), deviceManager);

        contentManager = new ContentManager(services);
    }
}


^ Erster Einsatz der XnaForms-Klasse


Die XnaForms-Klasse ist nun bereit für ihren ersten Einsatz.
Wir wechseln also wieder zum Code der Form-Klasse und deklarieren zwei weitere Objekte . Zum Einen ein SpriteBatch-Objekt und zum Anderen ein Texture2D-Objekt. Außerdem fügen wir unserem Projekt einen "Content"-Ordner hinzu und fügen in diesen eine bereits durch die Content-Pipeline geschickte XNB-Datei der gewünschten 2D-Grafik. In den Dateieigenschaften legen wir zudem fest, das sie ins Ausgabeverzeichnis kopiert wird.
Anschließend laden wir diese Datei mit unserem ContentManager und instanziieren das SpriteBatch-Objekt.
public partial class Form1 : Form
{
    private XnaForms xna;
    private SpriteBatch spriteBatch;
    private Texture2D texture;

    public Form1()
    {
        InitializeComponent();

        xna = new XnaForms(pnlOutput.Handle, pnlOutput.Width, pnlOutput.Height);
        xna.Content.RootDirectory = "Content";

        spriteBatch = new SpriteBatch(xna.GraphicsDevice);
        texture = xna.Content.Load<Texture2D>("frog");
    }
}

Anschließend fügen wir der Form einen Button hinzu, bei dessen Anklicken, der Inhalt des Panels gerendert werden soll. Das Zeichnen erfolgt, wie man es normal von XNA erwartet ist. Zu beachten ist lediglich, dass man am Ende aller Zeichenoperationen die Present-Methode des GraphicsDevice aufruft, durch die der Backbuffer angezeigt wird.
private void button1_Click(object sender, EventArgs e)
{
    RenderFrame();
}

public void RenderFrame()
{
    xna.GraphicsDevice.Clear(XnaColor.CornflowerBlue);

    spriteBatch.Begin();

    spriteBatch.Draw(texture, Vector2.Zero, XnaColor.White);

    spriteBatch.End();

    xna.GraphicsDevice.Present();
}

Wenn alles funktioniert, sollte ungefähr folgendes zu sehen sein:


Bild 1: CornflowerBlue und WinForms vereint



Auf Größenänderungen reagieren


Die XnaForms-Klasse hat ihre Aufgabe mit Bravour geleistet. Eine kleine Erweiterung werden wir ihr nun spendieren. Wenn der User die Größe des Panels zur Laufzeit ändert und neu zeichnen lässt, dann wird die Ausgabe verzerrt, da der Backbuffer des GraphicsDevice weiterhin die ursprüngliche Größe besitzt. Um diesen Zustand zu ändern, wird die XnaForms-Klasse eine Methode Resize erhalten, die die Größe des Backbuffers anpasst.
public void Resize(int width, int height)
{
    if (width > 0 && height > 0)
    {
        PresentationParameters pp = GraphicsDevice.PresentationParameters;
        pp.BackBufferWidth = width;
        pp.BackBufferHeight = height;

        GraphicsDevice.Reset(pp);
    }
}


Aufrufen können wir die Methode beim Resize-Event des Panels.
private void pnlOutput_Resize(object sender, EventArgs e)
{
    xna.Resize(pnlOutput.Width, pnlOutput.Height);
    RenderFrame();
}


^ GameLoops - Rendern


Bisher hatten wir es nur mit einem statischen Bild zu tun. Um dieses zu Rendern hatten wir bisher einen extra Button, wir hätten aber genauso gut auch bei erhalten des Paint-Events zeichnen können.
Wenn wir es allerdings mit animiertem oder sich bewegendem Inhalt zu tun haben, müssen wir kontinuierlich neu zeichnen. Dazu werden nun ingesamt 3 Varianten vorgestellt.

Timer


Der erste Ansatz ist, einen Timer zu verwenden. Dazu erstellen wir einen solchen auf unserer Form und rufen bei erhalten des Tick-Events die Render-Methode auf. Wenn wir den Timer starten und das Programm ausführen wird der Inhalt des Panels alle 100 Millisekunden neu gezeichnet, was für die meisten Anwendungen genügen sollte.

Gameloop - Variante 1


Eine andere Variante wäre, kontinuierlich in einer Schleife zu rendern, wie man es auch normal von Spielen gewohnt ist. Um das zu erreichen wechseln wir in die Program.cs-Datei, dem Startpunkt des Projekts. Als Erstes entfernen wir die 3 Zeilen in der Main-Methode und erstellen unsere Form stattdessen so:
[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    Form1 form = new Form1();
    form.Show();
}

Allerdings wird die Form nach dem Ausführen der Anwendung sofort wieder beendet, da auch das Ende der Main-Methode direkt nach dem Anzeigen der Form erreicht wird. Das können wir verhindern, indem wir danach eine Schleife einbauen, die so lange durchlaufen wird, bis die Form vom Benutzer beendet wird:
while (form.Created)
{
 
}

In der Schleife können wir nun die Render-Methode aufrufen. Da die Anwendung während dem Durchlaufen der Schleife auf keine Windows-Nachrichten eingeht, rufen wir zudem Application.DoEvents() auf, was genau das erledigt.
[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    Form1 form = new Form1();
    form.Show();

    while (form.Created)
    {
        form.RenderFrame();
        Application.DoEvents();
    }
}


Gameloop - Variante 2


Die folgende Variante benötigt ein wenig mehr Code, verzichtet aber auf den zusätzlichen Aufruf von DoEvents und ist damit etwas effizienter.
Anstatt zwischen dem Rendern separat mittels DoEvents nach Windows-Nachrichten zu fragen, werden wir nun nur dann zeichnen, wenn die Anwendungen keine Nachrichten mehr erhält bzw. alle anderen abgearbeitet wurden (Idle-Event). Sobald dieser Zustand eintritt, wird so lange gerendert, bis wieder andere Nachrichten die Anwendung erreichen.
Den Code der Program-Klasse verändern wir dementsprechend:
private static Form1 form;

[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    Application.Idle += new EventHandler(OnApplicationIdle);

    form = new Form1();
    Application.Run(form);
}

private static void OnApplicationIdle(object sender, EventArgs e)
{
    // ...
}

In der OnApplicationIdle-Methode zeichnen wir nun so lange bis die Anwendung wieder anderweitiges zu tun bekommt. Dazu müssen wir allerdings zusätzlich noch prüfen, ob der Idle-Zustand noch weiterhin bestand hat. Das .Net-Framework bietet dazu keine eigenen Klassen bzw. Methoden, weshalb wir direkt auf die WinAPI-Funktion PeekMessage (MSDN: PeekMessage Function) zurückgreifen. PeekMessage stellt fest, ob und welche Nachrichten für die Anwendung vorliegen. Wenn keine vorhanden sind wird false zurückgeliefert.
using System;
using System.Runtime.InteropServices;

namespace MeinNamespace
{
    public class NativeMethods
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct Message
        {
            public IntPtr hWnd;
            public IntPtr msg;
            public IntPtr wParam;
            public IntPtr lParam;
            public uint time;
            public System.Drawing.Point p;
        }

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("User32.dll", CharSet = CharSet.Auto)]
        public static extern bool PeekMessage(out Message msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);
    }
}

Anwendung findet die Funktion folgendermaßen:
static class Program
{
    private static Form1 form;

    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        Application.Idle += new EventHandler(OnApplicationIdle);

        form = new Form1();
        Application.Run(form);
    }

    private static void OnApplicationIdle(object sender, EventArgs e)
    {
        // So lange Rendern, wie es keine Nachrichten für die Anwendung gibt
        while (AppStillIdle)
        {
            form.RenderFrame();
        }
    }

    private static bool AppStillIdle
    {
        get
        {
            NativeMethods.Message msg;
            return !NativeMethods.PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
        }
    }
}

Quelle: http://blogs.msdn.com/tmiller/archive/2005/05/05/415008.aspx


^ GameLoops - Die gute alte Zeit


Neben dem Rendern spielt natürlich auch das Updaten eine wichtige Rolle. Für gleichmäßige und vor allem gleichschnelle Updaten auf verschiedenen Rechnern muss die, seit dem vorherigen Frame, vergangene Zeit gemessen werden. Die Game-Klasse des XNA Frameworks stellt uns diese Zeitdaten beispielsweise über die GameTime-Klasse automatisch zur Verfügung. In diesem Fall müssen wir allerdings, unabhängig von der gewählten Art der GameLoop, selbst rechnen. Für genaue Zeitmessung im Millisekundenbereich stellt das .Net-Framework die Klasse Stopwatch im Namespace System.Diagnostics bereit.
Um die Handhabung zu vereinfachen, werden wir eine kleine Klasse mit dem Namen GameClock einführen, die uns die abgelaufene Zeit ausrechnet und zurückgibt.
public class GameClock
{
    private Stopwatch watch;
    private TimeSpan elapsedGameTime;

    public TimeSpan ElapsedGameTime
    {
        get
        {
            return elapsedGameTime;
        }
    }

    public GameClock()
    {
        watch = new Stopwatch();
        elapsedGameTime = TimeSpan.Zero;
    }

    public void Start()
    {
        // Stopuhr starten
        watch.Start();
    }

    public void Stop()
    {
        // Stopuhr stoppen
        watch.Stop();

        // Zurückgelegte Zeit zuweisen
        elapsedGameTime = watch.Elapsed;

        // Stopuhr wieder auf 0 setzen.
        watch.Reset();
    }
}

Zusätzlich zur RenderFrame-Methode können wir nun noch eine UpdateFrame-Methode in die Form-Klasse integrieren. Beide Methoden erhalten zudem ein Parameter für die abgelaufene Zeit:
public void UpdateFrame(TimeSpan elapsedTime)
{
    // Bewegen, Animieren
}

public void RenderFrame(TimeSpan elapsedTime)
{
    // Zeichnen
}

Der Program-Klasse spendieren wir nun ein GameClock-Objekt und erweitern die Gameloop, damit UpdateFrame und RenderFrame die Zeit auch erhalten.
private static void OnApplicationIdle(object sender, EventArgs e)
{
    while (AppStillIdle)
    {
        gameClock.Start();
        form.UpdateFrame(gameClock.ElapsedGameTime);
        form.RenderFrame(gameClock.ElapsedGameTime);
        gameClock.Stop();
    }
}

Bei der GameLoop aus Variante 1 schaut das Ganze im Prinzip genauso aus:
while (form.Created)
{
    gameClock.Start();
    form.UpdateFrame(gameClock.ElapsedGameTime);
    form.RenderFrame(gameClock.ElapsedGameTime);
    Application.DoEvents();

    gameClock.Stop();
}

Infos

Name: XNA und WinForms
Autor: SteveKr
Kommentare: Thread