Контроллер от третьего лица в Godot 4.0.2

Результат

Вы получите рабочий 3D-контроллер от третьего лица:

  • персонаж ходит по W/S/A/D;
  • камера осматривается мышью;
  • Shift включает бег и анимацию running;
  • F запускает анимацию kick;
  • во время kick блокируются движение/локомоция, но обзор мышью остаётся доступным;
  • после завершения kick управление возвращается;
  • есть idle, walking, running, kick;
  • переходы анимаций сглажены Default Blend Time = 0.2 s;
  • чувствительность мыши экспортируется в Inspector и в main.tscn выставляется 0.2;
  • персонаж сталкивается с полом и тестовыми боксами.

Этот туториал намеренно не добавляет clamp по вертикальной оси камеры и не добавляет освобождение мыши. Это ограничение, а не скрытый шаг.


Требования и ресурсы

Нужен Godot 4.0.2 и проект, созданный на рендерере Forward+.

Ассеты: lukky-nl/third_person_controller_assets. Скачайте репозиторий через Code → Download ZIP, распакуйте его и скопируйте каталоги models и textures в созданную внутри проекта папку assets. Не оставляйте лишний уровень third_person_controller_assets-main, иначе пути ниже не совпадут.

В папке проекта должна получиться следующая структура:

res://assets/models/mixamo_base.glb
res://assets/textures/grids/Dark/texture_05.png
res://assets/textures/grids/Orange/texture_09.png
res://assets/textures/grids/Purple/texture_09.png
res://assets/textures/grids/Green/texture_09.png

В модели mixamo_base.glb должны быть анимации с точными именами:

idle
walking
running
kick

Имена узлов, анимаций и Input Map в Godot case-sensitive. Важно писать именно:

player
visuals
mixamo_base
camera_mount
AnimationPlayer
idle
walking
running
kick
forward
backward
left
right
run
kick

Пошаговая сборка

1. Создайте пустой проект

  1. Откройте Project Manager Godot 4.0.2.
  2. Нажмите Create New Project.
  3. Заполните: - Project Name: third_person_tutorial - Project Path: выберите каталог и создайте папку third_person_tutorial - Renderer: Forward+ - Version Control Metadata: Git, если хотите
  4. Нажмите Create & Edit.

Ожидаемо: открыт редактор Godot, вкладка сцены пуста, внизу слева виден Filesystem с res:// и icon.svg.

Пустой проект Godot 4.0.2 с пустой 3D-сценой


2. Импортируйте папку assets

  1. В панели File System выберите res://.
  2. Перетащите из операционной системы папку assets в окно File System на res://.
  3. Дождитесь завершения импорта.

Ожидаемо: в дереве появились:

res://assets/models/mixamo_base.glb
res://assets/textures/grids/Dark/texture_05.png
res://assets/textures/grids/Orange/texture_09.png
res://assets/textures/grids/Purple/texture_09.png
res://assets/textures/grids/Green/texture_09.png

Если импорта завис, нажмите правой кнопкой по res:// и выберите Reload.


3. Создайте корень сцены world

  1. В пустой сцене добавьте новый узел: - нажмите + рядом с Scene Tree; - или правый клик по пустому дереву → Add Child Node.
  2. В поиске введите Node3D.
  3. Выберите Node3D и создайте узел.
  4. Переименуйте узел в world.

Ожидаемое дерево:

world (Node3D)

4. Создайте контейнер карты map

  1. Выберите узел world.
  2. Добавьте дочерний узел: - + → поиск Node3D.
  3. Создайте Node3D.
  4. Переименуйте в map.

Ожидаемое дерево:

world (Node3D)
└─ map (Node3D)

5. Создайте пол floor

Родитель пола — map.

  1. Выберите узел map.
  2. Нажмите +.
  3. В поиске введите box.
  4. Выберите CSGBox3D.
  5. Создайте узел.
  6. Переименуйте его в floor.
  7. В Inspector установите: - Size: 1000, 1, 1000 - Position: 0, -0.5, 0

Если Use Collision сейчас Off, не включайте его пока. Мы включим коллизию позже массово для floor, box, box2, box3.

Ожидаемо: в 3D-вьюпорте большая плоская плита лежит так, что её верхняя грань находится примерно на y = 0.

Пол 1000×1×1000 с Position Y = -0.5


6. Назначьте материал полу

  1. В File System откройте:
res://assets/textures/grids/Dark
  1. Перетащите texture_05.png из File System прямо на поверхность floor в 3D-вьюпорте. Godot автоматически создаст StandardMaterial3D, поместит его в Geometry → Material Override и назначит картинку как albedo-текстуру.
  2. Снова выберите floor и в Inspector откройте созданный ресурс Geometry → Material Override.
  3. В инспекторе материала раскройте UV1 и включите Triplanar.
  4. Проверьте, что: - Albedo → Texture = texture_05.png - UV1 → Triplanar = On - верхнее CSG-поле Material осталось <empty>: материал находится именно в Geometry → Material Override.

