user1865027
user1865027

Reputation: 3647

How to add collision to Line2D dynamically in Godot4?

I'm totally new to godot engine. Wanted to create a game that allows player to draw lines on the screen. All the lines drawn on screen will have physics interaction with other collisionShape. I found the tutorial and was able to draw lines using the code below. However, I still can't get the collision to work with existing collisionShape

@onready var _lines := $Lines

var _pressed := false
var _current_line: Line2D

func _input(event):
    if event is InputEventMouseButton:
        _pressed = event.pressed
        
        if _pressed:
            _current_line = Line2D.new()
            _lines.add_child(_current_line)
        
    if event is InputEventMouseMotion && _pressed:
        _current_line.add_point(event.position)

Upvotes: 1

Views: 2058

Answers (1)

Theraot
Theraot

Reputation: 40305

The following is the code I made for a prior question (here) adapted to Godot 4:

extends Node2D

var _line:Line2D
var _segments:Node2D
var _loops:Node2D

func _ready() -> void:
    _line = Line2D.new()
    _line.name = "_line"
    add_child(_line)
    _segments = Node2D.new()
    _segments.name = "_segments"
    add_child(_segments)
    _loops = Node2D.new()
    _loops.name = "_loops"
    add_child(_loops)

func _physics_process(_delta:float) -> void:
    if Input.is_action_pressed("left_click"):
        add_position(get_global_mouse_position())
    if Input.is_action_just_released("left_click"):
        total_purge()

var _points:PackedVector2Array = PackedVector2Array()
var _max_points := 30
var _max_distance := 20

func add_position(pos:Vector2) -> void:
    var point_count := _points.size()
    if point_count == 0:
        _points.append(pos)
        _points.append(pos)
        add_segment(pos, pos)
    elif point_count == 1:
        _points.append(pos)
        add_segment(_points[-2], pos)
    elif point_count > _max_points:
        purge(point_count - _max_points)
    else:
        if _points[-2].distance_to(pos) > _max_distance:
            _points.append(pos)
            add_segment(_points[-2], pos)
        else:
            _points[-1] = pos
            change_segment(_points[-2], pos)

    _line.points = _points
    process_loop()

var _width := 5

func add_segment(start:Vector2, end:Vector2) -> void:
    var points := rotated_rectangle_points(start, end, _width)
    var segment := Area2D.new()
    var collision := create_collision_polygon(points)
    segment.add_child(collision)
    _segments.add_child(segment)

func change_segment(start:Vector2, end:Vector2) -> void:
    var points := rotated_rectangle_points(start, end, _width)
    var segment := (_segments.get_child(_segments.get_child_count() - 1) as Area2D)
    var collision := (segment.get_child(0) as CollisionPolygon2D)
    collision.set_polygon(points)

func rotated_rectangle_points(start:Vector2, end:Vector2, width:float) -> Array:
    var diff := end - start
    var normal := diff.rotated(TAU/4).normalized()
    var offset := normal * width * 0.5
    return [start + offset, start - offset, end - offset, end + offset]

func create_collision_polygon(points:Array) -> CollisionPolygon2D:
    var result := CollisionPolygon2D.new()
    result.set_polygon(points)
    return result

func total_purge() -> void:
    purge(_points.size())
    purge_loops()

func purge(index:int) -> void:
    var segments := _segments.get_children()
    for _index in range(0, index):
        if _points.size() > 0:
            _points.remove_at(0)
        if segments.size() > 0:
            _segments.remove_child(segments[0])
            segments[0].queue_free()
            segments.remove_at(0)

    _line.points = _points

func purge_loops() -> void:
    for loop in _loops.get_children():
        if is_instance_valid(loop):
            loop.queue_free()

func process_loop() -> void:
    var segments := _segments.get_children()
    for index in range(segments.size() - 1, 0, -1):
        var segment:Area2D = segments[index]
        var candidates := segment.get_overlapping_areas()
        for candidate in candidates:
            var candidate_index := segments.find(candidate)
            if candidate_index == -1:
                continue

            if abs(candidate_index - index) > 2:
                push_loop(candidate_index, index)
                purge(index)
                return

func push_loop(first_index:int, second_index:int) -> void:
    purge_loops()
    var loop := Area2D.new()
    var points := _points
    points.resize(second_index)
    for point_index in first_index + 1:
        points.remove_at(0)

    var collision := create_collision_polygon(points)
    loop.add_child(collision)
    _loops.add_child(loop)

Since you are adding points with the mouse, this should be a good fit for your question.

The code maintains a series of Area2D under _segments, created to match a Line2D that is being drawn by dragging the mouse (which results in calls to add_position which updates both the Line2D and the Area2D).

Since, as I said, it works by dragging, it uses a variable _max_distance to decide when to create a new segment of the Line2D.

The part you are likely to be interested in are the methods add_segment and change_segment. The reason why change_segment exists is because the last segment is updated as the pointer moves until it exceeds _max_distance, at which point a new segment is added.

As you can see the Area2D have each a CollisionPolygon2D which is made to be a rotated rectangle (created by the method rotated_rectangle_points based on the two points that define the segment and the variable _width).

It should be possible to create a single Area2D and make a connected polygon to represent the Line2D (for which you need a solution to make the segment connections)...

... However this solution was made to also make it easier to remove segments at the end of the line. In fact, the lines length is limited by the value of _max_points.

Upvotes: 0

Related Questions