Tutorial #01: Cube

Dieses Beispiel zeigt, wie ein neues Paket mit einfachen Graphenknoten erzeugt und in ein Programm eingebunden werden kann. Als Ergebnis erhalten wir einen sich drehenden Würfel.

Analog zum Hello World-Tutorial befindet sich das Cube-Projekt ebenfalls im Ordner tutorials/chapter01/01_cube/project, wiederum mit den unterschiedlichen Projektdateien in den entsprechenden Unterverzeichnissen für jede Zielplattform.

Version 1: Grundgerüst

Wir beginnen mit einem wie im vorigen Beispiel gezeigten Grundgerüst (create/destroy Funktionen, eine App-Klasse, eine Logik-Klasse), allerdings erweitern wir dieses ein wenig.

In der Configure()-Methode geben wir mit SetDisplaySurfaceSize() zusätzlich die Größe der Ausgabe-Surface und damit die Fenstergröße von 800x600 Pixeln an und definieren mit SetProductName() einen Produktnamen für die neue Applikation.

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

    engineConfig->SetProductName("Cube");
    appConfig->SetWindowTitle("cube powered by murl engine");
    appConfig->SetDisplaySurfaceSize(800, 600);
    appConfig->SetFullScreenEnabled(false);

    return true;
}

In der Init()-Methode der App-Klasse fügen wir eine if-Bedingung ein, welche die ausgewählte Build-Konfiguration (Debug/Release) evaluiert, sodass das "debug"-Paket nur bei einem Debug-Build geladen wird.

Zusätzlich fügen wir zwei Zeilen ein, um die Pakete "startup" und "main" zu laden und mit der Logik-Klasse zu verknüpfen. Diese Anweisung ist vorerst auskommentiert, da wir das Paket "main" erst erstellen müssen. Das mit dem Tutorial gelieferte "startup"-Paket befindet sich im Ordner data/v1/packages und beinhaltet einen einfachen, animierten Ladebildschirm.

Bool App::CubeApp::Init(const IAppState* appState)
{
    mLogic = new CubeLogic(appState->GetLogicFactory());
    ILoader* loader = appState->GetLoader();

    if (Util::IsDebugBuild())
    {
        loader->AddPackage("debug", ILoader::LOAD_MODE_STARTUP);
    }
    
    loader->AddPackage("startup", ILoader::LOAD_MODE_STARTUP);
    //loader->AddPackage("main", ILoader::LOAD_MODE_STARTUP);
    return true;
}

In der Deklarationsdatei der Logik-Klasse CubeLogic überschreiben wir nicht nur die Basisklassen-Methode OnInit(), sondern auch die beiden Methoden OnDeInit() und OnProcessTick(). Die Methode OnDeInit() wird genau einmal unmittelbar vor dem Entladen des dazugehörigen Pakets aufgerufen (also spätestens beim Beenden der Applikation). Die Methode OnProcessTick() wird zyklisch bei jedem Logik-Schritt aufgerufen. Im Normalfall gibt es genau einen Logik-Schritt pro Frame. Dies kann aber mit dem IEngineConfiguration-Objekt geändert werden.

        protected:
            virtual Bool OnInit(const Logic::IState* state);
            virtual Bool OnDeInit(const Logic::IState* state);
            virtual void OnProcessTick(const Logic::IState* state);

In der Methode OnInit() wird als erstes das "startup" Paket wieder entladen. Dieses dient nur als Ladeanzeige und wird nach dem Laden aller Pakete nicht mehr benötigt. Die Methode OnProcessTick() bleibt vorerst leer und die Methode OnDeInit() liefert einfach true zurück.

Bool App::CubeLogic::OnInit(const Logic::IState* state)
{
    state->GetLoader()->UnloadPackage("startup");

    if (!AreGraphNodesValid())
    {
        return false;
    }

    state->SetUserDebugMessage("Cube Init succeeded!");
    return true;
}

Bool App::CubeLogic::OnDeInit(const Logic::IState* state)
{
    return true;
}

