Оглавление
Проблема
Вам нужно создать контроллер автомобиля в top-down 2D.
Решение
При решении этой проблемы начинающие разработчики Godot часто создают что-то, что ведет себя совсем не как настоящий автомобиль. Некоторые общие ошибки, которые вы найдете в любительских автомобильных играх:
- Автомобиль не вращается вокруг своего центра. Иными словами, задние колеса автомобиля не скользят в стороны. (За исключением дрифта, но о нем мы поговорим позже.)
- Автомобиль может поворачиваться только в движении — он не может вращаться на месте.
- Автомобиль не поезд; он не движется по рельсам. Повороты при высокой скорости должны сопровождаться некоторым скольжением (дрифтом).
Существует много подходов к 2D физике автомобиля, в основном в зависимости от того, насколько «реалистичным» вы хотите, чтобы он был. В данном решении мы идем к уровню «аркады», что означает, что мы придаем приоритет действию перед реализмом.
Рецепт ниже разбит на 5 частей, каждая из которых добавляет различные характеристики движению автомобиля. Не стесняйтесь комбинировать их по своему усмотрению для удовлетворения ваших потребностей.
Настройка сцены
Вот настройка сцены с автомобилем:
CharacterBody2D
Sprite2D
CollisionShape2D
Camera2D
Добавьте любую текстуру спрайта, которая вам нравится. В этой демонстрации мы будем использовать изображения из Kenney’s Racing Pack. CapsuleShape2D — хороший выбор для коллизии, чтобы у автомобиля не было острых углов, за которые он мог бы зацепиться за препятствия.
Также мы будем использовать четыре действия ввода: «steer_right», «steer_left», «accelerate» и «brake» — установите их на те клавиши, которые вам удобны.
Часть 1: Движение
Первый шаг — написать код движения на основе описанного выше алгоритма.
Начнем с нескольких переменных:
extends CharacterBody2D
var wheel_base = 70 # Distance from front to rear wheel
var steering_angle = 15 # Amount that front wheel turns, in degrees
var steer_direction
Установите длину колесной базы (wheelbase) на значение, которое соответствует вашему спрайту.
steer_direction будет указывать на угол, на который повернуты колеса.
Примечание! Поскольку мы используем управление с клавиатуры, поворот — это все или ничего. Если вы используете аналоговый джойстик, вы можете вместо этого изменять это значение в зависимости от расстояния, на которое двигается джойстик.
func _physics_process(delta):
get_input()
calculate_steering(delta)
move_and_slide()
Каждый кадр нам нужно проверять ввод и рассчитывать управление. Затем мы передаем полученную скорость в move_and_slide(). Далее мы определим эти две функции:
func get_input():
var turn = Input.get_axis("steer_left", "steer_right")
steer_direction = turn * deg_to_rad(steering_angle)
velocity = Vector2.ZERO
if Input.is_action_pressed("accelerate"):
velocity = transform.x * 500
Здесь мы проверяем ввод пользователя и устанавливаем скорость. Примечание: скорость 500 используется временно для тестирования движения. Мы займемся этим в следующей части.
Вот где мы реализуем алгоритм из ссылки:
func calculate_steering(delta):
# 1. Find the wheel positions
var rear_wheel = position - transform.x * wheel_base / 2.0
var front_wheel = position + transform.x * wheel_base / 2.0
# 2. Move the wheels forward
rear_wheel += velocity * delta
front_wheel += velocity.rotated(steer_direction) * delta
# 3. Find the new direction vector
var new_heading = rear_wheel.direction_to(front_wheel)
# 4. Set the velocity and rotation to the new direction
velocity = new_heading * velocity.length()
rotation = new_heading.angle()
Запустите проект, и автомобиль должен двигаться и поворачивать. Тем не менее, он все еще ведет себя очень неестественно — машина мгновенно начинает движение и останавливается. Чтобы исправить это, мы добавим ускорение в расчет.
Часть 2: Ускорение
Нам понадобится еще одна переменная для настроек и одна для отслеживания общего ускорения автомобиля:
var engine_power = 900 # Forward acceleration force.
var acceleration = Vector2.ZERO
Измените код ввода для применения ускорения вместо непосредственного изменения скорости автомобиля.
func get_input():
var turn = Input.get_axis("steer_left", "steer_right")
steer_direction = turn * deg_to_rad(steering_angle)
if Input.is_action_pressed("accelerate"):
acceleration = transform.x * engine_power
После того как мы получили ускорение, мы можем применить его к скорости следующим образом:
func _physics_process(delta):
acceleration = Vector2.ZERO
get_input()
calculate_steering(delta)
velocity += acceleration * delta
move_and_slide()
Теперь, при запуске, автомобиль должен постепенно увеличивать свою скорость. Однако будьте осторожны: у нас пока нет способа замедлиться!
Часть 3: Трение/Сопротивление
Автомобиль испытывает две различные силы замедления: трение и сопротивление воздуха.
- Трение — это сила, приложенная к земле. Она очень велика, если ехать по песку, но очень низка, если ехать по льду. Трение пропорционально скорости — чем быстрее вы двигаетесь, тем сильнее сила.
- Сопротивление воздуха — это сила, обусловленная сопротивлением молекул воздуха. Она зависит от поперечного сечения автомобиля — большой грузовик имеет больше сопротивления, чем стремительный гоночный автомобиль. Сопротивление воздуха пропорционально квадрату скорости.
Это означает, что трение более значительно при медленном движении, но сопротивление воздуха становится доминирующим при высоких скоростях. Мы добавим обе эти силы в наш расчет. Кроме того, значения этих величин также дадут нашему автомобилю максимальную скорость — ту точку, где сила от двигателя уже не может преодолеть силу сопротивления воздуха.
Вот наши начальные значения для этих величин:
var friction = -55
var drag = -0.06
Как видно на этом графике, эти значения означают, что при скорости 600 сила сопротивления воздуха превышает силу трения.
В функции _physics_process() мы вызовем функцию для вычисления текущего трения и применим ее к силе ускорения.
func _physics_process(delta):
acceleration = Vector2.ZERO
get_input()
apply_friction(delta)
calculate_steering(delta)
velocity += acceleration * delta
velocity = move_and_slide(velocity)
func apply_friction(delta):
if acceleration == Vector2.ZERO and velocity.length() < 50:
velocity = Vector2.ZERO
var friction_force = velocity * friction * delta
var drag_force = velocity * velocity.length() * drag * delta
acceleration += drag_force + friction_force
Сначала мы установим минимальную скорость. Это гарантирует, что автомобиль не будет постоянно ползти вперед на очень низких скоростях, так как трение никогда не доводит скорость до нуля.
Затем мы вычисляем две силы и добавляем их к общему ускорению. Поскольку они обе отрицательны, они будут воздействовать на автомобиль в противоположном направлении.
Часть 4: Реверс/Тормоз
Нам понадобятся еще две переменные настроек:
var braking = -450
var max_speed_reverse = 250
Добавьте ввод в функцию get_input():
if Input.is_action_pressed("brake"):
acceleration = transform.x * braking
Это нужно для того, чтобы остановиться, но мы также хотим иметь возможность двигать автомобиль задним ходом. В настоящее время это не сработает, потому что ускорение всегда применяется в направлении «heading«, которое направлено вперед. Когда мы двигаемся задним ходом, нам нужно ускоряться назад.
func calculate_steering(delta):
var rear_wheel = position - transform.x * wheel_base / 2.0
var front_wheel = position + transform.x * wheel_base / 2.0
rear_wheel += velocity * delta
front_wheel += velocity.rotated(steer_angle) * delta
var new_heading = (front_wheel - rear_wheel).normalized()
var d = new_heading.dot(velocity.normalized())
if d > 0:
velocity = new_heading * velocity.length()
if d < 0:
velocity = -new_heading * min(velocity.length(), max_speed_reverse)
rotation = new_heading.angle()
Мы можем определить, ускоряемся ли мы вперед или назад, используя скалярное произведение (dot product). Если два вектора равны, результат будет больше 0. Если движение направлено в противоположном направлении к тому, куда смотрит автомобиль, то скалярное произведение будет меньше 0, и мы должны двигаться назад.
Часть 5: Дрифт/Скольжение
Мы могли бы остановиться здесь и у вас уже был бы удовлетворительный контроллер автомобиля. Тем не менее автомобиль все еще кажется, будто он двигается как «по рельсам». Даже при максимальной скорости повороты происходят идеально, как будто шины обладают идеальным «сцеплением».
При высоких скоростях (или даже низких, если это нужно) сила поворота должна заставлять шины скользить, вызывая дрифт движение.
var slip_speed = 400 # Speed where traction is reduced
var traction_fast = 2.5 # High-speed traction
var traction_slow = 10 # Low-speed traction
Применим эти значения при вычислении управления. Мы будем использовать интерполяцию — lerp() — чтобы заставить машину «поворачиваться» только на часть пути к новому направлению. Значения «тяги» будут определять, насколько «липкие» шины.
func calculate_steering(delta):
var rear_wheel = position - transform.x * wheel_base / 2.0
var front_wheel = position + transform.x * wheel_base / 2.0
rear_wheel += velocity * delta
front_wheel += velocity.rotated(steer_angle) * delta
var new_heading = (front_wheel - rear_wheel).normalized()
# choose which traction value to use - at lower speeds, slip should be low
var traction = traction_slow
if velocity.length() > slip_speed:
traction = traction_fast
var d = new_heading.dot(velocity.normalized())
if d > 0:
velocity = lerp(velocity, new_heading * velocity.length(), traction * delta)
if d < 0:
velocity = -new_heading * min(velocity.length(), max_speed_reverse)
rotation = new_heading.angle()
Здесь мы выбираем, какое значение тяги использовать и применяем lerp() к скорости.
Настройки
На данном этапе у нас есть много параметров, которые управляют поведением автомобиля. Их изменение может радикально изменить, как ведет себя автомобиль. Чтобы сделать эксперименты с разными значениями проще, загрузите проект для этого рецепта ниже. При запуске игры вы увидите набор ползунков, с помощью которых вы сможете изменять поведение автомобиля во время движения (нажмите <Tab>, чтобы показать/скрыть панель ползунков).
Скачать проект
Скачайте код проекта здесь: https://github.com/godotrecipes/2d_car_steering