Tutorial #00: Hello, World!

Im ersten Tutorial wird eine Minimal-Version einer Murl-Applikation erstellt. Das Ergebnis ist ein leeres Fenster. Dieses Grundgerüst wird für jede Murl-Applikation benötigt und zeigt das grundsätzliche Zusammenspiel zwischen Framework und User Code.

Wenn du deine Host-Umgebung noch nicht für die App-Entwicklung mit der Murl Engine konfiguriert hast, solltest du kurz im Quick Start Guide vorbeischauen bevor du mit den Tutorials fortfährst.

Die Dateien für die Tutorial-Beispiele liegen im Ordner tutorials/chapter01/00_hello_world/project. Die zugehörigen Projektdateien können in den Unterordnern für die jeweilige Zielplattform gefunden werden, beispielsweise win32/vs2008/hello_world.sln für Visual Studio 2008 oder osx/xcode3/hello_world.xcodeproj für Apple XCode 3.x / 4.x. Diese Projektdateien enthalten eine oder mehrere verschiedene Zielplattformen (V1 bis V3) entsprechend der unten angeführten Versionen.

Version 1: Murl Engine und App Code

Application (App) = murl engine + user code

Für unsere Minimal-Version benötigen wir drei Dateien:

  • hello_world.cpp : Connection between our app and the murl engine
  • hello_world_app.cpp : C++ file of our app class
  • hello_world_app.h : Header file of our app class

Die Verbindung zwischen der Murl engine und dem eigenen App Code ist in der Header-Datei murl_app.h definiert, welche Teil der Frameworks-API ist. Darin sind zwei externe Funktionen im Namensbereich Murl::App deklariert.

#ifndef __MURL_APP_H__
#define __MURL_APP_H__

#include "murl_app_types.h"

namespace Murl
{
    class IApp;
    
    namespace App
    {
        extern IApp* CreateApp();
        extern void DestroyApp(IApp* app);
    }
}

#endif // __MURL_APP_H__

Beim Starten der Applikation wird vom Multimedia Framework die Funktion CreateApp() aufgerufen, welche in unserem User-Code eindeutig definiert sein muss. Als Rückgabeparameter wird ein Objekt vom Typ IApp erwartet.

Beim Beenden der Applikation wird vom Multimedia Framework die Funktion DestroyApp() aufgerufen. Als Parameter wird das zuvor erhaltene IApp-Objekt übergeben.

Eine neue Applikation muss diese beiden Funktionen im vorgegebenen Namespace Murl::App implementieren. In unserem Fall implementieren wir diese Funktionen in der Datei hello_world.cpp

#include "murl_app.h"
#include "hello_world_app.h"

using namespace Murl;

In den ersten beiden Zeilen werden mit #include die Header-Datei murl_app.h und die Header-Datei für unsere App-Klasse inkludiert. Mit using namespace wird der zuoberst liegende Murl Namespace referenziert. Dadurch können alle Klassen, Variablen und Funktionen aus dem Namespace Murl direkt angesprochen werden.

IApp* App::CreateApp()
{
    return new HelloWorldApp;
}

Die Funktion CreateApp() erwartet als Rückgabewert ein IApp-Objekt. Mit new HelloWorldApp wird eine neue Instanz der Klasse HelloWorldApp (welche das Interface IApp implementieren muss) erzeugt. Mittels return wird diese Instanz als Rückgabeparameter an das Multimedia Framework übergeben.

void App::DestroyApp(IApp* app)
{
    Util::Release(app);
}

Die Funktion DestroyApp() wird beim Beenden der App vom Multimedia Framework aufgerufen. Mit Util::Release() wird der Destruktor unserer vorher übergebenen HelloWorldApp-Instanz aufgerufen und der belegte Speicher wieder freigegeben.

Generell wird die Verwendung der Funktion Util::Release() anstelle von delete dringend empfohlen, da diese Funktion für den übergebenen Zeiger vor der Speicherfreigabe eine Prüfung auf ungleich 0 durchführt und den Zeiger auf 0 setzt nachdem das Objekt freigegeben wurde.

App-Klasse

In der Header-Datei hello_world_app.h wird die Klasse HelloWorldApp im Namespace Murl::App deklariert. Für die Klasse werden Konstruktor, Destruktor und die notwendigen Methoden für das implementierte Interface IApp deklariert. Um nicht alle Methoden aus dem Interface IApp implementieren zu müssen, leiten wir von der abstrakten Klasse AppBase ab, welche auch vom API Framework bereitgestellt wird. Diese Klasse leitet bereits vom Interface IApp ab und beinhaltet Leerimplementierungen für die meisten Methoden.

#ifndef __HELLO_WORLD_APP_H__
#define __HELLO_WORLD_APP_H__