Ожидаемо: пол получил тёмную сетку, клетки выглядят примерно как 1×1 м.

Материал пола: StandardMaterial3D, Albedo texture, UV1 Triplanar On


7. Создайте три тестовых бокса

Все три бокса должны быть прямыми детьми map, а не детьми floor.

7.1. Бокс box

  1. Выберите map.
  2. + → поиск boxCSGBox3D → создать.
  3. Переименуйте в box.
  4. В Inspector оставьте Size: 1, 1, 1.
  5. Перетащите res://assets/textures/grids/Orange/texture_09.png на box в 3D-вьюпорте.
  6. Откройте созданный Geometry → Material Override и включите UV1 → Triplanar.

7.2. Бокс box2

  1. Снова выберите map.
  2. Добавьте новый CSGBox3D.
  3. Переименуйте в box2.
  4. Size: 2, 2, 2.
  5. Перетащите res://assets/textures/grids/Purple/texture_09.png на box2 в 3D-вьюпорте.
  6. Откройте созданный Geometry → Material Override и включите UV1 → Triplanar.

7.3. Бокс box3

  1. Выберите map.
  2. Добавьте новый CSGBox3D.
  3. Переименуйте в box3.
  4. Size: 3, 3, 3.
  5. Перетащите res://assets/textures/grids/Green/texture_09.png на box3 в 3D-вьюпорте.
  6. Откройте созданный Geometry → Material Override и включите UV1 → Triplanar.

Позиции и повороты боксов задайте gizmo визуально, чтобы получить похожую композицию. Точные координаты здесь не фиксируются.

Три тестовых бокса: оранжевый 1×1×1, фиолетовый 2×2×2, зелёный 3×3×3


8. Включите коллизию для пола и боксов

  1. В Scene Tree выделите сразу четыре узла: - floor - box - box2 - box3

Используйте Ctrl/Shift для множественного выделения.

  1. В Inspector установите:
Use Collision = On

Ожидаемо: Godot подтверждает множественное выделение, например CSGBox3D (4 selected), и Use Collision становится On для всех выбранных CSG-узлов.


9. Добавьте окружение и солнце

Порядок важен: сначала WorldEnvironment, затем DirectionalLight3D.

  1. В 3D-вьюпорте откройте панель окружения: - трёхточечное меню вьюпорта → Environment - или кнопку Environment в тулбаре 3D-вьюпорта.
  2. Нажмите Add Environment to Scene.
  3. Затем нажмите Add Sun to Scene.

Ожидаемо: под world появились узлы:

WorldEnvironment
DirectionalLight3D

WorldEnvironment и DirectionalLight3D добавлены в сцену


10. Настройте SDFGI и туман

  1. Выберите узел WorldEnvironment.
  2. В Inspector раскройте параметры окружения.
  3. Включите:
SDFGI → Enabled = On
  1. Раскройте Fog и включите:
Fog → Enabled = On
Fog → Light Color = #ffffff
Fog → Density = 0.001

Ожидаемо: в сцене появились мягкие тени/глобальное освещение и лёгкий белый туман.

Fog Enabled, Light Color белый, Density 0.001


11. Создайте папки scenes и scripts

  1. В File System выберите res://.
  2. Правый клик → Create Folder.
  3. Наберите scenes и подтвердите.
  4. Повторите: - правый клик по res://Create Folder - имя scripts.

Ожидаемо:

res://assets
res://scenes
res://scripts
res://icon.svg

12. Сохраните сцену мира как main.tscn

  1. Выберите меню SceneSave Scene As….
  2. Перейдите в res://scenes/.
  3. Сохраните файл как main.tscn.
  4. Нажмите Ctrl+S, если вкладка помечена изменённой.

Ожидаемо: вкладка называется main.tscn, в res://scenes/ появился файл main.tscn.


Сцена игрока

13. Создайте сцену player.tscn

  1. Выберите меню SceneNew Scene.
  2. В пустой сцене нажмите Other Node.
  3. Найдите и создайте CharacterBody3D.
  4. Переименуйте корневой узел в player.
  5. Сохраните сцену: - SceneSave Scene As… - путь res://scenes/ - имя player.tscn.

Ожидаемо: в дереве есть корень player типа CharacterBody3D. Пока может быть предупреждение, что нет collision shape — это нормально, следующий шаг добавит форму.

Корень player — CharacterBody3D


14. Добавьте CollisionShape3D

Родитель формы — player.

  1. Выберите узел player.
  2. Нажмите +.
  3. В поиске введите colli.
  4. Выберите CollisionShape3D.
  5. Создайте узел.
  6. В Inspector: - Shape: New CylinderShape3D - Height: 2 - Radius: 0.3 - Position: 0, 1.1, 0

