Skip to content

Instantly share code, notes, and snippets.

@Shilo
Last active March 23, 2024 11:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Shilo/e8ef6ea371c847b9267967b78d162412 to your computer and use it in GitHub Desktop.
Save Shilo/e8ef6ea371c847b9267967b78d162412 to your computer and use it in GitHub Desktop.
Godot tool for drawing 2d collision shapes. (C# or GDScript)
@tool
class_name ColorCollisionShape2D extends CollisionShape2D
@export var color: Color:
set(value):
color = value
queue_redraw()
@export var outline_color: Color:
set(value):
outline_color = value
queue_redraw()
@export var outline_width: int = 0:
set(value):
value = max(value, 0)
if outline_width == value:
return
outline_width = value
queue_redraw()
@export var outline_rect_tile_size: Vector2 = Vector2.ZERO:
set(value):
outline_rect_tile_size = value.abs()
queue_redraw()
func _draw():
if !shape:
return
if shape is CircleShape2D:
var outline: int = min(outline_width, shape.radius)
if outline > 0:
_draw_circle(Vector2.ZERO, shape.radius, outline_color)
_draw_circle(Vector2.ZERO, shape.radius, color, outline)
elif shape is CapsuleShape2D:
var outline: int = min(outline_width, shape.radius)
if outline > 0:
_draw_capsule(shape.radius, shape.height, outline_color)
_draw_capsule(shape.radius, shape.height, color, outline)
else:
var rect: Rect2 = Rect2(-shape.extents, shape.extents * 2)
var outline: int = min(outline_width, shape.extents.x, shape.extents.y)
if outline_rect_tile_size.x > 0 and outline_rect_tile_size.y > 0:
outline = min(outline, outline_rect_tile_size.x, outline_rect_tile_size.y)
for x in range(ceil(rect.size.x / outline_rect_tile_size.x)):
for y in range(ceil(rect.size.y / outline_rect_tile_size.y)):
var width = min(
outline_rect_tile_size.x, rect.size.x - x * outline_rect_tile_size.x
)
var height = min(
outline_rect_tile_size.y, rect.size.y - y * outline_rect_tile_size.y
)
var tile_rect = Rect2(
Vector2(x, y) * outline_rect_tile_size + rect.position,
Vector2(width, height)
)
_draw_rect_and_outline(tile_rect, outline)
else:
_draw_rect_and_outline(rect, outline)
func _draw_rect_and_outline(rect: Rect2, outline: int = 0):
if outline > 0:
_draw_rect(rect, outline_color)
_draw_rect(rect, color, outline)
func _draw_rect(rect: Rect2, rect_color: Color, inset_width: int = 0):
if inset_width > 0:
rect = Rect2(
rect.position + Vector2(inset_width, inset_width),
rect.size - Vector2(inset_width * 2, inset_width * 2)
)
draw_rect(rect, rect_color)
func _draw_circle(
circle_position: Vector2, radius: float, circle_color: Color, inset_width: int = 0
):
if inset_width > 0:
draw_circle(circle_position, radius - inset_width, circle_color)
else:
draw_circle(circle_position, radius, circle_color)
func _draw_capsule(radius: float, height: float, capsule_color: Color, inset_width: int = 0):
height -= inset_width * 2
radius -= inset_width
var center_height = height - radius * 2
var inset_height = center_height - inset_width * 2 + inset_width * 2
draw_rect(
Rect2(Vector2(-radius, -inset_height / 2), Vector2(radius * 2, inset_height)), capsule_color
)
draw_circle(Vector2(0, -height / 2 + radius), radius, capsule_color)
draw_circle(Vector2(0, height / 2 - radius), radius, capsule_color)
using System.Linq;
using Godot;
[Tool]
[GlobalClass]
public partial class ColorCollisionShape2D : CollisionShape2D
{
private Color _color = Colors.White;
[Export]
public Color Color
{
get => _color;
set
{
_color = value;
QueueRedraw();
}
}
private Color _outlineColor = Colors.White;
[Export]
public Color OutlineColor
{
get => _outlineColor;
set
{
_outlineColor = value;
QueueRedraw();
}
}
private int _outlineWidth;
[Export]
public int OutlineWidth
{
get => _outlineWidth;
set
{
_outlineWidth = Mathf.Max(value, 0);
QueueRedraw();
}
}
private Vector2 _outlineTileRectSize = Vector2.Zero;
[Export]
public Vector2 OutlineTileRectSize
{
get => _outlineTileRectSize;
set
{
_outlineTileRectSize = value.Abs();
QueueRedraw();
}
}
[Export]
public bool IncludeLightOccluder = false; // Only supports RectangleShape2D and ConvexPolygonShape2D
public override void _Ready()
{
if (!Engine.IsEditorHint())
UpdateLightOccluder();
}
public void UpdateLightOccluder()
{
if (!IncludeLightOccluder)
return;
LightOccluder2D lightOccluder = new LightOccluder2D();
OccluderPolygon2D occluderPolygon = new OccluderPolygon2D();
switch (Shape)
{
case RectangleShape2D rectangleShape:
{
var rect = new Rect2(-rectangleShape.Size / 2, rectangleShape.Size);
occluderPolygon.Polygon = new[]
{
rect.Position,
rect.Position + new Vector2(rect.Size.X, 0),
rect.Position + rect.Size,
rect.Position + new Vector2(0, rect.Size.Y)
};
break;
}
case ConvexPolygonShape2D convexPolygonShape:
{
occluderPolygon.Polygon = convexPolygonShape.Points;
break;
}
}
lightOccluder.Occluder = occluderPolygon;
foreach (var child in GetChildren())
{
if (child is LightOccluder2D)
RemoveChild(child);
}
AddChild(lightOccluder);
}
public override void _Draw()
{
switch (Shape)
{
case null:
return;
case CircleShape2D circleShape:
{
var outline = Mathf.Min(OutlineWidth, (int)circleShape.Radius);
if (outline > 0) DrawCircle(Vector2.Zero, circleShape.Radius, OutlineColor);
DrawCircle(Vector2.Zero, circleShape.Radius, Color, outline);
break;
}
case CapsuleShape2D capsuleShape:
{
var outline = Mathf.Min(OutlineWidth, (int)capsuleShape.Radius);
if (outline > 0) DrawCapsule(capsuleShape.Radius, capsuleShape.Height, OutlineColor);
DrawCapsule(capsuleShape.Radius, capsuleShape.Height, Color, outline);
break;
}
case RectangleShape2D rectangleShape:
{
var rect = new Rect2(-rectangleShape.Size / 2, rectangleShape.Size);
var outline = Mathf.Min(Mathf.Min(OutlineWidth, (int)rectangleShape.Size.X),
(int)rectangleShape.Size.Y);
if (OutlineTileRectSize is { X: > 0, Y: > 0 })
{
outline = Mathf.Min(Mathf.Min(outline, (int)OutlineTileRectSize.X), (int)OutlineTileRectSize.Y);
for (var x = 0; x < Mathf.CeilToInt(rect.Size.X / OutlineTileRectSize.X); x++)
for (var y = 0; y < Mathf.CeilToInt(rect.Size.Y / OutlineTileRectSize.Y); y++)
{
var width = Mathf.Min(OutlineTileRectSize.X, rect.Size.X - x * OutlineTileRectSize.X);
var height = Mathf.Min(OutlineTileRectSize.Y, rect.Size.Y - y * OutlineTileRectSize.Y);
var tileRect = new Rect2(
new Vector2(x, y) * OutlineTileRectSize + rect.Position,
new Vector2(width, height)
);
DrawRectAndOutline(tileRect, outline);
}
}
else
{
DrawRectAndOutline(rect, outline);
}
break;
}
case ConvexPolygonShape2D convexPolygonShape:
{
var outline = OutlineWidth;
if (outline > 0) DrawPolygon(convexPolygonShape.Points, OutlineColor);
DrawPolygon(convexPolygonShape.Points, Color, outline);
break;
}
}
}
private void DrawPolygon(Vector2[] points, Color polygonColor, int insetWidth = 0)
{
if (insetWidth > 0)
{
var insetPoints = new Vector2[points.Length];
var centerPoint = points.Aggregate(Vector2.Zero, (current, point) => current + point);
centerPoint /= points.Length;
for (var i = 0; i < points.Length; i++)
insetPoints[i] = points[i].MoveToward(centerPoint, insetWidth);
/* Old method that only works with 1 inset width
for (var i = 0; i < points.Length; i++)
{
var point = points[i];
var nextPoint = points[(i + 1) % points.Length];
var prevPoint = points[(i - 1 + points.Length) % points.Length];
var nextPointDir = (nextPoint - point).Normalized();
var prevPointDir = (prevPoint - point).Normalized();
var nextPointInset = nextPoint - nextPointDir * insetWidth;
var prevPointInset = prevPoint - prevPointDir * insetWidth;
var insetPoint = point + (nextPointInset - point).Normalized() + (prevPointInset - point).Normalized();
insetPoints[i] = insetPoint;
}
*/
points = insetPoints;
}
base.DrawPolygon(points, new[] { polygonColor });
}
private void DrawRectAndOutline(Rect2 rect, int outline = 0)
{
if (outline > 0) DrawRect(rect, OutlineColor);
DrawRect(rect, Color, outline);
}
private void DrawRect(Rect2 rect, Color rectColor, int insetWidth = 0)
{
if (insetWidth > 0)
rect = new Rect2(
rect.Position + new Vector2(insetWidth, insetWidth),
rect.Size - new Vector2(insetWidth * 2, insetWidth * 2)
);
base.DrawRect(rect, rectColor);
}
private void DrawCircle(Vector2 circlePosition, float radius, Color circleColor, int insetWidth = 0)
{
if (insetWidth > 0)
((CanvasItem)this).DrawCircle(circlePosition, radius - insetWidth, circleColor);
else
((CanvasItem)this).DrawCircle(circlePosition, radius, circleColor);
}
private void DrawCapsule(float radius, float height, Color capsuleColor, int insetWidth = 0)
{
height -= insetWidth * 2;
radius -= insetWidth;
var centerHeight = height - radius * 2;
var insetHeight = centerHeight - insetWidth * 2 + insetWidth * 2;
base.DrawRect(
new Rect2(new Vector2(-radius, -insetHeight / 2), new Vector2(radius * 2, insetHeight)),
capsuleColor
);
((CanvasItem)this).DrawCircle(new Vector2(0, -height / 2 + radius), radius, capsuleColor);
((CanvasItem)this).DrawCircle(new Vector2(0, height / 2 - radius), radius, capsuleColor);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment