Unityネイティブプラグインの作り方

Maeda, Kazuya
Dec 16, 2019 · 30 min read

概要

iOSやAndroid向けに開発したSDKをUnityアプリに対応させる場合、プラグインを作ってSDKとUnityアプリの橋渡しをしてやる必要があります。本記事ではネイティブアプリ向けに開発されたSDKのラッパーとして、Unityプラグインを作成する手法を紹介します。

サンプルコード

※本記事のサンプルコードはパブリックドメインであり、自由に改変して使用して頂いて構いません。

ISample.cs

Unity側からSDKのコードにアクセスするためのメソッドを宣言するインタフェースです。このインタフェースの実装については、それぞれ後述するiOS/Android側の実装を参考にしてください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SampleStatus {
Unavailable = 0,
Available = 1
}
public interface ISample {
void Start(string sampleCode);
void DoSomething();
SampleStatus GetStatus();
int GetScore();
bool IsAvailable();
string GetSomeText();
}

Sample.cs

上記インタフェースを実装したオブジェクトをsampleNativeとして保持し、そのメソッドの呼び出しを代理で行うWrapperクラスです。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using MiniJSON;
public class Sample : MonoBehaviour {
private ISample sampleNative;
public string iosSampleCode;
public string androidSampleCode;
private static Sample instance;
public static Sample GetInstance() {
if (instance == null) {
Sample existingInstance = GameObject.FindObjectOfType<Sample>();
if (existingInstance == null) {
return null;
}
existingInstance.SetSampleNative();
instance = existingInstance;
}
return instance;
}
private void Awake() {
if (instance != null) {
GameObject.Destroy(this);
GameObject.Destroy(this.gameObject);
return;
}
SetSampleNative();
GameObject.DontDestroyOnLoad(this.gameObject);
instance = this;
}
private void SetSampleNative() {
if (sampleNative != null) {
return;
}
#if UNITY_IOS
sampleNative = new Sample_iOS(this);
#elif UNITY_ANDROID
sampleNative = new Sample_Android(this);
#endif
}
public void DoSomething() {
sampleNative.DoSomething();
}
public SampleStatus GetStatus() {
return sampleNative.GetStatus();
}
public int GetScore() {
return sampleNative.GetScore();
}
public bool IsAvailable() {
return sampleNative.IsAvailable();
}
public string GetSomeText() {
return sampleNative.GetSomeText();
}
public ISample SampleNative {
get {
return sampleNative;
}
}
}

SampleEventListener.cs

SDKからのコールバックをUnity側で受けるためのListenerクラスです。

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using MiniJSON;
public class SampleEventListener : MonoBehaviour {
public static event Action<SampleStatus> NotifyStatusChanged;
private ISample nativeParent;
public void SetNativeParent(ISample nativeParent) {
this.nativeParent = nativeParent;
}
public void _Sample_didStatusChange(string message) {
SampleStatus status = (SampleStatus)int.Parse(message);
if (NotifyStatusChanged != null) {
NotifyStatusChanged(status);
}
}
}

使用例

Main.csを任意のSceneにアタッチすることでSampleオブジェクトの初期化が行われ、以降はSampleオブジェクト経由でSDKの機能にアクセスすることが可能になります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour {
void Start () {
Sample.GetInstance();
}
public void OnClickDoSomething() {
Sample.GetInstance().DoSomething();
}
public void OnClickGetStatus() {
SampleStatus status = Sample.GetInstance().GetStatus();
ToastUtil.Toast(this, "Status: " + status);
}
public void OnClickGetSomeText() {
string text = Sample.GetInstance().GetSomeText();
ToastUtil.Toast(this, "text: " + text);
}

void OnEnable() {
SampleEventListener.NotifyStatusChanged += NotifyStatusChanged;
}
void OnDisable() {
SampleEventListener.NotifyStatusChanged -= NotifyStatusChanged;
}
private void NotifyStatusChanged(SampleStatus status) {
Debug.Log("# Status changed: " + status);
}
}

iOS側の実装

次にiOS側の実装についてです。本記事ではUnity側のコードをC#、プラグイン側のコードをObjective-C++ (.mm)で記載するケースについて紹介します。

プラグイン側