Ожидаемо: вокруг origin появился проволочный цилиндр, а предупреждение о недостающей collision shape у player исчезло.


15. Создайте узел visuals

  1. Выберите player.
  2. Нажмите +.
  3. Добавьте Node3D.
  4. Переименуйте в visuals.

Ожидаемо:

player
├─ CollisionShape3D
└─ visuals

16. Добавьте модель mixamo_base

  1. В File System откройте:
res://assets/models
  1. Найдите mixamo_base.glb.
  2. Перетащите mixamo_base.glb на узел visuals в Scene Tree. - можно перетащить и в 3D-вьюпорт, но надёжнее перетаскивать на узел visuals в дереве.

Ожидаемо: под visuals появился узел mixamo_base.

Визуальная модель добавлена под узел visuals


17. Разверните модель на 180° вокруг Y

Нужно, чтобы forward персонажа совпадал с ожидаемым направлением Godot.

  1. Выберите mixamo_base.
  2. Либо в Inspector поставьте:
Rotation: 0, 180, 0

Либо поверните модель gizmo вокруг вертикальной оси на 180°.

Ожидаемо: модель развернута лицом в нужном направлении.

Модель развернута на 180 градусов вокруг Y


18. Создайте camera_mount

Родитель камеры — camera_mount, а не player и не visuals.

  1. Выберите player.
  2. Нажмите +.
  3. Добавьте Node3D.
  4. Переименуйте в camera_mount.
  5. В Inspector установите:
Position: 0, 1.4, 0

Ожидаемо: camera_mount находится на уровне груди персонажа.


19. Добавьте Camera3D

  1. Выберите camera_mount.
  2. Нажмите +.
  3. В поиске введите cam.
  4. Выберите Camera3D.
  5. Создайте узел.
  6. Убедитесь, что Current = On.

Точные локальные координаты камеры здесь не фиксируются. Нужно подобрать позицию визуально под over-the-shoulder вид.

  1. Выберите Camera3D.
  2. Включите предпросмотр камеры: - в тулбаре 3D-вьюпорта нажмите Preview - или нажмите Ctrl+P.
  3. Двигайте Camera3D gizmo так, чтобы персонаж был виден сзади и немного сбоку.

Ожидаемо: в Preview камера показывает персонажа в стиле от третьего лица, без «слишком близкого» и «слишком далёкого» вида.

Камера подобрана визуально для вида от третьего лица

  1. Отключите Preview, если хотите вернуть обычный вьюпорт.
  2. Сохраните player.tscn.

Привязка скрипта и первая проверка

20. Прикрепите скрипт к player

  1. Откройте res://scenes/player.tscn.
  2. Выберите корневой узел player.
  3. Нажмите кнопку Attach Script над Scene Tree или используйте правый клик по playerAttach Script….
  4. В диалоге: - Path: res://scripts/player.gd - Language: GDScript - Inherits: CharacterBody3D - Template: On - шаблон CharacterBody3D: Basic Movement.
  5. Нажмите Create.

Ожидаемо: создан файл res://scripts/player.gd, у player появился скрипт, в редакторе кода открыт базовый movement-шаблон.

Шаблон Basic Movement для CharacterBody3D

  1. Сохраните player.tscn.

21. Добавьте player в main.tscn

  1. Откройте res://scenes/main.tscn.
  2. В Scene Tree выберите корневой узел world.
  3. В File System откройте res://scenes/.
  4. Перетащите player.tscn на корневой узел world в Scene Tree. - альтернатива: правый клик по worldInstantiate Child Scene… → выберите player.tscn.
  5. Если нужно, переименуйте инстанс в player.
  6. Сохраните main.tscn.

Ожидаемо: player — прямой ребёнок world, на одном уровне с map, а не ребёнок map.

В main.tscn узлы map и player находятся на одном уровне под world


22. Назначьте main scene

  1. Выберите меню ProjectProject Settings….
  2. Перейдите:
Application → Run → Main Scene
  1. Установите:
res://scenes/main.tscn

Альтернатива: при первом F5 выберите текущую сцену main.tscn.

Ожидаемо: проект понимает, какую сцену запускать кнопкой Run.


23. Первый тест: движение на стрелках

  1. Запустите проект: - нажмите Run Project - или F5.
  2. В игре проверьте: - движение стрелками; - столкновение с полом и боксами.

Ожидаемо: персонаж ходит, не проходит сквозь геометрию, камера следует за ним.

Остановите игру, продолжая редактировать скрипт.


Мышь и чувствительность

24. Захватите мышь

Откройте res://scripts/player.gd.

Добавьте метод _ready() перед _physics_process(delta):

func _ready():
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

Сохраните файл.

Запустите игру.

Ожидаемо: курсор мыши скрыт и удерживается внутри игрового окна.

После захвата мыши курсор в игровом окне не виден

