If a required platform-dependent feature is not supported by the Murl Engine, a custom control can be used to add this feature. This tutorial shows with a fairly simple example, how you can implement your own custom controls.
Quick links to the individual sections in this tutorial:
Base Class
All control classes implement the IControlable
interface with the methods:
GetName()
FrameUpdate()
LogicUpdate()
ConfigChanged()
This applies to custom controls as well as to provided controls like the IMusicPlayerControl
, the IRumbleControl
, the IWebControl
, the IScreenshotControl
etc.
Custom controls additionally implement the ICustomControlable
interface with the methods:
Init()
DeInit()
PauseEngine()
ContinueEngine()
SuspendEngine()
ResumeEngine()
- ...
The class CustomControlable
provides an empty implementation of both interfaces and can be used as base class for custom controls.
MyControl
For our simple custom control we create a new abstract class MyControl
with CustomControlable
as base class and the two pure virtual methods Test()
and TestBack()
. Additionally we define the static method Create()
which we will use later to create a MyControl
object.
#include "murl_custom_controlable.h" namespace Murl { namespace App { class MyControl : public CustomControlable { public: static MyControl* Create(const String& name); MyControl(const String& name) : CustomControlable(name) {} virtual ~MyControl() {} virtual Bool Test(const String& text) = 0; virtual Bool TestBack(const String& text) = 0; }; } }
To simplify matters we implement the empty constructor and the empty destructor directly in the header file.
Registration
We are now able to create an object of our MyControl
class and to register the object with the Output::IDeviceHandler
. Ideally we do this in the OnInit()
method of our logic class.
Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler(); Output::IDeviceHandler* outputDeviceHandler = deviceHandler->GetOutputDeviceHandler(); // create MyControl instance mMyControl = MyControl::Create("MyControl"); // add custom control to outputDeviceHandler if (!outputDeviceHandler->AddCustomControl(mMyControl)) { return false; }
After the registration the methods of the IControlable
and CustomControlable
interface are called on their respective events.
We call the methods Test()
and TestBack()
if the corresponding button (or key) has been pressed.
if (deviceHandler->WasRawKeyPressed(RAWKEY_1) || mButton01->WasReleasedInside()) { mMyControl->Test("b1 pressed"); } if (deviceHandler->WasRawKeyPressed(RAWKEY_2) || mButton02->WasReleasedInside()) { mMyControl->TestBack("b2 pressed"); }
We also have to deregister the custom control object in the OnDeInit()
method. This can be done with the reference mMyControl
or with the name specified during creation.
Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler(); Output::IDeviceHandler* outputDeviceHandler = deviceHandler->GetOutputDeviceHandler(); // remove custom control from outputDeviceHandler if (!outputDeviceHandler->RemoveCustomControl(mMyControl)) { return false; }
Default Implementation
For the new abstract class we create a default implementation in the class MyControlDefault
.
#include "my_control.h" namespace Murl { namespace App { class MyControlDefault : public MyControl { public: MyControlDefault(const String& name); virtual ~MyControlDefault(); virtual Bool Test(const String& text); virtual Bool TestBack(const String& text); }; } }
No additional methods (Init()
, DeInit()
, FrameUpdate()
...) are needed for the default implementation.
The Create
method creates a new MyControlDefault
object and returns a pointer to the new object.
#include "my_control_default.h" using namespace Murl; App::MyControl* App::MyControl::Create(const String& name) { return new App::MyControlDefault(name); } App::MyControlDefault::MyControlDefault(const String& name) : MyControl(name) {} App::MyControlDefault::~MyControlDefault() {}
The two methods Test()
and TestBack()
just print a debug message.
Bool App::MyControlDefault::Test(const String& text) { MURL_TRACE(0, "text=\"%s\"", text.Begin()); return true; } Bool App::MyControlDefault::TestBack(const String& text) { MURL_TRACE(0, "text=\"%s\"", text.Begin()); return true; }
This default implementation can be used on all platforms where no specific implementation exists.
Android Implementation
Next we implement an Android implementation in the class MyControlAndroid
.
The method Test()
should call a Java method with the same name. Additionally we define a C++ method which is called from Java.
- Note
- Calls from C++ to Java and vice versa are done through the Java Native Interface (JNI) - see also Java Native Interface Specification
The header for Android is similar to the class MyControlDefault
but we additionally need the methods Init()
and DeInit()
as well as two member variables for the NativePlatform
object and the JniBridge
object.
#include "my_control.h" #include "murl_platform_android_native_platform.h" #include "murl_platform_android_jni_bridge.h" namespace Murl { namespace App { class MyControlAndroid : public MyControl { public: MyControlAndroid(const String& name); virtual ~MyControlAndroid(); virtual Bool Test(const String& text); virtual Bool TestBack(const String& text); protected: virtual Bool Init(IPlatform* platform); virtual Bool DeInit(); Platform::Android::NativePlatform* mPlatform; Platform::Android::JniBridge* mJniBridge; }; } }
We again use the Create()
method to create a new MyControlAndroid
object.
#include "my_control_android.h" using namespace Murl; App::MyControl* App::MyControl::Create(const String& name) { return new App::MyControlAndroid(name); } App::MyControlAndroid::MyControlAndroid(const String& name) : MyControl(name) { } App::MyControlAndroid::~MyControlAndroid() { }
The Init()
method is used to get pointers to the JniBridge
and the NativePlatform
. Additionally we store a pointer to the MyControlAndroid
object in the static variable sMyControl
.
static App::MyControl* sMyControl = 0; Bool App::MyControlAndroid::Init(IPlatform* platform) { mPlatform = dynamic_cast<Platform::Android::NativePlatform*>(platform); if (mPlatform == 0) { MURL_TRACE(0, "Failed to get Android platform handle."); return false; } mJniBridge = mPlatform->GetJniBridge(); if (mJniBridge == 0) { MURL_TRACE(0, "Failed to get JNI bridge."); return false; } sMyControl = this; return true; } Bool App::MyControlAndroid::DeInit() { sMyControl = 0; return true; }
Cpp2Java
The class JniBridge
provides useful template methods to easily call static Java methods:
CallStaticJavaProc
CallStaticJavaIntFunc
CallStaticJavaObjectFunc
The methods are defined in the file murl_platform_android_jni_bridge.h:
murl/base/source/platform/android/murl_platform_android_jni_bridge.h
We use the template method CallStaticJavaProc
to call a Java method without return parameter.
The template parameters define the number and type of the parameters for the Java method (<jlong, jstring>
).
The first function parameter defines the Java method. Per definition, this is the name of the Java class followed by a point and the name of the Java method ("MyTestClass.Test"
).
As second parameter we assign a platform handle (mPlatform->GetHandle()
) and as third parameter a String ("Greetings from CPP land!"
).
- Note
- The platform handle parameter is for demonstration purpose only and is not really required for this example. The handle can be used in the Java method to get a reference of the
MurlPlatform
object, which provides methods to access the Android activity, Android context etc.
Bool App::MyControlAndroid::Test(const String& text) { MURL_TRACE(0, "text=%s", text.Begin()); mJniBridge->CallStaticJavaProc<jlong, jstring>("MyTestClass.Test", mPlatform->GetHandle(), "Greetings from CPP land!"); return true; }
The corresponding Java method:
We store the Java file in the directory
source/android/java/com/mycompany/mystuff/MyTestClass.java
The Java method Test()
prints debug messages with Log.d()
and calls afterwards the method TestBack()
.
To add the Java file to the build process we need to specify it in the Makefile with MURL_ANDROID_JAVA_FILES
. Additionally we need to specify the Java class with MURL_ANDROID_JNI_CLASSES
as JNI class to register it with the JNI bridge.
- Note
- It is also possible to specify Java archives (JAR files) in the Makefile with
MURL_ANDROID_JAR_FILES
.MURL_ANDROID_JAR_PATH := source/android/jarMURL_ANDROID_JAR_FILES += myArchive.jar
Java2Cpp
To call a native C++ method from Java, we need to create a Java method and mark it as native
.
The corresponding function in C++ is named Java_com_mycompany_mystuff_MyTestClass_TestBack()
.
extern "C" JNIEXPORT void JNICALL Java_com_mycompany_mystuff_MyTestClass_TestBack(JNIEnv * env, jobject obj, jstring text) { String textStr; Platform::Android::JniData::GetCParam(env, textStr, text); MURL_TRACE(0, "text=%s", textStr.Begin()); if (sMyControl == 0) { MURL_TRACE(0, "Java_com_mycompany_mystuff_MyTestClass_TestBack(): No test control"); return; } sMyControl->TestBack(textStr); }
The statement extern "C"
instructs the compiler to export the function according to the C specification (instead of the C++ specification). JNIEXPORT
and JNICALL
are JNI macros used to ensure that the corresponding function is exported with appropriate linkage.
The naming convention for the C++ function is Java_{package_and_classname}_{function_name}(JNI arguments)
with dots replaced by underscores.
The first parameter is always a reference to the JNI environment, which lets you access all the JNI functions. The second parameter is always a reference to the this
Java object. The third parameter is the first parameter of the Java method. In our case this is a Java-String object (jstring
).
The function GetCParam
converts the jstring into a Murl Engine String
. This and other functions to convert Java data types to C++ data types and vice versa are defined in the file murl_platform_android_jni_data.h
.
murl/base/source/platform/android/murl_platform_android_jni_data.h
The static variable sMyControl
can be used to access the MyControlAndroid
object and to call the actual method TestBack()
.
The Android implementation is now complete.
Depending on the platform, we use either the default implementation or the Android implementation. Thus, we have to exclude the Android implementation in the Visual Studio and Xcode projects. For Android we have to exclude the default implementation.
To do so, we adjust the common Makefile:
When the button b1 is pressed we get the expected output:
Proguard
For debug builds the Android implementation is working nicely. But when doing a release build, the Java optimizer Proguard would optimize away our Java class because it looks unused.
Therefore we have to instruct the tool to keep our Java class in any case. To do so we create a text file proguard_MyTestClass.cfg
source/android/proguard_MyTestClass.cfg
and put a keep option for proguard into the file:
To add the file to the build process, we need to specify it in the Makefile as Proguard fragment.
Summary
Cpp2Java
Java2Cpp
Makefile
OSX Implementation
In the same way, we can create implementations for Windows, Linux, OSX or iOS. For those platforms the platform dependent code can be directly written in the C++ files. There is no need to use a JNI bridge like on Android.
As an example we show an OSX implementation. The class MyControlOsx
is declared in the header file my_control_osx.h
.
#include "my_control.h" namespace Murl { namespace App { class MyControlOsx : public MyControl { public: MyControlOsx(const String& name); virtual ~MyControlOsx(); virtual Bool Test(const String& text); virtual Bool TestBack(const String& text); }; } }
The implementation of the class is done in the Objective-C++ file my_control_osx.mm
. In this file we can use C++ code as well as Objective-C code.
We import the file Foundation.h
to be able to use the Objective-C method NSLog()
. The method Create()
is again used to create a MyControlOsx
object.
#include "my_control_osx.h" #import <Foundation/Foundation.h> using namespace Murl; App::MyControl* App::MyControl::Create(const String& name) { return new App::MyControlOsx(name); } App::MyControlDefault::MyControlDefault(const String& name) : MyControl(name) {} App::MyControlDefault::~MyControlDefault() {}
The method Test()
and TestBack()
use NSLog()
to print a message.
Bool App::MyControlOsx::Test(const String& text) { NSString* nsString = [NSString stringWithUTF8String:text.Begin()]; NSLog(@"text=%@", nsString); return true; } Bool App::MyControlOsx::TestBack(const String& text) { NSString* nsString = [NSString stringWithUTF8String:text.Begin()]; NSLog(@"text=%@", nsString); return true; }
We need to include these two files to the OSX Xcode project and exclude the files from the default and Android implementation.