PlayFabのログインユーザーの識別IDとして、SystemInfo.deviceUniqueIdentifierを使用している状況で。
アプリのアンインストールなどせずに、ただAppStoreでアップデートしただけなのにIDが変化して新規ユーザー扱いになってしまう事象が2件発生した。
ユーザー端末は、いずれもiPhone8で iOS 11.3 と iOS 12.2 だった。
iOSのバグなのか不明だが非常に困る。

ひとまず対策として
初回のみ SystemInfo.deviceUniqueIdentifierを取得してPlayerPrefsに保存し、
2回目以降はPlayerPrefsに保存しておいた値を使うことにした。
アンインストールしない限りPlayerPrefsは残っているはずなのでたぶん大丈夫だと思う。

対策としてKeychainにIDを保存することにした。
Keychainに保存したデータは、たとえアンインストールしても残るのでPlayerPrefsより安心。
有償アセットiOS Keychain Pluginを使ったが、ネイティブ部分はunity-ios-keychain-pluginと同じだったので買わなくても良かったかも。
有償の方は、iOS以外の場合にpersistentDataPathに暗号化したファイルとして保存するようになっていた。

PlayFabのローカライズ

  • 投稿日:
  • by

言語ごとに表示内容を変える方法について。
ニュース、メール、プッシュ通知については管理画面で各言語ごとに文章を登録できるようになっている。
これらはプレイヤーのLanguage設定に従って自動で表示文章が選ばれる。
プレイヤーのLanguageが未設定だったり、一致する言語がない場合はデフォルト言語で表示される。
デフォルト言語は管理画面のSettings>Default languageで設定する。

クライアントからプレイヤーの言語を登録する方法は下記のような感じで、Profile.VersionNumberが必要になるので先に取得してから、SetProfileLanguage APIを呼ぶ。


//言語を登録する
    public static void SetLanguage(string language)
    {
        //先にプロフィールのバージョン番号を取得する
        GetProfile((version) => {
            //取得したバージョン番号を用いて言語登録
            SetProfileLanguage(language, version,(success)=> {
                if(success){
                    Debug.Log("登録完了");
                }else{
                    Debug.Log("登録失敗");
                }
            });
        });
    }
//プロフィールのバージョン番号を取得する
    private static void GetProfile(System.Action callback) {
        PlayFabProfilesAPI.GetProfile(new GetEntityProfileRequest{}, result => {
            callback.Invoke(result.Profile.VersionNumber);
        },error => {});
    }
//言語登録APIを実行する
    private static void SetProfileLanguage(string language,int profileVersion, System.Action callback)
    {
        var request = new SetProfileLanguageRequest
        {
            Language = language,
            ExpectedVersion = profileVersion
        };
        PlayFabProfilesAPI.SetProfileLanguage(request, res =>
        {
            callback.Invoke(true);
        }, error=> {
            callback.Invoke(false);
        });
    }

Unityで取得できるOSの言語(Application.systemLanguage)をPlayFabの言語コードに変換するには下記のようになると思う。 (PlayFabのドキュメントに言語コード一覧が見当たらなかったので、管理画面のプルダウンで使用されている言語コードを使用)
※Application.systemLanguageで取得できる言語数より、PlayFabで設定できる言語数のほうが多いが割愛
※ノルウェー語やセルビアクロアチア語は、PlayFabでは2種類に分かれるが独断で選んだ。


    public static string GetLanguageCode()
    {
        switch (Application.systemLanguage)
        {
            case SystemLanguage.Afrikaans: return "af";//アフリカ語
            case SystemLanguage.Arabic: return "ar";//アラビア語
            case SystemLanguage.Basque: return "eu";//バスク語
            case SystemLanguage.Belarusian: return "be";//ベラルーシ語
            case SystemLanguage.Bulgarian: return "bg";//ブルガリア語
            case SystemLanguage.Catalan: return "ca";//カタロニア語
            case SystemLanguage.Chinese: return "zh-Hans";//中国語
            case SystemLanguage.ChineseSimplified: return "zh-Hans";//中国語簡体字(simplified)
            case SystemLanguage.ChineseTraditional: return "zh-Hant";//中国語繁体字(traditional)
            case SystemLanguage.Czech: return "cs";//チェコ語
            case SystemLanguage.Danish: return "da";//デンマーク語
            case SystemLanguage.Dutch: return "nl";//オランダ語
            case SystemLanguage.English: return "en";//英語
            case SystemLanguage.Estonian: return "et";//エストニア語
            case SystemLanguage.Faroese: return "fo";//フェロー語
            case SystemLanguage.Finnish: return "fi";//フィンランド語
            case SystemLanguage.French: return "fr";//フランス語
            case SystemLanguage.German: return "de";//ドイツ語
            case SystemLanguage.Greek: return "el";//ギリシャ語
            case SystemLanguage.Hebrew: return "he";//ヘブライ語
            case SystemLanguage.Hungarian: return "hu";//ハンガリー語
            case SystemLanguage.Icelandic: return "is";//アイスランド語
            case SystemLanguage.Indonesian: return "id";//インドネシア語
            case SystemLanguage.Italian: return "it";//イタリア語
            case SystemLanguage.Japanese: return "ja";//日本語
            case SystemLanguage.Korean: return "ko";//韓国語
            case SystemLanguage.Latvian: return "lv";//ラトビア語
            case SystemLanguage.Lithuanian: return "lt";//リトアニア語
            case SystemLanguage.Norwegian: return "nb";//ノルウェー語(ブークモール)
            case SystemLanguage.Polish: return "pl";//ポーランド語
            case SystemLanguage.Portuguese: return "pt";//ポルトガル語
            case SystemLanguage.Romanian: return "ro";//ルーマニア語
            case SystemLanguage.Russian: return "ru";//ロシア語
            case SystemLanguage.SerboCroatian: return "sr-Latn";//セルビアクロアチア語 (Serbian (Latin))
            case SystemLanguage.Slovak: return "sk";//スロバキア語
            case SystemLanguage.Slovenian: return "sl";//スロベニア語
            case SystemLanguage.Spanish: return "es";//スペイン語
            case SystemLanguage.Swedish: return "sv";//スウェーデン語
            case SystemLanguage.Thai: return "th";//タイ語
            case SystemLanguage.Turkish: return "tr";//トルコ語
            case SystemLanguage.Ukrainian: return "uk";//ウクライナ語
            case SystemLanguage.Vietnamese: return "vi";//ベトナム語
            default: return "en";
        }
    }

