Tutorial #00: Hello, World!

The first tutorial shows how to create a minimal version of an application using the Murl engine. The result will be an empty window. This basic structure is used in all subsequent tutorials and shows the fundamental interaction between framework and user code.

Please see the Quick Start Guide for detailed steps on how to install your host environment for developing Murl applications, if you haven't done so yet.

The tutorial sample projects can be found in the folder tutorials/chapter01/00_hello_world/project, with the actual project files located in additional sub-folders for each provided target platform, such as win32/vs2008/hello_world.sln for Visual Studio 2008 or osx/xcode3/hello_world.xcodeproj for Apple XCode 3.x / 4.x. These project files contain one or more individual targets (here: V1 to V3), corresponding to the different versions discussed below.

Version 1: Murl Engine and App Code

Application (App) = murl engine + user code

For our minimum application we need three files:

  • 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

The connection between the Murl engine and our own app code is defined in the C++ header file murl_app.h which is part of the framework API. It contains two external function declarations in the namespace Murl::App.

#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__

Upon application start the framework calls the function CreateApp(), which has to be uniquely defined in our user code. This function is expected to return a pointer to an object of the type IApp.

Upon application shutdown, the framework calls the function DestroyApp(), which takes the previously returned IApp object as the only parameter.

A new application has always to implement these two functions within the given namespace Murl::App. In this case, they are implemented in the file hello_world.cpp.

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

using namespace Murl;

The #include directives in the first and second line refer to the file murl_app.h and the header file for own app class. The using namespace statement references the top-level Murl namespace. By doing so, it is possible to directly reference all classes, variables and functions which are part of the Murl namespace.

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

The function CreateApp() expects an IApp object as return value. We use the new HelloWorldApp statement to create a new instance of our HelloWorldApp class, which has to implement the IApp interface. The return statement then passes this newly created instance as return parameter to the multimedia framework which made the actual call.

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

The framework calls the function DestroyApp() upon application shutdown. The call to Util::Release() invokes the destructor of our previously created HelloWorldApp instance and releases its memory.

Generally, it is highly recommended to use the function Util::Release() instead of delete as this function performs a null-pointer check on the given pointer, and clears the pointer after deleting the object.

App Class

In the header file hello_world_app.h our HelloWorldApp class within the namespace Murl::App is declared. Inside this class we define its constructor, destructor and the necessary methods to satisfy the IApp interface, which we want to derive our class from. As a convenience, we do not directly implement the IApp interface. Instead, we derive from the abstract AppBase class, which is also provided by the framework API. This class already derives from the IApp interface, hereby providing an empty implementation for most of the interface methods.

#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 the file hello_world_app.cpp the previously declared methods (i.e. the actual implementation) are defined. In our very simple case, the function bodies of our constructor and destructor remain empty. The Init() and DeInit() methods both always return true to indicate success.

The Configure() method defines the configuration for the app. The given settings of the IEngineConfiguration object can be manipulated in order to read and modify the engine’s general behavior. Additionally, by using the IPlatformConfiguration object, general platform-specific settings can be queried, and the IAppConfiguration object can be used to control app-specific behavior. Both can be accessed via the IEngineConfiguration object.

In our case, we define a title text to be shown in the title bar of the application window through the SetWindowTitle() call and explicitly switch to windowed instead of full screen mode by calling SetFullScreenEnabled(false). Usually, there is no "windowed mode" on mobile devices, so this call will have no effect on e.g. iOS and Android. A detailed description of all configuration parameters can be found in the interface declaration:

  • 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
Note
Tip – Visual Studio! Right-clicking the file or class name and selecting "Go to definition" quickly opens the corresponding file.
#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;
}

And that’s it for our minimal application. The result is a window with uninitialized content. On Windows the result is a black window, whereas on Mac OSX random data is shown in the window. In order to display a simple text, the "debug" package provided by the framework will be loaded as a next step.

To build and start version 1 of the Hello World tutorial in Visual Studio, simply right-click the hello_world_v1 target in the solution explorer and select "Set as StartUp Project" from the pop-up menu and press F5. In XCode 4, select the "hello_world_v1" configuration in the title bar and press the "Run" button.

tut0100_empty_window.png
Hello World V1 output window

Version 2: Debug Package

Essentially, the Murl engine represents a scene graph framework. The scene graph is an object-oriented data structure which represents the virtual scene that is to be displayed. So far, we have only created an empty class, which implements the IApp interface, and passed an instance of this class to the framework. The scene graph itself remained empty as our output window.

One way to define virtual scenes in the Murl engine is to create a scene description in one or more XML files, which must be included in one or more packages. In order to actually use a scene as part of the scene graph, the application is able load these packages into memory and instantiate the (sub-)graphs contained therein. One such package, named debug.murlpkg, is already available immediately after creating a new project, as it is directly built into the Murl Engine.

