diff --git a/assets/shaders/outline_material.tres b/assets/shaders/outline_material.tres new file mode 100644 index 0000000..0d6c912 --- /dev/null +++ b/assets/shaders/outline_material.tres @@ -0,0 +1,8 @@ +[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://c0q35rri3vb07"] + +[ext_resource type="Shader" path="res://assets/shaders/outline.gdshader" id="1_yi6vm"] + +[resource] +shader = ExtResource("1_yi6vm") +shader_parameter/line_color = Color(1, 1, 1, 0.75) +shader_parameter/line_thickness = 3.0 diff --git a/game.tscn b/game.tscn index 2e7a0d6..0e00a80 100644 --- a/game.tscn +++ b/game.tscn @@ -1,20 +1,15 @@ -[gd_scene load_steps=11 format=3 uid="uid://puuk72ficqhu"] +[gd_scene load_steps=10 format=3 uid="uid://puuk72ficqhu"] [ext_resource type="Script" path="res://scripts/MultiplayerManager.cs" id="1_7shyh"] [ext_resource type="Script" path="res://Game.cs" id="1_uywdd"] [ext_resource type="PackedScene" uid="uid://dmd7w2r8s0x6y" path="res://player/player.tscn" id="2_iv2f7"] [ext_resource type="PackedScene" uid="uid://bwfuet1irfi17" path="res://scenes/workshop.tscn" id="3_4u5ql"] -[ext_resource type="Shader" path="res://assets/shaders/outline.gdshader" id="4_gacvj"] +[ext_resource type="Material" uid="uid://c0q35rri3vb07" path="res://assets/shaders/outline_material.tres" id="5_a3fxj"] [ext_resource type="Script" path="res://scripts/OutlineCamera.cs" id="5_qpc14"] [ext_resource type="PackedScene" uid="uid://c5ooi36ibspfo" path="res://ui/menu.tscn" id="6_ol0j5"] [ext_resource type="Texture2D" uid="uid://lxxfestfg2dt" path="res://assets/crosshair.png" id="7_0l5tv"] [ext_resource type="Script" path="res://scripts/Crosshair.cs" id="8_mfhgr"] -[sub_resource type="ShaderMaterial" id="ShaderMaterial_ke1l3"] -shader = ExtResource("4_gacvj") -shader_parameter/line_color = Color(1, 1, 1, 0.75) -shader_parameter/line_thickness = 2.0 - [node name="Game" type="Node" node_paths=PackedStringArray("LocalPlayer")] script = ExtResource("1_uywdd") LocalPlayer = NodePath("Players/Local") @@ -33,7 +28,7 @@ PlayerScene = ExtResource("2_iv2f7") IsLocal = true [node name="OutlineViewportContainer" type="SubViewportContainer" parent="."] -material = SubResource("ShaderMaterial_ke1l3") +material = ExtResource("5_a3fxj") anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/objects/Grid.cs b/objects/Grid.cs index a2c04ca..55e4184 100644 --- a/objects/Grid.cs +++ b/objects/Grid.cs @@ -10,13 +10,22 @@ public partial class Grid : Area3D static readonly Color ThickLineColor = new(0.25f, 0.25f, 0.25f); static readonly Color ThinLineColor = new(0.35f, 0.35f, 0.35f); + // TODO: Make GridSize be three-dimensional? Vector2I _gridSize = new(16, 16); + Vector3 _halfGridActualSize = new(8.0f, 0.0f, 8.0f); [Export] public Vector2I GridSize { get => _gridSize; - set { _gridSize = value; Update(); } + set { + _gridSize = value; + // Helper value for converting grid pos to local pos and back. + _halfGridActualSize = new Vector3(GridSize.X, 0, GridSize.Y) * (StepSize / 2.0f); + Update(); + } } - public override void _Ready() => Update(); + + public override void _Ready() + => Update(); void Update() { @@ -24,6 +33,69 @@ public partial class Grid : Area3D UpdateImmediateMesh(); } + + public Vector3I LocalToGrid(Vector3 pos) + => (Vector3I)((pos + _halfGridActualSize) / StepSize); + public Vector3 GridToLocal(Vector3I pos) + => (pos + Vector3.One * 0.5f) * StepSize - _halfGridActualSize; + + public Vector3I GlobalToGrid(Vector3 pos) + => LocalToGrid(ToLocal(pos)); + public Vector3 GridToGlobal(Vector3I pos) + => ToGlobal(GridToLocal(pos)); + + public static Aabb LocalItemTransformToLocalAabb(Transform3D transform, Item item) + { + var bounds = (transform.Basis * ((Vector3)item.Size * StepSize)).Abs(); + return new(transform.Origin - bounds / 2, bounds); + } + + + /// + /// Returns whether the item can be placed at the specified global transform. + /// The transform needs to be grid-aligned, such as by calling . + /// + public bool CanPlaceAt(Item item, Transform3D transform) + { + transform = GlobalTransform.AffineInverse() * transform; + var region = LocalItemTransformToLocalAabb(transform, item).Grow(-0.01f); + foreach (var other in GetChildren().OfType()) + if (region.Intersects(LocalItemTransformToLocalAabb(other.Transform, other))) + return false; + return true; + } + + /// + /// Snaps a global transform to line up with the grid. + /// The transform will be "pushed out" into the normal vector's + /// direction according to the current rotation and item's size. + /// + public Transform3D Snap(Transform3D transform, Vector3 normal, Item item) + { + var inverseTransform = GlobalTransform.AffineInverse(); + var inverseBasis = GlobalBasis.Inverse(); + var halfSize = (Vector3)item.Size * StepSize / 2; + + // Get grid-local values of the global transform and normal. + var pos = inverseTransform * transform.Origin; + var basis = inverseBasis * transform.Basis; + normal = inverseBasis * normal; + + // Snap rotation to nearest axis. + basis = Basis.FromEuler(basis.GetEuler().Snapped(Tau / 4)); + + // Offset / "push out" by half of the item's size. + var offsetAxis = (int)(normal * basis).Abs().MaxAxisIndex(); + pos += halfSize[offsetAxis] * normal; + + // Snap the position to the grid. + var halfOff = basis * halfSize.PosMod(1.0f); + pos = halfOff + (pos - halfOff).Snapped(StepSize); + + return new(GlobalBasis * basis, GlobalTransform * pos); + } + + static StandardMaterial3D _material; static StandardMaterial3D GetOrCreateMaterial() { diff --git a/player/PickupController.cs b/player/PickupController.cs index 854571d..c687db4 100644 --- a/player/PickupController.cs +++ b/player/PickupController.cs @@ -1,18 +1,25 @@ 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; - Node3D _placementPreview; [Export] public float PickupDistance { get; set; } = 2.0f; Player _player; Node3D _world; + ShaderMaterial _outlineShaderMaterial; public override void _Ready() { _player = GetParent(); - // TODO: Find a better way to find the world. - _world = GetNode("/root/Game/Workshop"); + _world = GetNode("/root/Game/Workshop"); // TODO: Find a better way to get the world. + _outlineShaderMaterial = Load("res://assets/shaders/outline_material.tres"); } public override void _UnhandledInput(InputEvent @event) @@ -24,12 +31,12 @@ public partial class PickupController : Node3D if (@event.IsActionPressed("interact_pickup")) { if (!HasItemsHeld) { // Create clone of the item's model, use it as placement preview. - _placementPreview = (Node3D)CurrentItem.Model.Duplicate(0); - _placementPreview.Name = "PlacementPreview"; - _placementPreview.TopLevel = true; - _placementPreview.Visible = false; - SetMeshLayerOutline(_placementPreview, OutlineMode.Exclusive); - AddChild(_placementPreview); + _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; @@ -47,11 +54,12 @@ public partial class PickupController : Node3D // Parent item back to the world. var prevTransform = CurrentItem.GlobalTransform; RemoveChild(CurrentItem); - _world.AddChild(CurrentItem); - if (_placementPreview.Visible) { - CurrentItem.GlobalTransform = _placementPreview.GlobalTransform; + if (_preview.Visible && _grid != null) { + _grid.AddChild(CurrentItem); + CurrentItem.GlobalTransform = _preview.GlobalTransform; } else { + _world.AddChild(CurrentItem); CurrentItem.GlobalTransform = prevTransform; CurrentItem.Freeze = false; @@ -61,9 +69,13 @@ public partial class PickupController : Node3D CurrentItem.ApplyImpulse(direction * 2); } - RemoveChild(_placementPreview); - _placementPreview.QueueFree(); - _placementPreview = null; + // Reset the color of the outline shader material. + _outlineShaderMaterial.SetShaderParameter("line_color", OutlinePickup); + + RemoveChild(_preview); + _preview.QueueFree(); + _preview = null; + _grid = null; GetViewport().SetInputAsHandled(); } @@ -77,25 +89,19 @@ public partial class PickupController : Node3D if (HasItemsHeld) { if ((RayToMouseCursor() is RayResult ray) && (ray.Collider is Grid grid)) { - // Snao rotation to nearest axis. - var localBasis = grid.GlobalBasis.Inverse() * CurrentItem.GlobalBasis; - localBasis = Basis.FromEuler(localBasis.GetEuler().Snapped(Tau / 4)); - _placementPreview.GlobalBasis = grid.GlobalBasis * localBasis; - - // Snap the position to the grid. - var halfSize = (Vector3)CurrentItem.Size * Grid.StepSize / 2; - var localPos = ray.Position * grid.GlobalTransform; // Get grid-local ray position. - var localNormal = ray.Normal * grid.GlobalBasis; // Get grid-local ray normal. (Pointing away from surface hit.) - var off = localBasis * halfSize.PosMod(1.0f); // Calculate an offset for grid snapping. - off[(int)localNormal.Abs().MaxAxisIndex()] = 0; // Do not include offset in the normal direction. - localPos = off + (localPos - off).Snapped(Grid.StepSize); // Snap `localPos` to nearest grid value. - var axis = (localNormal * localBasis).Abs().MaxAxisIndex(); // Find object-local axis that the normal is pointing towards. - localPos += halfSize[(int)axis] * localNormal; // Offset (push out) object by half its size. - _placementPreview.GlobalPosition = grid.GlobalTransform * localPos; - - _placementPreview.Visible = true; + 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 { - _placementPreview.Visible = false; + _preview.Visible = false; + _grid = null; } } else { var interactable = RayToMouseCursor()?.Collider; @@ -114,10 +120,10 @@ public partial class PickupController : Node3D if (CurrentItem == null) return; if (!IsInstanceValid(CurrentItem)) { CurrentItem = null; - if (_placementPreview != null) { - RemoveChild(_placementPreview); - _placementPreview.QueueFree(); - _placementPreview = null; + if (_preview != null) { + RemoveChild(_preview); + _preview.QueueFree(); + _preview = null; } } }