The actual code is spread awkwardly across multiple nodes and functions because I haven’t settled on the final system yet, so I tried to clean it up to look uniform here. Just imagine all of the code running in a “for wheel in wheels” loop and you’ll be able to make sense of this version.
It’s important to note that, in both the “realistic” version and the “arcade” version of the code, the vehicle steers itself to face the direction the camera is facing (using wrapf(camera_rotation.y - global_rotation.y, -PI, PI)
as the target angle). You’ll see this in more detail in Acceleration and Braking.
Suspension Implementation
var current_length := wheel.global_position.distance_to(wheel.get_collision_point())
var rest_length := wheel.suspension_length - wheel.radius
var compression := rest_length - current_length
var velocity := wheel.last_length - current_length
var spring := stiffness * compression
var damper := wheel.damping * velocity
var force := get_collision_normal() * max((spring + damper) / state.step, 0.0)
state.add_force(force, get_collision_point() - state.transform.origin)
wheel.last_length = current_length
I have the vehicle rest midway through the raycast because I want the wheels to hang down when the car is airborne and will use the ray length to drive that animation.
Problem 1: The wheel doesn’t feel like a wheel. I tried to multiplying the spring force with the result of wheel.get_collision_normal().dot(wheel.global_transform.basis.y)
to fix it. It didn’t do much. I’m not sure what to make of this one.
Problem 2: Instead of rolling over obstacles that I would expect it to roll over smoothly (like a small rock), the wheels instead push the entire vehicle up every time, leading to a very bumpy ride.
It’s especially apparent when the vehicle hits a very steep ramp of any kind. Instead of crashing, the vehicle very quickly snaps up because it’s front end is getting rapidly and completely lifted to depenetrate the ray, and if the wall is steep enough, it kinda springs off of it.
Problem 3: I forgot to mention in my original post that the rest length doesn’t work correctly. The vehicle will float higher or lower depending on the stiffness
and damping
values.
Tire Implementation
I don’t know what this means, honestly. I’m not doing any sort of simulation, if that’s what you mean. My end goal is to replicate Halo’s style of car physics, which don’t appear to be all that deep.
Acceleration and Braking
This is the current “realistic” setup. It’s just acceleration and deceleration, but it actually uses the wheels to turn.
var steer_target := wrapf(camera_rotation.y - global_rotation.y, -PI, PI)
if wheel.steering == true:
wheel.rotation.y = move_toward(wheel.rotation.y, steer_target, max_turn_rate)
wheel.rotation.y = clamp(wheel.rotation.y, -max_turn, max_turn)
var drive_dir := Vector3.FORWARD.rotated(Vector3.UP, wheel.global_rotation.y)
var slide_dir := Vector3.RIGHT.rotated(Vector3.UP, wheel.global_rotation.y)
var wheel_vel := state.get_velocity_at_local_position(global_transform.basis * wheel.position)
var drive_vel := wheel_vel.normalized().dot(drive_dir)
var slide_vel := wheel_vel.normalized().dot(slide_dir)
var current_speed := abs(drive_vel * wheel_vel.length())
var acceleration := Vector3.ZERO
if is_zero_approx(input_direction.y) == true:
acceleration = -drive_dir * (deceleration_rate * drive_vel) * 2
elif current_speed < get_speed():
acceleration = drive_dir * (-input_direction.y * acceleration_rate) * 2
acceleration -= slide_dir * (friction_deceleration_rate * slide_vel)
state.add_force(mass * (acceleration/4), wheel.global_position - global_position)
As you can see, it’s basically a cross between character-style acceleration/deceleration and actual realistic vehicle physics. There’s some weirdness in there where I multiply by 2 and divide by 4, that’s compensating for the fact that this code is run once per wheel and the rates and max speed values are all for the vehicle as a whole.
Problem: The “instability” I mentioned has to do with add_force()
. If I add the force to wheel.get_collision_point() - global_position
, it pushes the vehicle from the wheels, thereby causing it to tilt back and making it very unstable as shown in basically every tutorial on raycast vehicles out there; applying it to wheel.global_position - global_position
is much more stable but it still tilts the vehicle (only forward this time).
It also has the problem of skidding to a stop on tight turns sometimes, which is probably a realistic behavior, but isn’t desirable. This might be related to the above problem, so I didn’t give it its own label.
The “Arcade” Approach
This approach solves most of the issues introduced by the “realistic” solution in the above section and appears to be the same method used in Halo (or similar at least), but it comes with its own set of problems.
It works by driving the vehicle using add_central_force()
and simple acceleration/deceleration forces. The vehicle steers by using add_torque()
to rotate the vehicle around Vector3.UP toward steer_target
. This means that steering and driving are completely separate things that cannot influence each other without some form of mathematical conversion between the two.
This would look something like this, but I deleted the original code so this is reconstructed from memory and untested:
var drive_dir := Vector3.FORWARD
var slide_dir := Vector3.RIGHT
var drive_vel := linear_velocity.normalized().dot(drive_dir)
var slide_vel := linear_velocity.normalized().dot(slide_dir)
var current_speed := abs(drive_vel * wheel_vel.length())
var acceleration := Vector3.ZERO
if is_zero_approx(input_direction.y) == true:
acceleration = -drive_dir * (deceleration_rate * drive_vel) * 2
elif current_speed < max_speed:
acceleration = drive_dir * (-input_direction.y * acceleration_rate) * 2
acceleration -= slide_dir * (friction_deceleration_rate * slide_vel)
state.add_central_force(mass * (acceleration/4))
var steering_diff := wrapf(camera_rotation.y - global_rotation.y, -PI, PI)
state.add_torque((steering_diff * turn_stiffness) - (turn_damping * angular_velocity.y), Vector3.UP)
Problem 1: is the “extreme overshoot” I mentioned before. Both versions overshoot the target direction (which is actually intentional), but arcade approach doesn’t compensate for it at all and will start zigzagging forever as it overshoots the target more and more. The only solution I came up with is to use an angular spring+damper force to rotate the vehicle, but in doing so eliminates the ability to control the turning speed or limit the angle of the turn.
Problem 2: Deceleration forces only interact with the linear velocity, so the vehicle spins indefinitely when not driving. I could apply the linear deceleration force as-is to the steering force, but if angular deceleration and linear deceleration aren’t perfectly in sync, the illusion will break as the vehicle stops moving and rotating at different times.
Problem 3: Because steering and driving are separate, scaling the steering speed is a lot harder. I suspect it wouldn’t be much of a problem if I can solve problem 1 without using a spring+damper system for steering, though.
I’m sure these problems could be solved by someone with better math/physics knowledge than me. If it is possible, I would prefer to use this approach over the “realistic” one because it’s much closer to how I want vehicles to feel.
A Few Notes
At some point during my last revision, I changed something that made reversing behave incorrectly (if you’re familiar with Halo’s vehicle controls, you might have noticed it). The only reason I haven’t fixed it yet is because I’m too lazy to do so in the face of the more difficult issues. It’ll probably be an easy fix once I get the time, so you can ignore it.
It’s been a while since I tried, but I don’t think the forum will let me include an attachment yet. This is my first post. Hopefully I was able to describe everything well enough.