This "debug" package has a special meaning: When loaded, it directly connects to the framework and allows showing certain statistical data (such as rendering performance) and user-definable debug messages on screen.

Note
If needed, the "debug" package always has to be the first package loaded by the application!

In order to add new nodes to the (empty) scene graph, we have to load them. For this purpose, the Murl engine provides an ILoader object and its AddPackage() method.

Loading a package is usually done in the Init() method, which gets called by the Murl engine upon application start. We can retrieve a pointer to the ILoader object from the given IAppState object via the GetLoader() method.

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

By doing so, the newly created nodes result in the display of development-related information in the upper-left corner of the window such as the current frames per second.

tut0100_debug_package.png
Hello World V2 output window showing system debug statistics

Again, to build and start version 2 of the tutorial in Visual Studio, set the hello_world_v2 target as start-up project and press F5. In XCode 4, select the "hello_world_v2" configuration and press "Run".

Version 3: Logic Class

In order to obtain information from the ILoader object about the time the scene has fully been loaded and to manipulate the scene, an object of the type Logic::IEngineProcessor can be passed to the loader->AddPackage() method. The interface Logic::IEngineProcessor declares methods, which can be called by the Murl Engine during certain events in order to allow the application to modify the graph.

To create a Logic::IEngineProcessor object for a specific purpose, we could define a new class directly implementing that interface. However, the simpler (and recommended) way to do so is to derive from the base class Logic::BaseProcessor, which contains a Logic::IProcessor object implementing the Logic::IEngineProcessor interface and provides useful additional functionality.

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

For our new class two additional files are necessary:

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

Within the file hello_world_logic.h the new class HelloWorldLogic is declared, which derives from the BaseProcessor base class mentioned above.

#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();

The constructor requires a reference to a Logic::IFactory object, which is needed for calling the constructor of the base class.

In addition, we override the virtual method OnInit() present in the base class:

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

#endif  // __HELLO_WORLD_LOGIC_H__

In the file hello_world_logic.cpp we start the actual implementation of our new class:

#include "hello_world_logic.h"

using namespace Murl;

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

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

Both constructor and destructor of our logic class remain empty. The IFactory object reference given to the constructor is directly passed to the constructor of the base class.

The overridden OnInit() method is exactly called once by the framework, directly after the corresponding package has been successfully loaded. This method can be used for retrieving references to nodes in the scene graph, error checking and general initialization of the graph:

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

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

The AreGraphNodesValid() method can be used to check if all requested node references were found in the present scene graph. If not, an error message is printed to the console. (So far, we do not request any node references, so this will always return true).

The OnInit() method has one single parameter, which represents a pointer to a Logic::IState object. The IState object provides methods to query relevant information, such as current and most recent tick duration. Additionally, it provides access to system components such as the configuration or the root of the scene graph as well as methods such as SetUserDebugMessage() or AddUserDebugMessage() to display simple debug messages on screen.

Hello World Message

To bring our new logic class to life, we have to first create a new instance and pass this instance to the ILoader object. It is recommended to also take care of a proper cleanup upon shutdown. That means to make sure to destroy that instance in the application’s DeInit() method, which gets called before the application is terminated.

In the file hello_world_app.h, we add a new member variable mLogic which can hold a pointer to a HelloWorldLogic object. Before this step, we need to introduce our HelloWorldApp to our new HelloWorldLogic class, which can be done via a forward declaration:

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;
        };
    }
}

Member variables should always be initialized to a default value, so we extend the constructor in the file hello_world_app.cpp to assign a null pointer to our member variable in the initializer list. We also need to include the header file hello_world_logic.h for the forward-declared HelloWorldLogic class:

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

using namespace Murl;

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

We extend the application’s Init() method to create a new logic instance using the statement new HelloWorldLogic(appState->GetLogicFactory()); which is then assigned to our mLogic member variable. The call to loader->AddPackage() is also changed. As a third parameter we pass the actual engine processor contained in our new logic class with GetProcessor(), when the debug package has finished loading:

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;
}

Instead of specifying mLogic->GetProcessor(), it is also possible to simply write *mLogic as the BaseProcessor class overloads the "*" operator, which then also returns the actual IEngineProcessor object.

By using Util::Release(mLogic) via the DeInit() method, the memory of the created instance is cleared upon program shutdown:

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

As a result, the additional text "Hello World!" is now displayed in the upper right corner of the window:

tut0100_logic_message.png
Hello World V3 output window showing the Hello World message

Exercises


Copyright © 2011-2024 Spraylight GmbH.