跳到正文
cccskills
免费在 GitHub 公开

tools-unity-ugui

Unity UI patterns including Canvas optimization, list virtualization, and mobile-friendly UI.

中文简介正在准备中。当前显示原文说明。

查看安装方法

包含的文件(1)

  • SKILL.md12.9 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Unity uGUI

Overview

Unity's uGUI system provides flexible UI but requires careful optimization to maintain performance, especially on mobile devices.

When to Use

  • Game menus and HUD
  • Inventory and shop screens
  • Scrolling lists
  • Modal dialogs
  • Health bars and floating text

Canvas Optimization

Canvas Splitting

// WRONG: Everything on one canvas
// Every change causes full rebuild

// RIGHT: Split by update frequency
public class CanvasOrganizer : MonoBehaviour
{
    [Header("Static UI - Never changes")]
    [SerializeField] private Canvas _staticCanvas;  // Background, frames
    
    [Header("Semi-Dynamic - Changes occasionally")]
    [SerializeField] private Canvas _dynamicCanvas; // Buttons, icons
    
    [Header("Highly Dynamic - Changes every frame")]
    [SerializeField] private Canvas _hudCanvas;     // Health, mana, timers
    
    private void Start()
    {
        // Static canvas doesn't need updates
        _staticCanvas.GetComponent<GraphicRaycaster>().enabled = false;
    }
}

Canvas Group for Visibility

public class UIPanel : MonoBehaviour
{
    private CanvasGroup _canvasGroup;
    
    private void Awake()
    {
        _canvasGroup = GetComponent<CanvasGroup>();
    }
    
    // BETTER than SetActive - doesn't rebuild canvas
    public void Show()
    {
        _canvasGroup.alpha = 1f;
        _canvasGroup.interactable = true;
        _canvasGroup.blocksRaycasts = true;
    }
    
    public void Hide()
    {
        _canvasGroup.alpha = 0f;
        _canvasGroup.interactable = false;
        _canvasGroup.blocksRaycasts = false;
    }
    
    public async UniTask FadeIn(float duration)
    {
        _canvasGroup.interactable = true;
        _canvasGroup.blocksRaycasts = true;
        
        float elapsed = 0;
        while (elapsed < duration)
        {
            elapsed += Time.unscaledDeltaTime;
            _canvasGroup.alpha = elapsed / duration;
            await UniTask.Yield();
        }
        _canvasGroup.alpha = 1f;
    }
}

Raycast Optimization

public class RaycastOptimizer : MonoBehaviour
{
    // Disable Raycast Target on non-interactive elements
    public void OptimizeHierarchy()
    {
        var graphics = GetComponentsInChildren<Graphic>(true);
        
        foreach (var graphic in graphics)
        {
            // Only enable on actually clickable items
            bool isInteractive = 
                graphic.GetComponent<Button>() != null ||
                graphic.GetComponent<Toggle>() != null ||
                graphic.GetComponent<Slider>() != null ||
                graphic.GetComponent<ScrollRect>() != null;
            
            graphic.raycastTarget = isInteractive;
        }
    }
}

List Virtualization

Simple Virtual List

public class VirtualizedList<TData, TView> : MonoBehaviour 
    where TView : Component
{
    [SerializeField] private ScrollRect _scrollRect;
    [SerializeField] private RectTransform _content;
    [SerializeField] private TView _itemPrefab;
    [SerializeField] private float _itemHeight = 100f;
    
    private readonly List<TView> _visibleItems = new();
    private readonly Queue<TView> _recycledItems = new();
    private IList<TData> _dataSource;
    private Action<TView, TData> _bindAction;
    
    private int _firstVisibleIndex;
    private float _viewportHeight;
    
    public void Initialize(IList<TData> data, Action<TView, TData> bindAction)
    {
        _dataSource = data;
        _bindAction = bindAction;
        
        // Set content height
        _content.sizeDelta = new Vector2(
            _content.sizeDelta.x,
            data.Count * _itemHeight
        );
        
        _viewportHeight = _scrollRect.viewport.rect.height;
        
        _scrollRect.onValueChanged.AddListener(OnScroll);
        
        RefreshVisibleItems();
    }
    
    private void OnScroll(Vector2 _)
    {
        RefreshVisibleItems();
    }
    
    private void RefreshVisibleItems()
    {
        float scrollY = _content.anchoredPosition.y;
        
        int newFirstVisible = Mathf.Max(0, Mathf.FloorToInt(scrollY / _itemHeight));
        int visibleCount = Mathf.CeilToInt(_viewportHeight / _itemHeight) + 2;
        int lastVisible = Mathf.Min(newFirstVisible + visibleCount, _dataSource.Count);
        
        // Recycle items that scrolled out
        for (int i = _visibleItems.Count - 1; i >= 0; i--)
        {
            var item = _visibleItems[i];
            int itemIndex = GetItemIndex(item);
            
            if (itemIndex < newFirstVisible || itemIndex >= lastVisible)
            {
                RecycleItem(item);
                _visibleItems.RemoveAt(i);
            }
        }
        
        // Create/reuse items that scrolled in
        for (int i = newFirstVisible; i < lastVisible; i++)
        {
            if (!IsItemVisible(i))
            {
                var item = GetOrCreateItem();
                PositionItem(item, i);
                _bindAction(item, _dataSource[i]);
                _visibleItems.Add(item);
            }
        }
        
        _firstVisibleIndex = newFirstVisible;
    }
    
    private TView GetOrCreateItem()
    {
        if (_recycledItems.Count > 0)
        {
            var item = _recycledItems.Dequeue();
            item.gameObject.SetActive(true);
            return item;
        }
        
        return Instantiate(_itemPrefab, _content);
    }
    
    private void RecycleItem(TView item)
    {
        item.gameObject.SetActive(false);
        _recycledItems.Enqueue(item);
    }
    
    private void PositionItem(TView item, int index)
    {
        var rect = item.GetComponent<RectTransform>();
        rect.anchoredPosition = new Vector2(0, -index * _itemHeight);
        item.gameObject.name = $"Item_{index}";
    }
    
    private bool IsItemVisible(int index)
    {
        return _visibleItems.Any(v => GetItemIndex(v) == index);
    }
    
    private int GetItemIndex(TView item)
    {
        var rect = item.GetComponent<RectTransform>();
        return Mathf.RoundToInt(-rect.anchoredPosition.y / _itemHeight);
    }
}

