Entwickler-Ecke

Alle Sprachen - Alle Plattformen - Entwicklung einer geeigneten Architektur einer Game Library


Kasko - Do 27.12.18 02:54
Titel: Entwicklung einer geeigneten Architektur einer Game Library
Ich muss für die Uni eine einfache Game Library erstellen.

Inhalt:
AssetHandler -> laden von Media-Elementen in den Speicher, und auslesen dieser mittels key aus einer map
Application -> Klasse welche das Spiel mit Main-Loop und Fenster enthält
Input -> Klasse die Input-Events aus dem Window-EventBuffer enthält um jederzeit auf die Events Pressed, Hold und released zu prüfen, nicht nur in den Callbacks
StateMachine -> Enthält einen stack an states, nur der oberste state wird bearbeitet. Ein State ist beispielsweise ein Screen(Splash-Screen) oder ein Level
Basis Object-Hierarchie mit Objecten und Komponenten angelehnt an die Unity-Hierarchie

Meine momentane Struktur (problematisch)

StateMachine ist eine abstrakte Klasse die an Application vererbt wird. Sie beinhaltet den privaten Stack und die öffentlichen Methoden getActiveState, addState(State state, bool replaceCurrent = true), removeCurrentState. Application beinhaltet einen Event-Loop und einen Update-Render-Loop, die in zwei unterschiedlichen Threads laufen.

Ein State ist eine abstrakte Klasse, welche eine Liste der übergeordnetsten GameObjects beinhaltet, also alle Objects die die Basis/Wurzel des Hierarchie-Baumes bilden und keinen Parent haben. Zudem die Methoden update, fixedUpdate und render. Update wird einmal pro Update-Render-Loop-Entry aufgerufen, fixedUpdate und render nach festen, voneinander unabhängigen Zeiten. Standard ist bei beiden 60mal die Sekunde. Gesetzt werden können diese Werte durch die Time-Klasse. Der Benutzer der Library soll also eigene States mit dieser Klasse als Basis erstellen.

GameObject ist eine finale/sealed Klasse welche lediglich als Container für die Komponenten dient. Dementsprechend besitzt diese Klasse nur die Liste an Komponenten und die Methoden update, fixedUpdate und render, welche lediglich die namentlich gleichen Methoden aller Komponenten aufrufen. Um die Baum-Hierarchie aufzuspannen besitzt sie zudem eine Referenz zum Parent und eine Liste aller Child-Objects

Component ist eine abstrakte Klasse, die die Basis aller Komponenten darstellt und beinhaltet die drei Methoden update, fixedUpdate und render sowie eine Referenz zum zugehörigen GameObject. Die Komponente bildet den Kern des Spiels, da die meisten Verhaltensbehandlungen innerhalb der Komponenten oder zwischen ihnen ablaufen. Der Rest wird meist komplett vom State behandelt.

Alle Elemente der Hierarchie (App->StateMachine->State->Object->Component) bis auf die Beziehung Object<->Component sind in sich abgeschlossen und führen daher keine Informationen darüber, wer sie verwendet. Ein State weiß daher nicht in welcher StateMachine er sich befindet und ein Object weiß nicht in welchem State er sich befindet. Nur die Information wer das Parent-Object ist wird dem Object im Konstruktor übergeben.

Diese Architektur finde ich aber nicht mehr gut und möchte sie ändern
1. Die render-Methode soll entfernt werden und ein zentraler Renderer in der Application verwendet werden. Dadurch muss sich nicht jedes Element selbst darum kümmern gezeichnet zu werden. Es soll reichen bestimmte Komponenten zu besitzen und schon regelt es die Library. Dafür sollen renderbare Komponenten bereitgestellt werden, die jedes Object dann verwenden kann.

2. Es soll eine Camera-Komponente eingebaut werden, wodurch Verhalten, wie Bewegung oder Zoom deutlich vereinfacht werden soll. Zudem können durch mehrere Kameras mehrere Schichten oder auch Splitscreens gezeichnet werden können.

Dem gegenüber stehen aber einige Probleme, weshalb ich diesen Beitrag schreibe.
1. Schlicht und einfach Kosteneffizienz -> wird in 2. erklärt
2. Da ein GameObject nicht weiß in welchem State es sich befindet (und das soll wenn möglich auch so bleiben) kann die Camera-Komponente das Rendern nicht selbst übernehmen, da es nicht auf alle GameObjects zugreifen kann, da sich die GameObjects ohne parent nicht gegenseitig ansprechen können. Daher muss der Renderer über den GameObjects liegen also auf der Ebene eines States oder der StateMachine. Daher würde ich ihn direkt in die Application einbauen. Aber das Problem ist, dass dann erst nach allen Cameras gesucht werden muss, bevor aus ihrer Sicht dann nach und nach gerendert wird, was verhältnismäßig nicht effizient ist.

