Physics "Deteriorate" Overtime Working with RigidBody3D

Godot Version

Godot_4.5.1-stable

Question

I am essentially creating arcade racing-style physics (cars). I am having this problem where, slowly over time, the car will start to rubberband and turn sharper. The video attached showcases this. I am not too sure what the problem is or when it started; I just noticed it now.

This is the script for the car:

extends RigidBody3D

# CREDIT: https://www.youtube.com/watch?v=fe-8J7_WAq0

@export var debug: bool = false
@export var is_player: bool = true

@export var engine_power: float = 1

@export var suspension_rest_dist: float = 0.5
@export var spring_strengh: float = 10
@export var spring_damper: float = 1
@export var wheel_radius: float = 0.33

@export var steering_angle: float = 30.0
@export var front_tire_grip: float = 2.0
@export var rear_tire_grip: float = 2.0

@export var raycast_collisions: Node3D

var accel_input = Input.get_axis("down", "up")
var steering_input

var direction = "up"
var can_turn = true


func _ready() -> void:
	if is_player:
		Global.car = self
	
	await get_tree().create_timer(2).timeout
	#direction = "left"
	


func _physics_process(delta: float) -> void:
	if is_player:
		player(delta)
	else:
		npc(delta)


func player(delta):
	accel_input = Input.get_axis("down", "up")
	
	steering_input = Input.get_axis("right", "left")
	var steering_rotation = steering_input * steering_angle
	
	var front_right: RayCast3D = $Wheels/FrontRight
	var front_left: RayCast3D = $Wheels/FrontLeft
	
	if steering_rotation != 0:
		var angle = clamp(front_left.rotation.y + steering_rotation, -steering_angle, steering_angle)
		var new_rotation = angle * delta
		
		front_left.rotation.y = lerp(front_left.rotation.y, new_rotation, 0.3)
		front_right.rotation.y = lerp(front_right.rotation.y, new_rotation, 0.3)
	else:
		front_left.rotation.y = lerp(front_left.rotation.y, 0.0, 0.2)
		front_right.rotation.y = lerp(front_right.rotation.y, 0.0, 0.2)
	
	#Drifting
	rear_tire_grip = 0.2 if Input.is_action_pressed("drift") else 0.5
	front_tire_grip = 2 if Input.is_action_pressed("drift") else 1


# Gets direction car is facing
func get_facing_dir():
	var facing_direction = global_transform.basis.z
	#print((facing_direction - Vector3(1, 0.0, 0.0)).length())
	if (facing_direction - Vector3(1, 0.0, 0.0)).length() < 0.1:
		return "left"
	elif (facing_direction - Vector3(-1, 0.0, 0.0)).length() < 0.1:
		return "right"
	elif (facing_direction - Vector3(0.0, 0.0, 1)).length() < 0.1:
		return "up"
	elif (facing_direction - Vector3(0.0, 0.0, -1)).length() < 0.1:
		return "down"


func npc(delta):
	
	var directions = {
		"left" = [1,1],
		"right" = [1,-1],
		"up" = [1,0],
		"down" = [-1,0],
		"stop" = [0,0],
	}
	
	accel_input = directions[direction][0]
	steering_input = directions[direction][1]
	
	raycasts()
	
	var steering_rotation = steering_input * steering_angle
	
	var front_right: RayCast3D = $Wheels/FrontRight
	var front_left: RayCast3D = $Wheels/FrontLeft
	
	if steering_rotation != 0:
		var angle = clamp(front_left.rotation.y + steering_rotation, -steering_angle, steering_angle)
		var new_rotation = angle * delta
		
		front_left.rotation.y = lerp(front_left.rotation.y, new_rotation, 0.3)
		front_right.rotation.y = lerp(front_right.rotation.y, new_rotation, 0.3)
	else:
		front_left.rotation.y = lerp(front_left.rotation.y, 0.0, 0.2)
		front_right.rotation.y = lerp(front_right.rotation.y, 0.0, 0.2)


