LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


Unity向けLINE SDKへの挑戦:課題と選択肢

LINE SDK開発チームのWei Wangです。昨年のLINE DEVELOPER DAY 2018において、私たちは新しいLINE SDK for iOSとLINE SDK for Androidをオープンソースとしてリリースしました。このSDKは、LINEログインといくつかのAPIをアプリに組み込む機能を提供することで、それぞれのアプリに合わせた魅力的なユーザーエクスペリエンスを作ることをお手伝いします。

リリースを終えたのち、私たちは、アプリ開発者に加えて、より多くのモバイルゲーム開発者もサポートする必要があることに気づきました。ゲームは、App StoreとGoogle Playのアプリの50%以上を占めており、圧倒的な収益を生み出しています。だからこそ、私たちはもうひとつの重要なプラットフォームである、Unity向けのLINE SDKを開発することを決断しました。LINE SDK for Unityは、ゲーム開発者が次の革新的なゲームタイトルを開発するうえでLINE SDKをより簡単に使えるように設計されました。

この記事では、既存のSDK for Unityをラップするために私たちがとったアプローチのほか、LINE SDKに関するいくつかの具体的なトピックについて取り上げています。

LINE SDK for iOSおよびLINE SDK for Androidのように、LINE SDK for Unityもオープンソースです(コードはGitHubで確認できます。また、UnityゲームでLINE SDKを使う方法については、セットアップガイドをご覧ください)。

概要

Unity SDKに関して、私たちは既存の(iOSおよびAndroid用)ネイティブSDKをラップして、ユーザーに使いやすいC#インターフェイスを提供することにしました。SDK全体を再実装する場合と異なり、既存の機能のラッピングには、次のようなメリットがあります。

  1. 保守コストの低減。既存のネイティブSDKのコードを再利用して、保守コストを低減することができます。LINE SDKはオープンソース化され、普及率が高く、評判も上々です。ネイティブSDKを使用すると、プロジェクト品質が確保されるだけでなく、ネイティブSDKの新機能や修正がUnity SDKと同期されます。
  2. ネイティブ機能の活用。LINE SDKのログイン機能では、LINEアプリがインストールされていない場合のWebビューでのログインや、異なるアプリ間でのデータ転送の処理など、システムプラットフォームのさまざまな機能が必要になります。ネイティブSDKを使用すると、このような問題にネイティブで適切に対処することができます。
  3. なじみのある操作。モデルとAPI定義をC#レベルで提供しているため、Unityユーザーは、iOSプラットフォームとAndroidプラットフォームの違いにかかわらず、C#の慣習のままLINE SDKを使用することができます。

LINE SDK for Unityの基本構造は以下のとおりです。

実装

このセクションでは、LINE SDK for Unityの実装について説明します。注:ソースコード全体がGitHubで公開されています。

Unity用のネイティブプラグイン

Unityでは、広範なネイティブプラグインがサポートされています。ネイティブのLINE SDKとやり取りできるようにするには、Unity C#の世界と「ネイティブ」の世界(iOSの場合はSwift、Androidの場合はJava)をつなぐ架け橋を作らなければなりません。

非同期操作

iOSの場合は、DllImportを使って、公開されたインターフェイスをObjective-C++からUnity C#にインポートします。Androidの場合は、AndroidJavaObjectを使ってLINE SDKネイティブからメソッドを呼び出します。

// iOS interface
[DllImport("__Internal")]
private static extern int foo();
public static int Foo() {
    return foo();
}

// Android interface
public static int Foo() {
    var androidObject = new AndroidJavaObject("com.linecorp.linesdk.SomeClass");
    return androidObject.Call("foo");
}

値を返す場合でも、返さない場合でも、同期的な方法でAPIをコールするのは簡単です。しかし、LINE SDKでは、ログインやその他のネットワーク関連操作のために、いくつかの非同期APIも提供しています。たとえば、iOSでは、ログインAPIに以下の署名があります。

// Login method signature in LINE SDK Swift.
public func login(
        permissions:Set<LoginPermission> = [.profile],
        options:LoginManagerOptions = [],
        completionHandler completion:@escaping (Result<LoginResult, LineSDKError>) -> Void) -> LoginProcess?

