Extra Cookie

Yet Another Programmer's Blog

Using iOS and Android Libraries in Unity3d

I have written about how to use iOS and Android Libraries in Cocos2d-x, in mobile game industry, there is another cross platform framework named unity3d, which is more elegant, powerful, and, super expensive!

This post will show how to enable the App using unity3d framework to use native libraries of iOS and Android.

Unity3d Plugin Mechanism

Unity3d supports plugins well, compared with Cocos2D-X.

In order to use a plugin in unity3d, two things have to be done.

  • Write functions in a C-based language and compile them into a library
  • Create a C# script which calls functions in the library

Seems similar to the cocos2d-x way, but unity3d organizes things well, by which, plugin can be distributed as a package file, in App, simply import the package file, and, it’s all.

To create an iOS and Android plugin, five steps have to be followed.

  1. Create a new unity3d project
  2. Make a folder “Plugins” in “Assets” folder, then, make “iOS” and “Android” folders in “Plugins” folder
  3. Put iOS .a and Android .jar lib files to their folders respectively
  4. Write the C wrapper code to expose the native APIs to C#
  5. Click the “Assets - Export Package” menu to export the plugin as a unity3d package

At last the file structure will be similar to this.

Assets
└── Plugins
    ├── Android
    │   ├── AndroidManifest.xml
    │   └── humbleAssistant.jar
    ├── HumbleAssistant.cs
    └── iOS
        ├── HumbleAssistant.h
        ├── HumbleAssistantWrapper.h
        ├── HumbleAssistantWrapper.m
        └── libHumbleAssistant.a

iOS

I have an iOS lib named libHumbleAssistant.a, and its header file HumbleAssistant.h.

1
2
3
@interface HumbleAssistant : NSObject
+ (void)doSomething:(NSString *)order;
@end

In order to expose doSomething: interface to C#, a C wrapper need to be implemented. We know, lovely Objc integrates with C very well.

At first, create an empty header file “HumbleAssistantWrapper.h”

1
2
@interface HumbleAssistantWrapper : NSObject
@end

Then its implementation,

1
2
3
4
5
6
#import "HumbleAssistant.h"
#import "HumbleAssistantWrapper.h"

void humble_assistant_do_something(char *order) {
    [HumbleAssistant doSomething:[NSString stringWithUTF8String:order]];
}

Now, the C function is exposed, then, how to call C functions in C# scripts?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;
using System.Collections.Generic;

public class HumbleAssistant
{
#if UNITY_IPHONE
    [DllImport("__Internal")]
    private static extern void humble_assistant_do_something (string order);

    public void doSomething (string order)
    {
    	humble_assistant_do_something(order);
    }
#endif
}

Macro UNITY_IPHONE means those code only take effect in iOS system, [DllImport("__Internal")] means to statically link the C wrapper code.

Then, make the doSomething method, which is the real interface that can be used by other C# scripts.

Android

My android lib is humbleAssistant.jar, and has an interface as follows.

1
2
3
4
5
6
package com.my.libs;
public final class HumbleAssistant {
    public static void doSomething(String order) {
        // do something
    }
}

In unity3d, C# can interact with java APIs by JNI directly.

No C wrapper is needed, just add elif UNITY_ANDROID (those code only take effect in Android system), and begin to write the real code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;
using System.Collections.Generic;

public class HumbleAssistant
{
#if UNITY_IPHONE
    // ...
#elif UNITY_ANDROID
    public void doSomething (string order)
    {
        AndroidJavaClass mHumbleAssistantClass = new AndroidJavaClass("com.my.libs.HumbleAssistant");
        mHumbleAssistantClass.CallStatic("doSomething", order);
    }
#endif
}

My lib API is a static method, I need to create an instance of AndroidJavaClass initialized with my class name “com.my.libs.HumbleAssistant” , then call the CallStatic method of this instance to finish the JNI invocation.

If some APIs need android Activity object as parameter, following code can get the Activity instance.

1
2
AndroidJavaClass unityPlayer = new AndroidJavaClass ("com.unity3d.player.UnityPlayer");
AndroidJavaObject mCurrentActivity = unityPlayer.GetStatic<AndroidJavaObject> ("currentActivity");

Extra Labor

