Einführung

Eine kurze Einführung in die Entwicklung mit der Murl Engine

Die Murl Engine ist ein natives, plattformunabhängiges, zeitgesteuertes Scene Graph Multimedia Framework für die Entwicklung von Spielen und Multimedia-Applikationen. Das Framework wurde für höchste Performance und Flexibilität entworfen und erlaubt die Cross-Plattform-Entwicklung von nativen Applikationen mit einer einzigen Code Basis.

Native Cross Plattform

Das Framework besteht aus einem plattformunabhängigen Kernstück ("Framework Code") und einer plattformspezifischen Anbindung ("Plattform Code") für jede Plattform. Der Framework Code wurde in C++ entwickelt, der Plattform Code plattformabhängig in C, C++, Objective C und Java.

Die Programmierung ("User Code") erfolgt in C++ entsprechend der zu Verfügung stehenden API (Programmierschnittstelle, engl. Application Programming Interface). Für jede Plattform kann aus User Code, Framework Code und Plattform Code eine native Applikation erzeugt werden.

intro-usercode-frameworkcode.png
Application = User Code + Framework Code + Plattform Code

Die Entwicklung von nativen Anwendungen in C++ erlaubt eine effiziente Programmierung auf hohem Abstraktionsniveau und optimale Performance.

Native bedeutet in diesem Zusammenhang, dass die Applikation direkt auf der Zielhardware und dem Zielbetriebssystem ausführbar ist. Es werden keine Plugins, Emulatoren, Laufzeitumgebungen oder ähnliches zwischen Hardware und der Applikation benötigt. Dadurch wird die bestmögliche Performance und Integration erreicht.

Die Applikationsentwicklung kann prinzipiell mit jeder beliebigen C++ IDE erfolgen. Unterstützung bei der Projekterstellung bieten jedoch nur Microsoft Visual Studio (Express) für Windows und Apple XCode für Mac OS X Rechner und sind daher dringend empfohlen.

Um iOS und/oder Mac OS X Applikationen erzeugen zu können, ist ein Rechner mit Mac OS X Betriebssystem und XCode erforderlich. Für Windows Applikationen wird ein Rechner mit einem Windows Betriebssystem und Visual Studio benötigt. Android Applikationen können sowohl mit Windows Rechnern als auch mit Mac OS X Rechnern entwickelt werden.

intro-framework-ide.png
Betriebssystemabhängigkeit beim Erstellen der nativen Appliaktion

Zeitgesteuert

Die Programmierung des Frameworks erfolgt zeitgesteuert. Dabei kommen ein Render-Thread und ein Logik-Thread zum Einsatz, die immer parallel in einer Schleife abgearbeitet werden.

Der Logik-Thread ruft bei jedem Durchlauf der Schleife eine User-Code Methode (z.B. OnProcessTick()) auf. In dieser Methode kann die Applikation upgedatet werden. Typischerweise wird abgefragt, wie viel Zeit seit dem letzten Aufruf vergangen ist (z.B. GetCurrentTickDuration()) und welche Benutzereingaben an den Input-Geräten anliegen. Mit diesen Informationen wird dann die Anwendung "simuliert" und die Objekte entsprechend upgedatet.

Nach ein- oder mehrmaligem Abfragen dieser Informationen startet der Render-Thread, alle sichtbaren Objekte zu zeichnen, während der Logik-Thread bereits mit der Evaluierung des nachfolgenden Schrittes beginnt.

Die Schrittweite (also die Zeit zwischen zwei Durchläufen) für den Render-Thread und für den Logik-Thread kann über ein Konfigurationsobjekt im Programm eingestellt werden. Dabei ist sowohl eine fixe als auch eine variable Schrittweite möglich. Des Weiteren kann auch ein minimaler und maximaler Zeitwert festgelegt werden. Zusätzlich besteht die Möglichkeit die Anzahl der Schritte, die für jeden Frame durchgeführt werden müssen, zu definieren.

Szenengraph

Die Murl Engine verwendet einen Szenengraph für die Beschreibung der virtuellen Welt. Ein Szenengraph ist eine objektorientierte Datenstruktur und entspricht einem gerichteten azyklischen Graphen. Mit einem Szenengraph können Objekte gehalten, gruppiert und transformiert werden.

