ゲーム開発の備忘録

趣味のゲーム開発でのノウハウや、技術的に嵌ったポイントを忘れないように書き記しておくブログです。

libGDXでRoboVMからIntel MOEへの移植を行う コーディング編

はじめに -libGDXでのiOSアプリケーションの作成について-

libGDXでiOSアプリケーションを動作させる場合、以下の2通りの選択肢があります。

・RoboVMでiosモジュール配下のiOSLauncherを動作させる。
Intel MOEでios-moeモジュール配下のiOSMoeLauncherを動作させる。

資料が多く取り組みやすいのは前者ですが、残念なことに、RoboVMの親会社であるXamarinが2016年4月5日にMicrosoftに買収されたことで、RoboVMの開発は終了してしまいました。
そして、RoboVMはiOS12をサポートしない旨の声明が公表され、RoboVMを利用していたiOSアプリケーションはRoboVM以外の動作環境への移植を余儀なくされました。

かの有名なIngressも、iOS版はlibGDXかつRoboVMを利用していたため、iOS12非サポートの告知がされ話題になりました。

「Ingress」はiOS 12をサポートしない。公式がようやく発表。Ingress Primeリリースまでの対応は? | TeraDas

その後、iOS12非サポートの告知が撤回されましたが、Intel MOEに移植して対応したのかどうかは不明です。
なお、Ingressの次世代版のIngress PrimeはUnityで作られているため、UnityがApple社に追随出来ている限りは問題なさそうですね。


さて、私もlibGDXかつRoboVM利用でiOSアプリケーションを作成していたため、RoboVMを捨ててIntel MOEへ移植する必要がありました。
しかし、libGDXでのIntel MOEの利用に関するマニュアルはlibGDXの公式サイトには存在せず、Intel MOE側のマニュアルも2016年で更新停止しているという有様でした。結局ひたすら試行錯誤するしかありませんでした。

Using LibGDX — Multi-OS Engine Documentation

そこで、今回はlibGDXのRoboVMのプロジェクトを、Intel MOEに移植する方法についてまとめることにしました。
とにかく情報が少ないので、もし間違いやより良い方法などあればご指摘いただけると助かります!

基本的なランチャーコードの移植

RoboVM関連のimport文の修正

まずは、iOSLauncher側のコードにあってiOSMoeLauncher側のコードにない部分をそのままコピペします。
iosモジュール側でiOSLauncher以外にクラスやインターフェイスを作成していた場合は、それも全てios-moeモジュール側にも同様に作成しましょう。

すると、コードがエラーで真っ赤っ赤になると思います。覚悟を決めましょう。
まずは、インポートするパッケージ名を以下のように変更します。
※なお、本記事のコードは全てKotlinで書かれています。

追記:Kotlinで書いていた場合、後々Javaに書き直すことになりますが、まずはRoboVM版のKotlinコードをIntel-MOE版にKotlinのまま書き直したほうが楽です。

(例)

//変更前
import org.robovm.apple.uikit.UIApplication
//変更後
import apple.uikit.UIApplication

org.robovm.の部分を削除すればOKです。
エラーを吐いているimport文を全て残らず修正していきましょう。

メソッド名の変更

次に簡単なのは、単にメソッド名を修正すればよいものです。
全てのパターンを本記事で網羅できているわけではありませんが、以下のような変更が殆どだと思いますので一気に直していきましょう。
下記のパターンにあてはまらない場合も、代替のメソッドがある場合が多いので公式ドキュメントをもとに探していきます。

パターン1:プロパティで取得できていた箇所を「プロパティ名()」 の形式に修正する
(例)

//変更前
saveData.name
//変更後
saveData.name()

Javaの場合は、Kotlinのプロパティの取得がgetterに当たりますので、getXXX()からXXX()の形式に修正することになりますね。

パターン2:イベントハンドラを渡すメソッドを名前が長いものに置き換える
(例)

//変更前
GKNotificationBanner.showBanner()
//変更後
GKNotificationBanner.showBannerWithTitleMessageCompletionHandler()

//変更前
localPlayer.generateIdentityVerificationSignature()
//変更後
localPlayer.generateIdentityVerificationSignatureWithCompletionHandler()

ソースコードが非常に横に長くなっていき、気持ち悪いですが我慢します。
他にもイベントハンドラを渡すメソッドに限らず、上記と同じように名前が長くなっただけの同機能のメソッドが用意されている場合がありますので、まずは置き換え可能なメソッドが無いかどうかを探してみましょう。

パターン3:プロパティをsetterで置き換える
(例)

