Sollte ein benötigtes Plattform-Feature von der Murl Engine nicht angeboten werden, so kann dies in einem Custom-Control implementiert werden. Dieses Tutorial zeigt mit einem einfachen Beispiel, wie eigene Controls implementiert werden können.
Quicklinks zu den einzelnen Abschnitten in diesem Tutorial:
- Basisklasse
- MyControl
- Default-Implementierung
- Android-Implementierung
- OSX-Implementierung
- Screenshot
Basisklasse
Alle Control-Klassen implementieren das IControlable
Interface mit den Methoden:
GetName()
FrameUpdate()
LogicUpdate()
ConfigChanged()
Das betrifft sowohl eigene Custom-Controls als auch bereitgestellte Controls, wie das IMusicPlayerControl
, das IRumbleControl
, das IWebControl
, das IScreenshotControl
etc.
Custom-Controls implementieren zusätzlich das ICustomControlable
Interface mit den Methoden:
Init()
DeInit()
PauseEngine()
ContinueEngine()
SuspendEngine()
ResumeEngine()
- ...
Die Klasse CustomControlable
bietet eine leere Default-Implementierung der beiden Interfaces und kann als Basisklasse für eigene Custom-Controls verwendet werden.
MyControl
Für unser einfaches Custom-Control erstellen wir eine neue abstrakte Klasse MyControl
mit CustomControlable
als Basisklasse und den beiden pure virtual Methoden Test()
und TestBack()
. Zusätzlich definieren wir eine statische Create()
Methode, die wir später nutzen werden, um ein MyControl
Objekt zu erzeugen.
#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; }; } }
Der Einfachheit halber implementieren wir den leeren Konstruktor und den leeren Destruktor gleich in der Header Datei.
Registrierung
Wir können nun ein Objekt unserer MyControl
-Klasse erstellen und beim Output::IDeviceHandler
registrieren. Das machen wir am besten in der OnInit()
Methode unserer Logik-Klasse:
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; }
Nach der Registrierung werden die im IControlable
und CustomControlable
Interface deklarierten Methoden bei den jeweiligen Events aufgerufen.
Wir rufen die definierten Methoden Test()
bzw. TestBack()
auf, wenn die entsprechende Taste gedrückt wurde:
if (deviceHandler->WasRawKeyPressed(RAWKEY_1) || mButton01->WasReleasedInside()) { mMyControl->Test("b1 pressed"); } if (deviceHandler->WasRawKeyPressed(RAWKEY_2) || mButton02->WasReleasedInside()) { mMyControl->TestBack("b2 pressed"); }
In der OnDeInit()
Methode müssen wir das Custom-Control-Objekt auch wieder deregistrieren. Das können wir mit der Referenz mMyControl
machen oder mit dem Namen, den wir bei Create()
angegeben haben.
Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler(); Output::IDeviceHandler* outputDeviceHandler = deviceHandler->GetOutputDeviceHandler(); // remove custom control from outputDeviceHandler if (!outputDeviceHandler->RemoveCustomControl(mMyControl)) { return false; }
Default-Implementierung
Für die neue abstrakte Klasse definieren wir eine Default-Implementierung in der Klasse 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); }; } }
Für die Klasse MyControlDefault
werden sonst keine weiteren Methoden (Init()
, DeInit()
, FrameUpdate()
...) benötigt.
Die Create
Methode erzeugt ein neues MyControlDefault
Objekt und gibt einen Zeiger darauf zurück.
#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() {}
Die beiden Methoden Test()
und TestBack()
geben lediglich eine Debug-Meldung aus.
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; }
Diese Default-Implementierung kann für alle Plattformen verwendet werden, für die keine spezielle Implementierung existiert.
Android-Implementierung
Als nächstes implementieren wir eine eigene Android-Implementierung in der Klasse MyControlAndroid
.
Die Methode Test()
soll dabei eine gleichnamige Methode in Java aufrufen. Zusätzlich definieren wir eine C++ Methode, die wir von Java aus aufrufen.
- Zu beachten
- Aufrufe von C++ nach Java und umgekehrt laufen immer über das Java Native Interface (JNI). Siehe auch Java Native Interface Specification.
Der Header für Android sieht ähnlich aus wie bei der Klasse MyControlDefault
, allerdings benötigen wir auch die Methoden Init()
und DeInit()
sowie zwei Membervariablen für das NativePlatform-Objekt
und das JniBridge-Objekt
.
#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; }; } }
Wir verwenden wieder die Create()
Methode um ein MyControlAndroid
Objekt zu erzeugen.
#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() { }
In der Init()
Methode holen wir uns einen Zeiger auf die JniBridge
und die NativePlatform
. Zusätzlich speichern wir einen Zeiger auf das MyControlAndroid
-Objekt in der statischen 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
Die Klasse JniBridge
bietet hilfreiche Template-Methoden für den Aufruf von statischen Java-Methoden:
CallStaticJavaProc
CallStaticJavaIntFunc
CallStaticJavaObjectFunc
Die Methoden sind in der Datei murl_platform_android_jni_bridge.h
definiert:
murl/base/source/platform/android/murl_platform_android_jni_bridge.h
Mit der Template-Methode CallStaticJavaProc
können wir eine Java-Methode ohne Rückgabewert aufrufen.
Die Template-Parameter definieren die Anzahl und Art der Übergabeparameter für die Java-Methode (<jlong, jstring>
)
Der erste Funktions-Parameter definiert die aufzurufende Java-Methode. Per Definition ist das der Name der Java-Klasse gefolgt von einem Punkt und dem Namen der Methode ("MyTestClass.Test"
).
Als zweiten Parameter übergeben wir einen Plattform-Handle (mPlatform->GetHandle()
) und als dritten Parameter einen String ("Greetings from CPP land!"
).
- Zu beachten
- Das Plattform-Handle ist für das Beispiel nicht notwendig und wird nur zu Demonstrationszwecken übergeben. Mit dem Handle kann im Java-Code eine Referenz auf das
MurlPlatform
Objekt geholt werden, das Methoden für den Zugriff auf die Android Activity, Context etc. bietet.
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; }
Die korrespondierende Java-Methode sieht so aus:
Die Java-Datei speichern wir im Verzeichnis
source/android/java/com/mycompany/mystuff/MyTestClass.java
Die Java-Methode Test()
gibt Debug-Meldungen mit Log.d()
aus und ruft anschließend die Methode TestBack()
auf.
Die Java-Datei müssen wir auch im Makefile mit MURL_ANDROID_JAVA_FILES
angeben, damit sie beim Build-Vorgang berücksichtigt wird. Außerdem müssen wir die Java-Klasse mit MURL_ANDROID_JNI_CLASSES
als JNI-Klasse deklarieren, damit sie von der JNI-Bridge registriert wird.
- Zu beachten
- Es können auch Java-Archive (JAR-Dateien) im Makefile mit
MURL_ANDROID_JAR_FILES
angegeben werden.MURL_ANDROID_JAR_PATH := source/android/jarMURL_ANDROID_JAR_FILES += myArchive.jar
Java2Cpp
Um eine native C++ Methode aus Java aufrufen zu können, muss zunächst eine Java-Methode als native
deklariert werden:
Die korrespondierende Funktion wird in C++ mit Java_com_mycompany_mystuff_MyTestClass_TestBack()
definiert:
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); }
extern "C"
weist den Compiler an, die Funktion nach der C Spezifikation zu exportieren (statt der C++ Spezifikation). JNIEXPORT
und JNICALL
sind JNI-Makros, die sicherstellen, dass der Funktionsname richtig exportiert wird.
Die Namenskonvention für die C++ Funktion ist Java_{package_and_classname}_{function_name}(JNI arguments)
, wobei Punkte durch Unterstriche ersetzt werden.
Der erste Parameter liefert immer einen Zeiger auf das JNI-Environment. Der zweite Parameter liefert immer einen Zeiger auf das this
Java-Objekt. Der dritte Parameter ist der erste Parameter der Java-Methode. In unserem Fall ist das ein Java-String Objekt (jstring
).
Die Funktion GetCParam
wandelt den jstring
in einen Murl Engine String
um. Diese und weitere Funktionen zum Umwandeln von Java-Datentypen in CPP-Datentypen und umgekehrt sind in der Datei murl_platform_android_jni_data.h
definiert.
murl/base/source/platform/android/murl_platform_android_jni_data.h
Über die statische Variable sMyControl
kann auf das MyControlAndroid
-Objekt zugegriffen werden und die eigentliche Methode TestBack()
aufgerufen werden.
Damit ist die Android-Implementierung fertig.
Abhängig von der Plattform, verwenden wir entweder die Default-Implementierung oder die Android-Implementierung für unser MyControl-Objekt
. Wir müssen also in den Visual-Studio- und Xcode-Projekten die Android-Implementierung ausklammern. Umgekehrt müssen wir bei Android die Default-Implementierung ausklammern.
Dafür passen wir das Common-Makefile an:
Beim Drücken des b1-Button erhalten wir den erwarteten Output:
Proguard
Im Debug-Build funktioniert die Android-Implementierung problemlos. Im Release-Build würde der Java-Optimizer Proguard die Java-Klasse allerdings wegoptimieren, da diese aus Java-Sicht unbenutzt scheint.
Wir müssen das Tool also anweisen, unsere Java-Klasse jedenfalls zu behalten. Dafür erstellen wir die Text-Datei proguard_MyTestClass.cfg
source/android/proguard_MyTestClass.cfg
und geben als Inhalt eine Keep-Option für Proguard an:
Diese Datei müssen wir wieder im Common-Makefile als Proguard-Input angeben, damit Sie im Build-Prozess berücksichtigt wird.
Zusammenfassung
Cpp2Java
Java2Cpp
Makefile
OSX-Implementierung
Nach demselben Prinzip können wir auch eigene Implementierungen für Windows, Linux, OSX (oder iOS) entwickeln. Dabei ist der Umweg über eine JNI-Bridge (wie bei Android) nicht notwendig, da der plattformabhängige Code direkt in den C++-Dateien geschrieben werden kann.
Exemplarisch zeigen wir eine OSX-Implementierung und erstellen dafür die Klasse MyControlOsx:
#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); }; } }
Die Implementierung der Klasse erfolgt in der Objective-C++ Datei my_control_osx.mm
. In dieser Datei können wir sowohl C++ Code als auch Objective-C Code verwenden.
Wir importieren die Datei Foundation.h
um die Objective-C Methode NSLog()
verwenden zu können. In der Methode Create()
erzeugen wir wieder das MyControlOsx
Objekt.
#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() {}
Die Methoden Test()
und TestBack()
verwenden NSLog()
um eine Meldung auszugeben.
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; }
Wir müssen wiederum diese beiden Dateien dem OSX Xcode-Projekt hinzufügen und die Default- und Android-Implementierungen ausklammern.