Ограничение: в этом туториале мышь не освобождается. Чтобы вернуть курсор, закройте игровое окно или остановите отладку в Godot.


25. Горизонтальный обзор

В player.gd добавьте переменную и метод _input(event).

Промежуточный фрагмент:

var sens_horizontal = 0.5


func _input(event):
    if event is InputEventMouseMotion:
        rotate_y(deg_to_rad(-event.relative.x * sens_horizontal))

Важно:

  • знак перед event.relative.x — минус;
  • вращается сам player по Y;
  • deg_to_rad() обязателен.

Сохраните и запустите игру.

Ожидаемо: движение мыши влево/вправо поворачивает обзор в естественную сторону и не слишком быстро.

Горизонтальный mouse look через rotate_y


26. Вертикальный обзор камеры

Добавьте ссылку на camera_mount и вертикальную чувствительность.

Промежуточный фрагмент:

@onready var camera_mount = $camera_mount

var sens_vertical = 0.5

Внутри _input(event), после горизонтального поворота, добавьте:

camera_mount.rotate_x(deg_to_rad(-event.relative.y * sens_vertical))

Итог блока _input на этом этапе:

func _input(event):
    if event is InputEventMouseMotion:
        rotate_y(deg_to_rad(-event.relative.x * sens_horizontal))
        camera_mount.rotate_x(deg_to_rad(-event.relative.y * sens_vertical))

Сохраните и запустите игру.

Ожидаемо: мышью можно осматриваться вверх/вниз; наклон делает именно camera_mount.


27. Экспортируйте чувствительность

В player.gd измените объявления:

@export var sens_horizontal = 0.5
@export var sens_vertical = 0.5

Затем:

  1. Откройте res://scenes/main.tscn.
  2. Выберите инстанс player.
  3. В Inspector найдите: - Sens Horizontal - Sens Vertical
  4. Установите оба значения:
Sens Horizontal = 0.2
Sens Vertical = 0.2
  1. Сохраните main.tscn.

Ожидаемо: чувствительность стала настраиваемой из Inspector, а в основной сцене используются значения 0.2.

Экспортированные Sens Horizontal / Sens Vertical в Inspector


Input Map

28. Создайте действия WASD

  1. Выберите меню ProjectProject Settings….
  2. Откройте вкладку Input Map.
  3. Убедитесь, что Show Built-in Actions выключен.
  4. В поле Add New Action создайте действия: - forward - backward - left - right

Для каждого действия:

  1. Нажмите + справа от имени действия.
  2. В режиме Listening for input… нажмите нужную физическую клавишу: - forwardW - backwardS - leftA - rightD
  3. В Additional Options выберите Physical Keycode (Position on US QWERTY Keyboard).
  4. Подтвердите.

У всех четырёх действий должен быть Deadzone = 0.5.

Ожидаемо: в Input Map есть:

forward = W (Physical)
backward = S (Physical)
left = A (Physical)
right = D (Physical)

Input Map: forward, backward, left, right на W/S/A/D Physical


29. Замените ввод на custom actions

В player.gd найдите строку:

var input_dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")

Замените на:

var input_dir = Input.get_vector("left", "right", "forward", "backward")

Важно: порядок аргументов должен совпадать:

left, right, forward, backward

Сохраните и запустите игру.

Ожидаемо: персонаж ходит на W/S/A/D.


Анимации

30. Сделайте mixamo_base локальным

  1. Откройте res://scenes/player.tscn.
  2. В Scene Tree разверните:
player
└─ visuals
   └─ mixamo_base
  1. Правый клик по mixamo_base.
  2. Выберите Make Local.
  3. Сохраните player.tscn.

Ожидаемо: содержимое импортированной сцены стало редактируемым.


31. Включите loop для нужных анимаций

  1. Выберите узел AnimationPlayer.
  2. В нижней панели Animation выберите анимацию idle.
  3. Включите Animation Looping.
  4. Выберите walking.
  5. Включите Animation Looping.
  6. Выберите running.
  7. Включите Animation Looping.
  8. Выберите kick.
  9. Убедитесь, что у kick loop выключен.

Ожидаемо: idle, walking, running зациклены; kick не зациклен.

AnimationPlayer с анимациями и настройкой loop


32. Установите Default Blend Time

  1. Оставьте выбранным узел AnimationPlayer.
  2. В Inspector раскройте Playback Options.
  3. Установите:
Default Blend Time = 0.2 s
  1. Сохраните player.tscn.

Ожидаемо: переходы между анимациями стали плавными, но короткими.

Default Blend Time = 0.2 s


Локомоция: ходьба, бег, kick

33. Добавьте ссылку на AnimationPlayer

В player.gd добавьте:

@onready var animation_player = $visuals/mixamo_base/AnimationPlayer

Путь должен быть точным:

$visuals/mixamo_base/AnimationPlayer

Если путь неправильный, Godot даст ошибку поиска узла.


