A game prototype built with Godot 4 exploring in-world inventory management mechanics.
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.
 

121 lines
3.9 KiB

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.
remove_child(current_item)
world.add_child(current_item)
# Set item's transform to where the placement preview is.
# TODO: If placement preview is not valid, don't allow placing the item.
current_item.global_transform = placement_preview.global_transform
current_item.freeze = false
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
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)