Cocos2d-x V4 iOS/Android プロジェクト移行作業(シェーダ関連含む)
ご挨拶
初めまして、私は普段は 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 をダウンロードしましょう。
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 を編集
PROJECT
の Build Settings
-> Search Paths
-> に以下を追加
$(SRCROOT)/../cocos2d/cocos
$(SRCROOT)/../cocos2d/cocos/editor-support/cocostudio
TARGETS
の Build Settings
-> User Header Search Paths
に以下を追加
$(inherited)
Other Linker Flags を追加
TARGETS
の Build 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 の修正
TARGETS
の Build Phases
-> Dependecies
に cocos2d
を追加
Link Binary With Libraries
に Metal.framework
と cocos2d
配下の *.a
ファイルを全て追加してください。
以上で iOS はビルドできるずです(╹◡╹)
Android のビルド設定
Android Studio がなければ DL & インストールしてください。
プロジェクトのコピー
先ほど cocos new
コマンドで作成した proj.android
を既存プロジェクトへコピペします。
コピペしたプロジェクトを Android Studio
で開きます。
Preference の設定
Preference
-> SystemSettings
-> Android SDK
で SDK Tools
タブを選択
NDK
と CMake
にチェックを入れて OK をクリックしてください。
CMakeLists の設定
プロジェクト直下
の CMakeLists
の add cross-platforms source files and header files
の APPEND GAME_SOURCE
と APPEND 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.txt
の cmake_minimum_required
を VERSION 3.10
に設定すればビルドが走ると思います。
cmake_minimum_required(VERSION 3.10)
でも、この場合は Xcode のビルドが実行されなくなってしまいます。
誰か解決策知っている人がいれば教えてください🙇♂️
ビルドは実行されたけど
ここまででビルドが走るまではいけたかと思いますが、Cocos2d-x Ver 4 の対応が別途必要なパターンがあります。
特にシェーダ周りは Metal
対応でガラッと変わっていますので、その対応の事例を書いてみたいと思います( ・∀・ )ゞ
シェーダの対応
というわけでシェーダを対応していきます。
まず基本的にシェーダ関連は以下のクラスに変わりました。
GLProgram -> backend::Program
GLProgramState -> backend::ProgramState
なのでまずはこれらに置き換える必要があります。
シェーダ生成
Program
と ProgramState
の生成方法はこちらになります。
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#newProgram
は ProgramMTL
を使用します。
Program* DeviceMTL::newProgram(const std::string& vertexShader, const std::string& fragmentShader)
{
return new (std::nothrow) ProgramMTL(vertexShader, fragmentShader);
}
ProgramMTL
は ShaderCache#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