Making a Slick User Interface in Godot Part 1

In the time that I have been making games, I have come to realize that the one of the largest contributing factors to making a game “feel” polished is the menus and the user interface. That is not to say that juice with the mechanics isn’t also important, but if you were to go and play some of your favourite games you will see that a lot more work went into the menus and the user interface than you realize.

I will be the first to admit that I am not a professional UI/UX designer, but I think that I have picked up a couple of tricks that can help. Most of the tricks that I have learned can be achieved with the Control Nodes, and an Animation Player Node. I am confident that a Tween Node can achieve the same result, but I personally find it faster to do with an Animation Player.

The easiest way you can tell how much effort has gone into an indie game is to look at how the game handles transitions from one scene to another. To look at an example of how to do this wrong, take a look at my older games Pixel Kitchen or Endless Abyss. Pixel Kitchen has a main menu and when you press the play button, the game jumps immediately to game play and it can be quite jarring, and Endless Abyss behaves in a very similar manner but I tried to be a little bit more clever.

Adding a very simple fade to black animation not only fixes how jarring the transition is, but it also obfuscates the game engine changing backgrounds and initializing objects and interface elements from the player. The polish to effort ratio on this transition animation is quite high. To do this every scene that you can transition to will need to have the same couple of nodes added for the effect.

We will need to add a Control Node and nested in that is a Colour Rect Node. Both of these nodes need to have their layout anchors set to “Full Rect” so that they take up the entire space of any Camera or Viewport that is active in the scene. The Colour Rect will need to be opaque by default, otherwise the animation will not look correct. An Animation Player Node will also need to be added to the scene, and a reference to it will need to be added in code.

onready var anim = $AnimationPlayer;

With that basic set up out of the way, the next step is to set up the transition animations. Each scene is going to need 2 animations, a fade in and a fade out. We can create the animations by selecting the Animation Player Node, and then at the bottom of the screen click on the “Animation” button and select “New Animation”.

The Animation Player in Godot is a lot like the Animation Player in Unity3D. You can animate everything that has a property that is accessible from the editor. The fade to black transition is going to “Animate” the Modulate property of the Colour Rect Node. When we fade in, we will animate the alpha channel from 255 to 0, making the black disappear, and when we fade out we will do the opposite.

When we fade out of the room, the box will turn black over 1 second, and we do the opposite when we fade in.

What we have done so far is create the animations, but we are not switching rooms yet. This is where the magic of the Godot Animation Player comes in, you are able to execute code and scripts from the Animation Player. Knowing that, we now need to create a function to switch to a new scene that can be called from the Animation Player.

func _update_Scene():
     var _scene = get_tree().change_scene("res://Path To Scene");

Once the script is finished we need to add it to the “Fade Out” animation. This block of code is not required on the fade in animation, as you are not switching rooms when you fade in.

To add the script to the Fade Out animation, click on the “Add Track” button (as seen in the screenshot below) and add a “Call Method Track”. Once the track has been added, we move the animation head to the final frame and add our function as the key on the track. Now on the final frame of the animation, the engine will switch rooms from one room to another and it should be completely transparent to our players.

Now that the animation is finished, we need something to trigger it. We can have this set up to be triggered by whatever we want. Probably the easiest way to do this is to have some sort of button that emits a signal when its pushed connected to a function that plays the animation.

func _fadeOut():
    anim.play("Fade Out");

That is only one way to trigger this animation. We can have this function connected to a player death signal, or a collision box that is at the border of a scene, or any number of other situations that would require transitioning the player from one screen to another.

What you may have noticed if you try to run this in your game, right now, is the game will fade out and will not fade back up. If you are wondering why that is, it is because we currently do not have anything calling the fade in animation.

What we need to do now, is move the contents of our _Ready() function into a different function and have the _Ready() event play the fade in animation. Finally, the fade in animation will need to call the new ready function on the final frame of the fade in just like with the Fade Out.

func _Ready():
    anim.play("Fade In");

func _New_Ready():
    Your Ready Code Goes Here Now

And that is it, a very basic scene transition animation. Having the default animation time of 1 second may be too long because you have 2 animations playing back to back. We can tweak the time to make sure that it feels just fright.

