Godot Tactics RPG – 06. Anchored UI

7thSage again. Welcome back to part 6. This time we’ll be working on some scripts to make working with anchor points in code a bit simpler, and allow us to animate some things in our UI.

Test Scene

Before we get into the lesson, I’d like to take a moment to set up the scene we’ll be using as a playground for the new scripts. While some of this may be a bit excessive for a scene we aren’t going to be using in our main project, I do think it will make it much easier to see what is going on and understand what the values are doing. In the next lesson we’ll be using the scripts we create here, but the scene itself won’t be used.

That being said, let’s create the scene. Right click on the “Scenes” folder and create a new scene named something like “AnchorTests.tscn”. It doesn’t really matter which scene type we choose as the root here, although oddly enough, anchors don’t seem to work with a 2d node directly, so I’d probably avoid that one. I’m probably missing some Godot secret there, but the other types let children stretch to the extents of the scene view.

Ok, back to the scene. First up, lets create a VBoxContainer. Later on we’ll be adding buttons inside this, but for now it will just be a placeholder. In the inspector under the section “Layout” find the property “Anchor Preset” and change it to “Custom”. Now under Anchor Points, set the Right value to around .33, and the Bottom value to 1. Reset all the Anchor Offsets to 0. This should create an empty box taking up around the first third of the view on the left.

Next we’ll create a panel. This will be used as a proxy for our scene’s camera. If we used the entire viewport for testing, it would be difficult to see what the anchors are doing when they leave the screen, so imagine this box as the edge of our screen. Under the root node, create a child node of the type “Panel” named “ParentPanel”. Set the Anchors Preset to Custom again. This time for Anchor Points set Left to .4, top to 0, and Right and Bottom to 1. For Anchor Offsets, leave Left at 0, put Top at 75, and Right and Bottom at -75. This will create a box with some shading taking up most of the right two thirds of the screen with a bit of padding.

The best way to put grid lines on the ParentPanel would probably be through a texture, but we can simulate the same thing with a couple more objects. Click on the ParentPanel and create 3 child nodes of the type ReferenceRect and name them VRect, HRect and FullRect respectively. Uncheck the box “Editor Only” on all three. Under layout set Layout Mode to Anchors, and set the Anchor Presets to Custom. Reset all the Anchor Offsets to zero and set the Anchor Points as follows:

VRect: Left 0.5, Top 0, Right 0.5, Bottom 1
HRect: Left 0, Top 0.5, Right 1, Bottom 0.5
FullRect: Left 0, Top 0, Right 1, Bottom 1

This should leave you with a red outline around ParentPanel, and a horizontal and vertical line going through the center. These will help us visualize exactly what anchor points are lining up to.

Next Create another child of ParentPanel of the type Label named “AnchorInfo”. We’ll put some debug info in this to also aid in understanding what is going on. You can place some placeholder text in the Text box to help visualize position and size. Next is Label Settings, click where it says empty and add a “New LabelSettings”. To expand Label Settings, click on the word, and not the arrow beside it. Here we’ll be able to set things like font size and color.

Scroll down to the Layout section. Set the Layout Mode to Anchors, and the Anchors Preset to Custom. Set the Anchor Points Left and Top to 0, and Right and Bottom to 1. For Anchor Offsets reset everything to 0, and set Top to -75.

Next create one last child of ParentPanel of type Panel named ChildPanel. This will be the main object we are animating in this lesson. Now we only need one more object, this time a child of ChildPanel. Create a child of type Node named “PanelTestController”.

A quick thing to note here, if we press the normal play button, our main scene will play instead of the scene we are viewing. So unless we want to change our project settings for one lesson, the easiest way is to go a couple buttons over to Play Current Scene instead of the Play Project button.

Anchors

Before we get too deep into programming our anchor points, I think it makes sense to dive into the anchor point and various values that intertwine with them. If we set the Anchors Preset to Custom, we can see what each preset is doing under the hood. If you’d like to play with this a bit, select the ChildPanel and set a preset, then switch it to custom to see how things have changed.

Once the preset is set to custom, there are three main pieces of the puzzle we’ll be looking at. Anchor Points, Anchor Offsets and just a bit down under Transform the size and position. You’ll have to be a bit careful playing with these values as changing any one section will likely result in something in the other two auto adjusting.

