Линейный рендеринг иногда не определяет пересечение и как сделать его более щадящим при зацикливании

У меня есть рендеринг 2d Line, который создает циклы, проблема, которую я заметил, заключается в том, что при быстром цикле он иногда не обнаруживает, я хочу знать, как этого не допустить, другая проблема - как сделать это более снисходительным например, если линия действительно близка к предыдущей строке, я бы хотел, чтобы она считалась циклом, а также как я могу сделать линию на самом деле указателем мыши, а не призрак позади, и это может быть проблемой в будущее У меня в настоящее время он создает область 2d для обнаружения предметов / объектов внутри себя. Мне было интересно, есть ли лучший способ точно обнаружить их внутри указанного цикла.

У меня есть ссылка на видео, которую я загрузил, чтобы визуально показать вам проблему: https://www.youtube.com/watch?v=Jau7YDpZehY

extends Node2D

var points_array = PoolVector2Array()
#var check_array = []
var colision_array = []
var index : int = 0
onready var Line = $Line2D
onready var collision = $Area2D/CollisionPolygon2D
onready var collision_area = $Loop_collider

func _physics_process(delta):
    Line.points = points_array # Link the array to the points and polygons
    collision.polygon = points_array
    
    if Input.is_action_just_pressed("left_click"): #This sets a position so that the next line can work together
        points_array.append(get_global_mouse_position()) # This makes a empty vector and the mouse cords is assigned too it
        #points_array.append(Vector2()) #This is the vector that follows the mouse
        
    if Input.is_action_pressed("left_click"): #This checks the distance between last vector and the mouse vector
        #points_array[-1] = get_global_mouse_position() # Gets the last position of the array and sets the mouse cords
        var mouse_pos = get_global_mouse_position()
        var distance = 20
        while points_array[-1].distance_to(mouse_pos) > distance:
            var last_point = points_array[-1]
            var cords = last_point + last_point.direction_to(mouse_pos) * distance
            points_array.append(cords)
            create_collision()

    if points_array.size() > 80: # This adds a length to the circle/line so it wont pass 18 mini lines
        points_array.remove(0) #Removes the first array to make it look like it has a length
        #check_array = []
        colision_array[0].queue_free()
        colision_array.remove(0)
    if Input.is_action_just_released("left_click"): # This just clears the screen when the player releases the button
        points_array = PoolVector2Array()
        #check_array = []
        for x in colision_array.size():
            colision_array[0].queue_free()
            colision_array.remove(0)
        #index = 0
    if points_array.size() > 3: # If the loop is long enough, to detect intersects
        if points_array[0].distance_to(get_global_mouse_position()) < 5: # Checks if the end of the loop is near the end, then start new loop
            new_loop()
        for index in range(0, points_array.size() - 3):
            if _segment_collision(
                    points_array[-1],
                    points_array[-2],
                    points_array[index],
                    points_array[index + 1]
                ):
                    new_loop()
                    break
                    
    #if check_array.size() != points_array.size():
    #   check_array = points_array
        #create_collision()

func _segment_collision(a1:Vector2, a2:Vector2, b1:Vector2, b2:Vector2) -> bool:
    # if both ends of segment b are to the same side of segment a, they do not intersect
    if sign(_wedge_product(a2 - a1, b1 - a1)) == sign(_wedge_product(a2 - a1, b2 - a1)):
        return false

    # if both ends of segment a are to the same side of segment b, they do not intersect     
    if sign(_wedge_product(b2 - b1, a1 - b1)) == sign(_wedge_product(b2 - b1, a2 - b1)):
        return false

    # the segments must intersect
    return true

func _wedge_product(a:Vector2, b:Vector2) -> float:
    # this is the length of the cross product
    # it has the same sign as the sin of the angle between the vectors
    return a.x * b.y - a.y * b.x

func new_loop(): # Creates a new loop when holding left click and or loop is complete
    var new_start = points_array[-1]
    collision.polygon = points_array
    points_array = PoolVector2Array()
    collision.polygon = []
    #check_array = []
    points_array.append(new_start)
    for x in colision_array.size():
        colision_array[0].queue_free()
        colision_array.remove(0)

func create_collision(): # Creates collisions to detect when something hits the line renderer
    var new_colision = CollisionShape2D.new()
    var c_shape = RectangleShape2D.new()
    var mid_point = Vector2((points_array[-1].x + points_array[-2].x) / 2,(points_array[-1].y + points_array[-2].y) / 2)
    c_shape.set_extents(Vector2(10,2))
    new_colision.shape = c_shape
    if points_array.size() > 1:
        colision_array.append(new_colision)
        collision_area.add_child(new_colision)
        new_colision.position = mid_point
        #new_colision.position = Vector2((points_array[-1].x),(points_array[-1].y))
        new_colision.look_at(points_array[-2])

func _on_Area2D_area_entered(area): # Test dummies 
    print("detect enemy")


func _on_Loop_collider_area_entered(area):
    print("square detected")

person Dragon20C    schedule 21.05.2021    source источник


Ответы (1)


Диагностика

Симптом 1

Я заметил, что при быстром цикле он иногда не обнаруживает

Вот что происходит:

  • Когда указатель мыши слишком сильно перемещается между кадрами, он создает несколько сегментов, здесь:

        var mouse_pos = get_global_mouse_position()
        var distance = 20
        while points_array[-1].distance_to(mouse_pos) > _distance:
            var last_point = points_array[-1]
            var cords = last_point + last_point.direction_to(mouse_pos) * distance
            points_array.append(cords)
            create_collision()
    
  • Но проверка на коллизии сравнивает только последнюю, здесь:

        for index in range(0, points_array.size() - 3):
            if _segment_collision(
                    points_array[-1],
                    points_array[-2],
                    points_array[index],
                    points_array[index + 1]
                ):
                    new_loop()
                    break
    

    * Помните, что [-1] дает последний элемент, а [-2] дает предпоследний элемент.

Как следствие, пересечение может произойти на одном из участков, которые не проверялись.


Симптом 2

как сделать его более снисходительным, например, если строка действительно близка к предыдущей строке, я бы хотел, чтобы это считалось циклом

Мы могли проверить расстояние от точки до сегмента.


Симптом 3

как сделать так, чтобы линия на самом деле была указателем мыши, а не призраком позади

В настоящее время все сегменты имеют одинаковую длину. Похоже, это ограничение способа создания CollisionShape2D.


Выбор лечения

Мы могли бы устранить Симптом 1, проверив каждый сегмент. Симптом 2 за счет улучшения указанной проверки. Но нам все равно понадобится решение для Симптома 3, которое допускает переменную длину сегментов.

Если мы создадим решение, поддерживающее переменную длину сегментов, нам не нужно будет создавать сразу несколько сегментов, что решает проблему 1. Нам все равно необходимо улучшить проверку, чтобы решить проблему 2.

Если нам нужно улучшить способ проверки столкновений, и мы все равно переписываем столкновения, мы могли бы также реализовать что-то, что позволяет нам обнаруживать самопересечения.

Мы собираемся трансплантировать новый способ определения форм столкновения, который позволяет нам создавать повернутые прямоугольники желаемых размеров.


Операция

В итоге я переписал весь сценарий. Думаю, потому, что я такой.

Я решил, что скрипт создает свои дочерние узлы в следующей структуре:

Node
├_line
├_segments
└_loops

Здесь_line будет Line2D, _segments будет содержать несколько Area2D, каждый сегмент. И _loops также будет содержать Area2D, но это многоугольники трассируемых петель.

Это будет сделано в _ready:

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)

Еще одно решение, которое я принял, заключалось в том, чтобы рассмотреть способ данных в заявке: мы занимаем позиции. Первая позиция - это когда щелчок просто нажимается. Последующие позиции - это когда он движется. С этих позиций мы берем точки, чтобы добавить как к линии, так и к сегментам. Из отрезков получим петли. И мы будем продолжать таким же образом, пока не будет отпущен щелчок.

Ну а если просто нажали или нет щелчок или он держится, не беда. Так или иначе, мы занимаем позицию мыши.

Теперь _physics_process будет выглядеть так:

func _physics_process(_delta:float) -> void:
    if Input.is_action_pressed("left_click"):
        position(get_global_mouse_position())
    # TODO

Нам также нужно обработать, когда щелчок отпущен. Давайте создадим для этого функцию и позже позаботимся об этом:

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

На position мы будем следовать этой странной уловке - перемещать последнюю точку, чтобы она соответствовала самой последней позиции. Нам нужно убедиться, что есть как минимум две точки. Таким образом, первая точка не перемещается, и мы можем смело перемещать последнюю точку.

var _points:PoolVector2Array = PoolVector2Array()
var _max_distance = 20

