_draw seems to be one step behind

Godot Version

v4.7.2.stable.official [ed1daf0bf]

Context

I’m trying to make a compass control that points north when the user turns around. Everything is working, except I noticed that the graphics output by _draw is one step behind.

I have a function that users of the Compass control can use to set the north direction which then forces a redraw with queue_redraw

func set_north( angle : float ) -> void:
	
	north_angle = angle
	print( "Compass: North Angle now ", rad_to_deg( north_angle ) )
	
	queue_redraw()

The _draw function looks like this:

func _draw() -> void:
	
	var scale_x = size.x / texture.get_width()
	var scale_y = size.y / texture.get_height()
	
	print( "Compass: Redraw with north as ", rad_to_deg( north_angle ))
	label.text = str( rad_to_deg( north_angle )) + "º"
	
	draw_set_transform( Vector2.ZERO, 0, Vector2( scale_x, scale_y ))
	offset_transform_rotation = -north_angle
	
	draw_texture( texture, Vector2.ZERO )

Additional Information after some discussion here

  1. The print and label.text updates are for debugging purposes only. The issue existed before they were added.
  2. The issue happens in a real game prototype where the rest of the game window continues updating normally, including other UI controls, responses to input, etc. This is not an issue that is only noticeable while stepping through the code.
  3. Using offset_transform_rotation makes the compass rotate around its centre, if I put the rotation into the draw_set_transform, the rotation is around the control’s top left corner.
  4. However, when I put the rotation into draw_set_transform the UI updates happen when expected, they just happen to be rotating around the wrong point.

Problem

label.text has the correct angle in it, but the UI shows the previous angle. If I call the set_north after _draw with the same value, the UI updates properly.

Question

What am I doing wrong? or is this a bug.

Example project on google drive: Godot Issue – Google Drive .

To use:

  • Left Arrow - rotate anticlockwise
  • Right Arrow - rotate clockwise
  • Up Arrow - reset to north
  • Down Arrow - don’t change the angle, but call set_north again to force a _draw call

Each function also logs data to help track the values.

How is set_north() called? Post that code.

in the sample it is called like this from the root Node2D:

func _process( _delta: float ) -> void:
	
	# pressing left rotates anticlockwise
	if Input.is_action_just_pressed("ui_left"):
		north_angle += PI / 4 # 45º
		print( "Left Pressed. Angle now: ", rad_to_deg( north_angle ))
		compass.set_north( north_angle )
	
	# pressing right rotates clockwise
	if Input.is_action_just_pressed("ui_right"):
		north_angle -= PI / 4 # 45º
		print( "Right Pressed. Angle now: ", rad_to_deg( north_angle ))
		compass.set_north( north_angle )
	
	# pressing up resets the compass to north
	if Input.is_action_just_pressed("ui_up"):
		north_angle = 0.00
		print( "Up Pressed. Angle now: ", rad_to_deg( north_angle ))
		compass.set_north( north_angle )
	
	# don't change the angle - just redraw the compass
	if Input.is_action_just_pressed("ui_down"):
		print( "Down Pressed. Angle STILL: ", rad_to_deg( north_angle ))
		compass.set_north( north_angle )

And here’s the logging:

Compass: Redraw with north as 0.0

Left Pressed. Angle now: 45.0
Compass: North Angle now 45.0
Compass: Redraw with north as 45.0

Left Pressed. Angle now: 90.0
Compass: North Angle now 90.0
Compass: Redraw with north as 90.0

Left Pressed. Angle now: 135.0
Compass: North Angle now 135.0
Compass: Redraw with north as 135.0

Up Pressed. Angle now: 0.0
Compass: North Angle now 0.0
Compass: Redraw with north as 0.0

Down Pressed. Angle STILL: 0.0
Compass: North Angle now 0.0
Compass: Redraw with north as 0.0

Note that the label always agrees with the logged value, but the compass itself is wrong. For example, with the above, the compass points:

  • Initially: North. (Ok)
  • Left Press: North. (Should be north west - 45º)
  • Left Press: North West. (Should be west - 90º)
  • Left Press: West. (Should be south west - 134º)
  • Up Press: South West. (Should be north - 0º)
  • Down Pressed: North. (Ok)

Scene Tree:

  • Node2D (with script)
    • CanvasLayer
      • MarginContainer
        • VBoxContainer
          • Label
          • Compass (with script)

Node2D’s script:

extends Node2D

@onready var compass : Compass = $CanvasLayer/MarginContainer/VBoxContainer/Compass

var north_angle : float = 0.0