I feel like Godot deals with transformations for 2D panels a bit strangely, although that may be just me not being used to dealing with 2D objects. Position is the top left corner of our panel, no matter which anchor we use, and includes the anchor offset. I think this will be in our favor later on as we won’t have to deal directly with the parent’s size or anchor point or anything to get the positions we need. Size is similar. This value is based on the stretch of our tile, plus any offset.

We could do most of our calculations on the size and position fields, but that would open us up to needing to do a lot more calculations under the hood. Instead we’ll be focusing mostly on the Anchor Points, and Anchor Offsets to derive our values.

It’s best to think of the anchor points as two pairs of values. Left/Right and Top/Bottom. For instance with Left/Right, a value of 0 is all the way to the left, a value of 1 is all the way right. If we set left to 0 and Right to 1, our panel will stretch to fill up the entire size of the parent or scene. We can’t swap the values, because left needs to always be the smaller number. If both values are the same the size will be 0 unless we make up for it in the offset values. So if both are the same and the Left and Right offsets are -20 and 20 respectively, our panel will have a size value of 40 on the x.

This is basically what we’ll be doing in code. We’ll lock the Left/Right anchor points together as well as the Top/Bottom points. Then using the values 0, 0.5, and 1 we’ll be able to get the 9 different anchor points of the parent. To line up the child anchor points to those points, we’ll use the size we want our panel, and calculate the anchor offsets with it to get our panel positioned and sized correctly.

Layout Anchor

Finally to scripting. Under the folder “Scripts->Common” create a new folder named “UI”. Inside this we’ll create our first script “LayoutAnchor.gd”. This time we’ll extend Panel and give it a class_name “LayoutAnchor”

extends Panel
class_name LayoutAnchor

Before we start setting the position of our panels, we need a couple helper functions to translate the anchor presets to values we can use. The first we’ll use to get the values for the parent’s anchor points. This will be the “Anchor Points” values on “ChildPanel”. These will be represented as floats from 0 to 1 as a percentage of the size of the parent. Because Left/Right and Top/Bottom are locked together, we only need a single value for each, which we’ll get from a Vector2.

We’ll be using the enum Control.LayoutPreset to define which anchor point we are moving to. There are technically more than the 9 values we’ll be using, but if anything beyond the original 9 are used, they will get swept into the default return value here, which will result in them being treated as the anchor point Top_Left.

One new little thing in the function call here. “-> Vector2” lets us statically type the functions return value.

func GetParentAnchor(anchor: Control.LayoutPreset) -> Vector2:
	var retValue:Vector2 = Vector2.ZERO

	#Set the x value of our return
	match anchor:
		Control.PRESET_TOP_RIGHT, Control.PRESET_BOTTOM_RIGHT, Control.PRESET_CENTER_RIGHT:
			retValue.x = 1
		
		Control.PRESET_CENTER_TOP, Control.PRESET_CENTER_BOTTOM, Control.PRESET_CENTER:
			retValue.x = .5
		
		_:
			retValue.x = 0
	
	#Set the y value of our return
	match anchor:
		Control.PRESET_BOTTOM_LEFT, Control.PRESET_BOTTOM_RIGHT, Control.PRESET_CENTER_BOTTOM:
			retValue.y = 1
		
		Control.PRESET_CENTER_LEFT, Control.PRESET_CENTER_RIGHT, Control.PRESET_CENTER:
			retValue.y = .5
		
		_:
			retValue.y = 0
			
	return retValue

For the offset of the ChildPanel itself we’ll be setting the values of “Anchor Offsets”. Here we need two sets of positions for Top Left corner and the Bottom Right, which we’ll store in a Rect2. We’ll also be taking into account the size and offset when calculating these points. The function works much the same as the previous one, but we need to multiply by the size to get our top left corner. We then shift the corner over with the offset value and add the size to get the bottom corner.