func position(pos:Vector2) -> void:
    var point_count = _points.size()
    if point_count == 0:
        _points.append(pos)
        _points.append(pos)
    elif  point_count == 1:
        _points.append(pos)
    else:
        if _points[-2].distance_to(pos) > _max_distance:
            _points.append(pos)
        else:
            _points[-1] = pos

Обратите внимание, мы проверяем расстояние от второй до последней точки. Мы не можем проверить по последней точке, потому что это та самая точка, которую мы перемещаем.

Если расстояние больше _max_dinstance, мы добавляем новую точку, в противном случае мы перемещаем последнюю точку.

Также нам нужно добавить и обновить сегменты:

var _points:PoolVector2Array = PoolVector2Array()
var _max_distance = 20

func 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)
    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)

Вы знаете, мы позже беспокоимся о том, как это работает.

Нам также необходимо обработать случай, когда точек слишком много:

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

func 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)

Нам нужно обновить Line2D, и нам нужно обрабатывать любые циклы:

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

func 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)

Здесь _width - это ширина полигонов столкновения, которые нам нужны.

Мы либо добавляем Area2D с многоугольником столкновения (созданный с помощью функции, о которой мы поговорим позже), либо берем последний Area2D и тем же способом обновляем его многоугольник столкновения.

Итак, как нам получить очки для повернутого прямоугольника?

static 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]

Итак, вы берете вектор, идущий от начала до конца сегмента, и поворачиваете его на четверть оборота (также известный как 90º). Это дает вам вектор, нормальный (перпендикулярный) сегменту, который мы будем использовать для придания ему ширины.

Из начальной точки мы находим первую точку прямоугольника, перемещая половину ширины в нормальном направлении, и находим вторую, перемещая другую половину ширины в противоположном направлении. Сделайте то же самое с конечной точкой, и у нас есть четыре угла прямоугольника.

И возвращаем их в таком порядке, чтобы они обходили прямоугольник.

Создать многоугольник столкновения с этими точками очень просто:

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

Хорошо, давайте поговорим о чистке. Я добавил функцию очистки точек (линии) и сегментов. Это часть тотальной чистки. Другая часть будет снимать петли:

func total_purge():
    purge(_points.size())
    purge_loops()

Это было легко.

Хорошо, чтобы очистить точки и сегментировать, мы повторяем и удаляем их.

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

    _line.points = _points

Между прочим, эта проверка на if segments.size() > 0 необходима. Иногда при очистке точки остаются без сегмента, что впоследствии вызывает проблемы. И это более простое решение.

И, конечно же, нам нужно обновить Line2D.

А как насчет продувки петель? Ну вы удалите их все:

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

Наконец, мы можем обработать петли. Мы будем проверять перекрывающиеся области сегментов, чтобы определить, пересекаются ли они друг с другом.

Одно предостережение: мы хотим игнорировать перекрытия соседних сегментов (которые неизбежны и не образуют петель).

Таким образом, мы перебираем сегменты, проверяем перекрывающиеся области, ищем их среди сегментов (если они вообще есть) и несмежные ли они (различие их индексов среди сегментов должно быть больше 1). Если все это произойдет, у нас будет цикл:

func process_loop() -> void:
    var segments = _segments.get_children()
    for index in range(segments.size() - 1, 0, -1):
        var segment = 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) > 1:
                push_loop(candidate_index, index)
                purge(index)
                return

Итак, когда возникает петля, мы хотим что-то с ней сделать, верно? Вот для чего нужен push_loop. Мы также хотим удалить точки и сегменты, которые были частью цикла (или были до цикла), поэтому мы вызываем purge.

Осталось только push_loop обсудить:

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(0)

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

Как видите, он создает Area2D с многоугольником столкновения, который соответствует циклу. Я решаю использовать rezise для удаления точек после цикла и цикл for для удаления точек, которые находятся до. Остались только точки петли.

Также обратите внимание, что я вызываю purge_loops в начале, чтобы гарантировать, что будет только один цикл за раз.


Возвращаясь к симптомам: симптомы 1 и 3 решаются с помощью этого трюка, заключающегося в постоянном перемещении последней точки (и обновлении сегмента). А Симптом 2 решается за счет ширины прямоугольников. Настройте это значение.

person Theraot    schedule 21.05.2021
comment
Я прочитал все это, и это потрясающе, мне нравится узнавать, как вы делаете то, что имеет смысл, и ваши объяснения отличные, спасибо за этот сценарий! - person Dragon20C; 21.05.2021