# Turn left or right
func change_directions(time, new_direction, stop_buffer):
	
	can_turn = false
	direction = "up"
	await get_tree().create_timer(time).timeout
	
	direction = new_direction
	
	# Buffer to wait for direction to change
	await get_tree().create_timer(stop_buffer).timeout
	
	# Wait until direction is changed
	var facing_dir = null
	while facing_dir == null:
		facing_dir = get_facing_dir()
		await get_tree().create_timer(0.01).timeout
	
	direction = "up"
	
	# Snap its rotation so it goes straight
	await get_tree().create_timer(0.1).timeout
	if facing_dir == "up":
		global_rotation.y = deg_to_rad(0)
	elif facing_dir == "left":
		global_rotation.y = deg_to_rad(90)
	elif facing_dir == "down":
		global_rotation.y = deg_to_rad(180)
	elif facing_dir == "right":
		global_rotation.y = deg_to_rad(270)
	
	steering_angle = 45
	can_turn = true 


func raycasts():
	
	for raycast: RayCast3D in raycast_collisions.get_children():
		# snap all of the raycasts in place so the do not move
		raycast.global_position = Vector3(raycast.global_position.x, 3, raycast.global_position.z)
		raycast.global_rotation = Vector3(0, raycast.global_rotation.y, PI/2)
		
		# Checks for collision in front of it
		if raycast.name == "RayCast3D90" and raycast.is_colliding() and direction == "up" and can_turn == true:
			direction = "down"
			await get_tree().create_timer(0.5).timeout
			direction = "stop"
			await get_tree().create_timer(0.5).timeout
			
			# Get a direction to move in
			var directions_can_move = []
			for direction_raycast: RayCast3D in raycast_collisions.get_children():
				#Right
				if direction_raycast.name == "RayCast3D15":
					if not direction_raycast.is_colliding():
						directions_can_move.append("right")
				#Left
				elif direction_raycast.name == "RayCast3D165":
					if not direction_raycast.is_colliding():
						directions_can_move.append("left")
			# Choose a random possible direction to move
			if directions_can_move:
				var chosen_direction = directions_can_move[randi_range(0, directions_can_move.size() - 1)]
				if chosen_direction == "left":
					change_directions(0.75, "left", 0.5)
				elif chosen_direction == "right":
					change_directions(0.25, "right", 0.5)
			
			# U-turn
			else:
				steering_angle = 75
				change_directions(0, "left", 1.5)

This is the script for the car’s wheels:

extends RayCast3D


@export var is_front_wheel: bool

@onready var car: RigidBody3D = get_parent().get_parent()
@onready var wheel = $Wheel
@onready var turn_debug = $"../FrontRight/turn_debug"

var previous_spring_length: float = 0.0


func _ready() -> void:
	add_exception(car)


func _physics_process(delta: float) -> void:
	if is_colliding():
		suspension(delta, get_collision_point())
		acceleration(get_collision_point())
		apply_z_force(get_collision_point())
		apply_x_force(delta, get_collision_point())
		set_wheel_position(to_local(get_collision_point()).y + car.wheel_radius)
		rotate_wheel(delta)
	else:
		set_wheel_position(-car.suspension_rest_dist)

func apply_x_force(delta, collision_point):
	var dir = global_basis.x
	var state := PhysicsServer3D.body_get_direct_state(car.get_rid())
	var tire_world_vel := state.get_velocity_at_local_position(global_position - car.global_position)
	var lateral_vel = dir.dot(tire_world_vel)
	
	var grip = car.rear_tire_grip
	if is_front_wheel:
		grip = car.front_tire_grip
	
	var max_change = 5.0
	
	var desired_vel_change = -lateral_vel * grip
	desired_vel_change = clamp(desired_vel_change, -max_change, max_change)
	
	var x_force = desired_vel_change / delta
	
	
	car.apply_force(dir * x_force, collision_point - car.global_position)
	
	if car.debug:
		DebugDraw3D.draw_arrow(global_position, global_position + (dir * x_force / 20), Color.RED, 0.1, true)


func apply_z_force(collision_point):
	var dir = global_basis.z
	var state := PhysicsServer3D.body_get_direct_state(car.get_rid())
	var tire_world_vel = state.get_velocity_at_local_position(global_position - car.global_position)
	var z_force = dir.dot(tire_world_vel) * car.mass / 5
	
	car.apply_force(-dir *z_force, collision_point - car.global_position)
	
	var point = Vector3(collision_point.x, collision_point.y + car.wheel_radius, collision_point.z)
	
	if car.debug:
		DebugDraw3D.draw_arrow(point, point + (-dir * z_force / 5), Color.BLUE_VIOLET, 0.1, true)


