extends Node3D @export var camera : Camera3D @export var pickup_distance := 2.0 @onready var world := find_parent("World") as Node3D var current_item : Item var is_current_item_held := false var placement_preview : MeshInstance3D # TODO: Support holding multiple items. # TODO: Allow rotation of the item while held. func _ready() -> void: pass func _input(event: InputEvent) -> void: if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED: return ensure_current_item_valid() if !current_item: return if event.is_action_pressed("interact_pickup"): if !is_current_item_held: is_current_item_held = true # Create a clone of the item's mesh and use it as a placement preview. placement_preview = current_item.get_node("MeshInstance3D").duplicate(0) as MeshInstance3D placement_preview.name = "PlacementPreview" placement_preview.layers = RenderLayer.OUTLINE placement_preview.top_level = true add_child(placement_preview) # Parent item to the pickup controller. var prev_rot := current_item.global_rotation current_item.get_parent().remove_child(current_item) add_child(current_item) current_item.mesh.layers &= ~RenderLayer.OUTLINE current_item.position = Vector3.ZERO current_item.global_rotation = prev_rot current_item.freeze = true get_viewport().set_input_as_handled() elif event.is_action_pressed("interact_place"): if is_current_item_held: is_current_item_held = false # Parent item back to the world. var prev_transform := current_item.global_transform current_item.freeze = false # FIXME: Item appears to just float after unfreezing. remove_child(current_item) world.add_child(current_item) # If placement is valid, move item to the preview's location. # Otherwise just restore the transform of where it was held. if placement_preview.visible: current_item.global_transform = placement_preview.global_transform else: current_item.global_transform = prev_transform # Throw item forward and up a bit. var cam_basis := camera.global_transform.basis var direction := -cam_basis.z + cam_basis.y current_item.apply_impulse(direction * 2) placement_preview.queue_free() placement_preview = null get_viewport().set_input_as_handled() func _process(_delta: float) -> void: pass func _physics_process(_delta: float) -> void: ensure_current_item_valid() if is_current_item_held: # Cast a ray but exlude the current item from being hit. var ray_result := ray_to_mouse_cursor([ current_item ]) if ray_result: var pos := ray_result.position as Vector3 var normal := ray_result.normal as Vector3 # Snap rotation to nearest axis. var global_rot = current_item.global_rotation global_rot.x = snappedf(global_rot.x, TAU / 4) global_rot.y = snappedf(global_rot.y, TAU / 4) global_rot.z = snappedf(global_rot.z, TAU / 4) placement_preview.global_rotation = global_rot # Snap the position to the grid. var half_size := current_item.size * Item.GRID_SIZE / 2 pos += half_size * (normal * placement_preview.global_transform.basis) pos = pos.snapped(Item.GRID_SIZE * Vector3.ONE) placement_preview.global_position = pos placement_preview.visible = true else: placement_preview.visible = false else: # Remove the outline from the previously looked-at item. if current_item: current_item.mesh.layers &= ~RenderLayer.OUTLINE var ray_result := ray_to_mouse_cursor() # If the ray hit anything and the object hit is an item, set it as current. if ray_result: current_item = ray_result.collider as Item if current_item: current_item.mesh.layers |= RenderLayer.OUTLINE func ensure_current_item_valid() -> void: if !current_item: return if !is_instance_valid(current_item): current_item = null is_current_item_held = false placement_preview.queue_free() func ray_to_mouse_cursor(exclude: Array[CollisionObject3D] = []) -> Dictionary: const COLLIDE_WITH := PhysicsLayer.WORLD | PhysicsLayer.ITEM var map_to_rid := func(obj: CollisionObject3D) -> RID: return obj.get_rid() var mouse := get_viewport().get_mouse_position() var from := camera.project_ray_origin(mouse) var to := from + camera.project_ray_normal(mouse) * pickup_distance var query := PhysicsRayQueryParameters3D.create(from, to, COLLIDE_WITH, exclude.map(map_to_rid)) return get_world_3d().direct_space_state.intersect_ray(query)