まずはプラグイン側のコードの一例です。Sample, SampleDelegateといったクラスはSDK側で定義されており、それらのメソッドにUnity側からアクセスするために各Wrapperメソッドを実装しています。このコードはUnityプロジェクトのAssets/Plugins/iOSディレクトリ配下に入れておくと良いでしょう。

SamplePlugin.mm

#include <stdio.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <SampleSDK/SampleSDK.h>
#import <SapleSDK/SampleSDK-Swift.h>
#define kSample_DefaultCallbackGameObjectName @"Main Camera"@interface SamplePlugin: NSObject
@end
SamplePlugin *__unityClientSharedInstance;@interface SamplePlugin() <SampleDelegate>
@property(nonatomic, strong) NSString *callbackGameObjectName;
- (void)invokeUnityGameObjectMethod:(NSString *)methodName message:(NSString *)message;@end@implementation SamplePlugin+ (SamplePlugin *)sharedInstance {
if (!__unityClientSharedInstance) {
__unityClientSharedInstance = [[SamplePlugin alloc] init];
}

return __unityClientSharedInstance;
}
# pragma mark - SampleDelegate- (void)didStatusChange:(enum SampleStatus)status {
NSString *statusStr = [NSString stringWithFormat:@"%ld", status];
[self invokeUnityGameObjectMethod:@"_Sample_didStatusChange" message:statusStr];
}
# pragma mark - Private- (void)invokeUnityGameObjectMethod:(NSString *)methodName message:(NSString *)message {
NSString *gameObject = self.callbackGameObjectName == nil ?
kSample_DefaultCallbackGameObjectName : self.callbackGameObjectName;

const char *gameObjectStr = [gameObject cStringUsingEncoding:NSUTF8StringEncoding];
const char *methodNameStr = [methodName cStringUsingEncoding:NSUTF8StringEncoding];
const char *messageStr = [message cStringUsingEncoding:NSUTF8StringEncoding];

UnitySendMessage(gameObjectStr, methodNameStr, messageStr);
}
@end// Declare each wrapper methods
extern "C" {
void SPStart(const char *sampleCode);
void SPDoSomething();
int SPGetStatus();
int SPGetScore();
bool SPIsAvailable();
const char* SPGetSomeText();
}
extern UIViewController* UnityGetGLViewController();
extern void UnitySendMessage(const char* obje, const char* method, const char* msg);
void SPSetCallbackGameObjectName(char *gameObjectName) {
NSString *name = [NSString stringWithCString:gameObjectName encoding:NSUTF8StringEncoding];
SamplePlugin *client = [SamplePlugin sharedInstance];
client.callbackGameObjectName = name;
}
void SPStart(const char *sampleCode) {
NSString *sampleCodeStr = [NSString stringWithCString:sampleCode encoding:NSUTF8StringEncoding];

[[Sample sharedInstance] startWithSampleCode: sampleCodeStr];
[Sample sharedInstance].delegate = [SamplePlugin sharedInstance];
}
void SPDoSomething() {
[[Sample sharedInstance] doSomething];
}
int SPGetStatus() {
return [[Sample sharedInstance] getStatus];
}
int SPGetScore() {
return [[Sample sharedInstance] getScore];
}
bool SPIsAvailable() {
return [[Sample sharedInstance] isAvailable];
}
const char* SPGetSomeText() {
NSString *textStr = [[Sample sharedInstance] getSomeText];
const char *textChar = [textStr cStringUsingEncoding:NSUTF8StringEncoding];

return textChar ? strdup(textChar) : NULL;
}

Unity側の実装

Unityアプリ側には上述したiSampleを実装したSample_iOSというクラスを用意し、ラッパーメソッドの定義やコールバックを受けるリスナーオブジェクトの保持を行います。

Sample_iOS.cs

