sunagimoブログ

主にUnityに関する技術を取り上げます

Firebase Remote Config【Unity】

Firebase Remote Configとは

予め設定したパラメータの値をRemoteで変更できるサービス。
ダウンロードが不要なので、ユーザーに素早く変更を通知できる。

firebase.google.com



使用例

・アプリのプラットフォームで提供する機能を分ける
・言語、国ごとに提供する機能を分ける
・指定したバージョンごとで機能を分ける
・指定した時間の間だけ機能を限定公開
・指定したパーセント値のユーザーのみに機能を限定公開
など

カスタマイズ可能
最大500の条件設定可能


使用できるパラメータの値

  • bool(true, false)
  • IEnumerable(json
  • double(0.05などの小数点)
  • long(整数)
  • source(この値のソースを示す、Default値やRemote値)
  • string(文字列)

パラメータ文字列は800000文字列以内
最大2000個のパラメーター


firebase.google.com



ポリシー

  • Remote Configを使ってユーザーの承認を必要とする機能を作成しない
  • 機密データをパラメータ値に格納しない
  • パラメータを使ってプラットフォームの要件を回避しようとしない


Remote Config設定の流れ(導入)

以下の導入方法に関してはスキップ

  • Firebaseプロジェクトの作成
  • Firebase Remote Config SDKの導入


プログラムは以下を参考
github.com



1.Firebase Remote Configコンソールでパラメータの設定

f:id:sunagimo_app:20190924002901p:plain


例:
プラットフォームごとに強制アップデートバージョンを変更したい

キー:force_update_version
条件1:android → "1.5.0"
条件2:iOS → "1.5.1"
デフォルト値:"1.5.0"

※キーの長さは256文字でアルファベットや数字を使用可能
※複数の条件でtrueとなった場合はコンソールで設定したUIの一番上が優先される
※条件に満たない場合はデフォルト値が使用される


パラメータキーとデフォルト値を入力
f:id:sunagimo_app:20190924003919p:plain


条件の値を追加を選択し、
androidプラットフォームの条件、iOSプラットフォームの条件をそれぞれ作成
f:id:sunagimo_app:20190924004242p:plain
f:id:sunagimo_app:20190924004251p:plain


条件作成後、条件に合わせたパラメータを入力
f:id:sunagimo_app:20190924004319p:plain


「変更を公開」を選択することで公開される
f:id:sunagimo_app:20190924004423p:plain


また、Conditionsタブで今まで作成した条件を確認可能
他のキーで条件の使い回しも可能
f:id:sunagimo_app:20190924004624p:plain



2.プログラム上でDefault値の設定

コンソール上で設定したキーのデフォルト値を設定する。
コンソール上で設定されていないキーも設定可能。

System.Collections.Generic.Dictionary defaults =
  new System.Collections.Generic.Dictionary();

defaults.Add("force_update_version", "1.5.0");

Firebase.RemoteConfig.FirebaseRemoteConfig.SetDefaults(defaults);



3.パラメータをフェッチ
Firebase Remote Configコンソールで設定した値を同期
TimeSpan.Zeroでキャッシュ時間を0に設定


Firebase.RemoteConfig.FirebaseRemoteConfig.ActivateFetched();
でようやく同期が完了。

System.Threading.Tasks.Task fetchTask = 
Firebase.RemoteConfig.FirebaseRemoteConfig.FetchAsync(TimeSpan.Zero);

      fetchTask.ContinueWithOnMainThread(() => {
        if (fetchTask.IsCanceled) {
          DebugLog("Fetch canceled.");
        } else if (fetchTask.IsFaulted) {
          DebugLog("Fetch encountered an error.");
        } else if (fetchTask.IsCompleted) {
          DebugLog("Fetch completed successfully!");
        }

        var info = Firebase.RemoteConfig.FirebaseRemoteConfig.Info;
        switch (info.LastFetchStatus) {
          case Firebase.RemoteConfig.LastFetchStatus.Success:
            Firebase.RemoteConfig.FirebaseRemoteConfig.ActivateFetched();
            DebugLog(String.Format("Remote data loaded and ready (last fetch time {0}).",
                                  info.FetchTime));
            break;
          case Firebase.RemoteConfig.LastFetchStatus.Failure:
            switch (info.LastFetchFailureReason) {
              case Firebase.RemoteConfig.FetchFailureReason.Error:
                DebugLog("Fetch failed for unknown reason");
                break;
              case Firebase.RemoteConfig.FetchFailureReason.Throttled:
                DebugLog("Fetch throttled until " + info.ThrottledEndTime);
                break;
            }
            break;
          case Firebase.RemoteConfig.LastFetchStatus.Pending:
            DebugLog("Latest Fetch call still pending.");
            break;
          }
      });



3.パラメータを取得
同期が完了している場合はConsoleで設定した条件にあった値
同期に失敗している場合はDefaultで設定した値が返ってくる

Firebase.RemoteConfig.FirebaseRemoteConfig.GetValue("force_update_version").StringValue);


4.パラメータの更新

1.Remote Configコンソール上で更新
2.フェッチ
3.値の取得(更新された値)




パラメータの取得方法


Boolean(真偽値)

(コンソール)
f:id:sunagimo_app:20190925023431p:plain

(プログラム)

Firebase.RemoteConfig.FirebaseRemoteConfig.GetValue("sample_boolean").BooleanValue);




String(文字列)

(コンソール)
f:id:sunagimo_app:20190925023933p:plain

(プログラム)

Firebase.RemoteConfig.FirebaseRemoteConfig.GetValue("sample_string").StringValue);




Double(小数)

(コンソール)
f:id:sunagimo_app:20190925023947p:plain

(プログラム)

Firebase.RemoteConfig.FirebaseRemoteConfig.GetValue("sample_double").DoubleValue);




Long(整数)

(コンソール)
f:id:sunagimo_app:20190925024001p:plain

(プログラム)