Daher frage ich ob ihr Ideen habt wie man dies lösen kann oder wie man die Library umstrukturieren kann um einen zentralen Renderer mit mehreren Kameras umzusetzen, welcher halbwegs effizient arbeitet.


Symbroson - Do 27.12.18 16:47

Hallo Kasko,

Deine Struktur ist sehr gut finde ich. Ich habe auch mal für einen kurzen Zeitraum so ein Projekt in sehr abgespeckter Form unzusetzen (ohne states und nur eine Kamera). Dort hatte ich aber auch eine Render-funktion die auf Basis der aktuellen Kamera die Objekte (des aktuellen States) gerendert hat. Das Problem mit mehreren Kameras hatte ich also noch gar nicht im Blick, ist jedoch durchaus interessant.

Wie wäre es denn jedem State eine Liste an verwendeten Kameras hinzuzufügen aufgrund derer der Renderer dann aus jeder Perspektive den State rendert? Die Kamera bräuchte dann jeweils auch Variablen für das Zielfeld in welches die Szene gezeichnet wird und eine 'active'-Eigenschaft um inaktive Kameras zu überspringen (oder eine weitere Liste mit den aktuell verwendeten, wobei es insgesamt nicht so viele Kameras pro State werden sollten, dass das nötig wäre)

Grüße,
Symbroson


Kasko - Do 27.12.18 18:02

Über diese Liste hab ich auch schon nachgedacht, allerdings wird dies nicht immer funktionieren. Das habe ich vergessen zu erwähnen. Jedes GameObject hat die Komponenten-List-Manipulationsmethoden addComponent, getComponent<T>, getComponents<T>, getComponents, removeComponent<T> und removeComponents<T>, da die Liste private ist. Da sich GameObjects innerhalb einer Hierarchie gegenseitig ansprechen können, können sie sich auch gegenseitig Komponenten hinzufügen, unter anderem auch Kameras. Wenn Kameras auf diesem Weg hinzugefügt werden, kann auf keinem Wege dem State diese Kamera übergeben werden. Dadurch ist diese Kamera nutzlos, es sei denn der State würde jeden Frame prüfen ob neue Kameras auf anderem Wege hinzugefügt wurden, wobei wir wieder bei der Effizienz wären.


Symbroson - Do 27.12.18 18:17

Warum hast du denn die Kameras als Kinder von Game-Objekten implementiert? Ich wüsste auch nicht warum sich Objekte gegenseitig Kameras hinzufügen sollten. Das ist doch eigentlich eine Frage der Spiel-Logik.

Also Kameras hätte ich als Liste innerhalb der State-Klasse implementiert die ggf von der Spiel-Logik geändert werden kann.
Die Positionierung und Ausrichtung kann dann vor dem Rendern einer Kamera relativ zu einem Game-Objekt durch eine Referenz auf dieses in der Kamera-Klasse geschehen


Kasko - Do 27.12.18 20:00

Der Grund weshalb die Kamera ein "Child" der GameObjects ist, also eine Komponente, ist ganz einfach. Eine Komponente soll kein alleinstehendes Object sein, sondern nur eine Funktionalität, eine Eigenschaft, die ein Object besitzen kann und es zu dem macht was es sein soll.

Ein einfaches Beispiel zur Erklärung mit Quellcode der Umsetzung in C#:
Ich möchte einen UI Button erstellen. Ein Button hat normalerweise zwei Elemente. Eine graphische Oberfläche und eine Möglichkeit den Click zu erkennen und Methoden auszuführen die auf Click ausgeführt werden sollen. Aber anstatt immer komplett neue Klassen zu erstellen, die immer mal wieder die selben Basiselemente besitzen, sollte mal lieber mehrere Komponenten, also Funktionalitäten, vereinen. So z.B. mit dem Button. Ich nehme eine Komponente Image und eine Komponente Button. Image ist eine rendererbare Komponente und beinhaltet die Bounds und die Grafik des Bildes. Die Komponente Button kümmert sich nur um das erkennen und behandeln von Clicks. Da man vielleicht eine graphische Rückmeldung bei hover und click haben möchte, sollte ein Button auf die Image Komponente zugreifen und wenn keine vorhanden ist eine hinzufügen können. Deshalb ist die Möglichkeit gegeben das sich GameObjects untereinander Komponenten hinzufügen können

