UniTaskを使ったポップアップの作成

UniTaskを使ったポップアップ(ダイヤログ)の作り方を紹介します。

ポップアップ内での選択結果で処理を分岐するのに適しています。

また、ポップアップのアニメーションは共通化しておき、ポップアップの内容だけ変えたい場合にも使えます。

サンプルアニメーションです。

f:id:tetsujp84:20191210225024g:plain

 

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フォルダ内に設置します。

f:id:tetsujp84:20191210233910p:plain

Animationを担当するオブジェクト。

フェードイン/フェードアウトはCanvasGroupのAlphaを変更することで実現しています。 f:id:tetsujp84:20191210233913p:plain

YesOrNo(Confirm)のポップアップはクラス名とプレハブ名を合わせてResource/Popup/Contentフォルダ内に設置します。 f:id:tetsujp84:20191210233918p:plain

見た目は適当に。

f:id:tetsujp84:20191210233921p:plain

ShowとHideのアニメーションも設定しておきます。 f:id:tetsujp84:20191210233925p:plain

f:id:tetsujp84:20191210233927p:plain

ポップアップの呼び出し

ボタンの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で公開しています

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作成時の圧縮設定を自動化します。
特にAndroidiOSなどのモバイル向け設定を変更します。

背景

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

参考

baba-s.hatenablog.com