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