34. Скорость ходьбы

В player.gd измените:

const SPEED = 5.0

на:

var SPEED = 3.0

Это нужно и для лучшей подгонки под анимацию, и для последующей переменной смены на running_speed.


35. Играть walking и idle

В _physics_process(delta) внутри блока:

if direction:

добавьте проверку и воспроизведение walking:

if animation_player.current_animation != "walking":
    animation_player.play("walking")

В блоке:

else:

добавьте:

if animation_player.current_animation != "idle":
    animation_player.play("idle")

Промежуточный фрагмент:

if direction:
    if animation_player.current_animation != "walking":
        animation_player.play("walking")

    velocity.x = direction.x * SPEED
    velocity.z = direction.z * SPEED
else:
    if animation_player.current_animation != "idle":
        animation_player.play("idle")

    velocity.x = move_toward(velocity.x, 0, SPEED)
    velocity.z = move_toward(velocity.z, 0, SPEED)

Сохраните и запустите игру.

Ожидаемо: в покое играет idle, при движении — walking.

Код переключения idle/walking


36. Ориентация модели по направлению движения

Добавьте ссылку на visuals:

@onready var visuals = $visuals

В _physics_process(delta) внутри if direction: добавьте:

visuals.look_at(position + direction)

В _input(event) внутри if event is InputEventMouseMotion: добавьте компенсирующее вращение модели:

visuals.rotate_y(deg_to_rad(event.relative.x * sens_horizontal))

Важные знаки:

player:      -event.relative.x
visuals:     +event.relative.x
camera_mount: -event.relative.y

Сохраните и запустите игру.

Ожидаемо:

  • при движении модель поворачивается в сторону движения;
  • при свободном обзоре мышью модель визуально остаётся стабильнее;
  • при начале движения снова ориентируется по направлению.

Код visuals.look_at и компенсации поворота


37. Добавьте действие run

  1. Откройте Project SettingsInput Map.
  2. Создайте действие:
run
  1. Для run: - нажмите +; - нажмите физическую клавишу Shift; - выберите Physical Keycode; - подтвердите.

Ожидаемо:

run = Shift (Physical)

38. Добавьте переменные скорости

В начале player.gd добавьте:

var walking_speed = 3.0
var running_speed = 5.0
var running = false

39. Переключайте скорость и анимацию бега

В начале _physics_process(delta) добавьте:

if Input.is_action_pressed("run"):
    SPEED = running_speed
    running = true
else:
    SPEED = walking_speed
    running = false

Важно: в else обязательно должно быть:

running = false

Затем в блоке if direction: замените логику выбора анимации на:

if running:
    if animation_player.current_animation != "running":
        animation_player.play("running")
else:
    if animation_player.current_animation != "walking":
        animation_player.play("walking")

Сохраните и запустите игру.

Ожидаемо:

  • без Shift — ходьба;
  • с Shift — бег;
  • при отпускании Shift — обратно ходьба;
  • переходы сглажены blend time.

Переключение walking/running и скорости


40. Добавьте действие kick

  1. Откройте Project SettingsInput Map.
  2. Создайте действие:
kick
  1. Для kick: - нажмите +; - нажмите физическую клавишу F; - выберите Physical Keycode; - подтвердите.

Ожидаемо:

kick = F (Physical)

Input Map: kick на F Physical


41. Добавьте блокировку во время kick

Добавьте переменную:

var is_locked = false

В начало _physics_process(delta) добавьте сброс блокировки, если AnimationPlayer больше ничего не играет:

if !animation_player.is_playing():
    is_locked = false

Добавьте обработку kick:

if Input.is_action_just_pressed("kick"):
    if animation_player.current_animation != "kick":
        animation_player.play("kick")
        is_locked = true

Важно:

  • kick запускается только если текущая анимация ещё не kick;
  • is_locked = true устанавливается внутри условия запуска.

Код запуска kick и установки is_locked


42. Заблокируйте локомоцию во время kick

В _physics_process(delta) оберните логику анимаций/ориентации в if !is_locked:.

В блоке движения:

if direction:
    if !is_locked:
        if running:
            if animation_player.current_animation != "running":
                animation_player.play("running")
        else:
            if animation_player.current_animation != "walking":
                animation_player.play("walking")

        visuals.look_at(position + direction)

    velocity.x = direction.x * SPEED
    velocity.z = direction.z * SPEED

В блоке покоя:

else:
    if !is_locked:
        if animation_player.current_animation != "idle":
            animation_player.play("idle")

    velocity.x = move_toward(velocity.x, 0, SPEED)
    velocity.z = move_toward(velocity.z, 0, SPEED)

И вызов move_and_slide() тоже должен выполняться только без блокировки:

if !is_locked:
    move_and_slide()

Сброс is_locked, когда анимация завершилась