func GetMyOffsets(anchor: Control.LayoutPreset, offset: Vector2) -> Rect2:
	var retValue:Rect2 = Rect2()
	
	#Set the x value of our return
	match anchor:
		Control.PRESET_TOP_RIGHT, Control.PRESET_BOTTOM_RIGHT, Control.PRESET_CENTER_RIGHT:
			retValue.position.x = -1 * self.size.x
		
		Control.PRESET_CENTER_TOP, Control.PRESET_CENTER_BOTTOM, Control.PRESET_CENTER:
			retValue.position.x = -.5 * self.size.x
		
		_:
			retValue.position.x = 0
	
	#Set the y value of our return
	match anchor:
		Control.PRESET_BOTTOM_LEFT, Control.PRESET_BOTTOM_RIGHT, Control.PRESET_CENTER_BOTTOM:
			retValue.position.y = -1 * self.size.y
		
		Control.PRESET_CENTER_LEFT, Control.PRESET_CENTER_RIGHT, Control.PRESET_CENTER:
			retValue.position.y = -.5 * self.size.y
		
		_:
			retValue.position.y = 0
	
	retValue.position += offset
	retValue.end = retValue.position + self.size
	return retValue

The last set of variables will get calculated automatically when the code sets the Anchor Points and the Offsets. However we do need to set the Size value under Transform before we run our script because the script will use the value to calculate the offsets and maintain the size. I set the size of ChildPanel to be 40×40.

Now that we can calculate all the points we need, we have two functions to set the position. One that will snap the panel directly into place, and the other that will animate it with tweens. We’ll start with the one that snaps into place as it’s the simpler of the two. The function takes three arguments, the two enum anchor points, and the offset.

We start by calling the functions we just created and setting their values to variables. The next four lines set the Anchor Points, and the four following set the Anchor Offsets.

func SnapToAnchorPosition(myAnchor:Control.LayoutPreset, parentAnchor:Control.LayoutPreset, offset:Vector2):
	var parentVector:Vector2 = GetParentAnchor(parentAnchor)
	var myOffsets:Rect2 = GetMyOffsets(myAnchor, offset)
	
	self.anchor_left = parentVector.x
	self.anchor_right = parentVector.x
	self.anchor_top = parentVector.y
	self.anchor_bottom = parentVector.y
	
	self.offset_left = myOffsets.position.x
	self.offset_right = myOffsets.end.x
	self.offset_top = myOffsets.position.y
	self.offset_bottom = myOffsets.end.y

The parameters on the next one were getting a bit out of hand so I tried to break them up on multiple lines. We have all the parameters from the first function, but this time we need to add a couple more to control the tweens. Duration, and two optional parameters Transition Type and Ease Type. Then we get the values the same as before.

We need to tween quite a few little pieces together this time, but we’ll still be using one tween object. We do something a little different this time after creating our tween. I’ve moved several of the extra settings to the whole object instead of setting it on each tween. There is also one new function on the tween, “set_parallel(true)”. This will allow us to run all the tweens at the same time, without the need of separate objects. The values used in the tween are the same as in SnapToAnchorPosition(), but with the added duration for the tween. At the end we send the await for tween.finished.

One final detail you’ll notice is that I listed all the properties of each tween on a single line. They can be spread out like before, but having 8 separate tweens all spread out starts to get hard to read.

func MoveToAnchorPosition(myAnchor:Control.LayoutPreset, parentAnchor:Control.LayoutPreset, 
		offset:Vector2, duration:float, trans:Tween.TransitionType=Tween.TRANS_LINEAR,
		anchorEase:Tween.EaseType=Tween.EASE_IN_OUT):
			
	var parentVector:Vector2 = GetParentAnchor(parentAnchor)
	var myOffsets:Rect2 = GetMyOffsets(myAnchor, offset)
	
	var tween = create_tween()
	tween.set_trans(trans).set_ease(anchorEase).set_parallel(true)
	
	tween.tween_property(self, "anchor_left", parentVector.x, duration)
	tween.tween_property(self, "anchor_right", parentVector.x, duration)
	tween.tween_property(self, "anchor_top", parentVector.y, duration)
	tween.tween_property(self, "anchor_bottom", parentVector.y, duration)
		
	tween.tween_property(self, "offset_left", myOffsets.position.x, duration)
	tween.tween_property(self, "offset_right", myOffsets.end.x, duration)
	tween.tween_property(self, "offset_top", myOffsets.position.y, duration)
	tween.tween_property(self, "offset_bottom", myOffsets.end.y, duration)
	
	await tween.finished

One last step for this script. Attach it to ChildPanel, and double check that the panel has a size value. Something like 40 x 40 works well.

