Let’s talk about the behavior of a RectTransform.
The RectTransform is an extension of the Unity Transform. It adds some extra variables and functionality on top of the Transform
class for 2D GUIs.
The Heirarchy
The RectTransform
, like it’s Transform
base class, is a transformation hierarchy. That’s where a node in a tree data structure has a position, location, and scale, but those attributes are also affected (compounded) by their parent nodes.
Instead of just 3D transform variables (position, location, and scale), it also includes support for positioning itself on a 2D rectangle. And while the RectTransform
keeps the 3D Transform
variables and can have nodes with depth and rotation – they typically don’t except for animated effects. It’s a hierarchy because a GameObject
with a RectTransform
will have (or at least should have) children GameObjects
that also have RectTransforms
.
A GameObject
with a RectTransform
is usually expected to have children with a RectTransform
– all the way up to a Canvas root. The GameObject
owning that Canvas
is often a root GameObject
. “root” means it doesn’t have a parent and is owned directly by the scene).
The Coordinate System
Let’s go over the basic coordinate system of the Canvas component, which is the root of a UI with UGUI.
The coordinate system for the UI uses Unity’s world coordinates, where:
- the positive X axis goes right
- the positive Y axis goes up
- and the positive Z axis is forward (into the screen).
The Canvas
exists in the scene like any other GameObject
and Behaviour
, but is defaulted to the “UI” layer. Usually the entire hierarchy under the Canvas
is also assigned into the UI layer.
The default Render Mode, Screen Space, will control the dimensions and placement of the Canvas
. The bottom left corner will be the origin, and the center pivot will be <0.5, 0.5>, which is smack dab in the middle of the rectangle.
The dimensions of the Canvas
will match the viewport – with each unit being a pixel.
- The viewport is the graphical output where the UI will be shown.
- In the editor it’s the preview area in the Game tab
- When deployed it’s the area your app is drawn in.
While the units of 3D objects are expected to be in meters, since UI elements use pixels for units, and displays can be hundreds to thousands of pixels in dimensions, meaning your UI will overshadow other 3D things in the scene view – and it might feel awkward, but this is normal.
Quicktip! If you find a huge UI in the world off-putting, or if it’s so large you find yourself constantly selecting stuff in the UI by accident, you can disable its visibility, or disable the editor’s ability to select UI stuff in the Layers pull-down.
Placement in the Hierarchy
It’s already been mentioned, but the Canvas
dimensions and variables are not directly in your control. Control of the position of the Canvas
(in Screen Space modes) is meaningless anyways since it’s always going to be matched to the viewport. The pivot
and origin is always in the center.
What about everything else? What about for every other RectTransform
in the Canvas
‘ hierarchy? The placement of their content is based on 7 RectTransform
variables (anchoredPosition, anchorMax, anchorMin, offsetMax, offsetMin, pivot, sizeDelta), and these same values but for the RectTransform
‘s parent.
We’ll see later on it’s actually only 5 variables, but play along for now!
Using the RectTransform
variables, a 2D rectangle will be defined where the UI content should exist, and children RectTransforms
will reference that to generate their own rectangle regions.
Note that there are other factors that can change how a RectTransfom is placed, such as 3D properties and the rotation and scale variables inherited from the Transform class. We’re mostly going to ignore them for this article – and always assume everything in the UI hierarchy has a localScale
of Vector2.one
, and a localRotation
of Quaternion.identity
.
Before we continue, I just want to mention you should avoid RectTransforms
that are inside-out (that have a negative width or height). Unity will refuse to draw them and will warn you about them by drawing a red X across their rectangle in the scene tab.
The Anchor Min/Max Variables
The anchor variables are of type Vector2
, and the x and y components are expected to go between 0.0 and 1.0. These values reference the parent’s region.
- For the x components
- a value of 0.0 means the very left of a parents region
- and 1.0 means the right of the parent’s region.
- For the y component
- a value of 0.0 means the bottom of the parent’s region
- and 1.0 means the top of the parent’s region.
anchorMin
defines where the bottom-left corner of theRectTransform
‘s rectangle will be, based off the parent’s rect.anchorMax
defines where the top-right corner of theRectTransform
‘s rectangle will be based off the parent’s rect.
The values for anchorMin
and anchorMax
can be coded to go lower than 0.0 or higher than 1.0, although usually it doesn’t make sense to. Also note that a min and max anchor can have the same value. This does not mean the rect is fully collapsed, because this is only the first step to figuring setting the RectTransform’s placement.
A few examples:
- top left – The child
RectTransform
expands to the full size of the parent because the minimum is set to be the parent’s minimum, and the maximums the same. - top right – Specifying both min and max to the top right corner of the parent (this makes more sense later when we also use
offsetMin
andoffsetMax
. - middle left – Setting the child to be the center third of the parent.
- middle right – Setting the child to be 10% from the left, all the way up to the horizontal middle. Vertically it starts 25% down from the top, and matches the parent’s bottom.
- bottom left – Matches the parent’s vertical space, but only extends out 1/5 the width from the parent’s left.
- bottom right – Matches the parent’s vertical space, but only the left edge.
There’s a series of steps to find the rectangular region of a child RectTransform
inside a parent. The anchors are the first step – they define a proportional rectangle inside the parent – the next step to define offsets from the corners of that proportional rectangle.
The Offset Min/Max Variables
The offsetsMin
and offsetMax
specify pixel offsets from the corners that anchorMin
and anchorMax
define. That’s all it is.
- Take the resulting point you get from
anchorMin
and addoffsetMin
to it to get the rectangle’s new bottom left. - Take the resulting point you get from
anchorMax
and addoffsetMax
to it to get the rectangle’s new bottom right.
This extra piece of the formula makes it clear why we would ever want to have anchorMin
and anchorMax
match, because we can add a constant offset of them afterwards.
Also note that nothing has changed with the coordinate convention. Positive x values still go right, positive y values still go up. This applies for both the offsetMin
and offsetMax
.
MoAr examples:
- top left – Anchors were set to take up the full parent, but offsets were added to inset a constant amount.
- top right – offsets were added to create a frame around where anchors were evaluated.
- middle left – Anchors were both set to the center, and offsets were applied – creating a rectangle of a constant size at the center of the parent.
- middle right – Anchors were both set to the top left, and negative offsets were applied – creating a rectangle of a consistent pixel size offset from the parent’s top left.
- bottom left – Similar to the top left example, but the
anchorMax
only extended halfway across the parent horizontally. - bottom right – Anchors were set to the left edge.
offsetMax.x
was increased. This results in a left-aligned rectangle that takes up the entire parent height, but has a constant pixel width.
In the diagrams the offsetMin
and offsetMax
are not labeled, but you can tell which is which. The arrow that ends up in the max (top-left) is the offsetMax
, and the other is the offsetMin
. Also, the offsetMax
will be seen radiating from the anchorMax
, and same for the offsetMin
and anchorMin
.
The sizeDelta Variable
Kids, I think you’re finally old enough to learn the truth. Santa isn’t real! You already knew that? What about the Easter Bunny? You knew that too? Hmm… what about… offsetMin
and offsetMax
. No!? Sweet!! offsetMin
and offsetMax
aren’t real member variables! They’re just C# properties based on a combination of other variables!
Muahaha!
“What does that even mean!? They’re OBVIOUSLY there, and ALL the RectTransform variables are properties!!”
Whoa, reader! Calm down! Well, let’s take a look at the RectTransform
definitions in Visual Studio and compare them to the inspector in Debug Mode.
Side note for those who are new to C#, properties are accessor and setter functions that look like variables, that are implemented with C#’s {get;set} syntax.
You’ll notice that the offsetMin
and offsetMax
aren’t there – while some other properties exist, with their real variables names slightly modified. So what really is offsetMin
and offsetMax
? – what is sizeDelta
– and how are they all related?
sizeDelta
is the size, in pixels applied on top of the anchors. Wait,… isn’t that part of what offsetMin
and offsetMax
are? Yes! The extra pixel regions added from offsetMin
and offsetMax
are equal to sizeDelta
– or if sizeDelta
is negative, the space removed instead of added (such as from an inset).
// Not usable code, just using a code-like syntax to make a statement RectTransform rt; rt.sizeDelta == rt.offsetMax - rt.offsetMin;
So if offsetMin
and offsetMax
aren’t real (just properties), and when combined they equal sizeDelta
, and sizeDelta
is the extra (or deficit) space that exists on top of the space claimed from anchorMin
and anchorMax
referencing the parent space – what decides how to split the space in sizeDelta
to distribute into offsetMin
and offsetMax
?
The Pivot Variable
The pivot value decides what percentage of sizeDelta
goes into offsetMin
, and what percentage goes into offsetMax
.
// Not usable code, just using similar syntax to make a statement RectTransform rt; rt.offsetMin.x = -rt.pivot.x * rt.sizeDelta.x + anchoredPosition.x; rt.offsetMin.y = -rt.pivot.y * rt.sizeDelta.y + anchoredPosition.y; rt.offsetMax.x = (1.0f - rt.pivot.x) * rt.sizeDelta.x + anchoredPosition.x; rt.offsetMax.y = (1.0f - rt.pivot.y) * rt.sizeDelta.y + anchoredPosition.y;
We haven’t covered anchoredPosition yet, but all you need to know is that it’s just some position offset variable.
So the pivot
is like a percentage value, it cuts a portion of sizeDelta
and assigns it to offsetMin
, and the rest to offsetMax
.
Is that all there is to it? No. We have to remember the RectTransform
is based on the Transform
class. The Transform
has a localPosition
, localRotation
and localScale
. The localPosition
is the origin of the GameObject
‘s coordinate space that stuff rotates and scales around (or a pivot point, if you will – see where this is going?). If the user rotates or scales a RectTransform
, how do we let developers author where in the rectangle that happens? The pivot
is how.
After and anchors are calculated, a rectangle is created (or a line or point, if the rectangle is collapsed because the width and/or height is 0). If we treat the pivot as horizontal and vertical proportions to travel into that rectangle, that resulting point is where the localPosition
of the transform will be placed.
So the pivot defines where the actual Transform
‘s position is, and the rest of the content (geometry) is offset so that it’s correctly placed based off that defined origin. And if/when the RectTransform
is rotated or scaled, it does so from the origin (the exact same way it would for a 3D model) which is defined by the authored pivot
value.
anchoredPosition
On top of everything, anchoredPosition
is a final offset added to the position. Changing this value will even move the base Transform
‘s localPosition
so that the pivot will stay the same.
Earlier we covered how offsetMin
and offsetMax
aren’t real, but are just portions of sizeDelta
. But there’s one last piece of the puzzle,… translations.
For example,
an anchorMin
of Vector2(-3, -5)
and an anchorMax
of Vector2(37, 45)
will have the same sizeDelta
as
an anchorMin
of Vector2(-33, -45)
and an anchorMax
of Vector2(7, 5)
.
They result in the same sizeDelta
(<40, 50>) even though they’re different offset values – this translation for the offset values is defined by anchoredPosition.
Two Modes of Coding RectTransforms
We’ve done it! We’ve done a deep dive of all the variables (I feel like I phoned it in for anchoredPosition
, but this is turning into a long article) Along the way, we talked about how offsetMin
and offsetMax
aren’t variables in the RectTransform
, but instead are properties dependent on anchoredPosition
, pivot
, and sizeDelta
.
When coding the placement of a RectTransform
in C#, there are two ways to go about it:
- Specify
offsetMin
andoffsetMax
and let theanchoredPosition
andsizeDelta
be modified as a side-effect. - Modify the
anchoredPosition
andsizeDelta
, and forget aboutoffsetMin
andoffsetMax
.
Both of these methods require modifying the anchorMin
, anchorMax
and pivot if you want to make sure the RectTransform
is fully set.
I find myself using both (offsetMin
/offsetMax
vs anchoredPosition
/sizeDelta
) depending on the situation.
If I collapse the anchorMin
and offsetMin
by setting them to the same value, I’ll usually use anchoredPosition
and sizeDelta
and think of them as the sprite’s position and size. In the image above, the anchorMin
and anchorMax
are set to the top left. Because of this, anchoredPosition
and sizeDelta
can be used to directly specify the pixel position and dimensions. While this is possible with offsetMin
and offsetMax
– for these situations I find offsetsMin/Max
less intuitive to comprehend, and less intuitive to read in code.
If I have different values for anchorMin
and anchorMax
, I’ll use offsetMin
and offsetMax
because it’s more intuitive for thinking about applying constant pixel offsets to a rectangular region that can dynamically resize when its parent RectTransform resizes.
The Relationships & Utilities
So with everything defined, let’s get to what we came here for,… identities!
// Not usable code, just using similar syntax to make a statement RectTransform rt; // rt.anchorMin is not based off anything, it's an independent variable. // rt.anchorMax is not based off anything, it's an independent variable. // rt.pivot is not based off anything, it's an independent variable. // I'm going to use a <==> symbol to signify a two way mapping. This is not legal syntax. rt.sizeDelta <==> rt.offsetMax - rt.offsetMin; rt.offsetMin <==> -Vector2.Scale(rt.pivot, rt.sizeDelta) + rt.anchoredPosition; rt.offsetMax <==> Vector2.Scale(Vector.one - rt.pivot, rt.sizeDelta) + rt.anchoredPosition rt.anchoredPosition.x <==> Mathf.lerp(rt.offsetMin.x, rt.offsetMax.x, rt.pivot.x); rt.anchoredPosition.y <==> Mathf.lerp(rt.offsetMin.y, rt.offsetMax.y, rt.pivot.y);
What about calculating the Transform
‘s localPosition
and world Position
? It’s tricky because the localPosition
and the RectTransform
‘s rectangle is based off the pivot, anchors, and anchoredPosition
. Not only that, but it also depends on the parent RectTransform
and those issues also apply to the parent’s content and position – and the parent’s parent, and the parent’s parent’s parent, and… While there may be a simple formula, I can’t think of a generic one, and some non-trivial linear algebra and hierarchy traversal will probably be involved.
But, the RectTransform
comes with helpful utility functions to get the local and world corners for you!
[RectTransform.GetLocalCorners][RectTransform.GetWorldCorners]
If you have pixel position in world coordinates, also don’t forget you can use the RectTransform’s parent-class’ Transform.InverseTransformPoint function to convert that world position to a local position, which will then should then be compatible with RectTransform.GetLocalCorners()
.
If you want the results of GetLocalCorners()
but don’t care about ordered 3D points, but care about the final local size and position, RectTransform.rect is useful. I don’t find the position that useful, but I find myself getting the rect’s size very often for my procedurally generated GUIs.[example]
If you just need to do collision tests with pixels, the RectTransformUtility class has some helpful functions.
– Stay strong, code on. William Leu