using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
#if UNITY_IOSpublic class Sample_iOS : ISample {
private Sample sampleGameObject;
private SampleEventListener listener;
[DllImport ("__Internal")]
protected static extern void SPSetCallbackGameObjectName(string gameObjectName);
public Sample_iOS(Sample sampleParent) {
sampleGameObject = sampleParent;
if (sampleParent.iosAppCode != null) {
Start(null);
}
CreateListenerObject();
}
private void CreateListenerObject() {
listener = sampleGameObject.gameObject.AddComponent<SampleEventListener>();
listener.SetNativeParent(this);
SPSetCallbackGameObjectName(sampleGameObject.gameObject.name);
}
[DllImport("__Internal")]
private static extern void SPStart(string sampleCode);
public void Start(string sampleCode) {
if (sampleGameObject.iosSampleCode != null) {
SPStart(sampleGameObject.iosSampleCode);
} else if (sampleCode != null) {
SPStart(sampleCode);
}
}
[DllImport("__Internal")]
private static extern void SPDoSomething();
public void DoSomething() {
SPDoSomething();
}
[DllImport("__Internal")]
private static extern int SPGetStatus();
public SampleStatus GetStatus() {
return (SampleStatus)SPGetStatus();
}
[DllImport("__Internal")]
private static extern int SPGetScore();
public int GetScore() {
return SPGetScore();
}
[DllImport("__Internal")]
private static extern bool SPIsAvailable();
public bool IsAvailable() {
return SPIsAvailable();
}
[DllImport("__Internal")]
private static extern string SPGetSomeText();
public string GetSomeText() {
return SPGetSomeText();
}
}
#endif

Android側の実装

最後にAndroid側の実装についてです。Android向けネイティブプラグインの実装方法は複数存在しますが、本記事ではUnityPlayerActivityを継承する方式を紹介します。

プラグイン側

BaseNativeActivity

BaseNativeActivityはUnityPlayerActivityを継承したクラスです。SampleやSampleLifecycleといったSDKで定義されたクラスのメソッドを代理で呼び出すことで、Wrapperとしての役割を果たしています。

