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 enginehello_world_app.cpp
: C++ file of our app classhello_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.
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.
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 classhello_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:
Exercises
- Try to comprehend the individual steps.
- Extend the
Configure()
method by also specifying a "product name" in theIEngineConfiguration
. - Open the interface header file for the
ILoader
class and find out, which other options exist for theLoadMode
parameter besidesLOAD_MODE_STARTUP
.