cocos2d-x 3.0での多言語化対応
今作っているゲームが完成を迎えそうです。リリースに向けて最後の調整を行っています。その1つが多言語化対応です。多言語に対応することでターゲットとなるユーザのパイを増やすことができ少しでも多くの人にダウンロードされ遊んでもらえる可能性が増えます。そのため、僕らの母国語である日本語以外にも少なくとも話者が多い英語は対応したいところです。欲を言えば中国語も。
しかし、cocos2d-xでは多言語化の機能を提供していません。そのため、その処理は自分で各必要があります。iOSアプリ開発を行っていた方はわかると思いますが、iOSでは非常に簡単に多言語化対応を行う事ができます。以下のようにすることで、設定した言語ごとにGameTitleに対応する文字列が取得できます。コードでは、if(英語){}elseif(日本語)といったことをやらなくても良いのです。そこで、今回はiOSにならって同等の多言語化対応のメソッドの実装を行います。
//日本語の場合には、"ほげタイトル"という文字列がそれ以外の場合には"hogeTitle"が返ってきます NSString *title = NSLocalizedString(@"GameTitle", @"");
どのように多言語化を実現しているのか簡単に説明すると、言語ごとにテキストファイルを用意し起動時に設定された言語によって読む込むテキストファイルを変更しています。テキストファイルの中には以下のようになっておりkeyとvalueで設定されています。今回作るメソッドに、keyとなる文字列を渡せば利用している言語に合わせた文字列が返ってきます。titleMenuStartを引数に渡した場合、戻り値には日本語端末では、スタートが返ってきて、それ以外の言語端末であればSTARTが返されるといったことを実現します。
前準備
まず、言語ごとにテキストファイルを用意します。起動時に設定された言語によって読む込むテキストファイルを変更しています。テキストファイルの中には以下のようになっておりkeyとvalueで設定されています。
日本語用ファイル
"titleMenuStart" = "スタート"; "titleMenuRanking" = "ランキング"; "titleMenuGallery" ="ギャラリー";
英語用ファイル
"titleMenuStart" = "START"; "titleMenuRanking" = "RANKING"; "titleMenuGallery" ="GALLERY";
ファイルの作成は、Android、iOSどちらからもアクセスできる共有のリソースになるのでResourcesディレクトリ以下にファイルの作成を行います。New File>Resource>String fileの順に選択しテキストファイルを作成してください。ファイル名は何でも良いのですがここでは、iOSにならってLocalizable.stringsとしました。
デフォルトでは作成したファイルを選択し、右側のペインでTypeをDefaultからPlain Textに変更してください。これは、Defaultのままだとビルド時にplistとしてバイナリに変換されてしまい意図したように動作しないためです。
最後に右側のペインからLocalizeボタンをクリックして日本語用のファイルを追加します。英語用、日本語用のファイルそれぞれに上述ファイル内容を記載します。
実装
ここまでで前準備が終了で、ここからは実装に入っていきます。まず、今回作るLocalizedStringメソッドはNSLocalizedString同様、対象の文字列に対応するキーと文字列がなかった場合に表示するコメントを引数にとります。LocalizedString内では、まず現在設定されている言語情報から日本語が設定されている場合は、ファイルの検索パスにja.lprojを追加し、それ以外の場合にはen.lprojを追加しています。これで、日本語の場合には日本語ファイルが読み込まれ、それ以外の場合には英語ファイルが読み込まれます。
const char* LocalizedString(const char* searchKey, const char* comment){ const char* ret = comment; static map<string, string> localizable; if(localizable.empty()){ FileUtils* fileUtil = FileUtils::getInstance(); LanguageType language = Application::getInstance()->getCurrentLanguage(); if(language == LanguageType::JAPANESE){ fileUtil->addSearchPath("ja.lproj"); } else{ fileUtil->addSearchPath("en.lproj"); } }
次に読み込んだファイル内容をパースして引数に渡されたキーに対応するものがあれば返しなければcommentを返しています。
list<string> stringLines = split(strings, ";"); for(list<string>::iterator begin = stringLines.begin(), end = stringLines.end(); begin != end; ++begin){ list<string> stringKeyValue = split(*begin, "="); if(stringKeyValue.size() >= 2){ list<string>::iterator keyv = stringKeyValue.begin(); localizable.insert(pair<string, string>(*keyv, *(++keyv))); } }
ソースはすべてgithubにあげましたので詳細はそちらを見てみてください。
ttdog/LocalizedStringForCocos2d-x · GitHub
自動変数の寿命と戻り値の関係
cocos2d-xを触るにあたってc++を真面目に勉強しています。その中で、これまで知らなくて目から鱗が落ちた事が合ったので備忘録的に残しておきたいと思います。
c++というかcでは、ローカル変数のスコープはブロック内に閉じ、ブロックを抜けた後の変数の存在は保証されません。このような、1時的な変数を自動変数と呼びます。ブロック内でスコープが閉じるっていうのは感覚的に知っているっていう事は多いと思います。
void Hoge(int b){ int c; for(int d = 0; d < 9; ++){ int e; //eの寿命はこのブロックが終わるまで } //dの寿命はこのループが終わるまで //cの寿命はこのメソッドを抜けるまで }
しかし自動変数の寿命は、ブロック内と言っているのにも関わらず戻り値として自動変数を返しそれを利用する事ができます。これってどういう事なんでしょう。
int intValue(){ int value = 10; return value; } printf("value %d", intValue());
まず、本当に自動変数の寿命が関数内で閉じているのか確認みます。関数内で定義された自動変数のポインタを返す関数を定義してみました。これは、ワーニングは確認できますが、エラーになりません。しかし、実行してみると実行時エラーになります。これは、一番最初に言った通り自動変数の寿命はブロック内なため、関数が終了した段階でvalueの存在が保証されないため、不正なアドレスにアクセスしてしまいエラーになっています。ここから、戻り値としては自動変数ではない何かが返ってきているらしい事がわかりました。
int* intPointer(){ int value = 10; printf("tes address: %p", &value); return &value; } printf("return pointer address: %p", intPointer());
では、何が返ってきているのでしょうか。試しに、intValue関数内の変数と戻り値として帰ってくる変数とが同じ実体なのか確認してみるために、アドレスの確認を行ってみます。しかしこのコードは実行できません戻り値のアドレスを表示しようとしている部分がエラーになります。エラー内容はコンパイラによって異なりますが、Xcodeでは、"Cannot take the addresss of an rvalue of type 'int'"と表示されます。日本語に訳すと、int型のrvalueのアドレスは取得できませんといった感じです。rvalueというのは、一時作られるオブジェクトの事で参照されなくなった時点で破棄されます。rvalueのアドレスは取得できません。元の話に戻ると、上記から関数の戻り値で返ってくる値は、関数内で使われている自動変数ではない異なるオブジェクトだということがわかります。
int intValue(){ int value = 10; printf("tes address: %p", &value); return value; } printf("value Pointer %p", &(intValue()));
この一時的なオブジェクトは、意識することなく利用されていると思います。僕が感動したのが後置インクリメントです。後置インクリメントの戻り値は、括弧で囲って優先度を高めようが高めまいがインクリメント前の値が返ってきていますよね。これ実は、後置インクリメントの戻り値にはインクリメント前の値をコピーした一時的なオブジェクトが返ってきているのです。後置インクリメントは、前置インクリメントに比べて速度が遅いと言われる理由もここにあります。c++プログラマは知っていて当然の知識なのかもしれませんがコレ本当に目から鱗がおちました...
int i = 0; int result = 10 + (i++); printf("%d", result);
cocos2d-x 3.0でのiOSアプリに対するGameCenterの実装その2
前回の記事では、cocos2d-xでiOSアプリにGameCenterを実装する前準備まで説明してきました。今回は、GameCenterを利用するための、Xcode上での実際のコーディングについて説明していきます。
GameCenter機能は、iOSネイティブで提供されている機能のため勿論Objective-Cで記載されています。そのため、cocos2d-xのc++のコードからobjective-cのコードを呼び出さなくてはなりません。ただし、勿論c++のコードからobjective-cの記法で書かれたヘッダファイルの読み込みはできません。そこで、c++とobjective-cの橋渡しをしてくれるのが、objective-c++です。objective-c++は、そういった新しい言語ではなく、objective-cとc++のコードを混在させたコードの事をさします。c++の記法で書かれたヘッダで定義されたメソッド内で、objective-cのコードを書く事でc++とobjective-cの混在を可能にしています。
用意すべきコードは2つあり、1つはGameCenterの機能を呼び出すObjective-cで書かれたコード、もう1つがそのObjective-cで書かれたコードを呼び出すObjective-c++のコードです。Objective-cで書かれたコードは、GameCenterObjC、Objective-c++のコードはGameCenterBrideというファイル名で用意しました。
GameCenterObjC.m
Objective-Cでアプリを作っていたかたなら何も問題ない内容だと思いますが、GameCenterObjCでは、ネイティブのAPIを呼び出しGameCenterへのログインなどをするメソッドの実装を行っています。下記のコードは、GameCenterへのログインをするためのメソッドになります。ログインメソッドだけではなく、必要に応じてリーダーボードの表示や、リーダーボードへの得点の投稿メソッドなどの実装を行います。コードの全文はgithubで確認してみてください。
@implementation GameCenterObjC +(void) loginGameCenter{ if(floor(NSFoundationVersionNumber) >= NSFoundationVersionNumber_iOS_6_0){ GKLocalPlayer* player = [GKLocalPlayer localPlayer]; UIViewController* rootController = [UIApplication sharedApplication].keyWindow.rootViewController; player.authenticateHandler = ^(UIViewController* ui, NSError* error ) { if( nil != ui ) { NSLog(@"Need to login"); [rootController presentViewController:ui animated:YES completion:nil]; } else if( player.isAuthenticated ) { NSLog(@"Authenticated"); } else { NSLog(@"Failed"); } }; } else{ //6未満でのgamecenter認証コード GKLocalPlayer* localPlayer = [GKLocalPlayer localPlayer]; [localPlayer authenticateWithCompletionHandler:^(NSError *error) { if(localPlayer.authenticated){ NSLog(@"Authenticated"); } else{ NSLog(@"Not authenticated"); } }]; } }
GameCenterBridge.h
Objeciteve-cとc++の記法を含んだObjective-c++のヘッダファイルです。c++で書かれたcocos2d-xのコードからinculudeできるようにObjective-cの文法は含まれていません。今回は、ログイン、リーダーボードの表示、スコアの送信の3つの機能を提供するためGameCenterBridgeでも同様の3つの機能を定義します。
class GameCenterBridge{ public: static void loginGameCenter(); static void openRanking(); static void postHighScore(int kind, int score); };
GameCenterBridge.mm
Objeciteve-cとc++の記法を含んだObjective-c++の実装ファイルです。先ほど作成したGameCenterObjCをインクルードし、対応するメソッドでGameCenterObjCのメソッドを呼び出しています。
#include "GameCenterBridge.h" #include "GameCenterObjC.h" void GameCenterBridge::loginGameCenter(){ [GameCenterObjC loginGameCenter]; } void GameCenterBridge::postHighScore(int kind, int score){ [GameCenterObjC postHighScore:[NSNumber numberWithInt:kind] score:[NSNumber numberWithInt:score]]; } void GameCenterBridge::openRanking(){ [GameCenterObjC openRanking]; }
cocos2d-xのコードからは、アプリケーション起動時にログイン処理メソッドを呼び出したり、ボタンの押下時にリーダーボードの表示メソッドを呼び出したりすればOKです。
https://github.com/ttdog/GameCenterBridge
cocos2d-x 3.0でのiOSアプリに対するGameCenterの実装
cocos2d-x 3.0を使い作っているゲームにランキングボードの実装を行いたかったので、iOSアプリ側ではGameCenterを使ってランキングボードの実装を行います。iOSのGameCenterとはオンラインでのマッチング、取得スコアの共有、ランキングなどオンラインゲームに必要なソーシャル機能を提供するiOS4から実装されたサービス群です。現在作っているゲームでは、ランキング機能の実装を行いたかったためiOSではGameCenterを利用します。GameCenterを使えば、自前でサーバサイドを作る手間もなくなるしね!
Androidでは2013年から、 Google Play game servicesという名前で同様のサービスが提供されています。今回作成しているゲームは、両方のプラットフォームで提供する予定のためAndroidでは、Google Play game servicesを使ってランキング機能の実装を行う予定ですが、まずiOSのGameCenterの実装方法について説明していきます。
実装の前準備
GameCenterを使いたい場合、コードを書く前にちょっとした前準備が必要になります。まず、iTunesConnectで、アプリケーションの登録が必要です。
とりあえずGameCenterの実装の確認をしたいだけなのでバイナリのアップロード等はする必要はありませんが、アイコンなどの準備は必要です。とりあえずダミーのアイコンを容易してアプリケーションの登録を行いました。
アプリケーションの登録が完了したら、そのアプリケーションの詳細からGameCenterの設定を行います。
デフォルトでGameCenterはオフになっているのでオンにしましょう。あと今回は、ランキング機能を使いたいのでLeaderBoardを追加します。LeaderBoardはランキング機能で、他にもAchievementsといったゲームの達成状況に合わせて称号などを設定できる機能などもありますが今回は利用しません。
LeaderBoardの追加には以下を設定する必要があります。
- Leaderboard Reference Name
ランキングを識別する内部的な名前。好きな名前をつけましょう。
- Leaderboard ID
ランキングを識別できる一意なIDです。これを使ってコードからランキングボードの参照などを行います。
- Score Format Type
スコアのタイプ、int型なのかdecimalなのか、状況に合わせて設定してください。
- Score Submission Type
スコアの種類。ランキング上位を表示するのか、多くの人が取得しているスコアを表示したいのかを選択します。
- Sort Order
スコアの並び順
あとは、バージョンごとにGameCenterの設定を行っていきます。
バージョンの詳細画面から、GameCenterをオンにして先ほど作成したリーダーボードを追加します。
me
これでGameCenterを利用するためのitunesConnect上での設定は一通り終わりです。
次回は、Xcodeでの実装方法を説明していきますー。
あ!ここまでだとcocos2d-xは何も関係なかった。。
LayerとNodeのanchorPoint
cocos2d-xでカスタムクラスを作るにあたってLayerを継承すべきなのかNodeを継承すべきなのか考えてみました。
継承関係は、Nodeは画面へのすべての描画を司るクラスのルートクラスで、Nodeを継承したクラスのうちの1つがLayerクラスです。
Layerのreferenceには、以下のように書かれています。
LayerはNodeのサブクラスで、ユーザからのタップを取得するためのTouchEventsDelegateプロトコルを実装している。
以下の2つの機能が追加で実装されているのがNodeとの違いです。
・iPhoneのTouchの取得ができる
・加速度センサの値の取得ができる
cocos2d-x: Layer Class Reference
これを見ると、カスタムクラス自身でタッチや加速度の取得を行わない限りNodeを継承したクラスを作るのが良さそうですね。
しかし上にあげた違い以外にも、違いがあるようで何も考えずにLayerを継承したカスタムクラスを作ったところ痛い目にあいました。
というのも、LayerとLayerを継承したクラス(あとScene)ではデフォルトで、anchorPointに何を設定してもそれが無視されてしまうのです。
そのため、自分の意図した場所への配置にならずなんでだ!?と焦りました。
この原因は、Nodeで定義されている_ignoreAnchorPointForPositionプロパティにあります。
このプロパティに、trueが設定されていると設定されているanchorPointを無視して常にPoint(0,0)を基準にして配置が行われます。
そして、SceneとLayerはこのプロパティにデフォルトでtrueが設定されているため、それらのクラスを継承してカスタムクラスを作った時に自分の意図した配置が行われないということがおこってしまっていました。
勿論、Layerを継承したクラスでもこのプロパティにfalseを設定してあげればほかのクラスのようにanchorPointの設定反映することができます。
しかし、このクラスの実装を鑑みると1Sceneに1Layerが基本なのかなと思いますので、カスタムクラスを作りたい場合には極力Nodeを継承したクラスを作ろうと思います。
cocos2d-xのアクションを仕組み
Easingよく利用しますよね?
ただし、Easingを含んだ連続したアニメーションをしたい場合って
アクションとアクションのつなぎ目がスムーズにいかないですよね?
僕はそんな状況に直面しました。
具体的には以下のようなコードで、ユーザ画像をY軸に10移動させた後、背景画像を-10移動させようとしています。
Sequence* sequence = Sequence::create(MoveBy::create(1, Point(0, 10)), CallFunc::create([&](){ bg->runAction(EaseOut::create(MoveBy::create(1, Point(0, -10)), 3)); }), ,NULL); user->runAction(sequence);
背景が動く事で、ユーザ画像はY軸に10しか動いていないのにY軸に20移動したように見えますよね。
しかしこれ動かしてみるとわかるのですが困る事が一点。
背景画像をEaseOutさせているため、ユーザの終速と背景画像の初速が異なってしまい、アクションの切り替えがスムージに見えません。
背景画像の初速を計算して、それをユーザ画像の動作の終速に設定しようかとも考えたんですが、背景画像の初速の初速がわからない!
というのも、EaseOut::createの第2引数がEasingの変化の割合を示しているのはわかるのですが、
その値がどのように実際のEasingに変化を与えるのかがわからなかったのです。
しょうがないので、ソースを読んで少しだけ理解してみました。
大体確認したソースは以下。
ActionInterval
ActionManager
EaseOut
特定のNodeにアクションを実行させるメソッドにrunActionがあります。
このrunAction実際に呼び出されると、第一引数で渡されたActionインスタンスは、ActionManagerのインスタンスにrunActionを実行したインスタンスとともに追加、保持されます。
ここで登場したActionManagerはすべてのアクションを管理する、シングルトンクラスです。
ActionManagerクラスにはupdateメソッドが実装されており、毎フレームupdateメソッドが呼び出されます。
updateメソッド内では、ActionManagerに追加されたすべてのActionを走査し、Actionが実行停止状態でなければ、Actionのstep関数をフレーム間の秒数を引数にして呼び出します。
ActionIntervalで定義されたstep関数は、これまでのアニメーション時間をアニメーション全体の時間で割ったアニメーションの進行割合を引数にしてupdateメソッドを呼び出します。
void ActionInterval::step(float dt) { if (_firstTick) { _firstTick = false; _elapsed = 0; } else { _elapsed += dt; } this->update(MAX (0, // needed for rewind. elapsed could be negative MIN(1, _elapsed / MAX(_duration, FLT_EPSILON) // division by 0 ) ) ); }
updateメソッドは、MoveByやMoveToなどのアクションクラスでそれぞれオーバーライドされています。
実際の位置の変更などのアクションは、このupdateメソッドで行われています。
updateメソッド引数にはこれまでのアニメーションに要した時間をアニメーション全体時間で割った、アニメーションの進行割合が設定されています。
以下はMoveByのupdateメソッドです。進行割合とアニメーションpositionDeltaを掛け合わせた物を最初のポジションに足し合わせて、そのポジションをtargetとなるインスタンスに設定しています。
void MoveBy::update(float t) { if (_target) { #if CC_ENABLE_STACKABLE_ACTIONS Point currentPos = _target->getPosition(); Point diff = currentPos - _previousPosition; _startPosition = _startPosition + diff; Point newPos = _startPosition + (_positionDelta * t); _target->setPosition(newPos); _previousPosition = newPos; #else _target->setPosition(ccpAdd( _startPosition, ccpMult(_positionDelta, t))); #endif // CC_ENABLE_STACKABLE_ACTIONS } }
ここまでで、大体どういった仕組みでアニメーションされているのか理解できましたが。
では、本題であるEasingをする際にはどういった動きになっているのか。
まず、クラスの継承関係から見ていきます。
EaseOutクラスは、EaseRateActionを継承し、EaseRateActionはActionRaseを継承し、ActionRaseがActionIntervalを継承しています。
次に、上記のクラスでここまででアクションに関係すると考えられる、setpとupdateメソッドについてオーバーライドの状況を追っていきます。
ActionEase
updateのオーバーライドをしている。
ActionEase::update(float time)
インスタンスを作るときに渡された第一引数のアクションのupdateメソッドを呼び出す。
EaseRateAction
step、updateのオーバーライドはしていない。
EaseOut
updateメソッドのオーバーライドをしている。
上記のようにstepはオーバーライドされていないのでEaseOutのupdateだけ見れば良さそうです。
void EaseOut::update(float time) { _inner->update(tweenfunc::easeOut(time, _rate)); }
EaseOutに渡したアクションのupdateメソッドにtweenfunc::easeOut(time, _rate)を渡しています。
tweenfunc::easeOutは何をやっているか見てみるとtimeを1/rate乗したものを返しています。
float easeOut(float time, float rate) { return powf(time, 1 / rate); }
ここで帰ってきた値使ってMoveByなどのupdateメソッドを呼び出しているようですね!
cocos2d-x rc1でのプロジェクト作成
cocos2d-x rc0からcreate_project.pyがなくなり、全く異なる新しい方法でプロジェクトの作成をするようになりました。
新しい方法は、大きく環境変数の設定とプロジェクトの作成に分かれます。
環境変数の設定は1度きり、プロジェクトの作成は都度行う必要があります。
環境変数の設定 setup.py
cocos2d-xライブラリディレクトリの直下にsetup.pyスクリプトが新しく作られています。
これを使って環境変数の設定を行います。
とりあえず何も考えずにsetup.pyを実行してみましょう。
./setup.py
環境変数が何も設定されていなければ対話的に環境変数の設定をできます。
$ ./setup.py Setting up cocos2d-x... -> Adding COCOS2D_CONSOLE_ROOT environment variable... ALREADY ADDED -> Looking for NDK_ROOT envrironment variable... NOT FOUND Please enter its path (or press Enter to skip): -> Looking for ANDROID_SDK_ROOT envrironment variable... NOT FOUND Please enter its path (or press Enter to skip): -> Looking for ANT_ROOT envrironment variable... NOT FOUND Please enter its path (or press Enter to skip):
必要な環境変数は、NDK_ROOT、ANDROID_SDK_ROOTとANR_ROOTです。
対話的に設定しても良いんですが、.bash_profileなどに上記の変数を直接追記しても大丈夫です。
export NDK_ROOT=~/Developer/lib/android-ndk-r9d export ANDROID_SDK_ROOT=~/Developer/lib/android/platform-tools export ANT_ROOT=/usr/local/bin/
パスは個々人で違うのでそれぞれの設定に合わせて設定してください。
上記を設定した上でもう一度setup.pyを実行してみます。
$ ./setup.py Setting up cocos2d-x... -> Adding COCOS2D_CONSOLE_ROOT environment variable... ALREADY ADDED -> Looking for NDK_ROOT envrironment variable... FOUND -> Looking for ANDROID_SDK_ROOT envrironment variable... FOUND -> Looking for ANT_ROOT envrironment variable... FOUND COCOS_CONSOLE_ROOT was already added. Edit "~/.bash_profile" for manual changes Please execute command: "source ~/.bash_profile" to make added system variables take effect
環境変数が正しく設定されている事がわかります。
言われた通り、.bash_profileを再読み込みさせます。
source .bash_profile
これで環境変数の設定はOKです。
これ以降、パス等変更することがなければsetup.pyを呼び出す必要はないでしょう。
プロジェクトの作成 cocosコマンド
次にプロジェクトの作成です。
プロジェクトの作成は、cocosコマンドによって行います。
このcocosコマンドは、前述のsetup.pyコマンドによってパスが追加されます。
cocosコマンドプロジェクトの作成のほか、コンパイルや実行などを行う事ができます。
ここでは、プロジェクトの作成の仕方のみだけ説明します。
プロジェクト作成時に渡す引数は、create_project.pyの時と同じようなものです。
cocos new testProject -l cpp -d ~/project/ -p com.sample.testProject
cocos newの後にプロジェクトの名前、-lは使う言語、-dはディレクトリ、-pはパッケージの名前をそれぞれ指定します。
これで以前と同じようなプロジェクトができるはずです。