~2 мин чтения

Практика — контроллер от первого лица

CharacterBody3D, move_and_slide, RayCast3D — играбельный FPS-каркас.

Иллюстрация: Практика — контроллер от первого лица

Собираем играбельного FPS-персонажа: ходит, смотрит мышью, прыгает, стреляет лучом. Используем CharacterBody3D (аналог Unity CharacterController, только с более фичастым move_and_slide).

Иерархия сцены

Player (CharacterBody3D)            ← скрипт player.gd
├── CollisionShape3D                ← shape: CapsuleShape3D (height=2, radius=0.4)
├── MeshInstance3D (optional)       ← визуальный mesh (часто скрыт в FPS — не видим себя)
└── Head (Node3D)                   ← на высоте глаз, например y=1.7
    └── Camera3D                    ← current = true, fov = 75
        └── AimRay (RayCast3D)      ← target_position = (0, 0, -100)

Разделение Head ↔ Player важно: yaw (горизонтальный поворот) применяется к Player’у (двигает всё тело и направление движения), pitch (вертикаль) — только к Head (чтобы коллайдер не наклонялся).

Input Map

В Project Settings → Input Map создайте:

  • move_forward (W, ↑)
  • move_back (S, ↓)
  • move_left (A, ←)
  • move_right (D, →)
  • jump (Space)
  • fire (Mouse Left)
  • ui_cancel (Escape) — обычно уже есть по умолчанию

player.gd — главный скрипт

extends CharacterBody3D

@export_group("Movement")
@export var speed: float = 5.5
@export var jump_velocity: float = 5.0

@export_group("Look")
@export var sensitivity: float = 0.0025
@export var max_pitch: float = 1.45

@export_group("Combat")
@export var fire_damage: int = 10
@export var impact_scene: PackedScene  # эффект попадания (опционально)

@onready var head: Node3D = $Head
@onready var camera: Camera3D = $Head/Camera3D
@onready var aim_ray: RayCast3D = $Head/Camera3D/AimRay

func _ready() -> void:
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("ui_cancel"):
        Input.mouse_mode = (
            Input.MOUSE_MODE_VISIBLE
            if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
            else Input.MOUSE_MODE_CAPTURED
        )

    if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
        rotate_y(-event.relative.x * sensitivity)
        head.rotate_x(-event.relative.y * sensitivity)
        head.rotation.x = clamp(head.rotation.x, -max_pitch, max_pitch)

    if event.is_action_pressed("fire") and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
        fire()

func _physics_process(delta: float) -> void:
    apply_gravity(delta)
    handle_jump()
    handle_move()
    move_and_slide()

func apply_gravity(delta: float) -> void:
    if not is_on_floor():
        velocity += get_gravity() * delta

func handle_jump() -> void:
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = jump_velocity

func handle_move() -> void:
    var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
    var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()

    if direction:
        velocity.x = direction.x * speed
        velocity.z = direction.z * speed
    else:
        velocity.x = move_toward(velocity.x, 0, speed)
        velocity.z = move_toward(velocity.z, 0, speed)

func fire() -> void:
    aim_ray.force_raycast_update()  # форсируем обновление в кадре
    if not aim_ray.is_colliding():
        return

    var hit = aim_ray.get_collider()
    var hit_point = aim_ray.get_collision_point()
    var hit_normal = aim_ray.get_collision_normal()

    # Нанести урон, если есть метод
    if hit.has_method("take_damage"):
        hit.take_damage(fire_damage)

    # Эффект попадания (опционально)
    if impact_scene:
        var impact = impact_scene.instantiate()
        impact.global_position = hit_point
        impact.look_at(hit_point + hit_normal, Vector3.UP)
        get_tree().current_scene.add_child(impact)
get_gravity() — гравитация из настроек проекта

get_gravity() возвращает Vector3(0, -9.8, 0) по умолчанию (настраивается в Project Settings → Physics → 3D). Это удобно: одно место регулирует гравитацию всей игры, плюс при изменении значение применяется ко всем телам.

health.gd — получатель урона

Простой компонент здоровья на любом узле:

class_name Health extends Node

signal damaged(amount: int)
signal died

@export var max_hp: int = 100
var hp: int

func _ready() -> void:
    hp = max_hp

func take_damage(amount: int) -> void:
    if hp <= 0:
        return
    hp = max(0, hp - amount)
    damaged.emit(amount)
    if hp == 0:
        died.emit()

Как использовать: добавьте Health как дочерний узел к врагу, а в take_damage-обработчик врага делегируйте на него:

# enemy.gd на корне врага
extends StaticBody3D

@onready var health: Health = $Health

func _ready() -> void:
    health.died.connect(_on_died)

func take_damage(amount: int) -> void:
    health.take_damage(amount)

func _on_died() -> void:
    queue_free()

Тогда aim_ray.get_collider().has_method("take_damage") будет работать — у Enemy-узла есть метод.

Тестовая сцена

Соберите минимальный уровень:

  1. GroundStaticBody3D + CollisionShape3D (BoxShape3D 50×0.1×50) + MeshInstance3D (PlaneMesh) для визуала.
  2. Walls — несколько StaticBody3D-кубов по периметру.
  3. Targets — 5–10 StaticBody3D с компонентом enemy.gd и Health ребёнком.
  4. DirectionalLight3D — основной свет, light_energy = 1.0, тени включены.
  5. WorldEnvironment — с ambient light и sky.
  6. Player.tscn — инстанциируйте на сцене.

Запускаете (F5) — должен ходить, прыгать, мышью смотреть, ЛКМ стрелять.

Что улучшить

  • Crouch — присесть: уменьшить высоту коллайдера и опустить камеру.
  • Sprint — кнопка Shift + множитель скорости.
  • Headbob — лёгкое покачивание камеры при ходьбе (head.position.y = sin(time * walk_freq) * walk_amp).
  • Footstep audio — AudioStreamPlayer3D на узле Player, играть при движении на полу.
  • Recoil — лёгкий импульс камеры вверх при стрельбе через анимацию или код.
  • Reload, Magazine, Ammo HUD — следующая итерация с UI.
  • Coyote time / jump buffer — как в Unity TPS-практикуме, чтобы прыжок ощущался “прощающе”.
  • AnimatorAnimationTree со state machine для анимаций оружия.
Это меньше кода, чем Unity-вариант

Сравните с FPS-controller’ом из Unity-главы: похожая функциональность, но в Godot короче за счёт move_and_slide, встроенного RayCast3D и Input Map. Это типично для Godot — каркас поднимается быстро.

В следующей главе — Navigation (NavigationServer3D, NavigationAgent3D).