func set_wheel_position(new_y_position: float):
	wheel.position.y = lerp(wheel.position.y, new_y_position, 0.6)
	
	if car.debug:
		turn_debug.visible = true


func rotate_wheel(delta):
	var dir = car.basis.z
	var rotation_direction = 1 if car.linear_velocity.dot(dir) > 0 else -1
	
	wheel.rotate_x(rotation_direction * car.linear_velocity.length() * delta)


func acceleration(collision_point):
	
	if is_front_wheel:
		return
	
	var accel_dir = -global_basis.z
	var torque = car.accel_input * car.engine_power
	var point = Vector3(collision_point.x, collision_point.y + car.wheel_radius, collision_point.z)
	
	car.apply_force(accel_dir * torque, point - car.global_position)
	
	if car.debug:
		DebugDraw3D.draw_arrow(point, point + (accel_dir * torque / 20), Color.BLUE, 0.1, true) 


func suspension(delta, collision_point):
# The direction the force will be applied
	var susp_dir = global_basis.y
	
	var raycast_origin = global_position
	var raycast_dest = collision_point
	var distance = raycast_dest.distance_to(raycast_origin)
	
	var spring_length = clamp(distance - car.wheel_radius, 0, car.suspension_rest_dist)
	var spring_force = car.spring_strengh * (car.suspension_rest_dist - spring_length)
	var spring_velocity = (previous_spring_length - spring_length) / delta
	
	var damper_force = car.spring_damper * spring_velocity
	
	var suspension_force = basis.y * (spring_force + damper_force)
	
	previous_spring_length = spring_length
	
	var point = Vector3(raycast_dest.x, raycast_dest.y + car.wheel_radius, raycast_dest.z)
	
	car.apply_force(susp_dir * suspension_force, point - car.global_position)
	
	if car.debug:
		#DebugDraw3D.draw_sphere(point, 0.1)
		var target = position + Vector3(-position.x + 0.001, suspension_force.y / 20, -position.z)
		DebugDraw3D.draw_arrow(global_position, to_global(target), Color.GREEN, 0.1, true)
		DebugDraw3D.draw_line_hit_offset(global_position, to_global(position + Vector3(-position.x, -1, -position.z)), true, distance, 0.2, Color.RED, Color.RED)

Again, not really sure if the problem arises from these scripts.

Here is a video of the problem:

Any response is a big help. Thank you!

That’s because RigidBody3D objects are not meant to have constant inputs. You want to use a CharacterBody3D instead.

The person in that video led you down the wrong path. That code is also needlessly complex.

So essentially I would need to rebuild the system from the ground up using a CharcterBody3D instead?

What do you mean when you say this? I’ve done plenty of vehicle simulations in Godot and other engines where a RigidBody was used without issue.

I agree that a CharacterBody3D might be better suited for the arcade-style driving that @packid879 is going for. I’m just a little confused by that initial comment because I, again, have never had any issue using a RigidBody3D in this context.


As for @packid879, I would strongly suggest you format the second script in your original post. Without indentation, I have to sit and interpret the code’s meaning instead of simply reading it. That’s not something I’m going to do.

As you pointed out, the code was poorly formatted. So I was basing that statement on what I saw with a quick pass. Which is that it seemed like that it was getting input every frame - notoriously a problem with RigidBody3D objects.

I have seen people do really cool things with RigidBody3D objects, but it is still not the recommended path.

Having said that, if the OP wanted to continue down the path they’re going, I suspect the RigidBody3D is either getting velocity directly applied, or too many calls to pass it an impulse.

I’ve never heard anything about this, and I’ve made vehicles with many, many wheels that all, in a single frame, apply forces to the same rigidbody every physics tick. Do you have a reference to something I can read? I want to learn about this in case I ever come across this myself.


