add train control game

This commit is contained in:
Overside srl
2026-01-27 19:11:56 +01:00
parent 2af450ffc5
commit 8222c7613d
238 changed files with 1778 additions and 476 deletions

9
scripts/engine.gd Normal file
View File

@@ -0,0 +1,9 @@
extends PathFollow3D
class_name TrainEngine
@onready var train: MeshInstance3D = $Train
func _physics_process(delta: float) -> void:
if train:
progress += train.current_speed * delta

1
scripts/engine.gd.uid Normal file
View File

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

View File

@@ -1,128 +1,15 @@
extends Node3D
extends Control
@onready var track_generator: TrackGenerator = $TrackGenerator
@onready var train: Train = $Train
@onready var camera: TrainCamera = $TrainCamera
var train_journey: String = "res://scenes/trainjourney.tscn"
var train_control: String = "res://scenes/train_control.tscn"
@onready var speed_label : Label = $HUD/speed_label
@onready var camera_label : Label = $HUD/camera_label
@onready var distance_label : Label = $HUD/tripdistance_label
@onready var total_distance_label : Label = $HUD/totaldistance_label
@onready var time_label : Label = $HUD/triptime_label
@onready var totaltime_label : Label = $HUD/totaltime_label
@onready var auto_save_timer = $AutoSaveTimer
@onready var trip_timer = $TripTime
var savegame_path: String = "user://savegame.save"
var total_time_in_secs : int = 0
func _ready() -> void:
# caricamento stato gioco
load_game()
# avvia autosaver
auto_save_timer.start()
# time
trip_timer.start()
train.track_generator = track_generator
camera.train = train
func _on_train_journey_pressed() -> void:
SceneSwitcher.switch_scene(train_journey)
# Terreno
var ground = MeshInstance3D.new()
ground.name = "Ground"
var plane = PlaneMesh.new()
plane.size = Vector2(2000, 2000)
ground.mesh = plane
var ground_mat = StandardMaterial3D.new()
ground_mat.albedo_color = Color(0.35, 0.48, 0.28)
ground_mat.roughness = 1.0
ground.material_override = ground_mat
ground.position.y = -0.15
add_child(ground)
func _on_train_control_pressed() -> void:
SceneSwitcher.switch_scene(train_control)
func _process(_delta: float) -> void:
_update_ui()
_update_ground()
if Input.is_action_just_pressed("ui_cancel"):
get_tree().quit()
func _update_ui() -> void:
if speed_label and train:
var kmh = train.get_speed() * 3.6
speed_label.text = "Velocità: %.0f km/h" % kmh
if distance_label and train:
var km = train.distance_traveled / 1000.0
distance_label.text = "Distanza viaggio: %.2f km" % km
if total_distance_label and train:
Gamestate.total_distance = train.distance_traveled
var km = (Gamestate.total_distance + Gamestate.initial_distance) / 1000
total_distance_label.text = "Distanza totale: %.2f km" % km
if time_label and train:
var h = int(total_time_in_secs / 3600.0)
var m = int(total_time_in_secs / 60.0)
var s = total_time_in_secs - m * 60
time_label.text = "Tempo viaggio: %02d:%02d:%02d" % [h, m, s]
if totaltime_label and train:
Gamestate.total_time = total_time_in_secs
var tot = Gamestate.total_time + Gamestate.initial_time
var h = int(tot / 3600.0)
var m = int(tot / 60.0)
var s = tot - m * 60
totaltime_label.text = "Tempo totale: %02d:%02d:%02d" % [h, m, s]
if camera_label and camera:
camera_label.text = "Camera: %s" % camera.get_mode_name()
func _update_ground() -> void:
var ground = get_node_or_null("Ground")
if ground and train:
var pos = train.get_locomotive_position()
ground.position.x = pos.x
ground.position.z = pos.z
func save_game():
var save_file = FileAccess.open(savegame_path, FileAccess.WRITE)
var save_nodes = get_tree().get_nodes_in_group("save_data")
for node in save_nodes:
if !node.has_method("save"):
continue
var node_data = node.call("save") # ogni nodo implementa il suo save()
save_file.store_line(JSON.stringify(node_data))
func load_game():
if not FileAccess.file_exists(savegame_path):
return # nessun salvataggio trovato
var save_file = FileAccess.open(savegame_path, FileAccess.READ)
while save_file.get_position() < save_file.get_length():
var json_string = save_file.get_line()
var node_data = JSON.parse_string(json_string)
if node_data == null:
continue
if node_data["filename"] == "res://scenes/gamestate.tscn" and node_data["parent"] == "/root":
Gamestate.SetInitialValue(node_data["total_distance"], node_data["total_time"])
func _on_timer_timeout() -> void:
save_game()
func _on_trip_time_timeout() -> void:
total_time_in_secs += 1
func _on_exit_pressed() -> void:
get_tree().quit()

