public partial class PickupController : Node3D { public Item CurrentItem { get; private set; } public bool HasItemsHeld => GetChildCount() > 0; MeshInstance3D _placementPreview; [Export] public Camera3D Camera { get; set; } [Export] public float PickupDistance { get; set; } = 2.0f; Player _player; Node3D _world; public override void _Ready() { _player = GetParent(); // TODO: Find a better way to find the world. _world = GetNode("/root/Game/Workshop"); } public override void _UnhandledInput(InputEvent @event) { if (!_player.IsLocal) return; EnsureCurrentItemValid(); if (CurrentItem == null) return; if (@event.IsActionPressed("interact_pickup")) { if (!HasItemsHeld) { // Parent item to the `PickupController`. var prevRot = CurrentItem.GlobalRotation; CurrentItem.GetParent().RemoveChild(CurrentItem); AddChild(CurrentItem); CurrentItem.Mesh.Layers &= (uint)~RenderLayer.Outline; CurrentItem.Position = Vector3.Zero; CurrentItem.GlobalRotation = prevRot; CurrentItem.CollisionLayer &= (uint)~PhysicsLayer.Static; // CurrentItem.Freeze = true; GetViewport().SetInputAsHandled(); } } else if (@event.IsActionPressed("interact_place")) { if (HasItemsHeld) { // Parent item back to the world. var prevTransform = CurrentItem.GlobalTransform; CurrentItem.CollisionLayer |= (uint)PhysicsLayer.Static; // CurrentItem.Freeze = false; RemoveChild(CurrentItem); _world.AddChild(CurrentItem); CurrentItem.GlobalTransform = prevTransform; GetViewport().SetInputAsHandled(); } } } public override void _PhysicsProcess(double delta) { if (!_player.IsLocal) return; EnsureCurrentItemValid(); if (HasItemsHeld) { if ((RayToMouseCursor() is RayResult ray) && (ray.Collider is Grid)) { // Snao rotation to nearest axis. // FIXME: This needs to snap to the // var globalRot = CurrentItem.GlobalRotation; // globalRot.X = Snapped(globalRot.X, Tau / 4); // globalRot.Y = Snapped(globalRot.Y, Tau / 4); // globalRot.Z = Snapped(globalRot.Z, Tau / 4); // CurrentItem.GlobalRotation = globalRot; // Snap the position to the grid. var halfSize = (Vector3)CurrentItem.Size * Grid.StepSize / 2; var pos = ray.Position + halfSize * (ray.Normal * CurrentItem.GlobalTransform.Basis); pos = pos.Snapped(Grid.StepSize * Vector3.One); // FIXME: This does global snapping only CurrentItem.GlobalPosition = pos; } } else { var interactable = RayToMouseCursor()?.Collider; // Remove the outline from the previously looked-at item. if (CurrentItem != null) CurrentItem.Mesh.Layers &= (uint)~RenderLayer.Outline; // If the ray hits anything and the object hit is an item, set it as current. CurrentItem = interactable as Item; // Add the outline to the currently looked-at item. if (CurrentItem != null) CurrentItem.Mesh.Layers |= (uint)RenderLayer.Outline; } } void EnsureCurrentItemValid() { if (CurrentItem == null) return; if (!IsInstanceValid(CurrentItem)) { CurrentItem = null; _placementPreview?.QueueFree(); } } RayResult RayToMouseCursor() { var mouse = GetViewport().GetMousePosition(); var from = Camera.ProjectRayOrigin(mouse); var to = from + Camera.ProjectRayNormal(mouse) * PickupDistance; var query = PhysicsRayQueryParameters3D.Create(from, to); query.CollisionMask = (uint)PhysicsLayer.Interactable; query.CollideWithAreas = true; // Exclude the `CurrentItem` from collision checking if it's being held. query.Exclude = HasItemsHeld ? [ CurrentItem.GetRid() ] : []; var result = GetWorld3D().DirectSpaceState.IntersectRay(query); return (result.Count > 0) ? new(result) : null; } class RayResult(Dictionary dict) { public CollisionObject3D Collider { get; } = dict["collider"].As(); public Vector3 Position { get; } = (Vector3)dict["position"]; public Vector3 Normal { get; } = (Vector3)dict["normal"]; } }