As for the problem described in this post, is it not also possible that the issue seen in the video is caused by odd camera movement rather than an underlying physics issue? If the vehicle (RigidBody3D) was to actually “rubber-band” like that, I imagine the physics state would become unstable and suddenly launch itself somewhere rather than remaining controllable like what is seen in the video.

Are you talking about using _integrate_forces? Because otherwise it is clearly documented in the RigidBody3D documentation:

Note: Changing the 3D transform or linear_velocity of a RigidBody3D very often may lead to some unpredictable behaviors. This also happens when a RigidBody3D is the descendant of a constantly moving node, like another RigidBody3D, as that will cause its global transform to be set whenever its ancestor moves.

Sorry about the weird formatting; it lost its indents when I copied and pasted it.

I switched the camera for a static one attached to the car, and that seemed to fix the car visually rubberbanding; however, the giraffe in the car still “tweaks out”. It looks the same as it does in the video.

It uses spring bones to animate its movement.

I still don’t know if that is the problem; if it was, I don’t know why it would have affected the camera in the main scene.

My best bet is most likely to remake the system with a CharacterBody3D to avoid future problems.

I’m only talking about what was mentioned by @dragonforge-dev. I was curious as to what was meant by a RigidBody3D “getting input every frame” and the problem it supposedly causes. If you are referring to direct state modifications (e.g. transform changes or velocity changes), then fair enough. This should never be done continuously, as you say.

Despite the poor formatting of the (yet unformatted) 2nd script, there doesn’t appear to be any direct modifications made to the car’s state – just these 4 lines invoking apply_force():

car.apply_force(dir * x_force, collision_point - car.global_position)
car.apply_force(-dir *z_force, collision_point - car.global_position)
car.apply_force(accel_dir * torque, point - car.global_position)
car.apply_force(susp_dir * suspension_force, point - car.global_position)

There is, however, a weird function that I can’t really make sense of. I assume it has something to do with the “arcade racing-style physics”. Perhaps @packid879 can explain the change_directions() method containing transform changes which, as mentioned by @dragonforge-dev and @athousandships, will cause issues if used repeatedly:

# Turn left or right
func change_directions(time, new_direction, stop_buffer):
	
	can_turn = false
	direction = "up"
	await get_tree().create_timer(time).timeout
	
	direction = new_direction
	
	# Buffer to wait for direction to change
	await get_tree().create_timer(stop_buffer).timeout
	
	# Wait until direction is changed
	var facing_dir = null
	while facing_dir == null:
		facing_dir = get_facing_dir()
		await get_tree().create_timer(0.01).timeout
	
	direction = "up"
	
	# Snap its rotation so it goes straight
	await get_tree().create_timer(0.1).timeout
	if facing_dir == "up":
		global_rotation.y = deg_to_rad(0)
	elif facing_dir == "left":
		global_rotation.y = deg_to_rad(90)
	elif facing_dir == "down":
		global_rotation.y = deg_to_rad(180)
	elif facing_dir == "right":
		global_rotation.y = deg_to_rad(270)
	
	steering_angle = 45
	can_turn = true

That’s what I meant.

This method is used for the NPC car. It is called with a raycast collision that looks in front of it, and turns the car appropriately.

It is not really used repeatedly, only when the raycast detects a wall in front of it.

I tried to remove the NPC car, and the code associated (the transform part as well), but this did not solve the problem.

(also I have no idea how to format the code properly)

Use the </> button to format code (hotkey: CTRL + E). Read this before your next post:


What’s the z_force for? Ordinarily, the force that turns a vehicle is a product of the slip in the x- and z- axis. In cases of approximated vehicle physics, the z-axis can be ignored because there is no simulation of a drivetrain that produces friction against the force generated by the tire. I’m guessing you’re using it to perform some sort of drag on the car. It’s just an odd way of doing it in my opinion.

Have you tried omitting apply_z_force()?

I’ve done a lot of this kinda stuff in the past, and achieving desirable behaviour from the addition of longitudinal forces can sometimes be iffy.


Let me know how it goes – and please, for the love of god, format that script.

I have already tried to debug all of those functions, including apply_z_force(), but that did not fix the issue. The only one that technically fixed the issue was acceleration(), but then again, the car doesn’t move without it.

