Up to date

This page is up to date for Godot 4.1. If you still find outdated information, please open an issue.

Custom drawing in 2D


Godot has nodes to draw sprites, polygons, particles, and all sorts of stuff. For most cases, this is enough. If there's no node to draw something specific you need, you can make any 2D node (for example, Control or Node2D based) draw custom commands.

Custom drawing in a 2D node is really useful. Here are some use cases:

  • Drawing shapes or logic that existing nodes can't do, such as an image with trails or a special animated polygon.

  • Visualizations that are not that compatible with nodes, such as a tetris board. (The tetris example uses a custom draw function to draw the blocks.)

  • Drawing a large number of simple objects. Custom drawing avoids the overhead of using a large number of nodes, possibly lowering memory usage and improving performance.

  • Making a custom UI control. There are plenty of controls available, but when you have unusual needs, you will likely need a custom control.


Add a script to any CanvasItem derived node, like Control or Node2D. Then override the _draw() function.

extends Node2D

func _draw():
    # Your draw commands here

Draw commands are described in the CanvasItem class reference. There are plenty of them.


The _draw() function is only called once, and then the draw commands are cached and remembered, so further calls are unnecessary.

If re-drawing is required because a state or something else changed, call CanvasItem.queue_redraw() in that same node and a new _draw() call will happen.

Here is a little more complex example, a texture variable that will be redrawn if modified:

extends Node2D

@export var texture: Texture:
        set = _set_texture

func _set_texture(value):
    # If the texture variable is modified externally,
    # this callback is called.
    texture = value  # Texture was changed.
    queue_redraw()  # Trigger a redraw of the node.

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

In some cases, it may be desired to draw every frame. For this, call queue_redraw() from the _process() callback, like this:

extends Node2D

func _draw():
    # Your draw commands here

func _process(delta):