You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
7.0 KiB
195 lines
7.0 KiB
public partial class PickupController : Node3D |
|
{ |
|
static readonly Color OutlinePickup = Colors.White with { A = 0.75f }; |
|
static readonly Color OutlineYesPlace = Colors.Green with { A = 0.75f }; |
|
static readonly Color OutlineNoPlace = Colors.Red with { A = 0.75f }; |
|
|
|
|
|
[Export] public float PickupDistance { get; set; } = 2.0f; |
|
|
|
|
|
/// <summary> Item that is currently being targeted by the mouse cursor. </summary> |
|
public Item TargetedItem { get; private set; } |
|
|
|
/// <summary> Item currently being held. </summary> |
|
public Item HeldItem => GetChildren().OfType<Item>().FirstOrDefault(); |
|
// TODO: Support holding multiple items. |
|
|
|
/// <summary> Whether any items are being held. </summary> |
|
public bool HasItemsHeld => HeldItem != null; |
|
|
|
|
|
Node3D _preview; // Placement preview of the item - a duplicate of its model. |
|
Grid _targetedGrid; // Grid currently being hovered over. |
|
|
|
Player _player; |
|
ShaderMaterial _outlineMaterial; |
|
public override void _Ready() |
|
{ |
|
_player = GetParent<Player>(); |
|
_outlineMaterial = Load<ShaderMaterial>("res://assets/shaders/outline_material.tres"); |
|
ChildEnteredTree += OnChildEnteredTree; |
|
ChildExitingTree += OnChildExitingTree; |
|
} |
|
|
|
public override void _UnhandledInput(InputEvent @event) |
|
{ |
|
if (!_player.IsLocal) return; |
|
|
|
if (@event.IsActionPressed("interact_pickup") |
|
&& (TargetedItem != null) && !HasItemsHeld) |
|
{ |
|
TargetedItem.Manager.TryPickup(TargetedItem); |
|
GetViewport().SetInputAsHandled(); |
|
} |
|
|
|
if (@event.IsActionPressed("interact_place") |
|
&& HasItemsHeld) |
|
{ |
|
if (_preview.Visible && (_targetedGrid != null)) { |
|
var inverse = _targetedGrid.GlobalTransform.AffineInverse(); |
|
HeldItem.Manager.TryPlace(HeldItem, |
|
_targetedGrid, inverse * _preview.GlobalTransform); |
|
} else |
|
HeldItem.Manager.TryThrow(HeldItem); |
|
GetViewport().SetInputAsHandled(); |
|
} |
|
} |
|
|
|
public override void _PhysicsProcess(double delta) |
|
{ |
|
if (!_player.IsLocal) return; |
|
|
|
if (HasItemsHeld) { |
|
if (GetValidPlacement(HeldItem) is var (grid, transform, isFree)) { |
|
var outlineColor = isFree ? OutlineYesPlace : OutlineNoPlace; |
|
_outlineMaterial.SetShaderParameter("line_color", outlineColor); |
|
|
|
_preview.GlobalTransform = grid.GlobalTransform * transform; |
|
_preview.Visible = true; |
|
_targetedGrid = isFree ? grid : null; |
|
} else { |
|
_preview.Visible = false; |
|
_targetedGrid = null; |
|
} |
|
} else { |
|
var interactable = RayToMouseCursor(PhysicsLayer.Pickup)?.Collider; |
|
|
|
// Remove the outline from the previously looked-at item. |
|
if (TargetedItem != null) SetMeshLayerOutline(TargetedItem.Model, OutlineMode.Disable); |
|
// If the ray hits anything and the object hit is an item, set it as current. |
|
TargetedItem = interactable as Item; |
|
// Add the outline to the currently looked-at item. |
|
if (TargetedItem != null) SetMeshLayerOutline(TargetedItem.Model, OutlineMode.Enable); |
|
} |
|
} |
|
|
|
|
|
void OnChildEnteredTree(Node node) |
|
{ |
|
if (node is not Item item) return; |
|
|
|
// TODO: Will be able to hold multiple items in the future. Map item => preview. |
|
AddChild(_preview = CreatePlacementPreview(item)); |
|
|
|
// TODO: Move this elsewhere. Or just redo outline handling. |
|
SetMeshLayerOutline(item.Model, OutlineMode.Disable); |
|
} |
|
|
|
void OnChildExitingTree(Node node) |
|
{ |
|
if (node is not Item) return; |
|
|
|
// Reset the color of the outline shader material. |
|
_outlineMaterial.SetShaderParameter("line_color", OutlinePickup); |
|
|
|
_preview.QueueFree(); |
|
_preview = null; |
|
_targetedGrid = null; |
|
} |
|
|
|
|
|
/// <summary> Creates a clone of the item's model, to be rendered as a placement preview. </summary> |
|
static Node3D CreatePlacementPreview(Item item) |
|
{ |
|
var preview = (Node3D)item.Model.Duplicate(0); |
|
preview.Name = "PlacementPreview"; |
|
preview.TopLevel = true; // Only use global space transformations. |
|
preview.Visible = false; // Starts out not visible. |
|
SetMeshLayerOutline(preview, OutlineMode.Exclusive); |
|
return preview; |
|
} |
|
|
|
(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<CollisionObject3D> { HeldItem }; |
|
var heldItemGrid = HeldItem.GetNodeOrNull<Grid>(nameof(Grid)); |
|
heldItemGrid?.AddItemsRecursively(excludeSet); |
|
|
|
// Cast a ray and make sure it hit something. |
|
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. |
|
var grid = ray.Collider.GetNodeOrNull<Grid>(nameof(Grid)) |
|
?? ray.Collider.GetParentOrNull<Grid>(); |
|
if (grid == null) return null; // No suitable grid found. |
|
|
|
var inverseTransform = grid.GlobalTransform.AffineInverse(); |
|
var inverseBasis = grid.GlobalBasis.Inverse(); |
|
|
|
var pos = inverseTransform * ray.Position; |
|
var normal = inverseBasis * ray.Normal; |
|
|
|
if (!grid.CanPlaceAgainst(itemToPlace, pos, normal)) return null; |
|
|
|
var transform = new Transform3D(inverseBasis * itemToPlace.GlobalBasis, pos); |
|
transform = grid.Snap(transform, normal, itemToPlace); |
|
var isFree = grid.CanPlaceAt(itemToPlace, transform); |
|
|
|
return (grid, transform, isFree); |
|
} |
|
|
|
record class RayResult(CollisionObject3D Collider, Vector3 Position, Vector3 Normal); |
|
RayResult RayToMouseCursor(PhysicsLayer collisionMask, IEnumerable<CollisionObject3D> excluded = null) |
|
{ |
|
var camera = _player.Camera.Camera; |
|
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)collisionMask; |
|
query.CollideWithAreas = true; |
|
query.Exclude = new((excluded ?? []).Select(obj => obj.GetRid())); |
|
|
|
var result = GetWorld3D().DirectSpaceState.IntersectRay(query); |
|
return (result.Count > 0) ? new( |
|
result["collider"].As<CollisionObject3D>(), |
|
(Vector3)result["position"], |
|
(Vector3)result["normal"] |
|
) : null; |
|
} |
|
|
|
enum OutlineMode { Disable, Enable, Exclusive } |
|
static void SetMeshLayerOutline(Node3D parent, OutlineMode mode) |
|
{ |
|
var children = parent.FindChildren("*", "MeshInstance3D", owned: false); |
|
foreach (var mesh in children.Cast<MeshInstance3D>()) |
|
switch (mode) { |
|
case OutlineMode.Disable: mesh.Layers &= ~(uint)RenderLayer.Outline; break; |
|
case OutlineMode.Enable: mesh.Layers |= (uint)RenderLayer.Outline; break; |
|
case OutlineMode.Exclusive: mesh.Layers = (uint)RenderLayer.Outline; break; |
|
} |
|
} |
|
}
|
|
|