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.
166 lines
5.5 KiB
166 lines
5.5 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 }; |
|
|
|
Node3D _preview; // Placement preview of the item - a duplicate of its model. |
|
Grid _grid; // Grid currently being hovered over. |
|
|
|
public Item CurrentItem { get; private set; } |
|
public bool HasItemsHeld => GetChildCount() > 0; |
|
|
|
[Export] public float PickupDistance { get; set; } = 2.0f; |
|
|
|
Player _player; |
|
Node3D _world; |
|
ShaderMaterial _outlineShaderMaterial; |
|
public override void _Ready() |
|
{ |
|
_player = GetParent<Player>(); |
|
_world = GetNode<Node3D>("/root/Game/Workshop"); // TODO: Find a better way to get the world. |
|
_outlineShaderMaterial = Load<ShaderMaterial>("res://assets/shaders/outline_material.tres"); |
|
} |
|
|
|
public override void _UnhandledInput(InputEvent @event) |
|
{ |
|
if (!_player.IsLocal) return; |
|
EnsureCurrentItemValid(); |
|
if (CurrentItem == null) return; |
|
|
|
if (@event.IsActionPressed("interact_pickup")) { |
|
if (!HasItemsHeld) { |
|
// Create clone of the item's model, use it as placement preview. |
|
_preview = (Node3D)CurrentItem.Model.Duplicate(0); |
|
_preview.Name = "PlacementPreview"; |
|
_preview.TopLevel = true; |
|
_preview.Visible = false; |
|
SetMeshLayerOutline(_preview, OutlineMode.Exclusive); |
|
AddChild(_preview); |
|
|
|
// Parent item to the `PickupController`. |
|
var prevRot = CurrentItem.GlobalRotation; |
|
CurrentItem.Freeze = true; |
|
SetMeshLayerOutline(CurrentItem.Model, OutlineMode.Disable); |
|
CurrentItem.GetParent().RemoveChild(CurrentItem); |
|
AddChild(CurrentItem); |
|
CurrentItem.Position = Vector3.Zero; |
|
CurrentItem.GlobalRotation = prevRot; |
|
|
|
GetViewport().SetInputAsHandled(); |
|
} |
|
} else if (@event.IsActionPressed("interact_place")) { |
|
if (HasItemsHeld) { |
|
// Parent item back to the world. |
|
var prevTransform = CurrentItem.GlobalTransform; |
|
RemoveChild(CurrentItem); |
|
|
|
if (_preview.Visible && _grid != null) { |
|
_grid.AddChild(CurrentItem); |
|
CurrentItem.GlobalTransform = _preview.GlobalTransform; |
|
} else { |
|
_world.AddChild(CurrentItem); |
|
CurrentItem.GlobalTransform = prevTransform; |
|
CurrentItem.Freeze = false; |
|
|
|
// Throw item forward and up a bit. |
|
var basis = _player.Camera.Camera.GlobalBasis; |
|
var direction = -basis.Z + basis.Y; |
|
CurrentItem.ApplyImpulse(direction * 2); |
|
} |
|
|
|
// Reset the color of the outline shader material. |
|
_outlineShaderMaterial.SetShaderParameter("line_color", OutlinePickup); |
|
|
|
RemoveChild(_preview); |
|
_preview.QueueFree(); |
|
_preview = null; |
|
_grid = null; |
|
|
|
GetViewport().SetInputAsHandled(); |
|
} |
|
} |
|
} |
|
|
|
public override void _PhysicsProcess(double delta) |
|
{ |
|
if (!_player.IsLocal) return; |
|
EnsureCurrentItemValid(); |
|
|
|
if (HasItemsHeld) { |
|
if ((RayToMouseCursor() is RayResult ray) && (ray.Collider is Grid grid)) { |
|
var transform = CurrentItem.GlobalTransform with { Origin = ray.Position }; |
|
transform = grid.Snap(transform, ray.Normal, CurrentItem); |
|
_preview.GlobalTransform = transform; |
|
|
|
var canPlace = grid.CanPlaceAt(CurrentItem, transform); |
|
var outlineColor = canPlace ? OutlineYesPlace : OutlineNoPlace; |
|
_outlineShaderMaterial.SetShaderParameter("line_color", outlineColor); |
|
|
|
_preview.Visible = true; |
|
_grid = canPlace ? grid : null; |
|
} else { |
|
_preview.Visible = false; |
|
_grid = null; |
|
} |
|
} else { |
|
var interactable = RayToMouseCursor()?.Collider; |
|
|
|
// Remove the outline from the previously looked-at item. |
|
if (CurrentItem != null) SetMeshLayerOutline(CurrentItem.Model, OutlineMode.Disable); |
|
// 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) SetMeshLayerOutline(CurrentItem.Model, OutlineMode.Enable); |
|
} |
|
} |
|
|
|
void EnsureCurrentItemValid() |
|
{ |
|
if (CurrentItem == null) return; |
|
if (!IsInstanceValid(CurrentItem)) { |
|
CurrentItem = null; |
|
if (_preview != null) { |
|
RemoveChild(_preview); |
|
_preview.QueueFree(); |
|
_preview = null; |
|
} |
|
} |
|
} |
|
|
|
RayResult RayToMouseCursor() |
|
{ |
|
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)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<CollisionObject3D>(); |
|
public Vector3 Position { get; } = (Vector3)dict["position"]; |
|
public Vector3 Normal { get; } = (Vector3)dict["normal"]; |
|
} |
|
|
|
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; |
|
} |
|
} |
|
}
|
|
|