Практика — контроллер от первого лица
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() возвращает 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-узла есть метод.
Тестовая сцена
Соберите минимальный уровень:
- Ground —
StaticBody3D+CollisionShape3D(BoxShape3D 50×0.1×50) +MeshInstance3D(PlaneMesh) для визуала. - Walls — несколько StaticBody3D-кубов по периметру.
- Targets — 5–10 StaticBody3D с компонентом
enemy.gdиHealthребёнком. - DirectionalLight3D — основной свет,
light_energy = 1.0, тени включены. - WorldEnvironment — с ambient light и sky.
- 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-практикуме, чтобы прыжок ощущался “прощающе”.
- Animator —
AnimationTreeсо state machine для анимаций оружия.
Сравните с FPS-controller’ом из Unity-главы: похожая функциональность, но в Godot короче за
счёт move_and_slide, встроенного RayCast3D и Input Map. Это типично для Godot — каркас
поднимается быстро.
В следующей главе — Navigation (NavigationServer3D, NavigationAgent3D).