Search code examples
godotgdscript

How to draw a Shape2D in toolmode?


I'm adding a collision shape using Physics2DServer like this:

Physics2DServer.body_add_shape(_body, _shape, Transform2D.IDENTITY, collision_disabled)

based on this solution

but I can't see what the shape visually is,
so is there a way to draw (and move it according to the body) in tool mode?


Solution

  • This took a litte figuring out, but it is actually simpler than I expected. The first insight is that Shape2D already has a draw method, so we can do something like this:

    Godot 3

    shape.draw(get_canvas_item(), Color.darkblue)
    

    Godot 4

    shape.draw(get_canvas_item(), Color.DARK_BLUE)
    

    Or using whatever color you prefer.

    Meaning that we can make a generic solution, instead of dealing with every kind of shape.


    We can take advantage of _draw for that. And to invalidate it, we can call update (Godot 3) or queue_redraw (Godot 4) every time it changes.

    By the way, Resource has a "changed" signal that should be emitted by all build-in resources (you have to do it manually for custom ones) when their state changes (if you find one that does not do this, please report it).

    So we have this:

    Godot 3

    tool
    extends Node2D
    
    
    export var shape:Shape2D setget set_shape
    
    
    func _draw() -> void:
        if not Engine.editor_hint:
            return
    
        if shape == null:
            return
    
        shape.draw(get_canvas_item(), Color.darkblue)
    
    
    func set_shape(new_value:Shape2D) -> void:
        if shape == new_value:
            return
    
        if shape != null and shape.is_connected("changed", self, "update"):
            shape.disconnect("changed", self, "update")
    
        shape = new_value
        if shape != null and not shape.is_connected("changed", self, "update"):
            shape.connect("changed", self, "update")
    
        update()
    

    Godot 4

    @tool
    extends Node2D
    
    
    @export var shape:Shape2D:
        set(new_value):
            if shape == new_value:
                return
    
            if shape != null and shape.changed.is_connected(queue_redraw):
                shape.changed.disconnect(queue_redraw)
    
            shape = new_value
            if shape != null and not shape.changed.is_connected(queue_redraw):
                shape.changed.connect(queue_redraw)
    
            queue_redraw()
    
    
    func _draw() -> void:
        if not Engine.is_editor_hint():
            return
    
        if shape == null:
            return
    
        shape.draw(get_canvas_item(), Color.DARK_BLUE)
    

    The drawback is that we cannot configure it draw the Shape2D at a custom position (it will be draw at the origin on the local coordinates of the Node2D). And, no, we cannot cheat on that.


    However, we can create a canvas item via the VisualServer (Godot 3) or RenderingServer (Godot 4) to position it. This setup might look familiar:

    Godot 3

    var canvas_item:RID
    var invalid_rid:RID
    
    
    func _enter_tree() -> void:
        canvas_item = VisualServer.canvas_item_create()
        VisualServer.canvas_item_set_parent(canvas_item, get_canvas_item())
    
    
    func _exit_tree() -> void:
        VisualServer.canvas_item_clear(canvas_item)
        canvas_item = invalid_rid
    
    
    func _draw() -> void:
        if not Engine.editor_hint:
            return
    
        if shape == null:
            return
    
        shape.draw(canvas_item, Color.darkblue)
    

    Godot 4

    var canvas_item:RID
    
    
    func _enter_tree() -> void:
        canvas_item = RenderingServer.canvas_item_create()
        RenderingServer.canvas_item_set_parent(canvas_item, get_canvas_item())
    
    
    func _exit_tree() -> void:
        RenderingServer.canvas_item_clear(canvas_item)
        canvas_item = RID()
    
    
    func _draw() -> void:
        if not Engine.is_editor_hint():
            return
    
        if shape == null:
            return
    
        shape.draw(canvas_item, Color.DARK_BLUE)
    

    And to move it, we can set its transform, like this:

    Godot 3

    export var offset:Vector2 setget set_offset
    
    func set_offset(new_value:Vector2) -> void:
        if offset == new_value:
            return
    
        offset = new_value
        VisualServer.canvas_item_set_transform(
            canvas_item,
            Transform2D.IDENTITY.translated(offset)
        )
    

    Godot 4

    @export var offset:Vector2:
        set(new_value):
            if offset == new_value:
                return
    
            offset = new_value
            RenderingServer.canvas_item_set_transform(
                canvas_item,
                Transform2D.IDENTITY.translated(offset)
            )