[Unity] Android App Bundle(AAB)どこで設定するの?

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

APKではなくAABにしたいのだけど、Unity 2018.4のBuild Settingsを見ても
あるはずのBuild App Bundle (Google Play) の項目が見当たらない。

検索してもわからず時間を取られてしまったけど結論としては、
一つ上の項目であるBuild Systemを「Gradle」にすると出てきた!
Internalだと表示されない。

[Unity] Android App Bundle(AAB)どこで設定するの?

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

APKではなくAABにしたいのだけど、Unity 2018.4のBuild Settingsを見ても
あるはずのBuild App Bundle (Google Play) の項目が見当たらない。

検索してもわからず時間を取られてしまったけど結論としては、
一つ上の項目であるBuild Systemを「Gradle」にすると出てきた!
Internalだと表示されない。

Visual Effect Graphのカスタムアトリビュート

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

Unity VFX Graphのサンプルを見ていると
検索しても出てこないアトリビュートブロック("Base Position"とか"Offset 1" とか)を使っている事があり
いったいどうやって使うのか調べた結果、
下記のチェックボックスをONにすると使えるようになるという事だった。
Preferences -> Visual Effects > Experimental Operators/Blocks

ONにすると Set Custom AttributeブロックとGet Custom Attributeノードを作成できるようになる。
作成するとデフォルトはfloatだが、ブロックを選択してInspectorから型を変えたり、ランダムにしたり、Attribute名を変えたりできる。

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 -b:v 5M 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 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);
		}
	}
	);
}