Der Szenengraph bildet eine baumähnliche Struktur aus Graphenknoten, welche aus genau einem Wurzelknoten und beliebig vielen Kinderknoten besteht. Jeder Graphenknoten beinhaltet Informationen über die virtuelle Szene, wie beispielsweise Geometrie, Farben, Lichtquellen, Audioquellen oder Transformationen.

intro-scenegraph.png
Beispiel für einen Szenengraphen

Jeder Graphenknoten wirkt auf all seine Kinderknoten. Ein Transform-Knoten kann beispielsweise dafür benutzt werden, die Position und Rotation des darunterliegenden Szenengraphen zu ändern. Das bedeutet, dass alle Kinderknoten und Kindeskinderknoten von solch einer Änderung betroffen sind. Generische Knoten können zum Beispiel unsichtbar gemacht werden, wovon ebenso alle Kinderknoten betroffen sind.

Eine Ausnahme bilden State-Knoten wie z.B. MaterialState, ParametersState, CameraState etc. State-Knoten bewirken eine Kontextänderung und wirken daher nicht nur auf die Kinderknoten, sondern auch auf alle nachfolgenden Knoten. Wird zum Beispiel mit dem Knoten MaterialState ein Material ausgewählt, werden alle nachfolgenden Knoten mit diesem Material gezeichnet. Dies gilt solange, bis ein neues Material ausgewählt wird.

Mit dem Graphenknoten SubState können State-Änderungen lokal begrenzt werden. Das heißt, dass State-Änderungen von Kinderknoten eines SubState Knoten, keine Auswirkung auf den Kontext nachfolgender Knoten haben. Der SubState Knoten setzt also nach Abarbeitung seiner Kinderknoten den Kontext wieder auf den ursprünglichen Zustand zurück.

Alle Graphenknoten haben einige grundlegende Eigenschaften gemeinsam:

  • eindeutige id Name
  • active Eigenschaft
  • visible Eigenschaft

Mit dem id Namen kann ein Knoten eindeutig identifiziert werden. Mit der visible Eigenschaft kann ein Knoten sichtbar bzw. nicht sichtbar geschaltet werden. Der Render-Thread überspringt dann diesen Knoten inkl. aller Kinderknoten (Ausgabe-Traversierung wird übersprungen). Mit der active Eigenschaft kann ein Knoten für alle Logikoperationen aktiviert bzw. deaktiviert werden. Der Logik-Thread überspringt dann diesen Knoten inkl. aller Kinderknoten (Logik-Traversierung wird übersprungen). Um einen Knoten vollständig zu deaktivieren, muss also active und visible auf false gesetzt werden. Das kann einzeln erfolgen oder optional gemeinsam mit der activeAndVisible Eigenschaft.

Zu beachten
Achtung, id Namen müssen aus alphanumerischen Zeichen sowie den Zeichen Punkt und Underscore zusammengesetzt werden und dürfen nicht mit einer Ziffer beginnen. Die Verwendung von anderen Zeichen (z.B. -, +, :, etc.) ist nicht erlaubt.

Bei jedem Durchlauf wird der Szenengraph ausgehend vom Wurzelknoten nach dem Verfahren der Tiefensuche (englisch depth-first search, DFS) traversiert. Alle Kinderknoten werden rekursiv in der Reihenfolge in der sie definiert wurden abgearbeitet. Für obigen Beispielgraphen ergibt sich folgende Traversierungsreihenfolge:

  1. Root
  2. View
  3. PerspectiveCamera
  4. FixedProgram
  5. Material
  6. MaterialState
  7. Transform
  8. CubeGeometry
  9. PlaneGeometry
  10. CameraTransform

Genaugenommen finden bei jedem Durchlauf zwei unterschiedliche Traversierungen statt - die Logik-Traversierung und die Ausgabe-Traversierung:

  • Eine Logik-Traversierung findet jedes Mal statt, wenn die Methoden OnProcessTick() im User Code abgeschlossen sind. Für alle Knoten deren Eigenschaften im User Code geändert wurden, wird der interne Status upgedatet. Wenn für spezielle Knoten eine Physik Simulation notwendig sein sollte, wird auch diese durchgeführt.
  • Wenn alle Logik-Traversierungen für ein Bild (Frame) abgeschlossen sind, wird einmal eine Ausgabe-Traversierung durchgeführt. Dabei werden alle relevanten Informationen für das Rendering aufbereitet.

XML Dateien