Firebase.RemoteConfig.FirebaseRemoteConfig.GetValue("sample_long").LongValue);





ByteArray(IEnumerable)

(コンソール)
f:id:sunagimo_app:20190925024414p:plain

(プログラム)
結果としてはIEnumerable< byte >が返ってくるので、
一度byte[]に変換して、MemoryStreamでStringで読み込んで、JsonUtilityで変換する。

using System.Linq;

public class Samle
{

   [Serializable]
  public class SampleParameter
  {
    public string version;
    public int id;
  }

  public void Read()
  {
      var sample_byte_array = Firebase.RemoteConfig.FirebaseRemoteConfig.GetValue("sample_byte_array").ByteArrayValue;
      byte[] sampleByteArray = sample_byte_array.ToArray();
      var jsonData = string.Empty;
      using(var stream = new System.IO.MemoryStream(sampleByteArray, false))
      {
        using(var reader = new System.IO.StreamReader(stream, System.Text.Encoding.UTF8))
        {
          jsonData = reader.ReadToEnd();
        }
      }
      var sampleParameter = JsonUtility.FromJson<SampleParameter>(jsonData);
      Debug.Log(sampleParameter.version);
      Debug.Log(sampleParameter.id);
  }
  
}




運用上の注意


フェッチ後の値
フェッチを行ったあとの値は、端末上に保存されるのでタスクキルなどをしても
Remote Configコンソール上の値で取得される


キャッシュ問題
フェッチを行っても値が同期されない場合があるらしい?

一応、私の方で試してみましたがリアルタイムで同期されました。
最新のUnity SDKのバージョンだと問題ない可能性?



【対策】
・開発中のみだが頻繁の更新に対応できるように、
Developerモードを導入する。

var settings = Firebase.RemoteConfig.FirebaseRemoteConfig.Settings;
settings.IsDeveloperMode = true;
Firebase.RemoteConfig.FirebaseRemoteConfig.Settings = settings;


Androidの場合はfetch回数の制限があるらしいので、
頻繁に更新をしない運用にする
qiita.com



まとめ

・データをダウンロードせずに値の変更が容易に可能
データ形式も豊富
・特にJSON形式で送れるのが素晴らしい
・非同期で同期を行うためパフォーマンスに影響がない
・キャッシュ問題があるため運用に気をつける
・大規模開発の場合はAPIレスポンスで機能を十分補えるのであまり向いていない
が、サーバーレスで実装したい場合にはとても役立つ

Unityへのクラッシュレポートの導入【Firebase】

クラッシュレポートについて

アプリやソフトウェアにエラーが発生し、強制終了した際に残される情報データ。
エラーが発生した際に開発側に送られることで、開発側は早期にエラーを認知し、解決することにより、ユーザーへの早期通知及び被害の拡大を抑えることができる。



クラッシュレポートツール

Cloud Diagnostics
unity3d.com

Unityのリアルタイムクラッシュ解析ツール。
Unity Editor上より設定可能。
ライセンス形態によって、収集できるレポート数などが異なる。
Slack連携もあるらしい。

※無料


Smart Beat
smrtbeat.com

日本製のリアルタイムクラッシュ解析ツール。
数多くのサービスで導入されており、精度がとても高い。
複数のゲームエンジンに対応しており、
クラッシュ前の画面キャプチャ機能など開発者にとって、
より効率的に解決する機能も豊富。

※有料


Firebase Crashlytics
firebase.google.com

Googleのリアルタイムクラッシュ解析ツール。
AndroidiOS、Unityへ対応している。
Slack連携あり。

※無料



Firebase Crashlyticsの導入

ライセンスにより収集制限もなく、
Slackへの連携もあり更に無料で導入できるということなので
今回はFirebase Crashlyticsを選択。


【環境】
Unityのバージョン :2019.2.5f1
Crashlytics Unityバージョン:4.5.2
Crashlytics iOSバージョン:5.0.0
Crashlytics Androidバージョン:2019年9月13日更新バージョン



Firebase

Googleが提供しているモバイル及びWebアプリケーションを開発する際に
役立つ機能をまとめたもの。
mBaaSといわれている。



1. Firebaseプロジェクトの作成

以下、公式のドキュメントを参考に進めていきます。
firebase.google.com



(1)Firebaseへアクセス
https://firebase.google.com/?hl=ja


(2)プロジェクトを作成を選択
f:id:sunagimo_app:20190903010029p:plain


(3)プロジェクト名を入力
入力後、規約に同意をチェックし続行を選択。
f:id:sunagimo_app:20190903010447p:plain


(4)Googleアナリティクスの設定
後々Crashlyticsに必要となるので、有効状態のまま、続行を選択。
f:id:sunagimo_app:20190903010826p:plain


(5)アナリティクス地域はご住まいの地域に合わせて設定。
それぞれ規約を確認し、チェックボックスを選択、プロジェクトを作成を選択。
f:id:sunagimo_app:20190903011051p:plain


(6)作成されるまでしばらく待機。
f:id:sunagimo_app:20190903011143p:plain


(7)作成完了後、続行を選択。
f:id:sunagimo_app:20190903011224p:plain



2. Firebaseプロジェクトの設定

(1)UnityのプロジェクトとFirebaseプロジェクトとの結びつけを行う。
下記のUnityのマークを選択。
f:id:sunagimo_app:20190903011503p:plain


(2)Unityのプロジェクトを作成し、そのパッケージ名を
iOSAndroidそれぞれFirebaseプロジェクト側へ入力。
入力後、アプリを登録を選択。
f:id:sunagimo_app:20190917004719p:plain


(3)設定ファイルをUnityのProjectの中に配置。
Assets以下の任意の場所に配置可能。
f:id:sunagimo_app:20190917005627p:plain


(4)Firebase Unity SDKをダウンロード。
f:id:sunagimo_app:20190917010406p:plain


