Unity: animated ellipsis
Posted by Dimitri | Mar 15th, 2013 | Filed under Featured, Programming
We have all seen it on loading screens of a variety of games and applications. I’m talking about animated ellipses, the animated three dots (…) on the end of a sentence which indicates that the game or software is being loaded.
This post explains how to make exactly that, an animated ellipsis in Unity. Here’s a video showing what the script on this post will do:
As usual, an example Unity project is available for download at the end of the post. There are basically two types of progress visual indicators and an animated ellipsis is no exception: it can be either determinate or indeterminate. The determinate type is when the current progress of a operation is known and can be measured throughout the whole operation. The indeterminate type is exactly the opposite: the progress isn’t known, we can only be sure whether the operation is still taking place or if it has already been completed.
With that in mind the below script takes care of those two cases. Here’s the code:
using UnityEngine; using System.Collections; public class AnimatedEllipsis : MonoBehaviour { //The string that will be displayed alongside with the ellipsis public string text; //Variables for both animation methods private int ellipsisAnimator = 0; public int numberOfDots = 3; public bool isCompleted = false; //Just a single variable for the indeterminate method private int initTextLength; //Variables specifically created for the determinate method private int oldEllipsisAnimator = 0; private int progressPerDot = 0; public int minProgress = 0; public int maxProgress = 100; private int progressRange = 0; /*This boolean flags whether to animate the ellipsis using the determinate or * the indeterminate method. */ public bool indeterminate = false; //Define the delegate that points to the method that runs when the process is completed public delegate void OnCompletionDel(); //Create an instance of the above delegate public OnCompletionDel onCompletion; void Start() { //Check if 'maxProgress' is bigger than 'minProgress' if(this.maxProgress > this.minProgress) { //The range defined by the maximum and minimum values this.progressRange = this.maxProgress - this.minProgress; //Calculate the value that adds a single dot the ellipsis (determinate mode only) this.progressPerDot = this.progressRange/this.numberOfDots; //Set 'ellipsisAnimator' and 'oldEllipsisAnimator' to zero, this.ellipsisAnimator = 0; this.oldEllipsisAnimator = 0; } else //'maxProgress' is smaller than 'minProgress' or the other way around. { Debug.LogError("Either 'minProgress' is bigger or equal to 'maxProgress' " + "or 'maxProgress' is smaller or equal to 'minProgress'. Double check these " + "values at the Inspector."); //Disable this script this.enabled = false; } } //This IEnumerator will animate the ellipsis (both in the indeterminate and determinate modes) private IEnumerator AnimateEllipsis(float rateOrProgress) { //If indeterminate is true if(indeterminate) { //Save the initial text length at the 'initTextLenght' variable this.initTextLength = this.text.Length; while (!this.isCompleted) { /*Increase the value of 'ellipsisAnimator' and get the rest of the division * by 'numberOfDots'. Save the results back at 'ellipsisAnimator' */ this.ellipsisAnimator = (++this.ellipsisAnimator) % this.numberOfDots; /*Test if the current 'text' string length is bigger the maximum expected * length */ if(this.text.Length >= this.initTextLength + this.numberOfDots) { //Remove the dots that are in excess this.text = this.text.Remove(this.initTextLength); } else //The 'text' string hasn't yet reached its maximum length { //Add one dot to it this.text += "."; } /*Make the code execution wait for the time defined at 'rateOrProgress' * (in seconds)*/ yield return new WaitForSeconds(rateOrProgress); } /*If the code has reached this line, the boolean 'isCompleted has been * set to 'true'. */ //Check if the 'onCompletion' has been assigned if(this.onCompletion != null) { //Run the method that this delegate points to this.onCompletion(); } } else //Indeterminate is false { /*Whether the progress passed as a parameter is is smaller than 'maxProgress' * Also, check if progress is completed. */ if (rateOrProgress + this.minProgress < this.progressRange && !this.isCompleted) { /*Divide the current progress by 'progressPerDot' and convert /* it to an integer */ this.ellipsisAnimator = (int)(rateOrProgress/this.progressPerDot); //If 'ellipsisAnimator' is bigger than its previous value if(this.ellipsisAnimator > this.oldEllipsisAnimator) { //Add a dot the the ellipsis this.text += "."; //Assign the value of 'ellipsisAnimator' to 'oldEllipsisAnimator' this.oldEllipsisAnimator = this.ellipsisAnimator; } } else { //Display a message at the console Debug.Log("Completed"); //Set 'is_Completed' to true this.isCompleted = true; //Check if the 'onCompletion' has been assigned if(this.onCompletion != null) { //Run the method that this delegate points to this.onCompletion(); } } } } //Encapsulate the coroutine invocation public void Animate(float rateOrProgress) { StartCoroutine(AnimateEllipsis(rateOrProgress)); } //When this script is disabled, stop all pending coroutines void OnDisable() { this.StopAllCoroutines(); } }
The first declared variable is a string that will be later concatenated with the animated ellipsis. It has been declared as public so that it can be easily edited at the Inspector. This is the string to store the sentence that to goes with the animated ellipsis like, for example, “Loading” and “Please wait” (line 7).
The next three variables, as stated in the code’s comments, are used for both indeterminate and determinate animations. The first one, an integer named ellipsisAnimator is used as a counter that gets incremented and its value will animate the ellipsis. The second one, also an integer, defines the number of dots this ellipsis will have. The third one is a boolean for reading/setting whether the end of the progress has been reached (lines 10 through 12).
Then, another integer is declared, just for the indeterminate ellipsis animation. This one will later store the initial text string length.
Moving on, five integers are declared in a row, and all of them are used for controlling the determinate ellipsis animation (lines 18 through 22). The first integer will store the previous value of the ellipsisAnimator counter (line 18). The second variable from this group will store the relationship between the current progress and the number of dots (line 19). The third and forth integers are there for storing the minimum and maximum progress values (lines 20 and 21). Finally, the last integer declared will save the range between minProgress and maxProgress (line 22).
A boolean is being declared at line 26. It flags whether this animated ellipsis is determinate or indeterminate. The last two things being declared in this script are a delegate definition and a delegate instance that uses that definition (lines 29 and 32). This delegate will call a method that returns void when the end of the ellipsis animation is reached. Delegates are like function pointers in C++. For more on C# delegates, click here.
Inside the Start() method a if-else statement checks if maxProgress is bigger than minProgress (line 38). That being true, the progressRange is calculated by subtracting maxProgress from minProgress (line 40).
With that, it’s easy to calculate progressPerDot, a value used for determining how much progress translates to a single ellipsis dot, for the determinate animation (line 42). Eg.: If numberOfDots is 4 and progressRange is 100, when the progress reaches 25, one dot is added to the ellipsis. Also, the values for the ellipsisAnimator and oldEllipsisAnimator are set to zero, just in case they were storing something else (line 45 and 46).
Otherwise, if maxProgress is smaller than minProgress, an error message is printed out on the console, and this script is disabled (lines 48 through 56).
Finally, we reach the private method AnimatedEllipsis() which returns a IEnumerator, so it can be yielded and called as a coroutine (line 60). It’s implementation is basically an if-else statement that checks whether this is a indeterminate or determinate animated ellipsis.
The float parameter this method takes, named rateOrProgress, works as a rate (in seconds) in which to add dots to the ellipsis when it’s indeterminate and as a way to update the progress, when using a determinate ellipsis animation.
Case it’s indeterminate, the length of the text string is stored at initTextLength (line 66). That way, it’s possible to know the original length of the string and add or remove dots to the ellipsis according to the result of the sum of this variable with numberOfDots.
This is exactly what line 72 achieves: it increments the ellipsisAnimator, divides it by numberOfDots and stores the rest of the division back at ellipsisAnimator.
In other words, the increments made to the value of ellipsisAnimator wrap back to zero when it is bigger than numberOfDots. Next, an if-else statement tests if the current text string length is bigger than the initial length added with the numberOfDots (line 76). If it is, all added dots of the ellipse are removed (line 79), returning the text back to its original state.
The else part of the aforementioned if-else statement just concatenates a dot to the string, meaning that the current text string length hasn’t surpassed the initial length added with the numberOfDots (lines 80 through 84).
Before exiting the loop, the yield adds a pause between each loop iteration. The duration of this pause is given in seconds by the variable rateOrProgress, passed to the method as a parameter (line 89).
Jumping back to a previous line of code from this script, since the animation is indeterminate, all of the logic of the previous four paragraphs took place inside a while loop, that will keep running until the value of isCompleted is set to true (line 68). This is done by setting this boolean member variable value from outside this script.
After exiting the while loop, if it has been assigned, the method that the delegate points to is called (lines 96 through 100).
When the ellipsis animation is set to determinate, the first thing being done is to check if the value of isCompleted hasn’t been set to true and whether the progress passed as a parameter is smaller than maxProgress (line 106). That being the case, the value of the progress is divided by the value of progressPerDot and cast into an integer.
That way, when progressPerDot is 25 and rateOrProgress is 24, the division will return 0. It will only return 1 when rateOrProgress is between 25 and 49. It will return 2 when rateOrProgress is between 50 and 74, and so on.
Right after that, an if-else statement tests if the result of the aforementioned division stored at ellipsisAnimator is bigger than it’s old value (line 113). Case it is, a single dot is concatenated to the end of text (line 116) and the oldEllipsisAnimator is set to store the current the ellipsisAnimator value (line 118). By doing so, this ensures that a single dot is added to the text string, one at a time.
When the rateOrProgress value is bigger than maxProgress or isCompleted has been set to true, “Completed” is printed out at the console, isCompleted is set to true (if it hasn’t already) and, if it has been assigned, the method that the delegate points to to is called (lines 124 through 134).
That’s all there’s to it for the AnimateEllipsis() method. It has been encapsulated by Animate() (lines 140 through 143) for the reasons described at the ‘Final Thoughts’ (see below). Additionally, any pending coroutine is removed when this game object is disabled (lines 146 through 149).
Using this script
To use this script, just attach it to a game object, and at the Inspector, set the text, check whether the animated ellipsis is indeterminate and the number of dots. Then, for a indeterminate ellipsis add these lines of code at another script attached to the same game object:
//Declare an animatedEllipisis instance private AnimatedEllipsis animatedEllipsis; void Start () { /*Initialize the animatedEllipsis. Or just make it public * and initialize it at the Inspector. */ this.animatedEllipsis = this.GetComponent<AnimatedEllipsis>(); //Initialize the delegate this.animatedEllipsis.onCompletion = DisplayMessageOnCompletion; //Start the animation of the indeterminate animated ellipsis this.animatedEllipsis.Animate(1f); } void OnGUI() { //Render the animated ellipsis GUI.Label(new Rect(100,100,200,200), this.animatedEllipsis.text); } //This method will be referenced by the 'animatedEllipsis' delegate public void DisplayMessageOnCompletion() { this.animatedEllipsis.text = "Completed!"; }
And for the determinate ellipsis animation method, you would have something like this:
//Declare an animatedEllipisis instance private AnimatedEllipsis animatedEllipsis; void Start () { /*Initialize the animatedEllipsis. Or just make it public * and initialize it at the Inspector. */ this.animatedEllipsis = this.GetComponent<AnimatedEllipsis>(); //Initialize the delegate this.animatedEllipsis.onCompletion = DisplayMessageOnCompletion; this.StartCoroutine(BackgroundOperation()); } void OnGUI() { //Render the animated ellipsis GUI.Label(new Rect(100,100,200,200), this.animatedEllipsis.text); } private IEnumerator BackgroundOperation() { yield return //An object to wait for, a 'new WaitForSeconds()' call, etc //Update progress this.animatedEllipsis.Animate(10); //Do whatever needs to be done and update progress again this.animatedEllipsis.Animate(25); //.... More lines of code with the appropriate Animate() method calls... //And so on, until Animate() receives the value defined for 'maxProgress' this.animatedEllipsis.Animate(100); } //This method will be referenced by the 'animatedEllipsis' delegate public void DisplayMessageOnCompletion() { this.animatedEllipsis.text = "Done!"; }
Final Thoughts
The reader might be wondering why I haven’t created one AnimatedEllipisis abstract base class and two child classes, one for each type of animated ellipsis. Although this would have been a much better implementation, I have put together everything on a just one script to make it simpler to explain this code on a single, contiguous text. Additionally, it’s easier to spot the differences in the logic between the determinate and indeterminate animation modes.
Also, I’ve encapsulated the StartCoroutine() method with the Animate() method because I’ve wanted the AnimateEllipsis() method to be called as a coroutine by the AnimateEllipsis script and not by the calling script. Futhermore, this makes the code far more readable. Also, this is the reason why the above AnimatedEllipsis script inherits from Unity’s MonoBehavior class, so it can call a coroutine.
That’s it! Please, feel free to leave a comment or suggestions.
Downloads
Be the first to leave a comment!