Getting the size of a (child) control node, using control.size in _ready(), returns (0,0) even when waiting

I understand. The process of creating a minimum project has helped me solve the issue. Thanks! I’ll document my findings here.

TL;DR:

:white_check_mark: Do use await get_tree().process_frame in _draw() for correct, consistent results for control.size:

func _draw():
    await get_tree().process_frame
    print(control.size) # consistent 0, 90

:x: Do not use call_deferred()
:x: Do not use await get_tree().process_frame in _ready() even if it prints the correct values for you, since the results can be inconsistent or wrong depending on your UI complexity

Reference code

For reference, this is the (simplified) code in the control:

func _ready():
	check_control_size_timings()
	
func _draw():
	check_control_size_timings()

func check_control_size_timings():
	print_container_size()
	
	call_deferred("print_container_size")
	
	await get_tree().process_frame
	print_container_size()
	
	await_to_be_sized()
	
func print_container_size():
	# Expected output: (0, 90)
	print("Control size: %s" % [control_to_get_size_of.size])

func await_to_be_sized():
	while control_to_get_size_of.size == Vector2.ZERO:
		await get_tree().process_frame
	print_container_size()

Hierarchy

And this was my hierarchy:

-> Main_Scene
    -> UI  <- (Has script mentioned above)

        -> A bunch of control nodes here...
            -> A bunch of child control nodes here...

        -> A bunch of nested control nodes here...
            -> A bunch of nested control nodes here...
                -> ControlToGetSizeOf <- control.size

        -> A bunch of control nodes here...
            -> A bunch of child control nodes here...

Tests

When running the scene with the control.size script directly:

Expected value is (0, 90)

  1. Directly calling print(control.size) in:
    1.1. _ready(): (0, 0) :x:
    1.2. _draw(): (0,0) :x:
  2. Call_deferred print in:
    2.1. _ready(): (0, 0) :x:
    2.2. _draw(): (0, 0) :x:
  3. await get_tree().process_frame in:
    3.1. _ready(): (0, 90) :white_check_mark: Consistently
    3.2. _draw(): (0, 90) :white_check_mark: Consistently
  4. await_to_be_sized():
    4.1. _ready(): (0, 90) :white_check_mark: Consistently
    4.2. _draw(): (0, 90) :white_check_mark: Consistently

However, I’m not running that scene directly. It’s a child scene, and it should not be visible until a button is pressed. So I have two options:

Initiate the node by drag and dropping in the editor, and turning it invisible. It is turned visible by the press of a button. Here are the results:

  1. All _ready() methods return (0,0) :x:
  2. _draw():
    2.1. Direct print (control.size): (0, 0) :x:
    2.2. Call_deferred print: (0, 0) :x:
    2.3: await get_tree().process_frame and await_to_be_resized() both consistently return (0, 90), the expected value :white_check_mark:

Instantiating the scene manually is a no-go for my use-case.
Each scene represents one “card reward” in my game, and I want it the ‘rewards’ to vary between 1 to (many) cards at a time.
For completeness sake, I will spawn them in, in 2 ways:

  1. When the “Make visible” button is pressed, instead of making the invisible scene → visible, the ui node will instantiate a new scene instead when the button is pressed;
  2. the ui node will instantiate the required scenes before the button is pressed. The button press will turn the required scenes visible.

Here are the results.

Instantiate on button press:

  1. Directly calling print(control.size) in:
    1.1. _ready(): (0, 0) :x:
    1.2. _draw(): (0,0) :x:
  2. Call_deferred print in:
    2.1. _ready(): (0, 0) :x:
    2.2. _draw(): (0, 0) :x:
  3. await get_tree().process_frame in:
    3.1. _ready(): (0, 90) :exclamation: INCONSISTENTLY, can also print (0, 0)
    3.2. _draw(): (0, 90) :white_check_mark: Consistently
  4. await_to_be_sized():
    4.1. _ready(): (0, 90) :white_check_mark: Consistently
    4.2. _draw(): (0, 90) :white_check_mark: Consistently

Instantiate the scene(s) at the _ready() of the parent scene:

  1. Directly calling print(control.size) in:
    1.1. _ready(): (0, 0) :x:
    1.2. _draw(): (0,0) :x:
  2. Call_deferred print in:
    2.1. _ready(): (0, 0) :x:
    2.2. _draw(): (0, 0) :x:
  3. await get_tree().process_frame in:
    3.1. _ready(): (0, 0) :x:
    3.2. _draw(): (0, 90) :white_check_mark: Consistently
  4. await_to_be_sized():
    4.1. _ready(): (0, 90) :white_check_mark: Consistently
    4.2. _draw(): (0, 90) :white_check_mark: Consistently

In previous (forum) threads, call_deferred was recommended as a solution for this issue. However, in my tests, it returns the wrong value, so I cannot recommend using call_deferred for getting the control.size

Seeing as getting the size in _ready() can be wrong or inconsistent
:x:

func _ready():
    await get_tree().process_frame
    print(control.size) # 0,0 or inconsistent 0, 90

the proper solution is to get the size in _draw()
:white_check_mark:

func _draw():
    await get_tree().process_frame
    print(control.size) # consistent 0, 90

TL;DR:

:white_check_mark: Do use await get_tree().process_frame in _draw() for correct, consistent results for control.size:

func _draw():
    await get_tree().process_frame
    print(control.size) # consistent 0, 90

:x: Do not use call_deferred()
:x: Do not use await get_tree().process_frame in _ready() even if it prints the correct values for you, since the results can be inconsistent or wrong depending on your UI complexity

2 Likes