(5)ダウンロード後、必要なSDKをUnityのプロジェクトにインポート。
dotnet4/FirebaseAnalytics.unitypackage
dotnet4/FirebaseCrashlytics.unitypackage

f:id:sunagimo_app:20190917010800p:plain

f:id:sunagimo_app:20190917010814p:plain



(6)左上のProject OverViewを選択し、TOPページへ移動。
f:id:sunagimo_app:20190917011426p:plain


(7)Crashlyticsを選択。
f:id:sunagimo_app:20190917011630p:plain


(8)「いいえ、新しいFirebaseアプリを設定します」を選択
f:id:sunagimo_app:20190917011657p:plain


(9)ここは先程完了したのでスキップ
ドキュメントに移動を選択して、ステップ3へ
f:id:sunagimo_app:20190917011814p:plain


(10)パッケージ名、SDKを導入したAndroidiOSアプリを
実機にビルドすると、Crashlytics上でレポートが表示可能になる
f:id:sunagimo_app:20190917011840p:plain



3. Unity側の設定

(1)Firebase SDKの初期化を行うため、ソースコードを記述。
アプリ起動時などに呼び出す。
RuntimeInitializeOnLoadMethodなど

[RuntimeInitializeOnLoadMethod]
static void Initialize()
{
  Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task => {
              var dependencyStatus = task.Result;
              if (dependencyStatus == Firebase.DependencyStatus.Available)
              {
                  Firebase.FirebaseApp app = Firebase.FirebaseApp.DefaultInstance;
              }
              else
              {
                  UnityEngine.Debug.LogError(System.String.Format(
                    "Could not resolve all Firebase dependencies: {0}",dependencyStatus));
              }
          });
}



ただ、公式ドキュメント通りだとContinueWithのコールバック内で、
UnityのMainThreadに関係する処理を呼んでしまうとアプリがクラッシュしてしまうので、Firebase.Extensionsを定義して、ContinueWithOnMainThreadを使用する。

using Firebase.Extensions;

[RuntimeInitializeOnLoadMethod]
static void Initialize()
{
  Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task => {
              var dependencyStatus = task.Result;
              if (dependencyStatus == Firebase.DependencyStatus.Available)
              {
                  Firebase.FirebaseApp app = Firebase.FirebaseApp.DefaultInstance;
              }
              else
              {
                  UnityEngine.Debug.LogError(System.String.Format(
                    "Could not resolve all Firebase dependencies: {0}",dependencyStatus));
              }
          });
}



4. Androidへの導入時

(1)CrashlyticsのライブラリはAndroidX対応されおり、
既存のプロジェクトにAndroid Support Libraryを使用している場合はAndroid Xに対応する必要がある。
developer.android.com


(2)Android Manifest上やC#ソースコード上でJavaを記述している場合は、
以下の移行ドキュメントをもとにSupport LibraryからAndroid Xに修正する。
developer.android.com

例:
android.support.v4.util.LogWriter → androidx.core.util.LogWriter


(3)既存のライブラリでSupport Libraryを使用している場合は、
gradleファイルに記述してAndroid Xへ置き換える。
以下、記事を参考。
qiita.com



5. iOSへの導入時

iOSへの導入には2つの方法がある。
・CocoaPodsで自動でフレームワークを導入する方法
フレームワークを手動で導入する方法


(CocoaPodsで自動でフレームワークを導入する方法)

・個人開発やクラウドビルドしない場合

既にFirebase Unity SDKで用意されている、Dependenciesファイルにより
CocoaPodsで導入するフレームワーク情報が記載されている。
f:id:sunagimo_app:20190922194637p:plain



(1)ビルドするマシンにCocoaPodsを導入する

homebrew

brew install cocoapods 

gem

sudo gem install cocoapods

もしくは

sudo gem install -n /usr/local/bin cocoapods

予めマシンに導入しておくことで、Xcodeビルド時に自動で必要なフレームワーク
導入される。



フレームワークを手動で導入する方法)

・チーム開発
・Cloud Build、Jenkinsでビルドする場合

firebase.google.com



(1)以下、公式ドキュメントよりの「Crashlytics SDKをダウンロードします。」
からフレームワークをダウンロード。
f:id:sunagimo_app:20190922200200p:plain


(2)ダウンロードした、
Crashlytics.framework
Fabric.framework

をUnityのAssses/Plugins/iOS以下に配置する。
f:id:sunagimo_app:20190922200333p:plain


(3)CocoaPodsの定義を削除

AnalyticsDependencies.xml
AppDependencies.xml
CrashlyticsDependencis.xml
にある以下を削除。

f:id:sunagimo_app:20190922202018p:plain



Firebase Crashlyticsの機能


1.送信されるタイミングと出力可能なエラー


送信されるタイミング
1.エラー発生
2.アプリ終了
3.アプリ起動(ここでクラッシュレポートが送信される)


出力可能なエラー

・Unity API系のエラー
Null Reference Exceptionなど
docs.unity3d.com

Debug.LogErrorは出力されない。



・システム系のエラー
System.ArgumentExceptionなど
docs.microsoft.com



・ネイティブ系のエラー
ClassNotFoundExceptionなど
docs.oracle.com



2.レポートのカスタマイズ

・ユーザーIDの登録
送信するレポートに識別子のID情報を付け加えることができる。
Firebase SDK初期化成功時に呼び出す。
Crashlytics上で設定した識別子IDで検索が可能。

Crashlytics.SetUserId("userId");


・レポートへのキー、詳細ログ登録
出力されるレポートに対して、更に詳細な情報を付け足すことが可能

(キーの登録)

Crashlytics.SetCustomKey("key1", "test");


(詳細ログ登録)

Crashlytics.Log("詳細ログ");


・意図的にエラレポートを出力する

void ForceOutputError()
{
 try
 {
  throw new InvalidOperationException("forceError");
 }
 catch(InvalidOperationException ex)
 {
  Crashlytics.LogException(ex);
 }
}


