mcpField
U
unity-ui-patterns

unity-ui-patterns

github.com/nice-wolf-studioデザイン・UI4065 回インストール6 週間前に更新
リポジトリ

unity-ui-patterns について

unity-ui-patternsは、UI Toolkitを使ったUnityのランタイムUI設計スキルです。画面遷移、リアクティブなビュー、HUD、フィードバック、リスト、トランジションのパターンを示し、UIの動作とデザインを分離します。

主な機能

  1. 01スタック型の画面ナビゲーション
  2. 02ViewとViewModelの分離
  3. 03イベント駆動のHUDウィジェット
  4. 04ScriptableObjectによるUIフィードバック
  5. 05ListView要素の再利用
  6. 06再利用可能な画面遷移契約

活用シーン

  1. 01ゲーム画面間のナビゲーションを整理
  2. 02ゲームデータの変化に応じてUIを更新
  3. 03イベントに反応するHUD要素を構築
  4. 04大規模なインベントリやショップ一覧を表示
  5. 05デザイナー向けに画面アニメーションを設定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/nice-wolf-studio/unity-claude-skills --skill unity-ui-patterns

インストール後、Claude Code で/unity-ui-patternsを入力して読み込まれたか確認します。

同梱ファイルreferences/ui-pattern-scaffolds.md

詳細

UI/UX パターン――デザイン翻訳パターン

前提スキル: unity-ui(UI Toolkit API、USS/UXML、データバインディング)、unity-async-patterns(非同期トランジション、キャンセル)、unity-game-architecture(イベント、サービスロケーター)

Claude はデータ取得、表示、アニメーションを混在させたモノリシックなスクリプトとして UI を構築します。画面が 1 つなら問題ありませんが、デザイナーがトランジション、動的リスト、複数の画面を追加すると保守が困難になります。これらのパターンでは構造と動作を分離し、画面を組み合わせ可能にして、演出をデザイナーが設定できるようにします。


パターン:画面フローアーキテクチャ

設計意図:メインメニュー、設定、インベントリ、HUD、一時停止メニューなど複数の画面があり、画面遷移と戻るボタンに対応するゲームを対象とします。

誤った例:

// One MonoBehaviour per screen with direct references -- breaks on every new screen
public class MainMenu : MonoBehaviour
{
    [SerializeField] private GameObject settingsPanel;
    [SerializeField] private GameObject inventoryPanel;

    public void OnSettingsClicked()
    {
        gameObject.SetActive(false);
        settingsPanel.SetActive(true); // Direct coupling
    }

    public void OnBackFromSettings()
    {
        settingsPanel.SetActive(false);
        gameObject.SetActive(true); // Every screen knows every other screen
    }
}

適切な例:スタックベースのナビゲーション(プッシュ/ポップ)を備えた ScreenManager を使用します。各画面は IScreen を実装し、ShowAsync/HideAsync を備えます。画面は UIDocument を介して UXML テンプレートから読み込みます。ナビゲーションは画面のロジックから分離されており、画面同士が互いを参照することはありません。

スキャフォールド:

using System;
using System.Threading;
using UnityEngine;
using UnityEngine.UIElements;

/// <summary>
/// Contract for all navigable screens.
/// </summary>
public interface IScreen
{
    /// <summary>Display name used for debugging and stack inspection.</summary>
    string ScreenName { get; }

    /// <summary>Transition the screen into view.</summary>
    Awaitable ShowAsync(CancellationToken ct);

    /// <summary>Transition the screen out of view.</summary>
    Awaitable HideAsync(CancellationToken ct);
}

/// <summary>
/// Stack-based screen navigator. Push screens to go forward, pop to go back.
/// </summary>
public class ScreenManager : MonoBehaviour
{
    [SerializeField] private UIDocument uiDocument;

    private readonly System.Collections.Generic.Stack<IScreen> _screenStack = new();
    private bool _isTransitioning;

    public static ScreenManager Instance { get; private set; }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void ResetStatic() => Instance = null;

    void Awake()
    {
        if (Instance != null) { Destroy(gameObject); return; }
        Instance = this;
    }

    /// <summary>Push a new screen onto the stack, hiding the current one.</summary>
    public async Awaitable PushAsync(IScreen screen)
    {
        if (_isTransitioning) return;
        _isTransitioning = true;

        var ct = destroyCancellationToken;

        if (_screenStack.TryPeek(out var current))
        {
            await current.HideAsync(ct);
        }

        _screenStack.Push(screen);
        await screen.ShowAsync(ct);

        _isTransitioning = false;
    }

