👾

Cocos2d-x V4 iOS/Android プロジェクト移行作業(シェーダ関連含む)

2020/10/21に公開

ご挨拶

初めまして、私は普段は Swift / Kotlin で iOS / Android アプリを開発しているエンジニアです。

最近、Flutter 製のゲーム「ペコシュー」というレトロ風味のシンプルな横シューティングゲームをリリースしました!
AppStore : https://apps.apple.com/us/app/id1527790378
GooglePlay : https://play.google.com/store/apps/details?id=jp.forestonegame.PekopekoShooting

Cocos2d-x に関しては
ゲーム製作超入門という記事も書いておりました。

というわけでお手柔らかにお願いします!
シェーダに関することも書いていますよ♪

Cocos2d-x Ver 4

去年の 12 月くらいに Cocos2d-x の Ver4 がリリースされました。
このバージョンから iOS の Metal に対応されるとのことで Cocos2d-x 制のゲームやアプリはいずれ対応が必要になります。

私もその内やらないとなと思っていたのですが、新しい iPhone が出るので、この機会にやってしまおうと思い今回移行することにしました( ・∀・ )ゞ
私の場合、旧バージョンは 3.17 です。
Xcode は 12.0.1 です。

基本的な移行手順

基本的な移行手順は公式と下記の参考記事を参考にしました。
iOS に関してはそちらで問題ない感じでしたので、下記のサイト見てもらっても良いと思います🙇‍♂️
一応、この記事でも簡単に書いておきます。

公式:https://docs.cocos2d-x.org/cocos2d-x/v4/en/upgradeGuide/migration.html
参考記事:http://shakezoomer.com/archives/1102

念のため書いておきますが、移行する時は元の状態へ復元できるようにしておいてください。
(バージョン管理していれば大丈夫と思いますが)

まずはともかくダウンロード

とりあえず Ver4 をダウンロードしましょう。
https://www.cocos2d-x.org/download

cocos コマンドの設定

setup.py で cocos コマンドを Ver4 のものにします。
完了したら cocos -v で確認してみましょう。

$ cocos -v
cocos2d-x-4.0
Cocos Console 2.3

CMake のインストール

cmake をインストールしていなければインストールしてください。

$ brew install cmake

Ver4 のプロジェクトを作成する

$ cocos new `Project名` -d ./ -l cpp -p `名前空間`

cocos2d ライブラリを移行する

ここから cocos2d Verの移行作業を行います。

cocos2d ライブラリと CMakeLists.txt を既存プロジェクトに上書きする

作成したプロジェクト直下の cocos2d フォルダCmakeLists.txt を既存プロジェクトの方へ上書きコピーします。

cocos2d_libs.xcodeproj を作成する

cmake で cocos2d_libs.xcodeproj を生成させるために既存プロジェクトの cocos2d/cocos/CMakeLists.txt を編集していきます。
ここは公式ガイドを参照してください。
Modify v4 cocos2d_libs CMakeLists.txt.CmakeLists.txt の箇所を参考にしてください。

cocos2d/ios-build のディレクトリを作成

$ mkdir cocos2d/ios-build 

cmake で cocos2d_libs.xcodeproj を生成

$ cd cocos2d/cocos
$ cmake -B ../ios-build -GXcode -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphoneos

作成した cocos2d_libs.xcodeproj をインポート

Xcode で 既存の proj.ios_mac/xxx.xcodeproj を開くと cocos2d_libs.xcodeproj が missing 状態なので cmake で作成した cocos2d_libs.xcodeproj をドラッグ&ドロップしてインポートしてください。

iOS 側のビルド設定

Build Settings を編集していきます。
ここも公式ガイドを参照してください。
簡単にまとめます。

Search Paths を編集

PROJECTBuild Settings -> Search Paths -> に以下を追加

$(SRCROOT)/../cocos2d/cocos
$(SRCROOT)/../cocos2d/cocos/editor-support/cocostudio

TARGETSBuild Settings -> User Header Search Paths に以下を追加

$(inherited)

Other Linker Flags を追加

TARGETSBuild Settings -> Other Linker Flats に以下を追加