If my iOS lib depend on some other libs or frameworks, what can I do?

In a unity3d project, export to iOS will generate a Xcode project, then you can add your dependencies there.

If my Android lib need to declare some permissions to Android system, what can I do?

Not like iOS, export to Android generate the APK file directly, but “AndroidManifest.xml” can be put into “Assets/Plugins/Android/” folder to solve this problem.

For example, I declare READ_PHONE_STATE, INTERNET, and other permissions as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.unity3d.player"
	android:installLocation="preferExternal"
    android:versionCode="1"
    android:versionName="1.0">

    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.INTERNET"/>

    <supports-screens
        android:smallScreens="true"
        android:normalScreens="true"
        android:largeScreens="true"
        android:xlargeScreens="true"
        android:anyDensity="true"/>

    <application
		android:icon="@drawable/app_icon"
        android:label="@string/app_name"
        android:debuggable="true">
        <activity android:name="com.unity3d.player.UnityPlayerProxyActivity"
                  android:label="@string/app_name"
                  android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.unity3d.player.UnityPlayerActivity"
                  android:label="@string/app_name"
                  android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
        </activity>
        <activity android:name="com.unity3d.player.UnityPlayerNativeActivity"
                  android:label="@string/app_name"
                  android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
            <meta-data android:name="android.app.lib_name" android:value="unity" />
            <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="false" />
        </activity>
        <activity android:name="com.unity3d.player.VideoPlayer"
                  android:label="@string/app_name"
                  android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
        </activity>
    </application>
</manifest>

How am I supposed to do if my iOS objc interface need a NSDictionary object as one parameter?

Although in .m file, Objc and C code can be written together, but C wrapper is about to provide a pure C interface, Objc object can’t be used directly in method parameters.

Following are what I have done to solve this.

First, in the C wrapper, use a const char * as input, then parse the string to recreate the NSDictionary instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void humble_assistant_doComplicatedThing(const char *attributes) {
    NSString *attris = [NSString stringWithUTF8String:attributes];
    NSArray *attributesArray = [attris componentsSeparatedByString:@"\n"];

    NSMutableDictionary *oAttributes = [[NSMutableDictionary alloc] init];
    for (int i=0; i < [attributesArray count]; i++) {
        NSString *keyValuePair = [attributesArray objectAtIndex:i];
        NSRange range = [keyValuePair rangeOfString:@"="];
        if (range.location != NSNotFound) {
            NSString *key = [keyValuePair substringToIndex:range.location];
            NSString *value = [keyValuePair substringFromIndex:range.location+1];
            [oAttributes setObject:value forKey:key];
        }
    }

    [HumbleAssistant doComplicatedThing:oAttributes];
}

In the “HumbleAssistant.cs” script file, we should convert C# Dictionary to const char *.

1
2
3
4
5
6
7
8
9
public void doComplicatedThing (Dictionary<string, string> attributes)
{
    string attributesString = "";
    foreach(KeyValuePair<string, string> kvp in attributes)
    {
        attributesString += kvp.Key + "=" + kvp.Value + "\n";
    }
    humble_assistant_doComplicatedThing(attributesString);
}

How am I supposed to do if my Android jar interface need a Map object as a parameter?

Java Map object can be generated in C# code by JNI.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void doComplcatedThing (Dictionary<string, string> attributes)
{
    using(AndroidJavaObject obj_HashMap = new AndroidJavaObject("java.util.HashMap"))
    {
        System.IntPtr method_Put = AndroidJNIHelper.GetMethodID(obj_HashMap.GetRawClass(), "put",
            "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

        object[] args = new object[2];
        foreach(KeyValuePair<string, string> kvp in attributes)
        {
            using(AndroidJavaObject k = new AndroidJavaObject("java.lang.String", kvp.Key))
            {
                using(AndroidJavaObject v = new AndroidJavaObject("java.lang.String", kvp.Value))
                {
                    args[0] = k;
                    args[1] = v;
                    AndroidJNI.CallObjectMethod(obj_HashMap.GetRawObject(),
                        method_Put, AndroidJNIHelper.CreateJNIArgArray(args));
                }
            }
        }

        mHumbleAssistantClass.CallStatic("doComplicatedThing", obj_HashMap);
    }
}

Comments