    /// <summary>Pop the current screen and reveal the one beneath it.</summary>
    public async Awaitable PopAsync()
    {
        if (_isTransitioning || _screenStack.Count <= 1) return;
        _isTransitioning = true;

        var ct = destroyCancellationToken;
        var leaving = _screenStack.Pop();
        await leaving.HideAsync(ct);

        if (_screenStack.TryPeek(out var revealed))
        {
            await revealed.ShowAsync(ct);
        }

        _isTransitioning = false;
    }
}

デザイン拡張ポイント:新しい画面を追加するには、IScreen と UXML テンプレートを実装します。ナビゲーションには ScreenManager.PushAsync(screen)/PopAsync() を使用します。画面同士が互いについて知る必要はありません。

注意点:画面スタックでは、同じ画面を 2 回プッシュする、最後の画面をポップする、トランジション中にプッシュするといったエッジケースに対応する必要があります。スキャフォールドでは _isTransitioning を使ってトランジションの重複を防ぎます。プッシュの重複を防ぐには、プッシュ前に _screenStack.Peek() != screen を確認してください。


パターン:ビュー/ビューモデルの分離

設計意図:UI が複数のシステム(ヘルス、インベントリ、クエスト)のデータを表示し、リアクティブに更新する場合を対象とします。

誤った例:

// UI script queries game systems directly every frame
public class InventoryUI : MonoBehaviour
{
    [SerializeField] private UIDocument doc;

    void Update()
    {
        // Polling every frame, tightly coupled to game internals
        var root = doc.rootVisualElement;
        var items = GameManager.Instance.Player.Inventory.Items;
        for (int i = 0; i < items.Count; i++)
        {
            root.Q<Label>($"item-{i}").text = items[i].Name;
            root.Q<Label>($"count-{i}").text = items[i].Count.ToString();
        }
    }
}

適切な例:ビューモデル(通常の C# クラス)がゲームデータを表示用データに変換し、変更時に C# イベントを公開します。ビューはビジュアル要素をビューモデルのプロパティにバインドします。ビューモデルはビジュアル要素に依存しません。ゲームシステムがビューモデルを更新し、ビューがそれに反応します。

スキャフォールド:

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

/// <summary>
/// ViewModel adapting inventory data for display.
/// No dependency on UnityEngine.UIElements.
/// </summary>
public class InventoryViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _selectedItemName = "";
    private int _selectedItemCount;
    private int _totalItems;

    /// <summary>Name of the currently selected item.</summary>
    public string SelectedItemName
    {
        get => _selectedItemName;
        set { _selectedItemName = value; OnPropertyChanged(); }
    }

    /// <summary>Stack count of the selected item.</summary>
    public int SelectedItemCount
    {
        get => _selectedItemCount;
        set { _selectedItemCount = value; OnPropertyChanged(); }
    }

    /// <summary>Total number of distinct items in inventory.</summary>
    public int TotalItems
    {
        get => _totalItems;
        set { _totalItems = value; OnPropertyChanged(); }
    }

    private void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}
using UnityEngine;
using UnityEngine.UIElements;

/// <summary>
/// View that binds VisualElements to InventoryViewModel properties.
/// </summary>
public class InventoryView : MonoBehaviour
{
    [SerializeField] private UIDocument uiDocument;

    private InventoryViewModel _viewModel;
    private Label _itemNameLabel;
    private Label _itemCountLabel;
    private Label _totalLabel;

    /// <summary>Bind the view to a ViewModel instance.</summary>
    public void Bind(InventoryViewModel viewModel)
    {
        if (_viewModel != null)
            _viewModel.PropertyChanged -= OnPropertyChanged;

        _viewModel = viewModel;
        _viewModel.PropertyChanged += OnPropertyChanged;

        var root = uiDocument.rootVisualElement;
        _itemNameLabel = root.Q<Label>("item-name");
        _itemCountLabel = root.Q<Label>("item-count");
        _totalLabel = root.Q<Label>("total-items");

        RefreshAll();
    }

    private void OnPropertyChanged(object sender,
        System.ComponentModel.PropertyChangedEventArgs e)
    {
        switch (e.PropertyName)
        {
            case nameof(InventoryViewModel.SelectedItemName):
                _itemNameLabel.text = _viewModel.SelectedItemName;
                break;
            case nameof(InventoryViewModel.SelectedItemCount):
                _itemCountLabel.text = _viewModel.SelectedItemCount.ToString();
                break;
            case nameof(InventoryViewModel.TotalItems):
                _totalLabel.text = _viewModel.TotalItems.ToString();
                break;
        }
    }