ログイン結果の処理には、非同期コールバックのようなもののほか、ネイティブプラットフォームとUnityゲーム間でデータを渡すことが必要になります。

より難しい方法

iOSでは、C#とC++の境界間のコールバックは通常、Marshalクラスによって行われます。このクラスは、アンマネージドメモリの割り当て、アンマネージドメモリブロックのコピー、マネージド型からアンマネージド型への変換を行うためのメソッドのコレクションを提供します。たとえば、C#デリゲートをアンマネージド関数ポインタに変換してから、C#メソッドをネイティブ側に渡すことができます。関数ポインタのパラメータは、アンマネージド型のポインタとして表すこともできます。

// Defines a Marshal function pointer to receive callback method from the native side.  
delegate void Callback(IntPtr foo);
[DllImport(DllLib)]
private static extern void method([MarshalAs(UnmanagedType.FunctionPtr)] Callback callback);
public static void Method() {
    method(CBMethod);
}

[MonoPInvokeCallback(typeof(Callback))]
private static void CBMethod(IntPtr foo) {
    // Convert Int pointer foo to a managed string
    string fooString = Marshal.PtrToStringAuto(foo);
    Debug.Log("Parameter is " + fooString);
}

ネイティブC++側(通常Objective-C++で記述)では、externメソッドを次のように定義します。

// Defines and invokes callback in the native side.
typedef void (*CallbackT)(const char *foo);

extern "C" void method(CallbackT callback);
void method(CallbackT callback) {
    // Do something asynchronously.Then call `callback`.
    //...
    callback("foo sent");
}

Unity側からMethod()を呼び出すことにより、ネイティブ側で非同期操作が終了すると、パラメータCBMethodとともに"foo sent"が呼び出されます。Androidでは、非同期操作から結果を取得するために、リスナーインターフェイスのあるオブザーバパターンを使用する傾向にあります。たとえば、LINE SDK for Androidには、LoginListenerがあります。

// Listener interface for observing login result in the LINE SDK for Android.
public interface LoginListener {
    void onLoginSuccess(@NonNull LineLoginResult result);
    void onLoginFailure(@Nullable LineLoginResult result);
}

Unityでは、Javaインターフェイスを表現するのにAndroidJavaProxyが最適です。AndroidJavaProxyから継承されるクラスを実装してから、リスナーパラメータで適切なAndroid APIに渡します。

// Login proxy in Unity C#, which behaves as a native listener.
class LoginCallback :AndroidJavaProxy
{
    public LoginCallback() :base("com.linecorp.linesdk.$LoginListener") {}
    void onLoginSuccess(AndroidJavaObject result) {
        Debug.Log("Login successfully.");
    }

    void onLoginFailure(AndroidJavaObject result) {
        Debug.Log("Login failed.");
    }
}

androidObject.Call<int>("login", new LoginCallback());

MarshalおよびAndroidJavaProxyは強力であり、ネイティブの世界とUnityの世界をつなぐ架け橋として機能させることもできます。ただし、LINE SDKの規模と、複数のプラットフォームをサポートする必要性を考慮すると、これらは実際には理想的ではありません。MarshalとAndroidJavaProxyには、次のようないくつかの欠点があります。

  1. アンマネージドメモリと型指定のないオブジェクトの導入。Marshalはアンマネージドポインタで機能し、AndroidJavaObjectまたはAndroidJavaProxyは型指定のないオブジェクトで機能します。いずれもC#の慣習には従いません。
  2. SDKの拡張が困難。機能を追加するには、ネイティブとUnityの両側で型定義を追加しなければならない場合があり、SDKが変更された場合に必要になる作業が倍になります。
  3. iOSとAndroidでアプローチが異なる。これにより、メンタルモデルが複雑になります。

より簡単な方法

私たちは、MarshalとAndroidJavaProxyを活用する代わりに、はるかに簡単なモデルを使用して、非同期のコールバックの処理します。そのモデルとは、トークンを識別子として、またJSONシリアライズされたパラメータとして使用し、データを渡す、ネイティブ側のシンプルなUnitySendMessageです。iOSとAndroidの両方で、UnityエンジンランタイムはUnitySendMessageメソッドを公開して、メッセージを任意のGameObjectに送信します。ゲームオブジェクトにメッセージと同じ名前のメソッドが含まれている場合、そのメソッドを呼び出すことによって、そのメッセージに応答させることができます。これにより、ネイティブからUnityのメソッドをコールすることができます。

