Position of control nodes in first frame

Godot Version

Version 4+

Question

Hi, I’m playing around trying to introduce a UI cursor to give feedback to the player when interacting with the multiple user interfaces available in a game, which is very common in rpg games. You can check images, and the history behind the element here a-little-about-the-evolution-of-the-cursors-in-jrpgs

My issue comes with the way that the position of the nodes is handled in the engine. Seems like you can’t get the correct global_position in the first frame after opening a menu for the first time, and listening to the focus_entered signal of a node in combination with grab_focus(), it’ll return Vector2(0,0).

I was able to get away using await get_tree().process_frame , but there’s a catch. If you use a dynamic node like a Scroll Container, it becomes harder to workaround, sometimes you need to wait 2 to 3 frames after scrolling, which seems to be tied to the project’s FPS settings.

Other workarounds that I have tried

  • Position the cursor constanly in screen using _process(delta), so that it gets the feel that is always drawn correctly.

  • Await ^multiples of the current FPS setting

  • Await using a short timer when opening a new menu for the first time.

  • Loop until the position is no longer 0,0

Do you know about other more elegant / efficient workaround that could be helpful to solve this issue? Otherwise I’d have to ditch the cursor. The rest of UI controls have been amazing to work with.

Thanks for the help! ~

For my RPG I use a specific Cursor controlled class. It might work for you I cant know without glancing through your code but try it out!

class_name CursorController
extends Node

@export var cursor_texture: Texture2D
@export var offset := Vector2(-20, 0)
@export var animation_speed := 0.5

var cursor: TextureRect
var target_node: Control
var tween: Tween

func _ready():
    # Create cursor
    cursor = TextureRect.new()
    cursor.texture = cursor_texture
    cursor.visible = false
    add_child(cursor)
    

    get_tree().root.connect("gui_focus_changed", _on_focus_changed)

func _on_focus_changed(control: Control):
    if control and is_instance_valid(control):
        track_node(control)

func track_node(node: Control):
    target_node = node
    

    if cursor.get_parent() != target_node.get_parent():
        cursor.get_parent().remove_child(cursor)
        target_node.get_parent().add_child(cursor)
    
    if tween and tween.is_valid():
        tween.kill()
    
    cursor.visible = true
    tween = create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
    tween.tween_property(cursor, "position", target_node.position + offset, animation_speed)

Its my code but I ran it through an AI really quick to fix some of my Coding idiosyncrasies and naming conventions. but it still worked in my code base lmk if the AI fucked anything up to bad.

2 Likes

Hi, thanks for answering, this is my current implementation, they are very similar, but I used an animation_player node instead of a Tween to animate it a bit, and I call it directly when required using the focus_entered signal in every screen, so that I can spawn multiples cursors (For submenus), or for picking multiple enemy targets (massive skills)

attack_button.focus_entered.connect(_on_button_focused.bind(attack_button))

func _on_button_focused(control: Control) -> void:
    pointer.change_focus(control, true)

class_name Pointer
extends Control

@export var animation_player: AnimationPlayer
@export var pointer_ui_audio: AudioStream
@export var pointer_texture: TextureRect

# Current control focused in the UI
var current_control: Control


# Hide it until is requested by the interface
func _ready() -> void:
	hide()


# Move pointer to the position of current focused element in UI
func change_focus(new_control: Control, center: bool = true) -> void:
	if current_control == new_control:
		return

	# Workaround, if the position hasn't been updated in the tree
	await get_tree().process_frame

	current_control = new_control
	var target_position := current_control.global_position

	# Center in the Y axis relative to the current focused control
	if center:
		target_position.y -= (current_control.size.y / 2)

	# Center the pointer relative to the pointer sprite size
	target_position = target_position + (pointer_texture.size / 2)

	# Left offset from the control focused [] -> Focused Control
	target_position = target_position - Vector2(24, 0)
	global_position = target_position

I updated my code to try to use parts of your approach, like reassigning the parent itself instead of global_position, and using a tween, which seems to help a bit with the position delay, but there are times when moving fast across menus, that the cursor will appear in non desired positions, it’d also get clipped in a Scroll Container, but I can workaround that scroll later.

Did you run out on any of these problems down the road? I think that my problem might come from the fact that I used anchor points rather than specific positions in the inspector. Thanks again for your time!

New approach:

class_name Pointer
extends Control

#@export var animation_player: AnimationPlayer
@export var pointer_ui_audio: AudioStream
@export var pointer_texture: TextureRect

# Current control focused in the UI
var current_control: Control


# Hide it until is requested by the interface
func _ready() -> void:
	hide()


# Move pointer to the position of current focused element in UI
func change_focus(new_control: Control, center: bool = true) -> void:
	if current_control == new_control:
		return

	# Workaround, if the position hasn't been updated in the tree
	#await get_tree().process_frame

	current_control = new_control
	var target_position := current_control.position

	## New solution, also tried using the texture_rect rather than the control object
	if pointer_texture.get_parent() != current_control.get_parent():
		pointer_texture.get_parent().remove_child(pointer_texture)
		current_control.get_parent().add_child(pointer_texture)

	# Center in the Y axis relative to the current focused control
	if center:
		target_position.y -= (current_control.size.y / 2)

	# Center the pointer relative to the pointer sprite size (No longer needed)
	# target_position = target_position + (pointer_texture.size / 2)

	# Left offset from the control focused [] -> Focused Control
	target_position = target_position - Vector2(24, 0)

	var tween := create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
	tween.tween_property(pointer_texture, "position", target_position, 0.5)

I can foresee a couple of issue with this, But don’t get TOO bogged down if you have it working now. Especially if its a personal game or project.

Firstly, Anchor points can cause issues when reparenting. When you move a control with anchors between containers, those anchors can behave unexpectedly because, You’re creating new tweens without killing old ones.

# Consider making your pointer_texture use simpler anchoring:
func _ready() -> void:
    hide()
    pointer_texture.anchor_left = 0
    pointer_texture.anchor_top = 0
    pointer_texture.anchor_right = 0
    pointer_texture.anchor_bottom = 0
# Add this to your class
var current_tween: Tween

# Then in change_focus:
if current_tween and current_tween.is_valid():
    current_tween.kill()
    
current_tween = create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
current_tween.tween_property(pointer_texture, "position", target_position, 0.5)

For ScrollContainer issues, you might need to add the pointer to the content itself rather than the ScrollContainer.

if current_control.get_parent() is ScrollContainer:
    var content = current_control.get_parent().get_child(0) # Usually the first child
    pointer_texture.get_parent().remove_child(pointer_texture)
    content.add_child(pointer_texture)
else:
    pointer_texture.get_parent().remove_child(pointer_texture)
    current_control.get_parent().add_child(pointer_texture)

Last thing, This isnt super important or even maybe relevant. I notice you hide the pointer in _ready but don’t have explicit visibility management when changing focus:

func change_focus(new_control: Control, center: bool = true) -> void:
    if current_control == new_control:
        return
        
    # Make sure it is visible
    pointer_texture.show()
    
    # Then go on to the rest of your function.

This should address any of those issues. I refined some of the mistakes I think I might have made too lol. Lmk if you need me to do a full update code block or if you can do the implementation on your own :slight_smile:

1 Like