Unity: expandable GUI Window
Posted by Dimitri | Mar 6th, 2013 | Filed under Programming
This Unity programming tutorial shows how to create a GUI window that changes between two pre-determined sizes, in other words, an expandable GUI window that can be shrunk down, back to its original size. The code on this post has been developed using Unity 4.0.1f2, and an example project is available for download at the bottom of this page.
Unity’s GUI windows elements are very useful, not only because they can visually group various GUI elements, but also due to the possibility of repositioning it on the screen. For that reason, the script features some additional logic that allows for the windows to be resized at the correct position on the screen, even if it has been dragged by the user.
Here’s a short video of what the code below achieves:
Here’s the code:
using UnityEngine; using System.Collections; public class ExpandableWindow : MonoBehaviour { //The rectangle that defines the current window position and area private Rect windowRect; //The rectangle that holds the window values before it has been compacted / expanded public Rect oldWindowRect = new Rect(100, 20, 200, 200); //The window width and height when it's expanded public int expandedWidth = 400; public int expandedHeight = 400; //Whether the window is expanded or compacted private bool expanded = false; //Whether the window is currently changing it's size private bool changingSize = false; //A float variable used for interpolation private float t = 0; void Start() { //Initially, 'windowRect' should have the same value as 'oldWindowRect' this.windowRect = this.oldWindowRect; } void OnGUI() { //Render a window and save its position and size at 'windowRect' this.windowRect = GUI.Window(0, this.windowRect, this.WindowMethod, "Window Title"); } //The window code void WindowMethod(int windowID) { //If the window is expanded if(expanded) { //Render the 'Compact' button and define what happens when it's pressed: if(GUI.Button(new Rect(this.windowRect.width - 110, this.windowRect.height - 40, 100, 30), "Compact") && !this.changingSize) { //Set 'expanded' to false this.expanded = false; //Start the 'Compact' Coroutine (defined below) StartCoroutine(Compact(0.03f)); } } else //Window is compacted { //Render the 'Compact' button and define what happens when it's pressed: if(GUI.Button(new Rect(this.windowRect.width - 110,this.windowRect.height - 40,100,30),"Expand") && !this.changingSize) { //Set 'true' to false this.expanded = true; //Start the 'Expand' Coroutine (defined below) StartCoroutine(Expand(0.03f)); } } //If the window size isn't being changed if(!this.changingSize) { //Make the entire window background to act as a drag area GUI.DragWindow(); } } //The coroutine for expanding the window private IEnumerator Expand(float rate) { //Update the 'oldWindowRect' with the current window position this.oldWindowRect.x = this.windowRect.x; this.oldWindowRect.y = this.windowRect.y; //Calculate the new window 'x' position. float newXpos = this.oldWindowRect.x - (this.expandedWidth - this.oldWindowRect.width)/2 ; float newYpos = this.oldWindowRect.y - (this.expandedHeight - this.oldWindowRect.height)/2 ; //Set 'changingSize' to true, so that the window can't be dragged while it's being expanded this.changingSize = true; //If 't' is smaller than one while (this.t < 1.0f) { //Increment the variable 't' based on 'rate' passed as an argument this.t += rate; /*Expand the window by interpolating the width, height, x and y positions to their new * values*/ this.windowRect.width = Mathf.SmoothStep(this.oldWindowRect.width, this.expandedWidth, t); this.windowRect.x = Mathf.SmoothStep(this.oldWindowRect.x, newXpos, t); this.windowRect.height = Mathf.SmoothStep(this.oldWindowRect.height, this.expandedHeight, t); this.windowRect.y = Mathf.SmoothStep(this.oldWindowRect.y, newYpos, t); //Yield (wait) the code execution for the time defined at 'rate' yield return new WaitForSeconds(rate); } //Set 'changingSize' to false. This makes the window draggable again. this.changingSize = false; } //The coroutine for compacting the window private IEnumerator Compact(float rate) { //Update the 'oldWindowRect' with the current window position this.oldWindowRect.x = this.windowRect.x; this.oldWindowRect.y = this.windowRect.y; //Calculate the new window position. float newXpos = this.oldWindowRect.x + (this.expandedWidth - this.oldWindowRect.width)/2 ; float newYpos = this.oldWindowRect.y + (this.expandedHeight - this.oldWindowRect.height)/2 ; //Set 'changingSize' to true, so that the window can't be dragged while it's being compacted this.changingSize = true; while (this.t > 0.0f) { //Decrement the variable 't' based on 'rate' passed as an argument this.t -= rate; /*Compact the window by interpolating the width, height, x and y positions to their new * values*/ this.windowRect.width = Mathf.SmoothStep(this.oldWindowRect.width, this.expandedWidth, t); this.windowRect.x = Mathf.SmoothStep(newXpos, this.oldWindowRect.x, t); this.windowRect.height = Mathf.SmoothStep(this.oldWindowRect.height, this.expandedHeight, t); this.windowRect.y = Mathf.SmoothStep(newYpos, this.oldWindowRect.y, t); //Yield (wait) the code execution for the time defined at 'rate' yield return new WaitForSeconds(rate); } //Set 'changingSize' to true, so that the window can't be dragged while it's being compacted this.changingSize = false; } }
At the beginning of the script, seven member variables are being declared. The first two are Rects that are responsible for storing the current and previous window position and area (lines 7 and 9). Then, two integers are being declared, and their purpose is to set the the window width and height when the window is expanded (lines 12 and 13). After that, two booleans are being declared, the former to flag whether the window is at its expanded or compacted state; the latter, to tell if the window is currently having its width and height changed (lines 16 and 19).
Finally, at line 22, a float named t is being declared and initialized to 0. This member variable is responsible for controlling the window width and height interpolation when its size is being changed (more on that later).
The Start() method just copies the values from oldWindowRect to windowRect. By doing so, the initial position of the GUI window is set (lines 24 through 28).
The OnGUI() method renders the window GUI element, which is achieved by calling the GUI.Window() method (line 33). This version of the GUI.Window() method takes four parameters. The first one is an integer with the window ID. It has been set to zero since the code above only manages a single window. The second parameter is a Rect that defines the window position and size, so windowRect is being passed.
A delegate is a reference to a function, much like a function pointer in C++, and it’s being passed as the third parameter. The function it makes a reference to is a method that renders the contents of the window. In this example, the delegate of the method passed as parameter is a reference to WindowMethod(). The fourth and final parameter is the title of the window, which is rendered at the top of the window.
As has been just explained, WindowMethod(), for the above script, is the one responsible for rendering the contents of the window (line 37). It contains an if-else statement that checks whether the window is in an expanded or compacted state. If the window is expanded, a Button is rendered at the right bottom corner of the window, using the window’s width and height as reference (line 43). That way, when this GUI Button is pressed, the button stays at the same distance from the borders of the window, even if its size has changed.
This GUI.Button() is being enclosed by an if statement, which tests if the the button has been pressed and whether the window isn’t currently changing its size (line 43) . When those two conditions are met, expanded is set to false and a coroutine is started by passing a delegate of the Compact() method with a float as parameter to the StartCoroutine() method (lines 46 and 48). As the method name suggests, this makes the window shrink back to it’s original size as specified at oldWindowRect.
There are good reasons to use a coroutine to do this. It results in a cleaner code because it’s possible to put the logic separately and the Expand() and a Compact() methods. Also, it saves some variables that would otherwise be needed to control the state of the window, which is one of the main benefits of coroutines. Additionally, coroutines can be yielded at a predefined rate, so it’s easier to set the rate in which the window is being expanded or compacted.
Back to the outermost if statement of the WindowMethod() method, if the window isn’t expanded (compacted), another GUI Button is rendered at the same position as the ‘Compact‘ button. This one has the label ‘Expand‘ on it and as the name suggests, it expands the window to meet the width and height specified at the expandedWidth and expandedHeight variables (line 54). When this Button is pressed and the window isn’t currently changing its size, expanded is set to true, the StartCoroutine() method is called passing a delegate of the Expand() method with a float parameter (lines 57 and 59).
Lastly, the final if-statement inside WindowMethod() makes the whole window draggable only when its size isn’t being changed (lines 64 through 68).
Moving on, the Expand() method returns a IEnumerator, meaning that it can be yielded and be called as a coroutine (line 37). It takes a float as a parameter named rate. This parameter isn’t only to set the interval between the while loop iterations inside the method, but also to increment the t variable, which is being used for interpolating the window width and height to the desired width and height on this method.
The first thing Expand() does is store the currentWindow x and y coordinates as the x and y attributes of oldWindowRect (lines 75 and 76). This needs to be done because, otherwise, the window would simply reset back to the initial x and y coordinates defined at oldWindowRect. For instance, the above script would make the window “jump” back to x: 100, y:20 when the button ‘Expand’ got pressed.
Then, the newXpos and newYpos float variables are calculated (lines 79 and 80). These are the positions in which the window must be moved to compensate for the fact that GUI window elements, as any other GUI element in Unity is rendered using a top-left coordinate system. In other words, if the width and height values of the window are increased without calculating this offset, the window would appear to be expanding only at its bottom and right borders. The newXpos and newYPos are offsets that makes the window appear to expand equally from its center.
Just before starting the while loop, changingSize is set to true, meaning that the window will not be able to be dragged and this method will not be accidentally called again, because, as previously explained, this variable is has been used on all if statements at the OnGUI() method (line 83).
The while loop checks if the value of the variable t is smaller than one (line 86). This loop increments the variable t by the value passed as the parameter rate (line 89). Most importantly, it interpolates the position, width and height of the window to the recently calculated position, and the width and height defined by the expandedWidth and expandedHeight variables (lines 93 through 97).
Almost all of the interpolation methods from Unity’s Mathf class are based on the same logic: there’s a minimum and maximum values and a third variable that controls the interpolation between those two. This third value makes the interpolation method return the minimum if it’s 0, the maximum, if it’s 1, and the intermediate value between the two if it’s 0.5. In the above script, this variable is the float t that has been used as the loop condition and to control the interpolation between window sizes. So, when the interpolation has reached the maximum value (t > 1.0), the code should exit the loop. Otherwise, it should continue executing the loop.
Also, line 100 yields the code execution by the value passed as the parameter rate. This makes the coroutine gradually expand the window size. Finally, after exiting the loop, changingSize is set to false, so the window can be dragged again (line 104).
The Compact() method works exactly as the Expand() method (lines 108 through 140), except this time, the window width and height needs to be interpolated from expandedWidth and expandedHeight back to its original values. This means that the condition for the while loop inside this method must test whether t is greater than zero and interpolate the width, height, x and y coordinates of the window. The rest of the logic and the code is just about the same.
That’s it! Is there something missing? What do you think? Please leave your comments below!
Thank you very much for this post. I’ve been looking for something pretty close to this, and with your code as a base I think I can achieve what I’m wanting. Great Tutorial, thank you.
Hey, I just wanted to let you know that it worked perfectly, thank you so much for this!
Thanks!