Test Drive

In the “Scripts” folder, create another folder named something like “Test” if you don’t already have one. Create a new script named something like “PanelTests.gd” and attach it to the node “PanelTestController”. The script will need a couple references to objects in the scene so we’ll use @export and then we can grab them in the inspector. If you click on Assign in the inspector you can select them from the scene tree. ChildPanel and AnchorInfo. If you aren’t able to select ChildPanel, it is most likely because the LayoutAnchor Script hasn’t been attached to it yet. There is also a bool value for animated, so we can choose whether to use the function with or without tweening.

extends Node

@export var childPanel: LayoutAnchor
@export var textLabel: Label
@export var animated: bool

The main part of the script is here. We loop through each of the possible offsets on both the child and parent anchors, and we pause slightly so you can see what each one is doing. Enums are represented behind the scenes as int values, so we can call them without their specific name as well, as we do here, to loop through them.

func _ready():
	for i in 9:
		for j in 9:
			textLabel.text = str("childAnchor: ", i, " parentAnchor: ", j)
			
			if(animated):
				await childPanel.MoveToAnchorPosition(i,j,Vector2.ZERO, .5)
				await get_tree().create_timer(.5).timeout
				
			else:				
				childPanel.SnapToAnchorPosition(i,j,Vector2.ZERO)
				await get_tree().create_timer(1).timeout

If you haven’t done so already, click the PanelTestController and in the inspector Assign the childPanel and textLabel objects. There is also the checkbox to determine whether our motion will be animated and that should be it. Click the “Run Current Scene” button.

You should see ChildPanel moving around between the points. If you’ve got tweening turned on and notice your panel stretching between the points instead of moving. You might need to double check that your points aren’t mixed up, and that you have the flag for parallel set.

Panel Anchor

For the next set, we’ll create a data type to store all of the relevant data, including a name we can refer to the set by. We’ll be able to set this in the inspector so we need to set @export on the variables. As a quick note, the anchor enums will have their full range of possible values in the inspector, so we’ll just have to ignore the ones after the first 9.

In the folder we created earlier “Scripts->Common->UI” create another script named “PanelAnchor.gd”. We’ll extend Resource this time so our class can be accessed in the inspector.

extends Resource
class_name PanelAnchor

@export var anchorName:String
@export var myAnchor:Control.LayoutPreset
@export var parentAnchor:Control.LayoutPreset
@export var offset:Vector2
@export var duration:float=.5
@export var trans:Tween.TransitionType=Tween.TRANS_LINEAR
@export var anchorEase:Tween.EaseType=Tween.EASE_IN_OUT

We’ll also include a function to set the values so we don’t need to do so one at a time. I’m not using _Init() this time, because the inspector doesn’t play nicely with a non blank _init() function, but we can set things just fine with a separate function instead of the constructor directly.

func SetValues(aName, mAnchor, pAnchor, v2offset, dur=.5, tran= Tween.TRANS_LINEAR, aEase=Tween.EASE_IN_OUT):
		anchorName = aName
		myAnchor = mAnchor
		parentAnchor = pAnchor
		offset = v2offset
		duration = dur
		trans = tran
		anchorEase = aEase

And that’s it for that script. Next we’ll make one tiny addition to “LayoutAnchor.gd” to make use of the new data type, by adding one more function to it. This will take our object and convert it to the individual values and pass it along to either the animated or non animated function.

So let’s open “LayoutAnchor.gd” and add it.

func ToAnochorPosition(anchor:PanelAnchor,animated:bool):
	if animated:
		MoveToAnchorPosition(anchor.myAnchor, anchor.parentAnchor, anchor.offset, anchor.duration, anchor.trans, anchor.anchorEase)
	else:
		SnapToAnchorPosition(anchor.myAnchor, anchor.parentAnchor, anchor.offset)

Test Drive 2

We don’t want the original tests running while we are doing the next set, so we can detach the PanelTests script from the “PanelTestController” node. Back inside our Test folder, create a new script named “PanelTests2.gd” and attach the new script to our PanelTestController node.

We don’t really need the text label this time, but we will make use of the VBox to place some buttons. Be sure to assign the childPanel and vbox in the inspector once we get the @export values in.

extends Node

