[Tool] public partial class Interactable : RigidBody3D { PackedScene _modelScene; [Export] public PackedScene ModelScene { get => _modelScene; set => _modelScene = OnModelSceneChanged(value); } Vector3I _gridSize; /// Get the size of this item in grid units. [Export] public Vector3I GridSize { get => _gridSize; set => _gridSize = OnGridSizeChanged(value); } public override void _Ready() { } public override void _Process(double delta) { } // FIXME: Only change this if ModelScene is actually changed, not when loaded. PackedScene OnModelSceneChanged(PackedScene value) { if (GetNodeOrNull("_Model") is Node oldModel) { RemoveChild(oldModel); oldModel.QueueFree(); } // Remove any previously added `CollisionShape3D` nodes. foreach (var child in GetChildren(true).OfType()) { RemoveChild(child); child.QueueFree(); } if (value is PackedScene scene) { var model = scene.Instantiate(); model.Name = "_Model"; var numShapes = 0; var min = Vector3.Zero; var max = Vector3.Zero; // Find all the `StaticBody3D` nodes in the model and parent // their `CollisionShape3D` children to the this `RigidBody3D`. // Required because shapes must be immediate children of the body. // See: https://github.com/godotengine/godot-proposals/issues/535 // https://github.com/godotengine/godot/pull/77937 foreach (var body in model.FindChildren("*", "StaticBody3D").Cast()) { body.GetParent().RemoveChild(body); body.QueueFree(); foreach (var shape in body.GetChildren().OfType()) { // Not unsetting the owner results in this warning: // "Adding 'CollisionShape3D' as child to 'Interactable' will make owner '...' inconsistent." shape.Owner = null; body.RemoveChild(shape); shape.Name = $"_{nameof(CollisionShape3D)}_{numShapes + 1}"; AddChild(shape, false, InternalMode.Front); // shape.Owner = this; numShapes++; // Finds the axis-aligned boundary of all collision shapes. // This assumes that the shape has an identity transformation. var vertices = (shape.Shape as ConvexPolygonShape3D)?.Points ?? (shape.Shape as ConcavePolygonShape3D)?.Data ?? throw new Exception("Shape must be either convex or concave"); foreach (var vert in vertices) { min = new(Min(min.X, vert.X), Min(min.Y, vert.Y), Min(min.Z, vert.Z)); max = new(Max(max.X, vert.X), Max(max.Y, vert.Y), Max(max.Z, vert.Z)); } } } AddChild(model, false, InternalMode.Front); // Set the grid size based on the boundary of all collision shapes. GridSize = (Vector3I)((max - min).Snapped(Epsilon) / Grid.StepSize).Ceil(); } else { GridSize = Vector3I.Zero; } return value; } Vector3I OnGridSizeChanged(Vector3I value) { if (GetNodeOrNull("_GridArea") is Node oldGridArea) { RemoveChild(oldGridArea); oldGridArea.QueueFree(); } if (value.X > 0 && value.Y > 0 && value.Z > 0) { var gridArea = new Area3D(); gridArea.Name = "_GridArea"; var shape = new CollisionShape3D(); shape.Shape = new BoxShape3D { Size = (Vector3)value * Grid.StepSize }; gridArea.AddChild(shape); AddChild(gridArea, false, InternalMode.Front); } return value; } }