Engine Design

Remastered / Burntime 2

Moderator: maVado

Zimble
Beiträge: 40
Registriert: 25.06.2008, 09:14
Wohnort: Leipzig
Kontaktdaten:

Engine Design

Beitrag von Zimble »

Passend zu meinem Vorschlag bzgl. OpenDevelopment versuche ich mich mal an einem Beispiel-Post für das Engine-Framework. Das Ganze ist natürlich nicht komplett und soll erst einmal ein Anfang bzw. Beispiel sein.

Engine Framework - the bloddy basics

All code below is in C# for reference. Should be no problem to port this to another language.
The code is not ment to be a complete engine but rather the import excerpts.

Main class - engine.cs

Here we've got our main game loop. What it basicaly does is to tell the ScreenManager to render the current game screen, the AI-Subsystem to update NPC movements (and what else is going on) and the InputManager to reread attached devices (gamepad, keyboard, etc).
This is done in a loop as long as the game is running.

Code: Alles auswählen

while(!isShutingDown)
{
   ScreenManager.Render();
   AI.Update();
   Input.Update();
}
ScreenManager
This subsystem is responsible for rendering the screen to... urmmm... the screen.
A screen is what we actualy see on the screen. Could be also named 'a scene'. Typical examples are MainMenu, OptionsMenu, WorldView, CityView, Inventory, etc...

The ScreenManager has a stack of screens.
So if we're in the city and looking at our character (CityView) we push the CityView on top of the stack. If the player then opens the options menu or the inventory we push another screen on the stack (InventoryView).
Normaly only the top most screen of the stack is rendered by the ScreenManager. In this case we would only see the inventory, not the CityView underneath it. But we can set a screen as popup or inactive.
So when we push the inventory on the stack, we mark it as popup. This tells the ScreenManager to also render the previous screen (CityView) from the stack. And it marks the CityView as inactive thus not receiving any inputs from the InputManager.

Once the player closes the inventory, we pop the InventoryView from the stack, which sets the CityView back to active.

The Render() method which is called by the game loop just calls the Render() method from the topmost screen (maybe the previous screen if marked as popup). The screen's Render() method then does the actual rendering (placing sprites and stuff on the screen)

Code: Alles auswählen

   void PushStack(GameScreen screen);
   void PopStack();
   void Render();
tbc & rfc
juern
Moderator
Beiträge: 626
Registriert: 09.10.2005, 14:53
Wohnort: Mainz

Re: Engine Design

Beitrag von juern »

Ich bin gespannt auf die Fortfuehrung. C# als Referenz zu verwenden ist eine gute Wahl, ohne schnickschnack gut verstaendlich ;)
Zimble
Beiträge: 40
Registriert: 25.06.2008, 09:14
Wohnort: Leipzig
Kontaktdaten:

Re: Engine Design

Beitrag von Zimble »

Mal kurz etwas zu meinem bisherigen Code-Layout.
Im Bild ist mein Projekt zum testen und entwicklen meiner GUI-Lib zu sehen. Vielleicht kann ja jemand noch einige Informationen daraus ziehen.

Bild

Das Ganze ist in 3 Teile aufgeteilt.

Zum einen gibt es die Engine.
Im Bild beinhaltet diese lediglich die 4 Klassen, die ich zum testen benötige.
Zum einen die Engine.cs - Hier ist Grunde nur die Game-Loop, Start(), Stop() und die Init-Routine(Auflösung, etc.).
Die Scene.cs ist eine abstrakte Klasse. Der SceneGraph oder ScreenManager ist dafür verantwortlich, was auf dem Schirm angezeigt wird und in welchen Status sich das Spiel momentan befindet. Will ich bspw. das Hauptmenu des Spiels darstellen, erstelle ich mir eine Klasse, die von Scene.cs erbt (sieht man unten in der Demo-App) und schiebe sie in den SceneGraph (siehe vorheriger Post).

Die GUI-Klassenbibliothek
Es gibt eine abstrakte Component Klasse, in der Dinge wie Dimension, relative und absolute Koordinaten, etc. drinstehen.
Es gibt eine IContainer Interface, welches es erlaubt GUI-Elemente in andere GUI-Elemente zu stecken.
Dann gibt es die verschiedenen GUI-Elemente. Der Frame bspw. erbt von Component und implementiert IContainer. In den Frame kann man dann Buttons, etc. reinpacken.
Steckt man den Button in einen Frame, gibt man seinem Konstruktor eine Referenz auf den Parent-Frame mit. Der Button fügt sich dann selbst der Element-Liste des Parent-Frames hinzu. Rendert man den Frame, rendert dieser alle seine Child-Elemente. Jedes Child-Element holt sich beim Rendern die absoluten ScreenKoordinaten des Parents. Somit kann man Frame samt Inhalt bequem verschieben.
Aufruf wie gewöhnlich:

Code: Alles auswählen

            Window wndMBox = new Window(null);
            wndMBox.BackgroundColor = Color.FromArgb(128, Color.Red);
            wndMBox.Alpha = 128;
            wndMBox.Size = new Size(200, 100);
            wndMBox.Location = new Point((1024-200)/2, (768-100)/2);

            Label lblGameExit = new Label(wndMBox);
            lblGameExit.Text = "Exit the application?";
            lblGameExit.AutoSize = false;
            lblGameExit.Size = new Size(200, 20);
            lblGameExit.Centered = true;
            lblGameExit.Location = new Point(10, 30);

            gui.Controls.Add(wndMBox);
