Unity 3D: Vertical Layout Group not placing elements where they should be - c#

I am having a problem with my layout groups and keeping my UI consistent. I have a menu that holds two empty elements Blue and Green boxes bellow that get filled at playtime depending on what the player does. The orange boxes are buttons that may add or remove both purple boxes or orange boxes depending on the button pressed. The Gray, Blue, and Green boxes all have Vertical Layout Group and Content Size fitter (vertical fit set to "Prefered Size"). So from my understanding, all children within these boxes should be stacked vertically and will expand and contract the box as needed.
The problem I am having is that the first time I press a button that adds a purple box and expands the blue box, the green box stays where it is. The second time I press a button the Green box shifts where it should be no matter what gets added to the blue box. Conversely if pressing a button for the first time removes purple boxes and contracts the blue box, the green box stays where it is until I press a button again then shifts where it should be.
If I manually change any of the Gray, Blue, or Green boxes attributes such as anchor pivot or scale the boxes rearrange themselves the way it should be.
Am I doing something wrong with Vertical Layout / Content Size fitter? I hope this is enough to go on. I can provide code if need be. I just don't know exactly what to share.
When the button is pressed call this method.
void AcceptUnlockButtonInput(Exit roomExit) {
if (PlayerHasKeyForExit(roomExit)) {
LogAction(roomExit.exitUnlockDescription);
roomExit.attemptedToExit = false;
roomExit.attemptedToUnlock = false;
} else if (roomExit.attemptedToUnlock == false) {
LogAction(roomExit.exitLockedDescription);
}
DisplayActionText();
foreach(Transform child in inputContainer.transform) {
Destroy(child.gameObject);
}
DisplayInputButtons();
Canvas.ForceUpdateCanvases();
}
LogActrion() just simply adds a string to a list then DisplayActionTest() reads off that list and creats/adds gameObjects to the container.
public void DisplayActionText() {
actionLog.ForEach(value => {
var textObject = Instantiate(displayTextPrefab) as GameObject;
var textMeshPro = textObject.GetComponentInChildren<TextMeshProUGUI>();
textMeshPro.text = value;
textObject.transform.SetParent(outputContainer.transform, false);
});
actionLog.Clear();
Canvas.ForceUpdateCanvases();
}
Simply creats a button for each exit and adds the original method to the buttons listener.
public void DisplayInputButtons() {
foreach (var exit in Exits) {
var unlockButton = Instantiate(userActionButton) as GameObject;
newButton.text = buttonText;
newButton.transform.SetParent(buttonGroup.transform, false);
unlockButton.onClick.AddListener(() => AcceptUnlockButtonInput(exit));
}
}

I have had the same problem with layout groups. From my understanding, layout groups don't update immediately when you put something in them.
Something what worked for me was:
LayoutRebuilder.ForceRebuildLayoutImmediate(recttransform of the layoutgroup);
You can also try disabling and enabling the layout group component:
IEnumerator UpdateLayoutGroup()
{
layoutGroupComponent.enabled = false;
yield return new WaitForEndOfFrame();
layoutGroupComponent.enabled = true;
}
If enabling and disabling the layoutgroup component doesn't work you can also try this:
IEnumerator UpdateLayoutGroup()
{
yield return new WaitForEndOfFrame();
layoutGroupComponent.enabled = false;
layoutGroupComponent.CalculateLayoutInputVertical();
LayoutRebuilder.ForceRebuildLayoutImmediate(recttransform of the layoutgroup);
layoutGroupComponent.enabled = true;
}

I have had similar problems and I found this to be helpful:
https://docs.unity3d.com/ScriptReference/Canvas.ForceUpdateCanvases.html
Sometimes when instantiating elements into a gameobject with a vertical layout group, the elements will only get into their right position after updating the canvas.