3.Crashlytics Consoleの機能

・アプリバージョンごとの抽出
f:id:sunagimo_app:20190922215219p:plain
f:id:sunagimo_app:20190922215229p:plain


・期間ごとの抽出
f:id:sunagimo_app:20190922215346p:plain


・OS、デバイスによる抽出
f:id:sunagimo_app:20190922215423p:plain
f:id:sunagimo_app:20190922215444p:plain


・ユーザーIDによる抽出
f:id:sunagimo_app:20190922215605p:plain
f:id:sunagimo_app:20190922215617p:plain


・クラッシュレポートの詳細
スタックトレースを確認可能
f:id:sunagimo_app:20190922215655p:plain
f:id:sunagimo_app:20190922215709p:plain


・レポートのステータス更新
レポートをタスクとして管理することが可能。
閉じたり、メモを残すことができる。
f:id:sunagimo_app:20190922215821p:plain


4.Slackとの連携

Slackと連携をすることで、クラッシュレポートが送信された際に通知を受け取ることができる。

(1)Firebaseプロジェクトの設定ページを選択
f:id:sunagimo_app:20190922223935p:plain
f:id:sunagimo_app:20190922224401p:plain


(2)連携機能を選択
f:id:sunagimo_app:20190922224419p:plain


(3)Slackをインストール
f:id:sunagimo_app:20190922224443p:plain


(4)Slackの情報を入力
・Incoming Webhook URL
・チャンネル
・投稿ユーザー名

f:id:sunagimo_app:20190922224605p:plain

WebHookURLはここから取得
https://slack.com/services/new/incoming-webhook

チャンネルを指定してWebHookURLを取得
f:id:sunagimo_app:20190922224744p:plain


(5)通知の送信設定
デフォルトでは2つしかチェックが付いていないので、
全てにチェックを入れておく
f:id:sunagimo_app:20190922225148p:plain


まとめ

・Firebase Crashlyticsは無料
・Unity APIのエラー検知が可能
・通知数の制限なし
・Slackへの連携が可能

【Unity】登録したSceneをenumで管理するコードを自動生成

Build Settingsで登録したシーン一覧をEnumで管理するクラスを自動生成できるようにしてみました。
Sceneを追加するたびに毎回手入力でシーン一覧を更新する手間や、ヒューマンエラーを防ぐのが目的です。

f:id:sunagimo_app:20190519231234p:plain:w420:h450


自動生成したEnumのクラスです。

namespace SunagimoGames
{
	/// <summary>
	/// Scenes(自動生成クラス)。 
	/// </summary> 
	public enum Scenes
	{
		SampleScene,
		Sample1,
		Sample2,
	}
}




ついでにScene名をStringで取得するためのクラスも自動生成しました。

namespace SunagimoGames
{
	/// <summary>
	/// Scenes拡張クラス(自動生成クラス)。
	/// </summary> 
	public static class ScenesHelper
	{
		/// <summary> 
		/// Scenesを文字列に変換するクラス。
		/// </summary> 
		public static string ScenesToString(this Scenes scenes)
		{
			switch(scenes)
			{
				case Scenes.SampleScene:
					return "SampleScene";
				case Scenes.Sample1:
					return "Sample1";
				case Scenes.Sample2:
					return "Sample2";
				default:
					return "";
			}
		}
	}
}




Unityに上部メニューから1クリックで生成できます。
f:id:sunagimo_app:20190519231712p:plain



実装サンプル

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;
using System.Text;

namespace SunagimoGames
{
	public class ScenesCreator
	{
		/// <summary>
		/// namespace。
		/// </summary>
		static readonly string NAMESPACE = "SunagimoGames";

		/// <summary>
		/// 改行。
		/// </summary>
		static readonly string NEWLINE = "\n";

		/// <summary>
		/// タブ。
		/// </summary>
		static readonly string TAB = "\t";

		/// <summary>
		/// スペース。
		/// </summary>
		static readonly string SPACE = " ";

		/// <summary>
		/// ファイルディレクトリパス。
		/// </summary>
		static readonly string FILE_DIRECTORY_PATH = "Assets/Scripts/Scene";

		/// <summary>
		/// Scenesファイル名。
		/// </summary>
		static readonly string SCENES_FILE_NAME = "Scenes";

		/// <summary>
		/// Scenesファイルパス。
		/// </summary>
		static readonly string SCENS_FILE_PATH = System.IO.Path.Combine(FILE_DIRECTORY_PATH, "Scenes.cs");

		/// <summary>
		/// ScenesHelperファイル名。
		/// </summary>
		static readonly string SCENESHELPER_FILE_NAME = "ScenesHelper";

		/// <summary>
		/// ScenesHelperファイルパス。
		/// </summary>
		static readonly string SCENESHELPER_FILE_PATH = System.IO.Path.Combine(FILE_DIRECTORY_PATH, "ScenesHelper.cs");

		/// <summary>
		/// ScenesHelper文字列変換メソッド名。
		/// </summary>
		static readonly string SCENESHELPER_TOSTRING_METHOD_NAME = "ScenesToString";

		[MenuItem("Menu/CreateScenes")]
		static void EditorCreateScenes()
		{
			CreateScenes();
			CreateScenesHelper();
		}

		/// <summary>
		/// シーン名一覧を取得。
		/// </summary>
		/// <returns>シーン名一覧。</returns>
		static List<string> LoadSceneNames()
		{
			var list = new List<string>();
			foreach(var scenes in EditorBuildSettings.scenes)
			{
				// 有効なもの。
				if(scenes.enabled)
				{
					// スラッシュからドットの間を取得。
					var slash = scenes.path.LastIndexOf("/");
					var dot = scenes.path.LastIndexOf(".");
					list.Add(scenes.path.Substring(slash + 1, dot - slash - 1));
				}
			}
			return list;
		}

