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.

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.

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.

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. Camera
  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" />

  <Camera 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" />
  </Camera>
</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!");
Array< String > StringArray
Bool AngleStringToDouble(const String &inputString, Double &value, Bool &hasUnit)
DataType RoundToRaster(DataType value, DataType raster, DataType &diff)
Bool StringToSInt32(const String &inputString, SInt32 &value)
Bool StringToColorComponent(const String &inputString, Float &value, ColorStringFormat &format)
DataType RoundToNextPowerOfTwo(DataType value)
DataType Tan(DataType radAngle)
DataType ArcTanHyp(DataType value)
MurlReal Real
Array< SInt16 > SInt16Array
DataType CosHyp(DataType value)
String SInt32ToString(SInt32 inputValue)
Bool StringToUInt32(const String &inputString, UInt32 &value)
constexpr Double INCHES_TO_MM
constexpr Double E
Array< UInt64 > UInt64Array
Index< String, StdHash< String > > StringIndex
const DataType & Max(const DataType &x, const DataType &y)
Bool StringToFloat(const String &inputString, Float &value)
DataType Exp(DataType value)
constexpr Double CM_TO_MM
Bool StringToBool(const String &inputString, Bool &value)
Array< SInt32 > SInt32Array
String DoubleToString(Double inputValue)
Array< UInt16 > UInt16Array
UInt32 GetNumberOfClearedBits(DataType value)
DataType ArcCosHyp(DataType value)
DataType Pow(DataType base, DataType exponent)
Array< SInt64 > SInt64Array
bool Bool
Bool HexStringToUInt32(const String &inputString, UInt32 &value)
Array< UInt32 > UInt32Array
Bool IsInfinite(DataType value)
MurlUInt16 UInt16
DataType SubAngle(DataType angle1, DataType angle2)
constexpr Double TWO_PI
float Float
MurlSInt8 SInt8
DataType TanHyp(DataType value)
DataType RoundToNextSixteenByteBoundary(DataType value)
constexpr Double MM_TO_CM
Bool IsFinite(DataType value)
DataType ModF(DataType value, DataType &intPart)
Bool StringToColor(const String &inputString, Color &value, ColorStringFormat &format)
DataType RoundToNextFourByteBoundary(DataType value)
void TrimStringArray(StringArray &inputString)
double Double
DataType Floor(DataType value)
UInt64 SwapBytes(UInt64 value)
constexpr Double CM_TO_INCHES
DataType AddAngle(DataType angle1, DataType angle2)
constexpr Double DEG_TO_RAD
Array< Real > RealArray
DataType ArcTan2(DataType y, DataType x)
Array< Float > FloatArray
char Char
MurlSInt64 SInt64
const DataType & Min(const DataType &x, const DataType &y)
Bool IsNaN(DataType value)
DataType ArcCos(DataType value)
DataType Sgn(DataType value)
DataType Ceil(DataType value)
String UInt32ToString(UInt32 inputValue)
constexpr Double INV_HALF_PI
DataType DegToRad(DataType degrees)
DataType Log10(DataType value)
Bool IsEqual(const DataType &a, const DataType &b, const DataType &epsilon=Limits< DataType >::Epsilon())
MurlSInt16 SInt16
MurlUInt64 UInt64
const DataType & Clamp(const DataType &val, const DataType &min, const DataType &max)
constexpr Double INV_PI
MurlUInt8 UInt8
UInt32 SplitString(const String &inputString, Char delimiter, StringArray &pieces, Bool acceptEmpty=false)
DataType ArcSin(DataType value)
void Swap(DataType &a, DataType &b)
Array< Double > DoubleArray
Bool StringToDouble(const String &inputString, Double &value)
DataType ArcTan(DataType value)
UInt32 GetNumberOfSetBits(DataType value)
MurlSInt32 SInt32
DataType ArcSinHyp(DataType value)
DataType SinHyp(DataType value)
constexpr Double INCHES_TO_CM
constexpr Double PI
MurlUInt32 UInt32
DataType Log(DataType value)
Array< Bool > BoolArray
DataType Fmod(DataType numerator, DataType denominator)
void SortStringArray(StringArray &array, Bool ascending)
constexpr Double RAD_TO_DEG
DataType RoundToNextEightByteBoundary(DataType value)
Array< SInt8 > SInt8Array
Bool IsPowerOfTwo(DataType value)
DataType Sin(DataType radAngle)
UInt32 GetNumberOfDigits(DataType value, DataType base=10)
constexpr Double HALF_PI
DataType Abs(DataType value)
DataType RadToDeg(DataType radiants)
DataType Cos(DataType radAngle)
DataType MapAngle(DataType angle)
DataType Sqrt(DataType value)
constexpr Double MM_TO_INCHES
constexpr Double INV_TWO_PI
DataType Log2(DataType value)
DataType Round(DataType value)
Interpolation


Copyright © 2011-2026 Spraylight GmbH.