To have a little bit more flexibility with the type of wipe / transition, that is playing, we could replace the Colour Rect node with a Texture Rect node to have whatever custom type of transition / wipe that we want.

To do that we would need to draw the custom transition in the graphics editor of our choice, export the animation so that each frame is its own png file (Texture Rect doesn’t support Sprite Sheets) and key each frame of the animation.

I was planning on explaining more of how the menus work in Captain Skyrunner and my new game Hexes, but this post is already over 1000 words, so I think that I am going to need to do a part 2 and probably a part 3 to this series.

Until Next Time
– Steven

Adapting Mobile Games for a Notch in Godot

I dislike the notch on new phones and so far I have avoided phones with a notch. When I bought my Pixel 3, I specifically bought the smaller phone because it did not have a notch.

Regardless of what you may or may not think about notches, they seem to be here to stay. Even the new Macbook Pro has a notch in it’s display. So if they are here to stay, we might as well learn how to handle them in our games.

What happens to your game if you do not properly account for a notch? Well, the notch cuts out a small little section of your screen, but anything that is “behind” the notch will still be there. This includes game objects, like the player, terrain or enemies; but, it also includes GUI elements like health bars and score counters.

Taking a screenshot doesn’t show the notch, it renders normally. The only way to demonstrate the notch cutting off the GUI is to take a picture of the actual phone.

Knowing that everything cut off by the notch is still there, we just need to push any GUI elements down. There must be some sort of way to do it in code right? When we look at the Godot API and Documentation, we can find a function that sounds like it will do what we want called get_window_safe_area() that is part of the OS class.

The description of this function says “Returns unobscured area of the window where interactive controls should be rendered.” This sounds exactly like what we want. The function returns this area as a Rect2. A Rect2 has a Vector2 as the starting point (top left), a width, and a height value.

The easiest thing to do would then to put all of our GUI elements inside of a control node (itself inside of a Canvas Layer), and use this function to get the top left corner of the safe area, then apply the width and height to the control node. Ideally this will return a rectangle that takes up the full width and height of the screen up to the notch.

func _ready():
var GUI_Control = $canvaslayer/control;
var safe_area = OS.get_window_safe_area();
var pos_x = safe_area.position.x;
var pos_y = safe_area.position.y;
var safe_width = safe_area.end.x;
var safe_height = safe_area.end.y;
var safe_position = Vector2(pos_x, pos_y);
var safe_size = Vector2(safe_width, safe_height);
GUI_Control.set_position(safe_position, false);
GUI_Control.set_size(safe_size, false);

Unfortunately, this doesn’t work 100% of the time. This method does not take into account any scaling that the engine does. For example, Captain Skyrunner was developed with a resolution of 360 by 640 with the stretch mode set to expand. The iPhone 11 has a resolution of 1792 x 828. When I ran this code the scaling being applied pushed my GUI elements off screen.

Oh no, you can’t see or press the buttons anymore.

Now, when I was at this stage myself, I got completely stuck and was very frustrated with this bug. Because I am not the best programmer in the world, I went to the Godot Github page and asked for some help. Thankfully the user kleonc was kind enough to help me out and came up with a solution that resolved the issue.

func _ready():
var window_to_root = Transform2D.IDENTITY.scaled(get_tree().root.size / OS.window_size)
var safe_area_root = window_to_root.xform(OS.get_window_safe_area())
var control = $CanvasLayer/Control
	
assert(control.get_viewport() == get_tree().root, "Assumption: control is not in a nested Viewport")
var parent_to_root = control.get_viewport_transform() * control.get_global_transform() * control.get_transform().affine_inverse()
var root_to_parent = parent_to_root.affine_inverse()

var safe_area_relative_to_parent = root_to_parent.xform(safe_area_root)
control.rect_position = safe_area_relative_to_parent.position
control.rect_size = safe_area_relative_to_parent.size

This code is what is currently being used in Captain Skyrunner to adjust for a notch at run time. It seems to work on both Android and iOS. I would like to once again thank Kleonc for providing me with this solution. Hopefully this helps somebody.

Until Next Time
– Steven