🤗

iOS アプリケーションの一部に Godot のコンテンツを設置する:Godot as a Library 最小実装

2023/12/31に公開

Godot ライブラリのヘッダーファイルを公開して iOS アプリケーションのコードから定義を参照するによって、Swift で書かれたアプリケーションのコードから Godot ライブラリの定義が見えるようになりました。しかし、記事の最後にも書いたように、いざ Godot ライブラリに定義されているクラスを参照してアプリケーションをビルドしようとすると、リンカエラーが発生してしまいました。

今回は、このリンカエラーを解消して、自前の iOS アプリケーションのビュー上で Godot エンジンを動作させるところまでをやってみます。

iOSアプリケーション上のビューでGodotエンジンが動いているようす
iOSアプリケーション上のビューでGodotエンジンが動いているようす

iOSアプリケーション上のビューでGodotエンジンが動いているようす(ビュー階層)
iOSアプリケーション上のビューでGodotエンジンが動いているようす(ビュー階層)

0. 修正のステップと作成した確認用のブランチ

今回は、本家のリポジトリを Fork し、修正のステップに応じたブランチで動作を確認できるようにしています。

1. リンカエラーを調べる

Godot ライブラリのヘッダーファイルを公開して iOS アプリケーションのコードから定義を参照するの最後にリンカエラーのスクリーンショットを掲載しました。以下に再掲します。

 のインスタンス化のコードを追加すると発生するエラー
GodotApplicalitionDelegate のインスタンス化のコードを追加すると発生するエラー

argc, argv という名前から分かるように、この変数は main 関数に渡されるコマンドライン引数に関するものです。外部から参照された際に発生するこのリンカエラーを解消するだけであれば、extern の変数宣言を実装ファイル(.m)からヘッダーファイル(.h)に移動し、定義を外部から見えるようにするだけです。

リンカエラーの解消(最小)
リンカエラーの解消(最小)

さて、上記で修正した gargcgargv は、Godot ライブラリの内部で定義された main 関数が引数として受け取った値を格納してあとで使うために用意された変数です。

iOS アプリケーションの一部として Godot エンジンを動作させる場合、main 関数の制御を iOS アプリケーションが奪う必要がありますから、 gargcgargv の扱いを根本的に変える必要があります。

2. gargcgargv を Gogot の外部から渡すように修正する

Swift で iOS アプリケーションを作成する場合、main 関数に渡される argc, argv に相当する値は、それぞれ CommandLine.argcCommandLine.unsafeArgv から参照できます。

gargcgargv は Godot の内部にあるファイル platform/ios/app_delegate.mm使用されています

元の gargcgargv の定義は削除して新しくインスタンス変数を定義し、外部から値を指定するためのメソッドを用意しておきます。

@implementation AppDelegate {
  int gargc;
  char **gargv;
}
// ...
- (void)setLaunchArguments:(int)_gargc argv:(char **)_gargv {
  gargc = _gargc;
  gargv = _gargv;
}

外部から定義が見えるように、ヘッダーファイルに宣言を追加することを忘れないようにします。

3. アプリケーションの主導権を奪い、Godot のクラスを呼び出す

Godot iOS アプリケーションの main 関数のありかを探るでは、Godot ライブラリの内部に定義されている main 関数がアプリケーションのエントリポイントとして振る舞い、結果としてアプリケーション側のコードに main 関数を定義しなくても動作が開始されるようになっていることを書きました。

このような状況で、さらにアプリケーション側に main 関数を定義すると、アプリケーション側の main 関数が実行され Godot ライブラリの main 関数は実行されなくなります。これによって、プログラムのエントリポイントがアプリケーションに移ります。

main.swift
import UIKit

exit(
  UIApplicationMain(
    CommandLine.argc,
    CommandLine.unsafeArgv,
    nil,
    NSStringFromClass(ApplicationDelegate.self)
  )
)

上のコードでは、アプリケーション側で定義された ApplicationDelegate クラスのインスタンスを UIApplicationDelegate として使用するように指定をしています。少し長いですが、ApplicationDelegate の実装も記載しておきます。

ApplicationDelegate.swift
import CoreGraphics
import Foundation
import Godot

let godotApplicalitionDelegate = Godot.GodotApplicalitionDelegate()

final class ApplicationDelegate: UIResponder, UIApplicationDelegate {
  /// Godot Framework 内で動的に `window` プロパティへのアクセスが発生するらしく、この定義は必須。
  var window: UIWindow?

  var godotWindow: UIWindow? {
    GodotApplicalitionDelegate.services.compactMap({ $0 as? Godot.AppDelegate }).first?.window
  }

  var godotViewController: UIViewController {
    Godot.AppDelegate.viewController
  }

  var godotView: Godot.GodotView {
    Godot.AppDelegate.viewController.godotView
  }

  func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    if let godotAppDelegate = GodotApplicalitionDelegate.services.compactMap({ $0 as? Godot.AppDelegate }).first {
      godotAppDelegate.setLaunchArguments(CommandLine.argc, argv: CommandLine.unsafeArgv)
    }
    godotApplicalitionDelegate.application(application, willFinishLaunchingWithOptions: launchOptions)
    return true
  }

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    godotApplicalitionDelegate.application(application, didFinishLaunchingWithOptions: launchOptions)

    let screenBounds = UIScreen.main.bounds
    let viewController = MainViewController()
    window = UIWindow(frame: screenBounds)
    window?.rootViewController = viewController
    window?.makeKeyAndVisible()

    viewController.loadViewIfNeeded()

    viewController.containerView.addSubview(godotViewController.view)
    godotViewController.view.frame = CGRect(
      x: 0,
      y: (viewController.containerView.bounds.height - godotViewController.view.bounds.height) / 2,
      width: viewController.containerView.bounds.width,
      height: screenBounds.height
    )

    return true
  }

  func applicationWillResignActive(_ application: UIApplication) {
    godotApplicalitionDelegate.applicationWillResignActive(application)
  }

  func applicationDidBecomeActive(_ application: UIApplication) {
    godotApplicalitionDelegate.applicationDidBecomeActive(application)
  }
}

ApplicationDelegate クラスの内部で Godot.GodotApplicalitionDelegate クラスのインスタンスを生成し、必要な処理を呼び出しています。また、Godot.AppDelegate.viewController を取得し、画面に view を設置しています。この view プロパティには GodotView クラスのインスタンスが格納されていて、結果として画面中央に Godot のコンテンツが描画されることになります。

iOSアプリケーション上のビューでGodotエンジンが動いているようす(再掲)
iOSアプリケーション上のビューでGodotエンジンが動いているようす(再掲)

4. 今後の展望

ここまで、Godot as a library の最小実装を試すことも目標に、数回に分けて記事を書きました。

  1. Godot エディタをソースコードから macOS 向けにビルドする
  2. Godot の iOS カスタムテンプレートを作成して Xcode プロジェクトをエクスポートする
  3. Godot iOS アプリケーションの main 関数のありかを探る
  4. Godot ライブラリのヘッダーファイルを公開して iOS アプリケーションのコードから定義を参照する
  5. この記事

この記事で当初想定していた最小実装はできたのですが、実用を考えた場合、iOS アプリケーションと Godot ライブラリ間のコミュニケーションを確立して、より柔軟に Godot のコンテンツを操作できるようになる必要があり、次の課題だろうと思っています。

Discussion