    private void RefreshAll()
    {
        _itemNameLabel.text = _viewModel.SelectedItemName;
        _itemCountLabel.text = _viewModel.SelectedItemCount.ToString();
        _totalLabel.text = _viewModel.TotalItems.ToString();
    }

    void OnDestroy()
    {
        if (_viewModel != null)
            _viewModel.PropertyChanged -= OnPropertyChanged;
    }
}

デザイン拡張ポイント:新しいデータソースを追加するには、ゲームシステムを変換する新しいビューモデルを作成します。ビューはゲーム内部の知識を持たずにバインドできます。ビューモデルは Unity を使わずにテストできます。

注意点:ゲームプレイ UI には Unity の組み込みデータバインディングを使わないでください。これはエディターのツール向けに設計されており、実行時のゲーム UI には適さないオーバーヘッドや制限があります。制御性とデバッグのしやすさを確保するには、明示的な C# イベントサブスクリプションを使用してください。


パターン:HUD アーキテクチャ

設計意図:ヘルスバー、弾薬カウンター、ミニマップのマーカー、照準などの HUD 要素がゲームプレイ中に常時表示され、異なる頻度で更新される場合を対象とします。

誤った例:

// All HUD logic in one script, polling every frame
public class HUD : MonoBehaviour
{
    [SerializeField] private UIDocument doc;

    void Update()
    {
        var root = doc.rootVisualElement;
        root.Q<ProgressBar>("health-bar").value = Player.Instance.Health;
        root.Q<Label>("ammo-count").text = Player.Instance.Ammo.ToString();
        root.Q<Label>("score").text = ScoreManager.Instance.Score.ToString();
        // Everything updates every frame whether it changed or not
    }
}

適切な例:各 HUD ウィジェットは IHudWidget を実装し、Initialize/Dispose のライフサイクルを備えます。ウィジェットはゲームイベント(unity-game-architecture の SO チャンネル)を購読し、変更時にのみ自らを更新します。HudController はウィジェットの登録とライフサイクルを管理します。

スキャフォールド:

using System;
using UnityEngine.UIElements;

/// <summary>
/// Contract for HUD widgets. Each widget owns a section of the HUD.
/// </summary>
public interface IHudWidget : IDisposable
{
    /// <summary>Bind to VisualElements and subscribe to game events.</summary>
    void Initialize(VisualElement hudRoot);
}
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

/// <summary>
/// Manages HUD widget lifecycle. Attach to the HUD UIDocument GameObject.
/// </summary>
public class HudController : MonoBehaviour
{
    [SerializeField] private UIDocument hudDocument;

    private readonly List<IHudWidget> _widgets = new();

    /// <summary>Register and initialize a widget.</summary>
    public void RegisterWidget(IHudWidget widget)
    {
        widget.Initialize(hudDocument.rootVisualElement);
        _widgets.Add(widget);
    }

    void OnDestroy()
    {
        foreach (var widget in _widgets)
            widget.Dispose();
        _widgets.Clear();
    }
}
using UnityEngine;
using UnityEngine.UIElements;

/// <summary>
/// Health bar widget. Updates only when health changes via event subscription.
/// </summary>
public class HealthBarWidget : IHudWidget
{
    // ScriptableObject event channel (see unity-game-architecture)
    private readonly FloatEventChannel _healthChanged;
    private ProgressBar _healthBar;

    public HealthBarWidget(FloatEventChannel healthChanged)
    {
        _healthChanged = healthChanged;
    }

    public void Initialize(VisualElement hudRoot)
    {
        _healthBar = hudRoot.Q<ProgressBar>("health-bar");
        _healthChanged.OnEventRaised += UpdateHealth;
    }

    private void UpdateHealth(float normalizedHealth)
    {
        _healthBar.value = normalizedHealth * 100f;
    }

    public void Dispose()
    {
        _healthChanged.OnEventRaised -= UpdateHealth;
    }
}

デザイン拡張ポイント:新しいウィジェットを追加するには、IHudWidget を実装し、HUD の UXML に要素を追加して、HudController に登録します。ウィジェットは自己完結していてテスト可能であり、データが変更されたときだけ更新されます。

注意点:HUD のルートには、ゲームプレイ画面とは別の UIDocument を使用する必要があります。UIDocument コンポーネントの sortingOrder でレイヤー順を制御します。HUD が画面 UI より前面に描画されるよう、HUD の sortingOrder は画面 UI より大きい値にしてください。1 つのシーン内に複数の UIDocument コンポーネントを共存させることができます。