@export var childPanel: LayoutAnchor
@export var vbox: VBoxContainer
@export var animated: bool

The next line we’ll add an array object to the inspector to store our anchor object. Like with the label earlier, after you add a new element in the array, you’ll have to click on the name part to expand the element to see the individual @export options of the PanelAnchor object.

@export var anchorList:Array[PanelAnchor] = []

We’ll add two elements to the array. One for “Show” and another for “Hide”. The values for each are in the following image.

Next we create a function to get the array element using the anchorName field. We also have a similar limitation in Godot that the original tutorial had, we can’t put a dictionary element in the inspector, at least not a statically typed one. Meaning we wouldn’t be able to have type hints. The biggest drawback is that it is possible to have multiple points with the same name. So you’ll have to be careful that you don’t double up any names. Our function here will just return the first one it finds.

func GetAnchor(anchorName: String):
	for anchor in self.anchorList:
		if anchor.anchorName == anchorName:
			return anchor
	return null

Before we create the buttons lets add the function that they’ll be calling. It’s fairly simple, the button sends a string value that corresponds to the anchorName in the previous function, and then passes the anchor to LayoutAnchor with the animated flag.

func AnchorButton(text:String):
	var anchor = GetAnchor(text)
	if anchor:
		childPanel.ToAnochorPosition(anchor, animated)
	else:
		print("Anchor is null")

And next to create our buttons. We do one more new thing here, instead of calling “connect(AnchorButton)” we add .bind(), which lets us pass parameters along with the function, in this case the string corresponding to each button.

func _ready():
	#Buttons
	var buttonShow = Button.new()
	buttonShow.text = "Show"
	buttonShow.pressed.connect(AnchorButton.bind("Show"))
	vbox.add_child(buttonShow)
	
	var buttonHide = Button.new()
	buttonHide.text = "Hide"
	buttonHide.pressed.connect(AnchorButton.bind("Hide"))
	vbox.add_child(buttonHide)
	
	var buttonCenter = Button.new()
	buttonCenter.text = "Center"
	buttonCenter.pressed.connect(AnchorButton.bind("Center"))
	vbox.add_child(buttonCenter)

Before we test our script though, we have one more position to add to the list, “Center”. We’ll add this one via code though. Still inside the _ready() function. We also gave this one a slight different animation with “Tween.TRANS_BACK”.

#Add new anchor to list
var anchorCenter:PanelAnchor = PanelAnchor.new()
anchorCenter.SetValues("Center",Control.PRESET_CENTER, Control.PRESET_CENTER, Vector2.ZERO, .5, Tween.TRANS_BACK )
anchorList.append(anchorCenter)

Now we can run the current scene and give the buttons a test.

Summary

There was quite a bit new in this lesson to cover, but we’ve made our first real jump into creating our UI. There are still a number of components we need to learn before making a full UI, but we got a glimpse of several of the components here that we’ll be using later, such as VboxContainers and Labels, even though we didn’t go over them in too much detail. We’ll be able to use the scripts created here going forward to make setting up our panels easier. We’ve also added a few more advanced methods to functions we learned in the past, such as expanding our usage of tweens and buttons.

In the repository I’ve duplicated the demo scene so you can see results of both “Test Drive” and “Test Drive 2” without having to re-setup the scene.

4 thoughts on “Godot Tactics RPG – 06. Anchored UI

  1. The children panels are opening in this order is simply to show referencing for dynamic calculation right? I’ve been struggling to imagine actual game UI panels opening at these positions in this order.

    (Child panel positions on parent Anchor #0)
    3 7 2
    6 8 4
    1 5 0

    Things will probably make more sense for me in the next post.

    1. Yes, this was to show that you can show relationships between a child and parent panel in a variety of ways. Also keep in mind that it kind of looked like it was a single panel positioned against the screen, but this is a parent/child relationship and could be applied to UI elements at any level of a complex hierarchy. Also, if you localize to different languages you may see more reason to use anchors on other sides or flows in other directions etc

    2. The weird order is just due to the order that Godot lays out its anchor enum. In an actual game we won’t be looping through them like this, but moving to one specific value, more similar to the second test.

      1. Ah ok thanks! Both of the comments made perfect sense. This is exciting to do this along side you. Keep up the great work!

Leave a Reply

Your email address will not be published. Required fields are marked *