// Send a message from native to Unity.
UnitySendMessage("GameObjectName", "MethodName", "Message to send");

このメソッドには、次の3つのパラメータがあります。

  • ターゲットGameObjectの名前
  • そのオブジェクトをコールするためのスクリプトメソッド
  • コールされたメソッドに渡すメッセージ文字列

Unity側では、このメッセージに応答させるために、GameObjectNameというゲームオブジェクトで、単一のstringパラメータを含むMethodNameというスクリプトが必要です。

// This script should be added as a component on "GameObjectName".
class MyClass:MonoBehaviour {
    void MethodName(string parameter) {
        //...
        Debug.Log(parameter);
        // "Message to send"
    }
}

識別子を使用してネイティブAPIをコールし、同じものをUnitySendMessageを介して戻すことにより、非同期操作のコール元を識別できます。iOSでは、基本的な考え方は以下のようになります。

// Passing the `identifier` to track asynchronous operation caller.

// Unity - interface for the SDK users.
public static void Method() {
    // Generates an identifier internally.
    var identifier = Guid.NewGuid().ToString();
    Foo(identifier);
}

// Unity - wrapper
[DllImport("__Internal")]
private static extern void foo(string identifier);
public static int Foo(string identifier) {
    foo(identifier);
}

// iOS
extern "C" void foo(const char *identifier);
void foo(const char *identifier) {
    // Do something asynchronously.Then call `UnitySendMessage` with `identifier`.
    //...
    UnitySendMessage("NativeListener", "CallbackMethod", identifier);
}

// In Unity, there is a game object with the name of `NativeListener`.
void CallbackMethod(string identifier) {
    // We know who initializes the operation by checking `identifier`.
    // Do something with the callback from native side.
}

受信したidentifierをチェックすることで、特定のコールバックについてどのように処理するかを決定できます。これにより、Unityとネイティブ間で非同期的な方法でやり取りを行えます。

JSONとSerializableでデータを渡す

UnitySendMessageでは、単一の文字列値のみを送受信することができます。上記の例では、identifier値を戻しました。ただし、LINE SDKでは、一部の値をネイティブ型からUnityに渡す必要もあります。たとえば、トークンの値文字列、有効期限、トークンスコープなどを含むアクセストークンなどが該当します。ネイティブとUnityの両方で解釈できるフォーマットにデータをシリアライズする方法が必要です。JSONは言語間でデータを交換する際の当然の選択肢です。JSONは理解しやすく、ネイティブとUnityの両方で完全にサポートされています。LINE SDK for Unityでは、コールバックペイロードを以下の形式で定義します。

// Shared structure for token between native side and Unity.
{
  "identifier":"abcdefg...", // The received GUID from Unity.
  "value":{                  // A nested object represents native object.
    "token":"...",
    ...
  }
}

つまり、ただidentifierを送信して戻す代わりに、重要な実際のデータを表現するvalueと組み合わせ、JSON文字列にシリアライズして送信します。

