Every game needs a way to show the player numbers, buttons, health bars, inventory grids, and menus, drawn on top of or instead of the 3D or 2D game world. That layer is the UI (user interface). Unity gives you two different systems for building it: UGUI, the older, GameObject-based system you have already brushed against if you have ever dragged a Button into a scene, and UI Toolkit, a newer system that looks and works a lot like building a web page out of markup and stylesheets. This chapter covers both, plus the ideas that apply no matter which one you pick: how a UI is built as a tree of elements, how that tree scales cleanly from a phone screen to a 4K monitor, how to read clicks and drags without polling every frame, and how to keep UI code from turning into a tangle of direct references into your game state.
As always, every idea below comes with runnable C# and, where there is nothing to print to a console, a worked trace you can follow by hand with a pencil.
A Unity UI is not one big drawing. It is a tree (a hierarchy where each item has exactly one parent and any number of children) of small pieces called UI elements: panels (background rectangles that group other elements), images (icons, sprites), text, buttons, sliders, and so on. Every element is a GameObject in the Hierarchy window, exactly like the characters and props you have already built — the only real difference is that a UI element carries a RectTransform component instead of the plain Transform every other GameObject uses.
A RectTransform stores a rectangle instead of a single point: where it sits, how big it is, and (as section 3 covers in depth) how it should behave when its parent resizes. You can read those values from a script exactly like any other component:
using UnityEngine;
public class InspectRect : MonoBehaviour
{
void Start()
{
RectTransform rt = GetComponent<RectTransform>();
Debug.Log("width=" + rt.rect.width + " height=" + rt.rect.height);
Debug.Log("anchoredPosition=" + rt.anchoredPosition);
}
}
Attach this to a Button that a designer placed 160 pixels right and 40 pixels down from its anchor point, sized 200 by 50 pixels, and the Console prints exactly those numbers back:
width=200 height=50
anchoredPosition=(160.0, -40.0)
Nothing magical happened — the script just read the same numbers you would see in the Inspector's Rect Transform component. The point is that a UI element is a completely ordinary piece of Unity data: a GameObject, a component you can read and write from C#, and a place in a tree. Text elements almost always use a component called TextMeshProUGUI (from the TMPro namespace, usually just called "TMP") rather than the older, lower-quality Text component — this chapter's examples assume TMP, since that is what modern Unity projects default to.
GameObject, every technique you already know still works: Instantiate to spawn one at runtime, SetActive(false) to hide one, transform.Find or a serialized field to get a reference to one. The only new vocabulary is the RectTransform and the components (Image, Button, Slider, …) that turn a plain rectangle into something visible and clickable.Every UI element must live somewhere underneath a special GameObject called a Canvas — it is the root of a UI tree and the thing that actually tells Unity "draw everything under me as 2D UI, not as part of the 3D scene." A Canvas GameObject usually carries three components together: Canvas itself, a CanvasScaler (section 5), and a GraphicRaycaster (section 7, for clicks). The Canvas component's renderMode field decides how that drawing happens, and it has three options:
Screen Space - Overlay is the default and the one you will use for most of a game's HUD and menus: the Canvas is drawn directly on top of everything, always fills the whole screen, and completely ignores where any camera is pointed. Screen Space - Camera assigns the Canvas to one specific camera and places it as a flat plane a fixed distance in front of that camera's lens — it still normally fills the screen, but because it is now part of that camera's render, 3D objects that get closer to the camera than the Canvas's plane distance can visually cover the UI, and any full-screen camera effects (like a screen-space color grade) apply to the UI too. World Space is the odd one out: the Canvas behaves like any other object in the 3D scene, with its own position, rotation, and scale, and gets drawn by whichever camera can see it, from whatever angle. That is how a health bar floats above an enemy's head and turns to face the camera, or how a UI panel exists as a physical object in a VR scene.
using UnityEngine;
public class SwitchRenderMode : MonoBehaviour
{
public Canvas canvas;
public Camera uiCamera;
public void UseOverlay()
{
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
}
public void UseCameraSpace()
{
canvas.renderMode = RenderMode.ScreenSpaceCamera;
canvas.worldCamera = uiCamera;
canvas.planeDistance = 10f; // 10 units in front of the camera
}
public void UseWorldSpace()
{
canvas.renderMode = RenderMode.WorldSpace;
canvas.transform.position = new Vector3(0f, 2f, 5f);
canvas.transform.localScale = Vector3.one * 0.01f; // shrink to normal size
}
}
UseWorldSpace. A World Space Canvas's RectTransform is measured in world units, not screen pixels, so a health bar panel sized 400 by 60 (which looks reasonable on a screen-space Canvas) becomes a wall 400 units wide in the 3D scene. Scaling the Canvas's Transform down (often around 0.01, i.e. one world unit per 100 UI units) is what makes it look like a normal-sized UI element again.This is the single most important idea in UGUI layout, and the one beginners trip over the most. A RectTransform does not store "x, y, width, height" the simple way a normal 2D rectangle might. Instead it stores four separate things: anchorMin and anchorMax (two points, each with x and y between 0 and 1, describing where this element is tied to within its parent's rectangle), pivot (a point, also 0 to 1, describing which part of this element's own rectangle is used as its origin), anchoredPosition (the offset of the pivot from the anchor, in pixels), and sizeDelta (the element's size, or — as you will see — sometimes an inset instead).
When anchorMin and anchorMax are the same point, the element behaves like a normal fixed-size box pinned to that one point of the parent — its size comes straight from sizeDelta, and it never stretches, no matter how the parent resizes.
using UnityEngine;
public class PinTopLeft : MonoBehaviour
{
void Start()
{
RectTransform rt = GetComponent<RectTransform>();
// pin this element to the parent's top-left corner
rt.anchorMin = new Vector2(0f, 1f);
rt.anchorMax = new Vector2(0f, 1f);
rt.pivot = new Vector2(0f, 1f);
// anchoredPosition is now measured from that corner, in pixels
rt.anchoredPosition = new Vector2(20f, -20f); // 20px right, 20px down
}
}
When anchorMin and anchorMax are different on an axis, the element stretches on that axis: its edge on that axis always touches the corresponding edge of the parent, no matter how the parent resizes, and sizeDelta on that axis stops meaning "width" — it means "how many pixels smaller than a full stretch," i.e. a margin.
using UnityEngine;
public class StretchTopBar : MonoBehaviour
{
void Start()
{
RectTransform rt = GetComponent<RectTransform>();
rt.anchorMin = new Vector2(0f, 1f); // left edge, top edge
rt.anchorMax = new Vector2(1f, 1f); // right edge, top edge -- stretches across
rt.pivot = new Vector2(0.5f, 1f);
rt.sizeDelta = new Vector2(0f, 60f); // x=0 means "no inset", height is fixed at 60px
rt.anchoredPosition = Vector2.zero;
}
}
The pivot is a separate concept from the anchor: it decides which part of this element is used as the origin for position, and — importantly — for rotation and scaling too.
anchorMin, anchorMax, and (if you hold Alt, or Alt+Shift for the pivot too) anchoredPosition and sizeDelta for you. Everything it does, you can also do by hand in code, as above.pivot at its default (0.5, 0.5) and then being confused why a scale-based effect (like a shrinking health bar) eats away evenly from both sides instead of from the edge you wanted. The pivot, not the anchor, controls where scaling and rotation happen from — check it first whenever a scale or rotation looks wrong.Anchors and pivots are great for a handful of elements you position by hand, but an inventory bar with a variable number of item slots, or a list of chat messages that grows over time, needs something that positions children for you automatically. A Layout Group component, attached to a parent, does exactly that: it looks at its children every time something changes and arranges them in a row, a column, or a grid, based on padding, spacing, and alignment settings you configure once.
using UnityEngine;
public class BuildInventoryRow : MonoBehaviour
{
public GameObject slotPrefab;
public Transform rowParent; // has a *LayoutGroup component attached
void Start()
{
for (int i = 0; i < 5; i++)
{
Instantiate(slotPrefab, rowParent);
}
}
}
Notice this script never touches anchoredPosition at all — it just parents five copies of slotPrefab under rowParent. Whether the result comes out as a row, a column, or a grid depends entirely on which layout component (HorizontalLayoutGroup, VerticalLayoutGroup, or GridLayoutGroup) is attached to rowParent in the Inspector — the same spawning code produces three completely different arrangements just by swapping that one component, with zero code changes.
A close relative is ContentSizeFitter, which does the opposite job: instead of arranging children, it resizes the parent itself to fit its content. A chat bubble panel that should grow taller as its text gets longer sets ContentSizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize, and the panel's height then tracks whatever height the TMP text inside it actually needs, every time the text changes.
HorizontalLayoutGroup or similar and then also trying to set a child's anchoredPosition from code to nudge it slightly. The Layout Group recalculates and overwrites every child's position on its own schedule, so a manual position change gets silently undone the next layout pass. If you need one-off manual control over a specific child's position, that child needs to live outside the layout group, or you disable the group temporarily.A UI designed by eye on a 1920x1080 monitor will not automatically look right on a 1080x2340 phone or a 2048x1536 tablet — without help, a button sized "200 pixels wide" is a sensible size on the monitor and a tiny, unusably small target on the phone, because "pixels" mean wildly different physical sizes across devices. The CanvasScaler component (which lives on the same GameObject as the Canvas) solves this with a scale mode called Scale With Screen Size: you design the UI once, at one reference resolution, and the Canvas Scaler works out a single number — the scale factor — to multiply every UI element by, so the whole layout grows or shrinks together to fit whatever screen it actually runs on.
using UnityEngine;
using UnityEngine.UI;
public class ConfigureScaler : MonoBehaviour
{
public CanvasScaler scaler;
void Start()
{
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 1f; // 0 = match width, 1 = match height
}
}
The matchWidthOrHeight slider decides which dimension the scale factor is based on. At 0, the scale factor is simply actual width / reference width; at 1, it is actual height / reference height. Trace it by hand for two real devices, with the reference resolution set to 1920x1080:
Once ScaleWithScreenSize is active, you stop thinking in real device pixels entirely and design every RectTransform size and position in "reference units" — numbers that make sense at the reference resolution — and trust the Canvas Scaler to convert them for whatever screen the game actually runs on.
matchWidthOrHeight value between 0 and 1, like the common default of 0.5, does not simply average the two scale factors — Unity blends them on a logarithmic curve, so a screen that is twice as wide and a screen that is twice as tall contribute symmetrically to the blend. You do not need the exact formula to use it well: treat 0 as "always fit the width exactly," 1 as "always fit the height exactly," and values in between as "a compromise that leans toward whichever end is closer," and tune it by eye against your actual target devices.Reference resolution and scale factor solve size, but modern phones add another problem: the physical screen is not entirely usable. A camera notch or a rounded corner can cut into the top of the screen, and a swipe-gesture home indicator can overlap the bottom — draw a button under either one and part of it becomes invisible or hard to tap. Unity exposes the drawable region as Screen.safeArea, a Rect in real screen pixels (measured from the bottom-left corner) that excludes exactly those unsafe zones.
The usual fix is a small script on one "SafeArea" panel that every other piece of UI is parented under, converting the pixel Rect from Screen.safeArea into normalized anchorMin/anchorMax values (0 to 1, exactly what section 3 covered):
using UnityEngine;
[RequireComponent(typeof(RectTransform))]
public class SafeAreaFitter : MonoBehaviour
{
RectTransform rectTransform;
Rect lastSafeArea;
void Awake()
{
rectTransform = GetComponent<RectTransform>();
Apply(Screen.safeArea);
}
void Update()
{
// cheap to compare every frame; actually re-applying is rare
// (only happens on rotation or a foldable device unfolding)
if (Screen.safeArea != lastSafeArea)
Apply(Screen.safeArea);
}
void Apply(Rect safeArea)
{
lastSafeArea = safeArea;
Vector2 anchorMin = safeArea.position;
Vector2 anchorMax = safeArea.position + safeArea.size;
anchorMin.x /= Screen.width;
anchorMin.y /= Screen.height;
anchorMax.x /= Screen.width;
anchorMax.y /= Screen.height;
rectTransform.anchorMin = anchorMin;
rectTransform.anchorMax = anchorMax;
}
}
Trace it for a 1080x2340 phone whose safe area is Rect(0, 102, 1080, 2166) (a 102px gap at the bottom for the home indicator, and the top 72 pixels cut off for the notch, since 102 + 2166 = 2268, leaving 2340 - 2268 = 72 unsafe pixels at the top):
anchorMin.x = 0 / 1080 = 0.0
anchorMin.y = 102 / 2340 = 0.0436
anchorMax.x = 1080 / 1080 = 1.0
anchorMax.y = 2268 / 2340 = 0.9692
Applied to the SafeAreaFitter's stretched RectTransform, this pulls its bottom edge up 4.36% of the screen height and its top edge down 3.08% of the screen height — small percentages, but exactly enough to slide every child element out from under the notch and the home indicator, on any device, without hardcoding a single pixel number.
Clicking a UI button is not a special case of reading input directly the way Input.GetKeyDown is — it flows through a small pipeline of its own. A single EventSystem GameObject (there should be exactly one per scene) receives all pointer and keyboard-navigation input, a GraphicRaycaster component on the Canvas figures out which UI element, if any, is under the pointer (accounting for which elements are on top of which — the same way a 3D raycast finds the closest hit), and finally the topmost element's own component (a Button, in this case) fires its event.
Button.onClick is a UnityEvent — a built-in Unity type for "a list of functions to call when something happens," very similar in spirit to the C# event you have already used, except it can also be wired up visually in the Inspector, without any code at all. Wiring it from code looks like this:
using UnityEngine;
using UnityEngine.UI;
public class PlayButtonHandler : MonoBehaviour
{
public Button playButton;
void OnEnable()
{
playButton.onClick.AddListener(HandlePlayClicked);
}
void OnDisable()
{
playButton.onClick.RemoveListener(HandlePlayClicked);
}
void HandlePlayClicked()
{
Debug.Log("Play button clicked -- starting game");
}
}
Click the button in Play Mode, and the Console prints:
Play button clicked -- starting game
AddListener in OnEnable and the matching RemoveListener in OnDisable is the same subscribe/unsubscribe discipline you already use for C# events — skipping the unsubscribe is exactly as dangerous here as anywhere else, since a destroyed or disabled listener object left subscribed can still be called, or can leak.
EventSystem in the scene at all (Unity offers to create one automatically the first time you add a UI element, but it can get deleted by accident), or some other, invisible UI element — often a full-screen transparent panel added later, like a fade overlay — sits on top of the button in the hierarchy with Raycast Target enabled on its Image, silently intercepting the click before it ever reaches the button underneath.Button.onClick only covers a full press-and-release on the same element. Dragging — press, move while held, release — needs a different mechanism: a set of small interfaces from UnityEngine.EventSystems that the Event System calls automatically, at the right moment, on any component that implements them. Implementing IBeginDragHandler, IDragHandler, and IEndDragHandler is exactly like implementing IPlayerInput in an earlier chapter: you promise to provide certain methods, and something else calls them for you.
using UnityEngine;
using UnityEngine.EventSystems;
public class DraggableIcon : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
RectTransform rectTransform;
CanvasGroup canvasGroup;
void Awake()
{
rectTransform = GetComponent<RectTransform>();
canvasGroup = GetComponent<CanvasGroup>();
}
public void OnBeginDrag(PointerEventData eventData)
{
// let drop targets underneath "see" the pointer through this icon
canvasGroup.blocksRaycasts = false;
}
public void OnDrag(PointerEventData eventData)
{
rectTransform.anchoredPosition += eventData.delta;
}
public void OnEndDrag(PointerEventData eventData)
{
canvasGroup.blocksRaycasts = true;
}
}
eventData.delta is how far the pointer moved since the previous frame, in pixels — the same "change since last time" idea as Time.deltaTime, just for pointer position instead of time. Trace four frames of a drag starting from anchoredPosition = (100, 50):
CanvasGroup is a component that applies a setting to a whole subtree of UI elements at once — here, blocksRaycasts = false during the drag means the GraphicRaycaster looks straight through this icon while it is being carried, so a drop target underneath (like an inventory slot) can detect the pointer hovering over it, not the icon on top of it. CanvasGroup.alpha (fade a whole subtree) and CanvasGroup.interactable (disable a whole subtree of buttons at once, useful for graying out a panel) are the same idea applied to other settings.
Everything so far has been UGUI. Unity's other UI system, UI Toolkit, builds a UI a different way: instead of one GameObject per element, it uses a lightweight tree of plain C# objects called VisualElements, described declaratively in a markup file — very much like building a web page out of HTML and CSS.
UXML is an XML-based markup format that describes what elements exist and how they nest, without any styling details mixed in:
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement name="root" class="panel">
<ui:Label text="Score: 0" name="score-label" class="score-text" />
<ui:Button text="Play" name="play-button" class="primary-button" />
</ui:VisualElement>
</ui:UXML>
USS (Unity Style Sheets) is a CSS-like language that styles elements by name or by class, exactly like the class="panel" attributes above:
.panel {
flex-direction: column;
align-items: center;
padding: 20px;
}
.score-text {
font-size: 32px;
color: white;
}
.primary-button {
width: 200px;
height: 60px;
}
.primary-button:hover {
background-color: rgb(80, 80, 80);
}
A single UIDocument component on one GameObject points at a UXML file and exposes its tree as rootVisualElement. From there, Q<T>(name) ("query") finds an element by name, much like GameObject.Find, but scoped to this UI tree:
using UnityEngine;
using UnityEngine.UIElements;
public class ScoreScreenController : MonoBehaviour
{
[SerializeField] UIDocument uiDocument;
Label scoreLabel;
Button playButton;
void OnEnable()
{
VisualElement root = uiDocument.rootVisualElement;
scoreLabel = root.Q<Label>("score-label");
playButton = root.Q<Button>("play-button");
playButton.clicked += HandlePlayClicked;
}
void OnDisable()
{
playButton.clicked -= HandlePlayClicked;
}
void HandlePlayClicked()
{
Debug.Log("Play button clicked (UI Toolkit)");
}
public void SetScore(int score)
{
scoreLabel.text = "Score: " + score;
}
}
Notice Button.clicked here is a plain C# event-style callback (+= / -=), not a UnityEvent like UGUI's onClick — one of several small naming differences you will run into moving between the two systems.
UI Toolkit is often described as a retained-mode UI system. "Retained" here means the system keeps ("retains") a persistent tree of elements between frames and only recomputes and redraws the parts that actually changed — the opposite is immediate mode, where you rebuild the entire UI from scratch, from code, every single frame (this is exactly how Unity's old, editor-only OnGUI/IMGUI system works, and why it was never a good fit for in-game UI). Both UGUI and UI Toolkit are retained in this sense — the real practical difference between them is weight per element: a UGUI element is a full GameObject with a Transform, a RectTransform, and one or more components, all participating in Unity's normal scene graph; a UI Toolkit element is a small, lightweight C# object with none of that overhead.
GameObject/Transform knowledge you already have. Pick up UI Toolkit once you are comfortable, especially for a screen where you start to notice UI performance problems (section 11).Now that you can build and click UI, the next question is how it should learn about changes to game state — a health value dropping, a score going up. The tempting first approach is to check the value every frame inside Update:
using UnityEngine;
using UnityEngine.UI;
public class HealthBarPolling : MonoBehaviour
{
public PlayerHealth playerHealth;
public Slider slider;
void Update()
{
// BAD: recomputes and reassigns every single frame, even when
// health has not changed at all since the last frame
slider.value = playerHealth.CurrentHealth / (float)playerHealth.MaxHealth;
}
}
This works, but it does real work — a division and a property write, which itself can trigger internal UI updates — on every one of potentially thousands of frames where nothing changed at all. The cleaner approach, exactly like the polling-vs-events comparison from the input chapter, is for the health system to fire an event only when it actually changes, and for the UI to just listen:
using System;
using UnityEngine;
public class PlayerHealth : MonoBehaviour
{
public event Action<int, int> OnHealthChanged; // (current, max)
public int MaxHealth = 100;
int currentHealth;
void Awake()
{
currentHealth = MaxHealth;
}
public void TakeDamage(int amount)
{
currentHealth = Mathf.Max(0, currentHealth - amount);
OnHealthChanged?.Invoke(currentHealth, MaxHealth); // fires ONLY here
}
}
using UnityEngine;
using UnityEngine.UI;
public class HealthBarView : MonoBehaviour
{
public PlayerHealth playerHealth;
public Slider slider;
void OnEnable()
{
playerHealth.OnHealthChanged += HandleHealthChanged;
}
void OnDisable()
{
playerHealth.OnHealthChanged -= HandleHealthChanged;
}
void HandleHealthChanged(int current, int max)
{
slider.value = current / (float)max;
}
}
Over a 10-second fight at 60 frames per second (600 frames) where the player takes exactly 3 hits, the polling version runs its division-and-assign 600 times; the event version runs it 3 times, and produces the exact same slider on screen the whole way through, because the slider's value genuinely only needed to change 3 times.
This split — a plain data holder that knows nothing about UI, and a UI script that knows nothing about game rules, connected by an event — is the beginning of the MVC (Model-View-Controller) or MVP (Model-View-Presenter) pattern family. The Model is the plain data (PlayerHealth above). The View is the UI elements themselves (the Slider, a Text). The Presenter (or Controller) is the small script that sits in between: it listens to the Model's events and updates the View, and it listens to the View's events (like a button click) and calls methods on the Model. Neither the Model nor the View ever needs a direct reference to the other.
You do not need a rigid, named framework to get the benefit here — HealthBarView above is already acting as a small Presenter. The important habit is simply: game-state classes fire events when they change, and only UI classes touch UI components, and the two only ever meet through an event subscription, never through a UI script reaching in and reading (or a game-logic script reaching out and writing to) the other side directly.
UGUI tries to draw many UI elements in as few GPU draw calls (one instruction telling the GPU "draw this batch of geometry now") as possible, by batching — combining elements that share the same material and texture (commonly, the same font atlas or the same UI sprite atlas) into one draw call instead of one call per element. This is why UI style guides encourage packing icons into a shared sprite atlas: elements drawing from the same atlas can batch together, while an element using a different, unshared texture forces a batch break.
Batching happens on top of a more basic cost: every UI element under a Canvas has its geometry (the actual triangles and vertex colors the GPU draws) generated by Unity and cached. The moment anything changes about an element under that Canvas — its text, its size, whether it is active, even just moving it — Unity marks that Canvas dirty and has to regenerate the geometry for everything in that Canvas's batch before the next frame renders. This is called a canvas rebuild, and it is not free: a Canvas containing a handful of buttons rebuilding every frame is nothing to worry about, but a Canvas containing a huge inventory grid, rebuilding every frame just because a small HUD number ticks up nearby, is wasted work repeated dozens of times a second.
Attaching a second Canvas component to a child GameObject creates a nested canvas — its own independent rebuild batch, separate from its parent's. Splitting a frequently-changing subtree (a timer, a damage-number popup, a chat log) into its own nested Canvas means its constant churn no longer forces a much larger, mostly-static subtree (an inventory grid, a settings panel) to rebuild along with it.
// Hierarchy sketch, not a runnable script:
//
// MainCanvas (Canvas + CanvasScaler + GraphicRaycaster)
// InventoryPanel -- 200 icons, rarely changes
// HUDPanel (its own Canvas component -- a nested canvas)
// AmmoText -- changes every shot
// TimerText -- changes every frame
//
// updating TimerText now only dirties HUDPanel's small rebuild batch,
// never InventoryPanel's much larger one
A few smaller habits add up alongside canvas splitting: use TMP text over the legacy Text component (TMP generates its geometry more efficiently and batches better); turn off Raycast Target on purely decorative images (every enabled raycast target is one more thing the GraphicRaycaster has to check on every click, even though it can never actually be clicked); and avoid animating a value (fading, pulsing, sliding) on an element that lives in a Canvas full of unrelated static content — give it its own small nested Canvas instead.
Transform on UI elements, storing anchors, pivot, anchored position, and size.Screen.safeArea).Button.onClick).RectTransform has anchorMin = (1, 0), anchorMax = (1, 0), pivot = (1, 0), anchoredPosition = (-20, 20), and sizeDelta = (160, 50). Its parent Canvas is 800 by 600, with (0,0) at the bottom-left corner and (800,600) at the top-right corner. Work out the pixel coordinates of the button's left, right, bottom, and top edges within the Canvas.anchor point = (800 * 1, 600 * 0) = (800, 0) -- bottom-right corner
pivot position = anchor point + anchoredPosition
= (800 - 20, 0 + 20) = (780, 20)
pivot=(1,0) means this point IS the button's own right edge and bottom edge:
right edge x = 780
left edge x = 780 - width(160) = 620
bottom edge y = 20
top edge y = 20 + height(50) = 70
button spans x: 620 to 780, y: 20 to 70
Start from the anchor point: anchorMin and anchorMax are both (1, 0), so they describe a single point at the Canvas's bottom-right corner, (800, 0) in pixels. anchoredPosition is the offset of the pivot from that anchor point, so the pivot sits at (800 - 20, 0 + 20) = (780, 20). Because pivot = (1, 0) as well, that point is simultaneously the button's own right edge (pivot.x = 1) and bottom edge (pivot.y = 0), so the rest of the rectangle extends left and up from there by sizeDelta: left edge at 780 - 160 = 620, top edge at 20 + 50 = 70. The result is a 160x50 button sitting near the bottom-right corner, inset 20 pixels from each edge — exactly the pattern a corner settings icon usually uses, and it would stay inset by that same 20 pixels no matter how the Canvas resizes, because its anchor is the corner itself.
ScaleWithScreenSize, reference resolution 1920x1080, and matchWidthOrHeight = 1 (match height only). The game runs on a phone at 1170x2532. (a) What is the scale factor? (b) A button's RectTransform width is 300 reference units — how many real screen pixels wide is it on this phone? (c) In reference units, how wide is the phone's actual visible width? What does that number tell you about designing at 1920 wide when match = 1?(a) scaleFactor = actualHeight / refHeight = 2532 / 1080 = 2.3444
(b) realPixelWidth = 300 * 2.3444 = 703.3 px
(c) visible width in reference units = actualWidth / scaleFactor
= 1170 / 2.3444 = 499.1 reference units
Because matchWidthOrHeight = 1, the scale factor only ever looks at height — the reference width of 1920 plays no role in computing it at all. That gives a scale factor of 2.3444, so a 300-unit-wide button is drawn 703.3 real pixels wide. The interesting part is (c): this phone is only 499.1 reference units wide, a little over a quarter of the 1920-unit-wide canvas the UI was designed on. Anything placed further than about 499 reference units from the left edge — a wide horizontal toolbar, or a HUD element anchored near the "middle" of a 1920-wide design — would be pushed off the visible edge of this phone's screen entirely. This is exactly why matching height alone is risky for very narrow, tall phones: it guarantees the full height is always visible, but says nothing about how much width survives, which is also why safe-area handling (section 6) and testing on real narrow-aspect devices both matter, not just picking a match value and trusting it.
.text every single Update() frame regardless of whether ammo changed. Version B only sets .text inside a handler subscribed to an OnAmmoChanged event, fired only when ammo actually decreases. (a) How many times does each version write to .text? (b) Each write to .text marks that element's Canvas dirty for a rebuild before the next frame renders — how many canvas rebuilds does each version trigger from the ammo text alone? (c) In one sentence, why does Version B's advantage grow if this HUD's Canvas also contains a 200-icon inventory grid, rather than just the ammo text?(a) Version A: 240 writes (once every frame, whether or not ammo changed)
Version B: 6 writes (only on the frames ammo actually changed)
(b) Version A: 240 canvas rebuild triggers
Version B: 6 canvas rebuild triggers
Both counts come straight from section 11: every single write to a Text/TMP component's .text field marks its Canvas dirty, so the number of writes and the number of dirty-triggers are the same number for each version. For (c): if the ammo text shares a Canvas with a 200-icon inventory grid instead of its own nested Canvas, every one of Version A's 240 dirty-triggers forces Unity to regenerate the geometry for the entire shared batch — all 200 icons — even though none of them changed, 234 times more often than necessary; Version B only pays that full-batch rebuild cost on the 6 frames something genuinely changed, and (as section 11 also covers) giving the ammo text its own small nested Canvas would shrink even Version A's wasted work down to rebuilding only that small subtree, leaving the icon grid untouched regardless of which version is used.
That covers the shape of Unity UI from both directions: a tree of elements under a Canvas, positioned with anchors and pivots that survive any screen size, arranged automatically by layout groups, scaled cleanly across phones, tablets, and monitors with the Canvas Scaler and a safe-area fitter, and read through an Event System that turns raw pointer input into clicks and drags. UGUI and UI Toolkit build that tree two different ways — one GameObject-heavy and familiar, one lightweight and markup-driven — but both games benefit from the same two habits underneath: let events, not per-frame polling, carry state changes into the UI, and keep an eye on what forces a Canvas to rebuild, splitting it apart once that cost actually shows up.