パターン:フィードバック&演出システム

設計意図:UI がゲームイベントに視覚的なフィードバック(ダメージ数値、画面揺れ、色のフラッシュ、拡大縮小の強調)で反応します。

誤った例:

// Hardcoded feedback inline in combat logic
public class CombatManager : MonoBehaviour
{
    [SerializeField] private UIDocument hudDoc;

    public void ApplyDamage(float amount)
    {
        // Combat script directly drives UI -- tight coupling
        var healthBar = hudDoc.rootVisualElement.Q("health-bar");
        healthBar.style.backgroundColor = Color.red;
        // No way to revert, no timing, no designer control
    }
}

正しい例:UIFeedbackPlayer コンポーネントと UIFeedback SO アセットを使用します。ゲームコードがイベントを発生させ、フィードバックシステムがそれを購読して視覚効果を適用します。フィードバックのシーケンス:USS クラスを追加し、USS の transition-* プロパティでトランジションを再生し、一定時間後に USS クラスを削除します。

ひな形:

using UnityEngine;

/// <summary>
/// Designer-configurable UI feedback effect.
/// </summary>
[CreateAssetMenu(menuName = "UI/Feedback Effect")]
public class UIFeedback : ScriptableObject
{
    [Tooltip("USS class to add that triggers the visual effect")]
    public string ussClassName = "feedback--damage";

    [Tooltip("Duration in seconds before removing the USS class")]
    public float duration = 0.3f;

    [Tooltip("Optional: target element name. Null = apply to root.")]
    public string targetElementName;
}
using System.Threading;
using UnityEngine;
using UnityEngine.UIElements;

/// <summary>
/// Plays UIFeedback effects on a UIDocument's visual tree.
/// </summary>
public class UIFeedbackPlayer : MonoBehaviour
{
    [SerializeField] private UIDocument uiDocument;

    /// <summary>Play a feedback effect, adding then removing a USS class.</summary>
    public async Awaitable PlayAsync(UIFeedback feedback)
    {
        var ct = destroyCancellationToken;
        var root = uiDocument.rootVisualElement;

        var target = string.IsNullOrEmpty(feedback.targetElementName)
            ? root
            : root.Q(feedback.targetElementName);

        if (target == null) return;

        // Add USS class -- USS transition-* properties handle the animation
        target.AddToClassList(feedback.ussClassName);

        await Awaitable.WaitForSecondsAsync(feedback.duration, ct);

        // Remove USS class -- transition plays in reverse
        target.RemoveFromClassList(feedback.ussClassName);
    }
}

ダメージフラッシュの USS の例:

/* Base state */
.health-bar {
    transition-property: background-color, scale;
    transition-duration: 0.15s;
    transition-timing-function: ease-out;
    background-color: rgb(50, 180, 50);
    scale: 1 1;
}

/* Feedback state -- applied by UIFeedbackPlayer */
.health-bar.feedback--damage {
    background-color: rgb(220, 40, 40);
    scale: 1.1 1.1;
}

設計上のポイント:デザイナーが UIFeedback SO を作成し、視覚効果用の USS クラスを割り当てます。新しい演出を追加する場合は、新しい SO と USS ルールを作成します。コードの変更は不要です。

注意点:USS のトランジションでは transition-duration と transition-timing-function を使用します。トランジションを再生するには、トランジションプロパティをベース状態(トリガーされるクラスではなく)に設定する必要があります。設定しないと、トランジションは再生されません。トリガーされるクラスでは、目標となるプロパティ値のみを変更します。


パターン:動的リスト/グリッドビュー

設計意図:インベントリ、ショップ、リーダーボードなど、データの長さが変わるスクロール可能なリストを効率よく描画します。

誤った例:

// Instantiate one VisualElement per data item
public void PopulateInventory(List<ItemData> items)
{
    var container = doc.rootVisualElement.Q("item-container");
    container.Clear();
    foreach (var item in items)
    {
        // 1000 items = 1000 elements in the visual tree -- laggy scroll
        var element = new VisualElement();
        element.Add(new Label(item.Name));
        element.Add(new Label(item.Count.ToString()));
        container.Add(element);
    }
}

正しい例:要素を再利用するため、makeItem/bindItem コールバックを使用した UI Toolkit の ListView を使います。データソースには List<T> または IList を使用します。アイテムのテンプレートには UXML を使います。表示中のアイテムのみがビジュアルツリーに存在します。