Umsetzung:
Image:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
public class Image : Component {
    public Sprite sprite { get; set; }

    public Texture texture {
        get { return sprite.Texture; }
        set { sprite.Texture = value; }
    }

    public Image(GameObject gameObject) : base(gameObject) {

    }

    public void SetWidth(float width, bool lockRelation) {
        float factor = width / texture.Size.X;
        sprite.Scale = new Vector2f(factor, lockRelation ? factor : sprite.Scale.Y);
        CenterOrigin();
    }

    public void SetHeight(float height, bool lockRelation) {
        float factor = height / texture.Size.Y;
        sprite.Scale = new Vector2f(lockRelation ? factor : sprite.Scale.X, height);
        CenterOrigin();
    }

    public void CenterOrigin() { sprite.Origin = new Vector2f(texture.Size.X / 2, texture.Size.Y / 2); }

    public override void Render(RenderWindow window) {
        window.Draw(sprite);
    }
}



Button:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
public class Button : Component {
    private Image image;

    bool hover = false;
    bool pressed = false;
    public Vector2f defaultScale, hoverScale;

    public List<Action> onClickActions = new List<Action>();

    public Button(GameObject gameObject) : base(gameObject) {
        if (gameObject.GetComponent<Image>() == null)
            gameObject.AddComponent(new Image(gameObject));

        image = gameObject.GetComponent<Image>();

        defaultScale = new Vector2f();
        hoverScale = new Vector2f();
    }

    public override void Update() {
        if (image.sprite.GetGlobalBounds().Contains(Input.GetMousePosition().X, Input.GetMousePosition().Y) && pressed) {
            image.sprite.Scale = defaultScale;
            hover = true;
        } else if (image.sprite.GetGlobalBounds().Contains(Input.GetMousePosition().X, Input.GetMousePosition().Y) && !pressed) {
            image.sprite.Scale = hoverScale;
            hover = true;
        }
        else {
            image.sprite.Scale = defaultScale;
            hover = false;
        }

        if (Input.GetMouseButtonDown(Mouse.Button.Left) && hover)
            pressed = true;

        if (Input.GetMouseButtonUp(Mouse.Button.Left) && hover && pressed) {
            pressed = false;

            foreach (Action a in onClickActions)
                a.Invoke();
        }
        else if (Input.GetMouseButtonUp(Mouse.Button.Left) && !hover && pressed)
            pressed = false;
    }
}


Eine Kamera soll auch nur eine Funktionalität sein. Sie beinhaltet nur Informationen über das Sichtfeld und wie gerendert werden soll. Position und Rotation/Ausrichtung beinhaltet das GameObject, denn nur ein GameObject kann im Raum orientiert werden, ob nun 2d wie bei mir oder 3d. Die Kamera-Komponente soll ein Object zu einer Kamera machen und durch bewegen dieses Objektes soll ein Kamerafahrt oder Zoom möglich sein. Sie soll nicht selbst eine Kamera sein.


Symbroson - Do 27.12.18 20:49

Eine Kamera ist doch aber keine 'Komponente' eines Game-Objekts! Sie beeinflusst doch nur die Perspektive auf die Game-Objekte in der der Spieler auf die Szene schaut. Es gibt nicht für jedes Objekt eine Kamera, sondern ein (oder mehrere bei genannten Features wie zB Split-Screen) Kameras für alle Obiekte einer Szene, die während des Renderns auf die Objekte angewendet wird, bevor sie auf den Buffer gezeichnet werden

Normalerweise wird diese Kameratransformationsmatrix auch erst im Shader auf die einzelnen Vektoren der Game-Objekte angewendet. Zumindest macht man das so in OpenGL, ich weiß jetzt nicht welche Bibliothek du verwendest, aber das grundlegende Prinzip sollte schon dasselbe sein. (auch ohne shader)

Die Positionierung und Ausrichtung lann dann wie in meinem vorherigen Post beschrieben erfolgen


Kasko - Do 27.12.18 22:39

Ich glaube ich muss noch einmal ein paar Unstimmigkeiten zu Begriffen klar stellen und etwas zum rendering der von mir genutzten Bibliothek sagen. Zunächst nochmal zur Komponente und zur Kamera. Darauf aufbauend dann zum rendering.