Häufig wird der Szenengraph bzw. werden Szenenteilgraphen bei der Murl Engine in einem oder mehreren Textdokumenten in XML Notation erstellt. Diese XML Files werden dann bei Bedarf von der Applikation in den Speicher geladen und die einzelnen Knoten je nach Anforderung modifiziert. Nachfolgend wird der Aufbau eines XML Dokuments überblicksmäßig beschrieben.

<?xml version="1.0" ?>

<Graph>
  <View id="view" />

  <PerspectiveCamera id="camera"
      viewId="view"
      fieldOfViewX="400"
      nearPlane="400" farPlane="2500"
      clearColorBuffer="1" >

    <!-- comment -->
    <FixedProgram id="prg_white" />

    <Material id="mat_white" 
        programId="prg_white" />

    <MaterialState materialId="mat_white" 
        slot="0" />
    
    <Transform id="transform">

        <CubeGeometry
          id="myCube01"
          scaleFactor="200"
          posX="0" posY="0" posZ="0" />

        <PlaneGeometry
          id="myPlane01"
          scaleFactorX="42"
          scaleFactorY="100"
          posX="0" posY="0" posZ="0" />
    </Transform>

    <CameraTransform cameraId="camera" 
        posX="0" posY="0" posZ="800" />
  </PerspectiveCamera>
</Graph> 

XML Dokumente beginnen üblicherweise mit der optionalen XML-Deklaration z.B. <?xml version="1.0" ?>

Elemente innerhalb eines XML Dokuments werden durch < > gekennzeichnet. Jedes Element hat ein Start-Tag <element> und ein End-Tag </element>. Kinderelemente werden innerhalb des Start- und End-Tags definiert. Für Elemente ohne Kinder kann optional das Empty-Element-Tag <element/> verwendet werden. Die Angabe von <element></element> ist gleichbedeutend mit der Angabe von <element/>.

Attribute für ein Element werden innerhalb des Start-Tags oder Empty-Element-Tags als Paar mit Attribut-Name und Attribut-Wert angegeben: attributeName="attributeValue"

Kommentare innerhalb einer XML Datei beginnen mit <!-- und enden mit -->.

Zu beachten
Achtung, einzelne Attribute können nicht auskommentiert werden.

Für die Erstellung eines XML Dokuments müssen ein paar grundsätzliche Regeln befolgt werden:

  • In einem XML Dokument muss genau ein Wurzelelement existieren, welches alle anderen Elemente umschließt.
  • Für jedes Start-Tag muss genau ein End-Tag in der gleichen Verschachtelungsebene existieren und umgekehrt (ebenentreu paarig verschachtelt).
  • Alle Attribut-Namen für ein Element müssen eindeutig sein (keine zwei Attribute mit demselben Namen innerhalb eines Elements).

Namenskonventionen

KLASSEN, NAMESPACES, FUNKTIONEN, METHODEN

Klassen-, Namespace-, Methoden- und Funktionsnamen beginnen mit einem Großbuchstaben

z.B. MyApp bzw. LoadData()

INTERFACES

Interface-Klassen beginnen mit I

z.B. IAppConfiguration

VARIABLEN

Variablen beginnen mit einem Kleinbuchstaben

z.B. rotationAngleX

MEMBER VARIABLEN

Membervariablen beginnen mit m

z.B. mVar

DATEINAMEN

Dateinamen setzen sich aus dem Namespace und dem Klassennamen zusammen, bestehen ausschließlich aus Kleinbuchstaben und verwenden "_" als Trennzeichen

z.B. murl_my_app.cpp / murl_my_app.h für die Klasse MyApp im Namespace Murl

Primitive Datentypen

Die C++ Standard-Datentypen werden auf unterschiedlichen Systemen unterschiedlich repräsentiert und haben daher unterschiedliche Wertebereiche. Daher sollten immer die im Framework definierten plattformunabhängigen Datentypen verwendet werden (Großschreibung beachten z.B. Char statt char). Die Datentypen sind in der Datei murl_types.h deklariert:

murl/base/include/engine/murl_types.h
UInt64 Unsigned 64 bit integer (0 to 18.446.744.073.709.551.615)
UInt32 Unsigned 32 bit integer (0 to 4.294.967.295)
UInt16 Unsigned 16 bit integer (0 to 65.535)
UInt8 Unsigned 8 bit integer (0 to 255)
SInt64 Signed 64 bit integer (−9.223.372.036.854.775.808 to 9.223.372.036.854.775.807)
SInt32 Signed 32 bit integer (−2.147.483.648 to 2.147.483.647)
SInt16 Signed 16 bit integer (−32.768 to 32.767)
SInt8 Signed 8 bit integer (-128 to 127)
Double 64bit IEEE floating point (52 Bit Mantisse, 11 Bit Exponent)
Float 32bit IEEE floating point (23 Bit Mantisse, 8 Bit Exponent)
Real platform-specific - either 32 bit or 64 bit IEEE floating point
Bool Boolean true or false (XML File: "true", "on", "yes", "1" or "false", "off", "no", "0")
Char Character

Passende Containerklassen aus murl_types.h:

Mathematische Konstanten sind in der Datei murl_math_types.h definiert:

Mathematische Funktionen befinden sich im Namespace Murl::Math:

Weitere Hilfsfunktionen sind im Namespace Murl::Util zu finden - z.B.:

Strings

Das Framework stellt auch eine eigene String Klasse mit zahlreichen Konvertierungs- und nützlichen Hilfsfunktionen zur Verfügung.

murl/base/include/engine/murl_string.h
murl/base/include/engine/util/murl_util_string.h

Debug Messages

Der Header murl_debug_trace.h beinhaltet Methoden zur Ausgabe von Debug Nachrichten.

murl/base/include/engine/debug/murl_debug_trace.h

DEBUG::TRACE, DEBUG::ERROR

Eine einfache Statusmeldung in der Konsole kann mit dem Befehl Debug::Trace ausgegeben werden. Bei manchen Plattformen werden zusätzlich Zeitangaben ausgegeben. Dieser Befehl kann wie der Befehl printf mit optionalen Formatparametern verwendet werden.

Debug::Trace("Hello World!");
Debug::Trace("The result is %u:", result);

Die Ausgabe von Debug::Trace Meldungen erfolgt nur im Debug-Modus und wird bei einem Release-Build automatisch "wegoptimiert". Wenn eine Meldung auch in einem Release-Build aufscheinen soll, kann dafür Debug::Error verwendet werden.

MURL_TRACE

Der Befehl MURL_TRACE gibt zusätzlich zur Statusmeldung auch noch den Methodenname und die Zeilennummer der Nachricht aus. Es können auch optionale Formatparameter verwendet werden.

MURL_TRACE(0, "Debug Hello World");

Der erste Parameter (0) gibt den "Log-Level" für diese Meldung an. Debug Nachrichten deren Log-Level über den globalen Log-Level liegen werden unterdrückt. Ein gesetzter globaler Log-Level von 1 zeigt also nur Debug Nachrichten mit einem Log-Level kleiner gleich 1 an. Der globale Log-Level kann übrigens mit SetLogLevel gesetzt und mit GetLogLevel abgefragt werden. Die Statusmeldung sieht dann ungefähr so aus:

Murl::App::ContainerLogic::PrintDemo(), line 63: Debug Hello World

Die Ausgabe von MURL_TRACE Meldungen erfolgt ebenfalls nur im Debug-Modus.

SYSTEM::CONSOLE::PRINT

Eine weiter Möglichkeit für Konsolen Statusmeldungen bietet die Funktion Murl::System::Console::Print. Diese Meldungen erscheinen auch im Release-Build. Die Ausgabe erfolgt "exakt wie angegeben" d.h. es werden keine Extrainformationen wie Zeitstempel, Zeilenumbrüche etc. hinzugefügt.

murl/base/include/engine/system/murl_system_console.h

Dabei können wiederum optionale Formatparameter verwendet werden.

System::Console::Print("Print with System Console");
System::Console::Print("Current Time %.10f", state->GetCurrentTickTime());

PRINTTREE

Mit dem Befehl PrintTree kann der aktuelle Szenengraph als Statusmeldung in der Konsole ausgegeben werden.

Graph::IRoot* root = state->GetGraphRoot();
root->PrintTree();

Je nach gegebenen Startknoten kann der gesamte Szenengraph oder auch nur ein Teilgraph ausgegeben werden.

Graph::INode* node = mBallTransform->GetNodeInterface();
node->PrintTree();

SETUSERDEBUGMESSAGE

Wenn das Debug-Paket geladen wurde, kann mit der Methode SetUserDebugMessage vom Interface Murl::Logic::IState eine einfache Statusmeldung am Bildschirm angezeigt werden.

state->SetUserDebugMessage("Package Loading succeeded!");


Copyright © 2011-2018 Spraylight GmbH.