Ga naar hoofdinhoud

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")

Volgende