[OpenSource] GameUtils

    • Beta
    • Open Source

    Es gibt 152 Antworten in diesem Thema. Der letzte Beitrag () ist von Artentus.

      Tutorial - Kapitel 2: Update-Zyklus und Zustände

      Dies ist das zweite Tutorial für GameUtils. Hier werde ich euch näher bringen, wie das Updateing in der Engine von statten geht, was es mit den Zuständen auf sich hat und die Anwendung aus dem ersten Kapitel nach eurem neuen Kenntnisstand erweitern.

      Zustände in der Engine
      Neben den oben genannten Engine-Komponenten und -Ressourcen gibt es noch eine weitere wichtige Gruppe von Objekten in einem auf GameUtils basierenden Spiel. Diese Objekte speichern Zustände und werden nicht von der GameEngine, sondern vom GameLoop verwaltet. Höchst wahrscheinlich werden diese Objekte die Mehrheit in einem Spiel ausmachen, da sie alles erdenklich darstellen können, solange es geupdatet werden kann/muss.

      Aus anderen Systemen (auch von GameUtils vor der Alpha 2.0) ist euch der Updatevorgang vielleicht so bekannt, dass regelmäßig eine Update-Methode auf allen Spielobjekten aufgerufen wird (wie und wo diese aufgerufen werden ist hier erstmal egal). Diese Variante ist einfach umzusetzen, hat jedoch einige Probleme, weswegen ich mich bewusst dagegen entschieden habe. Zum einen ist die Reihenfolge, in der die Objekte geupdatet werden, entscheidend. Möglicherweise hat sich z.B. vor einer Kollisionsprüfung ein anderes Objekte bereits vorher wieder weiterbewegt und kollidiert deshalb nicht, oder umgekehrt. Deshalb kann der Update-Vorgang auch nicht parallelisiert werden, sondern muss in einem einzigen Thread stattfinden, was auf modernen Computern Leistungsverschwendung ist. Wenn man nun noch das Rendering hinzuzieht, so werden die Probleme noch größer. Laufen Updateing und Rendering nebeneinander, dann ist es sehr wahrscheinlich, dass während dem REndering bestimmte Objekte bereits geupdatet wurden, andere jedoch nicht, wodurch sich die auf dem Bildschirm angezeigten Objekte in unterschiedlichen Update-Zuständen befinden. Einzige Möglichkeit, das effektiv zu lösen, ist Updateing und Rendering nicht nebeneinander, sondern nacheinander laufen zu lassen, und damit haben wir nun jegliche Form von Multithreading abgeschafft.
      Da diese Lösung also nicht akzeptabel für eine High-Performance-Engine war, musst etwas neues her, etwas, dass sich von der althergebrachten Methode deutlich unterschied. Ich habe mich für die sogenannte "Double Buffer"-Methode entschieden. Statt einem Objekt, dass sich regelmäßig updatet, existieren jetzt zwei Objekte (zwei Zustände) nebeneinander, zwar unzertrennbar verknüpft, aber dennoch nicht direkt voneinander abhängig. Der Trick besteht nun darin, dass immer einer dieser Zustände geupdatet wird (und zwar auf Basis des anderen Zustandes), während der andere gezeichnet wird. So wird der Zustand, auf den man während dem Update zugreift, nicht verändert, wodurch die Reihenfolge der Updates irrelevant werden. Außerdem kann der momentan unveränderte Zustand zum Rendern verwendet werden und garantiert, dass alle Objekte im gleichen Updatezustand gezeichnet werden. Updateing und Rendering lassen sich also auf Kosten des doppelten Speicherverbrauchs perfekt parallelisieren, das Updating soger soweit, dass die einzelnen Objekte in verschiedenen Threads geupdatet werden können.

      Keine Sorge, wenn ihr jetzt nicht gleich alles verstanden habt, anhand des Codes wird es nun deutlich werden. Es ist nicht so schwer, wie es sich anhört, nur ungewohnt (auch ich muss mich immer noch daran gewöhnen, falls euch das beruhigt).

      Spoiler anzeigen
      Um diese mehr oder weniger komplexe Struktur zu erstellen, benötigt ihr zuerst einmal einen RegistrationContext. Das ist eine abstrakte Klasse, die der Engine erlaubt, für euch diese oben genannten Beziehungen zu knüpfen.

      Euch wird vermutlich sofort auffallen, dass die Klasse generisch ist. Das ist ein weiterer Vorteil von GameUtils, ich habe einigen Aufwand betrieben, damit ihr immer genau angeben könnt, was ihr reingebt, und dann auch exakt dass wieder rausbekommt, es besteht keine Notwendigkeit mit Basisklassen, Interfaces und viel Typecasting zu arbeiten.
      In diesem Fall gibt das Typargument den Typen eurer Zustands-Klasse an, welche IBufferedState implementieren muss. Doch bevor wir dazu kommen ersteinmal die Member im Überblick:

      CurrentState: gibt den aktuellen Zustand an. Das ist derjenige Zustand, der gerade nicht geupdatet wird, also der, den andere Objekte zum Updaten verwenden sollten, wenn sie denn überhaupt von anderen Objekten abhängig sind. Der Typ ist der als Typparameter angegebene.

      Renderer: der Renderer für dieses Objekt. Dieser ist vom Typ IStateRenderer<TState>, ist also vom Typargument abhängig. Auch zu diesem Interface werde ich gleich noch kommen. Die Eigenschaft ist protected, da sie nur von der Engine intern verwendet wird.

      CreateBuffer: diese Funktion ist ebenfalls protected und erstellt einen Zustandspuffer. Bei korrekter Verwendung wird sie von der Engine genau zweimal aufgerufen. Der Rückgabetyp entspricht dem Typargument.

      CurrentStateChaged: ein Ereignis, das nur der RegistrationContext selbst empfangen kann und ausgelöst wird, wenn der aktuelle Status geändert wurde (dies geschieht einmal pro Update-Zyklus).

      Das Zustand-Interface
      Das Interface um die Zustände darzustellen, ist, wie bereits erwähnt, IBufferedState:

      Dies ist ein mehr oder weniger interessantes Interface, man beachte den Namen des Typparameters. Auch wenn es möglich ist, hier etwas anderes anzugeben, müsst ihr laut Engine-Constraint hier den eigenen Typn angeben, ansonsten wird nichts funktionieren. Bsp.:

      C#-Quellcode

      1. class TestState : IBufferedState<TestState>
      2. { }

      VB.NET-Quellcode

      1. Class TestState : Implements IBufferedState(Of TestState)
      2. End Class

      Das Interface besitzt genau eine Methode, nämlich Update(TSelf oldState, TimeSpan elapsed).
      Die Methode wird immer aufgerufen, wenn der Zustand geupdatet werden muss. Der erste Parameter ist dabei der alte Zustand, auf den ausschließlich lesend zugegriffen werden darf. Der zweite Parameter gibt die Zeit seit dem letztem Update an (wichtig: dies ist die Zeit seit dem letzten Update des gesamten Objektes, nicht seit dem letzten Update dieses einen Buffers).

      Das Render-Interface
      Obwohl jedes Objekt zwei Zustände speichert, besitzt es nur einen einzigen Renderer, verfügbar gemacht durch das IStateRenderer-Interfce.

      Die Member DepthPosition und DepthPositionChanged sind für Tiefeninformationen da, das interessiert uns aber im Moment noch nicht, weshalb ich sie einfach überspringen und später leer implementieren werde.

      Im Moment wichtig ist die Render(TState state, Renderer renderer)-Method, mit der man später Dinge auf den Bildschirm bekommt. Der erste Parameter ist der Zustand, der gerendert werden soll. Alles, was gerendert werden soll (mit Ausnahme vielleicht der Dinge, die immer unverändert dargestellt werden sollen) sollte nur auf diesem Zustand basieren, auf nichts anderem, weil nur so die Integrität gewährleistet werden kann. Als zweites bekommt ihr hier den Renderer, den ihr zum rendern verwenden könnt/müsst. Dies ist der Renderer, den ihr ganz am Anfang, siehe Kapitel 1, erstellt habt.


      Der Update-Zyklus
      Bevor wir an den Code gehen, gibts noch einen kleinen Exkurs dazu, wie die Engine Updateing und Rendering koordiniert.

      Gleich zu Anfang: ein Objekt kann selbstverständlich noch nicht gerendert werden, wenn es noch nie geupdatet wurde. Die Engine trägt dafür Sorge, dass ein Objekt immer mindestens einmal geupdatet wurde, bevor es das erste mal gerendert wird, ihr braucht euch darum also nicht kümmern.

      Updateing und Rendering sind in GameUtils so synchronisiert, dass es immer genauso viele Update-Zyklen wie Render-Zyklen gibt, das bedeutet, sie blockieren sich gegenseitig. Im Normalfall wird das Rendering das Updateing begrenzen, entweder durch das FPS-Limit oder weil das Rendern doch wesentlich länger dauert, weil es nicht so stark synchronisiert werden kann. Das ist aber nicht schlimm, denn die Engine bietet Möglichkeiten, auf Usereingaben zu reagieren unabhängig vom normalen Updateing. Grund für dieses Verhalten ist, dass nur so die ganz zu Beginn genannten Probleme beseitigt werden können.

      Ich will nicht zu sehr ins technische abdriften, das Grundprinzip ist dieses:

      Quelle: altdevblogaday.com/2011/07/03/threading-and-your-game-loop/

      Wir erweitern unser Spiel
      Jetzt werden wir das neue Wissen an unserer Testanwendung ausprobieren. Ziel ist es, einen einfachen Zähler korrekt in jedem Update zu inkrementieren. Leider können wir unsere Ergebnisse noch nicht anzeigen, aber spätestens nach dem nächsten Tutorial wird auch das dann kein Problem mehr sein.

      Also worauf warten wir noch? Als erstes brauchen wir unsere Zustands-Klasse (ich hab sie hier einfach mal CounterState genannt):

      C#-Quellcode

      1. public class CounterState : IBufferedState<CounterState>
      2. {
      3. public int Count { get; private set; }
      4. void IBufferedState<CounterState>.Update(CounterState oldState, TimeSpan elapsed)
      5. {
      6. this.Count = oldState.Count + 1;
      7. Debug.Print(this.Count.ToString());
      8. }
      9. }

      VB.NET-Quellcode

      1. Public Class CounterState : Implements IBufferedState(Of CounterState)
      2. Private _count As Integer
      3. Public Property Count As Integer
      4. Get
      5. Return _count
      6. End Get
      7. Private Set(value As Integer)
      8. _count = value
      9. End Set
      10. End Property
      11. Private Sub Update(oldState As CounterState, elapsed As TimeSpan) Implements IBufferedState(Of CounterState).Update
      12. Me.Count = oldState.Count + 1
      13. Debug.Print(Me.Count.ToString())
      14. End Sub
      15. End Class
      Das war auch schon die ganze Magie. Wir nehmen im Update den alten Wert aus dem anderen Zustand, addieren 1 und weisen es dem aktuellen Counter zu.
      Zur Veranschaulichung, dass auch wirklich das gewünschte passiert, habe ich noch eine Debug-Ausgabe eingebaut.

      Als nächstes brächten wir eigentlich unseren Renderer. Da wir diesen noch nicht benutzen können, lassen wir ihn aus und geben dann später einfach Nothing zurück. Die Engine wird dies akzeptieren und dieses Objekt als nicht darstellbar behandeln (das macht für euch im Moment überhaupt keinen Unterschied).

      Jetzt fehlt also nur noch der RegistrationContext. Dort können wir es ganz einfach halten, als Renderer geben wir, wie schon angekündigt, null zurück, in CreateBuffer können wir einfach immer einen neuen CounterState zurückgeben:

      C#-Quellcode

      1. public class Counter : RegistrationContext<CounterState>
      2. {
      3. protected override CounterState CreateBuffer()
      4. {
      5. return new CounterState();
      6. }
      7. protected override GameUtils.Graphics.IStateRenderer<CounterState> Renderer
      8. {
      9. get { return null; }
      10. }
      11. }

      VB.NET-Quellcode

      1. Public Class Counter : Inherits RegistrationContext(Of CounterState)
      2. Protected Overrides Function CreateBuffer() As CounterState
      3. Return New CounterState()
      4. End Function
      5. Protected Overrides ReadOnly Property Renderer As Graphics.IStateRenderer(Of CounterState)
      6. Get
      7. Return Nothing
      8. End Get
      9. End Property
      10. End Class
      War doch gar nicht so schwer. :)
      Als letztes müssen wir unsere Main-Methode noch um eine Zeile erweitern, damit auch was passiert:

      C#-Quellcode

      1. [STAThread]
      2. static void Main()
      3. {
      4. Application.EnableVisualStyles();
      5. Application.SetCompatibleTextRenderingDefault(false);
      6. var window = new GameWindow();
      7. GameEngine.RegisterComponent(Renderer.Create<GdiRenderer>(window));
      8. var loop = new GameLoop();
      9. GameEngine.RegisterComponent(loop);
      10. loop.Register(new Counter()); // <--- neu
      11. loop.Start();
      12. window.FormClosing += (sender, e) => loop.Stop();
      13. Application.Run(window);
      14. }

      VB.NET-Quellcode

      1. <STAThread()>
      2. Sub Main()
      3. Application.EnableVisualStyles()
      4. Application.SetCompatibleTextRenderingDefault(False)
      5. Dim window As New GameWindow()
      6. GameEngine.RegisterComponent(Renderer.Create(Of GdiRenderer)(window))
      7. Dim [loop] As New GameLoop()
      8. GameEngine.RegisterComponent([loop])
      9. [loop].Register(New Counter()) ' <--- neu
      10. [loop].Start()
      11. AddHandler window.FormClosing, Sub(sender, e) [loop].Stop()
      12. Application.Run(window)
      13. End Sub

      Hab ihr alles richtig gemacht, dann erscheinen in eurer Console nun Zahlen von 1 bis n. Herzlichen Glückwunsch, ihr habt diesen Teil des Tutorials erfolgreich gemeistert.


      Weiter: Tutorial - Kapitel 3: Factory-Ressourcen und das Rendering
      Dateien

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Artentus“ ()

      Tutorial - Kapitel 3: Factory-Ressourcen und das Rendering

      Im dritten Tutorial werden wir unsere kleine Anwendung dazu bringen, uns etwas anzuzeigen. Dazu erkläre ich euch erst ein mal, was Factory-Ressourcen sind und wie man sie benutzt, und wie sie mit dem Renderer zusammenspielen. Ich werde auch die Methoden des Renderers genauer besprechen.

      Factory-Ressourcen
      Factory-Ressourcen sind eine spezielle Art von Ressourcen, die von einer Factory erstellt werden (darauf werde ich jetzt nicht weiter eingehen, das ist Soff für ein viel späteres Kapitel). Ihr könnt selbst keine Factory-Ressourcen implementieren, alle vorhandenen werden euch von der Engine bereitgestellt, sie befinden sich im GameUtils.Graphics-Namespace.
      Sie implementieren alle IResource, das heißt ihr könnt ihnen auch einen Tag geben und sie darüber abrufen. Ihr müsst sie allerdings nicht registrieren, dies geschieht bereits automatisch bei ihrer Erstellung.

      Spoiler anzeigen
      Die Basisklasse dieser Ressourcen ist Resource<T>:

      Hier gibt es gar nicht allzu viel zu erklären. Wie bereits erwähnt besitzen auch die Factory-Ressourcen einen Tag, und ihr solltet sie selbstverständlich Disposen. IsCreated gibt an, ob die Ressource bereits erstellt wurde. Dies braucht euch vermutlich wenig zu kümmern, da die Engine dafür Sorge trägt, dass die Ressourcen definitiv erstellt sind, wenn ihr sie braucht.

      Um das Typargument braucht ihr euch zu diesem Zeitpunkt auch noch keine Sorgen zu machen, die bestehenden Factory-Ressourcen-Klassen kümmern sich bereits darum.


      Die momentan verfügbaren Factory-Ressourcen sind:
      • Brush (mit den abgeleiteten Klassen SolidColorBrush, LinearGradientBrush, RadialGradientBrush, TextureBrush)
      • Texture
      • Font
      • TextFormat
      • Geometry
      Was diese Klassen alle machen sollte, denke ich, klar sein, ebenso ihre Eigenschaften und Funktionen, das ganze ist sehr ähnlich zu GDI+ aufgebaut. Im Zweifel ist auch alles dokumentiert (so wie fast alles in GameUtils).

      Ganz wichtig beim Verwenden dieser Klassen ist, dass ihr niemals innerhalb der Render-Methode etwas an ihnen ändern dürft. Die Engine ist so designet, dass ihr alle Änderungen in der Update-Methode vornehmnt und beim Rendern die Ressourcen nur lesend verwendet. Änderungen in der Render-Methode werden nicht übernommen, erst beim nächsten Aufruf.
      Ihr solltet also nur Ressourcen, die sich über die Zeit nicht verändern, in den Renderer eures Objektes stecken, alle anderen kommen in die beiden Zustände. Ihr braucht aber nicht je Zustand eine Ressource erstellen (ihr könnt natürlich), die beiden Zustände können sich eine Ressource teilen, da in GameUtils (wie ich bereits erwähnt habe) alle Ressourcen gepuffert sind. Dadurch wird Speicherplatz gespart.

      Weitere Klassen/Strukturen
      Im Zusammenhang mit den Factory-Ressourcen stehen auch noch einige kleinere Klassen/Strukturen, die ihr sehr oft benötigen werdet. Es folgt eine List + jeweils eine kleine Erklärung.

      Vector2
      Zu finden in GameUtils.Math. Der Vector2 übernimmt die Rolle des Point/PointF und der Size/SizeF aus GDI+, die Aufteilung dieser beiden Klassen ist nicht nötig. Neben Positions- und Größenangaben beherrscht diese Struktur aber auch höhere Vektorgeometrie, alle wichtigen Funktionen sind vorhanden (siehe Klassendiagramm für die vollständige Liste).

      Rectangle
      Zu finden in GameUtils.Math. Diese Struktur ist quasi identisch zu der aus GDI+.

      Ellipse
      Zu finden in GameUtils.Math. Eine solche Struktur fehlt in GDI+, dort werden Ellipsen über Rectangles spezifiziert, ich habe mich aber dafür entschieden, sie als Center + Radius zu definieren.

      Polygon
      Zu finden in GameUtils.Math. Auch diese Struktur ist GDI+ fremd. Sie ersetzt jedoch nicht einfach ein Vector2-Array, mit ihr ist es möglich, komplexe Formen auf Überlappung zu testen, zu testen, ob ein Punkt enthalten ist, oder den Mittelpunkt zu berechnen.

      Color4
      Zu finden in GameUtils.Graphics. Diese Struktur stellt eine Farbe in Form von vier Gleitkommawerten für Alpha, Rot, Grün und Blau dar. Es wird auch die Konvertierung zum und vom HSV-Farbraum unterstützt, sowie einfache Farbmultiplikation.

      Matrix2x3
      Zu finden in GameUtils.Math. Die Matrix2x3 kann zweidimensionale Transformationen darstellen und ist damit das Gegenstück zur GDI+-Matrix. Im Gegensatzt zu dieser ist sie jedoch "gedreht", sodass die Indexierung so aussieht:

      Bzw. das bedeuten die verschiedenen Elemente:

      Für alle 4 affinen Transformationen, Skalierung, Scherung, Rotation und Translation, gibt es statische Funktionen, die sie für euch erstellen.


      Der Renderer
      Jetzt zum Renderer.
      Der Renderer aus GameUtils ist dem Graphics-Objekt aus GDI+ sehr ähnlich. Er besitzt Funktionen zum Zeichnen und Füllen von Rechtecken, Ellipsen, Polygonen und komplexeren Formen (repräsentiert durch die Geometry-Klasse), aber auch zum Zeichnen und abmessen von Text. Dazu besitzt er noch einige Member, die das Zeichnen indirekt beeinflussen.

      Spoiler anzeigen

      Die beiden statischen Funktionen Create und IntelligentSelect haben wir bereits besprochen, ich werde diese also außenvor lassen.

      PlatformVersion: die minimal erforderliche Windows-Version für diesen Renderer. Anhand dieser Eigenschaft trifft die Engine bei IntelligentSelect ihre Auswahl.

      SurfaceBounds: da Abmessungen der Zeichenfläche.

      Clear: füllt die gesamte Zeichenfläche mit der angegebenen Farbe.
      Diese Funktion wird am Anfang jedes Render-Zykluses vom GameLoop aufgerufen. Die dabei verwendete Farbe könnt ihr mit der BackColor-Eigenschaft des GameLoops festlegen.

      Draw...-Funktionen: zeichnet die angegebene Form/Textur/Text. Für Informationen zu den Parametern siehe Dokumentation.

      Fill...-Funktionen: füllt die angegebene Form. Für Informationen zu den Parametern siehe Dokumentation.

      MeasureText: misst die Abmessungen eines Strings in einer angegebenen Font.

      Set/ResetTransform: setzt die Welttransformation fest/zurück.

      Push/PopClip: legt einen Clippingbereich auf dem Stack ab/entfernt ihn davon.


      Unser Programm zeigt etwas an
      Im letzten Tutorial haben wir die aktuelle Zahl in der Console ausgegeben. Das ist natürlich nicht schön, wir möchten alles in dem Fenster des Spiels anzeigen.
      Wir schreiben uns also einen Renderer für unser Counter-Objekt, welcher die aktuelle Zahl auf das Fenster zeichnet.

      Spoiler anzeigen

      C#-Quellcode

      1. public class CounterRenderer : IStateRenderer<CounterState>, IDisposable
      2. {
      3. readonly SolidColorBrush brush;
      4. readonly Font font;
      5. public event EventHandler DepthPositionChanged;
      6. public float DepthPosition
      7. {
      8. get { return 0; }
      9. }
      10. public CounterRenderer()
      11. {
      12. brush = new SolidColorBrush(Color4.White);
      13. font = new Font("Arial", 20);
      14. }
      15. void IStateRenderer<CounterState>.Render(CounterState state, Renderer renderer)
      16. {
      17. renderer.DrawText(state.Count.ToString(), font, brush, new Vector2(10, 10));
      18. }
      19. private bool disposed;
      20. public void Dispose()
      21. {
      22. if (!disposed)
      23. {
      24. Dispose(true);
      25. GC.SuppressFinalize(this);
      26. disposed = true;
      27. }
      28. }
      29. protected virtual void Dispose(bool disposing)
      30. {
      31. brush.Dispose();
      32. font.Dispose();
      33. }
      34. ~CounterRenderer()
      35. {
      36. Dispose(false);
      37. }
      38. }

      VB.NET-Quellcode

      1. Public Class CounterRenderer : Implements IStateRenderer(Of CounterState) : Implements IDisposable
      2. ReadOnly brush As SolidColorBrush
      3. ReadOnly font As Font
      4. Public Event DepthPositionChanged As EventHandler Implements IStateRenderer(Of CounterState).DepthPositionChanged
      5. Public ReadOnly Property DepthPosition As Single Implements IStateRenderer(Of CounterState).DepthPosition
      6. Get
      7. Return 0
      8. End Get
      9. End Property
      10. Public Sub New()
      11. brush = New SolidColorBrush(Color4.White)
      12. font = New Font("Arial", 20)
      13. End Sub
      14. Private Sub Render(state As CounterState, renderer As Renderer) Implements IStateRenderer(Of CounterState).Render
      15. renderer.DrawText(state.Count.ToString(), font, brush, New Vector2(10, 10))
      16. End Sub
      17. #Region "IDisposable Support"
      18. Private disposedValue As Boolean
      19. Protected Overridable Sub Dispose(disposing As Boolean)
      20. If Not Me.disposedValue Then
      21. brush.Dispose()
      22. font.Dispose()
      23. End If
      24. Me.disposedValue = True
      25. End Sub
      26. Protected Overrides Sub Finalize()
      27. Dispose(False)
      28. MyBase.Finalize()
      29. End Sub
      30. Public Sub Dispose() Implements IDisposable.Dispose
      31. Dispose(True)
      32. GC.SuppressFinalize(Me)
      33. End Sub
      34. #End Region
      35. End Class
      Zum Zeichnen eines Textes benötigen wir zwei Factory-Ressourcen, einen Brush und eine Font, also legen wir beide an und initialisieren sie im Konstruktor. Ich habe mich hier für einen weißen Brush und als Font Arial mit 20 Pixeln Größe entschieden. Bedenkt bitte, dass wir diese Ressourcen nur in den Renderer packen konnten, weil wir sie nach der Erstellung nicht mehr anrühren, ansonsten hätten sie in die Zustands-Klasse gemusst. Zum Zeichnen des Textes schlussendlich rufen wir dann einfach DrawText auf dem übergebenen Renderer auf.
      Ich habe auch gleich IDisposable implementiert, da die beiden Ressourcen aufgeräumt werden müssen.
      Beachtet, dass ich für DepthPosition und DepthPositionChanged einfach die Standardimplementierung verwendet habe, da wir sie noch nicht brauchen.

      Wir sollten natürlich auch noch die Consolenausgabe aus CounterState entfernen. Das Löschen einer einzelnen Zeile schafft ihr sicher auch ohne mein Zutun, weswegen ich das jetzt nicht nochmal extra poste.

      Unser RegistrationContext muss der Engine den neuen Renderer natürlich auch weiterreichen, ansonsten bleibt der Bildschirm schwarz. Deswegen habe ich die Counter-Klasse ebenfalls etwas angepasst.
      Spoiler anzeigen

      C#-Quellcode

      1. public class Counter : RegistrationContext<CounterState>, IDisposable
      2. {
      3. CounterRenderer renderer;
      4. protected override CounterState CreateBuffer()
      5. {
      6. return new CounterState();
      7. }
      8. protected override GameUtils.Graphics.IStateRenderer<CounterState> Renderer
      9. {
      10. get { return renderer ?? (renderer = new CounterRenderer()); }
      11. }
      12. private bool disposed;
      13. public void Dispose()
      14. {
      15. if (!disposed)
      16. {
      17. Dispose(true);
      18. GC.SuppressFinalize(this);
      19. disposed = true;
      20. }
      21. }
      22. protected virtual void Dispose(bool disposing)
      23. {
      24. if (renderer != null) renderer.Dispose();
      25. }
      26. ~Counter()
      27. {
      28. Dispose(false);
      29. }
      30. }

      VB.NET-Quellcode

      1. Public Class Counter : Inherits RegistrationContext(Of CounterState) : Implements IDisposable
      2. Private _renderer As CounterRenderer
      3. Protected Overrides Function CreateBuffer() As CounterState
      4. Return New CounterState()
      5. End Function
      6. Protected Overrides ReadOnly Property Renderer As Graphics.IStateRenderer(Of CounterState)
      7. Get
      8. If (_renderer Is Nothing) Then _renderer = New CounterRenderer()
      9. Return _renderer
      10. End Get
      11. End Property
      12. #Region "IDisposable Support"
      13. Private disposedValue As Boolean
      14. Protected Overridable Sub Dispose(disposing As Boolean)
      15. If Not Me.disposedValue Then
      16. If (_renderer IsNot Nothing) Then _renderer.Dispose()
      17. End If
      18. Me.disposedValue = True
      19. End Sub
      20. Protected Overrides Sub Finalize()
      21. Dispose(False)
      22. MyBase.Finalize()
      23. End Sub
      24. Public Sub Dispose() Implements IDisposable.Dispose
      25. Dispose(True)
      26. GC.SuppressFinalize(Me)
      27. End Sub
      28. #End Region
      29. End Class
      Ich habe auch hier IDisposable implementiert, denn wir müssen ja jetzt den Renderer disposen. Den RegistrationContext, also die Counter-Klasse, müssen wir nun nicht mehr manuell disposen, im Gegenteil wäre es sogar schlimm, wenn wir das tun würden. Die Engine wird das stattdessen für uns übernehmen, und zwar dann, wenn es sicher ist, würden wir manuell disposen, könnte dies unter Umständen zu früh geschehen und das Programm würde abstürzen.

      Das wars, ihr könnt die Anwendung nun starten und seht einen Counter in der linken oberen Ecke des Fensters hochzählen. :thumbup:
      Dateien
      Moin!

      Dein Projekt schaut erstmal sehr interessant aus und deine Tuts sind meiner Meinung nach gut erklärt! Ich hab' selbst in der Vergangenheit kleinere Spiele mit GDI und anspruchsvollere Projekte mit XNA realisiert, mich nervte aber, dass GDI ab einer gewissen Fülle zu CPU-lastig wird und XNA erstmal beim Client installiert werden muss.
      Meine Fragen:
      Gibt's für deine Engine auch schon DirectX- oder SharpDX-Renderer? Wie und wo müssten Shader realisiert werden? Denn beides wäre für ein ambitioniertes und üppig angelegtes Projekt (Beispiel Terraria *grummel*) schon von Vorteil. Und ich würde deine Engine, da sie mir gut durchdacht scheint, gerne in Zukunft nutzen - Dann aber mit DirectX (auch mit < Windows 8).

      MfG,
      X-Zat / Mo
      Es gibt nen Direct2D-Renderer, der funktioniert ab Windows Vista und bringt für 2D-Spiele, egal wie groß, ausreichend Performance. Shader gibts in der Form in Direct2D aber nicht, höchstens Effects, bei GDI kannst dus komplett vergessen (logisch).
      Nen Direct3D- oder OGL-Renderer wirds von meiner Seite aber in absehbarer Zeit nicht geben, ich wüsste ehrlich gesagt nichtmal, wie ich das anstellen solle.
      Nabend, Artentus hättest Du eine Idee warum ich eine OverflowException um die Ohren geworfen bekomme?

      C#-Quellcode

      1. void IStateRenderer<CounterState>.Render(CounterState state, Renderer renderer)
      2. {
      3. renderer.DrawText(state.Count.ToString(), font, brush, new Vector2(10, 10)); //here
      4. }

      Habe den Code eig. nur kopiert. Ist natürlich Möglich das ich einen Fehler reinkopiert habe.
      Kann gerade noch so die Augen aufhalten :D
      Ja, ich hab da ne Idee. Genau das selbe Problem hatte ich auch, als ich das Tutorial geschrieben habe. :D
      Den Grund kenn ich bis heut nicht, GDI+ schmeißt einfach nen Fehler, aber der ist offensichtlich nirgends dokumentiert. Intern wird bei dieser Überladung der Funktion die GDI+-Funktion mit LayoutRectangle aufgerufen, die Abmessungen des Rechtecks sind dabei die maximale Größe (also so groß wie möglich, da man bei dieser Funktion hier keine Größenbeschränkungen erwartet). Das funktioniert aber offensichtlich nicht, ich habs jetzt auf Int16.MaxValue gesetzt. In der DLL, die dem Beispielprojekt beiliegt, es es schon behoben, du kannst also die verwenden, nen extra Release wars mir jetzt nicht wert, da ich auch sonst noch einiges intern am rumschrauben bin.
      Ich suche gerade nach einer angenehmen Möglichkeit, ein Spiel für WP8 zu entwickeln. Bevor ich mich mit SharpDX und Unity beschäftigen muss, schaue ich eher was nach was abstrahierteren.

      In der Annahme, dass es nen SharpDX-Modul für diese Game Library gibt: Läuft die auch auf WP8? Wenn ja, wie sieht's da mit Touch-Bedienung aus?
      Von meinem iPhone gesendet
      Ja, es gibt ein SharpDX-Modul auf Basis von Direct2D und laut Doku sind alle Klassen, die ich verwende, für WP8 ausgeschrieben. Ich hab allerdings überhaupt keine Erfahrung mit WP8-Entwicklung (besitze kein solches Handy) und weiß daher nicht, inwiefern das da funktionieren wird.
      Touch ist nicht drin, nur Maus (per DirectInput), und da weiß ich wieder nicht, inwiefern das kompatibel ist. Die Engine ist aber so konzipiert, dass sich im Zweifel auch eigene Input-Funktionen ganz einfach in den Rest integrieren lassen.
      Wenn gewünscht, kann ich mich mal darüber erkundigen, und wenns nicht zu kompliziert ist kann ich auch ne offizielle Unterstützung anbieten.

      Edit: es scheint so, als hätte Microsoft das Einstellen von Apps, die Direct2D verwenden, verboten.
      http://kennykerr.ca/2012/12/01/windows-phone-and-direct2d/
      Ich fürchte also, die Engine ist keine Option für dich.

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Artentus“ ()

      Hab mir die neue DLL in da Projekt kopiert und mit der neuen begrenzung läuft es.
      Die Struktur der Engine ist relativ ungewohnt, die zentralen Teile Update und Render
      fehlen bzw. sind in der DLL. Also braucht man für ein entity (mit Update und Draw) immer 3 Klassen:
      X : IBufferedState für die Update Methode, Y : IStateRenderer zum Zeichnen und Z : RegistrationContext<> zum Anmelden für das Updaten/Rendern?
      Falls das so richtig sein sollte, sieht CreateBuffer() immer so aus? Das muss wohl intern aufgerufen werden (incl. oldState übergabe).

      Eistee schrieb:

      Die Struktur der Engine ist relativ ungewohnt
      Ja, in der Tat. Das war leider notwendig, um die Objekte auf zwei Puffer aufzuteilen.

      Eistee schrieb:

      die zentralen Teile Update und Render [...] sind in der DLL
      Das ist der Sinn einer Engine. ^^

      Eistee schrieb:

      Also braucht man für ein entity (mit Update und Draw) immer 3 Klassen
      Korrekt. Das mag zwar zunächst sehr sperrig wirken, jedoch ist das am Ende nur zum eigenen Vorteil. Ich hab die Engine so designt, dass man zum sauberen Programmieren "gezwungen" wird, und durch die Aufteilung wir eine Trennung von Updateing und Rendering sichergestellt, sowie die Kommunikation zwischen den einzelnen Objekten. ;)

      Eistee schrieb:

      sieht CreateBuffer() immer so aus? Das muss wohl intern aufgerufen werden (incl. oldState übergabe).
      Allgemein schon, bei komplexeren Sachen kann aber noch was dazukommen. Die Tutorialreihe hat gerade erst angefangen, es werden noch einige weitere folgen, wo sowas dann im Detail besprochen wird, ich muss mir nur erst mal wieder Konzepte ausdenken (hatte erstmal nur die ersten drei vorgeplant).
      Die Funktion wird intern aufgerufen, und das ist auch der einzige Ort, wo sie aufgerufen werden sollte (also bitte nicht der Engine ins Handwerk spielen). OldState wird bei CreateBuffer() allerdings nirgends übergeben, das betrifft nur Update(), verwechselst du das was?

      Artentus schrieb:

      OldState wird bei CreateBuffer() allerdings nirgends übergeben, das betrifft nur Update(), verwechselst du das was?

      Jein, kann mich nur schlecht audrücken :rolleyes:
      CreateBuffer() gibt ja einen State zurück, welches so zu sagen Update beinhaltet.
      Gamestates (Menu, Levels, Interfaces) werden aber nicht durch das Registrieren alleine gehandelt?:
      Menu Registrieren => Gameplay Registrieren => Menu Disposen => (...) => Menu Registrieren => Gameplay Disposen => Menu Disposen => Exit.
      Oder sollte man lieber alles Registriert lassen und nur verstecken/nichtzeichnen bzw. das Update unterlassen.

      Welches Projekt ich zur anschau immer ganz gut fande, war diese Spiel LINK, hatte solch ein Tutorial (mit dem Projekt) mal mit Java erstellt (für die Schule).
      Ist eig. alles dabei und sehr klein gehalten, Objekte, Steuerung, Animation, Ton.
      Sollte weniger Aufwand sein, als dein Tetris neu zu schreiben (wenn es sich eben nicht so einfach portieren lässt).
      Du kannst Sachen, die du nicht brauchst, ruhig wieder deregistrieren, die Engine ist soweit optimiert, dass das nicht sehr viel Performance frisst. Je besser du strukturierst, desto effiziente ist die Engine hier auch (Methoden zur Strukturierung werde ich noch in einem Tutorial besprechen).
      Menüs werden jedoch ganz anders realisiert. Du musst dir da nichts eigenes schreiben, die Engine bietet eine highlevel UI-API, die dir sowas enorm vereinfachen wird (auch dazu in späteren Tutorials mehr).
      Ich bekomme leider folgende Fehlermeldung/Warnung beim VB-Tester:
      Warnung 1 Die Zustandsdatei "obj\Debug\GameUtils_Tutorial-Chapter2_VB.vbproj.GenerateResource.Cache" konnte nicht gelesen werden. Die Assembly "Microsoft.Build.Tasks.v12.0, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" kann nicht gefunden werden. GameUtils_Tutorial-Chapter2_VB
      Polling is trolling!

      Achtung: Ich habe die komische Angewohnheit, simple Dinge zu verkomplizieren..
      Jo, das sind die Standard-Steps die ich dann erstmal durchführe, wollte nicht gehen.

      Ich probiers gleich nochmal, vll. wollte der PC gestern einfach nicht mehr^^
      Polling is trolling!

      Achtung: Ich habe die komische Angewohnheit, simple Dinge zu verkomplizieren..
      Ich weiß nicht, wie gut sich das in deine Library einbauen lässt. Aber wo wir gerade beim Thema waren, könnte man ja etwas machen, um leichter unabhängig von der Auflösung zu programmieren.

      Dafür würde ich nicht das vorhandene System ersetzen, sondern einen separaten Layer hinzufügen, den man zusätzlich bzw. statt der Standard-Herangehensweise benutzen kann. Ich stelle mir da einfach was vor, was die Inputs/Outputs entsprechend anpasst, oder ggf. die virtuelle Fläche (oder wie man das nennt) zur Verfügung stellt. - Jedenfalls irgendwas, damit man so einfach wie möglich out-of-the-box ohne irgendwelche Wrapper-Properties (z. B. Transformation der Mauskoordinaten) ein auflösungsunabhängiges Spiel implementieren kann.

      Als ich das GitHub-Projekt klonen und kompilieren wollte, bekan ich einen Compilererror. Angeblich würde ein Interface fehlen (weiß nicht mehr genau welches). Stimmt da alles?
      Von meinem iPhone gesendet

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „nikeee13“ ()

      Die Solution auf GitHub ist in Ordnung, die Einstellungen sind nur offensichtlich etwas durcheinander gekommen. Die Projekte GameUtils.Network und GameUtils.Physics sollten eigentlich entladen sein (einfach Rechtsklick->Projekt entladen), sie funktionieren im Moment nicht. Außerdem dann noch das TestApp-Projekt als Startprojekt festlegen.

      Ein solcher Layer wäre grundsätzlich möglich, allerdings sehe ich da ein Problem. Nicht alles kann durch einfaches Downsizing/Upsizing gelöst werden, vor allem aufwändige UI, das bedeutet, eine einfache Skalierung von der angegebenen "Basisauflösung" zur tatsächlichen Auflösung würde den Programmierer zu stark einschränken. Das Verhalten in solchen Fällen muss also vom Spiel selber festgelegt werden, und ich glaube es würde alles nur komplizierter machen, wenn dann anfange ein System zu entwickeln, dass alle erdenklichen Fälle abdecken kann.
      Wenn ich die TestApp starte und dann Escape drücke (andere Tasten habe ich nicht ausprobiert) bekomme ich eine Exception:
      ​In einen geschlossenen TextWriter kann nicht geschrieben werden.

      Spoiler anzeigen

      Brainfuck-Quellcode

      1. Informationen über das Aufrufen von JIT-Debuggen
      2. anstelle dieses Dialogfelds finden Sie am Ende dieser Meldung.
      3. ************** Ausnahmetext **************
      4. System.ObjectDisposedException: In einen geschlossenen TextWriter kann nicht geschrieben werden.
      5. bei System.IO.StreamWriter.Flush(Boolean flushStream, Boolean flushEncoder)
      6. bei System.IO.StreamWriter.Write(Char[] buffer, Int32 index, Int32 count)
      7. bei System.IO.TextWriter.WriteLine(String value)
      8. bei GameUtils.Logging.LogFile.SendToOutput(String message) in c:\Intel\HS2\GameUtils\GameUtils\GameUtils\Logging\LogFile.cs:Zeile 36.
      9. bei GameUtils.Logging.Logger.PostMessage(String message, LogMessageKind messageKind, LogMessagePriority messagePriority) in c:\Intel\HS2\GameUtils\GameUtils\GameUtils\Logging\Logger.cs:Zeile 99.
      10. bei System.Windows.Forms.Form.WmClose(Message& m)
      11. bei System.Windows.Forms.Form.WndProc(Message& m)
      12. bei System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
      13. ************** Geladene Assemblys **************
      14. mscorlib
      15. Assembly-Version: 4.0.0.0.
      16. Win32-Version: 4.0.30319.18444 built by: FX451RTMGDR.
      17. CodeBase: file:///C:/Windows/Microsoft.NET/Framework64/v4.0.30319/mscorlib.dll.
      18. ----------------------------------------
      19. TestApp
      20. Assembly-Version: 1.0.0.0.
      21. Win32-Version: 1.0.0.0.
      22. CodeBase: file:///C:/Intel/HS2/GameUtils/GameUtils/TestApp/bin/Release/TestApp.exe.
      23. ----------------------------------------
      24. GameUtils
      25. Assembly-Version: 2.0.0.0.
      26. Win32-Version: 2.0.0.0.
      27. CodeBase: file:///C:/Intel/HS2/GameUtils/GameUtils/TestApp/bin/Release/GameUtils.DLL.
      28. ----------------------------------------
      29. System.Windows.Forms
      30. Assembly-Version: 4.0.0.0.
      31. Win32-Version: 4.0.30319.18408 built by: FX451RTMGREL.
      32. CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System.Windows.Forms/v4.0_4.0.0.0__b77a5c561934e089/System.Windows.Forms.dll.
      33. ----------------------------------------
      34. System.Drawing
      35. Assembly-Version: 4.0.0.0.
      36. Win32-Version: 4.0.30319.18408 built by: FX451RTMGREL.
      37. CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System.Drawing/v4.0_4.0.0.0__b03f5f7f11d50a3a/System.Drawing.dll.
      38. ----------------------------------------
      39. System
      40. Assembly-Version: 4.0.0.0.
      41. Win32-Version: 4.0.30319.18408 built by: FX451RTMGREL.
      42. CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System/v4.0_4.0.0.0__b77a5c561934e089/System.dll.
      43. ----------------------------------------
      44. GameUtils.Input
      45. Assembly-Version: 1.0.0.0.
      46. Win32-Version: 1.0.0.0.
      47. CodeBase: file:///C:/Intel/HS2/GameUtils/GameUtils/TestApp/bin/Release/GameUtils.Input.DLL.
      48. ----------------------------------------
      49. GameUtils.Renderers.Gdi
      50. Assembly-Version: 1.0.0.0.
      51. Win32-Version: 1.0.0.0.
      52. CodeBase: file:///C:/Intel/HS2/GameUtils/GameUtils/TestApp/bin/Release/GameUtils.Renderers.Gdi.DLL.
      53. ----------------------------------------
      54. GameUtils.Renderers.Direct2D1
      55. Assembly-Version: 1.0.0.0.
      56. Win32-Version: 1.0.0.0.
      57. CodeBase: file:///C:/Intel/HS2/GameUtils/GameUtils/TestApp/bin/Release/GameUtils.Renderers.Direct2D1.DLL.
      58. ----------------------------------------
      59. GameUtils.Renderers.Direct2D1_1
      60. Assembly-Version: 1.0.0.0.
      61. Win32-Version: 1.0.0.0.
      62. CodeBase: file:///C:/Intel/HS2/GameUtils/GameUtils/TestApp/bin/Release/GameUtils.Renderers.Direct2D1_1.DLL.
      63. ----------------------------------------
      64. System.Core
      65. Assembly-Version: 4.0.0.0.
      66. Win32-Version: 4.0.30319.18408 built by: FX451RTMGREL.
      67. CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System.Core/v4.0_4.0.0.0__b77a5c561934e089/System.Core.dll.
      68. ----------------------------------------
      69. SharpDX.DXGI
      70. Assembly-Version: 2.5.0.0.
      71. Win32-Version: 2.5.0.
      72. CodeBase: file:///C:/Intel/HS2/GameUtils/GameUtils/TestApp/bin/Release/SharpDX.DXGI.DLL.
      73. ----------------------------------------
      74. SharpDX
      75. Assembly-Version: 2.5.0.0.
      76. Win32-Version: 2.5.0.
      77. CodeBase: file:///C:/Intel/HS2/GameUtils/GameUtils/TestApp/bin/Release/SharpDX.DLL.
      78. ----------------------------------------
      79. SharpDX.Direct2D1
      80. Assembly-Version: 2.5.0.0.
      81. Win32-Version: 2.5.0.
      82. CodeBase: file:///C:/Intel/HS2/GameUtils/GameUtils/TestApp/bin/Release/SharpDX.Direct2D1.DLL.
      83. ----------------------------------------
      84. SharpDX.Direct3D11
      85. Assembly-Version: 2.5.0.0.
      86. Win32-Version: 2.5.0.
      87. CodeBase: file:///C:/Intel/HS2/GameUtils/GameUtils/TestApp/bin/Release/SharpDX.Direct3D11.DLL.
      88. ----------------------------------------
      89. SharpDX.DirectInput
      90. Assembly-Version: 2.5.0.0.
      91. Win32-Version: 2.5.0.
      92. CodeBase: file:///C:/Intel/HS2/GameUtils/GameUtils/TestApp/bin/Release/SharpDX.DirectInput.DLL.
      93. ----------------------------------------
      94. mscorlib.resources
      95. Assembly-Version: 4.0.0.0.
      96. Win32-Version: 4.0.30319.18408 built by: FX451RTMGREL.
      97. CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/mscorlib.resources/v4.0_4.0.0.0_de_b77a5c561934e089/mscorlib.resources.dll.
      98. ----------------------------------------
      99. System.Windows.Forms.resources
      100. Assembly-Version: 4.0.0.0.
      101. Win32-Version: 4.0.30319.18408 built by: FX451RTMGREL.
      102. CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System.Windows.Forms.resources/v4.0_4.0.0.0_de_b77a5c561934e089/System.Windows.Forms.resources.dll.
      103. ----------------------------------------
      104. ************** JIT-Debuggen **************
      105. Um das JIT-Debuggen (Just-In-Time) zu aktivieren, muss in der
      106. Konfigurationsdatei der Anwendung oder des Computers
      107. (machine.config) der jitDebugging-Wert im Abschnitt system.windows.forms festgelegt werden.
      108. Die Anwendung muss mit aktiviertem Debuggen kompiliert werden.
      109. Zum Beispiel:
      110. <configuration>
      111. <system.windows.forms jitDebugging="true" />
      112. </configuration>
      113. Wenn das JIT-Debuggen aktiviert ist, werden alle Ausnahmefehler an den JIT-Debugger gesendet, der auf dem
      114. Computer registriert ist, und nicht in diesem Dialogfeld behandelt.
      Von meinem iPhone gesendet
      Sorry, der aktuelle Release ist noch etwas buggy. Der Fehler sollte verschwinden, wenn du in der Program.cs den Closing-Handler so abänderst:

      C#-Quellcode

      1. window.Closing += (sender, e) =>
      2. {
      3. keyboard.EndCapture();
      4. loop.Stop();
      5. logFile.Close();
      6. };​
      Also das Logfile als letztes schließen statt als erstes, denn in loop.Stop() wird noch darauf zugegriffen.