UniTaskを使ったポップアップの作成
UniTaskを使ったポップアップ(ダイヤログ)の作り方を紹介します。
ポップアップ内での選択結果で処理を分岐するのに適しています。
また、ポップアップのアニメーションは共通化しておき、ポップアップの内容だけ変えたい場合にも使えます。
サンプルアニメーションです。
UniTaskを使ったYesOrNoの処理
UniTaksのWhenAnyは何番目のタスクが完了したかを返してくれるので、WhenAnyの結果からどのボタンが押されたかが判断できます。
var isYes = await UniTask.WhenAny(noButton.OnClickAsync(), yesButton.OnClickAsync()) == 1;
noButtonはIndex=0なので押されるとfalseが、yesButtonはindex=1なので押されるとtrueが返ります。
また、複数ボタンにも対応しています。
var index = await UniTask.WhenAny(buttons.Select(b => b.OnClickAsync()).ToArray());
どれか1つを押すと次に進むポップアップはこの仕組みと相性がよいです。
ポップアップでの使われ方イメージ
// 1. ポップアップ生成 var popup = await PopupCreator.CreateAsync<生成するポップアップの型>(); // 2. 表示アニメーション→入力待ち→閉じるアニメーション var isYes= await popup.ShowAsync(); // 3. ポップアップでの入力を受け取り外で分岐処理 if (isYes) "Yes時の処理~"
awaitすることにより同期的に書けるので、コールバックが不要になります。
// awaitを使わない場合のコールバック例 popup.ShowAsync(() => Yesの処理, () => Noの処理); // awaitを使った処理 var isYes = await popup.ShowAsync(); if (isYes) Yesの処理~ else Noの処理~
実装編
共通クラスの実装
- PopupCreator
ポップアップ作成クラス
PopupCreator.CreateAsync<生成するポップアップの型>()でポップアップを作る。
using UniRx.Async; using UnityEngine; /// <summary> /// ポップアップ生成クラス /// </summary> public static class PopupCreator { /// <summary> /// PopupとContentをロードしポップアップを生成する /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public static async UniTask<T> CreateAsync<T>() where T : PopupContentBase { var popupContainer = await Resources.LoadAsync<PopupContainer>("Popup/PopupContainer") as PopupContainer; if (!popupContainer) { Debug.LogAssertion("PopupContainerが見つかりません"); return null; } var popupInstance = Object.Instantiate(popupContainer); var content = await Resources.LoadAsync<T>("Popup/Content/" + typeof(T).Name) as T; if (!content) { Debug.LogAssertion($"{typeof(T).Name}が見つかりません"); return null; } var contentInstance = Object.Instantiate(content); popupInstance.SetContent(contentInstance); return contentInstance; } }
- PopupContainer
ポップアップの親となるクラス。
ポップアップのフレーム部分に相当。
using UnityEngine; /// <summary> /// ポップアップの格納、フレーム部分 /// アニメーションへの参照と要素の紐づけを行う /// </summary> public class PopupContainer : MonoBehaviour { [SerializeField] private PopupAnimator animator; public PopupAnimator PopupAnimator => animator; /// <summary> /// ポップアップの要素を設定する /// </summary> /// <param name="content"></param> public void SetContent(PopupContentBase content) { content.transform.SetParent(animator.transform, false); content.Initialize(this); } }
- PopupAnimator
ポップアップ開閉時のアニメーションクラス。
Animation/Animatorでアニメーションしてもいいし、DoTweenなどのスクリプトでアニメーションしてもよいです。 実際はインターフェースのほうがいいかも。
ShowとHideがawait可能なように記述することでアニメーションが終わるまで次の処理を待つことができます。
サンプルとしてAnimatorでShowとHideのアニメーションを設定しています。
using UniRx.Async; using UnityEngine; /// <summary> /// ポップアップアニメーション /// </summary> public class PopupAnimator : MonoBehaviour { // ShowPopupとHidePopupのアニメーションが設定されている [SerializeField] private Animator animator; public async UniTask PlayAnimationAsync() { animator.Play("ShowPopup"); await UniTask.WaitUntil(IsFinish); } public async UniTask HideAnimationAsync() { animator.Play("HidePopup"); await UniTask.WaitUntil(IsFinish); } private bool IsFinish() { return animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1; } }
- PopupContentBase
ポップアップの中身に相当し、各ポップアップごとにPopupContentBaseを継承してプレハブとして作ります。
using UniRx.Async; using UnityEngine; /// <summary> /// ポップアップの中身 /// 実際は継承して使われる /// </summary> public abstract class PopupContentBase : MonoBehaviour { private PopupContainer container; /// <summary> /// 初期化 /// </summary> /// <param name="container"></param> public void Initialize(PopupContainer container) { this.container = container; } /// <summary> /// 表示する /// </summary> /// <returns></returns> protected async UniTask ShowAnimationAsync() { await container.PopupAnimator.PlayAnimationAsync(); } /// <summary> /// 非表示にする /// </summary> /// <returns></returns> protected async UniTask HideAnimationAsync() { await container.PopupAnimator.HideAnimationAsync(); Destroy(container.transform.gameObject); } }
PopupContentの実装例
確認メッセージがあり、Yes or Noで選択するポップアップは以下のようになります。
using UniRx.Async; using UnityEngine; using UnityEngine.UI; /// <summary> /// 確認メッセージがあり、Yes or Noで選択するポップアップ /// </summary> public class ConfirmPopupContent : PopupContentBase { [SerializeField] private Text _confirmMessage; // noが左、yesが右想定 [SerializeField] private Button noButton; [SerializeField] private Button okButton; public void Initialize(string message) { _confirmMessage.text = message; } public async UniTask<bool> ShowAsync() { await ShowAnimationAsync(); var isYes = await UniTask.WhenAny(noButton.OnClickAsync(), okButton.OnClickAsync()) == 1; await HideAnimationAsync(); return isYes; } }
使用例
上記のクラスを使ったポップアップ生成までの実装例です。
プレハブの作成
PopupContainerプレハブを以下のように作り、Resources/Popupフォルダ内に設置します。
Animationを担当するオブジェクト。
フェードイン/フェードアウトはCanvasGroupのAlphaを変更することで実現しています。
YesOrNo(Confirm)のポップアップはクラス名とプレハブ名を合わせてResource/Popup/Contentフォルダ内に設置します。
見た目は適当に。
ShowとHideのアニメーションも設定しておきます。
ポップアップの呼び出し
ボタンのOnClickイベントに合わせ以下のように呼び出します。
confirmButton.onClick.AddListener(async () => { var popup = await PopupCreator.CreateAsync<ConfirmPopupContent>(); popup.Initialize("ポップアップ確認?"); var isYes = await popup.ShowAsync(); if (isYes) { text.text = "Yesが押された"; } else { text.text = "Noが押された"; } });
これでポップアップ生成→アニメーション→入力待ち→処理の分岐が行えました。
応用編
そのほかのポップアップ
複数選択肢があるポップアップは以下のようになります。
ボタンのラベル変更機能があり、複数ボタンのクリック待ちをLINQでUniTask化していること以外は、ほぼYesOrNoのポップアップと同じです。
using System.Linq; using UniRx.Async; using UnityEngine; using UnityEngine.UI; /// <summary> /// メッセージと複数選択肢があるポップアップ /// </summary> public class SelectionPopupContent : PopupContentBase { [SerializeField] private Text confirmMessage; [SerializeField] private Button[] buttons; public void Initialize(string message, string[] buttonLabels) { confirmMessage.text = message; var length = Mathf.Min(buttons.Length, buttonLabels.Length); for (var i = 0; i < length; i++) { buttons[i].GetComponentInChildren<Text>().text = buttonLabels[i]; } } /// <summary> /// Yes or Noのポップアップを表示 /// </summary> /// <returns></returns> public async UniTask<int> ShowAsync() { await ShowAnimationAsync(); var index = await UniTask.WhenAny(buttons.Select(b => b.OnClickAsync()).ToArray()); await HideAnimationAsync(); return index; } }
使い方
selectionButton.onClick.AddListener(async () => { var popup = await PopupCreator.CreateAsync<SelectionPopupContent>(); var labels = new string[]{"A","B","C"}; popup.Initialize("選択肢を表示します", labels); var index = await popup.ShowAsync(); text.text = $"{labels[index]}ボタンが押された"; });
ポップアップ表示の入力ブロック
アニメーション中にユーザーの入力を禁止したいときは、AnimationでGanvasGroupのInteractableをオフにすると簡単です。
アニメーション終了時に再度interactableをオンにするのを忘れずに。
また、ポップアップ表示中に後ろのUIへの入力を禁止したいときは、Parentオブジェクト(Animatorがアタッチされたオブジェクト)に透明なImageコンポーネントをアタッチすることで可能です。
環境
Unity:Unity2018.4
UniTask:Ver 1.2.0
今回紹介したサンプルはGitHubで公開しています
触ると表示され放置すると消えるスクロールバー
概要
アプリによくあるような、普段は見えないが、スクロール領域を動かしたらフェードインで表示され、スクロールが終わるとフェードアウトで消えていくようなスクロールバーを作った。
ステートパターンで非表示/フェードイン/表示/フェードアウトを切り替えている。
ステートが変わるたびにオブジェクトが生成されるのを避けるため、各ステートは初回で生成してしまっている。
フェード処理、遅延処理にはDoTweenを使用している。
パラメータ
- AutoFadeScrollbar.handle:表示・非表示となる操作用ハンドル画像。Sliding Areaは最初から非表示にしておく
- FadeInState.fadeDuration:フェードインにかかる時間
- ShowState.showDuration:表示し続ける時間
- FadeOutState.fadeDuration:フェードアウトにかかる時間
コード
以下を作成しScrollbarコンポーネントとともにアタッチする。
#region以下はステートの実装部。
using DG.Tweening; using UnityEngine; using UnityEngine.UI; /// <summary> /// 動かしたときに表示され、自動で消えるスクロールバー /// </summary> [RequireComponent(typeof(Scrollbar))] public class AutoFadeScrollbar : MonoBehaviour { [SerializeField] private Scrollbar scrollbar; [SerializeField] private Image handle; /// <summary> /// 初期処理 /// </summary> private void Awake() { var context = new Context(handle); scrollbar.onValueChanged.AddListener(_ => context.OnScroll()); } #region ハンドルコンテキストとステートのクラス定義 private enum HandleState { Hide, FadeIn, Show, FadeOut } /// <summary> /// コンテキストクラス /// </summary> private class Context { /// <summary> /// ハンドル画像 /// </summary> public readonly Image Image; private IHandleState currentState; /// <summary> /// ステートリスト /// 毎回newしたくないので状態は保持しておく /// </summary> private readonly IHandleState[] stateList = { new HideState(), new FadeInState(), new ShowState(), new FadeOutState() }; /// <summary> /// コンストラクタ /// ハンドル画像の取得と初期Stateのセット /// </summary> /// <param name="image"></param> public Context(Image image) { Image = image; ChangeState(HandleState.Hide); } /// <summary> /// ハンドル状態の更新 /// </summary> /// <param name="newState"></param> public void ChangeState(HandleState newState) { currentState = stateList[(int)newState]; currentState.Initialize(this); } /// <summary> /// 動かしたとき各ステートの実行 /// </summary> public void OnScroll() { currentState.OnScroll(this); } } /// <summary> /// ハンドルの状態を表すインターフェース /// </summary> private interface IHandleState { /// <summary> /// ステート遷移時の初期化 /// </summary> /// <param name="context"></param> void Initialize(Context context); /// <summary> /// 動かしたとき /// </summary> /// <param name="context"></param> void OnScroll(Context context); } /// <summary> /// 非表示状態 /// </summary> private class HideState : IHandleState { private readonly Color clearColor; /// <summary> /// コンストラクタ /// 透明色のキャッシュ /// </summary> public HideState() { clearColor = Color.white; clearColor.a = 0; } /// <summary> /// 完全に非表示化 /// </summary> /// <param name="context"></param> public void Initialize(Context context) { context.Image.color = clearColor; } /// <summary> /// 表示中に切り替え /// </summary> /// <param name="context"></param> public void OnScroll(Context context) { context.ChangeState(HandleState.FadeIn); } } /// <summary> /// フェード表示中 /// </summary> private class FadeInState : IHandleState { // フェード時間 private float fadeDuration = 0.2f; /// <summary> /// フェード表示の実行 /// </summary> /// <param name="context"></param> public void Initialize(Context context) { DOTween.ToAlpha(() => context.Image.color, color => context.Image.color = color, 1, fadeDuration) .OnComplete(() => context.ChangeState(HandleState.Show)); } public void OnScroll(Context context) { } } /// <summary> /// 表示中 /// </summary> private class ShowState : IHandleState { // 表示し続ける時間 private float showDuration = 3f; private Tween _tween; private readonly Color clearColor; /// <summary> /// コンストラクタ /// 表示色のキャッシュ /// </summary> public ShowState() { clearColor = Color.white; } /// <summary> /// 表示後、徐々にフェードアウトさせる /// </summary> /// <param name="context"></param> public void Initialize(Context context) { context.Image.color = clearColor; _tween = DOVirtual.DelayedCall(showDuration, () => context.ChangeState(HandleState.FadeOut)); } /// <summary> /// スクロールされると表示時間をリセット /// </summary> /// <param name="context"></param> public void OnScroll(Context context) { _tween.Restart(); } } /// <summary> /// フェードアウト中 /// </summary> private class FadeOutState : IHandleState { private float fadeDuration = 0.4f; /// <summary> /// フェードアウトの実行 /// </summary> /// <param name="context"></param> public void Initialize(Context context) { DOTween.ToAlpha(() => context.Image.color, color => context.Image.color = color, 0, fadeDuration) .OnComplete(() => context.ChangeState(HandleState.Hide)); } /// <summary> /// スクロールすると再表示 /// </summary> /// <param name="context"></param> public void OnScroll(Context context) { context.ChangeState(HandleState.FadeIn); } } #endregion }
SpriteAtlas作成時に圧縮設定を自動で適用する
SpriteAtlas作成時の圧縮設定を自動化します。
特にAndroid、iOSなどのモバイル向け設定を変更します。
背景
Unityではアセット作成時やインポート時にAssetPostprocessorを利用することでアセットの設定を自動で変えることができます。
TextureにはTextureImporter、3DモデルにはModelImporterといったAssetImporterが存在しており、簡単にインポート設定を変更することができます。
しかしSpriteAtlasにはImporterが存在しないため、一旦SerializeObjectとして読み込み、直接値を書き換えることで設定を反映させます。
今回は各プラットフォームの圧縮設定を変更します。
導入
下準備で圧縮設定やPacking設定などを適用済みのPreset用SpriteAtlasを Assets/Editor/DefaultSpriteAtlas.spriteatlas に作成しておきます。
次に以下のコードをEditorフォルダ以下に作成すればSpriteAtlas作成時に自動で設定が適用されます。
using System.Linq; using UnityEditor; using UnityEngine.U2D; /// <summary> /// SpriteAtlasのプラットフォーム圧縮設定を自動適用する /// </summary> public class SpriteAtlasPostprocessor : AssetPostprocessor { private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromPath) { // SpriteAtlasの変更のみ検知 var list = importedAssets.Where(c => c.EndsWith(".spriteatlas")).ToArray(); if (list.Length <= 0) return; // 設定をインポートするPreset用のSpriteAtlas var originAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>("Assets/Editor/DefaultSpriteAtlas.spriteatlas"); var originSetting = new SerializedObject(originAtlas).FindProperty("m_EditorData").FindPropertyRelative("platformSettings"); var originAndroid = originSetting.GetArrayElementAtIndex(0); var originiPhone = originSetting.GetArrayElementAtIndex(1); // 設定を適用 foreach (var path in list) { var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(path); var serializedObject = new SerializedObject(atlas); var editorData = serializedObject.FindProperty("m_EditorData"); // platform設定 var platFormSettings = editorData.FindPropertyRelative("platformSettings"); // Android、iOSのみ設定を変えるのでarraySizeは2 platFormSettings.arraySize = 2; var targetAndroid = platFormSettings.GetArrayElementAtIndex(0); var targetiPhone = platFormSettings.GetArrayElementAtIndex(1); CopyProperty(targetAndroid, originAndroid); CopyProperty(targetiPhone, originiPhone); // ついでにpacking設定も適用 var packingSetting = editorData.FindPropertyRelative("packingSettings"); packingSetting.FindPropertyRelative("enableRotation").boolValue = false; packingSetting.FindPropertyRelative("enableTightPacking").boolValue = false; // 上記の変更を適用 serializedObject.ApplyModifiedProperties(); } } /// <summary> /// プロパティの書き換え /// フォーマットは以下のようになっている /// - serializedVersion: 2 /// m_BuildTarget: Android /// m_MaxTextureSize: 2048 /// m_ResizeAlgorithm: 0 /// m_TextureFormat: 65 /// m_TextureCompression: 1 /// m_CompressionQuality: 50 /// m_CrunchedCompression: 0 /// m_AllowsAlphaSplitting: 0 /// m_Overridden: 1 /// m_AndroidETC2FallbackOverride: 0 /// </summary> /// <param name="_target">適用先</param> /// <param name="_origin">適用元</param> private static void CopyProperty(SerializedProperty _target, SerializedProperty _origin) { _target.FindPropertyRelative("m_BuildTarget").stringValue = _origin.FindPropertyRelative("m_BuildTarget").stringValue; _target.FindPropertyRelative("m_MaxTextureSize").intValue = _origin.FindPropertyRelative("m_MaxTextureSize").intValue; _target.FindPropertyRelative("m_TextureFormat").intValue = _origin.FindPropertyRelative("m_TextureFormat").intValue; _target.FindPropertyRelative("m_TextureCompression").intValue = _origin.FindPropertyRelative("m_TextureCompression").intValue; _target.FindPropertyRelative("m_CompressionQuality").intValue = _origin.FindPropertyRelative("m_CompressionQuality").intValue; _target.FindPropertyRelative("m_CrunchedCompression").boolValue = _origin.FindPropertyRelative("m_CrunchedCompression").boolValue; _target.FindPropertyRelative("m_AllowsAlphaSplitting").boolValue = _origin.FindPropertyRelative("m_AllowsAlphaSplitting").boolValue; _target.FindPropertyRelative("m_Overridden").boolValue = _origin.FindPropertyRelative("m_Overridden").boolValue; _target.FindPropertyRelative("m_AndroidETC2FallbackOverride").intValue = _origin.FindPropertyRelative("m_AndroidETC2FallbackOverride").intValue; } }
解説
AssetPostprocessorとOnPostprocessAllAssets
アセットの変更を検知します。
OnPostprocessAllAssetsには特定のファイル形式のみを検知する方法はないため、下記でspriteatlasのみをフィルタリングします。
var list = importedAssets.Where(c => c.EndsWith(".spriteatlas")).ToArray();
importedAssetsのpathには拡張子も含まれます。
SpriteAtlasのシリアライズ形式
デフォルトの.spriteAtlasファイルは以下のようになっています。
%YAML 1.1 %TAG !u! tag:unity3d.com,2011: --- !u!687078895 &4343727234628468602 SpriteAtlas: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: New Sprite Atlas m_EditorData: serializedVersion: 2 textureSettings: serializedVersion: 2 anisoLevel: 1 compressionQuality: 50 maxTextureSize: 2048 textureCompression: 0 filterMode: 1 generateMipMaps: 0 readable: 0 crunchedCompression: 0 sRGB: 1 platformSettings: [] packingSettings: serializedVersion: 2 padding: 4 blockOffset: 1 allowAlphaSplitting: 0 enableRotation: 1 enableTightPacking: 1 variantMultiplier: 1 packables: [] totalSpriteSurfaceArea: 0 bindAsDefault: 1 m_MasterAtlas: {fileID: 0} m_PackedSprites: [] m_PackedSpriteNamesToIndex: [] m_Tag: New Sprite Atlas m_IsVariant: 0
プラットフォームの圧縮設定をoverrideし圧縮設定を変更してみるとplatformSettingsが以下になります。
圧縮設定はAndroidで RGBA Crunched ETC2 、iOSで RGBA Compressed ASTC 6×6 Block です。
platformSettings: - serializedVersion: 2 m_BuildTarget: Android m_MaxTextureSize: 2048 m_ResizeAlgorithm: 0 m_TextureFormat: 65 m_TextureCompression: 1 m_CompressionQuality: 50 m_CrunchedCompression: 0 m_AllowsAlphaSplitting: 0 m_Overridden: 1 m_AndroidETC2FallbackOverride: 0 - serializedVersion: 2 m_BuildTarget: iPhone m_MaxTextureSize: 2048 m_ResizeAlgorithm: 0 m_TextureFormat: 50 m_TextureCompression: 1 m_CompressionQuality: 50 m_CrunchedCompression: 0 m_AllowsAlphaSplitting: 0 m_Overridden: 1 m_AndroidETC2FallbackOverride: 0
上記からm_EditorData->platformSettings->各値でたどって変更すれば良いことがわかります。
SerializeObjectからの値取得
元のSpriteAtlasの設定値を取得する流れです。
①SpriteAtlas形式のファイルをSerializedObjectに変換
new SerializedObject(originAtlas)
②FindPropertyで目的のプロパティを取得
.FindProperty("m_EditorData")
③FindPropertyRelativeでプロパティ内のプロパティを取得
.FindPropertyRelative("platformSettings")
④配列の場合はGetArrayElementAtIndex(番号)で取得
var originAndroid = originSetting.GetArrayElementAtIndex(0);
反映先も同様にして取得しますが、SpriteAtlasのPlatformSettingsは空だったのでサイズを拡張する必要があります。
platFormSettings.arraySize = 2
反映には各プロパティの実値を取得する必要があるため、stringValue/intValue/boolValueなどと型を指定します。
型はインスペクタでどのように見えているかを参考に。
(数値入力ならintValue、チェックボックスならboolValue)
最後に適用を保存すれば完了です。
serializedObject.ApplyModifiedProperties();
検証環境
- Unity2018.3
- Unity2019.1
参考