		/// <summary>
		/// Scenes作成。
		/// </summary>
		static void CreateScenes()
		{
			var sceneNameList = LoadSceneNames();
			if(sceneNameList.Count <= 0)
			{
				return;
			}

			var codeSb = new StringBuilder();
			codeSb.Append("namespace" + SPACE + NAMESPACE + NEWLINE + "{" + NEWLINE);
			codeSb.Append(TAB + "///" + SPACE + "<summary>" + NEWLINE);
			codeSb.Append(TAB + "///" + SPACE + SCENES_FILE_NAME + "(自動生成クラス)。" + SPACE + NEWLINE);
			codeSb.Append(TAB + "///" + SPACE + "</summary>" + SPACE + NEWLINE);
			
			codeSb.Append(TAB + "public enum" + SPACE + SCENES_FILE_NAME + NEWLINE + TAB + "{" + NEWLINE);

			if(sceneNameList.Count > 0)
			{
				for(var idx = 0; idx < sceneNameList.Count; ++idx)
				{
					codeSb.Append(TAB + TAB);
					codeSb.Append(sceneNameList[idx] + "," + NEWLINE);
				}
			}

			codeSb.Append(TAB + "}");
			codeSb.Append(NEWLINE + "}");

			// ディレクトリがないときはディレクトリを作成。
			if(!System.IO.Directory.Exists(FILE_DIRECTORY_PATH))
			{
				System.IO.Directory.CreateDirectory(FILE_DIRECTORY_PATH);
			}

			System.IO.File.WriteAllText(SCENS_FILE_PATH, codeSb.ToString(), System.Text.Encoding.UTF8);
			AssetDatabase.Refresh(ImportAssetOptions.ImportRecursive);
		}

		/// <summary>
		/// ScenesHelper作成。
		/// </summary>
		static void CreateScenesHelper()
		{
			var sceneNameList = LoadSceneNames();
			if(sceneNameList.Count <= 0)
			{
				return;
			}

			var codeSb = new StringBuilder();
			codeSb.Append("namespace" + SPACE + NAMESPACE + NEWLINE + "{" + NEWLINE);
			
			codeSb.Append(TAB + "///" + SPACE + "<summary>" + NEWLINE);
			codeSb.Append(TAB + "///" + SPACE + SCENES_FILE_NAME + "拡張クラス(自動生成クラス)。" + NEWLINE);
			codeSb.Append(TAB + "///" + SPACE + "</summary>" + SPACE + NEWLINE);
			
			codeSb.Append(TAB + "public static class" + SPACE + SCENESHELPER_FILE_NAME + NEWLINE + TAB + "{" + NEWLINE);

			codeSb.Append(TAB + TAB + "///" + SPACE + "<summary>" + SPACE + NEWLINE);
			codeSb.Append(TAB + TAB + "///" + SPACE + SCENES_FILE_NAME + "を文字列に変換するクラス。" + NEWLINE);
			codeSb.Append(TAB + TAB + "///" + SPACE + "</summary>" + SPACE + NEWLINE);

			codeSb.Append(TAB + TAB + "public static string" + SPACE + SCENESHELPER_TOSTRING_METHOD_NAME + "(this" + SPACE + SCENES_FILE_NAME + SPACE + "scenes)" + NEWLINE);
			codeSb.Append(TAB + TAB + "{" + NEWLINE);
			codeSb.Append(TAB + TAB + TAB + "switch(scenes)" + NEWLINE);
			codeSb.Append(TAB + TAB + TAB + "{" + NEWLINE);

			if(sceneNameList.Count > 0)
			{
				for(var idx = 0; idx < sceneNameList.Count; ++idx)
				{
					codeSb.Append(TAB + TAB + TAB + TAB + "case" + SPACE + SCENES_FILE_NAME + "." + sceneNameList[idx] + ":" + NEWLINE);
					codeSb.Append(TAB + TAB + TAB + TAB + TAB + "return" + SPACE + "\"" + sceneNameList[idx] + "\"" + ";" + NEWLINE);
				}
				codeSb.Append(TAB + TAB + TAB + TAB + "default:" + NEWLINE);
				codeSb.Append(TAB + TAB + TAB + TAB + TAB + "return" + SPACE + "\"\"" + ";" + NEWLINE);
			}
			
			codeSb.Append(TAB + TAB + TAB + "}" + NEWLINE);
			codeSb.Append(TAB + TAB + "}" + NEWLINE);
			codeSb.Append(TAB + "}" + NEWLINE);
			codeSb.Append("}");

			// ディレクトリがないときはディレクトリを作成。
			if(!System.IO.Directory.Exists(FILE_DIRECTORY_PATH))
			{
				System.IO.Directory.CreateDirectory(FILE_DIRECTORY_PATH);
			}

			System.IO.File.WriteAllText(SCENESHELPER_FILE_PATH, codeSb.ToString(), System.Text.Encoding.UTF8);
			AssetDatabase.Refresh(ImportAssetOptions.ImportRecursive);
		}
	}
}




【Unity】ドロップダウンのプレースホルダーの設定

選択式のUIコンポーネントです。

f:id:sunagimo_app:20190519181236p:plain:w400:h300

Dropdown、TMP_Dropdown(TextMeshPro)にはプレースホルダーを設定する機能はついていません。
今回はそれを独自に実装する方法を2つ紹介します。
(今回はTextMehPro向けに記載しています。)


リスト選択項目に追加する実装


f:id:sunagimo_app:20190519183231p:plain

ドロップダウンの1つ目の選択肢をプレースホルダーとして使います。
ただプレースホルダー自体も選択肢の一つになるので、
本来のプレースホルダーの機能とは異なっている気がします。

    /// <summary>
    /// ドロップダウン。
    /// </summary>
    [Header("ドロップダウン")]
    [SerializeField]
    TMP_Dropdown dropdown = default;

    void Start()
    {
        dropdown.options.Insert(0, new TMP_Dropdown.OptionData("Please select"));
        dropdown.value = 0;
    }