Usage Example

public class InventoryScreen : MonoBehaviour
{
    [SerializeField] private VirtualizedList<ItemData, InventorySlot> _list;
    
    public void ShowInventory(List<ItemData> items)
    {
        _list.Initialize(items, BindSlot);
    }
    
    private void BindSlot(InventorySlot slot, ItemData data)
    {
        slot.SetIcon(data.Icon);
        slot.SetName(data.Name);
        slot.SetQuantity(data.Quantity);
        slot.OnClick = () => OnItemClicked(data);
    }
}

Safe Text Updates

Cached Text Component

public class SafeText : MonoBehaviour
{
    private TMP_Text _text;
    private string _lastValue;
    
    private void Awake()
    {
        _text = GetComponent<TMP_Text>();
        _lastValue = _text.text;
    }
    
    // Only update if value changed
    public void SetText(string value)
    {
        if (_lastValue != value)
        {
            _text.text = value;
            _lastValue = value;
        }
    }
    
    public void SetTextFormat(string format, params object[] args)
    {
        var value = string.Format(format, args);
        SetText(value);
    }
}

Zero-Allocation Number Display

public class NumberDisplay : MonoBehaviour
{
    private TMP_Text _text;
    private int _lastValue = int.MinValue;
    private readonly char[] _buffer = new char[16];
    
    private void Awake()
    {
        _text = GetComponent<TMP_Text>();
    }
    
    public void SetValue(int value)
    {
        if (value == _lastValue) return;
        _lastValue = value;
        
        // Zero-allocation int to string
        int length = IntToChars(value, _buffer);
        _text.SetCharArray(_buffer, 0, length);
    }
    
    private int IntToChars(int value, char[] buffer)
    {
        if (value == 0)
        {
            buffer[0] = '0';
            return 1;
        }
        
        bool negative = value < 0;
        if (negative) value = -value;
        
        int index = buffer.Length;
        
        while (value > 0)
        {
            buffer[--index] = (char)('0' + value % 10);
            value /= 10;
        }
        
        if (negative)
        {
            buffer[--index] = '-';
        }
        
        int length = buffer.Length - index;
        Array.Copy(buffer, index, buffer, 0, length);
        return length;
    }
}

Layout Optimization

Manual Layout

public class OptimizedGrid : MonoBehaviour
{
    [SerializeField] private int _columns = 4;
    [SerializeField] private float _cellWidth = 100f;
    [SerializeField] private float _cellHeight = 100f;
    [SerializeField] private float _spacing = 10f;
    
    private RectTransform _rect;
    
    private void Awake()
    {
        _rect = GetComponent<RectTransform>();
    }
    
    // Call once after adding/removing children
    public void RefreshLayout()
    {
        int index = 0;
        
        foreach (Transform child in transform)
        {
            if (!child.gameObject.activeSelf) continue;
            
            int row = index / _columns;
            int col = index % _columns;
            
            var childRect = child.GetComponent<RectTransform>();
            childRect.anchoredPosition = new Vector2(
                col * (_cellWidth + _spacing),
                -row * (_cellHeight + _spacing)
            );
            childRect.sizeDelta = new Vector2(_cellWidth, _cellHeight);
            
            index++;
        }
        
        // Update content size
        int totalRows = Mathf.CeilToInt((float)index / _columns);
        _rect.sizeDelta = new Vector2(
            _rect.sizeDelta.x,
            totalRows * (_cellHeight + _spacing)
        );
    }
}