View File

@@ -1 +1 @@
uid://dfyo0h5yj3us8
uid://cfjaa6hq1wpvd

19
scripts/scene_switcher.gd Normal file
View File

@@ -0,0 +1,19 @@
extends Node
var current_scene = null
func _ready() -> void:
var root = get_tree().root
current_scene = root.get_child(root.get_child_count() - 1)
func switch_scene(res_path):
call_deferred("_deferred_switch_scene", res_path)
func _deferred_switch_scene(res_path):
current_scene.free()
var scene = load(res_path)
current_scene = scene.instantiate()
get_tree().root.add_child(current_scene)
get_tree().current_scene = current_scene

View File

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

31
scripts/train_movement.gd Normal file
View File

@@ -0,0 +1,31 @@
extends MeshInstance3D
signal speed_changed(new_speed: float)
@export_group("Train")
@export var base_speed: float = 15.0
@export var max_speed: float = 50.0
@export var min_speed: float = 3.0
@export var acceleration: float = 12.0
var current_speed: float = 15.0
func _ready() -> void:
current_speed = base_speed
func _process(delta: float) -> void:
_handle_input(delta)
func _handle_input(delta: float) -> void:
if Input.is_action_pressed("speed_up"):
current_speed = min(current_speed + acceleration * delta, max_speed)
speed_changed.emit(current_speed)
elif Input.is_action_pressed("speed_down"):
current_speed = max(current_speed - acceleration * delta, min_speed)
speed_changed.emit(current_speed)
func get_speed() -> float:
return current_speed

View File

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

24
scripts/traincontrol.gd Normal file
View File

@@ -0,0 +1,24 @@
extends Node3D
@export var main_scene: PackedScene
@onready var speed_label : Label = $HUD/speed_label
@onready var camera_label : Label = $HUD/camera_label
@onready var train: MeshInstance3D = $Ground/Rails/TrainEngine/Train
@onready var camera: TrainControlCamera = $TrainCamera
func _process(_delta: float) -> void:
_update_ui()
if Input.is_action_just_pressed("ui_cancel"):
SceneSwitcher.switch_scene(main_scene.resource_path)
func _update_ui() -> void:
if speed_label and train:
var kmh = train.get_speed() * 3.6
speed_label.text = "Velocità: %.0f km/h" % kmh
if camera_label and camera:
camera_label.text = "Camera: %s" % camera.get_mode_name()

View File

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

View File

