extends CharacterBody3D # --- Скорость и инерция (паркур-ощущение) --- const WALK_SPEED = 6.0 const SPRINT_SPEED = 13.0 const GROUND_ACCEL = 55.0 # Разгон на земле const GROUND_FRICTION = 45.0 # Торможение на земле без ввода const OVERSPEED_DECEL = 14.0 # Как быстро гасится "лишняя" скорость (после слайда/прыжка) при беге const AIR_ACCEL = 18.0 # Управление в воздухе const MAX_SPEED = 22.0 # Потолок скорости # --- Прыжок --- const JUMP_VELOCITY = 6.0 const FALL_GRAVITY_MULT = 1.4 # Быстрее падаем — прыжок «злее» const JUMP_CUT_GRAVITY_MULT = 2.5 # Отпустил пробел — прыжок ниже const COYOTE_TIME = 0.12 # Можно прыгнуть сразу после схода с края const JUMP_BUFFER_TIME = 0.12 # Нажал пробел чуть раньше приземления — прыжок сработает const SLIDE_JUMP_BOOST = 1.15 # Бонус скорости при прыжке из слайда # --- Подкат --- const SLIDE_START_SPEED = 15.0 # Минимальная стартовая скорость слайда const SLIDE_START_MULT = 1.25 # Множитель от скорости бега const SLIDE_FRICTION = 6.0 # Чем меньше — тем дальше слайд const SLIDE_END_SPEED = 6.5 # При какой скорости слайд заканчивается const SLIDE_STEER = 1.8 # Насколько можно рулить во время слайда const SLIDE_COOLDOWN = 0.3 const SLIDE_CAMERA_DROP = 0.6 const SLIDE_HEIGHT_FACTOR = 0.5 const CROUCH_SPEED = 2.5 # Ползём, если над головой низкий потолок const MIN_SLIDE_SPEED = 7.0 # Минимальная скорость для начала слайда # --- Камера --- const ZOOM_FOV = 35.0 const ZOOM_SPEED = 10.0 const SPRINT_FOV_BONUS = 7.0 # Расширение FOV на скорости const SLIDE_FOV_BONUS = 13.0 const STRAFE_ROLL = 3.0 # Наклон камеры при стрейфе (градусы) # --- Виньетка (затемнение по краям) --- const ZOOM_VIGNETTE = 0.85 const SLIDE_VIGNETTE = 0.45 const SPRINT_VIGNETTE = 0.2 const VIGNETTE_SPEED = 8.0 const VIGNETTE_SHADER = """ shader_type canvas_item; uniform float intensity : hint_range(0.0, 1.0) = 0.0; void fragment() { float d = distance(UV, vec2(0.5)); float v = smoothstep(0.25, 0.75, d); COLOR = vec4(0.0, 0.0, 0.0, v * intensity); } """ # Настройки чувствительности мыши @export var MOUSE_SENSITIVITY: float = 0.003 # --- Настройки тряски камеры (Head Bobbing) --- const BOB_FREQ = 2.4 const BOB_AMP = 0.06 var bob_time: float = 0.0 # Состояние подката var is_sliding: bool = false var slide_cooldown_timer: float = 0.0 var slide_dir: Vector3 = Vector3.ZERO var slide_speed: float = 0.0 # Прыжок var coyote_timer: float = 0.0 var jump_buffer_timer: float = 0.0 var space_was_down: bool = false # Камера var head_base_pos: Vector3 = Vector3.ZERO var head_offset_y: float = 0.0 var base_fov: float = 75.0 var is_zooming: bool = false # Виньетка var vignette_material: ShaderMaterial = null var vignette_value: float = 0.0 # Коллайдер (опционально, ищется по имени CollisionShape3D) var collision_shape: CollisionShape3D = null var capsule: CapsuleShape3D = null var default_capsule_height: float = 0.0 var default_collision_y: float = 0.0 @onready var head: Camera3D = $Camera3D func _ready() -> void: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED head_base_pos = head.position base_fov = head.fov collision_shape = get_node_or_null("CollisionShape3D") if collision_shape and collision_shape.shape is CapsuleShape3D: capsule = collision_shape.shape default_capsule_height = capsule.height default_collision_y = collision_shape.position.y _setup_vignette() func _setup_vignette() -> void: var layer := CanvasLayer.new() layer.layer = 10 add_child(layer) var rect := ColorRect.new() rect.mouse_filter = Control.MOUSE_FILTER_IGNORE layer.add_child(rect) rect.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) var shader := Shader.new() shader.code = VIGNETTE_SHADER vignette_material = ShaderMaterial.new() vignette_material.shader = shader rect.material = vignette_material func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("ui_cancel"): Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: var zoom_factor := head.fov / base_fov rotate_y(-event.relative.x * MOUSE_SENSITIVITY * zoom_factor) head.rotate_x(-event.relative.y * MOUSE_SENSITIVITY * zoom_factor) head.rotation.x = clamp(head.rotation.x, deg_to_rad(-80), deg_to_rad(80)) # Подкат: бег (Shift) + C if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_C: _try_start_slide() func _physics_process(delta: float) -> void: slide_cooldown_timer = max(slide_cooldown_timer - delta, 0.0) var space_down := Input.is_key_pressed(KEY_SPACE) # --- Таймеры прыжка (coyote time + jump buffer) --- if is_on_floor(): coyote_timer = COYOTE_TIME else: coyote_timer = max(coyote_timer - delta, 0.0) if space_down and not space_was_down: jump_buffer_timer = JUMP_BUFFER_TIME else: jump_buffer_timer = max(jump_buffer_timer - delta, 0.0) space_was_down = space_down # 1. Гравитация (падение быстрее, короткий прыжок при отпускании пробела) if not is_on_floor(): var g := get_gravity() if velocity.y < 0.0: g *= FALL_GRAVITY_MULT elif not space_down: g *= JUMP_CUT_GRAVITY_MULT velocity += g * delta # 2. Прыжок if jump_buffer_timer > 0.0 and coyote_timer > 0.0: if not is_sliding or _can_stand(): velocity.y = JUMP_VELOCITY jump_buffer_timer = 0.0 coyote_timer = 0.0 if is_sliding: _end_slide() # Прыжок из слайда сохраняет и усиливает скорость velocity.x *= SLIDE_JUMP_BOOST velocity.z *= SLIDE_JUMP_BOOST # 3. Скорость (Спринт) var current_speed := WALK_SPEED if Input.is_key_pressed(KEY_SHIFT): current_speed = SPRINT_SPEED # 4. Управление WASD var input_dir := Vector2.ZERO if Input.is_key_pressed(KEY_D): input_dir.x += 1 if Input.is_key_pressed(KEY_A): input_dir.x -= 1 if Input.is_key_pressed(KEY_S): input_dir.y += 1 if Input.is_key_pressed(KEY_W): input_dir.y -= 1 input_dir = input_dir.normalized() # 5. Движение с инерцией var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() var horizontal := Vector3(velocity.x, 0, velocity.z) var has_input := direction != Vector3.ZERO if is_sliding: horizontal = _process_slide(delta, direction, horizontal) elif is_on_floor(): var target := direction * current_speed var rate := GROUND_ACCEL if not has_input: rate = GROUND_FRICTION elif horizontal.length() > current_speed: rate = OVERSPEED_DECEL # плавно гасим лишнюю скорость, не рубим резко horizontal = horizontal.move_toward(target, rate * delta) else: # В воздухе импульс сохраняется, можно только подруливать if has_input: var speed_cap: float = max(horizontal.length(), current_speed) horizontal += direction * AIR_ACCEL * delta if horizontal.length() > speed_cap: horizontal = horizontal.normalized() * speed_cap if horizontal.length() > MAX_SPEED: horizontal = horizontal.normalized() * MAX_SPEED velocity.x = horizontal.x velocity.z = horizontal.z # 6. Камера _update_camera(delta, input_dir) move_and_slide() # --- ПОДКАТ --- func _try_start_slide() -> void: if is_sliding or slide_cooldown_timer > 0.0: return if not is_on_floor(): return if not Input.is_key_pressed(KEY_SHIFT): return var horizontal := Vector3(velocity.x, 0, velocity.z) if horizontal.length() < MIN_SLIDE_SPEED: return is_sliding = true slide_dir = horizontal.normalized() slide_speed = clamp(max(horizontal.length() * SLIDE_START_MULT, SLIDE_START_SPEED), 0.0, MAX_SPEED) _set_collider_crouched(true) func _process_slide(delta: float, direction: Vector3, horizontal: Vector3) -> Vector3: # Сорвались с края во время слайда — выходим, если есть место if not is_on_floor() and _can_stand(): _end_slide() return horizontal if slide_speed > SLIDE_END_SPEED: # Лёгкое рулёжка во время слайда if direction != Vector3.ZERO: var new_dir := slide_dir.lerp(direction, SLIDE_STEER * delta) if new_dir.length() > 0.01: slide_dir = new_dir.normalized() slide_speed = move_toward(slide_speed, 0.0, SLIDE_FRICTION * delta) return slide_dir * slide_speed # Слайд выдохся if _can_stand(): _end_slide() return slide_dir * slide_speed # Под низким потолком — ползём, пока не выберемся if direction != Vector3.ZERO: return direction * CROUCH_SPEED return horizontal.move_toward(Vector3.ZERO, CROUCH_SPEED) func _end_slide() -> void: is_sliding = false slide_cooldown_timer = SLIDE_COOLDOWN _set_collider_crouched(false) func _set_collider_crouched(crouched: bool) -> void: if capsule == null: return if crouched: var new_height := default_capsule_height * SLIDE_HEIGHT_FACTOR capsule.height = new_height collision_shape.position.y = default_collision_y - (default_capsule_height - new_height) * 0.5 else: capsule.height = default_capsule_height collision_shape.position.y = default_collision_y func _can_stand() -> bool: if capsule == null: return true var extra := default_capsule_height * (1.0 - SLIDE_HEIGHT_FACTOR) return not test_move(global_transform, Vector3.UP * extra) # --- КАМЕРА И ЭФФЕКТЫ --- func _update_camera(delta: float, input_dir: Vector2) -> void: var h_speed := Vector2(velocity.x, velocity.z).length() var sprint_factor: float = clamp((h_speed - WALK_SPEED) / (SPRINT_SPEED - WALK_SPEED), 0.0, 1.0) # Покачивание только при обычном беге/ходьбе if is_sliding or not is_on_floor(): bob_time = 0.0 else: bob_time += delta * h_speed var bob := Vector3.ZERO if (is_sliding or not is_on_floor()) else _head_bob(bob_time) # Опускание камеры при слайде var target_offset := -SLIDE_CAMERA_DROP if is_sliding else 0.0 head_offset_y = lerp(head_offset_y, target_offset, 12.0 * delta) head.position = head_base_pos + bob + Vector3(0, head_offset_y, 0) # Наклон камеры при стрейфе var target_roll := -input_dir.x * deg_to_rad(STRAFE_ROLL) * sprint_factor head.rotation.z = lerp(head.rotation.z, target_roll, 8.0 * delta) # Зум на ЛКМ is_zooming = Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) \ and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED # FOV: зум > слайд > скорость бега var target_fov := base_fov + sprint_factor * SPRINT_FOV_BONUS if is_zooming: target_fov = ZOOM_FOV elif is_sliding: target_fov = base_fov + SLIDE_FOV_BONUS head.fov = lerp(head.fov, target_fov, ZOOM_SPEED * delta) # Виньетка: зум > слайд > бег var target_vignette := sprint_factor * SPRINT_VIGNETTE if is_zooming: target_vignette = ZOOM_VIGNETTE elif is_sliding: target_vignette = SLIDE_VIGNETTE vignette_value = lerp(vignette_value, target_vignette, VIGNETTE_SPEED * delta) if vignette_material: vignette_material.set_shader_parameter("intensity", vignette_value) func _head_bob(time: float) -> Vector3: var pos := Vector3.ZERO pos.y = sin(time * BOB_FREQ) * BOB_AMP pos.x = cos(time * BOB_FREQ / 2) * BOB_AMP return pos