Bitte verwechselt die Namensgebung der Klasse Component nicht mit der handelsüblichen Definition für Komponente, also Teilkonstrukt oder Bestandteil. Eine Komponente in meiner Bibliothek ist nichts weiter als eine Eigenschaft, eine Funktionalität, ein möglicher Bestandteil, der einem Objekt hinzugefügt werden KANN aber NICHT MUSS. Als Basis besitzt ein GameObject nur EINE Komponente, die Transform-Komponente, welche Informationen über Position, Rotation und Skalierung, sowie das Parent und die Children beinhaltet. Alle anderen Informationen sind möglich aber nicht von vornherein gegeben. Es hat also nicht jedes Objekt einen Bestandteil namens Kamera, sondern nur ein Objekt (oder wenn mann mehrere Kameras möchte vllt auch zwei oder drei) besitzt die Eigenschaft/Komponente Camera und wird dadurch zu einer Kamera.

Nun zum Rendering. Ich verwende die 2D Multimedia Bibliothek SFML. Diese stellt bereits ein Fenster bereit, welches draw-Methoden besitzt um etwas auf das Fenster zu zeichnen. Diese Klasse heißt RenderWindow. Sie beinhaltet einen Member der Klasse View. View ist nichts weiter, als ein Rechteck. Das was sich innerhalb diese Rechteckes befindet wird auf das Fenster gezeichnet. Dabei wird der Inhalt des Rechteckes so skaliert, dass es dem Fenster angepasst ist, also entweder Höhe oder Breite der des Fensters entsprechen. Es können dadurch schwarze Ränder in einer der beiden Richtungen also horizontal oder vertikal entstehen aber nie bei beiden. Diese Ränder entstehen wenn das Rechteck nicht das selbe Format hat, wie das Fenster. Wenn man also ein Rechteck nimmt, das kleiner ist als das Fenster, dann zoomt man da ein kleiner Ausschnitt auf die Größe des Fensters skaliert wird. Die Kamera-Komponente würde also nicht mehr enthalten als ein View-Member der vor dem Rendern dem Fenster zugewiesen wird. Dies kann auch mehrmals mit unterschiedlichen Views gemacht werden, da es sich nach dem Rendern nur noch um Pixel handelt, die von keiner View geändert werden können auch wenn sich die View ändert. Man sieht also, dass etwas wie Transformationsmatrizen und deren Multiplikation mit den Normalen von z.b. 3D-Objekt-Polygonen, wie bei z.B. der Spezifikation OpenGL nicht notwendig sind, da es sich um eine 2D Bibliothek handelt und nur 2D Elemente, die alle auf einer Ebene liegen und nur um die Tiefenachse rotiert werden können, gezeichnet werden.


Symbroson - Do 27.12.18 22:51

Auch wenn es eine 2D Bibliothek ist bin ich der Meinung, dass eine Implementation der Kamaras als möglicher Bestandteil von Game-Objekten ungünstig ist und eine Implementation der Kameras als ein Bestandteil der State-Klasse mit Referenzen auf das sich bezogene Game-Objekt oder nur die jeweilige Transform-Komponente wenn dir das besser gefällt, besser geeignet ist, da du somit die 'Suche' nach Kameras umgehst.


[Edit:]

Vielleicht reden wir auch im Begriff der Kamera aneinander vorbei. Ich verstehe darunter wie bereits gesagt die Spieleransicht. In 2D würde diese eine Position auf dem Spielfeld in welcher Art auch immer beinhalten, einen Zoom-Faktor und außerdem ggf. Rotation um die Z-Achse. In 3D kommt dann noch Perspektive hinzu, das spielt aber hier keine Rolle.
Diese Parameter müssen dann während des Render-Vorgangs auf alle Game-Objekte angewendet werden.

Zur Implementation habe ich ja auch schon etwas gesagt.


jaenicke - Fr 28.12.18 07:35

Unabhängig von der Implementierung möchte ich nur ein paar Worte zur Benennung sagen:
StateMachine erinnert insbesondere in dem Umfeld doch sehr an einen endlichen Automaten (finite state machine). Insofern würde ich die Bezeichnung nicht benutzen, wenn dahinter nicht auch das entsprechende Prinzip steckt. Und das ist, wenn ich das richtig verstanden habe, hier nicht der Fall.