リスト選択項目に追加しない実装


f:id:sunagimo_app:20190519183438p:plain

リスト選択項目には追加せず、一度他の選択肢を選択すると
プレースホルダーを選択できなくなります。
本来のプレースホルダーの機能を果たしていると思います。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

[RequireComponent(typeof(TMP_Dropdown))]
public class DropdownPlaceHolder : MonoBehaviour
{
    #region Inspector
    /// <summary>
    /// プレースホルダーラベルカラー。
    /// </summary>
    [SerializeField]
    [Header("プレースホルダーカラー")]
    Color32 placeHolderLblColor = new Color32(50, 50, 50, 255);

    /// <summary>
    /// 通常のラベルカラー。
    /// </summary>
    [Header("通常のラベルカラー")]
    [SerializeField]
    Color32 normalLblColor = new Color32(255, 255, 255, 255);
    #endregion

    #region Param

    /// <summary>
    /// 選択が終了しているか。
    /// </summary>
    public bool IsFinishSelected
    { 
        get { return IsEnabledPlaceHolder; }
    }

    /// <summary>
    /// ドロップダウン。
    /// </summary>
    TMP_Dropdown dropdown;

    /// <summary>
    /// プレースホルダーが有効か。
    /// </summary>
    public bool IsEnabledPlaceHolder { get; private set; } = false;

    /// <summary>
    /// プレースホルダーテキスト。
    /// </summary>
    string placeHolderText = string.Empty;
    #endregion

    /// <summary>
    /// ドロップダウンコンポーネントを取得。
    /// </summary>
    TMP_Dropdown GetDropdownComponent
    {
        get 
        { 
            if(dropdown == null)
            {
                return dropdown = GetComponent<TMP_Dropdown>();
            }
            return dropdown;
        }
    }

    /// <summary>
    /// プレースホルダのラベルを設定。
    /// </summary>
    /// <param name="placeHolderText">プレースホルダーテキスト。</param>
    public void SetPlaceHolderLbl(string placeHolderText)
    {
        this.placeHolderText = placeHolderText;
        IsEnabledPlaceHolder = true;
        GetDropdownComponent.captionText.text = placeHolderText;
        SetPlaceHolderLblStyle();
    }
    
    /// <summary>
    /// ドロップダウンアイテム押下処理。
    /// </summary>
    public void OnClickDropdownItem()
    {
        IsEnabledPlaceHolder = false;
        GetDropdownComponent.captionText.text = dropdown.options[dropdown.value].text;
        SetDefaultLblStyle();
    }

    /// <summary>
    /// プレースホルダーのラベルスタイルセット。
    /// </summary>
    void SetPlaceHolderLblStyle()
    {
        GetDropdownComponent.captionText.fontStyle = FontStyles.Italic;
        GetDropdownComponent.captionText.color = placeHolderLblColor;
    }

    /// <summary>
    /// 通常のラベルスタイルセット
    /// </summary>
    void SetDefaultLblStyle()
    {
        GetDropdownComponent.captionText.fontStyle = FontStyles.Normal;
        GetDropdownComponent.captionText.color = normalLblColor;
    }

    void Update()
    {
        // 画面をアクティブ・非アクティブ時に戻ってしまうためUpdateで更新する。
        if(IsEnabledPlaceHolder)
        {
            GetDropdownComponent.captionText.text = placeHolderText;
        }
    }
}





using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Toggle))]
public class DropdownPlaceHolderItem : MonoBehaviour
{
    void Start()
    {
        var dropdownPlaceHolderComponent = GetComponentInParent<DropdownPlaceHolder>();
        var toggleComponent = GetComponent<Toggle>();

        toggleComponent.onValueChanged.AddListener(delegate(bool isOn)
        {
            if(isOn)
            {
                if(dropdownPlaceHolderComponent != null)
                {
                    dropdownPlaceHolderComponent.OnClickDropdownItem();
                }
            }
        });
    }
}


上記でやっていることは以下になります。

1.ドロップダウンで現在選択されているテキストをプレースホルダーのテキストで更新
2.ドロップダウンから選択されたら選択したアイテムの選択肢のテキストに更新


まとめ


dropdownのvalue自体は-1などにしても、自動で0に戻ってしまうため、
何も選択していない状態は作れません。
少々面倒くさい実装になりましたが、これで意図したプレースホルダーになっていると思います。


実装したサンプルコード


github.com

【Unity】PackageManagerにローカルの自作パッケージの登録

PackageManagerとは

・Unity2018から搭載された機能
Preview版の機能提供
・IAP、TextMeshProなどのライブラリの導入



以前までのPackage管理

.unitypackageでやりとりをしていました。
しかしバージョン管理が難しかったり、
どれがどのパッケージと結びついていたりと大変でした

PackageManagerによって、インストール、
アンインストールが一つのボタンで操作でき、
バージョン管理も楽に行なえます。



自作パッケージの登録


【手順】
・パッケージにするファイルを作成
・package.jsonを作成
・パッケージを登録


パッケージにするファイルを作成

パッケージにするディレクトリを作成して、
必要なファイルを中に入れます。

今回は「PackageManagerSample」というディレクトリを作成しました。
中にTextureというディレクトリに画像ファイル、
ScriptsにC#のファイルを入れました。

f:id:sunagimo_app:20190319013724p:plain



package.jsonを作成

パッケージのディレクトリ内に、package.jsonを作成します。

package.json

{
    "name": "com.sunagimoapp.package",
    "displayName": "PackageManagerSample",
    "version": "1.0.0",
    "unity": "2018.3",
    "description": "package manager sample",
    "keywords": ["Script", "sample"],
    "dependencies": { }
}


詳しいパラメーターなどは公式に掲載されています。
docs.unity3d.com

name : パッケージ名など
displayName: 表示名
version: バージョン
unity: Unityのバージョン
description: 説明

パッケージを登録

