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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
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")
const sparks := preload("res://assets/cars/sparks.tscn")

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 BOOSTER_FORCE := 25000
@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]

var shup := false
var shown := false

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:
	set_collision_mask_value(2, 0)
	gear_timer = 0
	clutch_position = 0
	steering = 0
	angular_damp = 0
	linear_damp = 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()
	for p in particles:
		p.emitting = false
	skids = [[inactive], [inactive], [inactive], [inactive]] # performance and complexity hack

func _ready() -> void:
	for whl in wheels:
		particles.append(whl.get_node(^"particles"))
	randomize()
	reset()
	if Globals.playing.night():
		night()

func kph() -> float:
	if can_accelerate == false:
		return 0
	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()), -1, 1) * .9

## 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 shown and current_gear > -1:
			shown = false
			current_gear = current_gear - 1
			gear_timer = gear_shift_time
			clutch_position = 0
			shifted.emit()
		elif shup and current_gear < gear_ratios.size():
			shup = false
			current_gear = current_gear + 1
			gear_timer = gear_shift_time
			clutch_position = 0
			shifted.emit()
		else:
			clutch_position = 1

func _process(delta: float):
	body_mesh.rotation.z = lerp(body_mesh.rotation.z, clampf((-steering * .001 + randf_range(-0.0005,0.0005)) * whl_rpm(), -.3, .3), 2 * delta)

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):
	steering = steer_target
	if can_shift:
		_process_gear_inputs(delta)

	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()
	engine_rpm = clampf(move_toward(engine_rpm, (wheel_rpm * engine_force * 0.0015), 800), 800, MAX_ENGINE_FORCE)

	limit(delta)
	downforce(5)

	for i in 4:
		if wheels[i].get_contact_body() is Booster:
			apply_central_force(
				(wheels[i].get_contact_body().transform.basis.x) * BOOSTER_FORCE
			)

		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, .561, 0))
		elif skids[i][-1].active:
			skids[i][-1].active = false
	
	if brake > 0:
		tail.albedo_color = Color.RED;
	else:
		tail.albedo_color = Color(1, 0.34902, 0.227451, 1)

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

func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
	var contact := state.get_contact_count()
	while contact > 0:
		contact -= 1
		if state.get_contact_local_velocity_at_position(contact).length() < 0.5:
			continue
		var p := state.get_contact_local_position(contact)
		var direction := state.get_contact_local_normal(contact)
		var sprk: GPUParticles3D = sparks.instantiate()
		var sprkmat: ParticleProcessMaterial = sprk.process_material
		sprkmat.direction = direction
		sprkmat.initial_velocity_min = state.get_contact_local_velocity_at_position(contact).length()/5
		sprkmat.initial_velocity_max = state.get_contact_local_velocity_at_position(contact).length()/3
		sprk.amount = clampi(floori(state.get_contact_local_velocity_at_position(contact).length()/4), 0, 20)
		get_parent().add_child(sprk)
		sprk.position = p

@onready var tail: Material = $body.mesh.surface_get_material(5);
@onready var head: Material = $body.mesh.surface_get_material(6);

func night() -> void:
	$lights.visible = true;
	tail.emission_enabled = true;
	head.emission_enabled = true;