Creating Dynamic UI Guide in Unity

Irfan Yigit Baysal
10 min readFeb 3, 2023

--

I’ve had the opportunity to practice with Unity UI for a long time. I had the chance to design the structure and implement UIs with multiple different types, both in the company I work for and in my own projects.

Each practice and product helped me learn more about the UI and use it with more different techniques. Although these techniques are mostly components & classes provided by Unity, it also includes some third party tools such as EnhancedScroller package that I have used recently and highly recommend as well as Unity UI Extensions package. I think it would be beneficial for you to have a look at it in your spare time.

However, the topics that I want to show and tell you in this article will be HorizontalLayoutGroup, VerticalLayoutGroup, ContentSizeFitter components, LayoutBuilder wrapper class, CanvasScaler for adapting multiple resolutions, TextMeshPro and usage of 9-Slicing Technique by using SpriteEditor.

Rather than full of descriptions, in this article, you will see shortcuts for creating dynamic UIs and very few code blocks. I also share my CanvasComponent pre-release package on GitHub so that you are able to find the examples & code blocks about these issues. Let’s start building a dynamic UI by taking a look at the 9-Slicing Technique.

9-Slicing Technique

This technique allows you to reuse an image at various sizes without needing to prepare multiple asset says Unity, I mainly use it also for reusability and size optimization. This is an important size reduction especially for mobile games if used properly. Let’s show this by making an example popup.

Before starting, I would like to remind you about texture import settings in order to use Sprite Editor.

In this example, the corners stay the same size, the top and bottom of the Sprite stretch horizontally, the sides of the Sprite stretch vertically, and the centre of the Sprite stretches horizontally and vertically to fit the Sprite’s size.

After applying this slicing, this texture is now ready for reusing and resizing operations.

Canvas Scaler

The UI layouts in particular need to be flexible because modern games and applications frequently need to support a wide range of various screen resolutions. A range of tools for this purpose are included in Unity’s UI System and can be integrated in numerous ways.

For example, let’s say we want to place a settings button in the upper left corner. The layout can be changed such that the buttons’ placements are related to the appropriate screen corners as one method of keeping the buttons inside the screen.

When the resolution is changed to a new aspect ratio, the UI elements stay attached to those corners once they have been anchored to them. The UI elements will likewise stay attached to their appropriate corners whether the resolution is increased or decreased. However, because they retain their original size, they might take more or less space on the screen.

(Safe Area Calculation is disabled in this examples)

This view is not exactly a desired view because as the resolution size changes, the size of the UI elements must change at the same rate.

Here is the example Canvas Scaler setup. We use Scale With Screen Size as scale mode because all UI elements are scaled up or down together with the screen resolution if the current screen resolution is smaller or bigger than this reference resolution, which is determined by the scale factor of the Canvas.

We use Expand as screen match mode. Expand the canvas area either horizontally or vertically and size of the canvas will never be smaller than the reference resolution.

Horizontal and Vertical Layout Group

Basically this components sort objects that are children of this component side by side (for horizontal) and below each other(for vertical) according to padding, spacing and other properties that we use and makes your life easier in order to sort objects according to some rules. An example of rule is “ Each object must have 16 units of space between each other.” then we modify spacing property to handle this rule.

All I want to add, without going into too much basic usage, is to demonstrate the “reverse arrangement” property, which I think has been overlooked.

What this property does is to reverse the draw order without breaking the hierarchical order. You may ask why I need this, but the need I encountered recently was all about the shade of the texture, although this seems a little design-issue need in my mind, I am sure that there have been many different needs related to this among the developers.

When you look at the UI object named as “0”, there is no order change in the hierarchy, but if we want to change the order, we need to write a few lines of code for this.

RectTransform RectTransform => transform as RectTransform;

private async void OrderSibling(int siblingIndex)
{
RectTransform.SetSiblingIndex(siblingIndex);
await UniTask.Yield(cancellationToken: this.GetCancellationTokenOnDestroy());
LayoutRebuilder.MarkLayoutForRebuild(RectTransform);
Canvas.ForceUpdateCanvases();
}

In this code above, After we set the RectTransform which UI element order we want to change, we can set the transform to the index we want with the SetSiblingIndex(int index) method.

We can also move the UI element to the end of the local transform list. For that, we can use SetAsLastSibling() method. Let’s modify the code by using this method.

RectTransform RectTransform => transform as RectTransform;

private async void SetLastSibling()
{
RectTransform.SetAsLastSibling();
await UniTask.WaitForEndOfFrame(cancellationToken:this.GetCancellationTokenOnDestroy());
LayoutRebuilder.MarkLayoutForRebuild(RectTransform);
Canvas.ForceUpdateCanvases();
}

Content Size Fitter

The Content Size Fitter functions as a layout controller that controls the size of its own layout element. For example, we have an object on canvas containing a vertical layout component and a content size fitter. Let it have a width of 100 and a height of 100. Then, let’s say we add UI elements inside this object as child. Each element will be sorted one under the other due to the vertical layout component. What about our object with 100 width and 100 height? For a dynamic UI, shouldn’t its height increase at a certain rate with the UI elements we add into it? Just for this case, we can use Content Size Fitter to handle this issue. For this example, we change Horizontal Fit property as Preferred Size and let this component handle this horizontal arrangement calculations.

LayoutBuilder

I added some sample codes to the Horizontal-Vertical Layout section. In these code blocks you see this two code lines. Apart from the methods I mentioned, you see wrapper class named as LayoutBuilder in the code. Although I’ve done a lot of research about this class, I still can’t say for sure it’s like this or that. So here I will convey the problems I encountered my solutions and advices.

