1
\$\begingroup\$

Ok this has stumped me. In Godot 4.x, how do I programmatically (GDscript) draw a sprite x number of times (with an offset)?

5 godot icon sprites in a row

I can manually add sprites in the editor over and over again. Only I don't want to do that or rely on add them and then switching off their respective visibility

var max_limit = 5
for n in range(max_limit):
    print(n)
    draw sprite at (x + 10*n,y)

... much later
max_limit -=1

So the above loop will draw four sprites, instead of five.

same images as above with one less sprite

Currently I've added a sprites with a number of frames and change the frame number to control the number of sprites shown. - But that really is going the long way around.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

For what you say, I take you want to avoid using multiple Nodes for this. And in fact, we can avoid using multiple objects.

Instead we will do it in _draw. To begin with you need an Node2D or a Control. In this answer I'll be working with a Node2D, the main change with using a Control is that you would also want to update its size or custom_minimum_size.

extends Node2D

And, of course, you need the texture you want to draw.

extends Node2D

@export var texture:Texture2D

Here @export tells Godot to allow setting this field (class/script variable) from the Inspector.

Now we are going to override _draw:

extends Node2D


@export var texture:Texture2D


func _draw():
    pass

And here we draw the texture using draw_texture:

extends Node2D


@export var texture:Texture2D


func _draw():
    draw_texture(texture, Vector2.ZERO)

We might want the texture to update when we change it in the editor, so let us make this a tool script, and give texture a setter:

@tool
extends Node2D


@export var texture:Texture2D:
    set(mod_value):
        if texture == mod_value:
            return

        texture = mod_value
        queue_redraw()


func _draw():
    draw_texture(texture, Vector2.ZERO)

The setter is a method that will run every time the field is set.

Here calling queue_redraw notifies Godot that the Node2D needs to be updated, so it calls _draw again.

So, just to be clear, Godot is not calling _draw every frame. Instead it caches the result.

Ah, we might want to handle a null texture:

@tool
extends Node2D


@export var texture:Texture2D:
    set(mod_value):
        if texture == mod_value:
            return

        texture = mod_value
        queue_redraw()


func _draw():
    if texture == null:
        return

    draw_texture(texture, Vector2.ZERO)

Better.

Now, about the loop, let us start with what you have:

@tool
extends Node2D


@export var texture:Texture2D:
    set(mod_value):
        if texture == mod_value:
            return

        texture = mod_value
        queue_redraw()


func _draw():
    if texture == null:
        return

    var max_limit = 5
    for n in range(max_limit):
        draw_texture(texture, Vector2(10*n, 0.0))

Ok, so what happened to x and y? Nothing, it is just that our drawing is attached to the Node2D, so you can move the Node2D to position it, which is why I have Vector2(10*n, 0.0) instead Vector2(x + 10*n, y). And yes, you can also rotate, scale or skew the Node2D.

And, of course, you want to be able to change max_limit, so let us make it a field:

@tool
extends Node2D


@export var texture:Texture2D:
    set(mod_value):
        if texture == mod_value:
            return

        texture = mod_value
        queue_redraw()

@export var max_limit := 5:
    set(mod_value):
        if max_limit == mod_value:
            return

        max_limit = mod_value
        queue_redraw()


func _draw():
    if texture == null:
        return

    for n in range(max_limit):
        draw_texture(texture, Vector2(10*n, 0.0))

And that's it.


Addendum

You hard-coded a - depending who you ask - magic number 10.

Without further context, I would suggest to promote it, either to a const or to an @export field.

However, if you wanted to use the width of the texture, well:

func _draw():
    if texture == null:
        return

    var width := texture.get_width()
    for n in range(max_limit):
        draw_texture(texture, Vector2(width*n, 0.0))

And, yes, it is also possible to clip/tile the texture, or draw it scaled. For that you would use draw_texture_rect or draw_texture_rect_region, respectively.

\$\endgroup\$
3
  • \$\begingroup\$ Brilliant. Thank you for a comprehensive explanation. It Does work, but with the error: E 0:00:00:0730 hud.gd:47 @ _draw(): Drawing is only allowed inside NOTIFICATION_DRAW, _draw() function or 'draw' signal. <C++ Error> Condition "!drawing" is true. <C++ Source> scene/main/canvas_item.cpp:706 @ draw_texture() at the line draw_texture(texture, Vector2(width*n, 0.0)) \$\endgroup\$
    – Ghoul Fool
    Sep 3, 2023 at 11:05
  • \$\begingroup\$ @GhoulFool I've tested the code and it works without the error. Are you calling _draw directly or using draw_texture outside of _draw? You are not supposed to. The error is telling you where you can draw (in _notification when you get NOTIFICATION_DRAW, in _draw, or a method that handles the draw signal). In the code I use _draw and use queue_redraw() to trigger it, I suggest you do the same. \$\endgroup\$
    – Theraot
    Sep 3, 2023 at 13:06
  • \$\begingroup\$ I've figured it ouT. I was also calling HUD._draw() on main . - Which naturally is doesn't need! \$\endgroup\$
    – Ghoul Fool
    Sep 3, 2023 at 14:15

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .