- Overworld: Gras-Terrain mit Noise-Shader, Berg, Dungeon-Tor, Felsen, Bäume - Dungeon: Prozedurale Generierung (Grid, Räume, L-Gänge), Multi-Ebenen mit Persistenz - Portal-System: Blau (zurück/raus), Rot (tiefer), Auswahl-UI ab Ebene 2+ - Gegner: Goblin-Modell + Animationen statt Warrior, Capsule angepasst - NavMesh: Manuell gebautes NavigationMesh im Dungeon mit Wand-Margin und shared Vertices - Pathfinding: Gegner nutzen NavigationAgent3D, laufen um Wände herum - Leash-System: Gegner verlieren Aggro ab 30 Einheiten vom Spawn - Kamera-Kollision: Raycast verhindert Durchsehen durch Wände, ignoriert Gegner - Respawn-Timer auf 60s, Death-Timer auf 10s erhöht - Dokumentation aktualisiert (Dungeon, NavMesh, Goblin, Kamera) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
101 lines
4.6 KiB
GDScript
101 lines
4.6 KiB
GDScript
# CameraPivot.gd
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Third-Person Kamerasystem – Kind-Node des Spielers
|
||
#
|
||
# Kamera-Modi:
|
||
# • Kein Ziel, LMB gehalten → Kamera orbitet um den Spieler (world_yaw ändert sich,
|
||
# Spielerrotation bleibt)
|
||
# • Kein Ziel, RMB gehalten → Spieler + Kamera drehen sich gemeinsam
|
||
# • Ziel markiert → Soft Lock-On: Spieler dreht sich smooth zum Ziel,
|
||
# Kamera bleibt direkt dahinter
|
||
# • Mausrad → Zoom (min_zoom … max_zoom)
|
||
#
|
||
# world_yaw: absolute Weltausrichtung der Kamera in Grad (Y-Achse).
|
||
# Unabhängig von player.rotation.y → verhindert Feedback-Loop bei Souls-Rotation.
|
||
# camera_pivot.rotation.y wird in _process() immer als (world_yaw - player.rotation.y)
|
||
# gesetzt, sodass die Kamera in World-Space stabil bleibt.
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
extends Node3D
|
||
|
||
@export var sensitivity = 0.3
|
||
@export var min_pitch = -40.0
|
||
@export var max_pitch = 20.0
|
||
@export var min_zoom = 5.0
|
||
@export var max_zoom = 20.0
|
||
@export var zoom_speed = 1.0
|
||
@export var lock_on_speed = 5.0
|
||
|
||
var pitch: float = 0.0
|
||
var world_yaw: float = 0.0 # Absolute Weltausrichtung der Kamera (unabhängig von Spielerrotation)
|
||
var desired_zoom: float = 10.0 # Gewünschter Zoom-Abstand
|
||
|
||
@onready var camera = $Camera3D
|
||
|
||
func _ready():
|
||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||
world_yaw = get_parent().rotation.y
|
||
desired_zoom = camera.position.z
|
||
|
||
func _input(event):
|
||
var player = get_parent()
|
||
var has_target = player.target != null and is_instance_valid(player.target)
|
||
|
||
if event is InputEventMouseMotion:
|
||
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
|
||
# LMB: nur Kamera dreht sich, Spieler bleibt (auch mit Target → umschauen)
|
||
world_yaw -= deg_to_rad(event.relative.x * sensitivity)
|
||
pitch -= event.relative.y * sensitivity
|
||
pitch = clamp(pitch, min_pitch, max_pitch)
|
||
rotation_degrees.x = pitch
|
||
elif Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
|
||
# RMB: Spieler + Kamera drehen sich gemeinsam (auch mit Target → weglaufen)
|
||
var delta_yaw = deg_to_rad(-event.relative.x * sensitivity)
|
||
world_yaw += delta_yaw
|
||
player.rotation.y += delta_yaw
|
||
pitch -= event.relative.y * sensitivity
|
||
pitch = clamp(pitch, min_pitch, max_pitch)
|
||
rotation_degrees.x = pitch
|
||
|
||
if event is InputEventMouseButton:
|
||
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||
desired_zoom = clamp(desired_zoom + zoom_speed, min_zoom, max_zoom)
|
||
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||
desired_zoom = clamp(desired_zoom - zoom_speed, min_zoom, max_zoom)
|
||
|
||
func _process(delta):
|
||
var player = get_parent()
|
||
|
||
var rmb_held = Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)
|
||
var lmb_held = Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
|
||
if player.target != null and is_instance_valid(player.target) and not player.is_rolling and not rmb_held and not lmb_held:
|
||
# Soft Lock-On: Spieler dreht sich zum Ziel, Kamera folgt direkt dahinter
|
||
# (RMB gehalten → Lock-On pausiert, Spieler kann sich wegdrehen)
|
||
var to_target = player.target.global_position - player.global_position
|
||
to_target.y = 0
|
||
if to_target.length() > 0.1:
|
||
var target_angle = atan2(-to_target.x, -to_target.z)
|
||
player.rotation.y = lerp_angle(player.rotation.y, target_angle, delta * lock_on_speed)
|
||
world_yaw = player.rotation.y
|
||
|
||
# Lokale Rotation so setzen dass Kamera immer auf world_yaw zeigt
|
||
rotation.y = world_yaw - player.rotation.y
|
||
|
||
# Kamera-Kollision: Raycast vom Pivot zur gewünschten Kameraposition
|
||
var space_state = get_world_3d().direct_space_state
|
||
var pivot_pos = global_position
|
||
# Kamera sitzt auf lokaler Z-Achse des Pivots
|
||
var desired_cam_local = Vector3(0, 0, desired_zoom)
|
||
var desired_cam_pos = global_transform * desired_cam_local
|
||
|
||
var query = PhysicsRayQueryParameters3D.create(pivot_pos, desired_cam_pos)
|
||
query.exclude = [player.get_rid()]
|
||
query.collide_with_bodies = true
|
||
query.collide_with_areas = false
|
||
query.collision_mask = 1
|
||
var result = space_state.intersect_ray(query)
|
||
|
||
if result and not result.collider is CharacterBody3D:
|
||
var hit_dist = pivot_pos.distance_to(result.position) - 0.5
|
||
camera.position.z = clamp(hit_dist, 1.0, desired_zoom)
|
||
else:
|
||
camera.position.z = lerp(camera.position.z, desired_zoom, delta * 5.0)
|