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 12 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://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

@ -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);
}
/// <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 GetOrCreateMaterial()
{

@ -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<Player>();
// TODO: Find a better way to find the world.
_world = GetNode<Node3D>("/root/Game/Workshop");
_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)
@ -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;
}
}
}

Loading…
Cancel
Save