func _process( _delta: float ) -> void:
	
	# pressing left rotates anticlockwise
	if Input.is_action_just_pressed("ui_left"):
		north_angle += PI / 4 # 45º
		print( "Left Pressed. Angle now: ", rad_to_deg( north_angle ))
		compass.set_north( north_angle )
	
	# pressing right rotates clockwise
	if Input.is_action_just_pressed("ui_right"):
		north_angle -= PI / 4 # 45º
		print( "Right Pressed. Angle now: ", rad_to_deg( north_angle ))
		compass.set_north( north_angle )
	
	# pressing up resets the compass to north
	if Input.is_action_just_pressed("ui_up"):
		north_angle = 0.00
		print( "Up Pressed. Angle now: ", rad_to_deg( north_angle ))
		compass.set_north( north_angle )
	
	# don't change the angle - just redraw the compass
	if Input.is_action_just_pressed("ui_down"):
		print( "Down Pressed. Angle STILL: ", rad_to_deg( north_angle ))
		compass.set_north( north_angle )

Compass’ script:

class_name Compass
extends Control

@onready var label   : Label = $"../Label"

@export  var texture : Texture2D :
	set (new_value) :
		texture = new_value
		queue_redraw()

var  north_angle : float = 0.0

func set_north( angle : float ) -> void:
	
	north_angle = angle
	print( "Compass: North Angle now ", rad_to_deg( north_angle ) )
	
	queue_redraw()

func _ready() -> void:
	
	offset_transform_enabled  = true

func _draw() -> void:
	
	var scale_x = size.x / texture.get_width()
	var scale_y = size.y / texture.get_height()
	
	print( "Compass: Redraw with north as ", rad_to_deg( north_angle ), "\n" )
	label.text = str( rad_to_deg( north_angle )) + "º"
	
	draw_set_transform( Vector2.ZERO, 0, Vector2( scale_x, scale_y ))
	offset_transform_rotation = -north_angle
	
	draw_texture( texture, Vector2.ZERO )

Set the drawing canvas rotation directly via draw_set_transform() instead of using offset_transform_rotation.

I don’t think it’s a bug.

rendering for the changes in _draw() happens different than rendering for a control node. Also queue_redraw() doesn’t mean it’ll redraw just when the function runs. It’ll run _draw() later in the process frame.
So, just because you added label.text = str( rad_to_deg( north_angle )) + "º" in _draw() function doesn’t mean both draw related rendering and label’s rendering will happen at the same time or in the order you’d thik it would.

As a general rule, you should have only draw related items (what to draw) on _draw(), not any state change/logic for a node.

I’d move label.text = str( rad_to_deg( north_angle )) + "º" from _draw() to set_north() function. Try that and see if it changes anything.

I can’t do that. That’ll break the rotation.

It took me ages to get the compass to rotate around it’s centre point rather than the control’s origin.

However, I did change it to check it, and at least the update works now, but the animation is wrong.

:grinning_face: :cry:

The documentation of what the offset_ stuff does is very light. I assumed it meant something about rotating around the offset position (that’s certainly what happens), but now it seems it also means offset in time?

Edit:

Further reading suggests that offset refers to the offset in the controls offsets and anchors. So that explains why the rotation works when I use offset, because the default offset is the centre.

The question is back to why doing this delays the drawing.

But Compass is a Control and the documentation for making your own controls says to use _draw.

Its should. but the whole point of this is that it doesn’t. I’m not stepping through the code. The code is running at full speed. The game keeps working, my avatar moves when I move it, enemies update, UIs update. Everything goes through thousands of frames, but the compass only changes (to the previous setting) when I move it again.

That’s fine. I added the label as part of the debugging process just to make it clear that the state inside the compass was what I thought it should be, even though the compass pointed incorrectly. As such, moving or removing the Label doesn’t change the compasses behaviour, it just makes the error less obvious because now you have to watch the messages being printed out.

You should be able to do it solely via draw_set_transform(). You don’t need offset_* whatsoever.

Ok. But how?

And I’d love to know why the delay’s happening anyway.

The offset_* properties are useful for applying visual transformations and animations to UI controls without interfering with the parent Container’s layout logic.

Before offset_* introduced, you couldn’t change any transform properties of a control node if it’s a child of a container. Because Container is responsible for the transform of its children.

The control keeps its regular transform and layout properties, while the offset transform is applied on top of them. This lets you animate things like rotation, scale, or position without changing the properties the parent Container uses to lay out its children.

I don’t see it says there anywhere in the link you shared.

To be fair, I really don’t understand why you are using _draw for this.

You can simply create a Control Node with container. And change its transform properties when buttons are pressed, no need to use _draw here.

func set_north( angle : float ) -> void:
	
	north_angle = angle
	print( "Compass: North Angle now ", rad_to_deg( north_angle ) )
	
	#define your texture_rect: TextureRect for the compass somewhere on your script..
	#and make sure offset transform is enabled on the property panel of the texture_rect node
	texture_rect.offset_transform_rotation = -north_angle

Exactly. Either just rotate the control, or redraw with rotated canvas transform.