Skip to content

Instantly share code, notes, and snippets.

@meloonics
Created June 20, 2022 15:01
Show Gist options
  • Save meloonics/791f284cbf319edd1a739fbf2f303600 to your computer and use it in GitHub Desktop.
Save meloonics/791f284cbf319edd1a739fbf2f303600 to your computer and use it in GitHub Desktop.
Script for Area2D to wrap its CollisionPolygon2D around a given Polygon2D
#######################################################################################
# ~~~~~~~~ Polygon-Snapping Area2D - written by meloonics in Godot 3.4, 2022 ~~~~~~~~ #
# HOW TO USE: #
# 1. Make a scene which has Area2D as root and a CollisionPolygon2D as child. #
# Call it whatever, but don't rename the CollisionPolygon2D. #
# 2. Attach this script to the root. #
# 3. Add the scene as child to either Polygon2D or CollisionPolygon2D. Won't work otherwise, no idea why you think it would smh my head
# 4. Adjust offset, distance and thickness to your liking. #
# The CollisionShape of the Area2D should update automatically. #
# 5. Don't overdo it with the thickness in concave shapes, or the polygon will break #
# 6. Feel free to adjust the maximum values of the below export vars. #
# or remove them. personally I like to have a slider for my floats #
# 7. If you accidentally drew your polygon counterclockwise and the shape is now #
# on the inside of it, toggle "invert_normals" to fix it. #
# 8. Shameless plug time: #
# https://meloonics.itch.io/ #
# https://github.com/meloonics #
# https://www.youtube.com/c/LucyLavend <-- not me, but subscribe anyways #
#######################################################################################
tool
extends Area2D
# v-- these ones!
export(float, 0.0, 2000.0) var offset = 0.0
export(float, 1.0, 2000.0) var distance = 1.0
export(float, 1.0, 100.0) var thickness = 10.0
export(bool) var invert_normals = false
var poly : PoolVector2Array
var vertex_normals : PoolVector2Array
var edge_normals : PoolVector2Array
var segment : PoolVector2Array
var segment_normals : PoolVector2Array
var segment_polygon : PoolVector2Array
func _ready() -> void:
if !Engine.editor_hint:
poggers_in_chat()
func _process(delta : float) -> void:
if Engine.editor_hint:
poggers_in_chat()
#Used for debugging. Activate by uncommenting the "update()"-call in Line 220
func _draw() -> void:
if Engine.editor_hint:
for i in poly.size():
draw_circle(poly[i], 3.0, Color.red)
draw_line(poly[i], poly[i] + vertex_normals[i] * 20.0, Color.red)
var next_vertex = poly[posmod(i + 1, poly.size())]
var middle = poly[i] + poly[i].direction_to(next_vertex) * (poly[i].distance_to(next_vertex) / 2)
draw_line(middle, middle + edge_normals[i] * 20.0, Color.red)
func poggers_in_chat() -> void:
if is_parent_valid():
construct_area_polygon()
else:
push_error("Invalid parent: Need CollisionPolygon2D or Polygon2D!")
set_process(false)
func clone_polygon() -> PoolVector2Array:
var poly : PoolVector2Array = get_parent().polygon
return poly
func is_parent_valid() -> bool:
return get_parent() is CollisionPolygon2D or get_parent() is Polygon2D
func construct_area_polygon() -> void:
poly = clone_polygon()
if poly.size() == 0:
$CollisionPolygon2D.polygon = PoolVector2Array([])
return
investigate_polygon(poly)
segment = PoolVector2Array([])
segment_normals = PoolVector2Array([])
segment_polygon = PoolVector2Array([])
var total_length : float = 0.0
for i in poly.size():
var next = poly[posmod(i + 1, poly.size())]
total_length += poly[i].distance_to(next)
if offset >= total_length:
offset = fmod(offset, total_length)
distance = clamp(distance, 1.0, total_length)
var remaining_length : float = offset
var current_idx : int = 0
var found_ending : bool = false
var ending : Vector2
var ending_normal : Vector2
var beginning : Vector2
var beginning_normal : Vector2
#find beginning point and potentially end-point
while true:
#find current vertex, next vertex and the distance between the two
var next_idx : int = posmod(current_idx + 1, poly.size())
var cv : Vector2 = poly[current_idx]
var nv : Vector2 = poly[next_idx]
var dist : float = cv.distance_to(nv)
# check if start-point is between current and next vertex:
if remaining_length < dist:
var point : Vector2
point = cv + cv.direction_to(nv) * remaining_length
beginning = point
if Array(poly).has(point):
beginning_normal = vertex_normals[current_idx]
else:
beginning_normal = edge_normals[current_idx]
#Now check, if the end-point is between the current vertices as well
dist = beginning.distance_to(nv)
remaining_length = distance
if remaining_length < dist:
found_ending = true
ending = beginning + beginning.direction_to(nv) * remaining_length
if Array(poly).has(ending):
if ending == beginning:
ending_normal = vertex_normals[current_idx]
else:
ending_normal = vertex_normals[next_idx]
else:
ending_normal = edge_normals[current_idx]
#if no ending has been found, subtract the remaining distance and add next vertex to segment.
#also set next vertex as starting point for next loop
else:
remaining_length -= dist
current_idx = next_idx
segment.append(beginning)
segment.append(nv)
segment_normals.append(beginning_normal)
segment_normals.append(vertex_normals[current_idx])
break
else:
remaining_length -= dist
current_idx = next_idx
if found_ending:
segment.append(ending)
segment_normals.append(ending_normal)
else:
#Here we look for the ending if none has been found, and all in-between-vertices
while true:
#same as before, could have probably done this in previous loop but wanted to keep it readable
var next_idx : int = posmod(current_idx + 1, poly.size())
var cv : Vector2 = poly[current_idx]
var nv : Vector2 = poly[next_idx]
var dist : float = cv.distance_to(nv)
if remaining_length < dist:
ending = cv + cv.direction_to(nv) * remaining_length
segment.append(ending)
if Array(poly).has(ending):
if ending == cv:
ending_normal = vertex_normals[current_idx]
else:
ending_normal = vertex_normals[next_idx]
else:
ending_normal = edge_normals[current_idx]
segment_normals.append(ending_normal)
break
else:
remaining_length -= dist
segment.append(nv)
segment_normals.append(vertex_normals[next_idx])
current_idx = next_idx
#PHEW! WHAT A RIDE! Now we finally stitch together the new polygon for the Area2D
#Basically we make a copy of the segment-array, offset each point by their normal-vector
#and invert it
segment_polygon.append_array(segment)
var inv_array : PoolVector2Array
for i in segment.size():
var normal : Vector2
if invert_normals:
normal = -segment_normals[i] * thickness
else:
normal = segment_normals[i] * thickness
inv_array.append(segment[i] + normal)
inv_array.invert()
segment_polygon.append_array(inv_array)
#BOOM! We're DONE!
$CollisionPolygon2D.polygon = segment_polygon
func investigate_polygon(poly : PoolVector2Array) -> void:
# Here we gather information about vertex- and edge-normals of the given polygon.
vertex_normals = PoolVector2Array([])
edge_normals = PoolVector2Array([])
for i in poly.size():
var vi : Vector2 = poly[i]
var vleft : Vector2 = poly[posmod(i - 1, poly.size())]
var vright : Vector2 = poly[posmod(i + 1, poly.size())]
var angle = vi.direction_to(vleft).angle_to(vi.direction_to(vright))
var normal = vi.direction_to(vleft).rotated(angle / 2)
vertex_normals.append(normal * (-1 if angle < 0 else 1))
var edge_normal = vi.direction_to(vright).rotated(-PI/2)
edge_normals.append(edge_normal)
#update()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment