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.
		
		
		
		
		
			
		
			
				
					
					
						
							177 lines
						
					
					
						
							6.1 KiB
						
					
					
				
			
		
		
	
	
							177 lines
						
					
					
						
							6.1 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 }; | |
| 
 | |
| 	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; | |
|  | |
| 	[Export] public float PickupDistance { get; set; } = 2.0f; | |
| 
 | |
| 	Player _player; | |
| 	Node3D _world; | |
| 	ShaderMaterial _outlineShaderMaterial; | |
| 	public override void _Ready() | |
| 	{ | |
| 		_player = GetParent<Player>(); | |
| 		_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) | |
| 	{ | |
| 		if (!_player.IsLocal) return; | |
| 		EnsureCurrentItemValid(); | |
| 		if (CurrentItem == null) return; | |
| 
 | |
| 		if (@event.IsActionPressed("interact_pickup")) { | |
| 			if (!HasItemsHeld) { | |
| 				// Create clone of the item's model, use it as placement preview. | |
| 				_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; | |
| 				CurrentItem.Freeze = true; | |
| 				SetMeshLayerOutline(CurrentItem.Model, OutlineMode.Disable); | |
| 				CurrentItem.GetParent().RemoveChild(CurrentItem); | |
| 				AddChild(CurrentItem); | |
| 				CurrentItem.Position = Vector3.Zero; | |
| 				CurrentItem.GlobalRotation = prevRot; | |
| 
 | |
| 				GetViewport().SetInputAsHandled(); | |
| 			} | |
| 		} else if (@event.IsActionPressed("interact_place")) { | |
| 			if (HasItemsHeld) { | |
| 				// Parent item back to the world. | |
| 				var prevTransform = CurrentItem.GlobalTransform; | |
| 				RemoveChild(CurrentItem); | |
| 
 | |
| 				if (_preview.Visible && _grid != null) { | |
| 					_grid.AddChild(CurrentItem); | |
| 					CurrentItem.GlobalTransform = _preview.GlobalTransform; | |
| 				} else { | |
| 					_world.AddChild(CurrentItem); | |
| 					CurrentItem.GlobalTransform = prevTransform; | |
| 					CurrentItem.Freeze = false; | |
| 
 | |
| 					// Throw item forward and up a bit. | |
| 					var basis     = _player.Camera.Camera.GlobalBasis; | |
| 					var direction = -basis.Z + basis.Y; | |
| 					CurrentItem.ApplyImpulse(direction * 2); | |
| 				} | |
| 
 | |
| 				// Reset the color of the outline shader material. | |
| 				_outlineShaderMaterial.SetShaderParameter("line_color", OutlinePickup); | |
| 
 | |
| 				RemoveChild(_preview); | |
| 				_preview.QueueFree(); | |
| 				_preview = null; | |
| 				_grid = null; | |
| 
 | |
| 				GetViewport().SetInputAsHandled(); | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	public override void _PhysicsProcess(double delta) | |
| 	{ | |
| 		if (!_player.IsLocal) return; | |
| 		EnsureCurrentItemValid(); | |
| 
 | |
| 		static bool CanPlaceAgainst(Grid grid, Vector3 normal) | |
| 		{ | |
| 			normal = grid.GlobalBasis.Inverse() * normal; | |
| 			return normal.IsEqualApprox(Vector3.Up); | |
| 		} | |
| 
 | |
| 		if (HasItemsHeld) { | |
| 			// This ray will be blocked by static and dynamic objects. | |
| 			const PhysicsLayer Mask = PhysicsLayer.Place | PhysicsLayer.Static | PhysicsLayer.Dynamic; | |
| 
 | |
| 			if ((RayToMouseCursor(Mask) is RayResult ray) && (ray.Collider is Grid grid) | |
| 					// Make sure this is placed against the top of the grid. | |
| 					&& CanPlaceAgainst(grid, ray.Normal) | |
| 					// Ensure item is not being added to itself or nested items. | |
| 					&& !((grid.GetParent() == CurrentItem) || grid.ContainsItem(CurrentItem))) { | |
| 				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 { | |
| 				_preview.Visible = false; | |
| 				_grid = null; | |
| 			} | |
| 		} else { | |
| 			var interactable = RayToMouseCursor(PhysicsLayer.Pickup)?.Collider; | |
| 
 | |
| 			// Remove the outline from the previously looked-at item. | |
| 			if (CurrentItem != null) SetMeshLayerOutline(CurrentItem.Model, OutlineMode.Disable); | |
| 			// If the ray hits anything and the object hit is an item, set it as current. | |
| 			CurrentItem = interactable as Item; | |
| 			// Add the outline to the currently looked-at item. | |
| 			if (CurrentItem != null) SetMeshLayerOutline(CurrentItem.Model, OutlineMode.Enable); | |
| 		} | |
| 	} | |
| 
 | |
| 	void EnsureCurrentItemValid() | |
| 	{ | |
| 		if (CurrentItem == null) return; | |
| 		if (!IsInstanceValid(CurrentItem)) { | |
| 			CurrentItem = null; | |
| 			if (_preview != null) { | |
| 				RemoveChild(_preview); | |
| 				_preview.QueueFree(); | |
| 				_preview = null; | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	record class RayResult(CollisionObject3D Collider, Vector3 Position, Vector3 Normal); | |
| 	RayResult RayToMouseCursor(PhysicsLayer collisionMask) | |
| 	{ | |
| 		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; | |
| 		// Exclude the `CurrentItem` from collision checking if it's being held. | |
| 		query.Exclude = HasItemsHeld ? [ CurrentItem.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; | |
| 			} | |
| 	} | |
| }
 | |
| 
 |