public class BaseNativeActivity extends UnityPlayerActivity {
public static final void setCallbackGameObjectName(String name) {
SampleUnityListener.getInstance().setCallbackGameObjectName(name);
}
@Override
protected void onStart() {
super.onStart();
SampleLifecycle.onStart(this);
}
@Override
protected void onResume() {
super.onResume();
SampleLifecycle.onResume(this);
}
@Override
protected void onPause() {
super.onPause();
SampleLifecycle.onPause(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
SampleLifecycle.onDestroy();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
return super.onPrepareOptionsMenu(menu);
}
public void start(String sampleCode) {
Sample.getInstance().start(this);
}
public void doSomething() {
runOnUiThread(new Runnable() {
@Override
public void run() {
Sample.getInstance().doSomething()
}
});
}
public int getScore() {
return Sample.getInstance().getScore();
}
public String getSomeText() {
return Sample.getInstance().getSomeText();
}
public boolean isAvailable() {
return Sample.getInstance().isAvailable();
}
}

SampleUnityListener

SampleUnityListenerはSDK側のコールバックをUnity側へ伝えるためのクラスです。SDK側で定義されたリスナークラス、SampleListenerを実装しています。

public class SampleUnityListener implements SampleListener {
private static final Sample sample = Sample.getInstance();
private static SampleUnityListener instance;
private String callbackGameObjectName;
public synchronized static SampleUnityListener getInstance() {
if (instance == null) {
instance = new SampleUnityListener();
sample.setListener(instance);
}
return instance;
}
public final void setCallbackGameObjectName(String name) {
callbackGameObjectName = name;
}
@Override
public void onStatusChanged(SampleStatus status) {
if (callbackGameObjectName != null) {
String statusIndex = String.valueOf(status.ordinal());
UnityPlayer.UnitySendMessage(callbackGameObjectName, "_Sample_didStatusChange", statusIndex);
}
}
}

build.grade

build.gradleの一例です。 SDK本体(SampleSDK-*.jar)に加え、UnityPlayerActivity等のクラスをimportするためclasses.jarをlibsディレクトリに含めていますが、ビルド時は不要のためexcludeしています。またUnity側から名前解決する必要があるため、minifyもfalseにしています。

apply plugin: 'com.android.library'android {
compileSdkVersion 26
buildToolsVersion "25.0.3"
defaultConfig {
minSdkVersion 14
targetSdkVersion 26
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:26.+'
}
android.libraryVariants.all { variant ->
variant.outputs.each { output ->
output.packageLibrary.exclude('libs/classes.jar')
output.packageLibrary.exclude('libs/SampleSDK-*.jar')
}
}

ビルドについて

AndroidStudioの場合、Build -> Make Moduleからaarを作成することができます。ビルドしたaarはUnityプロジェクトのAssets/Plugins/Android配下に置くと良いでしょう。また、コンパイル時に使用しているclasses.jarの取得方法等についてはこちらを参照して下さい。

Unity側

Sample_Android.cs

Sample_Android.csはISampleを実装したクラスで、プラグイン側で実装したクラス(BaseNativeActivity)にアクセスすることでUnityアプリとSDKの橋渡しを行います。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
public class Sample_Android : ISample {
private Sample sampleGameObject;
private SampleEventListener listener;
private static AndroidJavaObject androidInstance;
public Sample_Android(Sample sampleParent) {
sampleGameObject = sampleParent;
InitAndroidInstance();
CreateListenerObject();
if (sampleParent.androidSampleCode != null) {
Start(null);
}
}
private void CreateListenerObject() {
listener = sampleGameObject.gameObject.AddComponent<SampleEventListener>();
using (AndroidJavaObject activityObject = GetCurrentActivity()) {
activityObject.CallStatic("setCallbackGameObjectName", sampleGameObject.gameObject.name);
}
listener.SetNativeParent(this);
}
public void Start(string sampleCode) {
using (AndroidJavaObject activityObject = GetCurrentActivity()) {
if (sampleGameObject.androidSampleCode != null) {
activityObject.Call("start", sampleGameObject.androidSampleCode);
} else if (sampleCode != null) {
activityObject.Call("start", sampleCode);
}
}
}
public void DoSomething() {
using (AndroidJavaObject activityObject = GetCurrentActivity()) {
activityObject.Call("doSomething");
}
}
public int GetScore() {
using (AndroidJavaObject activityObject = GetCurrentActivity()) {
return activityObject.Call<int>("getScore");
}
}
public SampleStatus GetStatus() {
SampleStatus status = SampleStatus.Unavailable;
using (AndroidJavaObject statusObject = androidInstance.Call<AndroidJavaObject>("getStatus")) {
string statusName = statusObject.Call<string>("name");
if (statusName.Equals("Unavailable")) {
status = SampleStatus.Unavailable;
} else if(statusName.Equals("Available")) {
status = SampleStatus.Available;
}
}
return status;
}
public string GetSomeText() {
return androidInstance.Call<string>("getSomeText");
}
public bool IsAvailable() {
using (AndroidJavaObject activityObject = GetCurrentActivity()) {
return activityObject.Call<bool>("isAvailable");
}
}
public AndroidJavaObject GetCurrentActivity() {
using (AndroidJavaClass playerClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) {
return playerClass.GetStatic<AndroidJavaObject>("currentActivity");
}
}
protected static void InitAndroidInstance() {
using (AndroidJavaClass sampleClass = new AndroidJavaClass("com.sdk.Sample")) {
androidInstance = sampleClass.CallStatic<AndroidJavaObject>("getInstance");
}
}
}

AndroidManifest.xml

UnityプロジェクトのAssets/Plugins/Android配下に置くAndroidManifestの一例です。前述のBaseNativeActivityを記載しています。

<?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:theme="@android:style/Theme.NoTitleBar"
android:versionCode="1" android:versionName="1.0">
<supports-screens android:smallScreens="true"
android:normalScreens="true"
android:largeScreens="true"
android:xlargeScreens="true"
android:anyDensity="true" />
<application android:label="@string/app_name" android:debuggable="false">
<activity android:name="com.sample.plugin.BaseNativeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" />
</activity>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

Proguard

一部のクラスに対するMinifyの回避等の設定を行いたい場合、proguard-user.txtを作成してUnityプロジェクトのAssets/Plugins/Android配下に置いておくと、AndroidプロジェクトとしてExportした際に自動的に反映してくれます。

proguard-user.txt

-keepattributes *Annotation*
-keepattributes Signature
-keep class com.sample.sdk.** {
public <fields>;
public <methods>;
}
-keep class com.sample.plugin.** {
public <fields>;
public <methods>;
}
-dontwarn com.sample.sdk.**

ディレクトリ構成

下記を必要に応じてUnityプロジェクトのAssets/Plugins/Android配下に配置して下さい。

  • SDK本体(jar/aar)
  • プラグイン(jar/aar)
  • AndroidManifest.xml
  • proguard-user.txt

まとめ

作成したプラグインプロジェクトは.unitypackageとしてまとめておき、クライアントとなるUnityプロジェクトに取り込みやすくしておくと良いでしょう。

参考

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade