UnityでiOSとAndroidのネイティブプラグインを作るのは怖くない!

はじめに

はじめまして!ココネのクライアントエンジニアのK.Oです!

業務でネイティブプラグインを作成することがあったので、最初の触りを書いていきます。

Unityのネイティブプラグインの実装ってなんか怖いイメージがありますよね…

調べてみると簡単に作成する方法があまりまとまっていなかったりします。

なので、できるだけシンプルにまとめて、ネイティブプラグインの実装が怖くなくなるような記事を目指します!

 

iOSでObjective-Cと聞くと震え上がる方が多いと思いますが、今回はモダンなSwiftでサンプルを書いていくのでご安心ください。

実装の全体の流れ

作るサンプル

Unityからの呼び出しで、Androidはトーストを表示、iOSはアラートダイアログを表示するといったものを作成します。

環境

  • Unity 2022.3.4f1
  • AndroidStudio 2022.3.1 Patch 1
  • XCode Version 14.3.1

 

今回はネイティブコードを事前にビルドせず、直接Unityに入れてビルドする方法をとります。

(古いUnityバージョンだとこの方法が取れない場合があります)

手順

  1. ネイティブ側のコードを準備する
  2. Unity側(C#)のコードを準備する
  3. iOS,Androidそれぞれビルドする
  4. 実機で確認

 

ネイティブ側のコードを準備する

まずはじめに、JavaとSwiftのコードを準備します。

格納場所はUnityプロジェクト内の

Assets/Plugin/Android/AndroidNativePlugin.java

Assets/Plugin/iOS/iOSNativePlugin.swift

といった感じにしました。

 

JavaとSwiftの詳細な解説は、今回の本題ではないので省略させていただきます。

Android(Java)

package com.cocone.sample;

import android.app.Activity;
import android.widget.Toast;
import com.unity3d.player.UnityPlayer;

public class AndroidNativePlugin
{
    private Activity activity;

    public AndroidNativePlugin()
    {
        activity = UnityPlayer.currentActivity;
    }

    public void showMessage(final String message)
    {
        Toast.makeText(activity , message, Toast.LENGTH_LONG).show();
    }
}

Androidはプラグインを作り込んでいく際、activityが色々な処理に必要になってきます。

 

今回のサンプルでは必要ないですが、AndroidのViewに対してTextやButtonを追加したいときは、UIスレッド上で実行する必要があるので注意です。

var rootView = activity.findViewById(android.R.id.content);
activity.runOnUiThread(() -> rootView.addView(someLayout));

iOS(Swift)

public class NativePlugin
{
    public static func showMessage(_ message: String)
    {
        let unityAppController = UIApplication.shared.delegate as? UnityAppController
        let rootViewController = unityAppController?.rootViewController
        
        let alert = UIAlertController(title: "Message", message: message, preferredStyle: .alert)
        rootViewController?.present(alert, animated: true, completion: {
      // 2秒後に閉じる
            DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                alert.dismiss(animated: true, completion: nil)
            }
        })
    }
}

// UnityとSwiftを繋げるための関数
@_cdecl("showMessage")
public func showMessage(message: UnsafePointer<CChar>)
{
    NativePlugin.showMessage(String(cString: message))
}

iOSはrootViewControllerあたりが色々な起点になります。

 