Disable Auto Layout

public class LayoutDisabler : MonoBehaviour
{
    private LayoutGroup _layout;
    private ContentSizeFitter _sizeFitter;
    
    private void Start()
    {
        _layout = GetComponent<LayoutGroup>();
        _sizeFitter = GetComponent<ContentSizeFitter>();
        
        // Disable after initial layout
        StartCoroutine(DisableAfterFrame());
    }
    
    private IEnumerator DisableAfterFrame()
    {
        yield return null;
        
        if (_sizeFitter != null)
            _sizeFitter.enabled = false;
            
        if (_layout != null)
            _layout.enabled = false;
    }
    
    public void ForceRefresh()
    {
        if (_layout != null)
        {
            _layout.enabled = true;
            LayoutRebuilder.ForceRebuildLayoutImmediate(GetComponent<RectTransform>());
            _layout.enabled = false;
        }
    }
}

Modal Dialog System

Dialog Manager

public class DialogManager : MonoBehaviour
{
    public static DialogManager Instance { get; private set; }
    
    [SerializeField] private Canvas _dialogCanvas;
    [SerializeField] private GameObject _backdrop;
    
    private readonly Stack<UIPanel> _dialogStack = new();
    
    private void Awake()
    {
        Instance = this;
    }
    
    public async UniTask<T> ShowDialog<T>(UIPanel dialogPrefab) where T : UIPanel
    {
        var dialog = Instantiate(dialogPrefab, _dialogCanvas.transform);
        _dialogStack.Push(dialog);
        
        UpdateBackdrop();
        await dialog.FadeIn(0.2f);
        
        return (T)dialog;
    }
    
    public async UniTask CloseTopDialog()
    {
        if (_dialogStack.Count == 0) return;
        
        var dialog = _dialogStack.Pop();
        await dialog.FadeOut(0.15f);
        Destroy(dialog.gameObject);
        
        UpdateBackdrop();
    }
    
    public async UniTask CloseAllDialogs()
    {
        while (_dialogStack.Count > 0)
        {
            var dialog = _dialogStack.Pop();
            Destroy(dialog.gameObject);
        }
        
        UpdateBackdrop();
    }
    
    private void UpdateBackdrop()
    {
        _backdrop.SetActive(_dialogStack.Count > 0);
    }
}

Best Practices

  1. Split canvases by update frequency
  2. Disable Raycast Target on non-interactive elements
  3. Use CanvasGroup for visibility, not SetActive
  4. Virtualize long lists - Only render visible items
  5. Avoid Layout Groups in dynamic content
  6. Cache text components and avoid redundant updates
  7. Pool UI elements - Don't instantiate frequently
  8. Use sprite atlases for UI images
  9. Avoid transparency overdraw - Minimize overlapping alpha
  10. Profile with Frame Debugger - Check batch counts

Troubleshooting

IssueSolution
High SetPass callsUse sprite atlases
Canvas rebuild every frameSplit canvases, check hierarchy
Scroll stutteringVirtualize list
Touch unresponsiveCheck Raycast Target, Canvas order
Layout jitterDisable auto layout, use manual
Memory from UIPool elements, unload unused atlases

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

adopt

免费

Brownfield onboarding — audits existing project artifacts for template format compliance (not just existence), classifies gaps by impact, and produces a numbered migration plan. Run this when joining an in-progress project or upgrading from an older template version. Distinct from /project-stage-detect (which checks what exists) — this checks whether what exists will actually work with the template's skills.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Creates an Architecture Decision Record (ADR) documenting a significant technical decision, its context, alternatives considered, and consequences. Every major technical choice should have an ADR.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Validates completeness and consistency of the project architecture against all GDDs. Builds a traceability matrix mapping every GDD technical requirement to ADRs, identifies coverage gaps, detects cross-ADR conflicts, verifies engine compatibility consistency across all decisions, and produces a PASS/CONCERNS/FAIL verdict. The architecture equivalent of /design-review.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

art-bible

免费

Guided, section-by-section Art Bible authoring. Creates the visual identity specification that gates all asset production. Run after /brainstorm is approved and before /map-systems or any GDD authoring begins.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Audits game assets for compliance with naming conventions, file size budgets, format standards, and pipeline requirements. Identifies orphaned assets, missing references, and standard violations.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Generate per-asset visual specifications and AI generation prompts from GDDs, level docs, or character profiles. Produces structured spec files and updates the master asset manifest. Run after art bible and GDD/level design are approved, before production begins.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

查看 IdoCohen560 的全部技能

报告此技能的问题