소스 검색

Make block and core collision match what is drawn

Blocks and the core were drawn 2 px inside their collision shapes, so
the ball bounced and the hit sound played before it visibly touched
them. Both now collide with exactly the shape that is drawn: the
rounded, gapped tile outline and the inset core circle.

Godot's polygon offset can emit repeated points, which made convex
decomposition of some rounded tiles fail, so those are removed. Very
short segments collapse their edges to a point instead of crossing
over themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gabriel Capella 5 일 전
부모
커밋
cd05524d07
2개의 변경된 파일과 36개의 추가작업 그리고 11개의 파일을 삭제
  1. 33 9
      scripts/block.gd
  2. 3 2
      scripts/core.gd

+ 33 - 9
scripts/block.gd

@@ -2,7 +2,7 @@ class_name Block
 extends AnimatableBody2D
 ## One arc-shaped segment of a ring around the core.
 
-const GAP := 4.0 # px between neighbouring tiles, drawn only (collision has no gap)
+const GAP := 4.0 # px between neighbouring tiles
 const CORNER := 4.0 # corner rounding radius in px
 const BREAK_SECONDS := 0.15 # fade-out time, after a short flash
 
@@ -22,11 +22,14 @@ func _init(inner: float, outer: float, begin: float, end: float, is_indestructib
 	collision_mask = 0
 	indestructible = is_indestructible
 
-	var collision := CollisionPolygon2D.new()
-	collision.polygon = _sector(inner, outer, begin + 1.0 / outer, end - 1.0 / outer, 0.0)
-	add_child(collision)
-
+	# Collision uses the drawn outline, so the ball bounces exactly where it touches.
 	_polygon = _round_corners(_sector(inner + GAP / 2.0, outer - GAP / 2.0, begin, end, GAP / 2.0))
+	# Very short segments can leave a sliver too thin to split into convex pieces; skip its
+	# collision rather than let Godot fail on it.
+	if not Geometry2D.decompose_polygon_in_convex(_polygon).is_empty():
+		var collision := CollisionPolygon2D.new()
+		collision.polygon = _polygon
+		add_child(collision)
 
 	if indestructible:
 		color = Params.COLOR_ORANGE
@@ -48,6 +51,8 @@ func destroy() -> void:
 
 
 func _draw() -> void:
+	if _polygon.size() < 3:
+		return # segment too short to show at all
 	var fill := color.lerp(Color.WHITE, _flash)
 	draw_colored_polygon(_polygon, fill)
 	# Polygons have no antialiasing of their own; a smooth outline softens the edge.
@@ -57,11 +62,19 @@ func _draw() -> void:
 
 
 ## Ring segment between two radii. [param gap] trims each side by that many pixels,
-## so the space between neighbours has the same width at every radius.
+## so the space between neighbours has the same width at every radius. An edge too short
+## to trim collapses to a point, turning the segment into a wedge.
 static func _sector(inner: float, outer: float, begin: float, end: float, gap: float) -> PackedVector2Array:
 	var steps := maxi(2, int((end - begin) * sqrt(outer) / 1.5))
-	var outer_arc := _arc(outer, begin + gap / outer, end - gap / outer, steps)
-	return outer_arc + _arc(inner, end - gap / inner, begin + gap / inner, steps)
+	return _trimmed_arc(outer, begin, end, gap, steps) + _trimmed_arc(inner, end, begin, -gap, steps)
+
+
+static func _trimmed_arc(radius: float, from: float, to: float, gap: float, steps: int) -> PackedVector2Array:
+	var trim := gap / radius
+	if (to - from - 2.0 * trim) * signf(to - from) <= 0.0:
+		var middle := (from + to) / 2.0
+		return PackedVector2Array([Vector2(sin(middle), cos(middle)) * radius])
+	return _arc(radius, from + trim, to - trim, steps)
 
 
 ## Shrinks then regrows the polygon with round joins, which rounds its corners.
@@ -70,7 +83,18 @@ static func _round_corners(polygon: PackedVector2Array) -> PackedVector2Array:
 	if shrunk.is_empty():
 		return polygon
 	var rounded := Geometry2D.offset_polygon(shrunk[0], CORNER, Geometry2D.JOIN_ROUND)
-	return polygon if rounded.is_empty() else rounded[0]
+	return polygon if rounded.is_empty() else _without_duplicates(rounded[0])
+
+
+## offset_polygon can emit repeated points; zero-length edges break convex decomposition.
+static func _without_duplicates(polygon: PackedVector2Array) -> PackedVector2Array:
+	var result := PackedVector2Array()
+	for point in polygon:
+		if result.is_empty() or not point.is_equal_approx(result[-1]):
+			result.append(point)
+	if result.size() > 1 and result[0].is_equal_approx(result[-1]):
+		result.remove_at(result.size() - 1)
+	return result
 
 
 static func _arc(radius: float, from: float, to: float, steps: int) -> PackedVector2Array:

+ 3 - 2
scripts/core.gd

@@ -2,6 +2,8 @@ class_name Core
 extends StaticBody2D
 ## The red core. Hitting it in the game scene wins the level.
 
+## Nominal radius; the core is drawn and collides [constant Block.GAP] / 2 inside it, like the
+## blocks, so the gap to the first ring matches the gap between tiles.
 @export var radius := Params.CORE_RADIUS
 
 
@@ -9,7 +11,7 @@ func _ready() -> void:
 	collision_layer = Params.LAYER_BLOCKS
 	collision_mask = 0
 	var shape := CircleShape2D.new()
-	shape.radius = radius
+	shape.radius = radius - Block.GAP / 2.0
 	var collision := CollisionShape2D.new()
 	collision.shape = shape
 	add_child(collision)
@@ -24,5 +26,4 @@ func explode() -> void:
 
 
 func _draw() -> void:
-	# Inset like the blocks, so the gap to the first ring matches the gap between tiles.
 	draw_circle(Vector2.ZERO, radius - Block.GAP / 2.0, Params.COLOR_RED, true, -1.0, true)