インベントリアイテム名などはローカライズシステムが整っていないので、CustomDataにjson形式で文言を入れておいて自前で選ぶ。

[Unity ] 透過動画(WebM VP8)を作ったときのメモ

  • 投稿日:
  • by
  • カテゴリ:

Unityでサポートしているアルファチャンネル付き動画の形式はWebM(動画コーデックはVP8、音声コーデックはVorbis)。

FFmpegを使って、PNG連番画像をWebMにするなら下記で良い。

ffmpeg -r 30 -i frames\%04d.png -auto-alt-ref 0 -c:v libvpx output.webm

今回、音声コーデックがOpus形式のWebMファイルがあり、そのままではUnityで読み込めないためVorbis形式に変換する必要があった。
一発でいけるかもしれないけど、うまくできなかったので下記のような手順を踏んだ。
1)サウンドだけ抜き出す

ffmpeg -i original.webm -acodec libmp3lame -aq 4 sound.mp3

2)映像をいったん連番PNGに書き出す

ffmpeg -vcodec libvpx -i original.webm frames\%04d.png

3)連番PNGをWebMファイルに変換(その際フレームレートを60から30に変換、解像度を640x360に変更、ビットレートは20000kb/s)

ffmpeg -framerate 60 -i frames3\%04d.png -vf fps=30 -s 640x360 -auto-alt-ref 0 -c:v libvpx -pix_fmt yuva420p -metadata:s:v:0 alpha_mode="1" -b:v 20000k -r 30 noSound.webm

4)無音のWebMにサウンド(Vorbisコーデック)をつける

ffmpeg -i noSound.webm -i sound.mp3 -c:v copy -c:a libvorbis -map 0:v:0 -map 1:a:0 final.webm


なお、Androidで再生する場合は
Unityの映像ファイルのImport SettingsでTranscodeをオンにしてCodecをVP8にする必要がある。

映像をPlaneとかに貼り付けて透過表示するにはマテリアルのシェーダーをUnlit/Transparentにすれば良い。

PlayFabの不具合とか

  • 投稿日:
  • by
  • カテゴリ:

ゲームのバックエンドプラットフォーム「PlayFab」について。
一通り機能が揃っていて自前で用意する手間が省けるし、
インディープランもあるのでみんな使えばいいと思う一方でたまに変な不具合が起きて心配になる。
基本的にサービスは安定していると思うけど、これまでに主に管理画面側で遭遇した不具合を記しておく。

・Title News APIで、ある日を境にエラーが起きるようになった。
→Newsのローカライズ機能が告知なく実装され、既存のニュース記事があるとデータ取得できない状態に。機能リリース前にテストしてないのかな。

・他人のタイトルのニュースが混入して消せない現象。
→サポートチケット書いたら、インディープランは個別サポートしないのでフォーラムに投稿せよとのこと。1,2週間で直った。

・管理画面のメニューが押せない現象。
→日本語でのみ発生、1日くらいで直った。

・特定のLeaderboadのみ「StatisticAlreadyHasPrizeTable」エラーが出てPrize Tablesを作成できない。
→フォーラムに報告したらすぐに対応してもらえた。