// Serialize a native object to JSON, then send it as a message parameter to Unity.
void foo(const char *identifier) {
    // Do something asynchronously.
    // Then call `UnitySendMessage` with `identifier` and the actual data.

    [loginManager loginWithCompletionHandler:^(LineSDKLoginResult * result, NSError *error) {
        NSDictionary *dic = @{
            @"identifier":convertToNSString(identifier),
            @"value":[result toDictionary]
        };

        NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:kNilOptions error:nil];
        const char* payload = convertToCString([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);

        UnitySendMessage("NativeListener", "CallbackMethod", payload);
    }];

Unityでは、JsonUtilityを使ってJSON文字列をデシリアライズし、C#でのマネージドオブジェクトとして特定の型に変換し直します。このようにして、任意のデータをネイティブ側とUnityゲーム間で渡すことができます。

iOSでのLINE SDKの組み込み(Swift)

2018年、私たちはLINE SDK for iOSのバージョン5をObjective-CとSwiftの両方でリリースしました。このSDKは今後継続的に変更していく予定です。Swiftで作業を行うのはよいことですが、この言語では、まだモジュールの安定性が実現できていないため(Swift 5でさえも)、Swift SDKをバイナリ形式で提供することはできません。ネイティブのSwift SDKをUnityプロジェクトに組み込む場合、ソースコードの依存性を解決し、管理する方法が必要です。同時に、UnityではiOSのプロジェクトを主にObjective-C++でエクスポートします。また、ネイティブインターフェイスはCで記述されている必要があります。C、C++、Objective-CとSwift間の相互運用性も課題です。こうした課題のいくつかを、以下で説明します。

ネイティブSDKのインストール

Unityでは、バイナリをAssets/Plugins/iOSフォルダに配置することによって、バイナリライブラリのエクスポートが非常にうまくサポートされています。しかし、私たちはSwift SDKを選んでいるため、ユーザーのマシンにインストールされたSwiftツールチェーンを使用して、ソースからビルドする必要があります。ネイティブSDKと同様に、依存性マネージャーとしてCocoaPodsまたはCarthageを選択できます。その後、すべての組み込み作業はポストプロセススクリプトによって実行されます。PostProcessBuildAttributeは、Unityのエクスポートが終了した際のフックを提供します。これは、依存性を設定するスクリプトを実行できることに加えて、最終プロジェクトを構成できることを意味します。私たちのSwift SDKはオープンソースプロジェクトであるため、CocoaPodsまたはCarthageを通じてSDKをインストールするのは、LINE SDKを通常のiOSプロジェクトに追加するのと大して変わりません。

  1. PodfileまたはCartfileをエクスポートしたプロジェクトのルートに配置します。
  2. pod installまたはcarthage updateを実行します。ネイティブSDKのソースコードがGitHubからダウンロードされ、必要に応じてコンパイルされます。
  3. フレームワークのリンク付け、ビルド設定の変更など、追加の設定を行います。

興味深いのは、構成作業のほとんどを行うのに、UnityのXcode APIを使用できるという点です。これらのすべてのAPIはUnityのUnityEditor.iOS.Xcode名前空間にあり、エクスポートされたXcodeプロジェクトをUnityエディタ内でC#コードにより操作することができます。たとえば、Xcodeプロジェクトのビルド設定を変更するために、PBXProjectオブジェクトを作成して、それにSetBuildPropertyをコールすることができます。

// Set a build setting value in exported Xcode project.

var path = PBXProject.GetPBXProjectPath(projectRoot);
var project = new PBXProject();
project.ReadFromFile(path);

project.SetBuildProperty(target, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");

Unity Xcode APIを使用して、ネイティブSDKを組み込むために元々必要だったほとんどの設定(フレームワークのリンクフェーズへの追加や、コールバックURLスキームの設定など)を自動化できます。

相互運用性

iOS上のLINE SDK for Unityではさまざまな言語で作業する必要があります。ネイティブSDKのロジックは純粋なSwiftで実装されています。これには互換性のない言語機能(Swiftの列挙型および構造体など)がいくつか含まれ、Objective-Cで直接使用することはできません。Swift SDKをObjective-Cで使用するために、追加のラッパーを用意しました。このラッパーもSwiftで記述されていますが、Objective-Cと互換性のある言語機能のみが使用されています。Unityを操作するときに、エクスポートされたプロジェクトは、実際にはObjective-C++プロジェクトであるため、このラッパーとのやり取りが必要になります。つまり、以下の処理を行わなければなりません。

  • ネイティブのSwift SDKをC++プロジェクトにインポートするためのclangモジュールを有効にする
  • Swiftライブラリをバンドルに埋め込む
  • ランタイムに正しい検索パスを設定する

これらの処理を、先述のUnity Xcode APIで行うことができます。

// Enable clang module to use Swift SDK in Objective-C++.
project.SetBuildProperty(target, "CLANG_ENABLE_MODULES", "YES");

// Setup Swift environment.
// These two are only necessary when using Carthage.CocoaPods will help us to setup them.
project.SetBuildProperty(target, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
project.SetBuildProperty(target, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks");

UnityのXcode APIを使うことで、私たちはプロジェクト構成の時間を大幅に短縮することができました。皆さんにもこのAPIを使用してプロジェクトのカスタマイゼーションを強化することをお勧めします。

システムイベントを受信する

エンドユーザーがLINEアプリを自分のデバイスにインストール済みである場合、LINE SDKは、そのLINEアプリを認証のショートカットとして開くことができます。ユーザー名とパスワードは必要ありません。これは、UnityゲームとLINEアプリの間で通信が行われるということです。その通信では、多くの場合、URLスキームを使用してユーザーの認証情報をゲームに渡します。LINE SDK for Unityでは、そのイベントを取得する方法が必要です。iOSアプリを開発しているのであれば、その方法は簡単です。Objective-CまたはSwiftソースコードに数行を追加して、コードベースにコミットすれば済みます。しかし、Unityで作業を行っている場合は、XcodeプロジェクトをUnityからエクスポートするたびに、プロジェクトが上書きされます。幸いにも、Unityでは開いているアプリのイベント通知に登録する方法を提供しています。

// Code exported from Unity.It defines a listener interface for anyone to use.
@protocol AppDelegateListener<LifeCycleListener>

//...

// notification will be posted from
// - (BOOL)application:(UIApplication*)application openURL:(NSURL*)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation
// notification user data is the NSDictionary containing all the params
- (void)onOpenURL:(NSNotification*)notification;

@end

AppDelegateListenerに準拠するクラスを作成してからUnityRegisterAppDelegateListenerをコールして、通知オブザーバーとして追加することにより、対応する将来のイベントを取得できます。

// Register the app delegate listener to receive app life cycle events.

@interface LineSDKAppDelegateListener()<AppDelegateListener>
//...
@end

@implementation LineSDKAppDelegateListener
- (instancetype)init {
    self = [super init];
    if (self) {
        // ...
        UnityRegisterAppDelegateListener(self);
    }
    return self;
}

- (void)onOpenURL:(NSNotification *)notification {
    // Current app is opened (be navigated from LINE app).
}
@end

LineSDKAppDelegateListenerのコード全体をLINEのGitHubリポジトリで確認できます。

AndroidでのLINE SDKの組み込み

Unity側から機能を簡単にトリガできるように、ネイティブのLINE SDK APIをラップするためのunity-wrapper androidプロジェクトを提供しているため、Android SDKとの組み込みは簡単です。プラグインバイナリはビルド済みで、Assets/Plugins/Androidに格納されています。unity-wrapper androidプロジェクトで変更が必要な場合は、いつでも変更してバイナリを再度ビルドすることができます。

カスタムGradleテンプレートを使用して依存性を管理する

Androidプラットフォーム用のUnityプロジェクトでは、Gradleビルドシステムを利用します。必要なすべての依存性をunity-wrapperプロジェクトとLINE SDKライブラリに含めるためには、UnityプロジェクトでカスタムGradleテンプレートを構成することが重要です。[Unity Player Settings]の[Android settings]タブにある[Publishing Settings]で、[Custom Gradle Template]が有効であることを確認します。mainTemplate.gradleという名前のファイルがAssets/Plugins/Androidに作成されます。テンプレートファイルに以下を追加する必要があります。

  • buildscriptセクション:
buildscript {
    ext.kotlin_version = '1.3.11'
    ...

    dependencies {
        ...
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

    }
}
  • dependenciesセクション:
dependencies {
    implementation fileTree(dir:'libs', include:['*.jar'])

    implementation 'com.linecorp:linesdk:5.0.1'

    implementation 'com.google.code.gson:gson:2.8.5'
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    ...
}

組み込みに関する詳細については。『LINE SDK for Unityガイド』を参照してください。

まとめ

私たちは、皆さんが次の素晴らしいゲーム、あるいは既存のゲームにLINEを簡単に追加できるように、LINE SDK for Unityを作成しました。組み込みがよりシンプルになるように、そしてもっと使いやすくなるように注力して取り組みました。LINEログインやその他のLINE APIをUnityゲームに組み込むことにご関心があれば、LINE SDK for Unityに関するドキュメントをご確認ください。LINE SDK for iOSおよびLINE SDK for Androidと同様、LINE SDK for Unityもオープンソースプロジェクトです。何か問題が見つかった場合や、ご提案がある場合は、お気軽にリポジトリでイシューを開きコメントを入力してください。