Inventory management focused game written in Godot / C#
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

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;
}
}
}