Ожидаемо:

  • во время kick персонаж не скользит;
  • во время kick локомоционные анимации не перезаписывают удар;
  • обзор мышью остаётся доступным;
  • после завершения kick управление возвращается.

Итоговый player.gd

Для гарантированного совпадения замените всё содержимое res://scripts/player.gd на этот блок.

extends CharacterBody3D

@onready var camera_mount = $camera_mount
@onready var animation_player = $visuals/mixamo_base/AnimationPlayer
@onready var visuals = $visuals

var SPEED = 3.0
const JUMP_VELOCITY = 4.5
var walking_speed = 3.0
var running_speed = 5.0
var running = false
var is_locked = false
@export var sens_horizontal = 0.5
@export var sens_vertical = 0.5

var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")


func _ready():
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED


func _input(event):
    if event is InputEventMouseMotion:
        rotate_y(deg_to_rad(-event.relative.x * sens_horizontal))
        visuals.rotate_y(deg_to_rad(event.relative.x * sens_horizontal))
        camera_mount.rotate_x(deg_to_rad(-event.relative.y * sens_vertical))


func _physics_process(delta):
    if !animation_player.is_playing():
        is_locked = false

    if Input.is_action_just_pressed("kick"):
        if animation_player.current_animation != "kick":
            animation_player.play("kick")
            is_locked = true

    if Input.is_action_pressed("run"):
        SPEED = running_speed
        running = true
    else:
        SPEED = walking_speed
        running = false

    # Add the gravity.
    if not is_on_floor():
        velocity.y -= gravity * delta

    # Handle jump.
    if Input.is_action_just_pressed("ui_accept") and is_on_floor():
        velocity.y = JUMP_VELOCITY

    # Get the input direction and handle the movement/deceleration.
    var input_dir = Input.get_vector("left", "right", "forward", "backward")
    var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()

    if direction:
        if !is_locked:
            if running:
                if animation_player.current_animation != "running":
                    animation_player.play("running")
            else:
                if animation_player.current_animation != "walking":
                    animation_player.play("walking")

            visuals.look_at(position + direction)

        velocity.x = direction.x * SPEED
        velocity.z = direction.z * SPEED
    else:
        if !is_locked:
            if animation_player.current_animation != "idle":
                animation_player.play("idle")

        velocity.x = move_toward(velocity.x, 0, SPEED)
        velocity.z = move_toward(velocity.z, 0, SPEED)

    if !is_locked:
        move_and_slide()

После замены кода:

  1. Сохраните player.gd.
  2. Сохраните player.tscn.
  3. Откройте main.tscn.
  4. Убедитесь, что у инстанса player: - Sens Horizontal = 0.2 - Sens Vertical = 0.2
  5. Сохраните main.tscn.

Итоговые деревья сцен

main.tscn