#include "murl_app_base.h"

namespace Murl
{
    namespace App
    {
        class HelloWorldApp : public AppBase
        {
        public:
            HelloWorldApp();
            virtual ~HelloWorldApp();

            virtual Bool Configure(IEngineConfiguration* engineConfig, IFileInterface* fileInterface); 
            virtual Bool Init(const IAppState* appState);
            virtual Bool DeInit(const IAppState* appState);
        };
    }
}

#endif  // __HELLO_WORLD_APP_H__

In der Datei hello_world_app.cpp werden die deklarierten Methoden definiert. In unserem Fall bleiben die Funktionskörper von Konstruktor und Destruktor leer. Die beiden Funktionen Init() und DeInit() geben immer true zurück.

Mit der Configure()-Methode legen wir die Konfiguration für die App fest. Mit dem übergebenen IEngineConfiguration-Objekt kann das generelle Verhalten der Engine ausgelesen und verändert werden. Analog können mit dem IPlatformConfiguration-Objekt allgemeine plattformspezifische Einstellungen abgefragt werden, und das IAppConfiguration-Objekt kann benutzt werden um App-spezifische Einstellungen vorzunehmen. Auf beide kann über das IEngineConfiguration-Objekt zugegriffen werden.

Wir setzen mit SetWindowTitle() einen Fenstertitel für das Programmfenster und wählen mit SetFullScreenEnabled(false) den Fensteranzeigemodus statt dem Vollbildanzeigemodus. Auf Mobilgeräten gibt es üblicherweise keinen Fenstermodus, weshalb dieser Aufruf keine Auswirkung auf z.B. iOS oder Android hat. Eine genaue Auflistung aller Parameter findet sich in der Interface Deklaration:

  • murl/base/include/engine/murl_i_engine_configuration.h
  • murl/base/include/engine/murl_i_app_configuration.h
  • murl/base/include/engine/murl_i_plattform_configuration.h
Zu beachten
Tipp: Durch Drücken der rechten Maustaste über dem Datei- oder Klassennamen und Auswahl von "Go to Definition" kann in Visual Studio (wie bei "Jump to Definition" in XCode) auf schnelle und einfache Weise die zugehörige Datei geöffnet werden.
#include "hello_world_app.h"

using namespace Murl;

App::HelloWorldApp::HelloWorldApp()
{
}

App::HelloWorldApp::~HelloWorldApp()
{
}

Bool App::HelloWorldApp::Configure(IEngineConfiguration* engineConfig, IFileInterface* fileInterface)
{
    IAppConfiguration* appConfig = engineConfig->GetAppConfiguration();

    appConfig->SetWindowTitle("Hello World powered by Murl Engine");
    appConfig->SetFullScreenEnabled(false);

    return true;
}

Bool App::HelloWorldApp::Init(const IAppState* appState)
{
    return true;
}

Bool App::HelloWorldApp::DeInit(const IAppState* appState)
{
    return true;
}

Damit ist die Minimalversion einer Applikation fertig. Das Ergebnis ist ein leeres Fenster mit nicht initialisiertem Inhalt. Bei Windows erhält man ein schwarzes Fenster, während bei Mac OSX ein zufälliger Inhalt im Fenster zu sehen ist. Um einen einfachen Text anzeigen zu können, laden wir als nächstes das "debug"-Paket.

Um Version 1 des "Hello World"-Tutorials in Visual Studio zu erstellen und zu starten, kann einfach mit der rechten Maustaste auf das hello_world_v1-Target im Solution Explorer geklickt, "Set as StartUp Project" im Pop-Up-Menü ausgewählt und F5 gedrückt werden. In XCode4, wird die "hello_world_v1-Konfiguration in der Titelleiste ausgewählt und auf "Run" geklickt.

tut0100_empty_window.png
Ausgabefenster von Hello World V1

Version 2: Debug Package

Bei der Murl Engine handelt es sich um ein Szenengraphen-Framework. Der Szenengraph ist eine objekt-orientierte Datenstruktur, welche die aktuell darzustellende Szene beinhaltet. Bisher wurde nur eine leere Klasse erzeugt, die das Interface IApp implementiert. Eine Instanz dieser Klasse wurde dem Multimedia Framework übergeben. Der Szenengraph selbst ist leer geblieben und damit auch die Anzeige.

Bei der Murl Engine werden Szenen üblicherweise in eigenen XML Files definiert, welche in einem oder mehreren Paketen enthalten sein müssen. Um nun eine Szene als Teil eines Szenegraphen zu verwenden, kann die App diese Pakete in den Speicher laden und die darin enthaltenen Graphen instanzieren. Ein solches Paket namens "debug.murlpkg" steht sofort nach dem Anlegen eines neuen Projekts zur Verfügung, da es direkt in die Murl Engine eingebaut ist.

