[iOS]Unity5でGame CenterにAchievementを送れないバグに対処する


## 問題

Unity5(5.2.1)ではUnity付属のSocialPlatformsを使っても、Achievementを送れずエラーになるバグが存在するようです。

>参考：http://forum.unity3d.com/threads/problem-with-game-center-achievements.310817/

上記URLを見ると２０１５年３月ごろから症状が出ているようですが、現時点（２０１５年１０月下旬）では対処された様子がまだありません。

そこで[上記URLの解決方法](http://forum.unity3d.com/threads/problem-with-game-center-achievements.310817/#post-2081801)を参考にネイティブプラグインを作って解決することにしました。

## 対処方法

この記事の最後に掲げる４つのファイル(GCNative.h, GCNative.m, GCNative_Bridge.h, GCNative_Bridge.m)を、Assets/Plugins/iOSフォルダに突っ込んでビルドするだけです。

![スクリーンショット 2015-10-26 17.17.40.png](https://qiita-image-store.s3.amazonaws.com/0/20722/b11c8b30-05cd-5784-2587-b6865f137707.png "スクリーンショット 2015-10-26 17.17.40.png")


## 使い方

使えないのは、Social.ReportProgressだけなので、ネイティブプラグインで置き換えるのは、

```csharp:
Social.ReportProgress(achievementID, 100.0, success => {
	Debug.Log(success ? "アチーブメント報告は成功しました" : "アチーブメント報告は失敗しました");
});

```

のような部分だけになります。
上記のようなコードがあるとしたら、以下のコードで置き換えます。

```csharp:
_ReportAchievement(achievementID, 100.0f);
```

見ての通りエラーハンドリングは現在のバージョンでは出来ません。

また、上記のコードを使うファイルには、以下のコードも必要になります。詳しくは後掲するコードを参考にしてください。

```csharp:
using System.Runtime.InteropServices; // <- 必須

// ……

[DllImport("__Internal")]
private static extern void _ReportAchievement( string achievementID, float progress );
```

## 参考クラス

先に使い方の参考になるクラスを載せます。

実際に使っているコードから抜粋したものなので、各自で修正してお使いください。

```csharp:RankingUtility.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.SocialPlatforms;
using System.Runtime.InteropServices;

public class RankingUtility {
	[DllImport("__Internal")]
	private static extern void _ReportAchievement( string achievementID, float progress );
	
	public delegate void LoadAchievementsHandler(int handlerArgument);
	
	//このIDは任意の登録したものを使う
	const string leaderboardID = "score";
	
	static bool successAuth = false;
	
	//ユーザー認証
	public static void Auth() {
		// 認証のため ProcessAuthenticationをコールバックとして登録
		Social.localUser.Authenticate (ProcessAuthentication);        
	}
	
	// 認証が完了した時に呼び出される
	// 認証が成功した場合、サーバーからのデータがSocial.localUserにセットされている
	private static void ProcessAuthentication (bool success) {
		successAuth = success;
		if (success) {
			Debug.Log ("Authenticated, checking achievements");
		} else {
			Debug.Log ("Failed to authenticate");
		}
	}
	
	// リーダーボードを表示する
	public static void ShowLeaderboardUI() {
		if( ! successAuth ) return;

		Social.ShowLeaderboardUI();
	}
	
	// リーダーボードにスコアを送信する
	public static void ReportScoreNormal (long score) {
		if( ! successAuth ) return;
		
		Debug.Log ("スコア " + score + " を次の Leaderboard に報告します。" + leaderboardID);
		
		Social.ReportScore (score, leaderboardID, success => {
			Debug.Log(success ? "スコア報告は成功しました" : "スコア報告は失敗しました");
		});
	}

	private static IAchievement[] currentAchivements = null;
	private static Dictionary<string, IAchievement> dic = null;

	// 現在のアチーブメントの状況を取得する。
	public static void LoadAchievements(LoadAchievementsHandler handler, int handlerArg = 0){
		Social.LoadAchievements(achievements => {
			dic = new Dictionary<string, IAchievement>();
			foreach(var achievement in achievements){
				var percent = achievement.percentCompleted;
				var completed = achievement.completed;
				var id = achievement.id;

				dic[id] = achievement;

				Debug.Log ("" + id + ": percent = " + percent + ", completed = " + completed);
			}

			currentAchivements = achievements;
			if(handler != null){
				handler(handlerArg);
			}
		});
	}

	// Achivementの進捗状況を送信する（メソッド名は気にしない）。
	//呼び出し例：RankingUtility.ReportAchievementScore("SomeAchievement");
	//
	public static void ReportAchievementScore (string achievementID) {
		Debug.Log ("アチーブメント" + achievementID + " を送信します。");

		/*　ここを置き換えた。
		Social.ReportProgress(achievementID, 100.0, success => {
			Debug.Log(success ? "アチーブメント報告は成功しました" : "アチーブメント報告は失敗しました");
		});
		*/

		_ReportAchievement(achievementID, 100.0f);
	}
}

```

## ネイティブプラグイン

以下の４つのファイルをAssets/Plugins/iOSフォルダに入れます。

```objectivec:GCNative_Bridge.h
#ifndef GCNative_Bridge_h
#define GCNative_Bridge_h

void _ReportAchievement( const char* achievementID, float progress );

#endif /* GCNative_Bridge_h */
```

```objectivec:GCNative_Bridge.m
#import <Foundation/Foundation.h>
#include "GCNative_Bridge.h"
#include "GCNative.h"

void _ReportAchievement( const char* achievementID, float progress ){
    NSString* str = [[NSString alloc] initWithUTF8String:achievementID];
    GCNative* native = [[GCNative alloc] init];
    
    [native submitAchievement:str percentComplete:progress];
}
```

```objectivec:GCNative.h
#ifndef GCNative_h
#define GCNative_h

#import <Foundation/Foundation.h>
#import <GameKit/GameKit.h>

@protocol GameCenterManagerDelegate <NSObject>

@optional
- (void) processGameCenterAuth: (NSError*) error;
- (void) scoreReported: (NSError*) error;
- (void) reloadScoresComplete: (GKLeaderboard*) leaderBoard error: (NSError*) error;
- (void) achievementSubmitted: (GKAchievement*) ach error:(NSError*) error;
- (void) achievementResetResult: (NSError*) error;
- (void) mappedPlayerIDToPlayer: (GKPlayer*) player error: (NSError*) error;

@end



@interface GCNative : NSObject

-(void)submitAchievement: (NSString*)identifier percentComplete: (double)percentComplete;

@property (retain) NSMutableDictionary* earnedAchievementCache;
@property (nonatomic, assign)  id <GameCenterManagerDelegate> delegate;

@end

#endif /* GCNative_h */
```

```objectivec:GCNative.m
#import "GCNative.h"

@implementation GCNative

@synthesize earnedAchievementCache = _earnedAchievementCache;
@synthesize delegate = _delegate;

-(id)init {
    self = [super init];
    
    if(self!= NULL) {
        _earnedAchievementCache= NULL;
    }
    
    return self;
}

-(void)dealloc {
    self.earnedAchievementCache= NULL;
    
    //[super dealloc];
}

- (void) callDelegate: (SEL) selector withArg: (id) arg error: (NSError*) err {
    assert([NSThread isMainThread]);
    
    if([_delegate respondsToSelector: selector]) {
        if(arg != NULL) {
            [_delegate performSelector: selector withObject: arg withObject: err];
        } else {
            [_delegate performSelector: selector withObject: err];
        }
    } else {
        NSLog(@"Missed Method");
    }
}

- (void) callDelegateOnMainThread: (SEL) selector withArg: (id) arg error: (NSError*) err {
    dispatch_async(dispatch_get_main_queue(), ^(void) {
        [self callDelegate: selector withArg: arg error: err];
    });
}

-(void)submitAchievement: (NSString*)identifier percentComplete: (double)percentComplete {
    if(self.earnedAchievementCache == NULL) {
        [GKAchievement loadAchievementsWithCompletionHandler: ^(NSArray *scores, NSError *error) {
            if(error == NULL) {
                NSMutableDictionary* tempCache= [NSMutableDictionary dictionaryWithCapacity: [scores count]];
                
                for (GKAchievement* score in scores) {
                    [tempCache setObject: score forKey: score.identifier];
                }
                self.earnedAchievementCache= tempCache;
                
                [self submitAchievement: identifier percentComplete: percentComplete];
            } else {
                //Something broke loading the achievement list.  Error out, and we'll try again the next time achievements submit.
                //[self callDelegateOnMainThread: @selector(achievementSubmitted:error:) withArg: NULL error: error];
            }
        }];
        
    } else {
        //Search the list for the ID we're using...
        GKAchievement* achievement= [self.earnedAchievementCache objectForKey: identifier];
        
        if(achievement != NULL) {
            if((achievement.percentComplete >= 100.0) || (achievement.percentComplete >= percentComplete)) {
                //Achievement has already been earned so we're done.
                achievement= NULL;
            }
            
            achievement.percentComplete= percentComplete;
        } else {
            achievement= [[GKAchievement alloc] initWithIdentifier: identifier];
            achievement.percentComplete= percentComplete;
            
            //Add achievement to achievement cache...
            [self.earnedAchievementCache setObject: achievement forKey: achievement.identifier];
        }
        
        if(achievement != NULL) {
            //Submit the Achievement...
            [achievement reportAchievementWithCompletionHandler: ^(NSError *error) {
                //[self callDelegateOnMainThread: @selector(achievementSubmitted:error:) withArg: achievement error: error];

            }];
        }
    }
}

@end
```



## 参考URL

http://forum.unity3d.com/threads/problem-with-game-center-achievements.310817/
[UnityでiOSのGameCenterを利用するためのサンプルコード](http://qiita.com/yimajo/items/503cb2451e303e9f3841)
[AppleのサンプルプロジェクトGKTapperのソース](https://developer.apple.com/library/ios/samplecode/GKTapper/Introduction/Intro.html#//apple_ref/doc/uid/DTS40010283-Intro-DontLinkElementID_2)