void App::CubeLogic::OnProcessTick(const Logic::IState* state)
{
}

Das Ergebnis ist ein schwarzes Fenster mit Lade- und Debug-Anzeige.

tut0101_loading_screen.png
Ausgabefenster mit Ladeanzeige

Übungen

  • Warum wird die User-Debug-Message "Cube Init succeeded!" nicht angezeigt?
  • Was passiert wenn die Logik-Klasse anstatt mit dem "main" Paket mit dem "startup" Paket verknüpft wird? Warum?
  • Was passiert beim Versuch das "main" Paket zu laden?

Version 2: Paket erstellen

Als nächstes erstellen wir das fehlende Ressourcen-Paket "main". Dafür erzeugen wir im bereits bestehenden Unterverzeichnis data/v2/packages, welches bereits die "startup.murlpkg"-Datei enthält, zunächst einen Ordner mit dem gewünschten Namen und der Endung .murlres:

  • data/v2/packages/main.murlres

Jedes "murlres"-Paket benötigt zumindest eine Datei mit genau dem Namen package.xml, in der der Paketinhalt definiert wird. Zusätzlich benötigen wir eine Datei, in der wir unsere Graphenknoten erstellen können. Der Name kann frei gewählt werden – wir nennen es graph_main.xml.

  • data/v2/packages/main.murlres/package.xml
  • data/v2/packages/main.murlres/graph_main.xml

Beide Dateien beinhalten eine XML konforme Datenbeschreibung und beginnen mit der XML Deklaration <?xml version="1.0" ?>.

Die Datei package.xml definiert, welche Ressourcen in dem Paket existieren und welche Instanzen erzeugt werden. Sie beginnt mit dem Wurzelelement Package und definiert dafür ein id-Attribut. Das Wurzelelement kennzeichnet das Dokument als Package Resource Document. Mit dem id-Attribut wird ein eindeutiger Name für das Paket festgelegt. Dieser Name kann dann in weiterer Folge verwendet werden, um einen Bezug auf das Paket und darin befindliche Ressourcen herzustellen.

Zu beachten
Achtung! id-Attribute müssen immer eindeutig sein!

Innerhalb des Wurzelelements sind zwei Kinderelemente definiert. Die in <!-- und --> geklammerten Texte sind lediglich Kommentare und ohne weitere Bedeutung.

Mit dem Resource-Kinderelement wird die Datei graph_main.xml als Ressource bekannt gemacht und eine eindeutige ID dafür definiert. Mit dem Instance-Kinderelement wird eine Instanz dieser Ressource erzeugt und dem Szenengraphen hinzugefügt, sobald das Paket geladen wurde.

<?xml version="1.0" ?>

<Package id="package_main">
    
    <!-- Graph resources -->
    <Resource id="graph_main" fileName="graph_main.xml"/>
    
    <!-- Graph instances -->
    <Instance graphResourceId="graph_main"/>
    
</Package>

In der Datei graph_main.xml werden die einzelnen Knoten des Graphen definiert – und damit unsere Szene.

Das Graph-Element bildet das Wurzelelement und kennzeichnet das Dokument als Graph Resource Document. Innerhalb des Wurzelelements können die einzelnen Graphenknoten aufgelistet werden. Die Deklarationen für die verschiedenen Graphenknoten befinden sich im Unterverzeichnis base/include/engine/graph.

<?xml version="1.0" ?>

<Graph>

Für die Definition unserer Szene sind folgende Schritte notwendig:

  • View und damit einen Zeichenbereich festlegen
  • Kamera und damit den sichtbaren Bereich der virtuellen Welt festlegen
  • Ein Material erstellen, um das Aussehen eines zeichenbaren Objekts zu beschreiben
  • Das erstellte Material für die folgenden Zeichenoperationen auswählen
  • Objekt zeichnen

Alle Objekte werden in der virtuellen Welt anhand eines rechtshändigen, dreidimensionalen kartesischen Koordinatensystems positioniert. Dabei befindet sich der Nullpunkt im Zentrum, die X-Achse verläuft horizontal von links nach rechts, die Y-Achse vertikal von unten nach oben und die Z-Achse von hinten nach vorne.

