今回はCocos2d-xでゲーム内のランキング機能を実装したので、メモがてらここに貼っつけていきます
LobiなんかもCocos2d-xに対応していたりするので良いと思いますが、今回のアプリはカジュアルゲームだったので、ユーザー登録が必要ないGame CenterとPlay Games Servicesにしました
また、ここでは実装のみ紹介するので、コンソール側の登録などは以下などを参考にしてください
Game Center機能を実装する – Qiita
Google Play Game Servicesをとりあえず使ってみる(登録編) – Qiita
ちなみにログイン完了時のコールバック処理や認証失敗時のエラーダイアログ表示などは行っていないので、それが気になる方は各自で実装をお願いします
各OS共通
RankingController.h
#ifndef RankingController_h
#define RankingController_h
class RankingController {
public:
static RankingController* getInstance();
static void destroyInstance();
virtual bool isLogin();
virtual void start();
virtual void showRanking();
virtual void updateScore(int score);
protected:
static RankingController* _singletonInstancePointer;
};
#endif /* RankingController_h */
全OS共通のヘッダです
シングルトンパターンを使っているのでどこからでも呼べるようにしています
各メソッドがvirtualなのはiOSでオーバーライドを行うからです
RankingController.cpp
#include "RankingController.h"
#include "cocos2d.h"
RankingController* RankingController::_singletonInstancePointer = nullptr;
#if CC_TARGET_PLATFORM != CC_PLATFORM_IOS
RankingController* RankingController::getInstance()
{
if (_singletonInstancePointer == nullptr)
{
_singletonInstancePointer = new (std::nothrow) RankingController();
}
return _singletonInstancePointer;
}
#endif //CC_TARGET_PLATFORM != CC_PLATFORM_IOS
void RankingController::destroyInstance()
{
delete _singletonInstancePointer;
_singletonInstancePointer = nullptr;
}
#if CC_TARGET_PLATFORM != CC_PLATFORM_ANDROID
bool RankingController::isLogin() {
return false;
}
void RankingController::start() {
}
void RankingController::showRanking() {
}
void RankingController::updateScore(int score) {
}
#endif //CC_TARGET_PLATFORM != CC_PLATFORM_ANDROID
全OS共通の実装です
getInstanceはiOSのみRankingControllerを継承したインスタンスを生成するためここでは実装しません
また、Android以外のOSでもビルドが通るように何もしないメソッドを用意しています(iOSはオーバーライドで実装)
AndroidのisLogin等の実装はRankingController_Android.cppで行います
iOS
RankingController_iOS.h
#ifndef RankingController_iOS_h
#define RankingController_iOS_h
#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
#include "RankingController.h"
@interface GameCenterController : NSObject<GKGameCenterControllerDelegate> {
}
- (void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCenterViewController;
- (BOOL)isLogin;
- (void)start;
- (void)showRanking;
- (void)updateScore:(NSInteger)score;
@end
class RankingController_iOS : public RankingController {
public:
RankingController_iOS();
virtual ~RankingController_iOS() = default;
virtual bool isLogin() override;
virtual void start() override;
virtual void showRanking() override;
virtual void updateScore(int score) override;
GameCenterController* _gameCenterController;
};
#endif /* RankingController_iOS_h */
iOS用のヘッダです
RankingControllerの継承クラスと、Objective-CのGameCenterControllerクラスを宣言しています
GameKitを使用するので、Build PhasesのLink Binary With LibrariesからGameKit.frameworkを追加してください
GameCenterControllerクラスを用意する理由はGKGameCenterControllerDelegateを使用するためです
RankingController_iOS.mm
#include "cocos2d.h"
#if CC_TARGET_PLATFORM == CC_PLATFORM_IOS
#include "RankingController_iOS.h"
#import "AppController.h"
@implementation GameCenterController
static NSString* const LEADER_BOARD_ID = @"XXXXXXXXXXXXXXXXXXXXXX";
- (BOOL)isLogin
{
GKLocalPlayer* player = [GKLocalPlayer localPlayer];
return player.isAuthenticated;
}
- (void)start
{
GKLocalPlayer* player = [GKLocalPlayer localPlayer];
player.authenticateHandler = ^(UIViewController* ui, NSError* error )
{
if( nil != ui )
{
AppController* appController = [UIApplication sharedApplication].delegate;
[appController.viewController presentViewController:ui animated:YES completion:nil];
}
};
}
- (void)updateScore:(NSInteger)score
{
if (![self isLogin]) {
return;
}
GKScore* gkScore = [[GKScore alloc] initWithLeaderboardIdentifier:LEADER_BOARD_ID];
gkScore.value = score;
[GKScore reportScores:@[gkScore] withCompletionHandler:^(NSError *error) {
if (error) {
}
}];
}
- (void)showRanking
{
if (![self isLogin]) {
return;
}
GKGameCenterViewController *gcView = [GKGameCenterViewController new];
gcView.gameCenterDelegate = self;
gcView.viewState = GKGameCenterViewControllerStateLeaderboards;
gcView.leaderboardTimeScope = GKLeaderboardTimeScopeAllTime;
gcView.leaderboardIdentifier = LEADER_BOARD_ID;
AppController* appController = [UIApplication sharedApplication].delegate;
[appController.viewController presentViewController:gcView animated:YES completion:nil];
}
- (void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCenterViewController
{
[gameCenterViewController dismissViewControllerAnimated:YES completion:nil];
}
@end
RankingController* RankingController::getInstance()
{
if (_singletonInstancePointer == nullptr)
{
_singletonInstancePointer = new (std::nothrow) RankingController_iOS();
}
return _singletonInstancePointer;
}
RankingController_iOS::RankingController_iOS() {
_gameCenterController = [[GameCenterController alloc] init];
}
bool RankingController_iOS::isLogin() {
return [_gameCenterController isLogin];
}
void RankingController_iOS::start() {
[_gameCenterController start];
};
void RankingController_iOS::showRanking() {
[_gameCenterController showRanking];
};
void RankingController_iOS::updateScore(int score) {
[_gameCenterController updateScore:score];
};
#endif //CC_TARGET_PLATFORM == CC_PLATFORM_IOS
iOS用のObjective-C++の実装部分です
GameCenterControllerでは実際にGameKitを使ってログインやスコアの更新、ランキングの表示を行っています
LEADER_BOARD_IDにはiTunes Connectで設定したリーダーボードのIDを入れてください
スコア送信失敗時や未ログイン時のランキング表示失敗などのエラー表示は特に何もしていないので、必要な場合は何かしらの処理を入れてください
また、ランキングの集計期間を1日や1週間にしたい場合は、GKGameCenterViewControllerのleaderboardTimeScopeをGKLeaderboardTimeScopeTodayやGKLeaderboardTimeScopeWeekに変更してください
RankingController_iOSではGameCenterControllerの生成と呼び出しを行っています
ここでRankingControllerのgetInstanceの実装を行うことで、iOSでのみgetInstance時にRankingController_iOSを生成するようにしています
Android
RankingController_Android.cpp
#include "RankingController.h"
#include "cocos2d.h"
#if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID
#include <jni.h>
#include "platform/android/jni/JniHelper.h"
#define CLASS_NAME "org/cocos2dx/cpp/PlayGamesController"
USING_NS_CC;
jobject getObject() {
JniMethodInfo t;
bool isHave = JniHelper::getStaticMethodInfo(t,
CLASS_NAME,
"getObject",
"()Ljava/lang/Object;");
jobject instance;
if (isHave)
{
instance = t.env->CallStaticObjectMethod(t.classID, t.methodID);
}
return instance;
}
bool RankingController::isLogin() {
JniMethodInfo t;
jobject instance = getObject();
bool isHave = JniHelper::getMethodInfo(t, CLASS_NAME, "isLogin", "()Z");
if (!isHave) {
return false;
}
jboolean jIsLogin = t.env->CallBooleanMethod(instance, t.methodID);
t.env->DeleteLocalRef(t.classID);
return (bool)jIsLogin;
}
void RankingController::start() {
JniMethodInfo t;
jobject instance = getObject();
bool isHave = JniHelper::getMethodInfo(t, CLASS_NAME, "start", "()V");
if (!isHave) {
return;
}
t.env->CallVoidMethod(instance, t.methodID);
t.env->DeleteLocalRef(t.classID);
}
void RankingController::showRanking() {
JniMethodInfo t;
jobject instance = getObject();
bool isHave = JniHelper::getMethodInfo(t, CLASS_NAME, "showRanking", "()V");
if (!isHave) {
return;
}
t.env->CallVoidMethod(instance, t.methodID);
t.env->DeleteLocalRef(t.classID);
}
void RankingController::updateScore(int score) {
JniMethodInfo t;
jobject instance = getObject();
bool isHave = JniHelper::getMethodInfo(t, CLASS_NAME, "updateScore", "(I)V");
if (!isHave) {
return;
}
jint jScore = (jint)score;
t.env->CallVoidMethod(instance, t.methodID, jScore);
t.env->DeleteLocalRef(t.classID);
}
#endif //CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID
RankingControllerのAndroidでの実装部分です
基本的にはPlayGamesController.javaのメソッドをJNI経由で呼んでいるだけです
PlayGamesControllerはシングルトンなので、staticのメソッドを呼ぶのではなく、PlayGamesControllerのインスタンスを取得してからそのインスタンスのメソッドを呼んでいます
PlayGamesController.java
package org.cocos2dx.cpp;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.games.Games;
import com.google.android.gms.games.GamesActivityResultCodes;
import org.cocos2dx.lib.Cocos2dxActivity;
public class PlayGamesController implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private GoogleApiClient mGoogleApiClient;
private static final int RESOLUTION_REQUEST_CODE = 1000;
private static final int LEADER_BOARD_REQUEST_CODE = 1001;
private static PlayGamesController instance = new PlayGamesController();
public static PlayGamesController getInstance() {
return instance;
}
public static Object getObject() {
return instance;
}
private PlayGamesController() {
Context con = Cocos2dxActivity.getContext();
mGoogleApiClient = new GoogleApiClient.Builder(con)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(Games.API).addScope(Games.SCOPE_GAMES)
.build();
}
public boolean isLogin() {
return mGoogleApiClient.isConnected();
}
public void start() {
if (isLogin() || mGoogleApiClient.isConnecting()) {
return;
}
mGoogleApiClient.connect();
}
public void showRanking() {
if (!isLogin()) {
return;
}
Cocos2dxActivity activity = (Cocos2dxActivity)Cocos2dxActivity.getContext();
activity.startActivityForResult(Games.Leaderboards.getLeaderboardIntent(mGoogleApiClient,
activity.getString(R.string.leaderboard_)), LEADER_BOARD_REQUEST_CODE);
}
public void updateScore(int score) {
if (!isLogin()) {
return;
}
Context con = Cocos2dxActivity.getContext();
Games.Leaderboards.submitScore(mGoogleApiClient, con.getString(R.string.leaderboard_), score);
};
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == RESOLUTION_REQUEST_CODE) {
if (resultCode != Activity.RESULT_OK) {
return;
}
if (!mGoogleApiClient.isConnected()) {
mGoogleApiClient.connect();
}
} else if (requestCode == LEADER_BOARD_REQUEST_CODE && resultCode == GamesActivityResultCodes.RESULT_RECONNECT_REQUIRED) {
mGoogleApiClient.disconnect();
}
}
@Override
public void onConnected(@Nullable Bundle bundle) {
}
@Override
public void onConnectionSuspended(int i) {
//自動で再接続するらしいのでここいらないかも
mGoogleApiClient.connect();
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
if (connectionResult.getErrorCode() == ConnectionResult.SIGN_IN_REQUIRED
&& connectionResult.hasResolution()) {
try {
Cocos2dxActivity activity = (Cocos2dxActivity)Cocos2dxActivity.getContext();
connectionResult.startResolutionForResult(activity, RESOLUTION_REQUEST_CODE);
} catch (IntentSender.SendIntentException e) {
mGoogleApiClient.connect();
}
}
}
}
AndroidのJava側の実装です
この実装を行う前に、PlayGamesServicesAPIを使うためにapp/build.gradleに以下を追加してください
dependencies {
compile 'com.google.android.gms:play-services-games:9.4.0'
}
後ろのバージョンはその時のGooglePlayAPIのバージョンに合わせてください
また、GooglePlayDeveloperConsoleで実績を5つ以上、リーダーボードを1つ以上設定した上でリソースを取得し、res/valuesに保存してください
その上で、AndroidManifest.xmlに以下を追加してください
<meta-data android:name="com.google.android.gms.games.APP_ID"
android:value="@string/app_id" />
この状態で上記PlayGamesControllerを実装して、R.string.leaderboard_の部分を設定したリーダーボードの名前に書き換えてください
ちなみにこのクラスはActivityではないのでonActivityResultが呼ばれないため、AppActivityに以下を追加してください
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
PlayGamesController.getInstance().onActivityResult(requestCode, resultCode, data);
}
これらを実装した上で、C++部分で以下のような実装を追加してください
RankingController::getInstance()->start();
button1->addClickEventListener([](cocos2d::Ref* ref) {
RankingController::getInstance()->updateScore(100);
});
button2->addClickEventListener([](cocos2d::Ref* ref) {
RankingController::getInstance()->showRanking();
});
このようにすれば、iOSであればGameCenter、AndroidであればGooglePlayGamesServicesを使ってランキングの更新、ランキングの表示が行えると思います