small racing game im working on
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
extends VehicleBody3D
class_name Car

@export var STEER_SPEED := 1.0
@export var steer_curve: Curve = preload("res://assets/cars/kenney_sedan/steer_curve.tres")

var steer_target := 0.0

@export var MAX_ENGINE_FORCE := 4000.0
@export var MAX_BRAKE_FORCE := 35.0
@export var reverse_ratio := -2.5
@export var final_drive_ratio := 3.38
@export var max_engine_rpm := 8000.0
@export var gear_shift_time = 0.3
@export var power_curve: Curve = preload("res://assets/cars/kenney_sedan/power_curve.tres")
@onready var body_mesh := $body as MeshInstance3D
@onready var checkpoint_sound := $checkpoint as AudioStreamPlayer

@onready var wheels: Array[VehicleWheel3D] = [$bl as VehicleWheel3D, $br as VehicleWheel3D, $fl as VehicleWheel3D, $fr as VehicleWheel3D]
@onready var wheel_radius: float = wheels[0].wheel_radius
var particles: Array[GPUParticles3D] = []

signal shifted

var gear_ratios: Array[float] = [ 5 ]
var current_gear := 0 # -1 reverse, 0 = neutral, 1 - 6 = gear 1 to 6.
var clutch_position := 1 # 0.0 = clutch engaged
var gear_timer := 0.0
var throttle := 0.0
var engine_rpm := 800.0 # currently cosmetic
var wheel_rpm := 0.0
var can_shift := true
var can_accelerate := false

const inactive = {active = false};
const trail_scene = preload("res://scenes/trail.tscn")
var skids: Array[Array]

func ratio() -> float:
	match current_gear:
		0: return 0
		-1: return reverse_ratio
		_: return gear_ratios[current_gear - 1]

func downforce(force: float):
	apply_force(basis * Vector3(0,-force,0), Vector3(0,1.4,2.5))

func is_on_ground() -> bool:
	return wheels.all(func(whl: VehicleWheel3D): return whl.is_in_contact() != null)

func is_not_on_ground() -> bool:
	return wheels.any(func(whl: VehicleWheel3D): return !whl.is_in_contact())

func reset() -> void:
	gear_timer = 0
	clutch_position = 0
	steering = 0
	throttle = 0
	engine_force = 0
	brake = MAX_BRAKE_FORCE
	can_shift = true
	can_accelerate = false
	for wheel in skids:
		if wheel:
			for skid in wheel:
				if skid is Trail3D:
					skid.queue_free()
			wheel.clear()
	skids = [[inactive], [inactive], [inactive], [inactive]] # performance and complexity hack

func _ready() -> void:
	for whl in wheels:
		particles.append(whl.get_node(^"particles"))
	randomize()

func kph() -> float:
	return (3 * PI * wheel_radius * wheel_rpm) / 25;

# calculate the RPM of wheels
func whl_rpm() -> float:
	var sum := 0.0
	for wheel in wheels:
		sum += abs(wheel.get_rpm())
	return sum / 4

func steer(to: float) -> void:
	if (abs(to) < 0.05):
		to = 0.0
	else:
		to = -steer_curve.sample_baked(-to) if to < 0.0 else steer_curve.sample_baked(to)

	steer_target = clampf(lerpf(steer_target, to, 10 * get_physics_process_delta_time()), -.7, .7)

## virtual
func shift_down() -> bool:
	return false

## virtual
func shift_up() -> bool:
	return false

func _process_gear_inputs(delta: float):
	if gear_timer > 0.0:
		gear_timer = max(0.0, gear_timer - delta)
		clutch_position = 0
	else:
		if shift_down() and current_gear > -1:
			current_gear = current_gear - 1
			gear_timer = gear_shift_time
			clutch_position = 0
			shifted.emit()
		elif shift_up() and current_gear < gear_ratios.size():
			current_gear = current_gear + 1
			gear_timer = gear_shift_time
			clutch_position = 0
			shifted.emit()
		else:
			clutch_position = 1

func _process(delta: float):
	if can_shift:
		_process_gear_inputs(delta)
	steering = -steer_target
	body_mesh.rotation.z = lerp(body_mesh.rotation.z, clampf(((-steering * .2) * linear_velocity.length_squared() / 685.0) + randf_range(-0.05,0.05), -.4, .4), 10 * delta)
	engine_rpm = move_toward(engine_rpm, (wheel_rpm * engine_force * 0.0015) + 800, 800)

func limit(delta: float) -> void:
	linear_damp = max((.5 * delta) * (kph() - 400), 0) if kph() > 400 else 0.0
	angular_damp = max(5 * (angular_velocity.length_squared() - 45), 0) if angular_velocity.length_squared() > 45 else 0.0

func _physics_process(delta: float):
	if can_accelerate:
		var power_factor := power_curve.sample_baked(clampf(wheel_rpm / max_engine_rpm, 0.0, 1.0))
		if current_gear == -1:
			engine_force = throttle * power_factor * reverse_ratio * final_drive_ratio * MAX_ENGINE_FORCE * clutch_position
		elif current_gear > 0 and current_gear <= gear_ratios.size():
			engine_force = throttle * power_factor * gear_ratios[current_gear - 1] * final_drive_ratio * MAX_ENGINE_FORCE * clutch_position
		else:
			engine_force = 0.0

	wheel_rpm = whl_rpm()

	limit(delta)
	downforce(5)

	for i in 4:
		particles[i].emitting = wheels[i].get_skidinfo() < (.2 if i > 2 else .99) and wheels[i].is_in_contact() and kph() > 30
		if particles[i].emitting:
			@warning_ignore("narrowing_conversion")
			particles[i].amount = clampf(ceil(150 * (1 - wheels[i].get_skidinfo())) * 1 if i > 2 else 8, 0, 150)
			if !skids[i][-1].active:
				skids[i].append(trail_scene.instantiate() as Trail3D)
				get_parent().add_child(skids[i][-1])
			(skids[i][-1] as Trail3D).add(wheels[i].global_position - Vector3(0, .661, 0))
		elif skids[i][-1].active:
			skids[i][-1].active = false

func start() -> void:
	brake = 0
	can_shift = true
	can_accelerate = true