world (Node3D)
├─ DirectionalLight3D
├─ WorldEnvironment
├─ map (Node3D)
│  ├─ floor (CSGBox3D)
│  ├─ box (CSGBox3D)
│  ├─ box2 (CSGBox3D)
│  └─ box3 (CSGBox3D)
└─ player (instance of res://scenes/player.tscn)

Проверьте:

  • floor, box, box2, box3 — прямые дети map;
  • player — прямой ребёнок world, инстанс res://scenes/player.tscn;
  • WorldEnvironment и DirectionalLight3D — дети world.

player.tscn

player (CharacterBody3D, script: res://scripts/player.gd)
├─ CollisionShape3D
├─ visuals (Node3D)
│  └─ mixamo_base (Node3D, local)
│     ├─ Armature
│     │  └─ Skeleton3D
│     │     ├─ Beta_joints
│     │     └─ Beta_Surface
│     └─ AnimationPlayer
└─ camera_mount (Node3D)
   └─ Camera3D

Проверьте:

  • CollisionShape3D — ребёнок player;
  • visuals — ребёнок player;
  • mixamo_base — ребёнок visuals;
  • AnimationPlayer — доступен по пути $visuals/mixamo_base/AnimationPlayer;
  • camera_mount — ребёнок player;
  • Camera3D — ребёнок camera_mount.

Чек-лист непрерывности

Файлы

  • [ ] Проект Godot 4.0.2.
  • [ ] Renderer: Forward+.
  • [ ] res://assets/models/mixamo_base.glb импортирован.
  • [ ] res://assets/textures/grids/Dark/texture_05.png доступен.
  • [ ] res://assets/textures/grids/Orange/texture_09.png доступен.
  • [ ] res://assets/textures/grids/Purple/texture_09.png доступен.
  • [ ] res://assets/textures/grids/Green/texture_09.png доступен.
  • [ ] Существует res://scenes/main.tscn.
  • [ ] Существует res://scenes/player.tscn.
  • [ ] Существует res://scripts/player.gd.
  • [ ] Main Scene установлен на res://scenes/main.tscn.

Узлы main.tscn

  • [ ] Корень worldNode3D.
  • [ ] mapNode3D, ребёнок world.
  • [ ] floorCSGBox3D, ребёнок map.
  • [ ] boxCSGBox3D, ребёнок map.
  • [ ] box2CSGBox3D, ребёнок map.
  • [ ] box3CSGBox3D, ребёнок map.
  • [ ] WorldEnvironment — ребёнок world.
  • [ ] DirectionalLight3D — ребёнок world.
  • [ ] player — прямой ребёнок world, инстанс res://scenes/player.tscn.

Свойства мира

floor

  • [ ] Size = 1000, 1, 1000.
  • [ ] Position = 0, -0.5, 0.
  • [ ] Geometry → Material Override содержит StandardMaterial3D.
  • [ ] В материале Albedo Texture = res://assets/textures/grids/Dark/texture_05.png.
  • [ ] UV1 → Triplanar = On.
  • [ ] Use Collision = On.

box

  • [ ] Size = 1, 1, 1.
  • [ ] Geometry → Material Override содержит StandardMaterial3D.
  • [ ] Albedo Texture = res://assets/textures/grids/Orange/texture_09.png.
  • [ ] UV1 → Triplanar = On.
  • [ ] Use Collision = On.

box2

  • [ ] Size = 2, 2, 2.
  • [ ] Geometry → Material Override содержит StandardMaterial3D.
  • [ ] Albedo Texture = res://assets/textures/grids/Purple/texture_09.png.
  • [ ] UV1 → Triplanar = On.
  • [ ] Use Collision = On.

box3

  • [ ] Size = 3, 3, 3.
  • [ ] Geometry → Material Override содержит StandardMaterial3D.
  • [ ] Albedo Texture = res://assets/textures/grids/Green/texture_09.png.
  • [ ] UV1 → Triplanar = On.
  • [ ] Use Collision = On.

WorldEnvironment

  • [ ] SDFGI → Enabled = On.
  • [ ] Fog → Enabled = On.
  • [ ] Fog → Light Color = #ffffff.
  • [ ] Fog → Density = 0.001.

Узлы player.tscn

  • [ ] playerCharacterBody3D.
  • [ ] У player есть скрипт res://scripts/player.gd.
  • [ ] CollisionShape3D — ребёнок player.
  • [ ] CollisionShape3D.ShapeCylinderShape3D.
  • [ ] Height = 2.
  • [ ] Radius = 0.3.
  • [ ] Position = 0, 1.1, 0.
  • [ ] visualsNode3D, ребёнок player.
  • [ ] mixamo_base — ребёнок visuals.
  • [ ] mixamo_base развёрнут на 180° вокруг Y.
  • [ ] mixamo_base сделан локальным через Make Local.
  • [ ] AnimationPlayer доступен как $visuals/mixamo_base/AnimationPlayer.
  • [ ] camera_mountNode3D, ребёнок player.
  • [ ] camera_mount.Position = 0, 1.4, 0.
  • [ ] Camera3D — ребёнок camera_mount.
  • [ ] Camera3D.Current = On.

Input Map

  • [ ] forward существует.
  • [ ] forward назначен на W (Physical).
  • [ ] backward существует.
  • [ ] backward назначен на S (Physical).
  • [ ] left существует.
  • [ ] left назначен на A (Physical).
  • [ ] right существует.
  • [ ] right назначен на D (Physical).
  • [ ] run существует.
  • [ ] run назначен на Shift (Physical).
  • [ ] kick существует.
  • [ ] kick назначен на F (Physical).
  • [ ] Deadzone у custom actions: 0.5.
  • [ ] ui_accept не удалён и используется для прыжка.

Анимации

  • [ ] idle существует.
  • [ ] walking существует.
  • [ ] running существует.
  • [ ] kick существует.
  • [ ] idle loop включён.
  • [ ] walking loop включён.
  • [ ] running loop включён.
  • [ ] kick loop выключен.
  • [ ] AnimationPlayer → Playback Options → Default Blend Time = 0.2 s.

Код

  • [ ] extends CharacterBody3D.
  • [ ] Есть @onready var camera_mount = $camera_mount.
  • [ ] Есть @onready var animation_player = $visuals/mixamo_base/AnimationPlayer.
  • [ ] Есть @onready var visuals = $visuals.
  • [ ] Есть var SPEED = 3.0.
  • [ ] Есть const JUMP_VELOCITY = 4.5.
  • [ ] Есть var walking_speed = 3.0.
  • [ ] Есть var running_speed = 5.0.
  • [ ] Есть var running = false.
  • [ ] Есть var is_locked = false.
  • [ ] Есть @export var sens_horizontal = 0.5.
  • [ ] Есть @export var sens_vertical = 0.5.
  • [ ] Есть var gravity = ProjectSettings.get_setting("physics/3d/default_gravity").
  • [ ] В _ready() есть Input.mouse_mode = Input.MOUSE_MODE_CAPTURED.
  • [ ] В _input(event) обрабатывается InputEventMouseMotion.
  • [ ] Горизонтальный поворот: rotate_y(deg_to_rad(-event.relative.x * sens_horizontal)).
  • [ ] Компенсация модели: visuals.rotate_y(deg_to_rad(event.relative.x * sens_horizontal)).
  • [ ] Вертикальный поворот камеры: camera_mount.rotate_x(deg_to_rad(-event.relative.y * sens_vertical)).
  • [ ] Используется Input.get_vector("left", "right", "forward", "backward").
  • [ ] Используется visuals.look_at(position + direction).
  • [ ] При run:
  • SPEED = running_speed
  • running = true
  • [ ] При отсутствии run:
  • SPEED = walking_speed
  • running = false
  • [ ] При отсутствии движения:
  • velocity.x = move_toward(velocity.x, 0, SPEED)
  • velocity.z = move_toward(velocity.z, 0, SPEED)
  • [ ] При отсутствии анимации:
  • if !animation_player.is_playing(): is_locked = false
  • [ ] kick запускается только если:
  • Input.is_action_just_pressed("kick")
  • animation_player.current_animation != "kick"
  • [ ] При запуске kick:
  • animation_player.play("kick")
  • is_locked = true
  • [ ] Локомоционные анимации и visuals.look_at(...) защищены if !is_locked:.
  • [ ] move_and_slide() вызывается только внутри if !is_locked:.

Инстанс player в main.tscn

  • [ ] В Inspector инстанса player есть Sens Horizontal.
  • [ ] В Inspector инстанса player есть Sens Vertical.
  • [ ] Sens Horizontal = 0.2.
  • [ ] Sens Vertical = 0.2.
  • [ ] main.tscn сохранён после изменения этих значений.

Финальная проверка в игре

Запустите main.tscn.

Проверьте:

  1. Ходьба - W — вперёд; - S — назад; - A — влево; - D — вправо; - персонаж сталкивается с полом и боксами.

  2. Мышь - курсор захвачен; - горизонтальный обзор работает; - вертикальный обзор работает; - чувствительность комфортная.

  3. Анимации - в покое — idle; - при движении — walking; - с Shiftrunning; - переходы не являются мгновенными скачками.

  4. Kick - нажмите F; - запускается kick; - во время kick персонаж не скользит; - во время kick idle/walking/running не перезаписывают удар; - обзор мышью остаётся доступным; - после завершения kick управление возвращается.

  5. Бег - Shift увеличивает скорость до 5.0; - без Shift скорость возвращается к 3.0; - анимация меняется с walking на running.

Финальный результат: персонаж в тестовой сцене с боксами


Управление

Действие Ввод
Вперёд W
Назад S
Влево A
Вправо D
Обзор мышь
Бег Shift
Kick F
Прыжок ui_accept, по умолчанию Space/Enter

Важно:

  • forward, backward, left, right, run, kick — custom Input Map actions;
  • ui_accept остаётся стандартным действием для прыжка;
  • мышь захватывается и не освобождается в этом туториале.

Ограничения реализации

  1. Нет clamp по вертикальной оси камеры. - camera_mount.rotate_x(...) не ограничен вручную. - Это может привести к чрезмерному наклону камеры. - В этом туториале это сознательно не добавляется.

  2. Нет освобождения мыши. - Мышь остаётся в режиме MOUSE_MODE_CAPTURED. - Чтобы вернуть курсор, остановите игру или закройте игровое окно.

  3. Kick блокирует движение и локомоцию, но не обзор. - Во время kick не вызывается move_and_slide(). - Во время kick не переключаются idle/walking/running. - Мышью можно продолжать осматривать сцену.


Что в видео задаётся визуально, а не числом

Эти параметры нужно подбирать глазами, а не вводить фиксированными числами:

  1. Позиции и повороты box, box2, box3. - размеры заданы: 1×1×1, 2×2×2, 3×3×3; - финальные Position и Rotation задаются gizmo визуально; - точные координаты не являются обязательными.

  2. Локальная позиция Camera3D внутри camera_mount. - camera_mount.Position = 0, 1.4, 0 задан; - но локальный offset камеры подбирается через Camera Preview; - цель — удобный over-the-shoulder вид.

  3. Общая композиция тестовой сцены. - важно, чтобы боксы были видны и препятствовали движению; - их точная раскладка может немного отличаться.

  4. Визуальная подгонка скорости. - walking_speed = 3.0 и running_speed = 5.0 заданы; - но ощущение «скольжения» всё равно проверяется в игре глазами.

  5. Визуальная проверка анимаций. - loop, blend time и look_at проверяются через результат:

    • персонаж не «плывёт»;
    • ступни не скольжат чрезмерно;
    • повороты модели выглядят естественно.
Lady Godot Post Apocalypse