@@ -0,0 +1,127 @@
extends Camera3D
class_name TrainControlCamera
@export var train: Node3D
enum CameraMode { FOLLOW, SIDE, TOP, COCKPIT, CINEMATIC }
@export_group("Camera")
@export var follow_distance: float = 18.0
@export var follow_height: float = 7.0
@export var smoothness: float = 4.0
var mode: CameraMode = CameraMode.FOLLOW
var orbit_angle: float = 0.0
var cinematic_angle: float = 0.0
func _process(delta):
_handle_input(delta)
match mode:
CameraMode.FOLLOW:
_update_follow(delta)
CameraMode.SIDE:
_update_side(delta)
CameraMode.TOP:
_update_top(delta)
CameraMode.COCKPIT:
_update_cockpit(delta)
CameraMode.CINEMATIC:
_update_cinematic(delta)
func _handle_input(delta: float) -> void:
if Input.is_action_just_pressed("ui_accept"):
mode = ((mode + 1) % CameraMode.size()) as CameraMode
orbit_angle = 0.0
if Input.is_action_pressed("camera_left"):
orbit_angle -= 2.0 * delta
elif Input.is_action_pressed("camera_right"):
orbit_angle += 2.0 * delta
orbit_angle = clamp(orbit_angle, -PI/2.5, PI/2.5)
func _update_follow(delta: float) -> void:
if not train:
return
var train_pos = train.global_position
var forward = -train.global_transform.basis.z
var right = train.global_transform.basis.x
var rotated = forward * cos(orbit_angle) + right * sin(orbit_angle)
var target_pos = train_pos - rotated * follow_distance + Vector3.UP * follow_height
var target_look = train_pos + Vector3.UP * 2.0
global_position = global_position.lerp(target_pos, smoothness * delta)
look_at(target_look, Vector3.UP)
func _update_side(delta: float) -> void:
if not train:
return
var train_pos = train.global_position
var right = train.global_transform.basis.x
var side_dir = sign(orbit_angle) if abs(orbit_angle) > 0.1 else 1.0
var target_pos = train_pos + right * 12.0 * side_dir + Vector3.UP * 4.0
global_position = global_position.lerp(target_pos, smoothness * delta)
look_at(train_pos + Vector3.UP * 1.5, Vector3.UP)
func _update_top(delta: float) -> void:
if not train:
return
var train_pos = train.global_position
var target_pos = train_pos + Vector3.UP * 35.0
global_position = global_position.lerp(target_pos, smoothness * delta)
look_at(train_pos, Vector3.FORWARD)
func _update_cockpit(delta: float) -> void:
if not train:
return
# Offset: dentro la cabina
var cockpit_pos = train.global_position + train.global_transform.basis * Vector3(0, 3.2, 1.5)
var forward_point = train.global_position - (-train.global_transform.basis.z) * 40.0
var look_offset = train.global_transform.basis.x * orbit_angle * 15.0
global_position = global_position.lerp(cockpit_pos, smoothness * 2.0 * delta)
look_at(forward_point + look_offset + Vector3.UP * 1.0, Vector3.UP)
func _update_cinematic(delta: float) -> void:
if not train:
return
var train_pos = train.global_position
cinematic_angle += 0.25 * delta
var offset = Vector3(
cos(cinematic_angle) * 22.0,
8.0 + sin(cinematic_angle * 0.5) * 4.0,
sin(cinematic_angle) * 22.0
)
var target_pos = train_pos + offset
global_position = global_position.lerp(target_pos, smoothness * 0.4 * delta)
look_at(train_pos + Vector3.UP * 1.5, Vector3.UP)
func get_mode_name() -> String:
match mode:
CameraMode.FOLLOW: return "Segue"
CameraMode.SIDE: return "Laterale"
CameraMode.TOP: return "Alto"
CameraMode.COCKPIT: return "Prima persona"
CameraMode.CINEMATIC: return "Cinematica"
return "Sconosciuta"

View File

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

129
scripts/trainjourney.gd Normal file
View File