Jede Scene kann eine GUI haben. Ist eine GUI vorhanden werden erst alle anderen evtl. vorhandenen Objekte (Map, Player-Sprite, etc.) gerendert, danach die GUI obendrauf. In der GUI stecken die Frames, Buttons, etc.

Demo-App
Im Hautprogramm wird eine neue Engine-Instanz erzeugt. Dann werden zwei scenes erstellt - das Hauptmenu mit Hintergrund und GUI und eine Scene, die nur aus einer YES-NO-Messagebox besteht.
Die Hauptmenu-Scene wird in den SceneGraph der Engine geschoben. Danach wird die Engine gestartet. Diese rendert nun die Hauptmenu-Scene, obendrauf die entsprechende GUI und wartet auf Eingaben. Klickt der Nutzer auf ein GUI-Element, wird ein Event ausgelöst, welcher eine Instanz der YESNO-Messagebox-Scene erstellt, diese als Popup setzt und in den SceneGraph schiebt.
Nun werden beide Scenes gerendert (weil Popup). Drückt der User auf Exit:No, wird die Messagebox einfach aus dem SceneGraph entfernt und es wird wieder lediglich die verbleibende HauptMenu-Scene gerendert.


So im groben hab ich mir das bisher zusammengebaut.
In dem Engine-Screenshot oben fehlen nun noch die ganzen verschiedenen Objects (Weapon, Player, NPC) und die Helper-Dinge wie Pfadfindung etc. Das steckt noch in einem anderen Projekt. Aber dazu ein andermal.


Edit: Narf. Deutsch-Englisch-Schwäche ;)
juern
Moderator
Beiträge: 626
Registriert: 09.10.2005, 14:53
Wohnort: Mainz

Re: Engine Design

Beitrag von juern »

Hat es einen besonderen Grund, dass du den Container als Interface realisierst?
Mir faellt da z.B. unterschiedliche Autolayout-Verhalten ein, aber waere nicht
eine abstrakte Klasse nicht auch von Vorteil, wenn es z.B. darum geht CustomControls
zu schreiben?
Mich interessiert dabei die Designentscheidung dahinter, nicht was jetzt nun besser ist ;)
Zimble
Beiträge: 40
Registriert: 25.06.2008, 09:14
Wohnort: Leipzig
Kontaktdaten:

Re: Engine Design

Beitrag von Zimble »

Mmh, ich hätte den Thread vielleicht nicht Engine Design nennen sollen. Denn auf die schnelle fällt mir da jetzt keine Designentscheidung ein. :D

So gesehen sieht das in der Tat besser aus: (abstract)Component->(abstract)Container->Frame->Window

Ich guck da morgen mal in den Code, vielleicht springt mich dann die Erkenntnis an.
juern
Moderator
Beiträge: 626
Registriert: 09.10.2005, 14:53
Wohnort: Mainz

Re: Engine Design

Beitrag von juern »

Da ich gerade an Vorbereitungen fuer das PathFinding arbeite, dachte ich mir ich poste mal Beispielcode wie Burntime so aufgebaut ist.

Es handelt sich um zwei Klassen: SimpleMind und SimplePath.
Sie sind fuer die Steuerung eines NPCs gedacht.

SimplePath ist ein einfaches PathFinding, dass bei Hindernissen einfach stehen bleibt. SimpleMind ist eine einfache Steuerung wo der NPC nur wahllos umher rennt. Beides sind abgeleitete Klassen von einer Mind-Basisklasse bzw. PathFinding-Basisklasse. Dadurch lassen sich bequem Eigenschaften im Verhalten von NPCs einstellen und austauschen.

SimpleMind

Code: Alles auswählen

    class SimpleMind : CharacterMind
    {
        protected float takeSomeRest = 0;

        public override void Process(Character character, float elapsed)
        {
            // we are taking some rest
            if (takeSomeRest > 0)
            {
                takeSomeRest -= elapsed;
                return;
            }

            // reached its goal, make some new decisions
            if (character.Position == character.Path.MoveTo)
            {
                // decide to take some rest or go somewhere else 1:9
                if (Burntime.Platform.Math.Random.Next() % 9 == 0)
                {
                    // rest for about 2 seconds
                    takeSomeRest = 2;
                }
                else
                {
                    // just go somewhere nearby totally random
                    Vector2 go;
                    go.x = Burntime.Platform.Math.Random.Next() % 101 - 50;
                    go.y = Burntime.Platform.Math.Random.Next() % 101 - 50;

                    character.Path.MoveTo = character.Position + go;
                }
            }
        }
    }
SimplePath

Code: Alles auswählen

    public class SimplePath : PathState
    {
        protected Vector2f moveTo;
        protected Vector2f position;

        public override Vector2 MoveTo
        {
            get { return moveTo; }
            set { moveTo = value; }
        }

        public override Vector2 Process(PathMask mask, Vector2 position, float speed)
        {
            this.position = position;
            Vector2f dif = moveTo - this.position;

            // quit if goal is reached
            if (dif.Length < 0.1f)
                return position;

            // calculate distance
            if (dif.Length > speed)
            {
                dif.Normalize();
                dif *= speed;
            }

            // calculate new position
            Vector2f newpos = this.position + dif;

            // transform to map mask position
            Vector2 maskpos = ((Vector2)newpos + mask.Resolution / 2) / mask.Resolution;

            // set new position if there is no obstacle
            if (mask[maskpos])
                this.position = newpos;
            else // otherwise just cancel walking
                moveTo = this.position;

            return this.position;
        }

        public override void DebugRender(Burntime.Platform.Graphics.RenderTarget target)
        {
            if (position != moveTo)
                target.RenderLine(position, moveTo, new PixelColor(255, 0, 0));
        }
    }