Install
$ agentstack add skill-tjboudreaux-cc-plugin-unity-gamedev-tools-unity-ugui ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
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().enabled = false;
}
}
Canvas Group for Visibility
public class UIPanel : MonoBehaviour
{
private CanvasGroup _canvasGroup;
private void Awake()
{
_canvasGroup = GetComponent();
}
// 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 (true);
foreach (var graphic in graphics)
{
// Only enable on actually clickable items
bool isInteractive =
graphic.GetComponent() != null ||
graphic.GetComponent() != null ||
graphic.GetComponent() != null ||
graphic.GetComponent() != null;
graphic.raycastTarget = isInteractive;
}
}
}
List Virtualization
Simple Virtual List
public class VirtualizedList : MonoBehaviour
where TView : Component
{
[SerializeField] private ScrollRect _scrollRect;
[SerializeField] private RectTransform _content;
[SerializeField] private TView _itemPrefab;
[SerializeField] private float _itemHeight = 100f;
private readonly List _visibleItems = new();
private readonly Queue _recycledItems = new();
private IList _dataSource;
private Action _bindAction;
private int _firstVisibleIndex;
private float _viewportHeight;
public void Initialize(IList data, Action 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 = lastVisible)
{
RecycleItem(item);
_visibleItems.RemoveAt(i);
}
}
// Create/reuse items that scrolled in
for (int i = newFirstVisible; i 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();
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();
return Mathf.RoundToInt(-rect.anchoredPosition.y / _itemHeight);
}
}
Usage Example
public class InventoryScreen : MonoBehaviour
{
[SerializeField] private VirtualizedList _list;
public void ShowInventory(List 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();
_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();
}
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)
{
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();
}
// 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();
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();
_sizeFitter = GetComponent();
// 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());
_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 _dialogStack = new();
private void Awake()
{
Instance = this;
}
public async UniTask ShowDialog(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
- Split canvases by update frequency
- Disable Raycast Target on non-interactive elements
- Use CanvasGroup for visibility, not SetActive
- Virtualize long lists - Only render visible items
- Avoid Layout Groups in dynamic content
- Cache text components and avoid redundant updates
- Pool UI elements - Don't instantiate frequently
- Use sprite atlases for UI images
- Avoid transparency overdraw - Minimize overlapping alpha
- Profile with Frame Debugger - Check batch counts
Troubleshooting
| Issue | Solution | |-------|----------| | High SetPass calls | Use sprite atlases | | Canvas rebuild every frame | Split canvases, check hierarchy | | Scroll stuttering | Virtualize list | | Touch unresponsive | Check Raycast Target, Canvas order | | Layout jitter | Disable auto layout, use manual | | Memory from UI | Pool elements, unload unused atlases |
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: tjboudreaux
- Source: tjboudreaux/cc-plugin-unity-gamedev
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.