diff --git a/assets/shaders/ReplacePalette.cs b/assets/shaders/ReplacePalette.cs index f182cc7..d133c40 100644 --- a/assets/shaders/ReplacePalette.cs +++ b/assets/shaders/ReplacePalette.cs @@ -1,3 +1,4 @@ +// FIXME: Unable to cast object of type 'ReplacePalette' to type 'Godot.CollisionObject3D'. public partial class ReplacePalette : Node3D { readonly struct TextureInfo(float min, float max) diff --git a/objects/Interactable.cs b/objects/Interactable.cs new file mode 100644 index 0000000..b568f57 --- /dev/null +++ b/objects/Interactable.cs @@ -0,0 +1,110 @@ +[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; + } +} diff --git a/objects/Item.cs b/objects/Item.cs index f5a4280..aba23b9 100644 --- a/objects/Item.cs +++ b/objects/Item.cs @@ -1,5 +1,3 @@ -using Godot.NativeInterop; - public partial class Item : RigidBody3D { public MultiplayerSynchronizer Sync { get; internal set; } @@ -18,8 +16,11 @@ public partial class Item : RigidBody3D public override void _Ready() { // Set the collision properties here so we don't have to specify them in each item scene separately. - CollisionLayer = (uint)(PhysicsLayer.Item | PhysicsLayer.Pickup); - CollisionMask = (uint)(PhysicsLayer.Static | PhysicsLayer.Dynamic | PhysicsLayer.Player | PhysicsLayer.Item); + CollisionLayer = (uint)(PhysicsLayer.Small | PhysicsLayer.Interact | PhysicsLayer.Placable); + CollisionMask = (uint)(PhysicsLayer.Static | PhysicsLayer.Dynamic | PhysicsLayer.Small | PhysicsLayer.Body); + + // TODO: Create a separate area for .Placable. + // Though gotta keep in mind that for some objects it needs to be same as physics collider, like small box. // TODO: Find a better way to better import models with colliders. // TODO: Import items dynamically at runtime? diff --git a/objects/interactable.tscn b/objects/interactable.tscn new file mode 100644 index 0000000..e2a4e0b --- /dev/null +++ b/objects/interactable.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=3 format=3 uid="uid://d4l5bguk01qqr"] + +[ext_resource type="Script" path="res://objects/Interactable.cs" id="1_vnawe"] +[ext_resource type="PackedScene" uid="uid://dh18opmr8mjet" path="res://assets/models/plank.blend" id="2_cvuqp"] + +[node name="Interactable" type="RigidBody3D"] +script = ExtResource("1_vnawe") +ModelScene = ExtResource("2_cvuqp") +GridSize = Vector3i(16, 1, 3) +metadata/_edit_group_ = true diff --git a/player/PickupController.cs b/player/PickupController.cs index 84e5c6e..cc2631d 100644 --- a/player/PickupController.cs +++ b/player/PickupController.cs @@ -73,7 +73,7 @@ public partial class PickupController : Node3D _targetedGrid = null; } } else { - var interactable = RayToMouseCursor(PhysicsLayer.Pickup)?.Collider; + var interactable = RayToMouseCursor(PhysicsLayer.Interact)?.Collider; // Remove the outline from the previously looked-at item. if (TargetedItem != null) SetMeshLayerOutline(TargetedItem.Model, OutlineMode.Disable); @@ -122,22 +122,16 @@ public partial class PickupController : Node3D (Grid Grid, Transform3D Target, bool IsFree)? GetValidPlacement(Item itemToPlace) { - // This ray will be blocked by static and dynamic objects. - const PhysicsLayer Mask = PhysicsLayer.Static | PhysicsLayer.Dynamic | PhysicsLayer.Item; - // FIXME: Remove .Place and .Pickup physics layers? - // TODO: We need a separate physics layers for: - // - The physical item collider used for physics calculations (simplified). - // - The placement collider which should match the item's appearance. - // - The general space / size an item takes up, as a cuboid. - // TODO: Probably just overhaul the physics layers altogether. - // It would be better to have a "collides with player" and "player collides with it" layer, etc. - var excludeSet = new HashSet { HeldItem }; var heldItemGrid = HeldItem.GetNodeOrNull(nameof(Grid)); heldItemGrid?.AddItemsRecursively(excludeSet); // Cast a ray and make sure it hit something. - if (RayToMouseCursor(Mask, excludeSet) is not RayResult ray) return null; + // This ray will be blocked by static and dynamic objects. + const PhysicsLayer MASK = PhysicsLayer.Static + | PhysicsLayer.Dynamic + | PhysicsLayer.Placable; + if (RayToMouseCursor(MASK, excludeSet) is not RayResult ray) return null; // Find a grid to place against, which will be either the grid belonging // to the item the ray intersected, or the grid said item is placed upon. diff --git a/player/player.tscn b/player/player.tscn index 5757b7b..639d433 100644 --- a/player/player.tscn +++ b/player/player.tscn @@ -103,7 +103,7 @@ node_connections = [&"is_holding", 0, &"walk_state", &"is_holding", 1, &"hold", [node name="Player" type="CharacterBody3D"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.75, 0) -collision_layer = 4 +collision_layer = 8 collision_mask = 3 script = ExtResource("1_a0mas") @@ -112,15 +112,15 @@ shape = SubResource("CapsuleShape3D_h1mfd") [node name="WorkshopTracker" type="Area3D" parent="."] collision_layer = 0 -collision_mask = 1024 +collision_mask = 256 monitorable = false [node name="CollisionShape3D" type="CollisionShape3D" parent="WorkshopTracker"] shape = SubResource("BoxShape3D_gtuvx") [node name="PushbackArea" type="Area3D" parent="."] -collision_layer = 4 -collision_mask = 4 +collision_layer = 8 +collision_mask = 8 script = ExtResource("2_almik") [node name="CollisionShape3D" type="CollisionShape3D" parent="PushbackArea"] diff --git a/project.godot b/project.godot index fcab4a2..2fb2b56 100644 --- a/project.godot +++ b/project.godot @@ -85,11 +85,11 @@ interact_place={ 3d_render/layer_2="Outline" 3d_physics/layer_1="Static" 3d_physics/layer_2="Dynamic" -3d_physics/layer_3="Player" -3d_physics/layer_4="Item" -3d_physics/layer_9="Pickup" -3d_physics/layer_10="Place" -3d_physics/layer_11="Areas" +3d_physics/layer_3="Small" +3d_physics/layer_4="Body" +3d_physics/layer_9="Zone" +3d_physics/layer_10="Interact" +3d_physics/layer_11="Placable" [rendering] diff --git a/scenes/workshop.tscn b/scenes/workshop.tscn index 13c9f80..9423d32 100644 --- a/scenes/workshop.tscn +++ b/scenes/workshop.tscn @@ -81,7 +81,6 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0.5, -4) [node name="Table" type="StaticBody3D" parent="Objects"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -4) -collision_layer = 2 collision_mask = 0 [node name="Grid" parent="Objects/Table" instance=ExtResource("6_okibm")] @@ -151,7 +150,6 @@ skeleton = NodePath("../MeshInstance3D") [node name="Table2" type="StaticBody3D" parent="Objects"] transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 1.3, -4.6) -collision_layer = 2 collision_mask = 0 [node name="Grid" parent="Objects/Table2" instance=ExtResource("6_okibm")] diff --git a/scripts/globals/PhysicsLayer.cs b/scripts/globals/PhysicsLayer.cs index 9bf583e..f980a9e 100644 --- a/scripts/globals/PhysicsLayer.cs +++ b/scripts/globals/PhysicsLayer.cs @@ -1,20 +1,19 @@ [Flags] public enum PhysicsLayer : uint { - /// Objects that are part of the scene, and never move. - Static = 1 << 0, - /// Objects players collide with, but may move. - Dynamic = 1 << 1, - /// Any players, both local and remote. - Player = 1 << 2, - /// Small objects players don't collide with, but collide with players. - Item = 1 << 3, + /// Physical objects that are part of the scene and won't move. + Static = 0b0000_0000_0000_0001, + /// Physical objects that may move. Player collide with these. + Dynamic = 0b0000_0000_0000_0010, + /// Small physical objects that may move. Players don't collide with these. + Small = 0b0000_0000_0000_0100, + /// Physical bodies such as those of players. + Body = 0b0000_0000_0000_1000, - /// Objects that may be picked up. - Pickup = 1 << 8, - /// Objects that other objects may be placed against. - Place = 1 << 9, - - /// Various areas, such as workshops. - Areas = 1 << 10, + /// Areas in the game that can be entered or left by players. + Zone = 0b0000_0001_0000_0000, + /// Objects that may be ray-traced to interact with them. + Interact = 0b0000_0010_0000_0000, + /// Objects that others may be placed again, grid-aligned bounding box. + Placable = 0b0000_0100_0000_0000, }