$(SRCROOT)/../cocos2d/external/Box2D/prebuilt/ios/libbox2d.a
$(SRCROOT)/../cocos2d/external/jpeg/prebuilt/ios/libjpeg.a
$(SRCROOT)/../cocos2d/external/freetype2/prebuilt/ios/libfreetype.a
$(SRCROOT)/../cocos2d/external/webp/prebuilt/ios/libwebp.a
$(SRCROOT)/../cocos2d/external/bullet/prebuilt/ios/libLinearMath.a
$(SRCROOT)/../cocos2d/external/bullet/prebuilt/ios/libBulletDynamics.a
$(SRCROOT)/../cocos2d/external/bullet/prebuilt/ios/libBulletCollision.a
$(SRCROOT)/../cocos2d/external/bullet/prebuilt/ios/libLinearMath.a
$(SRCROOT)/../cocos2d/external/bullet/prebuilt/ios/libBulletMultiThreaded.a
$(SRCROOT)/../cocos2d/external/bullet/prebuilt/ios/libMiniCL.a
$(SRCROOT)/../cocos2d/external/websockets/prebuilt/ios/libwebsockets.a
$(SRCROOT)/../cocos2d/external/uv/prebuilt/ios/libuv_a.a
$(SRCROOT)/../cocos2d/external/openssl/prebuilt/ios/libssl.a
$(SRCROOT)/../cocos2d/external/glsl-optimizer/prebuilt/ios/libmesa.a
$(SRCROOT)/../cocos2d/external/glsl-optimizer/prebuilt/ios/libglsl_optimizer.a
$(SRCROOT)/../cocos2d/external/glsl-optimizer/prebuilt/ios/libglcpp-library.a
$(SRCROOT)/../cocos2d/external/png/prebuilt/ios/libpng.a
$(SRCROOT)/../cocos2d/external/curl/prebuilt/ios/libcurl.a
$(SRCROOT)/../cocos2d/external/openssl/prebuilt/ios/libcrypto.a
$(SRCROOT)/../cocos2d/external/chipmunk/prebuilt/ios/libchipmunk.a

改行ごとコピペすると自動で1行ずつ追加されます。

Build Phases の修正

TARGETSBuild Phases -> Dependeciescocos2d を追加
Link Binary With LibrariesMetal.frameworkcocos2d 配下の *.a ファイルを全て追加してください。

以上で iOS はビルドできるずです(╹◡╹)

Android のビルド設定

Android Studio がなければ DL & インストールしてください。

プロジェクトのコピー

先ほど cocos new コマンドで作成した proj.android を既存プロジェクトへコピペします。
コピペしたプロジェクトを Android Studio で開きます。

Preference の設定

Preference -> SystemSettings -> Android SDKSDK Tools タブを選択
NDKCMake にチェックを入れて OK をクリックしてください。

CMakeLists の設定

プロジェクト直下CMakeListsadd cross-platforms source files and header filesAPPEND GAME_SOURCEAPPEND GAME_HEADER を設定してください。

list(APPEND GAME_SOURCE
   Classes/AppDelegate.cpp
   // ... 追加する .cpp
   )
     
list(APPEND GAME_HEADER
   Classes/AppDelegate.h
   // ... 追加する .hpp / h
   )