ついでに欲しい機能:
・間違って作ったLeaderboardを消したいな。
・ユーザー毎のデータは充実してるけど、タイトル全体の統計情報(総プレイ回数とか流通コイン総額とか)を保存するデータベースがあればなぁ。
・有償コインと無償コインをまとめて扱う機能。現状はCloudScriptで実装。

アドフリくん Unity動画広告SDKのパーミッション

  • 投稿日:
  • by
  • カテゴリ:

アドフリくんのUnity動画広告SDK Ver.2.20.1から
下記のAndroidパーミッション要求が自動で追加されるようになった。

・SDカードのコンテンツの読み取り(SmaAdで使用
・SDカードのコンテンツを修正/削除する(SmaAdで使用)
・Wi-Fiからの接続と切断(Tapjoyで使用)

これらのパーミッションを取り除くためには、
/Assets/Plugins/Android/AndroidManifest.xmlに下記を追記する必要があるとの事。

・SDカードのコンテンツの読み取り
 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove"/>
・SDカードのコンテンツを修正/削除する
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove"/>
・Wi-Fiからの接続と切断
 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" tools:node="remove"/>
 <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" tools:node="remove"/>

PlayFabのセッション切れ

  • 投稿日:
  • by
  • カテゴリ:

PlayFab APIはログインから24時間経つとセッションが切れて、下記のエラーが起きる。

{
"code": 401,
"status": "Unauthorized",
"error": "NotAuthenticated",
"errorCode": 1074,
"errorMessage": "X-Authentication HTTP header contains invalid ticket"
}

ゲームを中断して、次の日に続きを遊ぶ場合などに起こり得るので、エラーチェックと再ログインを実装しておかないといけない。

Unity + PlayFabで課金処理

  • 投稿日:
  • by
  • カテゴリ:

App Store,Google Play Storeの課金処理はUnity IAPで行って、レシートチェックとアイテム付与をPlayFabAPIで行う。
以下、要所だけ載せます。


//////////////////////////
// Unityの課金システム構築
StandardPurchasingModule module = StandardPurchasingModule.Instance();
ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);
//商品ID登録(ストアに登録したIDとPlayFabのカタログアイテムIDを統一しておくと面倒がない)
builder.AddProduct("MyItemID", ProductType.Consumable);
// 非同期の初期化を開始
UnityPurchasing.Initialize(this, builder);


//////////////////////////
//Unity IAP 初期化完了時
//////////////////////////
void OnInitialized(IStoreController controller, IExtensionProvider extensions){
	//ストアの表示価格は下記のようにして取得
	Product p=controller.products.WithID("MyItemID");
	Debug.Log(p.metadata.localizedPriceString);
}


//////////////////////////
//購入ボタン押した時
//////////////////////////
controller.InitiatePurchase("MyItemID");


//////////////////////////
//購入完了時の呼ばれる関数
//////////////////////////
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e){
	//プロダクト不明のため無視
	if (e.purchasedProduct == null){
		return PurchaseProcessingResult.Complete;
	}
	//レシートがないので無視
	if (string.IsNullOrEmpty(e.purchasedProduct.receipt)){
		return PurchaseProcessingResult.Complete;
	}
	//レシート検証&アイテム付与。iOSとAndroidで分岐
	if (Application.platform == RuntimePlatform.Android){
		ValidateAndroidPurchase(e.purchasedProduct);
	} else {
		ValidateIosPurchase(e.purchasedProduct);
	}
	//PlayFabでレシート検証完了するまでペンディングにする
	return PurchaseProcessingResult.Pending;
}

//////////////////////////
//Androidのレシート検証&アイテム付与
//////////////////////////
private void ValidateAndroidPurchase(Product purchasedProduct) {
	var googleReceipt = GooglePurchase.FromJson(purchasedProduct.receipt);
	PlayFabClientAPI.ValidateGooglePlayPurchase(new ValidateGooglePlayPurchaseRequest()
	{
		CurrencyCode = purchasedProduct.metadata.isoCurrencyCode,
		PurchasePrice = (uint)(purchasedProduct.metadata.localizedPrice * 100),
		ReceiptJson = googleReceipt.PayloadData.json,
		Signature = googleReceipt.PayloadData.signature
	}, result =>
	{
		//購入処理が完了したものとする
		controller.ConfirmPendingPurchase(purchasedProduct);
		//TODO:インベントリ更新するなど
	},
	error =>
	{
		//使用済みレシートが残っていた場合は完了扱いにする
		if (error.ErrorMessage == "Receipt already used"){
			controller.ConfirmPendingPurchase(purchasedProduct);
		}
	}
	);
}

