State machine of the window animator controller

UI Window Management in Unity 5.3 (Part 2)

Time for part 2 of how you could manage your UI windows in Unity 5.3! Part 1 brought up some questions that I like to deal with first. Things like having only one root canvas for all windows, preloading windows to not have a lag when opening one and animations when opening/closing a window.

To follow my explanations more easily, check out the sample project I uploaded to bitbucket. Check out the single commits in the repository as well, they document how the additions are added one by one and what changed.

One canvas to render them all

An issue that we came up with in our own project as well is that each window in its scene has an own canvas. This is required to separately test a window, otherwise it won’t be visible when running the window scene.

But in a project you want to have only one single Canvas to put your windows under. The Canvas object contains some common settings that you don’t want to setup for each window separately:

  • Canvas
    • Render Mode
    • Sort Order
  • Canvas Scaler
    • UI Scale Mode
    • Reference Resolution and Screen Match Mode

Adding one common UI root object

So let’s just add a single UI root game object to the Root scene. This will be used to anchor all open windows underneath it. Just set it up the way you UI should work.

Single UIRoot game object
Single UIRoot game object

Adjusting the window manager

Now the WindowManager has to use the UI root. So first of all we will add a reference to the UIRoot game object to WindowManager script and link it in the Root scene.

Reference to UI Root in WindowManager
Reference to UI Root in WindowManager

Anchoring window roots when opening window

After a window was opened, its root transforms has to be re-parented underneath the UIRoot transform. We are already collecting the window root transforms after loading the window scene, so we just have to set their parents and adjust their RectTransform s so they are still stretched across the whole canvas area.

The method is just called after the window was loaded in the DoOpenWindow method.

Removing re-parented window roots on closing

As we moved the window roots under the UI root, unloading the scene won’t correctly remove the re-parented window root objects as they don’t belong to the scene anymore.

Therefore we have to remove all remaining window root objects manually after unloading the scene in the CloseWindow  method.

Disabling the window canvas

So right now we just re-parented the window roots under a common UI root object that has an own canvas and canvas scaler. The window canvases and canvas scalers still exist on the window root objects and are active.

Fortunately Unity takes care about it itself. Child canvases automatically inherit the settings from its main canvas and canvas scalers are automatically disabled. So there is nothing to do for us 🙂

Render order

Right now our windows are rendered in the order they are opened. This might be correct most of the time. But having a way to set the render order manually is often required when your windows are opened quite independent from each other.

For a canvas you can set the Sort Order in the inspector.

Canvas Sort Order
Canvas Sort Order

Unfortunately it is disabled by default when you re-parent the canvas under another one. So we have to enable the overrideSorting  flag after we moved the window canvas in our AnchorWindow  method:

This gives you the possibility to set the sort order for each window and they are correctly rendered independent from their loading order. The higher the sort order number, the more in front the window will be rendered.

Preloading windows to avoid lag

The performance question came up for a bunch of times. In fact there is a small lag when a window is opened as a new scene has to be loaded and a bit of setup stuff has to be made.

We can’t really remove the loading time, but what we can do is loading windows that are used regularly at startup and just showing/hiding them instead of loading/unloading them completely.

Specifying windows to preload

Therefore we add a list of window ids that should be preloaded to the window manager that we load when the window manager is started.

To not duplicate the code for loading a window, it was moved out of the DoOpenWindow  method and moved into an own DoLoadWindow  method which is used for preloading and for loading when opening a window.

Setting the ids of the windows that should be preloaded
Setting the ids of the windows that should be preloaded

Using preloaded window on opening window

When a window should be opened we now have to check if the window is already preloaded in the OpenWindow  method. In that case we don’t have to create a new window, but only show it and add it to the open windows.

In the DoOpenWindow  method we check if the window is already loaded and just show it if it is.

Hide instead of unload on closing window

When closing a preloaded window, we just want to hide it instead of unloading it. So we have to check if it is in the preloaded windows.

With these changes, windows will show up almost instantly. To haven’t too much game objects loaded at one time, preloading should possibly only used for windows that are used regularly in your game. Furthermore keep in mind that populating a window with the data it should show will take up time as well. If the data is is dynamic this will cause a bit of a delay anyway when the window is opened, no matter if the window is already preloaded or not.

Animations for windows

Animations are a pretty big topic and I will probably write some distinct posts about it. But as the question came up I just added an open and close animation to one of the popups of the example application.

Creating the animations

The first step was to create the two animations. I went for two simple ones for opening and closing the window:

  • Open Animation: Scales the game object up from 0 to 1 and rotates it by 360 degrees counter clockwise
  • Close Animation: Scales the game object down from 1 to 0 and rotates it by 360 degrees clockwise

The animator controller, which will be put onto the window’s Animator , has two states, Open and Closed, and one bool parameter is_open  which indicates if the window is currently open or closed. The Closed state is active when the is_open condition if false, the Open state is the default one.

State machine of the window animator controller
State machine of the window animator controller

Now we just have to add an Animator  on to the root panel of the window (not the Canvas, but the game object directly below it) and reference the created animator controller.

Adjusting the window manager

Right now the window manager immediately closes the window or hides it when the window is closed. What we want is to give the control to the window presentation, which will play the close animation, and only afterwards cleaning up the window.

This is a bit tricky as you want the window manager as independent from the final look and feel of the windows as possible. Therefore I created an interface ( IWindowClosingHandler ) which indicates that a script takes care about closing a window. When a window should get closed, the window manager searches for this interface on the window’s root objects. If it can’t find one it cleans up the window immediately as before.

If the interface is found, the control is given to the script that implements it by calling its OnWindowClosed  method. Furthermore a callback action is passed to the script which has to be invoked by the handler when it has finished its work.

Adding an animated closing handler

Now to connect the two parts, I added a WindowAnimator  which implements the IWindowClosingHandler  interface. It just switches the bool parameter is_open  to false as soon as the OnWindowClosed  method is called and waits until it gets the OnWindowCloseFinished  animation event to invoke the callback. The animation event is triggered at the end of the close animation.

What has to be considered is that the close animation is really triggered by changing the animator parameter and that it triggers the animation event. Otherwise the cleanup of the closed window won’t be performed, so it stays in the scene.

I am not 100% happy with the solution as those mistakes in the setup can cause the window manager to not unload closed windows. It separates the window management from the window presentation quite good though.

It’s a similar problem with many events that occur right before an object should be destroyed (e.g. death animations). If you have any hints how to maintain a clean separation but also have the logic not depend on a callback to be triggered by the presentation, I would be very happy to hear from you! 🙂

Conclusion

With this post I added some requested features to the window management system which can be used with Unity 5.3. I was very happy to have received some feedback on the first part, especially in the Facebook Indie Game Developers group. Hope those additions showed up how a window management system can be further extended.

If there are more topics about window management that you like to have covered, don’t hesitate to comment here or get in contact with me! I would be happy to do another part with your requests. The next part will also contain explanations how to use our Unity asset Data Bind for Unity to populate UI windows with the data they should visualize.

If you haven’t already, now would be a good time to checkout the repository with the sample project and play around a bit with it yourself. Let me know if you have any questions! 🙂

  • Felix

    Hi and thanks for the article.

    Have you thought about making the Window Manager a Singleton and letting the Windows self register to the manager where references to them are cached? I myself always try to avoid GetComponentsInChildren by any cost :).

    • coeing

      Hi Felix!
      Sorry for the late reply, unfortunately the mail system on the blog was broken, so I didn’t get informed about your comment.

      That’s true, GetComponent should be used wisely. But as it’s only used when a window is opened and when it is closed, I felt it is a better option than having a singleton.