MarkLayoutForRebuild, calculates layout with delay until next layout pass and prevents multiple recalculations for the same layout elements. The advantage is that we don’t need to force the entire canvas for one UI element. The rebuild will not happen immediately, but at the end of the current frame, just before rendering happens.

ForceRebuildLayoutImmediate, Forces an immediate rebuild of the layout element and child layout elements affected by the calculations. Unity says that “do not use if your layout does not contain sub-trees or if your change does not require recalculation of many layouts”. Although this is a more costly process than the other method, there are places we need to use.

private void UseMarkLayoutForRebuild()
{
LayoutRebuilder.MarkLayoutForRebuild(_rectTransform);
Canvas.ForceUpdateCanvases();
}

//UniTask version
private async void UseMarkLayoutForRebuild()
{
await UniTask.WaitForEndOfFrame(cancellationToken:this.GetCancellationTokenOnDestroy());
LayoutRebuilder.MarkLayoutForRebuild(_rectTransform);
Canvas.ForceUpdateCanvases();
}

//Coroutine version
private IEnumerator CRUseMarkLayoutForRebuild()
{
yield return new WaitForEndOfFrame();
LayoutRebuilder.MarkLayoutForRebuild(_rectTransform);
Canvas.ForceUpdateCanvases();
}

If I need to rebuild layout in runtime frequently, I use UniTask or Coroutine version because as I explained above, MarkLayoutForRebuild method does not work immediately, so I wait a frame then update canvases for handling correctly layout calculations.

Canvas.ForceUpdateCanvases method is like my “just-in case” method. I usually use this method in order to be ensure that canvas is up-to date.

This script is from my Canvas-Component package. This scripts basically handles automatically when RectTransform’s dimension is changed in run-time.

public enum BuilderType
{
MarkLayoutForRebuild,
ForceRebuildLayoutImmediate
}

protected override void OnRectTransformDimensionsChange()
{
base.OnRectTransformDimensionsChange();
ForceRebuilder();
}

protected async void ForceRebuilder(BuilderType type = BuilderType.MarkLayoutForRebuild)
{
if (type == BuilderType.MarkLayoutForRebuild)
{
await UniTask.Yield(cancellationToken: this.GetCancellationTokenOnDestroy());
LayoutRebuilder.MarkLayoutForRebuild(RectTransform);
}
else
LayoutRebuilder.ForceRebuildLayoutImmediate(RectTransform);

ForceUpdateCanvas();
}

protected void ForceUpdateCanvas()
{
if(!useForceCanvases)return;
Canvas.ForceUpdateCanvases();
}

TextMeshPro

One of the most powerful text solution for Unity is TextMeshPro. With its capabilities, TextMeshPro offers extensive control over text formatting and layout. After a bit reminders, I would like to explain a few features and implementations that I use the most.

TMPSettings

Construction settings for TextMesh Pro are saved in a unique Asset called TMP Settings. This scriptable must be under Resources folder. In DefaultFontAsset field, we set our default font asset for entire project. When we create a TextMeshPro element, it comes with the default font asset we set on it.

I suggest you to also set Fallback Font Asset at least one asset in order to set fallback asset when TexMesh Pro can’t find a character in a text object’s main font Asset.

Material Preset

Another important point is the method of material creation. One of the most common mistakes is to duplicate the material directly.

In the picture below you can see the correct way of creating material. If you create a material preset in this way, you can easily change it via the component in the Text element.

Text Spacing & Colorizing

One of my favorite implementation for text is to control text space. You can choose the space to leave on a single text element. Do not forget that RichText must be enabled in order to use these codeblocks.

[SerializeField] private string spacedText;
[SerializeField] private string explanation;

private void SetSpaceString()
{
for (int i = 0; i < spacing; i++)
{
spacedText = spacedText?.Insert(i, GetSpaceChar());
}

TextMeshProUGUI.SetText(TextMeshProUGUI.text+spacedText+explanation);
}

private string GetSpaceChar()
{
return "\u00A0";
}

Let’s also colorize the text. Let’s add first ColorizeString method.

public string ColorString(string text, Color color)
{
return "<color=#" + ColorUtility.ToHtmlStringRGBA(color) + ">" + text + "</color>";
}

And then create field for Color selection.

[SerializeField] private Color color;

Lastly, add these code blocks into “SetSpaceString method”.

var coloredExplanation = ColorString(explanation, color);
TextMeshProUGUI.SetText(TextMeshProUGUI.text+spacedText+coloredExplanation)

With a few lines of code, we get rid of creating extra text for different texts, spaces and colors that can be used especially in places such as character information screen, scoreboard or survey.

Margin

Finally, adjusting the margin, which is a feature I use often. In some cases, the rect transform of the text element and the margin being the same may not be pleasant for the view. For example, the text of a text element positioned to the edge is at the bottom of the corner. I recommend the use of margin instead of editing the alignment for this purpose. The margin field is under ExtraSettings expandable field.

public void SetMargin(Vector4 marginVector)
{
TextMeshProUGUI.margin = marginVector;
}

I have a margin field in my TextMeshProComponent. I also added RaycastTarget and AutoSize flag fields. Especially RaycastTarget flag is one of the most annoying flag that can be overlooked very easily. It may also affect input clicking if text has a button background for instance. That’s why I create a component setup like this in order to handle these issues rapidly.

--

--