Android Plugins part 4: No plugins! – ShiVa Engine

Android Plugins part 4: No plugins!

For part 4 of the Android Plugin tutorial series, we are not going to use plugins at all. Instead, we will be looking at ShiVa’s event hook mechanism and modify the Android Studio project directly. Before ShiVa 1.9, this was the only method of extending your game with native Java code, and therefor you will find many older tutorials featuring this technique. Despite its simplicity, it has one major drawback: Since you have to modify the project files themselves, you will lose your custom code every time you make a change in ShiVa and have to export to Android Studio again. For a rapid development process and easy debugging, making true plugins is ultimately quicker, safer and easier to re-use. For everyone else who either cannot or does not want to make plugins the way we talked about in parts 1 to 3, this tutorial is for you.

ShiVa events

We will once again use the Fire scene setup from previous tutorials. Instead of calling plugin functions, all communication between ShiVa and native code is handled through ShiVa AI events. If you have a look at the JavaConnectNoplug AI, you can find a number of empty events, such as

function JavaConnectNoplug.onDialogJava ( sQuestion )
end

These event functions all have empty bodies. Our goal will be to hook into these events and execute some native code every time a sendEvent() call to these events is made. Here is the full list of events we are going to use, as well as the test calls:

Believe it or not, this is all the ShiVa setup needed this time, which makes this technique so deceptively simple. You can now export the project to Android Studio and continue coding C++ and Java in there. If you plan on accessing JARs from your native code, add them to the Additional Files tab on export. I will be using the jartools.jar from part 3 with exactly the same code as before.

S3DClient hooks

Instead of writing all your native code into a plugin, you will need to modify the main C++ game file directly. This file is called S3DClient.cpp and can be found cpp directory of your project. As a first step, we will declare all the empty event functions we made in ShiVa as hooks for native code. Scroll down to around line 730 and add all your events through the S3DClient_InstallCurrentUserEventHook() function:

S3DClient_InstallCurrentUserEventHook("AI_NAME", "EVENT_NAME", CPP_CALLBACK, nullptr);

In our case, it looks like this:

This will forward all event calls to the empty ShiVa functions and forward them to the specified C++ function. These callback functions all have the same signature, regardless of how many argument the actual event takes:

    void init (unsigned char _iArgumentCount, const void * _pArguments, void * _pUserData);
    void addNums (unsigned char _iArgumentCount, const void * _pArguments, void * _pUserData);
    void showStr (unsigned char _iArgumentCount, const void * _pArguments, void * _pUserData);
    void makeToast (unsigned char _iArgumentCount, const void * _pArguments, void * _pUserData);
    void showYesNo (unsigned char _iArgumentCount, const void * _pArguments, void * _pUserData);

Separating your code as much as possible

But where do you put these functions? As I mentioned before, all your code changes will be lost as soon as you export your game from ShiVa again, so it makes sense to separate your code from S3DClient.cpp as much as possible and create your own C++ files. I decided on putting my files plugless.cpp/.h into the AIModels folder, since this is a location that is included in the build script by default. Right-click on the AIModels folder and create a new C++ class:

To keep things simple, I opted for a namespace instead of a class, that is why all my callbacks have a tut:: prefix. You will still need to include your freshly made C++ header in S3DClient.cpp, otherwise your callbacks will not be visible to the event hooks:

In current versions of Android Studio, there is a sync bug with JNI which prevents your new files from being added to the project automatically. There is a very simple workaround: Open your CMakeLists.txt, add a new empty line and delete it again, which will give you a notification that a project sync is necessary. Do this sync and your new files will compile. A ‘normal’ sync via the toolbar buttons or application menu will not work!

Writing native C++ code without a plugin

To have access to important ShiVa and Android resources, we need to include a few things in our separate C++ file:

// plugless.cpp line 5
#include 
#include 
#define LOGI2(...) __android_log_print(ANDROID_LOG_INFO, "NOPLUGCPP", __VA_ARGS__)

#include "../S3DClient_Wrapper.h"
#include "../S3DX/S3DXAIVariable.h"

All our callback functions work on the same principle. First, you must unpack the arguments. It is always a good idea to check your inputs before using them:

// plugless.cpp line 78
    void addNums (unsigned char _iArgumentCount, const void * _pArguments, void * _pUserData) {
        if (!_bInitOK) {
            LOGI2("ADD called, init failed");
            return;
        }

        if (!_pArguments || (_iArgumentCount != 2)) {
            LOGI2("ADD called, argcount not 2");
            return;
        }

        // capture inputs
        const S3DX::AIVariable * pVariables = (const S3DX::AIVariable *) _pArguments;
        float num1 = pVariables[0].GetNumberValue();
        float num2 = pVariables[1].GetNumberValue();

Then you do your operations, package the results as an AIVariable array and send your output to a ShiVa event:

// plugless.cpp line 93
        float result = num1 + num2;

        // compose output
        S3DX::AIVariable args[2];
        args[0].SetNumberValue( 0.f );
        args[1].SetNumberValue( result );

        S3DClient_SendEventToCurrentUser(_sDefaultAI, _sDefaultEvent, 2, (const void*)args);

To send events in plugins, you would use S3DX::user.sendEvent(). However we are not writing a plugin, so most of the S3DX API is not available to us. We have to use the S3DClient API instead, in our case, S3DClient_SendEventToCurrentUser().

Connecting to native Java code

To make your callbacks interact with native Java code in a JAR, we need to have access to the JNI environment. The simplest way to transmit the JNIEnv pointer is by modifying the GetJNIEnv() function in S3DClient.cpp:

You can then use this pointer to make calls to methods inside your JAR using essentially the same code as we did in the true plugin. GetStaticMethodID() and the funny Java function signatures make an appearance again:

// plugless.cpp line 134
    void makeToast (unsigned char _iArgumentCount, const void * _pArguments, void * _pUserData) {
        const S3DX::AIVariable * pVariables = (const S3DX::AIVariable *) _pArguments;
        const char * msg = pVariables[0].GetStringValue();

        jmethodID pJNIMethodID = _JNIEnv->GetStaticMethodID(_JNIClass, "jToastThreaded", "(Ljava/lang/String;)V");
        _JNIEnv->CallStaticVoidMethod(_JNIClass, pJNIMethodID, _JNIEnv->NewStringUTF(msg));
    }

If your Java code needs to send a notification to ShiVa, the principle is the same as in part 3. First, you need to declare a native function in Java:

// Java2.java line 83
private static native void apjt_msgDefault(final float kType, final String sMsg);

This method corresponds with a declaration in your C++ file:

// plugless.cpp line 27
    static JNINativeMethod noplug_methods[] = {
            {"apjt_msgDefault", "(FLjava/lang/String;)V", (void *)noplug_msgDefault}
    };

The callback in this declaration leads to a function which once again constructs an S3DX::AIVariable array and sends it to a ShiVa event:

// plugless.cpp line 20
    static void noplug_msgDefault (JNIEnv * jE, jclass jC, const float nType, const jstring sMsg) {
        S3DX::AIVariable args[2];
        args[0].SetNumberValue( nType );
        args[1].SetStringValue( jE->GetStringUTFChars(sMsg, 0) );
        S3DClient_SendEventToCurrentUser(_sDefaultAI, _sDefaultEvent, 2, (const void*)args);
    }

This is all very similar to the system set up in part 3 with the exception that we do not need a message pump this time since we can call events directly through the S3DClient API which plugins do not have access to.

If you did everything correctly, the demo project should give you something like this: