├── icon.png ├── models ├── sport_car.glb ├── sport_car_colorpalette8x8.png ├── kenney_carkit1 │ ├── sedanSports.glb │ ├── wheelDefault.glb │ ├── sedan_sports.tscn │ ├── License.txt │ ├── sedanSports.glb.import │ └── wheelDefault.glb.import ├── sport_car.glb.import └── sport_car_colorpalette8x8.png.import ├── sounds ├── engine_sample.wav └── engine_sample.wav.import ├── materials ├── gravel.tres ├── tarmac.tres └── tire.tres ├── default_env.tres ├── scenes ├── vehicle │ ├── tire_models │ │ ├── falloff_curve.tres │ │ ├── buildup_curve.tres │ │ ├── temp_mu.tres │ │ ├── pacejka_tire_model.gd │ │ ├── brush_tire_model.gd │ │ ├── base_tire_model.gd │ │ └── curve_tire_model.gd │ ├── diff_params.gd │ ├── wheel_params.gd │ ├── drivetrain_params.gd │ ├── clutch.gd │ ├── car_params.gd │ ├── base_car.tscn │ ├── wheel_suspension.gd │ ├── drivetrain.gd │ └── base_car.gd ├── gui │ ├── gui.gd │ ├── render_stats.gd │ ├── input_app.gd │ ├── render_stats.tscn │ ├── input_app.tscn │ ├── gui.tscn │ ├── tireinfo_app.gd │ └── tireinfo_app.tscn ├── camera.gd ├── track.tscn └── road.gd ├── resources ├── tire_wear_curve.tres ├── tires │ ├── pacejka_default.tres │ └── brush_default.tres └── car_setups │ └── kenney_sport_car.tres ├── .gitignore ├── icon.png.import ├── LICENSE ├── README.md ├── levels └── testlevels │ └── drag_test.tscn └── project.godot /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dechode/Godot-Advanced-Vehicle/HEAD/icon.png -------------------------------------------------------------------------------- /models/sport_car.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dechode/Godot-Advanced-Vehicle/HEAD/models/sport_car.glb -------------------------------------------------------------------------------- /sounds/engine_sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dechode/Godot-Advanced-Vehicle/HEAD/sounds/engine_sample.wav -------------------------------------------------------------------------------- /models/sport_car_colorpalette8x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dechode/Godot-Advanced-Vehicle/HEAD/models/sport_car_colorpalette8x8.png -------------------------------------------------------------------------------- /models/kenney_carkit1/sedanSports.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dechode/Godot-Advanced-Vehicle/HEAD/models/kenney_carkit1/sedanSports.glb -------------------------------------------------------------------------------- /models/kenney_carkit1/wheelDefault.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dechode/Godot-Advanced-Vehicle/HEAD/models/kenney_carkit1/wheelDefault.glb -------------------------------------------------------------------------------- /materials/gravel.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" format=2] 2 | 3 | [resource] 4 | albedo_color = Color( 0.380392, 0.294118, 0, 1 ) 5 | metallic_specular = 0.0 6 | -------------------------------------------------------------------------------- /materials/tarmac.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" format=2] 2 | 3 | [resource] 4 | albedo_color = Color( 0.219608, 0.219608, 0.219608, 1 ) 5 | metallic_specular = 0.0 6 | -------------------------------------------------------------------------------- /materials/tire.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" format=2] 2 | 3 | [resource] 4 | albedo_color = Color( 0.137255, 0.137255, 0.137255, 1 ) 5 | metallic_specular = 0.0 6 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=3 uid="uid://dei7bukiisidd"] 2 | 3 | [sub_resource type="Sky" id="1"] 4 | 5 | [resource] 6 | background_mode = 2 7 | sky = SubResource("1") 8 | -------------------------------------------------------------------------------- /scenes/vehicle/tire_models/falloff_curve.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Curve" format=3 uid="uid://drhcb0jpgo0da"] 2 | 3 | [resource] 4 | _data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 0.781818), 0.0, 0.0, 0, 0] 5 | point_count = 2 6 | -------------------------------------------------------------------------------- /scenes/vehicle/tire_models/buildup_curve.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Curve" format=3 uid="uid://dwkjlpikmfoay"] 2 | 3 | [resource] 4 | _data = [Vector2(0, 0), 0.0, 2.46118, 0, 0, Vector2(1, 1), 0.111189, 0.0, 0, 0] 5 | point_count = 2 6 | -------------------------------------------------------------------------------- /resources/tire_wear_curve.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Curve" format=2] 2 | 3 | [resource] 4 | min_value = 0.5 5 | _data = [ Vector2( 0, 1 ), 0.0, 0.0, 0, 0, Vector2( 0.508696, 0.972727 ), 0.0, 0.0, 0, 0, Vector2( 1, 0.677273 ), -1.3781, 0.0, 0, 0 ] 6 | -------------------------------------------------------------------------------- /resources/tires/pacejka_default.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" load_steps=2 format=3] 2 | 3 | [ext_resource type="Script" path="res://scenes/vehicle/tire_models/pacejka_tire_model.gd" id="1"] 4 | 5 | [resource] 6 | script = ExtResource( 1 ) 7 | tire_stiffness = 0.5 8 | -------------------------------------------------------------------------------- /models/kenney_carkit1/sedan_sports.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://d25y57k2nxxox"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://cakhmlww2amtf" path="res://models/kenney_carkit1/sedanSports.glb" id="1_53cfc"] 4 | 5 | [node name="sedanSports" instance=ExtResource("1_53cfc")] 6 | -------------------------------------------------------------------------------- /scenes/vehicle/tire_models/temp_mu.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Curve" format=3 uid="uid://pp5s6b4xc6lq"] 2 | 3 | [resource] 4 | min_value = 0.5 5 | max_value = 1.1 6 | _data = [Vector2(0, 0.905263), 0.0, 1.19947, 0, 0, Vector2(0.501048, 1.1), 0.0, 0.0, 0, 0, Vector2(1, 0.5), -0.11587, 0.0, 0, 0] 7 | point_count = 3 8 | -------------------------------------------------------------------------------- /scenes/vehicle/diff_params.gd: -------------------------------------------------------------------------------- 1 | class_name DiffParameters 2 | extends Resource 3 | 4 | enum DIFF_TYPE { 5 | LIMITED_SLIP, 6 | OPEN_DIFF, 7 | LOCKED, 8 | } 9 | 10 | @export var diff_preload = 50.0 11 | @export var power_ratio: float = 2.0 12 | @export var coast_ratio: float = 1.0 13 | 14 | @export var diff_type = DIFF_TYPE.LIMITED_SLIP 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_presets.cfg 8 | 9 | # Imported translations (automatically generated from CSV files) 10 | *.translation 11 | 12 | # Mono-specific ignores 13 | .mono/ 14 | data_*/ 15 | mono_crash.*.json 16 | 17 | addons/godot-git-plugin 18 | -------------------------------------------------------------------------------- /scenes/vehicle/wheel_params.gd: -------------------------------------------------------------------------------- 1 | class_name WheelSuspensionParameters 2 | extends Resource 3 | 4 | @export var tire_model: BaseTireModel 5 | @export var spring_length := 0.15 6 | @export var spring_stiffness := 45.0 7 | @export var bump := 6.0 8 | @export var rebound := 7.0 9 | @export var wheel_mass := 20.0 10 | @export var anti_roll := 1.0 11 | var ackermann := 0.15 12 | -------------------------------------------------------------------------------- /resources/tires/brush_default.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" load_steps=2 format=3] 2 | 3 | [ext_resource type="Script" path="res://scenes/vehicle/tire_models/brush_tire_model.gd" id="1"] 4 | 5 | [resource] 6 | resource_local_to_scene = true 7 | script = ExtResource( 1 ) 8 | tire_softness = 1.0 9 | tire_width = 0.25 10 | tire_radius = 0.3 11 | brush_contact_patch = 0.2 12 | -------------------------------------------------------------------------------- /sounds/engine_sample.wav.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="wav" 4 | type="AudioStreamWAV" 5 | uid="uid://cntdncgh30pxj" 6 | path="res://.godot/imported/engine_sample.wav-b0152c4bdea723efc5cd91cce587b3de.sample" 7 | 8 | [deps] 9 | 10 | source_file="res://sounds/engine_sample.wav" 11 | dest_files=["res://.godot/imported/engine_sample.wav-b0152c4bdea723efc5cd91cce587b3de.sample"] 12 | 13 | [params] 14 | 15 | force/8_bit=false 16 | force/mono=false 17 | force/max_rate=false 18 | force/max_rate_hz=44100 19 | edit/trim=false 20 | edit/normalize=false 21 | edit/loop_mode=0 22 | edit/loop_begin=0 23 | edit/loop_end=-1 24 | compress/mode=0 25 | -------------------------------------------------------------------------------- /scenes/vehicle/drivetrain_params.gd: -------------------------------------------------------------------------------- 1 | class_name DriveTrainParameters 2 | extends Resource 3 | 4 | 5 | enum DRIVE_TYPE{ 6 | FWD, 7 | RWD, 8 | AWD, 9 | } 10 | 11 | @export var drivetype := DRIVE_TYPE.RWD 12 | @export var gear_ratios := [ 3.1, 2.61, 2.1, 1.72, 1.2, 1.0 ] 13 | @export var final_drive := 3.7 14 | @export var reverse_ratio := 3.9 15 | @export var gear_inertia := 0.10 16 | @export var automatic := true 17 | 18 | @export var rear_diff: DiffParameters 19 | @export var front_diff: DiffParameters 20 | @export var center_diff: DiffParameters 21 | 22 | 23 | @export var center_split_fr := 0.4 # AWD torque split front / rear, unused if central diff is not limited slip 24 | -------------------------------------------------------------------------------- /scenes/gui/gui.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | 4 | 5 | @onready var car = get_parent() 6 | @onready var speedlabel = $Essentials/VBoxContainer/Speedlabel 7 | @onready var gearlabel = $Essentials/VBoxContainer/GearLabel 8 | @onready var rpmlabel = $Essentials/VBoxContainer/RpmLabel 9 | @onready var fuellabel = $Essentials/VBoxContainer/FuelLabel 10 | 11 | 12 | # Called every frame. 'delta' is the elapsed time since the previous frame. 13 | func _process(delta: float) -> void: 14 | speedlabel.text = "Speed = %d" % int(car.speedo) 15 | gearlabel.text = "gear = %d" % car.drivetrain.selected_gear 16 | rpmlabel.text = "RPM = %d" % int(car.rpm) 17 | fuellabel.text = "Fuel = %3.2f" % car.fuel 18 | -------------------------------------------------------------------------------- /scenes/gui/render_stats.gd: -------------------------------------------------------------------------------- 1 | class_name RenderStats 2 | extends Control 3 | 4 | 5 | func _process(delta: float) -> void: 6 | update_stats() 7 | 8 | 9 | func update_stats(): 10 | $Panel/VBoxContainer/FPSLabel.text = "FPS = %3.2f" % Performance.get_monitor(Performance.TIME_FPS) 11 | $Panel/VBoxContainer/StaticMemoryLabel.text = "Static Memory = %4.2f" % (Performance.get_monitor(Performance.MEMORY_STATIC) / 1000000.0) 12 | $Panel/VBoxContainer/VideoMemoryLabel.text = "Video Memory = %4.2f" % (Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED) / 1000000.0) 13 | $Panel/VBoxContainer/DrawCallsLabel.text = "Draw Calls = %d" % int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)) 14 | 15 | -------------------------------------------------------------------------------- /models/kenney_carkit1/License.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Car Kit (1.2) 4 | 5 | Created/distributed by Kenney (www.kenney.nl) 6 | Creation date: 19-02-2020 12:55 7 | 8 | ------------------------------ 9 | 10 | License: (Creative Commons Zero, CC0) 11 | http://creativecommons.org/publicdomain/zero/1.0/ 12 | 13 | This content is free to use in personal, educational and commercial projects. 14 | Support us by crediting Kenney or www.kenney.nl (this is not mandatory) 15 | 16 | ------------------------------ 17 | 18 | Donate: http://support.kenney.nl 19 | Request: http://request.kenney.nl 20 | Patreon: http://patreon.com/kenney/ 21 | 22 | Follow on Twitter for updates: 23 | http://twitter.com/KenneyNL -------------------------------------------------------------------------------- /scenes/vehicle/clutch.gd: -------------------------------------------------------------------------------- 1 | class_name Clutch 2 | extends Node 3 | 4 | @export var friction := 250.0 5 | 6 | var locked := true 7 | var prev_av := 0.0 8 | 9 | func get_reaction_torques(av1: float, av2: float, t1: float, t2: float, slip_torque: float, kick := 0.0): 10 | var clutch_torque := friction + kick 11 | var delta_torque := t1 - t2 12 | var delta_av := av1 - av2 13 | var reaction_torques := Vector2.ZERO 14 | 15 | # Locked situations are handled in car and drivetrain scripts atm 16 | if locked: 17 | if absf(delta_torque) >= slip_torque: 18 | locked = false 19 | else: 20 | if absf(delta_av) < 0.5: 21 | locked = true 22 | 23 | if av1 < av2: 24 | reaction_torques.x = -clutch_torque 25 | reaction_torques.y = clutch_torque 26 | else: 27 | reaction_torques.x = clutch_torque 28 | reaction_torques.y = -clutch_torque 29 | return reaction_torques 30 | 31 | -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://ld0g76ta0njn" 6 | path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.png" 14 | dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /models/sport_car.glb.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="scene" 4 | importer_version=1 5 | type="PackedScene" 6 | uid="uid://vls3meliu3jk" 7 | path="res://.godot/imported/sport_car.glb-29122e97902e58e72b4bbb8c09a9c60e.scn" 8 | 9 | [deps] 10 | 11 | source_file="res://models/sport_car.glb" 12 | dest_files=["res://.godot/imported/sport_car.glb-29122e97902e58e72b4bbb8c09a9c60e.scn"] 13 | 14 | [params] 15 | 16 | nodes/root_type="Node3D" 17 | nodes/root_name="Scene Root" 18 | nodes/apply_root_scale=true 19 | nodes/root_scale=1.0 20 | meshes/ensure_tangents=true 21 | meshes/generate_lods=true 22 | meshes/create_shadow_meshes=true 23 | meshes/light_baking=1 24 | meshes/lightmap_texel_size=0.2 25 | skins/use_named_skins=true 26 | animation/import=true 27 | animation/fps=30 28 | animation/trimming=false 29 | animation/remove_immutable_tracks=true 30 | import_script/path="" 31 | _subresources={} 32 | gltf/embedded_image_handling=1 33 | -------------------------------------------------------------------------------- /scenes/gui/input_app.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | 4 | # Declare member variables here. Examples: 5 | # var a: int = 2 6 | # var b: String = "text" 7 | 8 | 9 | # Called when the node enters the scene tree for the first time. 10 | func _ready() -> void: 11 | pass # Replace with function body. 12 | 13 | 14 | # Called every frame. 'delta' is the elapsed time since the previous frame. 15 | func _process(delta: float) -> void: 16 | $Panel/VBoxContainer/ThrottleInput.value = Input.get_action_strength("Throttle") * 100 17 | $Panel/VBoxContainer/BrakeInput.value = Input.get_action_strength("Brake") * 100 18 | $Panel/VBoxContainer/ClutchInput.value = Input.get_action_strength("Clutch") * 100 19 | $Panel/VBoxContainer/SteeringInput.value = (Input.get_action_strength("SteerRight") - Input.get_action_strength("SteerLeft")) * 100 20 | 21 | #print((Input.get_action_strength("SteerRight") - Input.get_action_strength("SteerLeft")) * 100) 22 | 23 | 24 | -------------------------------------------------------------------------------- /models/kenney_carkit1/sedanSports.glb.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="scene" 4 | importer_version=1 5 | type="PackedScene" 6 | uid="uid://cakhmlww2amtf" 7 | path="res://.godot/imported/sedanSports.glb-b51bc01146d3d5b3675a97877dfd4ded.scn" 8 | 9 | [deps] 10 | 11 | source_file="res://models/kenney_carkit1/sedanSports.glb" 12 | dest_files=["res://.godot/imported/sedanSports.glb-b51bc01146d3d5b3675a97877dfd4ded.scn"] 13 | 14 | [params] 15 | 16 | nodes/root_type="Node3D" 17 | nodes/root_name="Scene Root" 18 | nodes/apply_root_scale=true 19 | nodes/root_scale=1.0 20 | meshes/ensure_tangents=true 21 | meshes/generate_lods=true 22 | meshes/create_shadow_meshes=true 23 | meshes/light_baking=0 24 | meshes/lightmap_texel_size=0.1 25 | skins/use_named_skins=true 26 | animation/import=true 27 | animation/fps=15 28 | animation/trimming=false 29 | animation/remove_immutable_tracks=true 30 | import_script/path="" 31 | _subresources={} 32 | gltf/embedded_image_handling=1 33 | -------------------------------------------------------------------------------- /models/kenney_carkit1/wheelDefault.glb.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="scene" 4 | importer_version=1 5 | type="PackedScene" 6 | uid="uid://bwmpep5o2d7fr" 7 | path="res://.godot/imported/wheelDefault.glb-ff8a840e37cf41ed5f927dc3ac728d61.scn" 8 | 9 | [deps] 10 | 11 | source_file="res://models/kenney_carkit1/wheelDefault.glb" 12 | dest_files=["res://.godot/imported/wheelDefault.glb-ff8a840e37cf41ed5f927dc3ac728d61.scn"] 13 | 14 | [params] 15 | 16 | nodes/root_type="Node3D" 17 | nodes/root_name="Scene Root" 18 | nodes/apply_root_scale=true 19 | nodes/root_scale=1.0 20 | meshes/ensure_tangents=true 21 | meshes/generate_lods=true 22 | meshes/create_shadow_meshes=true 23 | meshes/light_baking=0 24 | meshes/lightmap_texel_size=0.1 25 | skins/use_named_skins=true 26 | animation/import=true 27 | animation/fps=15 28 | animation/trimming=false 29 | animation/remove_immutable_tracks=true 30 | import_script/path="" 31 | _subresources={} 32 | gltf/embedded_image_handling=1 33 | -------------------------------------------------------------------------------- /models/sport_car_colorpalette8x8.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://xcvloq7plggh" 6 | path.s3tc="res://.godot/imported/sport_car_colorpalette8x8.png-558b89eee826b210db7948516179bc55.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | generator_parameters={} 12 | 13 | [deps] 14 | 15 | source_file="res://models/sport_car_colorpalette8x8.png" 16 | dest_files=["res://.godot/imported/sport_car_colorpalette8x8.png-558b89eee826b210db7948516179bc55.s3tc.ctex"] 17 | 18 | [params] 19 | 20 | compress/mode=2 21 | compress/high_quality=false 22 | compress/lossy_quality=0.7 23 | compress/hdr_compression=1 24 | compress/normal_map=0 25 | compress/channel_pack=0 26 | mipmaps/generate=true 27 | mipmaps/limit=-1 28 | roughness/mode=0 29 | roughness/src_normal="" 30 | process/fix_alpha_border=true 31 | process/premult_alpha=false 32 | process/normal_map_invert_y=false 33 | process/hdr_as_srgb=false 34 | process/hdr_clamp_exposure=false 35 | process/size_limit=0 36 | detect_3d/compress_to=0 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dechode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scenes/gui/render_stats.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://25r3bi1ijyc7"] 2 | 3 | [ext_resource type="Script" path="res://scenes/gui/render_stats.gd" id="1_qfdjj"] 4 | 5 | [node name="RenderStats" type="Control"] 6 | custom_minimum_size = Vector2(200, 150) 7 | layout_mode = 3 8 | anchors_preset = 8 9 | anchor_left = 0.5 10 | anchor_top = 0.5 11 | anchor_right = 0.5 12 | anchor_bottom = 0.5 13 | grow_horizontal = 2 14 | grow_vertical = 2 15 | size_flags_horizontal = 4 16 | script = ExtResource("1_qfdjj") 17 | 18 | [node name="Panel" type="Panel" parent="."] 19 | layout_mode = 1 20 | anchors_preset = 15 21 | anchor_right = 1.0 22 | anchor_bottom = 1.0 23 | grow_horizontal = 2 24 | grow_vertical = 2 25 | 26 | [node name="VBoxContainer" type="VBoxContainer" parent="Panel"] 27 | layout_mode = 1 28 | anchors_preset = 15 29 | anchor_right = 1.0 30 | anchor_bottom = 1.0 31 | grow_horizontal = 2 32 | grow_vertical = 2 33 | 34 | [node name="FPSLabel" type="Label" parent="Panel/VBoxContainer"] 35 | layout_mode = 2 36 | text = "FPS = 144" 37 | 38 | [node name="StaticMemoryLabel" type="Label" parent="Panel/VBoxContainer"] 39 | layout_mode = 2 40 | text = "Mem = 0 Mb" 41 | 42 | [node name="VideoMemoryLabel" type="Label" parent="Panel/VBoxContainer"] 43 | layout_mode = 2 44 | text = "Mem = 0 Mb" 45 | 46 | [node name="DrawCallsLabel" type="Label" parent="Panel/VBoxContainer"] 47 | layout_mode = 2 48 | text = "Draw Calls = 0 " 49 | -------------------------------------------------------------------------------- /scenes/vehicle/car_params.gd: -------------------------------------------------------------------------------- 1 | class_name CarParameters 2 | extends Resource 3 | 4 | 5 | enum DIFF_TYPE{ 6 | LIMITED_SLIP, 7 | OPEN_DIFF, 8 | LOCKED, 9 | } 10 | 11 | enum DRIVE_TYPE{ 12 | FWD, 13 | RWD, 14 | AWD, 15 | } 16 | 17 | @export var max_steer := 0.3 18 | @export var ackermann := 0.15 19 | @export var front_brake_bias := 0.6 20 | @export var steer_speed := 5.0 21 | @export var max_brake_force := 20000.0 22 | @export var max_handbrake_torque := 3000.0 23 | @export var brake_effective_radius := 0.25 24 | 25 | @export var fuel_tank_size := 40.0 #Liters 26 | @export var fuel_percentage := 100.0 # % of full tank 27 | 28 | ######### Engine variables ######### 29 | @export var max_torque := 250.0 30 | @export var max_engine_rpm := 8000.0 31 | @export var rpm_idle := 900.0 32 | @export var torque_curve := Curve.new() 33 | @export var throttle_model := Curve.new() 34 | @export var engine_brake := 10.0 35 | @export var engine_moment := 0.25 36 | @export var engine_bsfc := 0.3 37 | @export var engine_sound: AudioStream 38 | @export var clutch_friction := 500.0 39 | 40 | ######### Drivetrain variables ######### 41 | @export var drivetrain_params: DriveTrainParameters 42 | 43 | ######### Aero ######### 44 | @export var cd = 0.3 45 | @export var air_density = 1.225 46 | @export var frontal_area = 2.0 47 | 48 | @export var wheel_params_fl: WheelSuspensionParameters 49 | @export var wheel_params_fr: WheelSuspensionParameters 50 | @export var wheel_params_bl: WheelSuspensionParameters 51 | @export var wheel_params_br: WheelSuspensionParameters 52 | -------------------------------------------------------------------------------- /scenes/vehicle/tire_models/pacejka_tire_model.gd: -------------------------------------------------------------------------------- 1 | class_name PacejkaTireModel 2 | extends BaseTireModel 3 | 4 | @export var pacejka_b := 10.0 5 | @export var pacejka_c_lat := 1.35 6 | @export var pacejka_c_long := 1.65 7 | @export var pacejka_d := 1.0 8 | @export var pacejka_e := 0.0 9 | 10 | 11 | func pacejka(slip, B, C, D, E, normal_load): 12 | return normal_load * D * sin(C * atan(B * slip - E * (B * slip - atan(B * slip)))) 13 | 14 | 15 | func update_tire_forces(slip: Vector2, normal_load: float, surface_mu: float): 16 | var temp_mu := TIRE_TEMP_MU.sample_baked(tire_temp / max_tire_temp) 17 | var wear_mu := TIRE_WEAR_CURVE.sample_baked(tire_wear) 18 | load_sensitivity = update_load_sensitivity(normal_load) 19 | var mu := surface_mu * load_sensitivity * wear_mu * temp_mu 20 | 21 | var peak_sa := pacejka_b / 20.0 * 0.5 22 | var peak_sr := peak_sa * 0.7 23 | 24 | var normalised_sr = slip.y / peak_sr 25 | var normalised_sa = slip.x / peak_sa 26 | var resultant_slip = sqrt(pow(normalised_sr, 2) + pow(normalised_sa, 2)) 27 | # 28 | var sr_modified = resultant_slip * peak_sr 29 | var sa_modified = resultant_slip * peak_sa 30 | 31 | var force_vec := Vector3.ZERO 32 | 33 | force_vec.x = pacejka(abs(sa_modified), pacejka_b, pacejka_c_lat, pacejka_d, pacejka_e, normal_load) * sign(slip.x) 34 | force_vec.y = pacejka(abs(sr_modified), pacejka_b, pacejka_c_long, pacejka_d, pacejka_e, normal_load) * sign(slip.y) 35 | force_vec.z = pacejka(slip.x, pacejka_b, 2.0, 0.1 * pacejka_e, -20, normal_load) # 36 | 37 | force_vec *= mu 38 | 39 | if resultant_slip != 0: 40 | force_vec.x = force_vec.x * abs(normalised_sa / resultant_slip) 41 | force_vec.y = force_vec.y * abs(normalised_sr / resultant_slip) 42 | return force_vec 43 | -------------------------------------------------------------------------------- /scenes/vehicle/tire_models/brush_tire_model.gd: -------------------------------------------------------------------------------- 1 | class_name BrushTireModel 2 | extends BaseTireModel 3 | 4 | @export_range(0.0, 1.0) var tire_stiffness := 0.5 5 | @export var contact_patch := 0.2 6 | 7 | 8 | func update_tire_forces(slip: Vector2, normal_load: float, surface_mu: float = 1.0) -> Vector3: 9 | var stiffness = 1000000 + 8000000 * tire_stiffness 10 | var cornering_stiffness = 0.5 * stiffness * pow(contact_patch, 2) 11 | 12 | var wear_mu = TIRE_WEAR_CURVE.sample_baked(tire_wear) 13 | var temp_mu := TIRE_TEMP_MU.sample_baked(tire_temp / max_tire_temp) 14 | load_sensitivity = update_load_sensitivity(normal_load) 15 | var mu = surface_mu * load_sensitivity * wear_mu * temp_mu 16 | var friction = mu * normal_load 17 | 18 | var critical_slip = friction / (2 * cornering_stiffness) 19 | var critical_length = 0 20 | if slip.x: 21 | critical_length = friction / (stiffness * contact_patch * tan(abs(slip.x))) 22 | 23 | var force_vector := Vector3.ZERO 24 | 25 | # Self aligning moment 26 | if critical_length >= contact_patch: # Adhesion region 27 | force_vector.z = cornering_stiffness * contact_patch * slip.x / 6 28 | else: # Sliding region 29 | if slip.x: 30 | var idk = (mu * pow(normal_load, 2) / (12 * contact_patch * stiffness * tan(slip.x))) 31 | force_vector.z = idk * (3 - ((2 * friction) / (pow(contact_patch, 2) * stiffness * tan(slip.x)))) 32 | 33 | var deflect = sqrt(pow(cornering_stiffness * slip.y, 2) + pow(cornering_stiffness * tan(slip.x), 2)) 34 | if deflect == 0: 35 | return Vector3.ZERO 36 | 37 | if deflect <= 0.5 * friction * (1 - slip.y): 38 | force_vector.y = cornering_stiffness * -slip.y / (1 - slip.y) 39 | force_vector.x = cornering_stiffness * tan(slip.x) / (1 - slip.y) 40 | else: 41 | var brushy = (1 - friction * (1 - slip.y) / (4 * deflect)) / deflect 42 | force_vector.y = friction * cornering_stiffness * slip.y * brushy 43 | force_vector.x = friction * cornering_stiffness * tan(slip.x) * brushy 44 | return force_vector 45 | -------------------------------------------------------------------------------- /scenes/vehicle/tire_models/base_tire_model.gd: -------------------------------------------------------------------------------- 1 | class_name BaseTireModel 2 | extends Resource 3 | 4 | const TIRE_WEAR_CURVE = preload("res://resources/tire_wear_curve.tres") 5 | const TIRE_TEMP_MU = preload("res://scenes/vehicle/tire_models/temp_mu.tres") 6 | 7 | @export var tire_radius := 0.3 8 | @export var tire_width := 0.205 9 | @export var tire_rated_load := 5500.0 10 | @export var load_sens0 := 1.7 11 | @export var load_sens1 := 0.9 12 | @export var max_tire_temp := 150.0 13 | @export var opt_tire_temp := 90.0 14 | @export var heating_multiplier := 0.0015 15 | @export var cooling_multiplier := -0.25 16 | 17 | var tire_wear := 0.0 18 | var load_sensitivity := 1.0 19 | var tire_temp := 20.0 20 | 21 | 22 | func _init() -> void: 23 | var point_pos := TIRE_TEMP_MU.get_point_position(1) 24 | var new_point_offset := opt_tire_temp / max_tire_temp 25 | TIRE_TEMP_MU.set_point_offset(1, new_point_offset) 26 | 27 | 28 | # Override this 29 | func update_tire_forces(_slip: Vector2, _normal_load: float, _surface_mu: float) -> Vector3: 30 | return Vector3.ZERO 31 | 32 | 33 | func update_tire_wear(delta: float, slip: Vector2, normal_load: float, mu: float): 34 | tire_wear += slip.length() * mu * delta * normal_load / 7000000.0 35 | tire_wear = clampf(tire_wear, 0 ,1) 36 | return tire_wear 37 | 38 | 39 | func update_load_sensitivity(normal_load: float) -> float: 40 | var load_factor := normal_load / tire_rated_load 41 | load_sensitivity = clampf(lerpf(load_sens0, load_sens1, load_factor), 0.2, load_sens0) 42 | 43 | return load_sensitivity 44 | 45 | 46 | func update_tire_temp(slip, normal_load, speed, mu, ambient_temp, delta): 47 | var delta_temp := 0.0 48 | # Heating 49 | if abs(speed) > 1.0: 50 | delta_temp += slip.length() * normal_load * mu * heating_multiplier * delta 51 | # Cooling 52 | var cooling: float = cooling_multiplier * (tire_temp / ambient_temp) 53 | delta_temp += cooling * delta 54 | # Clamp the temps 55 | var max_delta_temp_per_frame := 0.1 56 | delta_temp = clamp(delta_temp, -max_delta_temp_per_frame, max_delta_temp_per_frame) 57 | tire_temp = clampf(tire_temp + delta_temp, ambient_temp, max_tire_temp) 58 | return tire_temp 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot Advanced Vehicle 2 | A more advanced car controller for the Godot game engine. 3 | 4 | ## Description 5 | Custom rigidbody car controller with raycast suspension for the Godot game engine. This one is more realistic than the built-in vehiclebody with wheelcolliders. 6 | This project will be a small demo of the vehicle physics i have been able to craft so far. 7 | 8 | Features: 9 | - RWD, FWD and AWD drivetypes available 10 | - Pacejka and brush tire model available. 11 | - Tire wear 12 | - Fuel consumption using BSFC 13 | - Choose between preloaded limited slip diff, open diff and locked diff/solid axle 14 | - Manual clutch with adjustable clutch friction force 15 | - Manual and automatic gearbox 16 | 17 | This project would not have been possible without Wolfes written tutorial of his own car simulator physics. Also huge thank you to Bastiaan Olij for his vehicle demo. See the links in the Acknowledments section for more info. 18 | 19 | ## Controls 20 | Keyboard: 21 | - Arrow keys for throttle, brake and steering 22 | - Space for handbrake 23 | - A for upshifting and Z for downshifting 24 | - c for clutch 25 | 26 | Xbox controller: 27 | - R2 for throttle and L2 for braking 28 | - Left analog stick for steering 29 | - A button for upshifting and X button for downshifting 30 | - B button for handbrake 31 | - LB for clutch 32 | 33 | ## Help 34 | Make sure the physics FPS is set to atleast 120 or the physics start to get weird. In this project it is set to 240. 35 | 36 | ## License 37 | This project is licensed under the MIT License - see the LICENSE.md file for details. This project also contains models and textures owned by their authors. See links below for exact licenses. 38 | 39 | Engine sound sample found in /sounds folder is made with enginesound, available from https://github.com/DasEtwas/enginesound. The sound sample itself is licensed under cc0. 40 | 41 | ## Acknowledgments 42 | * [Kenney car kit](https://www.kenney.nl/assets/car-kit) 43 | * [Bastiaan Olij - Vehicle demo](https://github.com/BastiaanOlij/vehicle-demo/) 44 | * [Wolfe, written tutorial of his GDSim vehicle physics](https://www.gtplanet.net/forum/threads/gdsim-v0-4a-autocross-and-custom-setups.396400/) 45 | * [Racer.nl, Alot of great documentation about physics of racing sims](http://www.racer.nl/) 46 | -------------------------------------------------------------------------------- /scenes/vehicle/tire_models/curve_tire_model.gd: -------------------------------------------------------------------------------- 1 | class_name CurveTireModel 2 | extends BaseTireModel 3 | 4 | const buildup = preload("res://scenes/vehicle/tire_models/buildup_curve.tres") 5 | const falloff = preload("res://scenes/vehicle/tire_models/falloff_curve.tres") 6 | 7 | const MIN_PEAK_SA_START_DEG = 3.0 8 | const MAX_PEAK_SA_START_DEG = 12.0 9 | 10 | const MIN_DELTA_SA_DEG = 1.0 11 | const MAX_DELTA_SA_DEG = 4.0 12 | 13 | @export_range(0.0, 1.0) var tire_stiffness := 0.5 14 | 15 | 16 | func update_tire_forces(slip: Vector2, normal_load: float, surface_mu: float = 1.0) -> Vector3: 17 | var wear_mu := TIRE_WEAR_CURVE.sample_baked(tire_wear) 18 | var temp_mu := TIRE_TEMP_MU.sample_baked(tire_temp / max_tire_temp) 19 | load_sensitivity = update_load_sensitivity(normal_load) 20 | var mu := surface_mu * wear_mu * temp_mu * load_sensitivity 21 | # var mu := ((surface_mu * wear_mu * temp_mu) + load_sensitivity) * 0.5 22 | 23 | var load_factor := normal_load / tire_rated_load 24 | var peak_sa_deg: float = lerp(MAX_PEAK_SA_START_DEG, MIN_PEAK_SA_START_DEG, tire_stiffness) 25 | var delta_sa_deg: float = lerp(MAX_DELTA_SA_DEG, MIN_DELTA_SA_DEG, tire_stiffness) 26 | 27 | var sa0 := peak_sa_deg + 0.5 * delta_sa_deg 28 | var sa1 := peak_sa_deg - 0.5 * delta_sa_deg 29 | var peak_sa := deg_to_rad(lerp(sa1, sa0, load_factor)) 30 | 31 | var peak_sr_ratio := 0.65 32 | var peak_sr := peak_sa * peak_sr_ratio 33 | 34 | var normalised_sr := slip.y / peak_sr 35 | var normalised_sa := slip.x / peak_sa 36 | var resultant_slip := sqrt(pow(normalised_sr, 2) + pow(normalised_sa, 2)) 37 | # 38 | var sr_modified := resultant_slip * peak_sr 39 | var sa_modified := resultant_slip * peak_sa 40 | 41 | var tire_forces := Vector3.ZERO 42 | 43 | if abs(slip.x) < peak_sa: 44 | tire_forces.x = buildup.sample_baked(resultant_slip) * sign(slip.x) 45 | else: 46 | tire_forces.x = falloff.sample_baked(sa_modified - peak_sa) * sign(slip.x) 47 | 48 | if abs(slip.y) < peak_sr: 49 | tire_forces.y = buildup.sample_baked(resultant_slip) * sign(slip.y) 50 | else: 51 | tire_forces.y = falloff.sample_baked(sr_modified - peak_sr) * sign(slip.y) 52 | 53 | tire_forces *= mu * normal_load 54 | 55 | if resultant_slip != 0: 56 | tire_forces.x *= abs(normalised_sa / resultant_slip) 57 | tire_forces.y *= abs(normalised_sr / resultant_slip) 58 | 59 | return tire_forces 60 | -------------------------------------------------------------------------------- /scenes/camera.gd: -------------------------------------------------------------------------------- 1 | extends Camera3D 2 | 3 | # This script is taken from Bastiaan Olijs vehicle demo, available at https://github.com/BastiaanOlij/vehicle-demo 4 | 5 | #MIT License 6 | # 7 | #Copyright (c) 2018 Bastiaan Olij 8 | # 9 | #Permission is hereby granted, free of charge, to any person obtaining a copy 10 | #of this software and associated documentation files (the "Software"), to deal 11 | #in the Software without restriction, including without limitation the rights 12 | #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | #copies of the Software, and to permit persons to whom the Software is 14 | #furnished to do so, subject to the following conditions: 15 | # 16 | #The above copyright notice and this permission notice shall be included in all 17 | #copies or substantial portions of the Software. 18 | # 19 | #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | #SOFTWARE. 26 | 27 | 28 | @export var follow_this_path: NodePath 29 | 30 | @export var target_distance := 3.0 31 | @export var target_height := 1.0 32 | @export var lerp_speed := 20.0 33 | 34 | var follow_this = null 35 | var last_lookat = null 36 | 37 | 38 | func _ready(): 39 | follow_this = get_node(follow_this_path) 40 | last_lookat = follow_this.global_transform.origin 41 | set_as_top_level(true) 42 | 43 | 44 | func _physics_process(delta): 45 | set_as_top_level(true) 46 | var delta_v = global_transform.origin - follow_this.global_transform.origin 47 | var target_pos = global_transform.origin 48 | 49 | # ignore y 50 | delta_v.y = 0.0 51 | 52 | if (delta_v.length() > target_distance): 53 | delta_v = delta_v.normalized() * target_distance 54 | delta_v.y = target_height 55 | target_pos = follow_this.global_transform.origin + delta_v 56 | else: 57 | target_pos.y = follow_this.global_transform.origin.y + target_height 58 | 59 | global_transform.origin = global_transform.origin.lerp(target_pos, delta * lerp_speed) 60 | last_lookat = last_lookat.lerp(follow_this.global_transform.origin, delta * lerp_speed) 61 | 62 | look_at(last_lookat, Vector3(0.0, 1.0, 0.0)) 63 | -------------------------------------------------------------------------------- /scenes/gui/input_app.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3] 2 | 3 | [ext_resource type="Script" path="res://scenes/gui/input_app.gd" id="1"] 4 | 5 | [sub_resource type="StyleBoxFlat" id=2] 6 | bg_color = Color( 0, 1, 0.0392157, 1 ) 7 | 8 | [sub_resource type="StyleBoxFlat" id=3] 9 | bg_color = Color( 1, 0, 0, 1 ) 10 | 11 | [sub_resource type="StyleBoxFlat" id=4] 12 | bg_color = Color( 0, 0.835294, 1, 1 ) 13 | 14 | [sub_resource type="StyleBoxFlat" id=5] 15 | bg_color = Color( 0.756863, 1, 0, 1 ) 16 | 17 | [node name="InputApp" type="Control"] 18 | anchor_left = 1.0 19 | anchor_top = 1.0 20 | anchor_right = 1.0 21 | anchor_bottom = 1.0 22 | offset_left = -275.0 23 | offset_top = -223.0 24 | script = ExtResource( 1 ) 25 | __meta__ = { 26 | "_edit_use_anchors_": false 27 | } 28 | 29 | [node name="Panel" type="Panel" parent="."] 30 | self_modulate = Color( 1, 1, 1, 0.556863 ) 31 | anchor_left = 1.0 32 | anchor_top = 1.0 33 | anchor_right = 1.0 34 | anchor_bottom = 1.0 35 | offset_left = -275.0 36 | offset_top = -150.0 37 | __meta__ = { 38 | "_edit_use_anchors_": false 39 | } 40 | 41 | [node name="VBoxContainer" type="VBoxContainer" parent="Panel"] 42 | anchor_left = 1.0 43 | anchor_top = 1.0 44 | anchor_right = 1.0 45 | anchor_bottom = 1.0 46 | offset_left = -275.0 47 | offset_top = -150.0 48 | custom_constants/separation = 10 49 | __meta__ = { 50 | "_edit_use_anchors_": false 51 | } 52 | 53 | [node name="ThrottleInput" type="ProgressBar" parent="Panel/VBoxContainer"] 54 | offset_right = 275.0 55 | offset_bottom = 20.0 56 | custom_minimum_size = Vector2( 200, 20 ) 57 | custom_styles/fg = SubResource( 2 ) 58 | value = 50.0 59 | percent_visible = false 60 | 61 | [node name="BrakeInput" type="ProgressBar" parent="Panel/VBoxContainer"] 62 | offset_top = 30.0 63 | offset_right = 275.0 64 | offset_bottom = 50.0 65 | custom_minimum_size = Vector2( 200, 20 ) 66 | custom_styles/fg = SubResource( 3 ) 67 | value = 50.0 68 | percent_visible = false 69 | 70 | [node name="ClutchInput" type="ProgressBar" parent="Panel/VBoxContainer"] 71 | offset_top = 60.0 72 | offset_right = 275.0 73 | offset_bottom = 80.0 74 | custom_minimum_size = Vector2( 200, 20 ) 75 | custom_styles/fg = SubResource( 4 ) 76 | value = 50.0 77 | percent_visible = false 78 | 79 | [node name="SteeringInput" type="ProgressBar" parent="Panel/VBoxContainer"] 80 | offset_top = 90.0 81 | offset_right = 275.0 82 | offset_bottom = 110.0 83 | custom_minimum_size = Vector2( 200, 20 ) 84 | custom_styles/fg = SubResource( 5 ) 85 | min_value = -100.0 86 | percent_visible = false 87 | -------------------------------------------------------------------------------- /scenes/track.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3] 2 | 3 | [ext_resource type="Script" path="res://scenes/road.gd" id="1"] 4 | [ext_resource type="Material" path="res://materials/tarmac.tres" id="2"] 5 | [ext_resource type="Material" path="res://materials/gravel.tres" id="3"] 6 | 7 | [sub_resource type="Curve3D" id=1] 8 | _data = { 9 | "points": PackedVector3Array( -34.7, 0, -56.2, 34.7, 0, 56.2, 47.1, 0.1, 87.6, -28.87, 0, 15.53, 28.87, 0, -15.53, 142.571, 0.072282, 141.668, -13.3, 0, 47.4, 13.3, 0, -47.4, 201.7, 0, 59, 22.98, 0, 24.39, -22.98, 0, -24.39, 181.1, 0, -83.9, 16.05, 0, 11.4, -16.05, 0, -11.4, 109.4, 0, -95.3, 11.76, 0, 11.87, -11.76, 0, -11.87, 91.4, -0.1, -155.5, 50.2, 0, -42.5, -50.2, 0, 42.5, -176.1, -0.1, -139.5, -5.6, 0, -43.2, 5.6, 0, 43.2, -207.5, 0, 0.7, -16.32, 0, -26.43, 16.32, 0, 26.43, -139.871, 0.0468903, 76.3188, -21.53, 0, -18.01, 21.53, 0, 18.01, -100.442, 0.143051, 193.109, -26.55, 0, 37.35, 26.55, 0, -37.35, -17.4581, 0.141373, 180.703, 7.2, 0, 19.3, -7.2, 0, -19.3, -80.7745, -0.000213623, 9.30077, -16.47, -0.18, -11.69, 16.47, 0.18, 11.69, -32.8, 0.6, -30.9, 0, 0, 0, 0, 0, 0, 37.1024, -6.10948e-06, 71.8207 ), 10 | "tilts": PackedFloat32Array( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ) 11 | } 12 | 13 | [node name="Track" type="Node3D"] 14 | 15 | [node name="Path3D" type="Path3D" parent="."] 16 | curve = SubResource( 1 ) 17 | script = ExtResource( 1 ) 18 | track_width = 17.131 19 | lower_ground_width = 13.498 20 | 21 | [node name="Road" type="CSGPolygon3D" parent="Path3D" groups=["Tarmac"]] 22 | use_collision = true 23 | polygon = PackedVector2Array( -8.5655, 0, -8.5655, -0.1, 8.5655, -0.1, 8.5655, 0 ) 24 | mode = 2 25 | path_node = NodePath("..") 26 | path_interval_type = 0 27 | path_interval = 10.0 28 | path_simplify_angle = 0.0 29 | path_rotation = 2 30 | path_local = false 31 | path_continuous_u = false 32 | path_u_distance = 0.0 33 | path_joined = true 34 | smooth_faces = true 35 | material = ExtResource( 2 ) 36 | 37 | [node name="Ground" type="CSGPolygon3D" parent="Path3D" groups=["Gravel"]] 38 | use_collision = true 39 | polygon = PackedVector2Array( -10.5655, -0.1, 10.5655, -0.1, 13.498, -4.01, 13.598, -4.1, -13.598, -4.1, -13.498, -4 ) 40 | mode = 2 41 | path_node = NodePath("..") 42 | path_interval_type = 0 43 | path_interval = 7.0 44 | path_simplify_angle = 0.0 45 | path_rotation = 2 46 | path_local = false 47 | path_continuous_u = false 48 | path_u_distance = 1.0 49 | path_joined = true 50 | smooth_faces = true 51 | material = ExtResource( 3 ) 52 | 53 | [connection signal="curve_changed" from="Path3D" to="Path3D" method="_on_Path_curve_changed"] 54 | -------------------------------------------------------------------------------- /scenes/gui/gui.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://djbt3ltge1aat"] 2 | 3 | [ext_resource type="Script" path="res://scenes/gui/gui.gd" id="1_bv3ap"] 4 | [ext_resource type="PackedScene" uid="uid://dacyugrkjb2o4" path="res://scenes/gui/tireinfo_app.tscn" id="2_6tyms"] 5 | [ext_resource type="PackedScene" path="res://scenes/gui/input_app.tscn" id="3_m5e8j"] 6 | [ext_resource type="PackedScene" uid="uid://25r3bi1ijyc7" path="res://scenes/gui/render_stats.tscn" id="4_4dmvl"] 7 | 8 | [node name="Gui" type="Control"] 9 | layout_mode = 3 10 | anchors_preset = 15 11 | anchor_right = 1.0 12 | anchor_bottom = 1.0 13 | grow_horizontal = 2 14 | grow_vertical = 2 15 | script = ExtResource("1_bv3ap") 16 | 17 | [node name="Essentials" type="Panel" parent="."] 18 | self_modulate = Color(1, 1, 1, 0.556863) 19 | custom_minimum_size = Vector2(200, 100) 20 | layout_mode = 1 21 | anchors_preset = 2 22 | anchor_top = 1.0 23 | anchor_bottom = 1.0 24 | offset_top = -141.0 25 | offset_right = 200.0 26 | grow_vertical = 0 27 | 28 | [node name="VBoxContainer" type="VBoxContainer" parent="Essentials"] 29 | layout_mode = 1 30 | anchors_preset = 15 31 | anchor_right = 1.0 32 | anchor_bottom = 1.0 33 | grow_horizontal = 2 34 | grow_vertical = 2 35 | 36 | [node name="GearLabel" type="Label" parent="Essentials/VBoxContainer"] 37 | layout_mode = 2 38 | text = "Gear" 39 | 40 | [node name="Speedlabel" type="Label" parent="Essentials/VBoxContainer"] 41 | layout_mode = 2 42 | text = "Speed" 43 | 44 | [node name="RpmLabel" type="Label" parent="Essentials/VBoxContainer"] 45 | layout_mode = 2 46 | text = "Rpm" 47 | 48 | [node name="FuelLabel" type="Label" parent="Essentials/VBoxContainer"] 49 | layout_mode = 2 50 | text = "Fuel" 51 | 52 | [node name="TireInfoApp" parent="." instance=ExtResource("2_6tyms")] 53 | anchors_preset = 0 54 | anchor_left = 0.0 55 | anchor_top = 0.0 56 | anchor_right = 0.0 57 | anchor_bottom = 0.0 58 | offset_left = 0.0 59 | offset_top = 0.0 60 | offset_right = 164.0 61 | offset_bottom = 242.0 62 | grow_horizontal = 1 63 | grow_vertical = 1 64 | wheel_fl_path = NodePath("../../Wheel_fl") 65 | wheel_fr_path = NodePath("../../Wheel_fr") 66 | wheel_bl_path = NodePath("../../Wheel_bl") 67 | wheel_br_path = NodePath("../../Wheel_br") 68 | 69 | [node name="InputApp" parent="." instance=ExtResource("3_m5e8j")] 70 | layout_mode = 1 71 | anchors_preset = 3 72 | offset_left = -283.0 73 | offset_top = -158.0 74 | grow_horizontal = 0 75 | grow_vertical = 0 76 | 77 | [node name="RenderStats" parent="." instance=ExtResource("4_4dmvl")] 78 | layout_mode = 1 79 | anchors_preset = 1 80 | anchor_left = 1.0 81 | anchor_top = 0.0 82 | anchor_right = 1.0 83 | anchor_bottom = 0.0 84 | offset_left = -200.0 85 | offset_bottom = 100.0 86 | grow_horizontal = 0 87 | grow_vertical = 1 88 | -------------------------------------------------------------------------------- /scenes/gui/tireinfo_app.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | 4 | @export var wheel_fl_path: NodePath 5 | @export var wheel_fr_path: NodePath 6 | @export var wheel_bl_path: NodePath 7 | @export var wheel_br_path: NodePath 8 | 9 | 10 | var wheel_fl 11 | var wheel_fr 12 | var wheel_bl 13 | var wheel_br 14 | 15 | var wheels_init: int = 0 16 | 17 | var wheels := [] 18 | 19 | func _ready() -> void: 20 | if wheel_fl_path != null: 21 | wheel_fl = get_node(wheel_fl_path) 22 | wheels_init += 1 23 | wheels.append(wheel_fl) 24 | 25 | if wheel_fr_path != null: 26 | wheel_fr = get_node(wheel_fr_path) 27 | wheels_init += 1 28 | wheels.append(wheel_fr) 29 | 30 | if wheel_bl_path != null: 31 | wheel_bl = get_node(wheel_bl_path) 32 | wheels_init += 1 33 | wheels.append(wheel_bl) 34 | 35 | if wheel_br_path != null: 36 | wheel_br = get_node(wheel_br_path) 37 | wheels_init += 1 38 | wheels.append(wheel_br) 39 | 40 | 41 | # Called every frame. 'delta' is the elapsed time since the previous frame. 42 | func _process(delta: float) -> void: 43 | if wheels_init != 4: 44 | return 45 | 46 | $Panel/VBoxContainer/HBoxContainer/WheelFL/TireWearLabel.text = "%3.1f %% " % float(wheel_fl.tire_wear * 100) 47 | $Panel/VBoxContainer/HBoxContainer/WheelFR/TireWearLabel.text = "%3.1f %% " % float(wheel_fr.tire_wear * 100) 48 | $Panel/VBoxContainer/HBoxContainer2/WheelBL/TireWearLabel.text ="%3.1f %% " % float(wheel_bl.tire_wear * 100) 49 | $Panel/VBoxContainer/HBoxContainer2/WheelBR/TireWearLabel.text ="%3.1f %% " % float(wheel_br.tire_wear * 100) 50 | 51 | var colors := [] 52 | for wheel in wheels: 53 | if wheel.tire_model.tire_temp < wheel.tire_model.opt_tire_temp: 54 | colors.append(lerp(Color.AQUA, Color.GREEN, wheel.tire_model.tire_temp / wheel.tire_model.opt_tire_temp)) 55 | else: 56 | var amount: float = (wheel.tire_model.tire_temp - wheel.tire_model.opt_tire_temp) / (wheel.tire_model.max_tire_temp - wheel.tire_model.opt_tire_temp) 57 | colors.append(lerp(Color.GREEN, Color.FIREBRICK, amount)) 58 | 59 | 60 | $Panel/VBoxContainer/HBoxContainer/WheelFL/TireTemp.color = colors[0] 61 | $Panel/VBoxContainer/HBoxContainer/WheelFR/TireTemp.color = colors[1] 62 | $Panel/VBoxContainer/HBoxContainer2/WheelBL/TireTemp.color = colors[2] 63 | $Panel/VBoxContainer/HBoxContainer2/WheelBR/TireTemp.color = colors[3] 64 | 65 | $Panel/VBoxContainer/HBoxContainer/WheelFL/TireTemp/Label.text = "%2.1f c" % wheel_fl.tire_model.tire_temp 66 | $Panel/VBoxContainer/HBoxContainer/WheelFR/TireTemp/Label.text = "%2.1f c" % wheel_fr.tire_model.tire_temp 67 | $Panel/VBoxContainer/HBoxContainer2/WheelBL/TireTemp/Label.text = "%2.1f c" % wheel_bl.tire_model.tire_temp 68 | $Panel/VBoxContainer/HBoxContainer2/WheelBR/TireTemp/Label.text = "%2.1f c" % wheel_br.tire_model.tire_temp 69 | 70 | -------------------------------------------------------------------------------- /scenes/vehicle/base_car.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=3 uid="uid://exyybiakl0yk"] 2 | 3 | [ext_resource type="Script" path="res://scenes/vehicle/base_car.gd" id="1_n8n57"] 4 | [ext_resource type="Script" path="res://scenes/camera.gd" id="3_5upga"] 5 | [ext_resource type="Script" path="res://scenes/vehicle/wheel_suspension.gd" id="4_6hvcn"] 6 | [ext_resource type="Material" path="res://materials/tire.tres" id="5_cuc4d"] 7 | [ext_resource type="PackedScene" uid="uid://djbt3ltge1aat" path="res://scenes/gui/gui.tscn" id="6_cp0hb"] 8 | 9 | [sub_resource type="CylinderMesh" id="10"] 10 | material = ExtResource("5_cuc4d") 11 | top_radius = 0.3 12 | bottom_radius = 0.3 13 | height = 0.2 14 | radial_segments = 16 15 | 16 | [node name="RigidBodyCar" type="RigidBody3D"] 17 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) 18 | collision_layer = 2 19 | mass = 700.0 20 | script = ExtResource("1_n8n57") 21 | 22 | [node name="Camera3D" type="Camera3D" parent="."] 23 | transform = Transform3D(1, 0, 0, 0, 0.965926, 0.258819, 0, -0.258819, 0.965926, 0, 1.6, 4.4) 24 | script = ExtResource("3_5upga") 25 | follow_this_path = NodePath("../CamTarget") 26 | 27 | [node name="Wheel_br" type="RayCast3D" parent="."] 28 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.8, 0, 1.5) 29 | script = ExtResource("4_6hvcn") 30 | 31 | [node name="MeshInstance3D" type="MeshInstance3D" parent="Wheel_br"] 32 | transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, 0, 0) 33 | mesh = SubResource("10") 34 | skeleton = NodePath("") 35 | 36 | [node name="Wheel_bl" type="RayCast3D" parent="."] 37 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.8, 0, 1.5) 38 | script = ExtResource("4_6hvcn") 39 | 40 | [node name="MeshInstance3D" type="MeshInstance3D" parent="Wheel_bl"] 41 | transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, 0, 0) 42 | mesh = SubResource("10") 43 | skeleton = NodePath("") 44 | 45 | [node name="Wheel_fr" type="RayCast3D" parent="."] 46 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.8, 0, -1.4) 47 | script = ExtResource("4_6hvcn") 48 | 49 | [node name="MeshInstance3D" type="MeshInstance3D" parent="Wheel_fr"] 50 | transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, 0, 0) 51 | mesh = SubResource("10") 52 | skeleton = NodePath("") 53 | 54 | [node name="Wheel_fl" type="RayCast3D" parent="."] 55 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.8, 0, -1.4) 56 | script = ExtResource("4_6hvcn") 57 | 58 | [node name="MeshInstance3D" type="MeshInstance3D" parent="Wheel_fl"] 59 | transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, 0, 0) 60 | mesh = SubResource("10") 61 | skeleton = NodePath("") 62 | 63 | [node name="EngineSound" type="AudioStreamPlayer" parent="."] 64 | 65 | [node name="CamTarget" type="Marker3D" parent="."] 66 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.17515, 0) 67 | 68 | [node name="Gui" parent="." instance=ExtResource("6_cp0hb")] 69 | 70 | [editable path="Gui"] 71 | -------------------------------------------------------------------------------- /scenes/road.gd: -------------------------------------------------------------------------------- 1 | ## Made by Bastiaan Olij. Available from: https://github.com/BastiaanOlij/vehicle-demo 2 | #MIT License 3 | # 4 | #Copyright (c) 2018 Bastiaan Olij 5 | # 6 | #Permission is hereby granted, free of charge, to any person obtaining a copy 7 | #of this software and associated documentation files (the "Software"), to deal 8 | #in the Software without restriction, including without limitation the rights 9 | #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | #copies of the Software, and to permit persons to whom the Software is 11 | #furnished to do so, subject to the following conditions: 12 | # 13 | #The above copyright notice and this permission notice shall be included in all 14 | #copies or substantial portions of the Software. 15 | # 16 | #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | #SOFTWARE. 23 | 24 | 25 | @tool 26 | extends Path3D 27 | 28 | @export var track_width = 8.0 : get = get_track_width, set = set_track_width 29 | @export var lower_ground_width = 12.0 : get = get_lower_ground_width, set = set_lower_ground_width 30 | 31 | var is_dirty = true 32 | 33 | 34 | # Called when the node enters the scene tree for the first time. 35 | func _ready(): 36 | call_deferred("_update") 37 | 38 | 39 | func set_track_width(new_width): 40 | if track_width != new_width: 41 | track_width = new_width 42 | is_dirty = true 43 | call_deferred("_update") 44 | 45 | func get_track_width(): 46 | return track_width 47 | 48 | 49 | func set_lower_ground_width(new_width): 50 | if lower_ground_width != new_width: 51 | lower_ground_width = new_width 52 | is_dirty = true 53 | call_deferred("_update") 54 | 55 | 56 | func get_lower_ground_width(): 57 | return lower_ground_width 58 | 59 | 60 | func _update(): 61 | if !is_dirty: 62 | return 63 | 64 | var track_half_width = track_width * 0.5 65 | 66 | var track = $Road.polygon 67 | track.set(0, Vector2(-track_half_width, 0.0)) 68 | track.set(1, Vector2(-track_half_width, -0.1)) 69 | track.set(2, Vector2( track_half_width, -0.1)) 70 | track.set(3, Vector2( track_half_width, 0.0)) 71 | $Road.polygon = track 72 | 73 | var ground = $Ground.polygon 74 | ground.set(1, Vector2( track_half_width + 2.0, -0.1)) 75 | ground.set(0, Vector2(-track_half_width - 2.0, -0.1)) 76 | ground.set(2, Vector2( lower_ground_width, -4.01)) 77 | ground.set(3, Vector2( lower_ground_width + 0.1, -4.1)) 78 | ground.set(4, Vector2(-lower_ground_width - 0.1, -4.1)) 79 | ground.set(5, Vector2(-lower_ground_width, -4.0)) 80 | $Ground.polygon = ground 81 | 82 | is_dirty = false 83 | 84 | 85 | func _on_Path_curve_changed(): 86 | is_dirty = true 87 | call_deferred("_update") 88 | -------------------------------------------------------------------------------- /resources/car_setups/kenney_sport_car.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="CarParameters" load_steps=11 format=3 uid="uid://cu4jo10x8b1se"] 2 | 3 | [ext_resource type="AudioStream" path="res://sounds/tres/engine_sample.tres" id="1_wqlgx"] 4 | [ext_resource type="Script" path="res://scenes/vehicle/car_params.gd" id="2_8ru0w"] 5 | [ext_resource type="Script" path="res://scenes/vehicle/wheel_params.gd" id="3_scbug"] 6 | [ext_resource type="Script" path="res://scenes/vehicle/tire_models/pacejka_tire_model.gd" id="4_ilwyn"] 7 | 8 | [sub_resource type="Curve" id="Curve_g3qlw"] 9 | _data = [Vector2(0, 0.463636), 0.0, 0.0, 0, 0, Vector2(0.4, 0.945454), 0.0, 0.0, 0, 0, Vector2(0.646753, 1), 0.0, 0.0, 0, 0, Vector2(1, 0.290909), -4.9, 0.0, 0, 0] 10 | point_count = 4 11 | 12 | [sub_resource type="Resource" id="Resource_kubsx"] 13 | script = ExtResource("4_ilwyn") 14 | tire_stiffness = 0.75 15 | 16 | [sub_resource type="Resource" id="Resource_wl1bc"] 17 | script = ExtResource("3_scbug") 18 | tire_model = SubResource("Resource_kubsx") 19 | spring_length = 0.15 20 | spring_stiffness = 55.0 21 | bump = 6.0 22 | rebound = 7.0 23 | wheel_mass = 20.0 24 | tire_radius = 0.3 25 | tire_width = 0.225 26 | ackermann = 0.15 27 | anti_roll = 50 28 | 29 | [sub_resource type="Resource" id="Resource_x3xtx"] 30 | script = ExtResource("3_scbug") 31 | tire_model = SubResource("Resource_kubsx") 32 | spring_length = 0.15 33 | spring_stiffness = 55.0 34 | bump = 6.0 35 | rebound = 7.0 36 | wheel_mass = 20.0 37 | tire_radius = 0.3 38 | tire_width = 0.225 39 | ackermann = 0.15 40 | anti_roll = 50 41 | 42 | [sub_resource type="Resource" id="Resource_qk3ry"] 43 | script = ExtResource("3_scbug") 44 | tire_model = SubResource("Resource_kubsx") 45 | spring_length = 0.15 46 | spring_stiffness = 55.0 47 | bump = 6.0 48 | rebound = 7.0 49 | wheel_mass = 20.0 50 | tire_radius = 0.3 51 | tire_width = 0.225 52 | ackermann = 0.15 53 | anti_roll = 50 54 | 55 | [sub_resource type="Resource" id="Resource_ofeud"] 56 | script = ExtResource("3_scbug") 57 | tire_model = SubResource("Resource_kubsx") 58 | spring_length = 0.15 59 | spring_stiffness = 55.0 60 | bump = 6.0 61 | rebound = 7.0 62 | wheel_mass = 20.0 63 | tire_radius = 0.3 64 | tire_width = 0.225 65 | ackermann = 0.15 66 | anti_roll = 50 67 | 68 | [resource] 69 | script = ExtResource("2_8ru0w") 70 | max_steer = 0.3 71 | front_brake_bias = 0.6 72 | steer_speed = 5.0 73 | max_brake_force = 20000.0 74 | max_handbrake_torque = 4000.0 75 | brake_effective_radius = 0.25 76 | fuel_tank_size = 40.0 77 | fuel_percentage = 100.0 78 | max_torque = 150.0 79 | max_engine_rpm = 8000.0 80 | rpm_idle = 900.0 81 | torque_curve = SubResource("Curve_g3qlw") 82 | engine_drag = 0.03 83 | engine_brake = 10.0 84 | engine_moment = 0.25 85 | engine_bsfc = 0.3 86 | engine_sound = ExtResource("1_wqlgx") 87 | clutch_friction = 300.0 88 | drivetype = 1 89 | automatic = false 90 | gear_ratios = [3.1, 2.61, 2.1, 1.72, 1.2, 1.0] 91 | final_drive = 3.7 92 | reverse_ratio = 3.9 93 | gear_inertia = 0.12 94 | front_diff = 0 95 | front_diff_preload = 50.0 96 | front_diff_power_ratio = 2.0 97 | front_diff_coast_ratio = 1.0 98 | rear_diff = 0 99 | rear_diff_preload = 50.0 100 | rear_diff_power_ratio = 2.0 101 | rear_diff_coast_ratio = 1.0 102 | center_split_fr = 0.4 103 | cd = 0.3 104 | air_density = 1.225 105 | frontal_area = 2.0 106 | wheel_params_fl = SubResource("Resource_qk3ry") 107 | wheel_params_fr = SubResource("Resource_ofeud") 108 | wheel_params_bl = SubResource("Resource_wl1bc") 109 | wheel_params_br = SubResource("Resource_x3xtx") 110 | -------------------------------------------------------------------------------- /levels/testlevels/drag_test.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=14 format=3] 2 | 3 | [ext_resource type="PackedScene" uid="uid://m4urncdemtj7" path="res://scenes/vehicle/SportCar.tscn" id="1"] 4 | 5 | [sub_resource type="BoxMesh" id=1] 6 | size = Vector3( 200, 2, 2000 ) 7 | 8 | [sub_resource type="OpenSimplexNoise" id=3] 9 | period = 21.4 10 | 11 | [sub_resource type="NoiseTexture" id=10] 12 | seamless = true 13 | noise = SubResource( 3 ) 14 | 15 | [sub_resource type="NoiseTexture" id=4] 16 | seamless = true 17 | as_normal_map = true 18 | noise = SubResource( 3 ) 19 | 20 | [sub_resource type="StandardMaterial3D" id=5] 21 | albedo_color = Color( 0.243137, 1, 0, 1 ) 22 | albedo_texture = SubResource( 10 ) 23 | normal_enabled = true 24 | normal_scale = 1.0 25 | normal_texture = SubResource( 4 ) 26 | 27 | [sub_resource type="ConcavePolygonShape3D" id=12] 28 | data = PackedVector3Array( -100, 1, 1000, 100, 1, 1000, -100, -1, 1000, 100, 1, 1000, 100, -1, 1000, -100, -1, 1000, 100, 1, -1000, -100, 1, -1000, 100, -1, -1000, -100, 1, -1000, -100, -1, -1000, 100, -1, -1000, 100, 1, 1000, 100, 1, -1000, 100, -1, 1000, 100, 1, -1000, 100, -1, -1000, 100, -1, 1000, -100, 1, -1000, -100, 1, 1000, -100, -1, -1000, -100, 1, 1000, -100, -1, 1000, -100, -1, -1000, 100, 1, 1000, -100, 1, 1000, 100, 1, -1000, -100, 1, 1000, -100, 1, -1000, 100, 1, -1000, -100, -1, 1000, 100, -1, 1000, -100, -1, -1000, 100, -1, 1000, 100, -1, -1000, -100, -1, -1000 ) 29 | 30 | [sub_resource type="BoxMesh" id=6] 31 | size = Vector3( 10, 2, 2000 ) 32 | 33 | [sub_resource type="OpenSimplexNoise" id=8] 34 | period = 0.1 35 | 36 | [sub_resource type="NoiseTexture" id=11] 37 | seamless = true 38 | noise = SubResource( 8 ) 39 | 40 | [sub_resource type="NoiseTexture" id=9] 41 | seamless = true 42 | as_normal_map = true 43 | noise = SubResource( 8 ) 44 | 45 | [sub_resource type="StandardMaterial3D" id=7] 46 | albedo_color = Color( 0.290196, 0.290196, 0.290196, 1 ) 47 | albedo_texture = SubResource( 11 ) 48 | metallic_specular = 0.0 49 | normal_enabled = true 50 | normal_scale = 1.0 51 | normal_texture = SubResource( 9 ) 52 | uv1_scale = Vector3( 2, 200, 100 ) 53 | 54 | [sub_resource type="ConcavePolygonShape3D" id=13] 55 | data = PackedVector3Array( -5, 1, 1000, 5, 1, 1000, -5, -1, 1000, 5, 1, 1000, 5, -1, 1000, -5, -1, 1000, 5, 1, -1000, -5, 1, -1000, 5, -1, -1000, -5, 1, -1000, -5, -1, -1000, 5, -1, -1000, 5, 1, 1000, 5, 1, -1000, 5, -1, 1000, 5, 1, -1000, 5, -1, -1000, 5, -1, 1000, -5, 1, -1000, -5, 1, 1000, -5, -1, -1000, -5, 1, 1000, -5, -1, 1000, -5, -1, -1000, 5, 1, 1000, -5, 1, 1000, 5, 1, -1000, -5, 1, 1000, -5, 1, -1000, 5, 1, -1000, -5, -1, 1000, 5, -1, 1000, -5, -1, -1000, 5, -1, 1000, 5, -1, -1000, -5, -1, -1000 ) 56 | 57 | [node name="DragTest" type="Node3D"] 58 | 59 | [node name="Ground" type="MeshInstance3D" parent="."] 60 | mesh = SubResource( 1 ) 61 | material/0 = SubResource( 5 ) 62 | 63 | [node name="StaticBody3D" type="StaticBody3D" parent="Ground" groups=["Grass"]] 64 | 65 | [node name="CollisionShape3D" type="CollisionShape3D" parent="Ground/StaticBody3D"] 66 | shape = SubResource( 12 ) 67 | 68 | [node name="Road" type="MeshInstance3D" parent="."] 69 | transform = Transform3D( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0 ) 70 | mesh = SubResource( 6 ) 71 | material/0 = SubResource( 7 ) 72 | 73 | [node name="StaticBody3D" type="StaticBody3D" parent="Road" groups=["Tarmac"]] 74 | 75 | [node name="CollisionShape3D" type="CollisionShape3D" parent="Road/StaticBody3D"] 76 | shape = SubResource( 13 ) 77 | 78 | [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] 79 | transform = Transform3D( 1, 0, 0, 0, 0.5, 0.866025, 0, -0.866025, 0.5, 0, 10.62, 0 ) 80 | 81 | [node name="Sportcar" parent="." instance=ExtResource( 1 )] 82 | transform = Transform3D( -1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0.93, 1.54, -985.07 ) 83 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Advanced Vehicle Demo" 14 | run/main_scene="res://levels/world.tscn" 15 | config/features=PackedStringArray("4.1") 16 | config/icon="res://icon.png" 17 | 18 | [dotnet] 19 | 20 | project/assembly_name="Advanced Vehicle Demo" 21 | 22 | [editor_plugins] 23 | 24 | enabled=PackedStringArray() 25 | 26 | [input] 27 | 28 | ShiftUp={ 29 | "deadzone": 0.5, 30 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"echo":false,"script":null) 31 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null) 32 | ] 33 | } 34 | ShiftDown={ 35 | "deadzone": 0.5, 36 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":122,"echo":false,"script":null) 37 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":2,"pressure":0.0,"pressed":false,"script":null) 38 | ] 39 | } 40 | Throttle={ 41 | "deadzone": 0.05, 42 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"echo":false,"script":null) 43 | , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":5,"axis_value":1.0,"script":null) 44 | ] 45 | } 46 | Brake={ 47 | "deadzone": 0.05, 48 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"echo":false,"script":null) 49 | , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":2,"axis_value":1.0,"script":null) 50 | ] 51 | } 52 | Handbrake={ 53 | "deadzone": 0.05, 54 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"echo":false,"script":null) 55 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":1,"pressure":0.0,"pressed":false,"script":null) 56 | ] 57 | } 58 | SteerLeft={ 59 | "deadzone": 0.05, 60 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"echo":false,"script":null) 61 | , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":-1.0,"script":null) 62 | ] 63 | } 64 | SteerRight={ 65 | "deadzone": 0.05, 66 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"echo":false,"script":null) 67 | , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":1.0,"script":null) 68 | ] 69 | } 70 | Clutch={ 71 | "deadzone": 0.5, 72 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"echo":false,"script":null) 73 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":4,"pressure":0.0,"pressed":false,"script":null) 74 | ] 75 | } 76 | 77 | [physics] 78 | 79 | common/physics_ticks_per_second=240 80 | 3d/physics_engine="GodotPhysics3D" 81 | 3d/default_linear_damp=0.0 82 | 3d/default_angular_damp=0.0 83 | 3d/solver/solver_iterations=100 84 | common/physics_fps=240 85 | 3d/active_soft_world=false 86 | 87 | [rendering] 88 | 89 | environment/defaults/default_environment="res://default_env.tres" 90 | threads/thread_model=2 91 | -------------------------------------------------------------------------------- /scenes/gui/tireinfo_app.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://dacyugrkjb2o4"] 2 | 3 | [ext_resource type="Script" path="res://scenes/gui/tireinfo_app.gd" id="1"] 4 | 5 | [node name="TireInfoApp" type="Control"] 6 | layout_mode = 3 7 | anchors_preset = 8 8 | anchor_left = 0.5 9 | anchor_top = 0.5 10 | anchor_right = 0.5 11 | anchor_bottom = 0.5 12 | offset_left = -100.0 13 | offset_top = -150.0 14 | offset_right = 100.0 15 | offset_bottom = 150.0 16 | grow_horizontal = 2 17 | grow_vertical = 2 18 | size_flags_horizontal = 4 19 | size_flags_vertical = 4 20 | script = ExtResource("1") 21 | 22 | [node name="Panel" type="Panel" parent="."] 23 | self_modulate = Color(1, 1, 1, 0.556863) 24 | layout_mode = 0 25 | anchor_right = 1.0 26 | anchor_bottom = 1.0 27 | 28 | [node name="VBoxContainer" type="VBoxContainer" parent="Panel"] 29 | layout_mode = 1 30 | anchors_preset = 15 31 | anchor_right = 1.0 32 | anchor_bottom = 1.0 33 | grow_horizontal = 2 34 | grow_vertical = 2 35 | theme_override_constants/separation = 10 36 | 37 | [node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer"] 38 | layout_mode = 2 39 | size_flags_vertical = 3 40 | theme_override_constants/separation = 10 41 | 42 | [node name="WheelFL" type="VBoxContainer" parent="Panel/VBoxContainer/HBoxContainer"] 43 | layout_mode = 2 44 | size_flags_horizontal = 3 45 | theme_override_constants/separation = 2 46 | 47 | [node name="TireWearLabel" type="Label" parent="Panel/VBoxContainer/HBoxContainer/WheelFL"] 48 | layout_mode = 2 49 | text = "0.0" 50 | horizontal_alignment = 1 51 | 52 | [node name="TireTemp" type="ColorRect" parent="Panel/VBoxContainer/HBoxContainer/WheelFL"] 53 | layout_mode = 2 54 | size_flags_vertical = 3 55 | color = Color(0, 0.545098, 0.2, 1) 56 | 57 | [node name="Label" type="Label" parent="Panel/VBoxContainer/HBoxContainer/WheelFL/TireTemp"] 58 | layout_mode = 1 59 | anchors_preset = 8 60 | anchor_left = 0.5 61 | anchor_top = 0.5 62 | anchor_right = 0.5 63 | anchor_bottom = 0.5 64 | offset_left = -20.0 65 | offset_top = -11.5 66 | offset_right = 20.0 67 | offset_bottom = 11.5 68 | grow_horizontal = 2 69 | grow_vertical = 2 70 | text = "20.0 c" 71 | 72 | [node name="WheelFR" type="VBoxContainer" parent="Panel/VBoxContainer/HBoxContainer"] 73 | layout_mode = 2 74 | size_flags_horizontal = 3 75 | theme_override_constants/separation = 2 76 | 77 | [node name="TireWearLabel" type="Label" parent="Panel/VBoxContainer/HBoxContainer/WheelFR"] 78 | layout_mode = 2 79 | text = "0.0" 80 | horizontal_alignment = 1 81 | 82 | [node name="TireTemp" type="ColorRect" parent="Panel/VBoxContainer/HBoxContainer/WheelFR"] 83 | layout_mode = 2 84 | size_flags_vertical = 3 85 | color = Color(0, 0.545098, 0.2, 1) 86 | 87 | [node name="Label" type="Label" parent="Panel/VBoxContainer/HBoxContainer/WheelFR/TireTemp"] 88 | layout_mode = 1 89 | anchors_preset = 8 90 | anchor_left = 0.5 91 | anchor_top = 0.5 92 | anchor_right = 0.5 93 | anchor_bottom = 0.5 94 | offset_left = -20.0 95 | offset_top = -11.5 96 | offset_right = 20.0 97 | offset_bottom = 11.5 98 | grow_horizontal = 2 99 | grow_vertical = 2 100 | text = "20.0 c" 101 | 102 | [node name="HBoxContainer2" type="HBoxContainer" parent="Panel/VBoxContainer"] 103 | layout_mode = 2 104 | size_flags_vertical = 3 105 | theme_override_constants/separation = 10 106 | 107 | [node name="WheelBL" type="VBoxContainer" parent="Panel/VBoxContainer/HBoxContainer2"] 108 | layout_mode = 2 109 | size_flags_horizontal = 3 110 | theme_override_constants/separation = 2 111 | alignment = 2 112 | 113 | [node name="TireTemp" type="ColorRect" parent="Panel/VBoxContainer/HBoxContainer2/WheelBL"] 114 | layout_mode = 2 115 | size_flags_vertical = 3 116 | color = Color(0, 0.545098, 0.2, 1) 117 | 118 | [node name="Label" type="Label" parent="Panel/VBoxContainer/HBoxContainer2/WheelBL/TireTemp"] 119 | layout_mode = 1 120 | anchors_preset = 8 121 | anchor_left = 0.5 122 | anchor_top = 0.5 123 | anchor_right = 0.5 124 | anchor_bottom = 0.5 125 | offset_left = -20.0 126 | offset_top = -11.5 127 | offset_right = 20.0 128 | offset_bottom = 11.5 129 | grow_horizontal = 2 130 | grow_vertical = 2 131 | text = "20.0 c" 132 | 133 | [node name="TireWearLabel" type="Label" parent="Panel/VBoxContainer/HBoxContainer2/WheelBL"] 134 | layout_mode = 2 135 | text = "0.0" 136 | horizontal_alignment = 1 137 | 138 | [node name="WheelBR" type="VBoxContainer" parent="Panel/VBoxContainer/HBoxContainer2"] 139 | layout_mode = 2 140 | size_flags_horizontal = 3 141 | theme_override_constants/separation = 2 142 | alignment = 2 143 | 144 | [node name="TireTemp" type="ColorRect" parent="Panel/VBoxContainer/HBoxContainer2/WheelBR"] 145 | layout_mode = 2 146 | size_flags_vertical = 3 147 | color = Color(0, 0.545098, 0.2, 1) 148 | 149 | [node name="Label" type="Label" parent="Panel/VBoxContainer/HBoxContainer2/WheelBR/TireTemp"] 150 | layout_mode = 1 151 | anchors_preset = 8 152 | anchor_left = 0.5 153 | anchor_top = 0.5 154 | anchor_right = 0.5 155 | anchor_bottom = 0.5 156 | offset_left = -20.0 157 | offset_top = -11.5 158 | offset_right = 20.0 159 | offset_bottom = 11.5 160 | grow_horizontal = 2 161 | grow_vertical = 2 162 | text = "20.0 c" 163 | 164 | [node name="TireWearLabel" type="Label" parent="Panel/VBoxContainer/HBoxContainer2/WheelBR"] 165 | layout_mode = 2 166 | text = "0.0" 167 | horizontal_alignment = 1 168 | -------------------------------------------------------------------------------- /scenes/vehicle/wheel_suspension.gd: -------------------------------------------------------------------------------- 1 | class_name RaycastSuspension 2 | extends RayCast3D 3 | 4 | ############# Choose what tire formula to use ############# 5 | var tire_model: BaseTireModel 6 | 7 | ############# Suspension stuff ############# 8 | var spring_length = 0.2 9 | var spring_stiffness = 45.0 10 | var bump = 3.5 11 | var rebound = 4.0 12 | var anti_roll = 0.0 13 | 14 | var spring_load_mm:float = 0 15 | var prev_spring_load_mm:float = 0 16 | var spring_speed_mm_per_seconds:float = 0 17 | var spring_load_newton:float = 0 18 | 19 | ############# Tire stuff ############# 20 | var wheel_mass = 15.0 21 | var tire_radius = 0.3 22 | var tire_width = 0.2 23 | var ackermann = 0.15 24 | 25 | var tire_wear: float = 0.0 26 | 27 | var surface_mu = 1.0 28 | var y_force: float = 0.0 29 | 30 | var wheel_inertia: float = 0.0 31 | var spin: float = 0.0 32 | var z_vel: float = 0.0 33 | var local_vel := Vector3.ZERO 34 | 35 | var rolling_resistance: float = 0.0 #Vector2 = Vector2.ZERO 36 | var rol_res_surface_mul: float = 0.02 37 | 38 | var force_vec = Vector3.ZERO 39 | var slip_vec: Vector2 = Vector2.ZERO 40 | var prev_pos: Vector3 = Vector3.ZERO 41 | 42 | var spring_curr_length: float = spring_length 43 | 44 | @onready var car = $'..' #Get the parent node as car 45 | @onready var wheelmesh = $MeshInstance3D 46 | 47 | 48 | func _ready() -> void: 49 | wheel_inertia = 0.5 * wheel_mass * pow(tire_radius, 2) 50 | set_target_position(Vector3.DOWN * (spring_length + tire_radius)) 51 | 52 | 53 | func set_params(params: WheelSuspensionParameters): 54 | tire_model = params.tire_model 55 | spring_length = params.spring_length 56 | spring_stiffness = params.spring_stiffness 57 | bump = params.bump 58 | rebound = params.rebound 59 | wheel_mass = params.wheel_mass 60 | tire_radius = params.tire_model.tire_radius 61 | tire_width = params.tire_model.tire_width 62 | ackermann = params.ackermann 63 | anti_roll = params.anti_roll 64 | 65 | wheel_inertia = 0.5 * wheel_mass * pow(tire_radius, 2) 66 | set_target_position(Vector3.DOWN * (spring_length + tire_radius)) 67 | 68 | 69 | # Move back to physics process when physics interpolation comes to godot4 70 | func _process(delta: float) -> void: 71 | wheelmesh.rotate_x(wrapf(-spin * delta,0, TAU)) 72 | wheelmesh.position.y = -spring_curr_length 73 | 74 | 75 | func _physics_process(delta: float) -> void: 76 | var spin_treshold := 10.0 77 | if abs(spin) > spin_treshold or abs(z_vel) > 1.0: 78 | tire_wear = tire_model.update_tire_wear(delta, slip_vec, y_force, surface_mu) 79 | 80 | var ambient_temp := 20.0 81 | tire_model.update_tire_temp(slip_vec, y_force, local_vel.length(), surface_mu, ambient_temp, delta) 82 | 83 | 84 | func apply_forces(opposite_comp, delta): 85 | ############# Local forward velocity ############# 86 | force_vec = Vector3.ZERO 87 | 88 | local_vel = (global_transform.origin - prev_pos) / delta * global_transform.basis 89 | z_vel = -local_vel.z 90 | var planar_vect = Vector2(local_vel.x, local_vel.z).normalized() 91 | prev_pos = global_transform.origin 92 | 93 | var surface 94 | ############# Suspension ################# 95 | if is_colliding(): 96 | if get_collider().get_groups().size() > 0: 97 | surface = get_collider().get_groups()[0] 98 | if surface: 99 | surface_mu = 1.0 100 | if surface == "Tarmac": 101 | surface_mu = 0.85 102 | rol_res_surface_mul = 0.01 103 | elif surface == "Gravel": 104 | surface_mu = 0.6 105 | rol_res_surface_mul = 0.03 106 | elif surface == "Grass": 107 | surface_mu = 0.55 108 | rol_res_surface_mul = 0.025 109 | elif surface == "Snow": 110 | surface_mu = 0.4 111 | rol_res_surface_mul = 0.035 112 | 113 | spring_curr_length = get_collision_point().distance_to(global_transform.origin) - tire_radius 114 | else: 115 | spring_curr_length = spring_length 116 | 117 | 118 | #Calculate the spring load in mm (absolut) 119 | spring_load_mm = (spring_length - spring_curr_length) * 1000 120 | 121 | #Calculate spring movement in mm per seconds 122 | spring_speed_mm_per_seconds = (spring_load_mm - prev_spring_load_mm) / delta 123 | prev_spring_load_mm = spring_load_mm 124 | 125 | #Calculate the force of the spring in N (mm * N/mm equals m * kN/m) 126 | spring_load_newton = spring_load_mm * spring_stiffness 127 | 128 | #Calculate the damping force in N and add it to spring_load_newton 129 | if spring_speed_mm_per_seconds >= 0: 130 | spring_load_newton += spring_speed_mm_per_seconds * bump # bump 131 | else : 132 | spring_load_newton += spring_speed_mm_per_seconds * rebound # rebound 133 | 134 | y_force = spring_load_newton 135 | y_force = max(0, y_force) 136 | 137 | ############### Slip ####################### 138 | slip_vec.x = asin(clamp(-planar_vect.x, -1, 1)) # X slip is lateral slip 139 | slip_vec.y = 0.0 # Y slip is the longitudinal Z slip 140 | 141 | if is_colliding(): 142 | if not is_zero_approx(z_vel): 143 | slip_vec.y = (z_vel - spin * tire_radius) / abs(z_vel) 144 | else: 145 | slip_vec.y = (z_vel - spin * tire_radius) / abs(z_vel + 0.0000001) 146 | 147 | if spring_load_mm !=0: 148 | y_force += anti_roll * (spring_load_mm - opposite_comp) 149 | 150 | force_vec = tire_model.update_tire_forces(slip_vec, y_force, surface_mu) 151 | 152 | var contact = get_collision_point() - car.global_transform.origin 153 | var normal = get_collision_normal() 154 | 155 | car.apply_force(normal * y_force, contact) 156 | car.apply_force(global_transform.basis.x * force_vec.x, contact) 157 | car.apply_force(global_transform.basis.z * force_vec.y, contact) 158 | 159 | return spring_load_mm 160 | else: 161 | spin -= sign(spin) * delta * 2 / wheel_inertia # stop undriven wheels from spinning endlessly 162 | return 0.0 163 | 164 | 165 | func apply_torque(drive_torque, brake_torque, drive_inertia, delta): 166 | var prev_spin = spin 167 | var net_torque = force_vec.y * tire_radius 168 | net_torque += drive_torque 169 | if abs(spin) < 5 and brake_torque > abs(net_torque): 170 | spin = 0 171 | else: 172 | net_torque -= (brake_torque + rolling_resistance) * sign(spin) 173 | spin += delta * net_torque / (wheel_inertia + drive_inertia) 174 | 175 | if drive_torque * delta == 0: 176 | return 0.5 177 | else: 178 | return (spin - prev_spin) * (wheel_inertia + drive_inertia) / (drive_torque * delta) 179 | 180 | 181 | func set_spin(value): 182 | spin = value 183 | 184 | 185 | func get_spin(): 186 | return spin 187 | 188 | 189 | func get_reaction_torque(): 190 | return force_vec.y * tire_radius 191 | 192 | 193 | func steer(input, max_steer): 194 | rotation.y = max_steer * (input + (1 - cos(input * 0.5 * PI)) * ackermann) 195 | 196 | -------------------------------------------------------------------------------- /scenes/vehicle/drivetrain.gd: -------------------------------------------------------------------------------- 1 | class_name DriveTrain 2 | extends Node 3 | 4 | const AV_2_RPM: float = 60 / TAU 5 | 6 | enum DIFF_TYPE{ 7 | LIMITED_SLIP, 8 | OPEN_DIFF, 9 | LOCKED, 10 | } 11 | 12 | enum DIFF_STATE { 13 | LOCKED, 14 | SLIPPING, 15 | OPEN, 16 | } 17 | 18 | enum DRIVE_TYPE{ 19 | FWD, 20 | RWD, 21 | AWD, 22 | } 23 | 24 | @export var drivetrain_params: DriveTrainParameters 25 | 26 | var selected_gear := 0 27 | var _diff_clutch := Clutch.new() 28 | var _engine_inertia := 0.0 29 | var _diff_split := 0.5 30 | var last_shift_time := 0 31 | 32 | var avg_rear_spin := 0.0 33 | var avg_front_spin := 0.0 34 | 35 | var drive_inertia := 10.0 36 | var reaction_torque := 0.0 37 | 38 | func automatic_shifting(cur_torque, lower_gear_torque, higher_gear_torque, rpm, max_rpm, brake_input, speed): 39 | if !drivetrain_params.automatic: 40 | return 41 | 42 | var reversing = false 43 | var shift_time = 700 44 | 45 | if selected_gear == -1: 46 | reversing = true 47 | 48 | if higher_gear_torque > cur_torque and selected_gear >= 0: 49 | if rpm > 0.85 * max_rpm: 50 | if Time.get_ticks_msec() - last_shift_time > shift_time: 51 | shift_up() 52 | 53 | if selected_gear > 1 and rpm < 0.5 * max_rpm and lower_gear_torque > cur_torque: 54 | if Time.get_ticks_msec() - last_shift_time > shift_time: 55 | shift_down() 56 | 57 | if abs(selected_gear) <= 1 and abs(speed) < 3.0 and brake_input > 0.2: 58 | if not reversing: 59 | if Time.get_ticks_msec() - last_shift_time > shift_time: 60 | shift_down() 61 | else: 62 | if Time.get_ticks_msec() - last_shift_time > shift_time: 63 | shift_up() 64 | 65 | 66 | func set_selected_gear(gear): 67 | gear = clamp(gear, -1, drivetrain_params.gear_ratios.size()) 68 | selected_gear = gear 69 | 70 | 71 | func shift_up(): 72 | if selected_gear < drivetrain_params.gear_ratios.size(): 73 | selected_gear += 1 74 | last_shift_time = Time.get_ticks_msec() 75 | set_selected_gear(selected_gear) 76 | 77 | 78 | func shift_down(): 79 | if selected_gear > -1: 80 | selected_gear -= 1 81 | last_shift_time = Time.get_ticks_msec() 82 | set_selected_gear(selected_gear) 83 | 84 | 85 | func get_gearing() -> float: 86 | if selected_gear > drivetrain_params.gear_ratios.size(): 87 | return 0.0 88 | if selected_gear > 0: 89 | return drivetrain_params.gear_ratios[selected_gear - 1] * drivetrain_params.final_drive 90 | if selected_gear == -1: 91 | return -drivetrain_params.reverse_ratio * drivetrain_params.final_drive 92 | return 0.0 93 | 94 | 95 | func set_input_inertia(value): 96 | _engine_inertia = value 97 | 98 | 99 | func differential(torque: float, brake_torque, wheels, diff: DiffParameters, delta: float): 100 | var diff_state = DIFF_STATE.LOCKED 101 | var tr1 = abs(wheels[0].get_reaction_torque()) 102 | var tr2 = abs(wheels[1].get_reaction_torque()) 103 | 104 | var delta_torque := 0.0 105 | var bias := 0.0 106 | 107 | if tr1 >= tr2: 108 | bias = tr1 / tr2 109 | else: 110 | bias = tr2 / tr1 111 | 112 | delta_torque = tr1 - tr2 113 | var t1 := torque * 0.5 114 | var t2 := torque * 0.5 115 | 116 | var ratio = diff.power_ratio 117 | if torque * sign(get_gearing()) < 0: 118 | ratio = diff.coast_ratio 119 | 120 | if diff.diff_type == DIFF_TYPE.OPEN_DIFF: 121 | diff_state = DIFF_STATE.OPEN 122 | 123 | elif diff.diff_type == DIFF_TYPE.LOCKED: 124 | diff_state = DIFF_STATE.LOCKED 125 | 126 | else: # Limited Slip Differential 127 | if abs(delta_torque) > diff.diff_preload and bias >= ratio: 128 | diff_state = DIFF_STATE.SLIPPING 129 | 130 | match diff_state: 131 | DIFF_STATE.OPEN: 132 | var diff_sum := 0.0 133 | t2 *= _diff_split 134 | t1 *= (1 - _diff_split) 135 | 136 | diff_sum += wheels[0].apply_torque(t1, brake_torque * 0.5, drive_inertia, delta) 137 | diff_sum -= wheels[1].apply_torque(t2, brake_torque * 0.5, drive_inertia, delta) 138 | _diff_split = 0.5 * (clamp(diff_sum, -1.0, 1.0) + 1.0) 139 | 140 | DIFF_STATE.SLIPPING: 141 | _diff_clutch.friction = diff.diff_preload 142 | var diff_torques = _diff_clutch.get_reaction_torques(wheels[0].get_spin(), wheels[1].get_spin(), tr1, tr2, diff.diff_preload * ratio, 0.0) 143 | t1 += diff_torques.x 144 | t2 += diff_torques.y 145 | 146 | wheels[0].apply_torque(t1, brake_torque * 0.5, drive_inertia, delta) 147 | wheels[1].apply_torque(t2, brake_torque * 0.5, drive_inertia, delta) 148 | 149 | DIFF_STATE.LOCKED: 150 | var net_torque = wheels[0].get_reaction_torque() + wheels[1].get_reaction_torque() 151 | net_torque += t1 + t2 152 | 153 | var spin := 0.0 154 | var avg_spin = (wheels[0].get_spin() + wheels[1].get_spin()) * 0.5 155 | var rolling_resistance = wheels[0].rolling_resistance + wheels[1].rolling_resistance 156 | 157 | if abs(avg_spin) < 5.0 and brake_torque > abs(net_torque): 158 | spin = 0.0 159 | else: 160 | net_torque -= (brake_torque + rolling_resistance) * sign(avg_spin) 161 | 162 | spin = avg_spin + delta * net_torque / (wheels[0].wheel_inertia + drive_inertia + wheels[1].wheel_inertia) 163 | wheels[0].set_spin(spin) 164 | wheels[1].set_spin(spin) 165 | 166 | 167 | func drivetrain(torque: float, rear_brake_torque: float, front_brake_torque: float, wheels: Array, clutch_input: float, delta: float): 168 | var rear_wheels = [wheels[0], wheels[1]] 169 | var front_wheels = [wheels[2], wheels[3]] 170 | 171 | avg_rear_spin = (wheels[0].get_spin() + wheels[1].get_spin()) * 0.5 172 | avg_front_spin = (wheels[2].get_spin() + wheels[3].get_spin()) * 0.5 173 | 174 | drive_inertia = (_engine_inertia + pow(abs(get_gearing()), 2) * drivetrain_params.gear_inertia) * (1 - clutch_input) 175 | var drive_torque = torque * get_gearing() 176 | 177 | if drivetrain_params.drivetype == DRIVE_TYPE.RWD: 178 | differential(drive_torque, rear_brake_torque, rear_wheels, drivetrain_params.rear_diff, delta) 179 | front_wheels[0].apply_torque(0.0, front_brake_torque * 0.5, 0.0, delta) 180 | front_wheels[1].apply_torque(0.0, front_brake_torque * 0.5, 0.0, delta) 181 | reaction_torque = (rear_wheels[0].get_reaction_torque() + rear_wheels[1].get_reaction_torque()) * 0.5 182 | reaction_torque *= (1.0 / get_gearing()) 183 | 184 | elif drivetrain_params.drivetype == DRIVE_TYPE.FWD: 185 | differential(drive_torque, front_brake_torque, front_wheels, drivetrain_params.front_diff, delta) 186 | rear_wheels[0].apply_torque(0.0, rear_brake_torque * 0.5, 0.0, delta) 187 | rear_wheels[1].apply_torque(0.0, rear_brake_torque * 0.5, 0.0, delta) 188 | reaction_torque = (front_wheels[0].get_reaction_torque() + front_wheels[1].get_reaction_torque()) * 0.5 189 | reaction_torque *= (1.0 / get_gearing()) 190 | 191 | elif drivetrain_params.drivetype == DRIVE_TYPE.AWD: 192 | reaction_torque = (rear_wheels[0].get_reaction_torque() + rear_wheels[1].get_reaction_torque()) * 0.25 193 | reaction_torque += (front_wheels[0].get_reaction_torque() + front_wheels[1].get_reaction_torque()) * 0.25 194 | reaction_torque *= (1.0 / get_gearing()) 195 | 196 | match drivetrain_params.center_diff.diff_type: 197 | DIFF_TYPE.LOCKED: # Locked center diff currently means raw 4x4 198 | var avg_spin = (avg_front_spin + avg_rear_spin) * 0.5 199 | 200 | var net_torque := 0.0 201 | var combined_wheel_inertias := 0.0 202 | var rolling_resistance := 0.0 203 | 204 | for w in wheels: 205 | net_torque += w.get_reaction_torque() 206 | combined_wheel_inertias += w.wheel_inertia 207 | rolling_resistance += w.rolling_resistance 208 | 209 | net_torque += drive_torque 210 | var brake_torque := rear_brake_torque + front_brake_torque 211 | var spin := 0.0 212 | 213 | if abs(avg_spin) < 5.0 and brake_torque > abs(net_torque): 214 | spin = 0.0 215 | else: 216 | net_torque -= (brake_torque + abs(rolling_resistance)) * sign(avg_spin) 217 | spin = avg_spin + delta * net_torque / (drive_inertia + combined_wheel_inertias) 218 | 219 | wheels[0].set_spin(spin) 220 | wheels[1].set_spin(spin) 221 | wheels[2].set_spin(spin) 222 | wheels[3].set_spin(spin) 223 | 224 | DIFF_TYPE.LIMITED_SLIP: 225 | var rear_drive = drive_torque * (1 - drivetrain_params.center_split_fr) 226 | var front_drive = drive_torque * drivetrain_params.center_split_fr 227 | 228 | differential(rear_drive, rear_brake_torque, rear_wheels, drivetrain_params.rear_diff, delta) 229 | differential(front_drive, front_brake_torque, front_wheels, drivetrain_params.front_diff, delta) 230 | 231 | DIFF_TYPE.OPEN_DIFF: 232 | var rear_drive = drive_torque * 0.5 233 | var front_drive = drive_torque * 0.5 234 | 235 | differential(rear_drive, rear_brake_torque, rear_wheels, drivetrain_params.rear_diff, delta) 236 | differential(front_drive, front_brake_torque, front_wheels, drivetrain_params.front_diff, delta) 237 | 238 | -------------------------------------------------------------------------------- /scenes/vehicle/base_car.gd: -------------------------------------------------------------------------------- 1 | class_name BaseCar 2 | extends RigidBody3D 3 | 4 | @export var car_params := CarParameters.new() 5 | 6 | ######## CONSTANTS ######## 7 | const PETROL_KG_L: float = 0.7489 8 | const NM_2_KW: int = 9549 9 | const AV_2_RPM: float = 60 / TAU 10 | 11 | ##### Refs 12 | 13 | var drivetrain := DriveTrain.new() 14 | var clutch := Clutch.new() 15 | 16 | ######### Controller inputs ######### 17 | var throttle_input: float = 0.0 18 | var steering_input: float = 0.0 19 | var brake_input: float = 0.0 20 | var handbrake_input: float = 0.0 21 | var clutch_input: float = 0.0 22 | 23 | ######### Misc ######### 24 | var fuel: float = 0.0 25 | var drag_torque: float = 0.0 26 | var torque_out: float = 0.0 27 | var net_drive: float = 0.0 28 | var engine_net_torque = 0.0 29 | 30 | var clutch_reaction_torque = 0.0 31 | var drive_reaction_torque = 0.0 32 | 33 | var rpm: float = 0.0 34 | var engine_angular_vel: float = 0.0 35 | 36 | var rear_brake_torque: float = 0.0 37 | var front_brake_torque: float = 0.0 38 | 39 | var steering_amount: float = 0.0 40 | 41 | var speedo: float = 0.0 42 | var susp_comp: Array = [0.5, 0.5, 0.5, 0.5] 43 | 44 | var avg_rear_spin := 0.0 45 | var avg_front_spin := 0.0 46 | 47 | var local_vel: Vector3 = Vector3.ZERO 48 | var prev_pos: Vector3 = Vector3.ZERO 49 | var z_vel: float = 0.0 50 | var x_vel: float = 0.0 51 | 52 | @onready var wheel_fl = $Wheel_fl as RaycastSuspension 53 | @onready var wheel_fr = $Wheel_fr as RaycastSuspension 54 | @onready var wheel_bl = $Wheel_bl as RaycastSuspension 55 | @onready var wheel_br = $Wheel_br as RaycastSuspension 56 | @onready var audioplayer = $EngineSound 57 | 58 | 59 | func _ready() -> void: 60 | clutch.friction = car_params.clutch_friction 61 | 62 | drivetrain.drivetrain_params = car_params.drivetrain_params 63 | drivetrain.set_input_inertia(car_params.engine_moment) 64 | 65 | car_params.wheel_params_fl.ackermann = car_params.ackermann 66 | car_params.wheel_params_fr.ackermann = -car_params.ackermann 67 | 68 | wheel_fl.set_params(car_params.wheel_params_fl.duplicate(true)) 69 | wheel_fr.set_params(car_params.wheel_params_fr.duplicate(true)) 70 | wheel_bl.set_params(car_params.wheel_params_bl.duplicate(true)) 71 | wheel_br.set_params(car_params.wheel_params_br.duplicate(true)) 72 | 73 | fuel = car_params.fuel_tank_size * car_params.fuel_percentage * 0.01 74 | self.mass += fuel * PETROL_KG_L 75 | 76 | 77 | func _unhandled_input(event: InputEvent) -> void: 78 | if event.is_action_pressed("ShiftUp"): 79 | shift_up() 80 | if event.is_action_pressed("ShiftDown"): 81 | shift_down() 82 | 83 | 84 | func _physics_process(delta): 85 | brake_input = Input.get_action_strength("Brake") 86 | steering_input = Input.get_action_strength("SteerLeft") - Input.get_action_strength("SteerRight") 87 | throttle_input = Input.get_action_strength("Throttle") 88 | handbrake_input = Input.get_action_strength("Handbrake") 89 | clutch_input = Input.get_action_strength("Clutch") 90 | 91 | var brakes_torques = get_brake_torques(brake_input, delta) 92 | front_brake_torque = brakes_torques.x 93 | rear_brake_torque = brakes_torques.y + handbrake_input * car_params.max_handbrake_torque 94 | 95 | local_vel = (global_transform.origin - prev_pos) / delta * global_transform.basis 96 | prev_pos = global_transform.origin 97 | z_vel = -local_vel.z 98 | x_vel = local_vel.x 99 | 100 | ##### Steerin with steer speed ##### 101 | if (steering_input < steering_amount): 102 | steering_amount -= car_params.steer_speed * delta 103 | if (steering_input > steering_amount): 104 | steering_amount = steering_input 105 | 106 | elif (steering_input > steering_amount): 107 | steering_amount += car_params.steer_speed * delta 108 | if (steering_input < steering_amount): 109 | steering_amount = steering_input 110 | 111 | wheel_fl.steer(steering_amount, car_params.max_steer) 112 | wheel_fr.steer(steering_amount, car_params.max_steer) 113 | 114 | ##### Engine loop ##### 115 | torque_out = get_engine_torque(rpm, throttle_input) 116 | engine_net_torque = torque_out + clutch_reaction_torque 117 | 118 | rpm += AV_2_RPM * delta * engine_net_torque / car_params.engine_moment 119 | engine_angular_vel = rpm / AV_2_RPM 120 | 121 | if rpm >= car_params.max_engine_rpm: 122 | torque_out = 0 123 | rpm -= 500 124 | 125 | # if rpm <= car_params.rpm_idle + 10 and abs(z_vel) < 10 and throttle_input <= 0.05: 126 | # clutch_input = 1.0 127 | 128 | var next_gear_rpm = 0 129 | if drivetrain.selected_gear < car_params.drivetrain_params.gear_ratios.size(): 130 | next_gear_rpm = car_params.drivetrain_params.gear_ratios[drivetrain.selected_gear] * car_params.drivetrain_params.final_drive * avg_front_spin * AV_2_RPM 131 | 132 | var prev_gear_rpm = 0 133 | if drivetrain.selected_gear - 1 > 0: 134 | prev_gear_rpm = car_params.drivetrain_params.gear_ratios[drivetrain.selected_gear - 1] * car_params.drivetrain_params.final_drive * avg_front_spin * AV_2_RPM 135 | 136 | var next_gear_torque := get_engine_torque(next_gear_rpm, throttle_input) 137 | var prev_gear_torque := get_engine_torque(prev_gear_rpm, throttle_input) 138 | 139 | drivetrain.automatic_shifting(torque_out - drag_torque, prev_gear_torque, 140 | next_gear_torque, rpm, car_params.max_engine_rpm, 141 | brake_input, z_vel) 142 | 143 | if drivetrain.selected_gear == 0: 144 | freewheel(delta) 145 | else: 146 | engage(delta) 147 | 148 | rpm = max(rpm, car_params.rpm_idle) 149 | 150 | if fuel <= 0.0: 151 | torque_out = 0.0 152 | rpm = 0.0 153 | stop_engine_sound() 154 | 155 | play_engine_sound() 156 | burn_fuel(delta) 157 | 158 | ##### Anti-roll bar and applying forces ##### 159 | var prev_comp := susp_comp 160 | susp_comp[2] = wheel_bl.apply_forces(prev_comp[3], delta) 161 | susp_comp[3] = wheel_br.apply_forces(prev_comp[2], delta) 162 | susp_comp[0] = wheel_fr.apply_forces(prev_comp[1], delta) 163 | susp_comp[1] = wheel_fl.apply_forces(prev_comp[0], delta) 164 | 165 | drag_force() 166 | 167 | 168 | func get_engine_torque(p_rpm, p_throttle) -> float: 169 | var rpm_factor = clamp(p_rpm / car_params.max_engine_rpm, 0.0, 1.0) 170 | var torque_factor = car_params.torque_curve.sample_baked(rpm_factor) 171 | # var t0 = - 0.1 * car_params.engine_brake - car_params.engine_brake * rpm_factor 172 | var t0 = -car_params.engine_brake * rpm_factor 173 | var t1 = torque_factor * car_params.max_torque 174 | var thr := car_params.throttle_model.sample_baked(p_throttle) 175 | return lerpf(t0, t1, thr) 176 | 177 | 178 | func get_brake_torques(p_brake_input: float, delta): 179 | var clamping_force := p_brake_input * car_params.max_brake_force * 0.5 180 | var brake_pad_mu := 0.4 181 | var effective_radius := 0.25 182 | var braking_force := 2.0 * brake_pad_mu * clamping_force 183 | 184 | var torques := Vector2.ZERO 185 | 186 | torques.x = braking_force * car_params.brake_effective_radius * car_params.front_brake_bias 187 | torques.y = braking_force * car_params.brake_effective_radius * (1 - car_params.front_brake_bias) 188 | return torques 189 | 190 | 191 | func freewheel(delta): 192 | clutch_reaction_torque = 0.0 193 | avg_front_spin = 0.0 194 | wheel_fl.apply_torque(0.0, front_brake_torque, 0.0, delta) 195 | wheel_fr.apply_torque(0.0, front_brake_torque, 0.0, delta) 196 | wheel_bl.apply_torque(0.0, rear_brake_torque, 0.0, delta) 197 | wheel_br.apply_torque(0.0, rear_brake_torque, 0.0, delta) 198 | avg_front_spin += (wheel_fl.spin + wheel_fr.spin) * 0.5 199 | speedo = avg_front_spin * wheel_fl.tire_radius * 3.6 200 | 201 | 202 | func engage(delta): 203 | avg_rear_spin = 0.0 204 | avg_front_spin = 0.0 205 | 206 | avg_rear_spin += (wheel_bl.spin + wheel_br.spin) * 0.5 207 | avg_front_spin += (wheel_fl.spin + wheel_fr.spin) * 0.5 208 | 209 | var gearbox_shaft_speed: float = 0.0 210 | 211 | if car_params.drivetrain_params.drivetype == car_params.drivetrain_params.DRIVE_TYPE.RWD: 212 | gearbox_shaft_speed = avg_rear_spin * drivetrain.get_gearing() 213 | 214 | elif car_params.drivetrain_params.drivetype == car_params.drivetrain_params.DRIVE_TYPE.FWD: 215 | gearbox_shaft_speed = avg_front_spin * drivetrain.get_gearing() 216 | 217 | elif car_params.drivetrain_params.drivetype == car_params.drivetrain_params.DRIVE_TYPE.AWD: 218 | gearbox_shaft_speed = (avg_front_spin + avg_rear_spin) * 0.5 * drivetrain.get_gearing() 219 | 220 | var speed_error = engine_angular_vel - gearbox_shaft_speed 221 | var clutch_kick = abs(speed_error) * 0.2 222 | var tr := drivetrain.reaction_torque 223 | var clutch_slip_torque := 0.8 * clutch.friction 224 | var reaction_torques : Vector2 = clutch.get_reaction_torques(engine_angular_vel, gearbox_shaft_speed, torque_out, tr, clutch_slip_torque, clutch_kick) 225 | 226 | if clutch.locked: 227 | reaction_torques.x = torque_out 228 | drive_reaction_torque = reaction_torques.x * (1 - clutch_input) 229 | clutch_reaction_torque = reaction_torques.y * (1 - clutch_input) 230 | 231 | net_drive = drive_reaction_torque 232 | drivetrain.drivetrain(net_drive, rear_brake_torque, front_brake_torque, 233 | [wheel_bl, wheel_br, wheel_fl, wheel_fr], clutch_input, delta) 234 | speedo = avg_front_spin * wheel_fl.tire_radius * 3.6 235 | 236 | 237 | func drag_force(): 238 | var spd = sqrt(x_vel * x_vel + z_vel * z_vel) 239 | var cdrag = 0.5 * car_params.cd * car_params.frontal_area * car_params.air_density 240 | 241 | # fdrag.y is positive in this case because forward is -z in godot 242 | var fdrag: Vector2 = Vector2.ZERO 243 | fdrag.y = clamp(cdrag * z_vel * spd, -100000, 100000) 244 | fdrag.x = clamp(-cdrag * x_vel * spd, -100000, 100000) 245 | 246 | apply_central_force(global_transform.basis.z.normalized() * fdrag.y) 247 | apply_central_force(global_transform.basis.x.normalized() * fdrag.x) 248 | 249 | 250 | func burn_fuel(delta): 251 | var fuel_burned = car_params.engine_bsfc * torque_out * rpm * delta / (3600 * PETROL_KG_L * NM_2_KW) 252 | fuel -= fuel_burned 253 | self.mass -= fuel_burned * PETROL_KG_L 254 | 255 | 256 | func shift_up(): 257 | drivetrain.shift_up() 258 | 259 | 260 | func shift_down(): 261 | drivetrain.shift_down() 262 | 263 | 264 | func play_engine_sound(): 265 | var pitch_scaler = rpm / 1000 266 | if rpm >= car_params.rpm_idle and rpm < car_params.max_engine_rpm: 267 | if audioplayer.stream != car_params.engine_sound: 268 | audioplayer.set_stream(car_params.engine_sound) 269 | if !audioplayer.playing: 270 | audioplayer.play() 271 | 272 | if pitch_scaler > 0.1: 273 | audioplayer.pitch_scale = pitch_scaler 274 | 275 | 276 | func stop_engine_sound(): 277 | audioplayer.stop() 278 | 279 | --------------------------------------------------------------------------------