tut0101_coordinate_system.png
Das Koordinatensystem der Murl Engine

In einem Graphen können ein oder mehrere Graph::View-Knoten mit je einem oder mehreren Kamera-Knoten (Graph::Camera) existieren. Jedem View-Knoten kann eine beliebige Anzahl von Kamera-Knoten zugewiesen werden, jedoch hat jede Kamera nur einen einzigen zugewiesenen View-Knoten. Während der View-Knoten den Zeichenbereich im Fenster definiert, gibt der Kameraknoten den sichtbaren Bereich in der virtuellen Welt an. Dieser sichtbare Bereich wird dann im View-Bereich gezeichnet. Standardmäßig füllt der View-Bereich den gesamten Fensterinhalt aus.

Zunächst erzeugen wir einen solchen View-Knoten und weisen ihm die eindeutige ID "view" zu. Anschließend erzeugen wir mit dem Camera-Knoten eine Kamera mit der ID "camera" und verknüpfen diese über das Attribut viewId mit dem zuvor erzeugten View-Knoten.

Um Knoten in die virtuelle Welt dieser Kamera hinzuzufügen, muss die Kamera mit einem Graph::CameraState Knoten aktiviert werden. Alternativ können die Knoten auch als Kinderknoten innerhalb des Kameraknotens zwischen Start-Tag und End-Tag definiert werden (wie in diesem Beispiel). In diesem Fall ist die Kamera nur für Knoten innerhalb dieses Sub-Graphen aktiv.

Mit dem Graph::CameraTransform-Knoten und den Attributen posX , posY und posZ kann die Position der Kamera im Raum angegeben werden. Um die Kamera zu drehen, können die Attribute axisX , axisY, axisZ und angle verwendet werden. Standardmäßig steht die Kamera am Punkt (0/0/0) und blickt entlang der z-Achse in Richtung Minus ∞.

    <View id="view"/>
    
    <Camera
        id="camera"
        viewId="view"
        fieldOfViewX="400"
        nearPlane="400" farPlane="2500"
        clearColorBuffer="true"
    >
        
        <CameraTransform
            cameraId="camera"
            posX="0" posY="0" posZ="800"
        />

Mit den Angaben von nearPlane und farPlane wird der Abstand von der Kamera zu der Near-Plane und zu der Far-Plane festgelegt. Mit fieldOfViewX wird die Breite der Near-Plane in X-Richtung festgelegt. Dadurch wird ein sichtbarer Bereich der virtuellen Welt als Pyramidenstumpf ("Frustum") definiert, welcher dann im Darstellungsfeld (View) angezeigt wird.

tut0101_view_frustum.png
Das View-Frustum einer perspektivischen Kamera

Da in Y-Richtung (fieldOfViewY) keine Angabe gemacht wurde, wird die Höhe des Viewport passend zum Seitenverhältnis des Fensters automatisch berechnet, sodass sich quadratische Pixel ergeben.

Anstelle von fieldOfViewX könnte auch mit fieldOfViewY die Höhe angegeben werden. In diesem Fall würde die Breite des Viewport passend berechnet werden.

Wenn sowohl die Breite mit fieldOfViewX als auch die Höhe mit fieldOfViewY angegeben werden, ergibt sich je nach Fenster-Seitenverhältnis ein eventuell verzerrtes Bild mit nicht quadratischen Pixeln.

Die Angabe von clearColorBuffer="true" sorgt dafür, dass der Bildschirm bei jedem Frame vor dem Zeichnen automatisch gelöscht wird.

tut0101_view_frustum_top.png
Draufsicht auf das View-Frustum