//////////////////////////
//iOSのレシート検証&アイテム付与
private void ValidateIosPurchase(Product purchasedProduct){
	Dictionary receipt=PlayFabSimpleJson.DeserializeObject>(purchasedProduct.receipt);
	var request = new ValidateIOSReceiptRequest
	{
		CurrencyCode = purchasedProduct.metadata.isoCurrencyCode,
		PurchasePrice = (int)(purchasedProduct.metadata.localizedPrice * 100),
		ReceiptData = (string)(receipt["Payload"])
	};
	PlayFabClientAPI.ValidateIOSReceipt(request, result =>
	{
		//購入処理が完了したものとする
		controller.ConfirmPendingPurchase(purchasedProduct);
		//TODO:インベントリ更新するなど
	},
	error =>
	{
		//使用済みレシートが残っていた場合は完了扱いにする
		if (error.ErrorMessage == "Receipt already used"){
			controller.ConfirmPendingPurchase(purchasedProduct);
		}
	}
	);
}

iOS IAP Sandboxアカウントの切り替え

  • 投稿日:
  • by
  • カテゴリ:

「設定> iTunesとApp Store」でログアウトしてるのにSandbox用のアカウントを選べない状況。
購入ボタンを押すと"Confirm Your In-App Purchase"ダイアログが表示されるが アカウント入力欄が無く、パスワードしか尋ねられない。
どうやるのか探し回った結果、
「iTunesとApp Store」をスクロールした下の方にSANDBOXアカウントの項目があった
昔からありました?

PlayFabの使い方 ログを採る

  • 投稿日:
  • by
  • カテゴリ:

ユーザーの操作ログやエラーログをサーバーに送るには下記のような感じで実行するとPlayStreamに記録される。


PlayFabClientAPI.WritePlayerEvent(new WriteClientPlayerEventRequest()
	{
		Body = new Dictionary() {{ "foo", "bar"}},
		EventName = "my_custom_event"
	},
	result => Debug.Log("Logged"),
	error => Debug.LogError(error.GenerateErrorReport())
);


ログイン完了していないとAPI実行できないのでログインエラーのタイミングでは送れない。

PlayFabの使い方 ユーザーデータ周り

  • 投稿日:
  • by
  • カテゴリ:

ゲームアプリのバックエンドとしてPlayFabを触り始めました。一通り機能が揃っていてすごく便利な感じです。
英語ドキュメントやフォーラムを検索すれば大抵の事は解決しますが、日本語情報が少ないのでここにメモしておきます。

まずユーザーに紐づく情報をどう取得または保存するかについて、下記のように使い分けるのが良さそうです。

・プレイヤー表示名...
  PlayerProfileのDisplayNameで取得。UpdateUserTitleDisplayNameで更新。

・プレイヤーの経験値やハイスコアなどの数値...
  Statisticsデータとしてサーバー側のCloudScriptを呼び出して保存する。
  不正対策としてクライアントからは直接書き込めないようにデフォルト設定されている。
  Statisticsとして保存しているデータはLeaderboard(ランキング機能)として扱える。

・ゲーム内通貨の所持数...
  GetPlayerCombinedInfoのUserVirtualCurrencyで取得。
  不正対策としてクライアントからは通貨の加算減算ができないようにデフォルト設定されている。

・武器やアイテムやスキルの所持フラグ...
  マスター情報はCatalogで管理し、ユーザーの所持データはGetUserInventoryで取得。
  販売アイテムじゃない物はprice 0でカタログ登録しておき、サーバー側のGrantItemsToUserで付与するか、クライアント側のPurchaseItemで所持する。

・フレンド情報...
  GetFriendsListAddFriendを使う。
  フレンドというより一方的なフォロー登録なので、相互フォローしたいなら自前で処理を書く。

・そのほか諸々のユーザ情報...
  UserDataとして取得や保存する。
  クライアントから書き換えられたくないデータは、サーバー側でUpdateUserReadOnlyDataを使う。


ログイン時にまとめてデータを取得するには、下記のようにGetPlayerCombinedInfoRequestParamsを指定する。


GetPlayerCombinedInfoRequestParams InfoRequestParams = new GetPlayerCombinedInfoRequestParams()
{
	GetPlayerProfile = true,
	ProfileConstraints = new PlayerProfileViewConstraints()
	{
		ShowDisplayName = true
	},
	GetPlayerStatistics =true,
	GetUserInventory = true,
	GetUserData = true,
	GetUserReadOnlyData = true,
	GetUserVirtualCurrency = true
};

PlayFabClientAPI.LoginWithCustomID(
	new LoginWithCustomIDRequest()
	{
		TitleId = PlayFabSettings.TitleId,
		CustomId = SystemInfo.deviceUniqueIdentifier,
		CreateAccount = true,
		InfoRequestParameters = InfoRequestParams
	}, 
	(result) => {}, 
	(error) => {}
);