Diesem "debug"-Paket kommt eine spezielle Bedeutung zu, da damit genaue Informationen über die Zeichengeschwindigkeit und einfache User Debug Messages angezeigt werden können.

Zu beachten
Achtung! Das Debug-Paket muss systembedingt immer als erstes Paket geladen werden!

Damit wir dem leeren Szenengraphen neue Knoten hinzufügen können, müssen wir diese zunächst laden. Für das Laden stellt die Murl Engine das ILoader-Objekt mit seiner Methode AddPackage() zur Verfügung.

Das Laden erfolgt üblicherweise in der Methode Init(), die von der Murl Engine beim Starten der App aufgerufen wird. Einen Zeiger auf das ILoader-Objekt können wir vom übergebenen IAppState-Objekt mit der Methode GetLoader() holen.

Bool App::HelloWorldApp::Init(const IAppState* appState)
{
    ILoader* loader = appState->GetLoader();
    loader->AddPackage("debug", ILoader::LOAD_MODE_STARTUP);
    return true;
}

Die neuen Knoten im Szenengraphen bewirken eine Anzeige von entwicklungsrelevanten Informationen an der linken oberen Ecke, wie zum Beispiel die aktuellen Frames pro Sekunde.

tut0100_debug_package.png
V2 Ausgabefenster mit Debug-Statistiken

Um Version 2 des Hello World Tutorials in Visual Studio zu erstellen und zu starten, wird das hello_world_v2-Target als Start-Up-Project festgelegt und F5 gedrückt. In XCode4, wird die "hello_world_v2-Konfiguration ausgewählt und auf "Run" geklickt.

Version 3: Logik-Klasse

Um vom ILoader-Objekt Informationen darüber zu erhalten, wann die Szene fertig geladen ist und um die Szene gezielt manipulieren zu können, kann der Methode loader->AddPackage() ein Objekt vom Typ Logic::IEngineProcessor übergeben werden. Das Interface IEngineProcessor deklariert Methoden, die bei bestimmten Ereignissen von der Murl Engine aufgerufen werden, damit die Anwendung entsprechende Änderungen am Graphen vornehmen kann.

Um ein Logic::IEngineProcessor-Objekt zu erstellen, könnten wir eine neue Klasse definieren, die das Interface implementiert. Die einfachere und empfohlene Vorgehensweise ist aber, von der Basisklasse Logic::BaseProcessor abzuleiten. Diese Klasse beinhaltet ein Logic::IProcessor-Objekt, welches das Logic::IEngineProcessor-Interface implementiert, und bietet nützliche Zusatzfunktionalität.

  • murl/base/include/engine/logic/murl_logic_i_engine_processor.h
  • murl/base/include/engine/logic/murl_logic_base_processor.h

Für die neue Klasse benötigen wir zwei weitere Dateien:

  • hello_world_logic.h : Header file of our new class
  • hello_world_logic.cpp : C++ file of our new class

In der Datei hello_world_logic.h deklarieren wir die neue Klasse HelloWorldLogic, welche von der Basisklasse BaseProcessor abgeleitet ist.

#ifndef __HELLO_WORLD_LOGIC_H__
#define __HELLO_WORLD_LOGIC_H__

#include "murl_app_types.h"
#include "murl_logic_base_processor.h"

namespace Murl
{
    namespace App
    {
        class HelloWorldLogic : public Logic::BaseProcessor
        {
        public:
            HelloWorldLogic(Logic::IFactory* factory);
            virtual ~HelloWorldLogic();

Der Konstruktor fordert als Parameter einen Pointer auf ein Logic::IFactory-Objekt. Dieses Objekt wird für den Konstruktor der Basisklasse benötigt.

Zusätzlich überschreiben wir die virtuelle Methode OnInit() der Basisklasse.

        protected:
            virtual Bool OnInit(const Logic::IState* state);
        };
    }
}

#endif  // __HELLO_WORLD_LOGIC_H__

In der Datei hello_world_logic.cpp nehmen wir die Implementierung der neuen Klasse vor:

#include "hello_world_logic.h"

using namespace Murl;

App::HelloWorldLogic::HelloWorldLogic(Logic::IFactory* factory)
: BaseRootProcessor(factory)
{
}

App::HelloWorldLogic::~HelloWorldLogic()
{
}

Der Destruktor und der Konstruktor der neuen Klasse bleiben leer. Mit dem übergebenen Zeiger auf das IFactory-Objekt wird der Konstruktor der Basisklasse aufgerufen.