Die Angaben für die Kamera wurden nicht ganz zufällig genau so gewählt. Das Weltkoordinatenfenster auf der Z-Ebene 0 ist bei diesem Frustum genau doppelt so groß, wie das Koordinatenfenster auf der Near-Plane und hat damit genau eine Breite von 800 Koordinateneinheiten. In einem Fenster, das wie in unserem Fall eine Breite von genau 800 Pixeln hat, ergibt sich dadurch auf der Z-Ebene 0 eine 1:1 Zuordnung zwischen Koordinateneinheit und Pixelgröße.

Das ist vor allem bei 2D-Anwendungen praktisch, da damit ein Verschieben eines Objektes auf der Z-Ebene 0 um eine Koordinateneinheit auch ein Verschieben um genau einen Pixel im Fenster zur Folge hat. Ein Verschieben eines Objektes auf der Nearplane um eine Koordinateneinheit würde hingegen ein Verschieben um genau zwei Pixel zur Folge haben.

Damit ein Objekt sichtbar gezeichnet wird, muss es innerhalb des definierten Frustums liegen. Insbesondere bei der Positionierung von ebenen Flächen (z.B. Graph::PlaneGeometry-Knoten) sollte auf genügend Abstand zu Near- und Far-Plane geachtet werden, da es sonst durch Rechenungenauigkeiten zu flackernden Bildern kommen kann.

Im nächsten Schritt wird das Material definiert, mit dem der Würfel gezeichnet werden soll. Ein Material besteht aus

  • einem Programm-Knoten für den Shader,
  • einem Material-knoten und
  • optionalen Parameterwerten für das Material.

Das Programm für den Shader ist dabei entweder ein vorgefertigtes, fixes Programm (Graph::FixedProgram) oder ein selbst erstelltes Shader-Programm (Graph::ShaderProgram). Der Einfachheit halber definieren wir zunächst nur ein FixedProgram mit Standardwerten und definieren lediglich das id-Attribut:

        <FixedProgram
            id="prg_white"
        />

Das Material selbst definiert Attribute, die unabhängig vom eigentlichen Shader sind. Vorerst verwenden wir einen Graph::Material-Knoten mit Standardwerten. Wir geben nur eine ID an und verknüpfen es über das programId-Attribut mit dem vorher definierten FixedProgram:

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

Im nächsten Schritt wird das Material aktiviert, indem wir einen Graph::MaterialState Knoten verwenden. Mit dem materialId Attribut wählen wir das Material aus und weisen ihm einen aus 128 möglichen Material-Slots zu. In unserem Fall werden alle folgenden Objekte, die den Slot 0 zum Zeichnen ver-wenden, nun mit dem Material "mat_white" gezeichnet.

        <MaterialState
            materialId="mat_white"
            slot="0"
        />

Als Letztes müssen wir noch unseren Würfel erzeugen. Als ID wird "myCube01" definiert, der Mittelpunkt des Würfels wird mit posX, posY und posZ auf die Koordinate (0/0/0) positioniert und die Kantenlänge auf 200 Einheiten festgelegt. Der Graph::CubeGeometry Knoten erstellt standardmäßig einen Würfel und wir skalieren ihn auf die gewünschte Größe.

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

Die Reihenfolge der Elemente ist in zweierlei Hinsicht von Bedeutung:

  • Es darf nur auf bekannte IDs referenziert werden d.h. ein Element muss zuerst bekannt gemacht werden und erst danach kann darauf referenziert werden. In anderen Worten, z.B. ein MaterialState kann nicht mit einem Material verbunden werden, welches "weiter unten" in der XML-Datei oder in einer anderen zu einem späteren Zeitpunkt geladenen XML-Datei definiert ist.
  • State-Knoten wie CameraState, MaterialState oder ParametersState ändern den Kontext und gelten für alle nachfolgenden Elemente.

Paket laden

Jetzt müssen nur noch in der Datei cube_app.cpp die Kommentarzeichen vor der loader->AddPackage("main", ...)-Zeile entfernt werden, damit das neu definierte Paket auch geladen wird.