@@ -0,0 +1,129 @@
extends Node3D
@onready var track_generator: TrackGenerator = $TrackGenerator
@onready var train: Train = $Train
@onready var camera: TrainJourneyCamera = $TrainCamera
@onready var speed_label : Label = $HUD/speed_label
@onready var camera_label : Label = $HUD/camera_label
@onready var distance_label : Label = $HUD/tripdistance_label
@onready var total_distance_label : Label = $HUD/totaldistance_label
@onready var time_label : Label = $HUD/triptime_label
@onready var totaltime_label : Label = $HUD/totaltime_label
@onready var auto_save_timer = $AutoSaveTimer
@onready var trip_timer = $TripTime
var savegame_path: String = "user://savegame.save"
var total_time_in_secs : int = 0
@export var main_scene: PackedScene
func _ready() -> void:
# caricamento stato gioco
load_game()
# avvia autosaver
auto_save_timer.start()
# time
trip_timer.start()
train.track_generator = track_generator
camera.train = train
# Terreno
var ground = MeshInstance3D.new()
ground.name = "Ground"
var plane = PlaneMesh.new()
plane.size = Vector2(2000, 2000)
ground.mesh = plane
var ground_mat = StandardMaterial3D.new()
ground_mat.albedo_color = Color(0.35, 0.48, 0.28)
ground_mat.roughness = 1.0
ground.material_override = ground_mat
ground.position.y = -0.15
add_child(ground)
func _process(_delta: float) -> void:
_update_ui()
_update_ground()
if Input.is_action_just_pressed("ui_cancel"):
SceneSwitcher.switch_scene(main_scene.resource_path)
func _update_ui() -> void:
if speed_label and train:
var kmh = train.get_speed() * 3.6
speed_label.text = "Velocità: %.0f km/h" % kmh
if distance_label and train:
var km = train.distance_traveled / 1000.0
distance_label.text = "Distanza viaggio: %.2f km" % km
if total_distance_label and train:
Gamestate.total_distance = train.distance_traveled
var km = (Gamestate.total_distance + Gamestate.initial_distance) / 1000
total_distance_label.text = "Distanza totale: %.2f km" % km
if time_label and train:
var h = int(total_time_in_secs / 3600.0)
var m = int(total_time_in_secs / 60.0)
var s = total_time_in_secs - m * 60
time_label.text = "Tempo viaggio: %02d:%02d:%02d" % [h, m, s]
if totaltime_label and train:
Gamestate.total_time = total_time_in_secs
var tot = Gamestate.total_time + Gamestate.initial_time
var h = int(tot / 3600.0)
var m = int(tot / 60.0)
var s = tot - m * 60
totaltime_label.text = "Tempo totale: %02d:%02d:%02d" % [h, m, s]
if camera_label and camera:
camera_label.text = "Camera: %s" % camera.get_mode_name()
func _update_ground() -> void:
var ground = get_node_or_null("Ground")
if ground and train:
var pos = train.get_locomotive_position()
ground.position.x = pos.x
ground.position.z = pos.z
func save_game():
var save_file = FileAccess.open(savegame_path, FileAccess.WRITE)
var save_nodes = get_tree().get_nodes_in_group("save_data")
for node in save_nodes:
if !node.has_method("save"):
continue
var node_data = node.call("save") # ogni nodo implementa il suo save()
save_file.store_line(JSON.stringify(node_data))
func load_game():
if not FileAccess.file_exists(savegame_path):
return # nessun salvataggio trovato
var save_file = FileAccess.open(savegame_path, FileAccess.READ)
while save_file.get_position() < save_file.get_length():
var json_string = save_file.get_line()
var node_data = JSON.parse_string(json_string)
if node_data == null:
continue
if node_data["filename"] == "res://scenes/gamestate.tscn" and node_data["parent"] == "/root":
Gamestate.SetInitialValue(node_data["total_distance"], node_data["total_time"])
func _on_timer_timeout() -> void:
save_game()
func _on_trip_time_timeout() -> void:
total_time_in_secs += 1

View File

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

View File

@@ -1,5 +1,5 @@
extends Camera3D
class_name TrainCamera
class_name TrainJourneyCamera
enum CameraMode { FOLLOW, SIDE, TOP, COCKPIT, CINEMATIC }