apply_z_force() does, as you said, apply drag so the car does not speed up or exceed a max speed, and slows down when nothing is pressed.

(my bad for the formatting, did not notice that)

Hmm… my other guess would, then, be the computation of the x-force.

As eluded to previously, the force generated by a tire is the product of the current slip value multiplied by the load on the tire.

force_x = slip * wheel_load

In a realistic context (i.e. not arcade physics), the weight of the vehicle is distributed onto the attached spring joints. For a standard 4-wheeled vehicle whose spring configuration has reached equilibrium, the force acting on the springs (and thus the tires) is roughly the vehicle’s mass / 4. When the vehicle is in motion, for example when turning, the weight is unevenly distributed; more weight is placed on the wheels located at the outer arc of the turning circle which results in more force being generated by those wheels. Consequently, the wheels on the inside of the turning circle have less weight on them thus generating less force.

The key takeaway from how the physics of a vehicle behave in real life is: the friction forces of a tire varies wildly while driving, but the aggregate friction force produced by the vehicle configuration (all 4 tires) is roughly always equal to the weight placed on it.

In contrast, your arcade implementation takes the simple, but naive, approach of simply counteracting the car’s velocity in the x-direction, multiplied by some constant (grip).

	var desired_vel_change = -lateral_vel * grip
	desired_vel_change = clamp(desired_vel_change, -max_change, max_change)
	
	var x_force = desired_vel_change / delta

A single wheel is enough to counteract the vehicle x-axis motion. Any additional wheels that the car may have introduces additional counteracting forces. In aggregate, the lateral force acting on the car is essentially tire_friction * the_amount_of_wheels. In extreme states (e.g. hard turning or hard landing) this can cause instability as the force generated makes the car flip-flop back and forth between two unstable states. There is essentially a feedback loop.


I’m not sure if this is a correct explanation but it’s the only thing I could come up with that would cause an issue like the one you’re seeing; one where the issue grows over time.

To test this, you can try defining the x_force like:

# Get lateral velocity from velocity transformed to local space
var lateral_vel = (global_basis.inverse() * tire_world_vel).X
# Note: your current definition of lateral_vel
# lets z-velocity affect it which is not correct.

var wheel_count = 4
var x_force = lateral_vel * car.mass / wheel_count

…or for a more physically realistic, but still arcady, friction model you can try:

# Weight-based lateral force
var arcade_slip = lateral_vel / (1 / grip)
arcade_slip = CLAMP(arcade_slip, -1, 1)

var x_force = spring_force * arcade_slip
# You will have to store the spring_force from your suspension() invocation.

# Note: In this model, the grip-variable essentially controls
# how fast the wheel's friction force scales in proportion its x-velocity.
# Pay attention to how the arcade_slip is clamped to [-1, 1] to prevent
# the wheel(s) from generating counter-acting forces that would make
# the vehicle move in the opposite direction (become unstable).

I hope it works. It’s hard to know for sure when you’re not sitting with the project in front of you. Honestly, I feel like I’m missing something but it’s my best guess.

Let me know how it goes.

Pretty interesting solution to the problem, never really thought of thinking about a problem like this with a real-world scenario.

However, unfortunately, it did not fix the problem. I tried implementing both solutions, mixing in parts of the old script, changing some numbers, and trying it without the apply_x_force()function, but it still acted the same over time.

It is most likely just a problem with it being a RigidBody3D and how the physics work in the game engine, as @athousandships and @dragonforge-dev said because I have already tried to debug most of the code, to no avail.

Aaahr… I hate when this happens!

Okay, give me a couple of days – maybe a week. I’ll test some stuff and get back to you. Even if you decide to switch to a CharacterBody3D-based approach, I would still like to figure out how to fix this issue.

Sounds good! :+1:

Do you want the project file?

It would be nice to have access to a Github repo so I can look at specific files, if possible. I’d rather not work in the project though. I want to test stuff in a fresh project to avoid any unnecessary complexity.

So I tried to isolate just the car to put in a Github repository, basically removing everything besides the car, camreas, and giraffe character, and through removing everything, the problem seemed to fix.

If I had to guess, it might have something to do with either the collision shapes on the roads or something else in the code that is overloading the physics engine.

I will do some more testing tommorow.