Bool App::CubeApp::Init(const IAppState* appState)
{
    mLogic = new CubeLogic(appState->GetLogicFactory());
    ILoader* loader = appState->GetLoader();

    if (Util::IsDebugBuild())
    {
        loader->AddPackage("debug", ILoader::LOAD_MODE_STARTUP);
    }
    
    loader->AddPackage("startup", ILoader::LOAD_MODE_STARTUP);
    loader->AddPackage("main", ILoader::LOAD_MODE_BACKGROUND, mLogic->GetProcessor());
    return true;
}

Das Ergebnis ist ein statischer, weißer Würfel, der als Quadrat im Zentrum des Fensters angezeigt wird. Da wir ihn direkt von vorne anschauen, sehen wir nur ein weißes Quadrat.

tut0101_cube_front_face.png
Die Vorderseite eines weißen Würfels

Bei genauem Vergleich der beiden Zeilen für das Laden des "startup"-Pakets und des "main"-Pakets fällt auf, dass sich der angegebene LoadMode unterscheidet.

Load Modes

Für das Laden eines Paketes gibt es drei verschiedene Modi:

Der LOAD_MODE_STARTUP dient dazu, möglichst schnell ein Startup-Logo oder einen Ladebildschirm anzeigen zu können. Dabei werden die Pakete geladen, noch bevor die eigentliche Anzeige des Graphen läuft. Der Bildschirm bzw. das Fenster bleibt schwarz.

Bei Paketen mit LOAD_MODE_BACKGROUND erfolgt das Laden im Hintergrund. Dabei werden geladene LOAD_MODE_STARTUP Pakete bereits angezeigt. Nach erfolgtem Laden werden die Pakete dem Graphen hinzugefügt und die OnInit()-Methode einer gegebenenfalls verknüpften Logik aufgerufen.

Pakete mit der Angabe von LOAD_MODE_ON_DEMAND werden nicht automatisch geladen, sondern vorerst nur bekannt gemacht. Das Laden und Entladen der Pakete kann dann je nach Bedarf mit den Methoden LoadPackage() und UnloadPackage() erfolgen. Ein hierfür geeignetes ILoader-Objekt liefert z.B. das IState-Objekt, das etwa bei OnProcessTick() oder OnInit() übergeben wird:

state->GetLoader()->LoadPackage("demand_package");
state->GetLoader()->UnloadPackage("demand_package"); 

Die einzelnen Pakete werden nacheinander in der angegebenen Reihenfolge geladen. Es ist daher für ein Logik-Objekt sichergestellt, dass sowohl das verknüpfte Paket als auch alle vorherigen Pakete fertig geladen sind, wenn die OnInit() Methode aufgerufen wird.

Grundsätzlich sollten nur kleine Pakete für die Startup-Anzeige mit LOAD_MODE_STARTUP geladen werden und alle weiteren Pakete mit LOAD_MODE_BACKGROUND bzw. LOAD_MODE_ON_DEMAND. Daher werden üblicherweise die Pakete debug und startup mit LOAD_MODE_STARTUP geladen und alle weiteren Pakete mit LOAD_MODE_BACKGROUND, wobei das letzte Paket mit der Logik-Klasse verknüpft wird.

Resource Packer

Ein weiterer Unterschied zwischen dem startup Paket und dem main Paket ist die Art und Weise wie die Dateien vorliegen.

Das main-Paket liegt als Ressourcen-Verzeichnis mit dem Namen main.murlres vor, in dem sich die einzelnen Ressourcen-Dateien befinden.

Das startup-Paket liegt als Ressourcen-Paket mit dem Namen startup.murlpkg vor. Ein Ressourcen-Paket ist eine einzelne Datei mit der Endung .murlpkg. Darin befinden sich ein oder mehrere Ressourcen-Dateien, die in binärer Form in das Paket gepackt wurden.

Mit dem Tool resource_packer kann aus einem Ressourcen-Verzeichnis ein Ressourcen-Paket erstellt werden. Am einfachsten geht dies mit dem Dashboard. Der Befehl Project → Resource Packer Build erstellt aus allen .murlres-Verzeichnissen im Ordner data/packages je ein Ressourcen-Paket.