Unity側(C#)のコードを準備する

呼び出しを共通化できるようにインターフェースを定義

インタフェースを定義するほどのサンプルではないですが、コードの可読性が上がるだけではなく、その後の拡張性に関わってくるのでしっかり定義します。

using System;

public interface INativePluginCall : IDisposable
{
    void ShowMessage(string message);
}

作るプラグインによっては、破棄処理も必要なのでIDisposableも実装します。

Android用の呼び出し

#if !UNITY_EDITOR && UNITY_ANDROID
using UnityEngine;
public class AndroidNativePlugin : INativePluginCall
{
    private AndroidJavaObject androidJavaObject;
    
    public void ShowMessage(string message)
    {
        androidJavaObject ??= new AndroidJavaObject("com.cocone.sample.AndroidNativePlugin");
        if (androidJavaObject == null)
        {
            return;
        }
        
        androidJavaObject.Call("showMessage", message);
    }
    public void Dispose()
    {
        androidJavaObject.Dispose();
    }
}
#endif

AndroidJavaObjectを生成する際に渡している引数は、

[Javaサンプルの一行目に宣言しているパッケージ名].[クラス名]

の形になっています。

 

もしshowMessageの戻り値がboolでそれを受け取りたい時は以下のように書きます。

bool isSuccess = androidJavaObject.Call<bool>("showMessage", message);

iOS用の呼び出し

#if !UNITY_EDITOR && UNITY_IOS
using System.Runtime.InteropServices;

public class iOSNativePlugin : INativePluginCall
{
     [DllImport("__Internal", EntryPoint = "showMessage")]
     static extern void iOSShowMessage(string message);
     
     public void ShowMessage(string message)
     {
          iOSShowMessage(message);
     }

     public void Dispose()
     {
          // 今回のサンプルでは必要なし
     }
}
#endif

 

実行結果

呼び出し部分は省略しますが、OSごとに上記の処理を呼び出した結果が以下になります。

ネイティブ側からUnityの処理を呼び出したいときは?

何かしらネイティブ側で発生したイベントをUnityに伝えたい時が出てくると思います。

AndroidとiOSのどちらも共通で、何のGameObject名の何のメソッドにどんな文字列を送るかという指定になっています。

それぞれサンプルコードを記載します。

Android

UnityPlayer.UnitySendMessage(gameObjectName, methodName, message);

iOS

UnityFramework.getInstance().sendMessageToGO(withName: gameObjectName, functionName: methodName, message: message)

C#側

[UsedImplicitly]
private void OnNativeMessage(string message)
{
    Debug.Log(message);
}

UsedImplicitly属性は付けなくても大丈夫ですが、使われてない関数と判定されないように付けています。

 

NativeSampleというGameObject名にアタッチされているMonobehaviourに上記の関数がある場合は以下のように呼び出します。(Androidの場合)

UnityPlayer.UnitySendMessage("NativeSample","OnNativeMessage","Hello");

実装のコツ

JavaとSwift両方使わなければいけないし、通常のネイティブアプリではやらないような実装をするのである程度の工夫が必要になってきます。

試した中で特に有効だったものを2つ紹介します。

 

エクスポートしたプロジェクト上でプラグインを実装する

コード補完がなく、実機確認もできない環境で実装するのはしんどいですし、時間もかなりかかってしまいます。

そこで、一度Unityでプロジェクトをビルドして、AndroidはAndroidStudio、iOSはXCode上で実装するのがやりやすいです。

Androidはデフォルトではapkが生成されますが、設定を変更するとプロジェクトを生成することができます。

 

今回のサンプルの場合、生成されたプロジェクトの以下の場所にファイルがあるのでそれぞれのIDE上で開いて編集します。
Android:

Android/unityLibrary/src/main/java/com/cocone/sample/AndroidNativePlugin.java
iOS:
Libraries/Plugin/iOS/iOSNativePlugin.swift

 

ChatGPTに聞く

JavaやSwiftの書き方、ネイティブのUIをどうやって表示するか、レイアウト周りについてなどわからないことなどを聞いたら割とちゃんとした答えが返ってきて助かりました。

話題すぎてここに書く必要もないと思いますが、しっかり活用していきましょう!

 

最後に

ネイティブプラグインは怖くなくなったでしょうか!?

実装前は結構大変なイメージがありましたが、Unityのアップデートもあって結構シンプルに組み込めるようになってるなと感じます。

しかし、プラグインをしっかり作り込もうとなるとそれはそれで結構大変だと思います。

やりたいことを実現するために、恐れずにネイティブプラグインを実装していきましょう!

ココネでは一緒に働く仲間を募集中です。

ご興味のある方は、ぜひこちらのエンジニア採用サイトをご覧ください。

→ココネ株式会社エンジニアの求人一覧

 

Category

Tag

%d