Interactie Systeem
Object interacties, hotspots, en click/tap handling.
Overzicht
┌─────────────────────────────────────────────────────────────┐
│ Interactie Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ Player Tap ──► Hit Detection ──► Interactable? │
│ │ │
│ ┌─────┴─────┐ │
│ ▼ ▼ │
│ Yes No │
│ │ │ │
│ Show Popup Move Player │
│ │ │
│ ┌──────┴──────┐ │
│ ▼ ▼ │
│ Execute Show Dialog │
│ Action │
│ │
└─────────────────────────────────────────────────────────────┘
Interactable Component
Base Class
# interactable.gd
class_name Interactable extends Area2D
signal interacted()
signal hover_started()
signal hover_ended()
@export var interaction_name: String = "Interact"
@export var interaction_icon: Texture2D
@export var requires_proximity: bool = true
@export var interaction_range: float = 100.0
@export var one_time_only: bool = false
var is_hovered: bool = false
var has_interacted: bool = false
func _ready() -> void:
input_event.connect(_on_input_event)
mouse_entered.connect(_on_mouse_entered)
mouse_exited.connect(_on_mouse_exited)
func can_interact() -> bool:
if one_time_only and has_interacted:
return false
if requires_proximity:
var player = get_tree().get_first_node_in_group("player")
if player:
return position.distance_to(player.position) <= interaction_range
return true
func interact() -> void:
if not can_interact():
return
has_interacted = true
interacted.emit()
_on_interact()
# Override in subclass
func _on_interact() -> void:
pass
Input Handling
func _on_input_event(viewport: Node, event: InputEvent, shape_idx: int) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_handle_click()
elif event is InputEventScreenTouch:
if event.pressed:
_handle_click()
func _handle_click() -> void:
if can_interact():
interact()
elif requires_proximity:
# Move player closer first
_request_player_approach()
Interactable Types
NPC Interactable
# npc_interactable.gd
extends Interactable
@export var npc_id: String = ""
@export var dialog_id: String = ""
@export var portrait: Texture2D
func _on_interact() -> void:
# Open dialog
var dialog_manager = get_node("/root/DialogManager")
dialog_manager.start_dialog(dialog_id, {
"npc_id": npc_id,
"portrait": portrait
})
Item Pickup
# pickup_interactable.gd
extends Interactable
@export var item_id: String = ""
@export var quantity: int = 1
@export var pickup_sound: AudioStream
func _ready() -> void:
super._ready()
interaction_name = "Oprapen"
one_time_only = true
func _on_interact() -> void:
# Add to inventory
var inventory = get_node("/root/InventoryManager")
var success = inventory.add_item(item_id, quantity)
if success:
# Play pickup effect
_play_pickup_effect()
# Notify
NotificationManager.toast("Item opgepakt!", "success")
# Remove from scene
queue_free()
func _play_pickup_effect() -> void:
if pickup_sound:
AudioManager.play_sound_at(pickup_sound, global_position)
# Particle effect
var particles = preload("res://scenes/effects/pickup_particles.tscn").instantiate()
particles.global_position = global_position
get_parent().add_child(particles)
Container Interactable
# container_interactable.gd
extends Interactable
@export var container_id: int = -1
@export var container_type: String = "chest"
@export var locked: bool = false
@export var key_item_id: String = ""
func _ready() -> void:
super._ready()
interaction_name = "Openen" if not locked else "Vergrendeld"
func _on_interact() -> void:
if locked:
_try_unlock()
return
# Open container UI
var inventory_panel = get_node("/root/UI/InventoryPanel")
inventory_panel.open_container(container_id, container_type)
func _try_unlock() -> void:
if key_item_id.is_empty():
NotificationManager.toast("Deze kist is vergrendeld", "warning")
return
var inventory = get_node("/root/InventoryManager")
if inventory.has_item(key_item_id):
locked = false
interaction_name = "Openen"
inventory.remove_item(key_item_id, 1)
NotificationManager.toast("Kist ontgrendeld!", "success")
AudioManager.play_sound("unlock")
else:
NotificationManager.toast("Je hebt een sleutel nodig", "warning")
Farm Plot Interactable
# farm_plot_interactable.gd
extends Interactable
@export var plot_index: int = 0
var current_seed: String = ""
var growth_stage: int = 0
var planted_time: int = 0
func _ready() -> void:
super._ready()
_update_interaction_name()
func _update_interaction_name() -> void:
if current_seed.is_empty():
interaction_name = "Planten"
elif _is_ready_to_harvest():
interaction_name = "Oogsten"
elif _needs_water():
interaction_name = "Water geven"
else:
interaction_name = "Bekijken"
func _on_interact() -> void:
if current_seed.is_empty():
_show_seed_selection()
elif _is_ready_to_harvest():
_harvest()
elif _needs_water():
_water()
else:
_show_status()
Hover & Highlight
Visual Feedback
@onready var highlight_shader: ShaderMaterial = preload("res://shaders/outline.tres")
@onready var sprite: Sprite2D = $Sprite2D
func _on_mouse_entered() -> void:
if not can_interact():
return
is_hovered = true
hover_started.emit()
# Apply highlight
sprite.material = highlight_shader
# Show tooltip
_show_tooltip()
func _on_mouse_exited() -> void:
is_hovered = false
hover_ended.emit()
# Remove highlight
sprite.material = null
# Hide tooltip
_hide_tooltip()
Tooltip System
var tooltip_instance: Control = null
func _show_tooltip() -> void:
if tooltip_instance:
return
tooltip_instance = preload("res://scenes/ui/interaction_tooltip.tscn").instantiate()
tooltip_instance.set_text(interaction_name)
tooltip_instance.set_icon(interaction_icon)
# Position above interactable
var screen_pos = get_viewport().get_camera_2d().unproject_position(global_position)
tooltip_instance.position = screen_pos + Vector2(0, -60)
get_tree().root.add_child(tooltip_instance)
func _hide_tooltip() -> void:
if tooltip_instance:
tooltip_instance.queue_free()
tooltip_instance = null
Proximity Detection
Approach System
func _request_player_approach() -> void:
var player = get_tree().get_first_node_in_group("player")
if not player:
return
# Calculate approach position
var approach_pos = _get_approach_position()
# Tell player to move there
player.move_to(approach_pos)
# Wait for arrival
player.arrived.connect(_on_player_arrived, CONNECT_ONE_SHOT)
func _get_approach_position() -> Vector2:
var player = get_tree().get_first_node_in_group("player")
var direction = (player.global_position - global_position).normalized()
return global_position + direction * (interaction_range * 0.8)
func _on_player_arrived() -> void:
# Now interact
interact()
Range Indicator
# Optioneel: toon interactie range
func _draw() -> void:
if Engine.is_editor_hint() and requires_proximity:
draw_arc(Vector2.ZERO, interaction_range, 0, TAU, 32, Color(0, 1, 0, 0.3), 2.0)
Interaction Queue
Multiple Interactables
# interaction_manager.gd (autoload)
extends Node
var nearby_interactables: Array[Interactable] = []
var current_target: Interactable = null
func register_nearby(interactable: Interactable) -> void:
if interactable not in nearby_interactables:
nearby_interactables.append(interactable)
_update_target()
func unregister_nearby(interactable: Interactable) -> void:
nearby_interactables.erase(interactable)
_update_target()
func _update_target() -> void:
if nearby_interactables.is_empty():
current_target = null
return
# Prioritize by distance
var player = get_tree().get_first_node_in_group("player")
if not player:
return
nearby_interactables.sort_custom(func(a, b):
return a.global_position.distance_to(player.global_position) < \
b.global_position.distance_to(player.global_position)
)
current_target = nearby_interactables[0]
func interact_with_current() -> void:
if current_target and current_target.can_interact():
current_target.interact()
Action Context Menu
Radial Menu
# Voor objecten met meerdere acties
func _show_context_menu() -> void:
var menu = preload("res://scenes/ui/radial_menu.tscn").instantiate()
menu.add_option("Bekijken", preload("res://assets/ui/icons/eye.png"), _on_examine)
menu.add_option("Oprapen", preload("res://assets/ui/icons/hand.png"), _on_pickup)
menu.add_option("Gebruiken", preload("res://assets/ui/icons/use.png"), _on_use)
menu.position = get_global_mouse_position()
get_tree().root.add_child(menu)
func _on_examine() -> void:
var item_def = ItemDatabase.get_item(item_id)
NotificationManager.toast(item_def.description, "info")