cocos2d_libs.xcodeprojを生成してcocos2d-x v4に移行する

# 背景

cocos2d-xはC++を用いてマルチプラットフォームでゲーム開発ができるフレームワークです。
iOS/Androidでのゲーム開発で用いられていることが多く、
最近の有名どころでは「[ポケモンマスターズ](https://pokemonmasters-game.com/ja-JP)」がcocos2d-xでできています。

このcocos2d-xですが、1つ爆弾を抱えていました。
それはAppleがOpenGLの利用を非推奨としたことです。
cocos2d-xの描画エンジンはOpenGLに依存しており、Appleが近い将来OpenGLを廃止したタイミングで、それ以降cocos2d-x製のソフトがiOS上で動作しなくなってしまうのです。

OpenGLの代替手段として、Appleは[Metal](https://developer.apple.com/metal/)を提供しています。
cocos2d-xでも描画エンジンにMetalを利用できるよう開発が進みました。

そして満を持してMetal対応バージョンとなる [cocos2d-x v4.0がリリース](https://github.com/cocos2d/cocos2d-x/releases/tag/cocos2d-x-4.0)されました。
Apple環境においてMetalが利用できるようになり、これまでは遅かったiOSシミュレータ上での動作もMetalによって爆速になりました🎉


# v4における大きなアップデート

私はcocos2d-x v4.0の特に大きな変更点は以下の2つだと思っています。

- Metalレンダラのサポート
- 全プラットフォームでCMakeを利用したビルドシステムを利用

Metal対応はめでたいですね！
次点のCMake利用についてですが、
cocos2d-xはv3系後半においてゆるやかにCMake移行が行われてきました。
Androidにおいてはビルド環境がCMakeに移行したことでビルドが安定するようになりました。

# v4移行における問題点

CMakeに全体移行されたことにより、iOSをはじめとしたApple環境においてはv4へのマイグレーションにやや難が発生しました。

## 自動生成が前提とされたxcodeproj

xcodeprojファイルは開発者にはおなじみのプロジェクト設定ファイルです。
これまでは、cocos2d-xプロジェクトの新規作成時にxcodeprojファイルも自動で生成されました。
v4においては、以下のコマンドでxcodeprojファイルを生成できます（iOSの場合。以降暗黙にiOSを前提とします）。

```
$ cmake <プロジェクトルートへのパス> -GXcode -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphoneos
```

ここで生成されるxcodeprojは**ローカルへの依存が大量に埋め込まれ**てしまいます。
これは、PCや環境が変化するとそのxcodeprojが容易に壊れてしまうということです。例えば、
プロジェクトのあるディレクトリ位置が変化したときなど。

これはつまり、xcodeprojは環境ごとに毎回生成しバージョン管理はしない。という開発者からの暗黙のメッセージだと受け取れます。

## 既存設定のCMakeへの移行が難しい

さて、毎回xcodeprojを生成すると何が問題となるでしょうか。
私たちは普段、アプリに関する情報をxcodeprojに保存します。
アイコン画像やビルドスキーマの設定などです。

CMakeでxcodeprojを生成する場合、xcodeproj側で設定した項目は再生成のたびに失われます。
設定を維持するためには、**これまでxcodeproj内で表現されていたすべての設定をCMakeLists.txt内に書き直さないといけません**。

これがなかなかに難しいです。
移行のためにはxcodeprojにおけるある設定がCMakeではどう表現されるかを知る必要があるのですが、
[CMakeのXcodeGeneratorについてのドキュメント](https://cmake.org/cmake/help/latest/generator/Xcode.html)をみても何も書いてありませんし、[実装](https://github.com/Kitware/CMake/blob/master/Source/cmGlobalXCodeGenerator.cxx)を見ても全然わかりません。
どこかに良いドキュメントがあれば教えて下さい。

```CMakeLists.txt
# 例えば、リソースファイルを設定するための記述
set_target_properties(${APP_NAME} PROPERTIES RESOURCE "${APP_UI_RES}")
```

## ネイティブライブラリとの連携も高難度

iOSでは広告SDKなどの外部ライブラリを導入するためにCocoaPodsを利用することがあると思います。
cocos2d-xの公式ドキュメントには[CMakeとCocoaPodsを連携するための手順](https://docs.cocos2d-x.org/cocos2d-x/v4/en/upgradeGuide/migration.html#using-cmake-and-cocoapods)が記載されていますが、これは `pod install` のたびに手動操作が必要で全く現実的ではありません。

これを解決するためには、CocoaPodsが暗に施すビルド設定を手動で設定する必要があります。
幸いにもCocoaPodsはxcconfig[^1]を吐いてくれますし、Ruby製であるためモンキーパッチをあてて挙動を捻じ曲げることができます。

[^1]: xcodeproj用の設定ファイル。各種TargetとConfigurationごとに設定できる。

じゃあ頑張ればなんとかなるかな？と思って頑張りましたが、わたしはここで脱落しました。
xcconfigを設定するだけでビルドが通る状態には持ち込めたのですが、肝心のxcconfigをCMakeから設定する方法がわかりませんでした。
その他、全体的に筋肉で解決させなければいけない課題が多かったためここで方針を変える決意をしました。

- モンキーパッチをあてて出力した怪しいxcconfig
<img width="380" alt="ayashi_xcconfig.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121299/349528aa-bfac-dd4e-d915-163551bd6f04.png">

# 解決策 - cocos2d_libs.xcodeprojの自作

cocos2d_libs.xcodeprojは、これまでのcocos2d-xにおいてプロジェクト新規作成時に埋め込まれていたものです。
cocos2d-xのビルド設定はほぼすべてこの中にありました。
これがあれば、**これとソースコードを既存プロジェクトのそれと入れ替えるだけで簡単にv4へ移行できる**のです。

CMake移行にともなってv4では削除されてしまいましたが、cocos2d_libs.xcodeprojがないなら自分で作ればいいのです。
cocos2d-xはあくまでC++のライブラリの集合体でしかないので、要はコンパイルしてリンクできてしまえば何でもいいのです。

## 1からxcodeprojを作る難しさ

大規模プロジェクトのxcodeprojを1から作るのは大変です。
単純にファイルを追加していくのが手間で、ちょっとマウス操作をミスると構造が壊れてUndoもできなくて泣きます。

一度作ったとしても、今後cocos2d-xがさらにバージョンアップしてソースファイルが増減したときにまたxcodeprojを編集するのは骨が折れます。

そこで自動生成ツールを使用します。

## XcodeGen

[XcodeGen](https://github.com/yonaskolb/XcodeGen)は近年iOS開発の現場において人気を獲得しつつあるxcodeproj生成ツールです。
xcodeproj生成という意味ではCMakeと変わりませんが、よりアプリケーション開発に特化して設定ファイルが簡単に書けるようになっています。

以下、XcodeGenを用いてcocos2d_libs.xcodeprojを生成するための手順を解説していきます。

## cocos2d-xのプロジェクト構成

cocos2d-xを構成するファイル群のうち、ビルドに必要なものは3つのディレクトリに絞られます。

```
cocos2d
├── cocos       // 本体となるソースファイル群
├── extensions  // 拡張機能群。必要ないことも多い
└── external    // 本体が依存しているライブラリと開発に使用できる外部ライブラリ
```

## メインとなるソースコード郡のビルド

cocos2d-xライブラリ（ `libcocos2d.a` ） の本体となる `cocos` と `extensions` は `.h` や `.cpp` のソースファイル群であるためそのままコンパイル対象として指定します。
これはXcodeGenのproject.yml[^2]では次のように記述されます。

[^2]: XcodeGen用の設定ファイルのデフォルト名。

```project.yml
    sources: 
      - path: ../cocos
        includes: ["**/*.{h,hpp,c,cc,cpp,mm}"]
      - path: ../extensions
        includes: ["**/*.{h,hpp,c,cc,cpp,mm}"]
```

このように記述することで、XcodeGenは対象ディレクトリの中のファイルを自動でxcodeprojにディレクトリ構造をそのままに登録してくれます。

なお、実際の設定にはこれだけでは足らず、他プラットフォーム向けやOpenGL実装のファイルを除外する必要があります。
最終的なproject.ymlは最後に掲載します。

### HEADER_SEARCH_PATHSの設定

`libcocos2d.a` のソースコードはいくつかの外部ライブラリに依存しているため、コンパイルのためにはその外部ライブラリのヘッダーファイルを与える必要があります。
これはHEADER_SEARCH_PATHSというビルドオプションで指定できます。
いくつかのビルドオプションを与えるために　`cocos2d_libs.xcconfig` ファイルを作成します。

なおビルドオプションはproject.ymlから与えることもできるのですが、私はできるだけ公式の仕組みに乗っていたほうが良いと考えており可能な範囲は公式の仕組みであるxcconfigを使用します。

```cocos2d_libs.xcconfig
HEADER_SEARCH_PATHS = $(SRCROOT)/../ $(SRCROOT)/../cocos $(SRCROOT)/../cocos/platform $(SRCROOT)/../cocos/editor-support $(SRCROOT)/../extensions $(SRCROOT)/../external $(SRCROOT)/../external/chipmunk/include $(SRCROOT)/../external/tinyxml2 $(SRCROOT)/../external/xxhash $(SRCROOT)/../external/xxtea $(SRCROOT)/../external/clipper $(SRCROOT)/../external/edtaa3func $(SRCROOT)/../external/ConvertUTF $(SRCROOT)/../external/poly2tri $(SRCROOT)/../external/md5 $(SRCROOT)/../external/unzip

SYSTEM_HEADER_SEARCH_PATHS = $(SRCROOT)/../external/Box2D/include $(SRCROOT)/../external/chipmunk/include $(SRCROOT)/../external/freetype2/include/ios/freetype2 $(SRCROOT)/../external/bullet/include $(SRCROOT)/../external/bullet/include/bullet $(SRCROOT)/../external/jpeg/include/ios $(SRCROOT)/../external/openssl/include/ios $(SRCROOT)/../external/uv/include $(SRCROOT)/../external/webp/include/ios $(SRCROOT)/../external/websockets/include/ios $(SRCROOT)/../external/curl/include/ios $(SRCROOT)/../external/png/include/ios $(SRCROOT)/../external/glsl-optimizer/include

GCC_PREPROCESSOR_DEFINITIONS[config=Debug] = USE_FILE32API __APPLE__ "CC_ENABLE_CHIPMUNK_INTEGRATION=1" "COCOS2D_DEBUG=1"
GCC_PREPROCESSOR_DEFINITIONS[config=Release] = USE_FILE32API __APPLE__ "CC_ENABLE_CHIPMUNK_INTEGRATION=1"

CLANG_ENABLE_OBJC_ARC = NO
ENABLE_BITCODE = NO
```

（長々としていますが、単にヘッダがあるディレクトリへのパスを羅列しているだけです。）

ついでに他のいくつかのフラグを与えています。例えば `CLANG_ENABLE_OBJC_ARC` はObjective-CのARCを切り替えるオプションで、MRCのcocos2d-xでは無効にしています。

使用するxcconfigファイルはXcodeGenでは次のように指定します。

```project.yml
    configFiles:
      Debug: cocos2d_libs.xcconfig
      Release: cocos2d_libs.xcconfig
```

(CMakeではこれをどう指定するかが分からなかった・・・)

## 2種類ある外部ライブラリのビルド

残る `external` ディレクトリについてですが、この中の外部ライブラリには2種類あります。
ソースファイルが置かれたものと、事前ビルドされたファイルが置かれたものです。

### ソースファイルのあるライブラリの場合

そのライブラリを自前でビルドするためにXcodeGenでTargetを生成します。
設定は `unzip` を例に、次のようになります。

```project.yml
  unzip:
    platform: iOS
    type: library.static
    sources:
      - path: ../external/unzip
        group: external
        includes: ["**/*.{h,hpp,c,cc,cpp}"]
    settings:
      HEADER_SEARCH_PATHS: $(SRCROOT)/../external
```

たったこれだけです。これで `unzip` Targetが生成され、 `libunzip.a` がビルドできるようになります。

dependenciesにTargetを記載しておくと本体のビルド時に一緒にビルドが走るようになるため追加しておきます。

```project.yml
    dependencies:
      - target: unzip
```

### 事前ビルド済みライブラリの場合

その名の通りビルドが済んでいるため、何もすることがありません。

## 外部ライブラリをリンクする

ここまでの設定で各種ライブラリのビルドが完了しますが、いざアプリケーション側のコードをビルドしようとするとリンクエラーとなります。

アプリケーションバイナリへのstatic libraryのリンクには `ld` コマンドを用います。
static libraryのある場所を `-L` オプションで指定し、名前を `-l` オプションで指定します。
Xcodeはこれを内部的にやってくれています。

通常、このリンク設定はアプリケーション側のビルド設定に記述する必要があります。
既存のアプリのxcodeprojには `libcocos2d iOS.a` への依存が記載されているでしょうが、外部ライブラリの依存については記載されていません。
これはどういうことでしょうか。

XcodeにはライブラリTargetの `Build Phases > Link Binary With Libraries` の項目にstatic libraryを追加すると、そのライブラリを使うアプリTargetのビルド時に追加したstatic libraryのリンク設定を自動でしてくれる機能があります。

この機能を活用するために `Link Binary With Libraries` のところに外部ライブラリ群を入れます。
XcodeGenでは `link: true` の記載をするだけです。

```project.yml
    dependencies:
      - target: unzip
        link: true
      - framework: ../external/chipmunk/prebuilt/ios/libchipmunk.a
        link: true
```

また、この機能では本来ライブラリTargetの使用しないリンクプロセス用のビルド引数がアプリTargetに引き継がれる特性があるため、その引数も指定します。

```cocos2d_libs.xcconfig
LIBRARY_SEARCH_PATHS = $(SRCROOT)/../external/chipmunk/prebuilt/ios $(SRCROOT)/../external/png/prebuilt/ios $(SRCROOT)/../external/jpeg/prebuilt/ios  $(SRCROOT)/../external/webp/prebuilt/ios $(SRCROOT)/../external/freetype2/prebuilt/ios $(SRCROOT)/../external/curl/prebuilt/ios $(SRCROOT)/../external/websockets/prebuilt/ios $(SRCROOT)/../external/chipmunk/prebuilt/ios $(SRCROOT)/../external/openssl/prebuilt/ios $(SRCROOT)/../external/bullet/prebuilt/ios $(SRCROOT)/../external/Box2D/prebuilt/ios $(SRCROOT)/../external/uv/prebuilt/ios $(SRCROOT)/../external/glsl-optimizer/prebuilt/ios
```

これですべての設定が完了しました。
最終的なファイルは次のようになりました→
[cocos2d_libs_generate](https://github.com/sidepelican/cocos2d_libs_generate/tree/858e682cd128052c2cd0163f335c9c6d2687595b/build)

## xcodeprojの生成

v3時代と似た構造にするために、 `build` ディレクトリ下にxcodeprojを配置することにします。
設定ファイルを次のように配置し、XcodeGenを実行します。

```
cocos2d
├── build
│   ├── cocos2d_libs.xcconfig
│   └── project.yml
```

```
$ cd cocos2d/build
$ xcodegen generate
```

生成したxcodeprojを既存のものと入れ替えて、
無事にアプリケーションTargetをビルドすることができました。

一連のファイルとビルド済みxcodeprojと簡単な操作方法は以下のリポジトリに置いておきます。
[cocos2d_libs_generate](https://github.com/sidepelican/cocos2d_libs_generate)

# おわりに

Metalの世界へようこそ！
シミュレータが爆速なことにとにかく感動しています。
私のプロジェクトではGLSLシェーダなどを特にいじってなかったのでビルドさえできれば簡単にMetal移行できましたが、現実としてカスタムシェーダたくさんつかってる大規模プロジェクトではここからが本番という形になると思います。

cocos2d-x製の商業アプリはどれくらいv4移行するんでしょうかね？
その他にUIWebView廃止の話もあったりして、v4に追従しなければいけないのは明確ですが現実としてバックポートで済ます場面もあると思います。
今後のcocos2d-x製アプリケーションの展開が気になりますね！

AndroidではもともとビルドにCMakeが使われているので移行は簡単でした。
Android.mkから脱却できていない場合はこれを機にCMakeへ移行することをオススメします。