うどんてっくメモ

技術的なメモをまったりと

【Unity】「uPalette」で色を管理すると捗るという話

はじめに

UIデザインを作っていく上で必要になってくるのが色を管理する仕組みです。 決定形のボタンにはこの青色を、警告的な文言にはこの赤色を、レベルに合わせてこの色の段階を、などなど、色のルールづけが多い場面は多々あります。 そんな時に便利なのがuPaletteです。Haruma-K(@harumak_11) さんがOSSとして公開しているツールで、プロジェクト内の色の管理及び一元変更を実現してくれます。

light11.hatenadiary.com

github.com

実際に開発で使っている筆者が、uPaletteはいいぞという点をいくつか紹介します。 本記事で紹介および検証を行なっているツールのバージョンは次の通りです。

  • Unity 2021.3.0f1
  • uPalette 2.1.1

バージョンによっては挙動に差異がある場合もありますので、ご了承ください。

基礎的な機能の利便性

GUI操作によるシンプルな色の設定の実現

uPaletteではColorSynchronizerというコンポーネントをGraphicにアタッチし、色の設定を行います。 UnityのWindowとしてエディタ拡張が実装されており、GUIの操作をぽちぽちすることで色の登録からColorSynchronizerのアタッチ、色の反映までを実現します。 UIを量産する上で簡単なGUI操作でポチポチと設定を変えられるのはとても重要で、uPalleteはしっかりその部分が作り込まれています。

特徴的なのが、設定側の色の変更に対してリアルタイムで対応するGraphicの色が変わることです。設定側でやっぱり色味を変えようとした際に一律でシーンやPrefab Modeで色の更新を確認することができます。 また、この色を使ってるのどれだっけ?みたいな場面で、逆にハイライトすることが可能です。

もちろんColorSynchronizerのコンポーネントから設定した色は参照可能で、インスペクターから変更することも可能です。

このように、UIパーツの色を管理する上で必要な機能はとても使いやすい形で提供されています。

システム的な使い勝手を考慮した設計

静的に配置しているオブジェクトの色をよしなに変えられることも大切ですが、システムを開発する上では動的にスクリプト側から色を変えたい場合も存在します。 たとえば動的に表示したいデータが持つenumに伴って、このenumは赤、このenumは青という色の変化を行うケースです。 そんな時はスクリプトからの反映が必要ですが、uPaletteではそれらの機能もスクリプト側で公開されており、システム側でもよしなに取り扱うことが可能です。 まずは設定した色の参照ですが、uPaletteではPaletteというクラスで管理されています。

public abstract class Palette<T> : ISerializationCallbackReceiver
{
    ...

    public IObservable<(string entryId, int index)> EntryOrderChangedAsObservable => _entryOrderChangedSubject;
    public IObservable<(string themeId, int index)> ThemeOrderChangedAsObservable => _themeOrderChangedSubject;

    public IReadOnlyObservableProperty<Theme> ActiveTheme => _activeTheme;
    public IReadOnlyObservableDictionary<string, Theme> Themes => _themes;
    public IReadOnlyObservableDictionary<string, Entry<T>> Entries => _entries;

    ...

色はもちろん、2.0.0から追加されたGradationなどの情報もこのPaletteで管理されています。 それぞれのPaletteはPalleteStoreというシングルトンで管理されており、シンプルな実装で色を参照可能です。

public sealed class PaletteStore : ScriptableObject
{
    ...

    public Palette<Color> ColorPalette => _colorPalette;
    public Palette<Gradient> GradientPalette => _gradientPalette;
    public Palette<CharacterStyle> CharacterStylePalette => _characterStylePalette;
    public Palette<CharacterStyleTMP> CharacterStyleTMPPalette => _characterStyleTMPPalette;

    ...
image.color = PaletteStore.Instance.ColorPalette.Entries["color_id"];

色の反映対象となるColorSynchronizerのコンポーネントについても、継承することで独自の拡張をすることを想定した作りになっています。 これによって独自で実装した描画コンポーネントなどにもuPaletteの反映ロジックを適応させることが可能になっています。 わかりやすい例としてそもそものuPaletteが実装するGraphicに色をつけるコンポーネントの実装とその元となるクラスを示します。

// 何かしらにuPaletteが作用するための大元のクラス
public abstract class ValueSynchronizerBase<T> : MonoBehaviour
{
        
    public abstract EntryId EntryId { get; }

    protected virtual void OnEnable()
    {
        StartObserving();
    }

    protected virtual void OnDisable()
    {
        StopObserving();
    }

    internal abstract Palette<T> GetPalette(PaletteStore store);