Update answer
In my case there were multiple layout groups nested into each other, so the rebuild of the parent layout group did not work. In the first try I rebuild all in hierarchy (old answer), but ran into performance issues for large lists. Then I found a much more robust and performant way (with help from Answer from Adam-Buckner and Auto-Layout Docs):
Only the topmost layout group of the layout group hierarchy has a content size fitter, any descendant must not have one
All vertical layout groups have checked Control Child Size (Width & Height), Use Child Scale (Only Height), Child Force Expand (Only Width)
When the layout is being affected by code, due to adding/removing elements or setting a component active, I use the following code (it uses MarkLayoutForRebuild, which prevents recomputing a node multiple times)
(starting from element with changes, first line can be omitted if element is not a LayoutGroupon its own)
LayoutRebuilder.MarkLayoutForRebuild((RectTransform)transform);
LayoutGroup[] parentLayoutGroups = gameObject.GetComponentsInParent<LayoutGroup>();
foreach (LayoutGroup group in parentLayoutGroups) {
LayoutRebuilder.MarkLayoutForRebuild((RectTransform)group.transform);
}
It works really well and I have no longer elements jumping around. As far as I understand content size fitters control a RectTransform, but competes with the parent LayoutGroup which also want to control it (therefore the warning in the editor). If only LayoutGroups are nested into each other, the algorithm can easily compute the sizes from bottom up, so the bottom most layout groups derive their size from their children (Caused by Use Child Scale), which is then applied to it by their parent layout group (caused by Control Child Size). LayoutGroups does not affect the RectTransform of the element it is placed on, only on its children. Finally the top element gets a height from the hierarchy which is then applied to itself the content size fitter.
Old and inefficient answer
In my case there were multiple layout groups nested into each other, so the rebuild of the parent layout group did not work. I had to rebuild all in hierarchy, to properly update the layout.
LayoutGroup[] parentLayoutGroups = gameObject.GetComponentsInParent<LayoutGroup>();
foreach (LayoutGroup group in parentLayoutGroups) {
LayoutRebuilder.ForceRebuildLayoutImmediate((RectTransform)group.transform);
}