それと同時に target_include_directories(${APP_NAME} も設定してください

target_include_directories(${APP_NAME}
        PRIVATE Classes
        PRIVATE Classes/hoge
        ...

以上でビルドすれば出来るかと思います。

CMake 3.15 or higher is required. のエラーが出た時

実は私の場合は CMake 3.15 or higher is required. You are running version 3.10.2 のエラーが出てしまったのですが、その際は cocos2d/cocos/CMakeLists.txtcmake_minimum_requiredVERSION 3.10 に設定すればビルドが走ると思います。

cmake_minimum_required(VERSION 3.10)

でも、この場合は Xcode のビルドが実行されなくなってしまいます。
誰か解決策知っている人がいれば教えてください🙇‍♂️

ビルドは実行されたけど

ここまででビルドが走るまではいけたかと思いますが、Cocos2d-x Ver 4 の対応が別途必要なパターンがあります。
特にシェーダ周りは Metal 対応でガラッと変わっていますので、その対応の事例を書いてみたいと思います( ・∀・ )ゞ

シェーダの対応

というわけでシェーダを対応していきます。
まず基本的にシェーダ関連は以下のクラスに変わりました。

GLProgram -> backend::Program
GLProgramState -> backend::ProgramState

なのでまずはこれらに置き換える必要があります。

シェーダ生成

ProgramProgramState の生成方法はこちらになります。

auto fileUtiles = FileUtils::getInstance();
auto vertexFullPath = fileUtiles->fullPathForFilename(vertFilePath);
auto vertexSource = fileUtiles->getStringFromFile(vertexFullPath);
auto fragmentFullPath = fileUtiles->fullPathForFilename(fragFilePath);
auto fragmentSource = fileUtiles->getStringFromFile(fragmentFullPath);

auto program = backend::Device::getInstance()->newProgram(vertexSource.c_str(), fragmentSource.c_str());
auto programState = new backend::ProgramState(program);

Cocos2d-x Ver3 だと

GLProgramCache::getInstance()->addGLProgram(shader, getKey(type));

みたいにキャッシュさせる必要があるのかなと思いましたが、ゲームエンジンのソースコード見た感じ不要だと思いました。
以下にちょっとだけ調べた内容を記載します。

シェーダのキャッシュ追加処理が不要な理由

シェーダのキャッシュ追加処理自体は ShaderCache.cpp#newShaderModule で行っています。

// ShaderCache.cpp

backend::ShaderModule* ShaderCache::newVertexShaderModule(const std::string& shaderSource)
{
    auto vertexShaderModule = newShaderModule(backend::ShaderStage::VERTEX, shaderSource);
    return vertexShaderModule;
}

backend::ShaderModule* ShaderCache::newFragmentShaderModule(const std::string& shaderSource)
{
    auto fragmenShaderModule = newShaderModule(backend::ShaderStage::FRAGMENT, shaderSource);
    return fragmenShaderModule;
}

backend::ShaderModule* ShaderCache::newShaderModule(backend::ShaderStage stage, const std::string& shaderSource)
{
    std::size_t key = std::hash<std::string>{}(shaderSource);
    auto iter = _cachedShaders.find(key);
    if (_cachedShaders.end() != iter)
        return iter->second;
    
    auto shader = backend::Device::getInstance()->newShaderModule(stage, shaderSource);
    shader->setHashValue(key);
    _cachedShaders.emplace(key, shader);
    
    return shader;
}

Device::getInstance()DeviceMTL がデフォルトで使用されています。

Device* Device::getInstance()
{
    if (! Device::_instance)
        Device::_instance = new (std::nothrow) DeviceMTL();

    return Device::_instance;
}

DeviceMTL#newProgramProgramMTL を使用します。

Program* DeviceMTL::newProgram(const std::string& vertexShader, const std::string& fragmentShader)
{
    return new (std::nothrow) ProgramMTL(vertexShader, fragmentShader);
}

ProgramMTLShaderCache#newVertexShaderModule を使用しているのでシェーダプログラムを作成したら自動でキャッシュが生成されます。

ProgramMTL::ProgramMTL(const std::string& vertexShader, const std::string& fragmentShader)
: Program(vertexShader, fragmentShader)
{
    _vertexShader = static_cast<ShaderModuleMTL*>(ShaderCache::newVertexShaderModule(vertexShader));
    _fragmentShader = static_cast<ShaderModuleMTL*>(ShaderCache::newFragmentShaderModule(std::move(metalSpecificDefine + fragmentShader)));

    CC_SAFE_RETAIN(_vertexShader);
    CC_SAFE_RETAIN(_fragmentShader);
}

以上の理由からゲームエンジン側が自動でキャッシュ処理をしてくれるので、キャッシュ追加処理を改めて実装する必要はありません( ・∀・ )ゞ
(たぶん、そのはず)

スプライトにシェーダをセットする

auto program = backend::Device::getInstance()->newProgram(vertexSource.c_str(), fragmentSource.c_str());
auto programState = new backend::ProgramState(program);

cloneProgramState = programState.state->clone();
auto uHoge = cloneProgramState->getUniformLocation("uHoge");
float uHogeValue = 1.0f;

// 値を設定する時.
auto cloneProgramState->setUniform(uHoge, &uHogeValue, sizeof(uHogeValue));

// シェーダ更新の度に値を設定したい時.
cloneProgramState->setCallbackUniform(uHoge, [this](backend::ProgramState *programState, backend::UniformLocation uniform){
    float value = 1.0f;
    programState->setUniform(uniform, &value, sizeof(value));
});

sprite->setProgramState(cloneProgramState);

こんな感じでシェーダを設定します。
setProgram みたいなのは不要っぽいです。

以上が Ver4 での Sprite にシェーダを設定する方法になります。

注意事項

Sprite に対してシェーダをセットしていた時 setTexture しているとシェーダが外れてしまいます。

sprite->setTexture(...);

なので、setTexture を呼んだ後には再度 setProgramState を呼ぶ必要があるみたいです。

sprite->setTexture(...);
sprite->setProgramState(cloneProgramState);

こちらは少しイケてない気がしたので issue を投げてみました

追記:投げた issue に対して海外の方が反応してくれました。

CCSprite#setTexture を下記のように書き換えてやると良いかもです。
試したところパッと見は問題なさそうでした!

void Sprite::setTexture(Texture2D *texture)
{
    if (_programState == nullptr || _programState->getProgram()->getProgramType() == backend::ProgramType::POSITION_TEXTURE_COLOR)  // if でチェック入れとく.
    {
        auto isETC1 = texture && texture->getAlphaTextureName();
        setProgramState((isETC1) ? backend::ProgramType::ETC1 : backend::ProgramType::POSITION_TEXTURE_COLOR);    
    }
    ...
    updateProgramStateTexture();
}

こちらの方法だと一度 setProgramState しておけば OK です。

シェーダの書き方変更

Metal 対応に伴い、シェーダの書き方も若干変更されました。
Forum を参考にしました。

Vertex Shader:

attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;

uniform mat4 u_MVPMatrix;

void main() {
    gl_Position = u_MVPMatrix * a_position;
    v_texCoord = a_texCoord;
    v_fragmentColor = a_color;
}

Fragment Shader:

// Fragment
varying vec2 v_texCoord;

uniform vec3 u_random;
uniform sampler2D u_texture;

vec2 SineWave(vec2 p) 
{
    float x;
    x = 0.03 * sin( 25.0 * p.y + u_random.x * 5.0 );
    return vec2(p.x+x, p.y);
}

void main() {
    gl_FragColor = texture2D(u_texture, SineWave(v_texCoord));
}

予め定義されている uniform は以前では CC_* を使用していたと思いますが u_* に変わったんですかね?

添付画像は別シェーダのラスタスクロールですが、うまく実行されることを確認できました!

スクロールビューに関して

とても残念なのですが、スクロールビューのクリッピングは現在の Ver4 ではまだ実装されていないようです。ScrollView_clippingToBounds 関連が軒並みにコメントアウトされています😭
これはかなり痛いのではないでしょうか・・・?

void ScrollView::onBeforeDraw()
{
    //TODO: minggo
//    if (_clippingToBounds)
//    {
//        _scissorRestored = false;
//        Rect frame = getViewRect();
//        auto glview = Director::getInstance()->getOpenGLView();
//
//        if (glview->getVR() == nullptr) {
//            if (glview->isScissorEnabled()) {
//                _scissorRestored = true;
//                _parentScissorRect = glview->getScissorRect();
//                //set the intersection of _parentScissorRect and frame as the new scissor rect
//                if (frame.intersectsRect(_parentScissorRect)) {
//                    float x = MAX(frame.origin.x, _parentScissorRect.origin.x);
//                    float y = MAX(frame.origin.y, _parentScissorRect.origin.y);
//                    float xx = MIN(frame.origin.x + frame.size.width, _parentScissorRect.origin.x + _parentScissorRect.size.width);
//                    float yy = MIN(frame.origin.y + frame.size.height, _parentScissorRect.origin.y + _parentScissorRect.size.height);
//                    glview->setScissorInPoints(x, y, xx - x, yy - y);
//                }
//            }
//            else {
//                glEnable(GL_SCISSOR_TEST);
//                glview->setScissorInPoints(frame.origin.x, frame.origin.y, frame.size.width, frame.size.height);
//            }
//        }
//    }
}

void ScrollView::onAfterDraw()
{
    //TODO:minggo
//    if (_clippingToBounds)
//    {
//        auto glview = Director::getInstance()->getOpenGLView();
//        if (glview->getVR() == nullptr) {
//            if (_scissorRestored) {//restore the parent's scissor rect
//                glview->setScissorInPoints(_parentScissorRect.origin.x, _parentScissorRect.origin.y, _parentScissorRect.size.width, _parentScissorRect.size.height);
//            }
//            else {
//                glDisable(GL_SCISSOR_TEST);
//            }
//        }
//    }
}

おしまい

以上で私が行った Version4 移行は終了です(╹◡╹)
シェーダも iOS/Android 両方で実行されていることが確認できました( ・∀・ )ゞ
スクロールビューはちょっと残念ですが、その内対応してくれることを信じましょうw

Cocos2d-x に幸あれ〜♪

Discussion