ひな形:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

/// <summary>
/// Sets up a ListView with element recycling for efficient scrolling.
/// </summary>
public class InventoryListView : MonoBehaviour
{
    [SerializeField] private UIDocument uiDocument;
    [SerializeField] private VisualTreeAsset itemTemplate;

    private ListView _listView;
    private List<ItemData> _items = new();

    void OnEnable()
    {
        _listView = uiDocument.rootVisualElement.Q<ListView>("inventory-list");

        _listView.makeItem = () => itemTemplate.Instantiate();

        _listView.bindItem = (element, index) =>
        {
            var item = _items[index];
            element.Q<Label>("item-name").text = item.Name;
            element.Q<Label>("item-count").text = item.Count.ToString();
        };

        _listView.unbindItem = (element, index) =>
        {
            // Clean up any subscriptions or dynamic content
            element.Q<Label>("item-name").text = "";
            element.Q<Label>("item-count").text = "";
        };

        _listView.itemsSource = _items;
        _listView.fixedItemHeight = 60f;
        _listView.selectionType = SelectionType.Single;

        _listView.selectionChanged += OnSelectionChanged;
    }

    /// <summary>Update the data source and refresh the list.</summary>
    public void SetItems(List<ItemData> items)
    {
        _items = items;
        _listView.itemsSource = _items;
        _listView.RefreshItems();
    }

    private void OnSelectionChanged(IEnumerable<object> selection)
    {
        foreach (var obj in selection)
        {
            if (obj is ItemData item)
                Debug.Log($"Selected: {item.Name}");
        }
    }

    void OnDisable()
    {
        _listView.selectionChanged -= OnSelectionChanged;
    }
}

設計上のポイント:アイテムの見た目には UXML テンプレートを使用し、データバインディングは bindItem コールバックで行います。新しい種類のリストにも、異なるデータとテンプレートを使用してこのパターンを再利用できます。デザイナーは UXML と USS でアイテムのレイアウトを調整できます。

注意点:データ変更後は ListView.RefreshItems()(Rebuild() ではありません)を呼び出します。Rebuild はすべての要素を一から再作成しますが、RefreshItems は再利用中の既存要素を再バインドします。Rebuild() はアイテムのテンプレート自体が変わった場合にのみ使用してください。


パターン:トランジションとアニメーションの契約

設計意図:画面トランジション(フェード、スライド、スケール)に一貫性を持たせ、デザイナーが設定できるようにします。

誤った例:

// Manual alpha lerps per screen, no standard contract
public async Awaitable ShowMainMenu(VisualElement root)
{
    root.style.opacity = 0;
    root.style.display = DisplayStyle.Flex;
    float t = 0;
    while (t < 0.3f)
    {
        t += Time.deltaTime;
        root.style.opacity = t / 0.3f;
        await Awaitable.NextFrameAsync();
    }
    // Every screen duplicates this. Duration/easing hardcoded. No designer control.
}

正しい例:PlayAsync(VisualElement target, CancellationToken ct) を持つ IScreenTransition インターフェースを使います。実装では USS トランジション(opacity、translate、scale)を使用します。トランジションは、再生時間とイージングの設定を持つ SO アセットです。ScreenManager は画面遷移の呼び出しごとにトランジションを受け取ります。

ひな形:

using System.Threading;
using UnityEngine.UIElements;

/// <summary>
/// Contract for screen transitions.
/// </summary>
public interface IScreenTransition
{
    /// <summary>Play the transition on the target element.</summary>
    Awaitable PlayAsync(VisualElement target, CancellationToken ct);
}
using System.Threading;
using UnityEngine;
using UnityEngine.UIElements;

/// <summary>
/// Fade transition using USS opacity. Designer-configurable duration.
/// </summary>
[CreateAssetMenu(menuName = "UI/Transitions/Fade")]
public class FadeTransition : ScriptableObject, IScreenTransition
{
    [Tooltip("Fade duration in seconds")]
    [SerializeField] private float duration = 0.3f;

    [Tooltip("True = fade in (0 to 1), False = fade out (1 to 0)")]
    [SerializeField] private bool fadeIn = true;