I know there is an issue with the VLG not updating correctly. Changing any of its value after adding/ removing elements forces its layout to resize and may fix your issue (at least that's the way I do it).
Can you try :
- Add/remove your element
- VerticalLayoutLement.spacing += Random.Range(-1,1);
To be sure it rescales properly i usually do it in a coroutine with an"yield return new WaitForNewFrame()" before the spacing part.

After trying a lot of tricks that I found on the web I felt a little frustrated because none of them worked for me. When I ran the scene all the elements in the vertical layout looked messed, but when I clicked on Pause and Continue they magically fitted perfectly in the right position, so I wrote this simple funcion to emulate it, that works fine in my app:
IEnumerator RefreshPrefab(GameObject prefabInstance)
{
prefabInstance.SetActive(false);
yield return new WaitForSeconds(0.1f);
prefabInstance.SetActive(true);
}
And call it sending as parameter the parent prefab that contains the layout group:
StartCoroutine(RefreshPrefab(instantiatedPrefab));

Related

Not being able to add large number of controls in a panel

I have a scenario in which I want to display numerous images in a panel using only horizontal scroll.
To enable only Horizontal Scroll only, I used the following code in the constructor
public Form()
{
InitializeComponent();
panelImageGallery.AutoScroll = true;
panelImageGallery.VerticalScroll.Enabled = false;
panelImageGallery.VerticalScroll.Visible = false;
}
And then to display and use images in the panel I wrote the following lines of code
int increment = 0;
lblCount.Text = "0/" + files.Length;
for (int i=0;i<files.Length;i++)
{
PanelPictureBox box = new PanelPictureBox();
box.IMAGE= files[i];
box.Location = new Point(panelImageGallery.Location.X + increment, panelImageGallery.Location.Y+10);
box.INDEX = i+1;
panelImageGallery.Controls.Add(box);
increment += 300;
}
Following is the result, and it can be seen that although, I have 350 images but NOT all images are in the panel as there are only 109 images and even both the horizontal and vertical scrolls are there.
Also, when I scrolled the panel all the way to the end then there are some display issues at the end too as the last image gets joined with the second last image.
Another thing that I observed was that when I increased the margin between the images, then fewer and fewer images got displayed inside the panel. So for example, when I set the increment to 500 then only 66 images got displayed in the panel instead of all the 350 images. My feeling is that there could be restriction on the maximum size of the panel, So what is actually happening here and how to resolve this issue ?
This is a limitation because of some of the Windows messages, for example as mentioned in documentations:
Note that the WM_HSCROLL message carries only 16 bits of scroll box
position data. Thus, applications that rely solely on WM_HSCROLL (and
WM_VSCROLL) for scroll position data have a practical maximum position
value of 65,535.
In general, it's not a good idea to try to host too many controls on the form.
In your case, you may want to try controls which performs automatic layout, like FlowLayoutPanel, TableLayoutPanel, Docking into the left of a panel or decrease the margin between your controls.
Or as a proper fix, you can rely on ListView control which support virtual mode, or implement a custom drawn control, or use paging to show a limited number of controls in each page.

Why is TextMeshPro not Updating textBounds After Setting text?

I have implemented a tooltip system on hover. For this I use a textmeshpro text and an image. This image should be a simple background depending on the length of the text, which is always different.
If I hover over a certain element, the textmeshpro text is replaced. The background image is moved to the element and the size is shall change based on the length of the text:
// Hover over slot in
public void OnPointerEnter(PointerEventData eventData) {
if (currentItem != null) {
Debug.Log("hover over slot item in");
GamePlay.EnableCanvas(GamePlay.GetHoverCanvas());
TextMeshProUGUI txtMeshPro = GamePlay.GetHoverCanvas().GetComponentInChildren<TextMeshProUGUI>();
Debug.Log(currentItem.GetItemDescription(currentItem));
txtMeshPro.text = currentItem.GetItemDescription(currentItem);
var hoverCanvasText = txtMeshPro.GetComponent<RectTransform>();
var hoverCanvasTextBG = GamePlay.GetHoverCanvas().GetComponentInChildren<Image>().GetComponent<RectTransform>();
hoverCanvasText.position = new Vector3(this.GetComponent<RectTransform>().position.x, this.GetComponent<RectTransform>().position.y + 50, 0);
hoverCanvasTextBG.position = new Vector3(this.GetComponent<RectTransform>().position.x, this.GetComponent<RectTransform>().position.y + 50, 0);
hoverCanvasTextBG.sizeDelta = new Vector2(txtMeshPro.textBounds.size.x + 15, txtMeshPro.textBounds.size.y + 15);
// No clue why image is not resized properly on first hover
}
}
The weird issue I have is that the first time I hover over the element, the image size wont change.
Replacing the text and changing the position always works without issues.
Issue description:
First hover over element A: Image background size wont change at all.
Second hover over element B: Image background size changes, but has the correct size of element A
Third hover over element A: Image background size changes, but has the correct size of element B
How is this possible? I dont get whats happening here. The code works without any issues for the position, why the same logic does not work for the size.
Note: I am very new to unity, tinkering arround for a few days now, so I could be a very simple issue, but after researching for a few hours now, I haven't found anything useful.
After setting the text of a TextMeshProUGUI, the textBounds and its size are not updated until the following frame. This is caused by Unity's deferred layout rebuilding. In their UI Layout Documentation, it reads:
The rebuild will not happen immediately, but at the end of the current
frame, just before rendering happens. The reason it is not immediate
is that this would cause layouts to be potentially rebuild many times
during the same frame, which would be bad for performance.
I'm gonna go ahead and guess GamePlay.GetHoverCanvas() is maintaining an object that's never destroyed and, therefore, your code is working in all cases except its very first run.
There are probably a few ways you can go about fixing this but the easiest would be to call Canvas.ForceUpdateCanvases() between setting the text and reading the size of the textbounds. It's not the fastest function but it should do the trick.
Another option would be to, instead of a synchronous function, kick off a coroutine that sets the text one one frame, yields the next frame, then reads the textbounds on its second frame of execution. (This would likely lead to the size popping in and might look undesirable).
Instead of refreshing all your Canvases, you can use TMP_Text.ForceMeshUpdate(); right after you assign your string. This makes sure your text-information are correct within the same frame.
If the parents of your text uses Layout-Groups, there is a fair chance that you need to update those as well. You do that with LayoutRebuilder.ForceRebuildLayoutImmediate( [Layout-Group.RectTransform] ); (read more here)
Please note that the order is important.
Assign your text
Rebuild Layout-Group
Force Mesh Update
To prevent the need to force update the Canvas, you can use TextMeshProUGUI.GetPreferredValues() to ascertain ahead of time what the width and height would be for the text currently assigned to it. It'll return a Vector2. You can then use that Vector2 instead of TextMeshProUGUI.textBounds.
Or if you need the size for a certain text without actually assigning it, use TextMeshProUGUI.GetPreferredValues(string text).
There are other overloads of that method to get the size given constraints in either width or height.

How to place a label or a button exactly in the middle of the Form?

I can't find tools or properties to place a label or a button exactly in the middle of the Form. For example, on the X axis. VS 2015.
Design time :
In my VisualStudio2010 I have these 2 buttons to center horizontally and vertically:
Its located in the toolbar "Layout". If it isn't, you can add them by clicking the small button to the right. It is also in the Format menu.
To keep centered at Runtime: Turn off all anchoring.
Note:This will keep the control at its relative position as long as it doesn't change it Size. If it does, like autosize Labels are prone to, you will have to code the Resize event. Examples are here
For controls that may change in size, you need to catch the Resize event.
In my case I have a Panel, representing a page, inside another Panel which is the workspace. The workspace is set to autoscroll. In this scenario, it's important that the control is only centered when smaller than the container.
Whenever the form changes size or when I change the content, I call this function:
private void resetPagePos()
{
int wWS = pnlWorkspace.Width;
int hWS = pnlWorkspace.Height;
int wPage = pnlPage.Width;
int hPage = pnlPage.Height;
pnlPage.Location = new Point(Math.Max(0, (wWS - wPage) / 2), pnlPage.Top = Math.Max(0, (hWS - hPage) / 2));
}
The use of Math.Max(0, ...) makes sure that if the item doesn't fit, and the scrollbars are activates, then our page scrolls correctly. If the Left or Top are set to a negative number, you would get unwanted side-effects.

Unity 2D Setting the value of a Scrollbar to 0

I just wanted to set the value of the scrollbar of my scrollview object to 0 every time a new text object is created. This way the newest object would always remain at the bottom without needing to scroll down (think of the chat of a video game for example). My problem is that for some reason the program is setting the value to 0 and then creating the text object, so it keeps the 2nd newest object at the bottom of the scrollview.
My code:
//Creates the Text objects
private GameObject GenerateTextObject(string content)
{
//Creates the Text objects
GameObject messagePrefab = Instantiate(MessagePrefab);
Text textComponent = messagePrefab.GetComponent<Text>();
//Sets the content of the text and parent
textComponent.text = content;
messagePrefab.transform.SetParent(ContentPanel.transform, false);
ScrollbarVertical.value = 0;
return messagePrefab;
}
In the code I am setting the value at the end of the function, but still sets the value of the scrollbar to 0 (properly moving the scrollview to the bottom) before the object is created.
https://gyazo.com/897982521f13d7792ec26540490a40c0
In the Gyazo picture you can see how it doesn't scroll all the way down.
I have tried using a coroutine and waitForEndFrame aswell as waitforseconds(1), but none seem to work.
Edit: when loading up Unity and sending new messages to the scrollview, I see the scrollbar go all the way down and then really quickly move up just a bit hiding the new text object.
What you can do is create a new Scroll View and follow these steps:
The result is from calling GenerateTextObject(Time.time.ToString()); every second.
private GameObject GenerateTextObject(string content)
{
//Creates the Text objects
GameObject messagePrefab = Instantiate(MessagePrefab);
Text textComponent = messagePrefab.GetComponent<Text>();
//Sets the content of the text and parent
textComponent.text = content;
messagePrefab.transform.SetParent(ContentPanel.transform, false);
//Force reset to the bottom on each message
//ScrollbarVertical.value = 0;
return messagePrefab;
}
I have not come up with a proper fix for this problem (I honestly do not know the cause of problem), but I have put together a little work-around for this situation.
To do this I simply created a small function:
public void Testing()
{
if (testing == true)
{
ScrollbarVertical.value = 0;
testing = false;
}
}
This would be attached to the built-in functionality of scrollbars "On Value Changed (Single)". In order to allow this to work, I just change the bool "testing" to true after creating a text object on GenerateTextObject(string content).
This fix is really ugly, and probably would cause more problems in the near future, but for now, it is a quick and dirty solution for this problem.
Sorry to necro this, but I had a similar problem and googling couldn't find a good answer. So I'm posting the solution that I found.
I tried to change the scrollbar value to 1 or 0, depending on the direction, so that everytime I open a UI panel, it'll start at the top. But no matter what I did, nothing seemed to work.
So my solution is to change the Y position of the content instead of the scrollbar value.
My scrollbar direction is set at bottom to top. Then I take the sizeDelta of the content, divide it by 2, (since I need the value of 1 for the scrollbar.value). I then multiplied it by -1 (if I dont, it'll start at the bottom). Then I set that value as the Y position of the content.
So far it works like a charm everytime I open my UI panel.
Hope this helps anyone looking for a solution.

Visual C# creating list of items

I need to make an application that shows codes that update from time to time.
Now I got the back-end part covered but I am completely new to the front end part.
I'd basicly like it took look something like this:
Now the idea is that I'll have a List that holds items that should be converted to elements in the below visual list.. The logo is something I staticly added and that is anchored to the left top corner, I'd like to create the rest of the elemnents dynamically (I have a timed event loop to check if the list has changed, etc)
So I made the following method as a starting point:
private void createOTPEntry()
{
Rectangle entry = new Rectangle();
entry.Name = "entry1";
entry.Stroke = new SolidColorBrush(Color.FromRgb(0, 111, 0));
entry.Fill = new SolidColorBrush(Color.FromRgb(243, 86, 13));
entry.Height = 65;
entry.Width = 497;
Thickness margin = entry.Margin;
margin.Left = 10;
margin.Top = 83;
entry.Margin = margin;
entry.BringIntoView();
}
Now this isn't relative yet to the logo or anything.. But it's supposed to atleast be a white rectangle with an orange border.. And it's not showing. I tried to find if my rectangle object has like a "show" or "draw" method but it doesn't.. And I'm still miles away from what I really want..
So the logic of what I want:
Loop that goes over list elements and draws the visual list elements, probably a Rectangle containing 2 TextBoxes or Labels (one of the labels will change)
These elements will be added to another Container? that has a scrollbar in case there are to many elements.
I'd like some help of possible on what control do I choose to add the elements to? (the container witht he scrollbar?). How exactly do I show my rectangle? Is there an easy way to make them kinda automaticly space when I add them to the "parent control" (the scrollable container).
Thanks in advance for giving me a jump start!

Categories

Resources