Alternativ kann direkt mit dem Command Line Tool resource_packer gearbeitet werden. In Abhängigkeit der verwendeten Host-Plattform befindet sich das Programm in verschiedenen Unterverzeichnissen:

  • murl/base/binaries/win32/vs2008/Release/resource_packer.exe
  • murl/base/binaries/win32/vs2010/Release/resource_packer.exe
  • murl/base/binaries/osx/Release/resource_packer

Während der Entwicklung ist es vorteilhaft, mit einem Ressourcen-Verzeichnis zu arbeiten, da jede Änderung an einer Datei automatisch übernommen wird und der Zwischenschritt des Packens entfällt.

Für das Release ist es vorteilhaft mit Ressourcen-Paketen zu arbeiten, da sich damit die Ladezeiten erheblich verkürzen und diese direkt in die Applikation mit eingebunden werden können.

Standardmäßig wird (auf allen Plattformen außer Android) bei einem Debug-Build zuerst versucht ein Paket aus einem .murlres-Verzeichnis zu laden wenn der Typ nicht explizit angegeben wurde. Release-Builds (und auch Debug-Builds auf Android) bevorzugen .murlpkg-Dateien.

Um dieses Verhalten zu ändern ist es möglich, die Dateiendung .murlpkg oder .murlres an den Namen des zu ladenden Packages anzuhängen. Dies zwingt den Loader, ausschließlich ein Paket dieses Typs zu akzeptieren:

// Load Resource Directory
loader->AddPackage("main.murlres", ILoader::LOAD_MODE_BACKGROUND);

// Load Binary Package
loader->AddPackage("main.murlpkg", ILoader::LOAD_MODE_BACKGROUND);

// Load what is preferred in the current build mode
loader->AddPackage("main", ILoader::LOAD_MODE_BACKGROUND);

Alternativ ist es möglich, die Reihenfolge bevorzugter Typen global zu ändern, und zwar mit der Methode SetPreferredResourcePackageType() des IEngineConfiguration-Objekts.

Zu beachten
Tipp! Mit der Methode SetResourceFileCategory() kann das Arbeitsverzeichnis, in dem der Package-Loader nach Ressourcen sucht, umgestellt werden. Standardmäßig wird auf Desktop-Plattformen im Debug-Mode im aktuellen Verzeichnis gesucht und im Release-Mode im Applikations-Ressource-Bundle. Mit z.B. engineConfig->SetResourceFileCategory(IEnums::FILE_CATEGORY_CURRENT) wird immer im aktuellen Verzeichnis gesucht.

Übungen

  • Wie muss die Position des Würfels geändert werden, damit die rechte Kante am rechten Bildschirmrand abschließt?
  • Wie müssen Größe und Position des Würfels geändert werden, damit der Würfel ein Feld von 600x600 Pixel im Fenster ausfüllt?
  • Verändere Größe und das Seitenverhältnis des Fensters. Wie ändert sich die Anzeige?
  • Wie ändert sich die Anzeige, wenn bei der Kamera zusätzlich der Wert fieldOfViewX="400" angegeben wird?

Version 3: Transform-Knoten

Als nächstes wollen wir den Würfel noch drehen. Dafür müssen wir uns eine Referenz auf das Würfelobjekt holen und in jedem Schritt ein wenig weiterdrehen.

In den Dateien murl_logic_graph_node_types.h und murl_logic_graph_node.h sind passende Logik-Klassen zum Manipulieren und Beobachten für alle Graphenknoten definiert:

  • murl/base/include/engine/logic/murl_logic_graph_node.h
  • murl/base/include/engine/logic/murl_logic_graph_node_types.h

Eine passende Klasse zum Drehen des Würfels ist die Klasse Logic::TransformNode. Diese Klasse eignet sich zum Transformieren aller Graphenknoten, die das Interface Graph::ITransform implementieren. Wir deklarieren für unsere Logik-Klasse die neue Membervariable mCubeTransform vom Typ TransformNode

        class CubeLogic : public Logic::BaseProcessor
        {
        public:
            CubeLogic(Logic::IFactory* factory);
            virtual ~CubeLogic();
            
        protected:
            virtual Bool OnInit(const Logic::IState* state);
            virtual Bool OnDeInit(const Logic::IState* state);
            virtual void OnProcessTick(const Logic::IState* state);

            Murl::Logic::TransformNode mCubeTransform;
        };