    protected abstract void OnValueChanged(T value);
}

// ValueSynchronizerBase<T>を継承した色の反映クラス
public abstract class ColorSynchronizer : ValueSynchronizer<UnityEngine.Color>
{
    [SerializeField] private ColorEntryId _entryId = new ColorEntryId();

    public override EntryId EntryId => _entryId;

    internal override Palette<UnityEngine.Color> GetPalette(PaletteStore store)
    {
        return store.ColorPalette;
    }
}

public abstract class ColorSynchronizer<T> : ColorSynchronizer where T : Component
{
    [SerializeField] [HideInInspector] private T _component;

    protected T Component
    {
        get
        {
            if (_component == null)
            {
                _component = GetComponent<T>();
            }

            return _component;
        }
    }
}

// Graphicを対象に色の反映を行うクラス
// Attributeはエディタ拡張で色を設定する際に対応するValueSynchronizerを見つけるために必要
[ColorSynchronizer(typeof(Graphic), "Color")]
public sealed class GraphicColorSynchronizer : ColorSynchronizer<Graphic>
{
    protected internal override UnityEngine.Color GetValue()
    {
        return Component.color;
    }

    protected internal override void SetValue(UnityEngine.Color value)
    {
        Component.color = value;
    }

    protected override bool EqualsToCurrentValue(UnityEngine.Color value)
    {
        return Component.color == value;
    }
}

独自のコンポーネントに色をいい感じにつける場合にはColorSynchronizerを継承した独自クラスを定義し、反映部分のロジックだけ実装すればいいわけです。 このように、uPaletteはシステム的にエンジニアが使いやすいように作られている点も個人的な推しポイントです。

ver2.0.0でさらに便利になったポイント

uPaletteは最近2.0としてメジャーアップデートがありました。実は1.0では「こうなると嬉しいな〜」と思っていた点があったのですが、いい感じに改修していただけました。 改修された点についてもいくつか紹介します。

enumが自動で生成されるように

前述にもあるように、uPaletteではstringのidをキーとして色を管理します。エンジニアとしてはstringのidをそのままコードに使うよりは何かしらの識別子が欲しいものです。 筆者も1.0の頃は独自のenumを定義しており、色の設定更新にともなってよしなにuPaletteが作るようにならないかなと思っていました。 2.0からは色の設定を更新すると、次のようなenumと付随する実装が生成されます。

namespace uPalette.Generated
{
    public enum ColorTheme
    {
        Default,
    }

    public static class ColorThemeExtensions
    {
        public static string ToThemeId(this ColorTheme theme)
        {
            switch (theme)
            {
                case ColorTheme.Default:
                    return "ef6ad2f2-968e-4e08-b17d-45be08828273";
                default:
                    throw new ArgumentOutOfRangeException(nameof(theme), theme, null);
            }
        }
    }

    public enum ColorEntry
    {
        ColorName1,
        ColorName2,
    }

    public static class ColorEntryExtensions
    {
        public static string ToEntryId(this ColorEntry entry)
        {
            switch (entry)
            {
                case ColorEntry.ColorName1:
                    return "1cb426b2-2b90-4234-b637-3bee8c7d3157";
                case ColorEntry.ColorName2:
                    return "c7c6fc92-47c0-4ff1-b0ec-4b507b25a97e";
                default:
                    throw new ArgumentOutOfRangeException(nameof(entry), entry, null);
            }
        }
    }
...

enumだけでなく、拡張メソッドとしてidへの変換まで用意してくれるのは親切です。 生成するかどうかの設定や生成先はProject Settingsから調整可能です。

Gradationやテキストのスタイルにも対応

色だけではなく、GradationやテキストのスタイルまでuPalette上で管理できるようになりました。

色同様にUIを量産する上で一元管理したい要素なのでこれもありがたい更新です。

一連の色の設定をまとめたThemeという単位で管理が可能に

個々の色の設定だけでなく、一定の設定をまとめてテーマという単位で取り扱うことも可能になりました。 こちらは公式のデモを見るのが一番わかりやすいです。

色はもちろん、文字のスタイルなどもテーマごとに一括で切り替わっているのがわかるかと思います。 たとえば画面ごとに全体の雰囲気を変える必要があるようなケースだったり、設定でダークモードなどの色のテーマを選択できるような機能を作りたい場合にはかなり重宝する機能です。

おわりに

uPaletteを活用することで、色の一元管理を行う機能をさくっとプロジェクトに導入することができます。 独自でこの辺りの機能を作るのが億劫なみなさまはぜひ一度試してみて欲しいです。

引用

github.com