Improve Grid handling, prevent overlapping items

- Move snapping logic to Grid class
- Add Grid.CanPlaceAt to check for item overlap
  (no test for collision with physics-enabled items)
- Change outline color depending on if can place
main
copygirl 11 months ago
parent 17da96969a
commit 9b7cb3d2fd
  1. 8
      assets/shaders/outline_material.tres
  2. 11
      game.tscn
  3. 76
      objects/Grid.cs
  4. 80
      player/PickupController.cs

@ -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

@ -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://scripts/MultiplayerManager.cs" id="1_7shyh"]
[ext_resource type="Script" path="res://Game.cs" id="1_uywdd"] [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://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="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="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="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="Texture2D" uid="uid://lxxfestfg2dt" path="res://assets/crosshair.png" id="7_0l5tv"]
[ext_resource type="Script" path="res://scripts/Crosshair.cs" id="8_mfhgr"] [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")] [node name="Game" type="Node" node_paths=PackedStringArray("LocalPlayer")]
script = ExtResource("1_uywdd") script = ExtResource("1_uywdd")
LocalPlayer = NodePath("Players/Local") LocalPlayer = NodePath("Players/Local")
@ -33,7 +28,7 @@ PlayerScene = ExtResource("2_iv2f7")
IsLocal = true IsLocal = true
[node name="OutlineViewportContainer" type="SubViewportContainer" parent="."] [node name="OutlineViewportContainer" type="SubViewportContainer" parent="."]
material = SubResource("ShaderMaterial_ke1l3") material = ExtResource("5_a3fxj")
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0

@ -10,13 +10,22 @@ public partial class Grid : Area3D
static readonly Color ThickLineColor = new(0.25f, 0.25f, 0.25f); static readonly Color ThickLineColor = new(0.25f, 0.25f, 0.25f);
static readonly Color ThinLineColor = new(0.35f, 0.35f, 0.35f); static readonly Color ThinLineColor = new(0.35f, 0.35f, 0.35f);
// TODO: Make GridSize be three-dimensional?
Vector2I _gridSize = new(16, 16); Vector2I _gridSize = new(16, 16);
Vector3 _halfGridActualSize = new(8.0f, 0.0f, 8.0f);
[Export] public Vector2I GridSize { [Export] public Vector2I GridSize {
get => _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() void Update()
{ {
@ -24,6 +33,69 @@ public partial class Grid : Area3D
UpdateImmediateMesh(); 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);
}
/// <summary>
/// Returns whether the item can be placed at the specified global transform.
/// The transform needs to be grid-aligned, such as by calling <see cref="Snap"/>.
/// </summary>
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<Item>())
if (region.Intersects(LocalItemTransformToLocalAabb(other.Transform, other)))
return false;
return true;
}
/// <summary>
/// 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.
/// </summary>
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 _material;
static StandardMaterial3D GetOrCreateMaterial() static StandardMaterial3D GetOrCreateMaterial()
{ {

@ -1,18 +1,25 @@
public partial class PickupController : Node3D 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 Item CurrentItem { get; private set; }
public bool HasItemsHeld => GetChildCount() > 0; public bool HasItemsHeld => GetChildCount() > 0;
Node3D _placementPreview;
[Export] public float PickupDistance { get; set; } = 2.0f; [Export] public float PickupDistance { get; set; } = 2.0f;
Player _player; Player _player;
Node3D _world; Node3D _world;
ShaderMaterial _outlineShaderMaterial;
public override void _Ready() public override void _Ready()
{ {
_player = GetParent<Player>(); _player = GetParent<Player>();
// TODO: Find a better way to find the world. _world = GetNode<Node3D>("/root/Game/Workshop"); // TODO: Find a better way to get the world.
_world = GetNode<Node3D>("/root/Game/Workshop"); _outlineShaderMaterial = Load<ShaderMaterial>("res://assets/shaders/outline_material.tres");
} }
public override void _UnhandledInput(InputEvent @event) public override void _UnhandledInput(InputEvent @event)
@ -24,12 +31,12 @@ public partial class PickupController : Node3D
if (@event.IsActionPressed("interact_pickup")) { if (@event.IsActionPressed("interact_pickup")) {
if (!HasItemsHeld) { if (!HasItemsHeld) {
// Create clone of the item's model, use it as placement preview. // Create clone of the item's model, use it as placement preview.
_placementPreview = (Node3D)CurrentItem.Model.Duplicate(0); _preview = (Node3D)CurrentItem.Model.Duplicate(0);
_placementPreview.Name = "PlacementPreview"; _preview.Name = "PlacementPreview";
_placementPreview.TopLevel = true; _preview.TopLevel = true;
_placementPreview.Visible = false; _preview.Visible = false;
SetMeshLayerOutline(_placementPreview, OutlineMode.Exclusive); SetMeshLayerOutline(_preview, OutlineMode.Exclusive);
AddChild(_placementPreview); AddChild(_preview);
// Parent item to the `PickupController`. // Parent item to the `PickupController`.
var prevRot = CurrentItem.GlobalRotation; var prevRot = CurrentItem.GlobalRotation;
@ -47,11 +54,12 @@ public partial class PickupController : Node3D
// Parent item back to the world. // Parent item back to the world.
var prevTransform = CurrentItem.GlobalTransform; var prevTransform = CurrentItem.GlobalTransform;
RemoveChild(CurrentItem); RemoveChild(CurrentItem);
_world.AddChild(CurrentItem);
if (_placementPreview.Visible) { if (_preview.Visible && _grid != null) {
CurrentItem.GlobalTransform = _placementPreview.GlobalTransform; _grid.AddChild(CurrentItem);
CurrentItem.GlobalTransform = _preview.GlobalTransform;
} else { } else {
_world.AddChild(CurrentItem);
CurrentItem.GlobalTransform = prevTransform; CurrentItem.GlobalTransform = prevTransform;
CurrentItem.Freeze = false; CurrentItem.Freeze = false;
@ -61,9 +69,13 @@ public partial class PickupController : Node3D
CurrentItem.ApplyImpulse(direction * 2); CurrentItem.ApplyImpulse(direction * 2);
} }
RemoveChild(_placementPreview); // Reset the color of the outline shader material.
_placementPreview.QueueFree(); _outlineShaderMaterial.SetShaderParameter("line_color", OutlinePickup);
_placementPreview = null;
RemoveChild(_preview);
_preview.QueueFree();
_preview = null;
_grid = null;
GetViewport().SetInputAsHandled(); GetViewport().SetInputAsHandled();
} }
@ -77,25 +89,19 @@ public partial class PickupController : Node3D
if (HasItemsHeld) { if (HasItemsHeld) {
if ((RayToMouseCursor() is RayResult ray) && (ray.Collider is Grid grid)) { if ((RayToMouseCursor() is RayResult ray) && (ray.Collider is Grid grid)) {
// Snao rotation to nearest axis. var transform = CurrentItem.GlobalTransform with { Origin = ray.Position };
var localBasis = grid.GlobalBasis.Inverse() * CurrentItem.GlobalBasis; transform = grid.Snap(transform, ray.Normal, CurrentItem);
localBasis = Basis.FromEuler(localBasis.GetEuler().Snapped(Tau / 4)); _preview.GlobalTransform = transform;
_placementPreview.GlobalBasis = grid.GlobalBasis * localBasis;
var canPlace = grid.CanPlaceAt(CurrentItem, transform);
// Snap the position to the grid. var outlineColor = canPlace ? OutlineYesPlace : OutlineNoPlace;
var halfSize = (Vector3)CurrentItem.Size * Grid.StepSize / 2; _outlineShaderMaterial.SetShaderParameter("line_color", outlineColor);
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.) _preview.Visible = true;
var off = localBasis * halfSize.PosMod(1.0f); // Calculate an offset for grid snapping. _grid = canPlace ? grid : null;
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;
} else { } else {
_placementPreview.Visible = false; _preview.Visible = false;
_grid = null;
} }
} else { } else {
var interactable = RayToMouseCursor()?.Collider; var interactable = RayToMouseCursor()?.Collider;
@ -114,10 +120,10 @@ public partial class PickupController : Node3D
if (CurrentItem == null) return; if (CurrentItem == null) return;
if (!IsInstanceValid(CurrentItem)) { if (!IsInstanceValid(CurrentItem)) {
CurrentItem = null; CurrentItem = null;
if (_placementPreview != null) { if (_preview != null) {
RemoveChild(_placementPreview); RemoveChild(_preview);
_placementPreview.QueueFree(); _preview.QueueFree();
_placementPreview = null; _preview = null;
} }
} }
} }

Loading…
Cancel
Save