feat: car revamp and camera attachment

This commit is contained in:
Asger Juul Brunshøj 2025-06-03 23:50:27 +02:00
parent a39f898f8e
commit 3c13ef9226
15 changed files with 334 additions and 58 deletions

View File

@ -1,4 +1,5 @@
# Summary
- [Brain Dump](./brain_dump.md)
- [2D Vehicle Physics](./2d_vehicle_physics.md)
- [Multiplayer](./multiplayer.md)

6
docs/src/brain_dump.md Normal file
View File

@ -0,0 +1,6 @@
# Brain Dump
Need to implement weight shift on car so front wheel steering has more effect when breaking.
Otherwise the breaking force prevents the steering.
Implement a CarDebugger node that displays force vectors.

BIN
godot/assets/police_car.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cds01w4trqeei"
path="res://.godot/imported/police_car.png-f484def1c543f947419256a0decbc9b1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/police_car.png"
dest_files=["res://.godot/imported/police_car.png-f484def1c543f947419256a0decbc9b1.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

0
godot/camera_2d.gd Normal file
View File

1
godot/camera_2d.gd.uid Normal file
View File

@ -0,0 +1 @@
uid://duwspluoy4xuu

13
godot/car.gd Normal file
View File

@ -0,0 +1,13 @@
extends RigidBody2D
@onready var cam = $Camera2D
# zoom range: from close-in to far-out
var min_zoom = Vector2(1.0, 1.0)
var max_zoom = Vector2(0.2, 0.2)
var max_speed = 5000.0 # max expected speed
func _process(delta):
var speed = linear_velocity.length()
var t = clamp(speed / max_speed, 0.0, 1.0)
cam.zoom = min_zoom.lerp(max_zoom, t)

1
godot/car.gd.uid Normal file
View File

@ -0,0 +1 @@
uid://bic64vi6lpelp

View File

@ -1,17 +1,19 @@
[gd_scene load_steps=3 format=3 uid="uid://ukvcgdjxtkqw"]
[ext_resource type="Texture2D" uid="uid://cl4nm8ajleyjy" path="res://assets/police_car_white.png" id="1_7822p"]
[ext_resource type="Texture2D" uid="uid://cds01w4trqeei" path="res://assets/police_car.png" id="1_7822p"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_37kl0"]
size = Vector2(194, 493)
size = Vector2(631, 1429)
[node name="Car" type="Car"]
mass = 1300.0
inertia = 5e+07
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0.5, -0.5)
scale = Vector2(0.5, 0.5)
shape = SubResource("RectangleShape2D_37kl0")
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(0.265446, 0.265446)
scale = Vector2(0.5, 0.5)
texture = ExtResource("1_7822p")

View File

@ -1,30 +1,34 @@
[gd_scene load_steps=5 format=4 uid="uid://7713s03g7nxw"]
[gd_scene load_steps=5 format=3 uid="uid://7713s03g7nxw"]
[ext_resource type="PackedScene" uid="uid://ukvcgdjxtkqw" path="res://car.tscn" id="1_80nbo"]
[ext_resource type="Texture2D" uid="uid://dd3nv8l28i3qg" path="res://atlases/test.png" id="1_e2o6t"]
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_feb5d"]
texture = ExtResource("1_e2o6t")
texture_region_size = Vector2i(256, 256)
0:0/0 = 0
1:0/0 = 0
[sub_resource type="TileSet" id="TileSet_fc0e3"]
tile_size = Vector2i(256, 256)
sources/6 = SubResource("TileSetAtlasSource_feb5d")
[ext_resource type="PackedScene" uid="uid://drkq82mmv8ofa" path="res://test_track.tscn" id="1_e2o6t"]
[ext_resource type="Script" uid="uid://bic64vi6lpelp" path="res://car.gd" id="4_7jktm"]
[ext_resource type="Texture2D" uid="uid://bxwhirvk4jjpl" path="res://assets/textures/curb_1.png" id="4_fc0e3"]
[node name="Node2D" type="Node2D"]
[node name="Camera2D" type="Camera2D" parent="."]
position = Vector2(0, -1)
zoom = Vector2(0.077, 0.077)
[node name="Node2D" parent="." instance=ExtResource("1_e2o6t")]
position = Vector2(1024, -7680)
rotation = 1.57079
[node name="Roads" type="TileMapLayer" parent="."]
tile_map_data = PackedByteArray("AAD+/wAAAwAAAAAAAAD+////AwAAAAAAAAD9/wAAAgAAAAAAAFD9////AgAAAAAAAFD9/wEAAgAAAAAAAFD+/wIAAgAAAAAAAAD//wIAAgAAAAAAAAAAAAIAAgAAAAAAAAABAAIAAgAAAAAAAAD+/wEAAwAAAAAAAAD//wEAAwAAAAAAAAA=")
tile_set = SubResource("TileSet_fc0e3")
[node name="Node2D2" parent="." instance=ExtResource("1_e2o6t")]
position = Vector2(1024, -17920)
rotation = 1.57079
[node name="Roads2" type="TileMapLayer" parent="."]
tile_map_data = PackedByteArray("AAD+/wAABgAAAAAAAAD+////BgAAAAAAAAD9/wAABgABAAAAAAD9////BgABAAAAAAD9/wEABgABAAAAAAD+/wIABgABAAAAAAD//wIABgABAAAAAAAAAAIABgABAAAAAAABAAIABgABAAAAAAD+/wEABgAAAAAAAAD//wEABgAAAAAAAAA=")
tile_set = SubResource("TileSet_fc0e3")
[node name="Node2D3" parent="." instance=ExtResource("1_e2o6t")]
position = Vector2(1024, -28160)
rotation = 1.57079
[node name="Curb1" type="Sprite2D" parent="."]
position = Vector2(-1024, 1022.44)
rotation = 1.57079
scale = Vector2(1.99747, 0.147406)
texture = ExtResource("4_fc0e3")
[node name="Car" parent="." instance=ExtResource("1_80nbo")]
script = ExtResource("4_7jktm")
[node name="Camera2D" type="Camera2D" parent="Car"]
position = Vector2(0, -1)
scale = Vector2(0.1, 0.1)
zoom = Vector2(0.36, 0.36)

166
godot/test_track.tscn Normal file
View File

@ -0,0 +1,166 @@
[gd_scene load_steps=3 format=3 uid="uid://drkq82mmv8ofa"]
[ext_resource type="Texture2D" uid="uid://boku1251l3foe" path="res://assets/textures/sidewalk.png" id="1_dd2be"]
[ext_resource type="Texture2D" uid="uid://dtfhvbp4vov3l" path="res://assets/textures/road_1.png" id="2_t3gmr"]
[node name="Node2D" type="Node2D"]
[node name="Sidewalk2" type="Sprite2D" parent="."]
position = Vector2(0, -512)
texture = ExtResource("1_dd2be")
[node name="Road1" type="Sprite2D" parent="."]
position = Vector2(0, 512)
texture = ExtResource("2_t3gmr")
[node name="Road2" type="Sprite2D" parent="."]
position = Vector2(0, 1536)
texture = ExtResource("2_t3gmr")
[node name="Sidewalk" type="Sprite2D" parent="."]
position = Vector2(0, 2560)
texture = ExtResource("1_dd2be")
[node name="Sidewalk2" type="Sprite2D" parent="Sidewalk"]
position = Vector2(1024, -3072)
texture = ExtResource("1_dd2be")
[node name="Road1" type="Sprite2D" parent="Sidewalk"]
position = Vector2(1024, -2048)
texture = ExtResource("2_t3gmr")
[node name="Road2" type="Sprite2D" parent="Sidewalk"]
position = Vector2(1024, -1024)
texture = ExtResource("2_t3gmr")
[node name="Sidewalk" type="Sprite2D" parent="Sidewalk"]
position = Vector2(1024, 0)
texture = ExtResource("1_dd2be")
[node name="Sidewalk2" type="Sprite2D" parent="Sidewalk/Sidewalk"]
position = Vector2(1024, -3072)
texture = ExtResource("1_dd2be")
[node name="Road1" type="Sprite2D" parent="Sidewalk/Sidewalk"]
position = Vector2(1024, -2048)
texture = ExtResource("2_t3gmr")
[node name="Road2" type="Sprite2D" parent="Sidewalk/Sidewalk"]
position = Vector2(1024, -1024)
texture = ExtResource("2_t3gmr")
[node name="Sidewalk" type="Sprite2D" parent="Sidewalk/Sidewalk"]
position = Vector2(1024, 0)
texture = ExtResource("1_dd2be")
[node name="Sidewalk2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -3072)
texture = ExtResource("1_dd2be")
[node name="Road1" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -2048)
texture = ExtResource("2_t3gmr")
[node name="Road2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -1024)
texture = ExtResource("2_t3gmr")
[node name="Sidewalk" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, 0)
texture = ExtResource("1_dd2be")
[node name="Sidewalk2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -3072)
texture = ExtResource("1_dd2be")
[node name="Road1" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -2048)
texture = ExtResource("2_t3gmr")
[node name="Road2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -1024)
texture = ExtResource("2_t3gmr")
[node name="Sidewalk" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, 0)
texture = ExtResource("1_dd2be")
[node name="Sidewalk2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -3072)
texture = ExtResource("1_dd2be")
[node name="Road1" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -2048)
texture = ExtResource("2_t3gmr")
[node name="Road2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -1024)
texture = ExtResource("2_t3gmr")
[node name="Sidewalk" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, 0)
texture = ExtResource("1_dd2be")
[node name="Sidewalk2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -3072)
texture = ExtResource("1_dd2be")
[node name="Road1" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -2048)
texture = ExtResource("2_t3gmr")
[node name="Road2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -1024)
texture = ExtResource("2_t3gmr")
[node name="Sidewalk" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, 0)
texture = ExtResource("1_dd2be")
[node name="Sidewalk2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -3072)
texture = ExtResource("1_dd2be")
[node name="Road1" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -2048)
texture = ExtResource("2_t3gmr")
[node name="Road2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -1024)
texture = ExtResource("2_t3gmr")
[node name="Sidewalk" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, 0)
texture = ExtResource("1_dd2be")
[node name="Sidewalk2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -3072)
texture = ExtResource("1_dd2be")
[node name="Road1" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -2048)
texture = ExtResource("2_t3gmr")
[node name="Road2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -1024)
texture = ExtResource("2_t3gmr")
[node name="Sidewalk" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, 0)
texture = ExtResource("1_dd2be")
[node name="Sidewalk2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -3072)
texture = ExtResource("1_dd2be")
[node name="Road1" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -2048)
texture = ExtResource("2_t3gmr")
[node name="Road2" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, -1024)
texture = ExtResource("2_t3gmr")
[node name="Sidewalk" type="Sprite2D" parent="Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk/Sidewalk"]
position = Vector2(1024, 0)
texture = ExtResource("1_dd2be")

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,6 @@
//! Implements a vehicle model based on the bicycle model.
//! Implements a vehicle dynamics model
//!
//! Roughly based on the bicycle model.
use godot::classes::IRigidBody2D;
use godot::classes::RigidBody2D;
@ -6,6 +8,11 @@ use godot::global::deg_to_rad;
use godot::prelude::*;
use std::f32::consts::PI;
// Touch and feel value
const NEGLIGIBLE_SPEED_THRESHOLD: f32 = 10.0;
const NEGLIGIBLE_SPEED_THRESHOLD_SQUARED: f32 = NEGLIGIBLE_SPEED_THRESHOLD * NEGLIGIBLE_SPEED_THRESHOLD;
const CORNER_TAPER_SPEED_THRESHOLD: f32 = 800.0;
#[derive(GodotClass)]
#[class(base=RigidBody2D)]
struct Car {
@ -43,26 +50,26 @@ impl Wheel {
#[godot_api]
impl IRigidBody2D for Car {
fn init(base: Base<RigidBody2D>) -> Self {
let engine_force = 4000000.0;
let engine_force = 2_000_000.0;
let brake_force = 2.5 * engine_force;
let max_cornering_force = 7000000.0;
let max_cornering_force = 3_000_000.0;
Self {
base,
engine_force,
brake_force,
air_drag_coefficient: 0.05,
mechanical_drag: 2000.0,
air_drag_coefficient: 0.02,
mechanical_drag: 15000.0,
max_steering_angle: deg_to_rad(32.0) as f32,
max_cornering_force,
front_wheel: Wheel {
y_distance: 200.0,
y_distance: -200.0,
has_grip: true,
},
rear_wheel: Wheel {
y_distance: -200.0,
y_distance: 200.0,
has_grip: true,
},
}
@ -70,7 +77,18 @@ impl IRigidBody2D for Car {
fn physics_process(&mut self, _delta: f64) {
// self.base_mut().set_mass(1300.0);
// self.base_mut().set_inertia(260000000000.0);
// self.base_mut().set_inertia(260000000.0);
let speed_is_negligible = self.speed_is_negligible();
let angular_speed_is_negligible = self.angular_speed_is_negligible();
if speed_is_negligible {
self.base_mut().set_linear_velocity(Vector2::default());
}
if angular_speed_is_negligible {
self.base_mut().set_angular_velocity(0.0);
}
let input = Input::singleton();
let move_forward = input.is_action_pressed("move_forward");
@ -89,8 +107,8 @@ impl IRigidBody2D for Car {
let rear_wheel_velocity = self.wheel_velocity(&self.rear_wheel);
let front_wheel_angle = match (turn_left, turn_right) {
(true, false) => self.steering_angle(),
(false, true) => -self.steering_angle(),
(true, false) => -self.steering_angle(),
(false, true) => self.steering_angle(),
(true, true) | (false, false) => 0.0,
};
@ -118,20 +136,36 @@ impl IRigidBody2D for Car {
// front wheel drive
self.base_mut().apply_force_ex(thrust_force).position(front_wheel_position).done();
let front_slip_angle = front_wheels_direction.angle_to(front_wheel_velocity);
let rear_slip_angle = rear_wheels_direction.angle_to(rear_wheel_velocity);
let speed_is_negligible = self.speed_is_negligible();
if speed_is_negligible {
self.base_mut().set_linear_velocity(Vector2::default());
}
let speed = self.speed();
// Cornering forces
if !speed_is_negligible {
self.apply_cornering_force(front_wheel_position, front_slip_angle);
self.apply_cornering_force(rear_wheel_position, rear_slip_angle);
};
if front_wheel_velocity.length_squared() > NEGLIGIBLE_SPEED_THRESHOLD_SQUARED {
let front_slip_angle = front_wheels_direction.angle_to(front_wheel_velocity);
let f_corner = self.cornering_force(front_slip_angle);
let f = if speed < CORNER_TAPER_SPEED_THRESHOLD {
// Taper at low speeds to prevent simulation instability
f_corner * speed / CORNER_TAPER_SPEED_THRESHOLD
} else {
f_corner
};
self.base_mut().apply_force_ex(f).position(front_wheel_position).done();
}
if rear_wheel_velocity.length_squared() > NEGLIGIBLE_SPEED_THRESHOLD_SQUARED {
let rear_slip_angle = rear_wheels_direction.angle_to(rear_wheel_velocity);
let f_corner = self.cornering_force(rear_slip_angle);
let f = if speed < CORNER_TAPER_SPEED_THRESHOLD {
// Taper at low speeds to prevent simulation instability
f_corner * speed / CORNER_TAPER_SPEED_THRESHOLD
} else {
f_corner
};
// Put more force on the rear wheel to correct heading.
// This is contrary to the physics though - on a normal car there is more grip on the front wheels afaict.
let f = f * 2.0;
self.base_mut().apply_force_ex(f).position(rear_wheel_position).done();
}
// Air drag
self.apply_air_drag();
@ -142,15 +176,16 @@ impl IRigidBody2D for Car {
}
impl Car {
fn apply_cornering_force(&mut self, wheel_position: Vector2, slip_angle: f32) {
fn cornering_force(&mut self, slip_angle: f32) -> Vector2 {
debug_assert!(slip_angle.is_finite());
let right = self.right();
let a = slip_angle.abs();
let a = if a > PI / 2.0 { PI - a } else { a };
let f = cornering_force(self.max_cornering_force, a);
let f = cornering_force_curve(self.max_cornering_force, a);
debug_assert!(f >= 0.0);
let force_vector = -slip_angle.sign() * right * f;
self.base_mut().apply_force_ex(force_vector).position(wheel_position).done();
-slip_angle.sign() * right * f
}
/// Returns normal vector in the direction the car is facing.
@ -179,16 +214,29 @@ impl Car {
self.base().get_linear_velocity().length()
}
/// Returns true if current speed is low enough that lateral friction on wheels etc should not be calculated.
fn speed_is_negligible(&self) -> bool {
// Touch and feel value
const THRESHOLD: f32 = 10.0;
/// Returns current angular speed.
// TODO: in what unit?
fn angular_speed(&self) -> f32 {
self.base().get_angular_velocity()
}
self.speed() < THRESHOLD
/// Returns true if current speed is low enough that the car should just be stopped.
fn speed_is_negligible(&self) -> bool {
self.speed() < NEGLIGIBLE_SPEED_THRESHOLD
}
/// Returns true if current angular speed is low enough that the car should just be stopped.
fn angular_speed_is_negligible(&self) -> bool {
// Touch and feel value
const THRESHOLD: f32 = 0.1;
self.angular_speed().abs() < THRESHOLD
}
/// Returns steering angle, which is reduced from the max steering angle, the faster the car goes.
fn steering_angle(&self) -> f32 {
return self.max_steering_angle;
// Steering angle as function of speed s.
// f(0) = 1
// f(s) -> 0 as s -> infinity
@ -222,7 +270,7 @@ impl Car {
let vel = self.base().get_linear_velocity();
if let Some(direction) = vel.try_normalized() {
// TODO: Note in book that powf is non-deterministic.
let f = self.mechanical_drag * vel.length().powf(0.8);
let f = self.mechanical_drag * vel.length().powf(0.4);
let f_vec = -direction * f;
self.base_mut().apply_force(f_vec);
}
@ -234,7 +282,8 @@ impl Car {
/// Takes positive slip angle in [0; pi/2]
///
/// Returns a non-negative value.
fn cornering_force(peak: f32, slip_angle: f32) -> f32 {
fn cornering_force_curve(peak: f32, slip_angle: f32) -> f32 {
debug_assert!(slip_angle.is_finite());
debug_assert!(slip_angle >= 0.0);
debug_assert!(slip_angle <= PI / 2.0);
@ -258,7 +307,6 @@ fn cornering_force(peak: f32, slip_angle: f32) -> f32 {
peak - c * peak * (slip_angle - peak_angle) / deg_to_rad(90.0) as f32
};
// dbg!(rad_to_deg(slip_angle as f64), peak, magnitude);
debug_assert!(magnitude >= 0.0);
debug_assert!(peak >= magnitude);