Die überschriebene Methode OnInit() wird genau einmal aufgerufen, nachdem das dazugehörige Paket fertig geladen wurde. Diese Methode sollte für die Fehlerprüfung und Initialisierung des Graphen verwendet werden.

Bool App::HelloWorldLogic::OnInit(const Logic::IState* state)
{
    if (!AreGraphNodesValid())
    {
        return false;
    }

    state->SetUserDebugMessage("Hello World");  
    return true;
}

Die Methode AreGraphNodesValid() prüft ob alle Referenzen im Graph gefunden wurden und gibt anderenfalls eine Fehlermeldung aus. (Bis jetzt haben wir noch keine Referenzen abgefragt, somit wird immer true zurückgegeben.)

Als Parameter wird der OnInit()-Methode ein Zeiger auf ein Logic::IState- Objekt übergeben. Das IState-Objekt ist die zentrale Sammelstelle für alle relevanten Informationen wie z.B. Tick Time, Graphenknoten, Konfiguration etc. Außerdem können mit den Methoden SetUserDebugMessage() bzw. AddUserDebugMessage() einfache Debug-Meldungen am Bildschirm ausgegeben werden.

Hello World Message

Als nächstes müssen wir eine Instanz der neuen Logik-Klasse erzeugen und dem ILoader-Objekt übergeben. Es wird empfohlen, sicherzustellen, dass beim Beenden der Applikation der belegte Speicher wieder freigegeben wird. Das bedeutet, dass die Instanz in der DeInit()-Methode, welche vor Beendigung der Applikation aufgerufen wird, wieder entfernt werden soll.

In der Datei hello_world_app.h wird der Klasse ein Zeiger auf ein HelloWorldLogic-Objekt als neue Membervariable mLogic hinzugefügt. Davor ist es notwendig, die neue Klasse HelloWorldLogic z.B. mittels Vorwärtsdeklaration bekannt zu machen:

namespace Murl
{
    namespace App
    {
        class HelloWorldLogic;
        
        class HelloWorldApp :  public AppBase
        {
        public:
            HelloWorldApp();
            virtual ~HelloWorldApp();
            
            virtual Bool Configure(IEngineConfiguration* engineConfig, IFileInterface* fileInterface); 
            virtual Bool Init(const IAppState* appState);
            virtual Bool DeInit(const IAppState* appState);
                
        protected:
            HelloWorldLogic* mLogic;
        };
    }
}

In der Datei hello_world_app.cpp wird die neue Membervariable mit dem Konstruktor auf den Wert 0 (Nullpointer) initialisiert. Weiters ist es notwendig, das Header-File hello_world_logic.h für die forward-deklarierte Klasse HelloWorldLogic zu inkludieren:

#include "hello_world_app.h"
#include "hello_world_logic.h"

using namespace Murl;

App::HelloWorldApp::HelloWorldApp()
: mLogic(0)
{
}

In der Methode Init() wird mit new HelloWorldLogic(appState->GetLogicFactory()); eine neue Instanz im Speicher erzeugt und in mLogic gespeichert. Der Aufruf von loader->AddPackage() wird auch geändert. Als dritter Parameter wird mit der Methode GetProcessor() der Engine-Prozessor aus unserer neuen Logikklasse, übergeben, wenn das Debug-Paket fertig geladen wurde.

Bool App::HelloWorldApp::Init(const IAppState* appState)
{
    mLogic = new HelloWorldLogic(appState->GetLogicFactory());
    ILoader* loader = appState->GetLoader();
    loader->AddPackage("debug", ILoader::LOAD_MODE_STARTUP, mLogic->GetProcessor());
    return true;
}

Anstatt mLogic->GetProcessor() kann auch *mLogic geschrieben werden, da der "*" Operator in der BaseProcessor-Klasse überladen ist und ebenfalls das IEngineProcessor-Objekt zurückliefert.

In der Methode DeInit() wird noch mit Util::Release(mLogic) bei Programmende der Speicher der erzeugten Instanz wieder freigegeben:

Bool App::HelloWorldApp::DeInit(const IAppState* appState)
{
    Util::Release(mLogic);
    return true;
}

Als Ergebnis wird nun zusätzlich rechts oben der Text "Hello World!" angezeigt:

tut0100_logic_message.png
Ausgabefenster von Hello World V3 mit "Hello World!"-Text

Übungen

  • Versuche die einzelnen Schritte nachzuvollziehen
  • Gib in der Methode Configure() zusätzlich zum Fenstertitel auch einen "product name" in der IEngineConfiguration an.
  • Öffne die Interface-Deklaration der Klasse ILoader und finde heraus, welche zusätzlich gültigen LoadMode-Parameter es abgesehen von LOAD_MODE_STARTUP gibt.


Copyright © 2011-2024 Spraylight GmbH.