Als nächstes müssen wir die Variable mCubeTransform mit dem Würfelknoten im Graphen "verbinden". Dies geschieht in der OnInit()-Methode. Mit der Methode GetGraphRoot() des Logic::IState-Objekts erhalten wir einen Zeiger auf den Wurzelknoten im Graphen. Dieser Knoten existiert immer – auch in einem völlig leeren Graphen.

Die Methode mCubeTransform.GetReference() sucht ausgehend vom Wurzelknoten den Knoten mit der angegebenen ID und speichert eine Referenz darauf im Objekt mCubeTransform.
Als Rückgabeparameter wird ein Zeiger auf ein Logic::IObservableNode-Objekt geliefert. Solange eine Referenz auf einen Graphenknoten besteht, muss sichergestellt werden, dass dieser Graphenknoten nicht durch z.B. UnloadPackage() aus dem Graphen entfernt wird. Anderenfalls könnte es passieren, dass die Referenz auf einen nicht mehr existierenden Graphenknoten verweist und damit auf einen zufälligen Speicherbereich.

Daher existiert in der Murl Engine ein Sicherheitsmechanismus, welcher sicherstellt, dass kein Graphen-knoten entfernt wird, solange noch zumindest eine Referenz darauf verweist. Wird eine Referenz nicht mehr benötigt, also spätestens im Destruktor, muss diese mit RemoveReference() wieder freigegeben werden:

// Get Reference for mCubeTransform
Logic::IObservableNode* observableNode = mCubeTransform.GetReference(root, "myCube01");

// Remove Reference 
observableNode->RemoveReference(); 
// alternatively  
mCubeTransform.RemoveReference();

Die Klasse BaseProcessor bietet die bequeme Methode AddGraphNode(). Damit kann die Freigabe der Referenzen dem BaseProcessor-Objekt überlassen werden. Alle Referenzen werden beim Aufruf des Destruktors vom BaseProcessor Objekt automatisch freigegeben.

Bool App::CubeLogic::OnInit(const Logic::IState* state)
{
    state->GetLoader()->UnloadPackage("startup");
    
    Graph::IRoot* root = state->GetGraphRoot();
    AddGraphNode(mCubeTransform.GetReference(root, "myCube01"));
    
    if (!AreGraphNodesValid())
    {
        return false;
    }
    
    state->SetUserDebugMessage("Cube Init succeeded!");
    return true;
}

Nachdem wir eine Referenz für den Würfelknoten in mCubeTransform gespeichert haben, können wir damit den Würfel drehen. Dies erfolgt in der OnProcessTick()-Methode. Die Drehung erfolgt mit der Methode SetRotation() des Graph::ITransform-Interfaces. Für die X-Achse und die Y-Achse wird der Wert angle festgelegt. Dieser berechnet sich einfach aus der aktuellen Tick Time und wird mit Math::Fmod() auf den Wertebereich 0 – 2*π begrenzt.

Zusätzlich zeigen wir mit der Funktion SetUserDebugMessage() den aktuellen Winkel rechts oben an. Dafür muss allerdings zuerst die Double Variable mit der Funktion Util::DoubleToString() in einen String umgewandelt werden:

void App::CubeLogic::OnProcessTick(const Logic::IState* state)
{
    Double angle = state->GetCurrentTickTime();
    angle = Math::Fmod(angle, Math::TWO_PI);
    mCubeTransform->SetRotation(angle, angle, 0);
    state->SetUserDebugMessage(Util::DoubleToString(angle));
}

Das Ergebnis ist ein sich drehender Würfel.

tut0101_cube_spinning.png
Drehender weißer Würfel


Copyright © 2011-2024 Spraylight GmbH.