PackageManagerを開いて、下の+ボタンを選択
f:id:sunagimo_app:20190319014405p:plain


作成したパッケージのpackage.jsonを選択
f:id:sunagimo_app:20190319014430p:plain


エラーが出ずに一覧に表示されれば成功
f:id:sunagimo_app:20190319014458p:plain



参考にさせていただいたサイト様

tsubakit1.hateblo.jp

【Unity】ECS(Entity Component System)を使ってみる

ECSとは

C#JobSystemと同様に、CPUを極限まで使う、
新しいアーキテクチャパターン


ECSの要素

Entity

GameObjectに変わるもの

Component Data

Entityに格納するデータ

Component System

処理部分

Component Group

ComponentDataの種類を設定


ECSの動作
Component SystemとComponent Dataは別々に実装
Entity内に複数のComponent Dataが格納される
Component DataがComponent Groupの要求するデータ設定を満たしていれば、
Component Systemが呼ばれる


サンプル
Updateで数値をカウントさせて、ラベルに反映させる

MonoBehaviour

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
using TMPro;

public class EntityMain : MonoBehaviour
{
    [SerializeField]
    TextMeshProUGUI textLbl;

    EntityManager manager;
    Entity entity;

    void Start()
    {
        manager = World.Active.GetOrCreateManager<EntityManager>();
        var archeType = manager.CreateArchetype(typeof(CountData));
        entity = manager.CreateEntity(archeType);
    }
    
    void Update()
    {
        textLbl.text = manager.GetComponentData<CountData>(entity).Count.ToString();
    }
}



ComponentData

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
using System;

[Serializable]
public struct CountData : IComponentData
{
    public int Count;
}



ComponentSystem、ComponentGroup

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
using TMPro;

public class EntityCountSystem : ComponentSystem
{
    struct Group
    {
        public readonly int Length;
        public ComponentDataArray<CountData> countData;
    }

    [Inject]
    Group group;

    protected override void OnUpdate()
    {
        for(var idx = 0; idx < group.Length; ++idx)
        {
            var countData = group.countData[idx];
            countData.Count++;
            group.countData[idx] = countData;
        }
    }
}



まとめ

ECS、JobSystem、BurstCompilerを組み合わせることで
より効果が発揮されると思います。
今までの実装方法と大きく変わっているので、学習時間は必要ですが
今後もTinyModeのように必須の機能となっていくかもしれません


参考にさせていただいたサイト様

tsubakit1.hateblo.jp

qiita.com

【Unity】C# Job Systemを使ってみる(Burst Compilerも)

C# Job Systemとは

・並列処理を行う機能
・仕事が割り振られていないCPUに割り当てて、複数で処理を行う
・バッテリーの消費が抑えられる


C# Job Systemの特徴

・データ構造はstructのみ
・.NETやUnityのAPIは使えない
・すべてが早くなるわけではない(距離計算、AI)


基本定義

IJob

一つのジョブを発行する


ジョブ未使用時と使用時を比較

<ジョブ未使用>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Jobs;
using Unity.Collections;

public class JobSystemSample : MonoBehaviour
{
    private int processCount = 1000000;

    void Update()
    {
        ExecuteSample();
    }

    void ExecuteSample()
    {
        var result = new NativeArray<int>(processCount, Allocator.TempJob);
        var value1 = 10;
        var value2 = 20;

        for(var idx = 0; idx < result.Length; ++idx)
        {
            result[idx] = value1 + value2;
        }
        result.Dispose();
    }
}



<ジョブ使用>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Jobs;
using Unity.Collections;

public class JobSystemSample : MonoBehaviour
{
    struct SampleJob : IJob
    {
        public int value1;
        public int value2;
        public NativeArray<int> result;

        public void Execute()
        {
            for(var idx = 0; idx < result.Length; ++idx)
            {
                result[idx] = value1 + value2;
            }
        }
    }

    private int processCount = 1000000;

    void Update()
    {
        ExecuteSampleJob();
    }

    void ExecuteSampleJob()
    {
        // バッファ生成。
        var result = new NativeArray<int>(processCount, Allocator.TempJob);

        // ジョブ生成。
        var sampleJob = new SampleJob();
        sampleJob.value1 = 10;
        sampleJob.value2 = 20;
        sampleJob.result = result;

        // ジョブ実行。
        var jobHandle = sampleJob.Schedule();

        // ジョブ完了待機。
        jobHandle.Complete();

        // バッファの破棄。
        result.Dispose();
    }
}



ジョブ未使用
f:id:sunagimo_app:20190317030748p:plain


ジョブ使用
f:id:sunagimo_app:20190317030852p:plain


ジョブ使用時に他のワーカースレッドに差し込まれる


IJobParalellFor

配列の各要素単位で並行して処理を行う


ジョブ未使用時と使用時を比較

<ジョブ未使用>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Jobs;
using Unity.Collections;

public class JobSystemSample : MonoBehaviour
{
    private int processCount = 100000;

    void Update()
    {
        ExecuteSampleParallel();
    }

    void ExecuteSampleParallel()
    {
        // バッファ生成。
        var result = new NativeArray<int>(processCount, Allocator.TempJob);
        var value1 = new NativeArray<int>(processCount, Allocator.TempJob);
        var value2 = new NativeArray<int>(processCount, Allocator.TempJob);

        for(var idx = 0; idx < value1.Length; ++idx)
        {
            value1[idx] = idx;
        }

        for(var idx = 0; idx < value2.Length; ++idx)
        {
            value2[idx] = idx;
        }

        for(var idx = 0; idx < result.Length; ++idx)
        {
            result[idx] = value1[idx] + value2[idx];
        }

        // バッファの破棄。
        value1.Dispose();
        value2.Dispose();
        result.Dispose();
    }
}



<ジョブ使用>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Jobs;
using Unity.Collections;