//変更前
window.backgroundColor = UIColor.white()
//変更後
window.setBackgroundColor(UIColor.whiteColor())

//変更前
localPlayer.authenticateHandler = VoidBlock2{
            view_controller: UIViewController?, error: NSError? ->
  /* 実装は省略 */
}
//変更後
localPlayer.setAuthenticateHandler {
            view_controller: UIViewController?, error: NSError? ->
  /* 実装は省略 */
}
ジェネリッククラスインスタンスの共変指定

ジェネリッククラスのインスタンスを渡すようなメソッドでは、引数に共変指定が追加されている場合があります。
実引数へのout修飾を求められた場合はout修飾を行い、共変制約に引っかからないようにコードを調整しましょう。

(例)

//変更前
val deleteSaveData = NSArray(it.value.drop(1))
localPlayer.resolveConflictingSavedGames(deleteSaveData, null){
  savedGames, fetchError ->

       readData(savedGames, fetchError, syncData, conflictRetryCount + 1, completedFunc)
}
//変更後
val deleteSaveDataArray = it.value.drop(1) as NSArray<out GKSavedGame>
localPlayer.resolveConflictingSavedGamesWithDataCompletionHandler(deleteSaveDataArray, null){
  savedGames, fetchError ->

  readData(savedGames, fetchError, syncData, conflictRetryCount + 1, completedFunc)
}
配列取得コードの変更

RoboVMでは配列変数はByteArray等の配列型で取得できていましたが、Intel MOEではConstVoidPtr型等、ポインタの形式で値が降ってくる場合があります。
そのような場合はBuffer型を介して配列型を取得する必要があります。

(例)

//変更前
syncData.add(CloudSaveData(block.name, block.ordinal, data.bytes))
//変更後
val ptr = data.bytes()
val buffer = ptr as Buffer
val array = buffer.array() as ByteArray
syncData.add(CloudSaveData(block.name, block.ordinal, array))
NSMutableArrayが必要なコードの対応

最も面倒くさい修正パターンです。RoboVMでMutableList型が必要だった箇所は、全てNSMutableArray型に変換しなければなりません。
しかし、NSMutableArray型は初期化時( NSMutableArray.alloc().init() )にスター投影されて返却されるため、そのままだと要素の追加が一切できません。
そこで、NSMutableArray<String>等にキャストする必要がありますが、スター投影された型からのキャストはUnchecked Cast警告が発出されるため、Suppressアノテーションで警告を抑制する必要があります。
Javaの場合は非境界ワイルドカード形式で返却されます。

(例)

//変更前
val myDeviceID = "AAA"
request.testDevices = Arrays.asList(myDeviceID)
//変更後
@Suppress("UNCHECKED_CAST")
val devices = NSMutableArray.alloc().init() as NSMutableArray<String>
val myDeviceID = "AAA"
devices.add(myDeviceID)
request.setTestDevices(devices)

特殊なコードの移植

ある特定機能を利用している場合に必要になる修正作業をまとめます。

Intel MOEのNSDate型を利用してComparableが要求される比較処理を行う

RoboVMのNSDate型はDate型に変換できますが、Intel MOEのNSDate型はDate型に変換できません。
その場合、Comparableを実装していないかつ、Dateに変換することができないため、Comparableが要求される比較処理で値を利用する際に一工夫が必要になります。

(例)

//変更前
savedGames.sortByDescending { it.modificationDate.toDate() }
//変更後
class ComparableNSDate(val date :NSDate) : Comparable<ComparableNSDate>{
  override fun compareTo(other: ComparableNSDate): Int = date.compare(other.date).toInt()
}

savedGames.sortByDescending {
        ComparableNSDate(it.modificationDate())
 }

Comparableを実装した、NSDate型のメンバを保有するローカルクラスを定義し、そのインスタンスを使います。

Robopodに代わるバインディングライブラリ

RoboVMには、JVM言語で記述されたサードパーティライブラリをバインディングして使えるようにしたRobopodというバインディングライブラリがありました。
Intel MOEでは、以下の場所にあるmoe-bindingsを利用します。

github.com

Google Mobile Ads等、一部のバインディングライブラリはビルド時に名前の競合でエラーを吐くことがあるので、バインディングライブラリ内のメソッドの手動リネームが必要な場合があります。注意しましょう。
残念ながら、GDPR対応で必要なConsent SDKバインディングしてくれていないようです……

おわりに

お疲れ様でした。次は、実際に移植したIntel MOE利用アプリケーションをエミュレータ上で動作確認してみましょう。

deep-verdure.hatenablog.com