From 7a9292242f88cd073397f502f162ce78a6348d60 Mon Sep 17 00:00:00 2001 From: Sofia Date: Sun, 26 Jul 2026 19:20:59 +0300 Subject: [PATCH] Start adding replays --- godot/maps/default_map/map0.tscn | 2 + godot/scenes/misc/ball.tscn | 2 +- godot/scenes/player/replay_player.tscn | 98 +++++++++++++ rust/src/game_manager.rs | 19 +++ rust/src/lib.rs | 1 + rust/src/map.rs | 28 ++++ rust/src/player/local_player.rs | 6 +- rust/src/replay.rs | 181 +++++++++++++++++++++++++ 8 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 godot/scenes/player/replay_player.tscn create mode 100644 rust/src/replay.rs diff --git a/godot/maps/default_map/map0.tscn b/godot/maps/default_map/map0.tscn index 6cfc5cd..1fc2762 100644 --- a/godot/maps/default_map/map0.tscn +++ b/godot/maps/default_map/map0.tscn @@ -2,6 +2,7 @@ [ext_resource type="PackedScene" uid="uid://ct70bpbca8one" path="res://scenes/player/local_player.tscn" id="1_yjvui"] [ext_resource type="PackedScene" uid="uid://kfxc0migw80o" path="res://scenes/player/remote_player.tscn" id="2_kcncr"] +[ext_resource type="PackedScene" uid="uid://hmh2lrrlo2wy" path="res://scenes/player/replay_player.tscn" id="3_8af6u"] [ext_resource type="PackedScene" uid="uid://dfnbc8sbufa4j" path="res://scenes/misc/ball.tscn" id="3_e0nj3"] [ext_resource type="PackedScene" uid="uid://c1r8nk58cci3a" path="res://scenes/misc/goal.tscn" id="4_w7til"] @@ -24,6 +25,7 @@ spawners = [NodePath("team1_spawner"), NodePath("team2_spawner")] ball_spawner = NodePath("ball_spawner") local_player = ExtResource("1_yjvui") remote_player = ExtResource("2_kcncr") +replay_player = ExtResource("3_8af6u") ball = ExtResource("3_e0nj3") [node name="terrain" type="MeshInstance3D" parent="." unique_id=1474127252] diff --git a/godot/scenes/misc/ball.tscn b/godot/scenes/misc/ball.tscn index 20325c4..e2899c4 100644 --- a/godot/scenes/misc/ball.tscn +++ b/godot/scenes/misc/ball.tscn @@ -5,7 +5,7 @@ [sub_resource type="SphereShape3D" id="SphereShape3D_1cgct"] [node name="ball" type="Ball" unique_id=816748600] -collision_layer = 12 +collision_layer = 8 collision_mask = 7 contact_monitor = true max_contacts_reported = 10 diff --git a/godot/scenes/player/replay_player.tscn b/godot/scenes/player/replay_player.tscn new file mode 100644 index 0000000..bdec3a9 --- /dev/null +++ b/godot/scenes/player/replay_player.tscn @@ -0,0 +1,98 @@ +[gd_scene format=3 uid="uid://hmh2lrrlo2wy"] + +[ext_resource type="PackedScene" uid="uid://bod6b3befmyt5" path="res://scenes/player/soldier_m.tscn" id="1_bfcd3"] +[ext_resource type="PackedScene" uid="uid://cl66unms2gim0" path="res://scenes/player/effects.tscn" id="2_rejks"] + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1cgct"] + +[sub_resource type="ViewportTexture" id="ViewportTexture_1cgct"] +viewport_path = NodePath("health_bar_sprite/health_bar_viewport") + +[sub_resource type="Curve" id="Curve_yli5f"] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0] +point_count = 2 + +[sub_resource type="CurveTexture" id="CurveTexture_3g557"] +width = 70 +curve = SubResource("Curve_yli5f") + +[sub_resource type="Curve" id="Curve_dmbod"] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0] +point_count = 2 + +[sub_resource type="CurveTexture" id="CurveTexture_1cgct"] +width = 70 +curve = SubResource("Curve_dmbod") + +[sub_resource type="Curve" id="Curve_qn18y"] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0] +point_count = 2 + +[sub_resource type="CurveTexture" id="CurveTexture_840kl"] +width = 70 +curve = SubResource("Curve_qn18y") + +[node name="player" type="RemotePlayer" unique_id=845968872 node_paths=PackedStringArray("soldier_mesh", "collider", "effects", "spectator_camera", "camera_origin", "name_label", "health_bar", "health_bar_sprite")] +move_speed = 5.0 +soldier_mesh = NodePath("soldier_m") +collider = NodePath("collision_shape_3d") +effects = NodePath("effects") +health = 100 +punch_cooldown = 1.0 +spectator_camera = NodePath("spectate_root/spectate_camera") +camera_origin = NodePath("camera_origin") +name_label = NodePath("name_label") +health_bar = NodePath("health_bar_sprite/health_bar_viewport/health_bar") +health_bar_sprite = NodePath("health_bar_sprite") +collision_layer = 16 +collision_mask = 4 + +[node name="soldier_m" parent="." unique_id=281995630 instance=ExtResource("1_bfcd3")] +transform = Transform3D(-0.5, 0, -4.371139e-08, 0, 0.5, 0, 4.371139e-08, 0, -0.5, 0, 0, 0) + +[node name="collision_shape_3d" type="CollisionShape3D" parent="." unique_id=1110995513] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +shape = SubResource("CapsuleShape3D_1cgct") + +[node name="effects" parent="." unique_id=986677517 instance=ExtResource("2_rejks")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4540546, 0) + +[node name="spectate_root" type="Node3D" parent="." unique_id=624336254] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.7611488, 0) + +[node name="spectate_camera" type="Camera3D" parent="spectate_root" unique_id=1408688016] +transform = Transform3D(1, 0, 0, 0, 0.89379096, 0.44848388, 0, -0.44848388, 0.89379096, 0, 0.6601318, 1.0407685) + +[node name="camera_origin" type="Node3D" parent="." unique_id=1445680156] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8724979, -0.2644562) + +[node name="name_label" type="Label3D" parent="." unique_id=142179925] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.3304863, 0) +pixel_size = 0.002 +billboard = 1 +text = "asd" +font_size = 64 + +[node name="health_bar_sprite" type="Sprite3D" parent="." unique_id=1478315510] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.1606798, 0) +billboard = 1 +texture = SubResource("ViewportTexture_1cgct") + +[node name="health_bar_viewport" type="SubViewport" parent="health_bar_sprite" unique_id=1819065918] +disable_3d = true +transparent_bg = true +size = Vector2i(70, 10) +size_2d_override = Vector2i(70, 10) +size_2d_override_stretch = true + +[node name="health_bar" type="TextureProgressBar" parent="health_bar_sprite/health_bar_viewport" unique_id=121411216] +offset_right = 70.0 +offset_bottom = 1.0 +scale = Vector2(1, 5) +value = 100.0 +texture_under = SubResource("CurveTexture_3g557") +texture_over = SubResource("CurveTexture_1cgct") +texture_progress = SubResource("CurveTexture_840kl") +tint_under = Color(0, 0, 0, 0.3647059) +tint_over = Color(1, 1, 1, 0) +tint_progress = Color(0.5660922, 1.7688051e-06, 3.3691526e-07, 1) diff --git a/rust/src/game_manager.rs b/rust/src/game_manager.rs index 1f73c9a..4916c84 100644 --- a/rust/src/game_manager.rs +++ b/rust/src/game_manager.rs @@ -23,6 +23,7 @@ use crate::{ DamageSource, IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, remote_player::RemotePlayer, weapon::WeaponType, }, + replay::{ReplayPlayback, ReplayRecorder}, team_resource::TeamResource, ui::cli::{Command, CommandLinePanel}, }; @@ -229,6 +230,9 @@ pub struct Game { pub goals: HashMap, + pub replay_recorder: Option>, + pub current_replay_playback: Option>, + base: Base, } @@ -342,6 +346,17 @@ impl Game { .map(|g| g.cast::()) } + pub fn start_playback(&mut self, time_delta: f64) { + if let Some(recorder) = &self.replay_recorder { + let mut playback = ReplayPlayback::new_alloc(); + playback.bind_mut().replay = recorder.bind().replay.clone(); + playback.bind_mut().current_map = self.current_map.clone(); + playback.bind_mut().current_time = recorder.bind().time_elapsed + time_delta; + self.current_replay_playback = Some(playback.clone()); + self.base_mut().add_child(&playback); + } + } + pub fn start_game(&mut self) { self.game_started = true; if let Some(map) = &self.selected_map { @@ -362,6 +377,10 @@ impl Game { } } }); + + let recorder = ReplayRecorder::new_alloc(); + self.replay_recorder = Some(recorder.clone()); + self.base_mut().add_child(&recorder); } pub fn reset_game(&mut self) { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index ac9e1fb..b3c8a24 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -13,6 +13,7 @@ pub mod map_resource; pub mod net; pub mod player; pub mod popup_queue; +pub mod replay; pub mod team_resource; pub mod ui; diff --git a/rust/src/map.rs b/rust/src/map.rs index 9af2544..6d1b6e2 100644 --- a/rust/src/map.rs +++ b/rust/src/map.rs @@ -6,6 +6,7 @@ use crate::{ player::{ IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, remote_player::RemotePlayer, }, + replay::ReplayPlayerState, }; #[derive(GodotClass)] @@ -20,6 +21,8 @@ pub struct Map { #[export] pub remote_player: Option>, #[export] + pub replay_player: Option>, + #[export] pub ball: Option>, base: Base, @@ -93,6 +96,31 @@ impl Map { } } + pub fn spawn_replay_player( + &mut self, + player_id: u16, + state: ReplayPlayerState, + ) -> Option> { + if let Some(replay_player) = &self.replay_player { + let mut node: Gd = replay_player.instantiate_as(); + node.bind_mut().player_id = Some(player_id); + self.base_mut().add_child(&node); + + node.bind_mut() + .base_mut() + .set_global_position(state.transform.location.into()); + node.bind_mut() + .base_mut() + .set_global_rotation(state.transform.rotation.into()); + node.bind_mut().set_movement_dir(state.movement_dir.into()); + node.bind_mut().apply_look_up(state.look_up); + + Some(node) + } else { + None + } + } + pub fn spawn_ball( &mut self, transform: Option, diff --git a/rust/src/player/local_player.rs b/rust/src/player/local_player.rs index a4749a1..611e741 100644 --- a/rust/src/player/local_player.rs +++ b/rust/src/player/local_player.rs @@ -350,10 +350,10 @@ impl ICharacterBody3D for LocalPlayer { if let Some(camera) = &self.camera { if camera.is_current() && self.dead_for > 3. { - if let Some(game) = Game::singleton() + if let Some(mut game) = Game::singleton() && let Some(killer_id) = self.killer_id { - let game = game.bind(); + let mut game = game.bind_mut(); let remote_player = game.find_player(killer_id); if let Some(remote_player) = &remote_player && let Some(remote_character) = remote_player.character.clone() @@ -363,6 +363,8 @@ impl ICharacterBody3D for LocalPlayer { { camera.set_current(true); } + + game.start_playback(-5.); } } } diff --git a/rust/src/replay.rs b/rust/src/replay.rs new file mode 100644 index 0000000..ac9e8f4 --- /dev/null +++ b/rust/src/replay.rs @@ -0,0 +1,181 @@ +use std::collections::HashMap; + +use godot::prelude::*; + +use crate::{ + game_manager::{Game, Player}, + map::Map, + net::util::NetVector3, + player::{IPlayer, NetTransform, remote_player::RemotePlayer, weapon::WeaponType}, +}; + +#[derive(GodotClass)] +#[class(base=Node, init)] +pub struct ReplayPlayback { + pub replay: Replay, + pub current_map: Option>, + current_frame_idx: usize, + player_characters: HashMap>, + + pub current_time: f64, + + base: Base, +} + +#[godot_api] +impl INode for ReplayPlayback { + fn ready(&mut self) {} + + fn physics_process(&mut self, delta: f64) { + self.current_time += delta; + + while let Some(frame) = self.replay.frames.get(self.current_frame_idx) + && frame.time <= self.current_time + { + if let Some(map) = &mut self.current_map { + for (id, data) in &frame.player_data { + if !self.player_characters.contains_key(id) { + if let Some(player) = map.bind_mut().spawn_replay_player(*id, data.clone()) + { + self.player_characters.insert(*id, player); + } + } + } + } + + for (id, mut character) in self.player_characters.clone() { + if let Some(state) = frame.player_data.get(&id) { + character.bind_mut().apply_transform(state.transform); + character.bind_mut().apply_look_up(state.look_up); + character + .bind_mut() + .set_movement_dir(state.movement_dir.into()) + } else { + character.queue_free(); + self.player_characters.remove(&id); + } + } + self.current_frame_idx += 1; + } + } +} + +#[derive(GodotClass)] +#[class(base=Node, init)] +pub struct ReplayRecorder { + pub time_elapsed: f64, + + pub replay: Replay, + current_frame: ReplayFrame, + + base: Base, +} + +#[godot_api] +impl INode for ReplayRecorder { + fn ready(&mut self) { + self.run_deferred(|s| { + s.current_frame = s.record_frame(); + }); + } + + fn physics_process(&mut self, delta: f64) { + self.time_elapsed += delta; + + if self.time_elapsed > (self.current_frame.time + (1. / 60.)) { + self.replay.frames.push(self.current_frame.clone()); + self.current_frame = self.record_frame(); + } + } +} + +impl ReplayRecorder { + fn record_frame(&mut self) -> ReplayFrame { + let mut player_data = HashMap::new(); + + if let Some(game) = Game::singleton() { + for player in &game.bind().players { + if let Some(state) = player.get_state() { + player_data.insert(player.id(), state); + } + } + } + + ReplayFrame { + time: self.time_elapsed, + player_data, + events: Vec::new(), + } + } +} + +#[derive(Debug, Clone)] +pub struct Replay { + players: HashMap, + frames: Vec, +} + +impl Default for Replay { + fn default() -> Self { + Self { + players: Default::default(), + frames: Default::default(), + } + } +} + +#[derive(Debug, Clone)] +pub struct ReplayPlayer { + id: u16, + name: String, + initial_state: ReplayPlayerState, +} + +#[derive(Debug, Default, Clone)] +pub struct ReplayFrame { + time: f64, + player_data: HashMap, + events: Vec, +} + +#[derive(Debug, Clone)] +pub enum ReplayEvent {} + +#[derive(Debug)] +pub enum ReplayBall { + World(ReplayBallData), + Held, +} + +#[derive(Debug, Clone)] +pub struct ReplayPlayerState { + pub transform: NetTransform, + pub movement_dir: NetVector3, + pub look_up: f32, + pub current_weapon: WeaponType, +} + +impl Player { + fn get_state(&self) -> Option { + if let Some(character) = &self.character { + Some(ReplayPlayerState { + transform: character.dyn_bind().get_transform(), + movement_dir: character.dyn_bind().get_movement_dir().into(), + look_up: character.dyn_bind().get_look_up(), + current_weapon: character + .dyn_bind() + .get_weapon() + .map(|w| w.dyn_bind().get_type()) + .unwrap_or(WeaponType::Raygun), + }) + } else { + None + } + } +} + +#[derive(Debug)] +pub struct ReplayBallData { + transform: NetTransform, + velocity: NetVector3, +}