public class JobSystemSample : MonoBehaviour
{
    struct SampleParallelJob : IJobParallelFor
    {
        public NativeArray<int> value1;
        public NativeArray<int> value2;
        public NativeArray<int> result;

        public void Execute(int i)
        {
            result[i] = value1[i] + value2[i];
        }
    }

    private int processCount = 100000;

    void Update()
    {
        ExecuteSampleParallelJob();
    }

    void ExecuteSampleParallelJob()
    {
        // バッファ生成。
        var result = new NativeArray<int>(processCount, Allocator.TempJob);
        var value1 = new NativeArray<int>(processCount, Allocator.TempJob);
        var value2 = new NativeArray<int>(processCount, Allocator.TempJob);

        for(var idx = 0; idx < value1.Length; ++idx)
        {
            value1[idx] = idx;
        }

        for(var idx = 0; idx < value2.Length; ++idx)
        {
            value2[idx] = idx;
        }

        // ジョブ生成。
        var parallelJob = new SampleParallelJob();

        parallelJob.value1 = value1;
        parallelJob.value2 = value2;
        parallelJob.result = result;

        // ジョブ実行。
        var jobHandle = parallelJob.Schedule(result.Length, 1);

        // ジョブ完了待機。
        jobHandle.Complete();

        // バッファの破棄。
        value1.Dispose();
        value2.Dispose();
        result.Dispose();
    }
}



ジョブ未使用
f:id:sunagimo_app:20190317031315p:plain


ジョブ使用
f:id:sunagimo_app:20190317031331p:plain

複数のワーカースレッドが差し込まれる

IJobParalellForTransform

複数のTransformに対して並行して処理を行う


ジョブ未使用時と使用時を比較

<ジョブ未使用>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Jobs;
using Unity.Collections;
using UnityEngine.Jobs;

public class JobSystemSample : MonoBehaviour
{
    private int processCount = 10000;

    [SerializeField]
    private Transform prefab = null;

    private TransformAccessArray prefabTransformAccessArray;

    void Start()
    {
        CreatePrefab();
    }

    void CreatePrefab()
    {
        var transforms = new Transform[processCount];
        for(var idx = 0; idx < processCount; ++idx)
        {
            var prefabInstance = Instantiate(prefab);
            transforms[idx] = prefabInstance;
        }
        prefabTransformAccessArray = new TransformAccessArray(transforms);
    }

    void OnDestroy()
    {
        prefabTransformAccessArray.Dispose();
    }

    void ExecuteSampleParallelTransform()
    {
        for(var idx = 0; idx < prefabTransformAccessArray.length; ++idx)
        {
            prefabTransformAccessArray[idx].localPosition = new Vector3(Random.Range(-15, 15), Random.Range(-15, 15), Random.Range(-15, 15));
        }
    }

    void Update()
    {
        ExecuteSampleParallelTransform();
    }
}



<ジョブ使用>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Jobs;
using Unity.Collections;
using UnityEngine.Jobs;

public class JobSystemSample : MonoBehaviour
{
    struct SampleParallelTransformJob : IJobParallelForTransform
    {
        public NativeArray<Vector3> positions;
        public void Execute(int idx, TransformAccess transform)
        {
            var pos = positions[idx];
            transform.localPosition = pos;
        }
    }

    private int processCount = 10000;

    [SerializeField]
    private Transform prefab = null;

    private TransformAccessArray prefabTransformAccessArray;

    void Start()
    {
        CreatePrefab();
    }

    void CreatePrefab()
    {
        var transforms = new Transform[processCount];
        for(var idx = 0; idx < processCount; ++idx)
        {
            var prefabInstance = Instantiate(prefab);
            transforms[idx] = prefabInstance;
        }
        prefabTransformAccessArray = new TransformAccessArray(transforms);
    }

    void OnDestroy()
    {
        prefabTransformAccessArray.Dispose();
    }

   void ExecuteSampleParallelTransformJob()
    {
        var positions = new NativeArray<Vector3>(prefabTransformAccessArray.length, Allocator.TempJob);

        for(var idx = 0; idx < prefabTransformAccessArray.length; ++idx)
        {
            positions[idx] = new Vector3(Random.Range(-15, 15), Random.Range(-15, 15), Random.Range(-15, 15));
        }

        var sampleParallelTransformJob = new SampleParallelTransformJob();
        sampleParallelTransformJob.positions = positions;

        var jobHandle = sampleParallelTransformJob.Schedule(prefabTransformAccessArray);
        jobHandle.Complete();

        positions.Dispose();
    }

    void Update()
    {
        ExecuteSampleParallelTransformJob();
    }
}



ジョブ未使用
f:id:sunagimo_app:20190317040922p:plain


ジョブ使用
f:id:sunagimo_app:20190317040933p:plain


あれ、なぜかジョブ使用すると遅くなってる…
計測の仕方が悪かったのかもしれません。



BurstCompiler

JobSystemやECSと合わせるとパフォーマンスが更に向上する機能

Package Managerより導入できます。
docs.unity3d.com



using Unity.Burst
[BurstCompile]のアトリビュートをつけるだけです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Jobs;
using Unity.Collections;
using UnityEngine.Jobs;
using Unity.Burst;

public class JobSystemSample : MonoBehaviour
{
    [BurstCompile]
    struct SampleParallelTransformJob : IJobParallelForTransform
    {
        public NativeArray<Vector3> positions;
        public void Execute(int idx, TransformAccess transform)
        {
            var pos = positions[idx];
            transform.localPosition = pos;
        }
    }
}


バーストコンパイラ使用時
f:id:sunagimo_app:20190317041711p:plain

処理時間が半分ぐらいになりました。
ここまでパフォーマンスが向上するとは…



まとめ

大量のデータ、オブジェクトを処理する際はとても便利な機能だと思います。
普段はそこまで使う機会はないかと思いますが、いつか必要なときが来るかと思います。




参考にさせていただいたサイト様

tsubakit1.hateblo.jp

www.shibuya24.info