    public async Awaitable PlayAsync(VisualElement target, CancellationToken ct)
    {
        float startOpacity = fadeIn ? 0f : 1f;
        float endOpacity = fadeIn ? 1f : 0f;

        // Set initial state without transition
        target.style.transitionDuration = new List<TimeValue> { new(0, TimeUnit.Second) };
        target.style.opacity = startOpacity;

        // Wait one frame for the initial state to apply
        await Awaitable.NextFrameAsync(ct);

        // Set transition duration then animate
        target.style.transitionProperty = new List<StylePropertyName> { new("opacity") };
        target.style.transitionDuration = new List<TimeValue> { new(duration, TimeUnit.Second) };
        target.style.transitionTimingFunction =
            new List<EasingFunction> { new(EasingMode.EaseInOut) };
        target.style.opacity = endOpacity;

        await Awaitable.WaitForSecondsAsync(duration, ct);
    }
}
using System.Threading;
using UnityEngine;
using UnityEngine.UIElements;

/// <summary>
/// Slide transition using USS translate. Configurable direction and duration.
/// </summary>
[CreateAssetMenu(menuName = "UI/Transitions/Slide")]
public class SlideTransition : ScriptableObject, IScreenTransition
{
    [SerializeField] private float duration = 0.4f;
    [SerializeField] private bool slideIn = true;
    [SerializeField] private Vector2 offset = new(100f, 0f); // percentage

    public async Awaitable PlayAsync(VisualElement target, CancellationToken ct)
    {
        var start = slideIn
            ? new Translate(Length.Percent(offset.x), Length.Percent(offset.y))
            : new Translate(Length.Percent(0), Length.Percent(0));
        var end = slideIn
            ? new Translate(Length.Percent(0), Length.Percent(0))
            : new Translate(Length.Percent(offset.x), Length.Percent(offset.y));

        // Set initial state
        target.style.transitionDuration = new List<TimeValue> { new(0, TimeUnit.Second) };
        target.style.translate = new StyleTranslate(start);

        await Awaitable.NextFrameAsync(ct);

        // Animate
        target.style.transitionProperty = new List<StylePropertyName> { new("translate") };
        target.style.transitionDuration = new List<TimeValue> { new(duration, TimeUnit.Second) };
        target.style.transitionTimingFunction =
            new List<EasingFunction> { new(EasingMode.EaseOut) };
        target.style.translate = new StyleTranslate(end);

        await Awaitable.WaitForSecondsAsync(duration, ct);
    }
}

設計上のポイント:新しいトランジションは IScreenTransition を ScriptableObject として実装します。デザイナーは画面変更ごとにトランジション SO を選択できます。ScreenManager.PushAsync(screen, transition) で適用します。

注意点:USS の transition-property には、アニメーションさせるすべてのプロパティを含める必要があります。opacity と translate をアニメーションさせる場合は、両方を指定します。代替策として transition-property: all を使用できますが、アニメーションさせるつもりのないレイアウト変更を含め、すべてのプロパティ変更がアニメーションされる点に注意してください。


アンチパターンのまとめ

アンチパターン問題パターンによる解決策
巨大な画面スクリプトすべての画面がほかのすべての画面を把握しているため、画面を1つ追加するだけで5つのファイルが変わる画面フローアーキテクチャ — スタックベースの画面遷移
Update 内でのシステムの直接ポーリングサイクルの無駄遣い、UI とゲームロジックの密結合View/ViewModel — イベント駆動の更新
単一の HUD MonoBehaviour変更によってすべての HUD 要素が壊れ、すべてが毎フレームポーリングするHUD アーキテクチャ — イベント購読を行う独立したウィジェット
ゲームロジックへのフィードバックのハードコード戦闘スクリプトが UI を直接操作し、デザイナーが制御できないフィードバック&演出 — SO による USS クラス効果
要素の手動インスタンス化1000 個のアイテムで 1000 個の VisualElements が作成され、スクロールが遅くなる動的 ListView — makeItem/bindItem による再利用
画面ごとのアニメーションコードすべての画面でトランジションのロジックが重複し、一貫性がないトランジション契約 — SO ベースの IScreenTransition

関連するスキル

  • unity-ui -- UI Toolkit の基礎、USS/UXML の作成、VisualElement の階層構造
  • unity-async-patterns -- Awaitable、CancellationToken、非同期ライフサイクル管理
  • unity-game-architecture -- イベントチャネル、Service Locator、ScriptableObject のパターン
  • unity-scene-assets -- 画面ごとにシーンを分けるアーキテクチャのシーン読み込み

追加リソース

  • skills/unity-ui-patterns/references/ui-pattern-scaffolds.md -- すべてのパターンの完全な実装
  • Unity Manual:UI Toolkit のランタイムでの使用
  • Unity Manual:ListView と仮想化
  • Unity Manual:USS のトランジションとアニメーション

コメント