Start adding replays

This commit is contained in:
Sofia 2026-07-26 19:20:59 +03:00
parent b8578a0a95
commit 7a9292242f
8 changed files with 334 additions and 3 deletions

View File

@ -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://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://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://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"] [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") ball_spawner = NodePath("ball_spawner")
local_player = ExtResource("1_yjvui") local_player = ExtResource("1_yjvui")
remote_player = ExtResource("2_kcncr") remote_player = ExtResource("2_kcncr")
replay_player = ExtResource("3_8af6u")
ball = ExtResource("3_e0nj3") ball = ExtResource("3_e0nj3")
[node name="terrain" type="MeshInstance3D" parent="." unique_id=1474127252] [node name="terrain" type="MeshInstance3D" parent="." unique_id=1474127252]

View File

@ -5,7 +5,7 @@
[sub_resource type="SphereShape3D" id="SphereShape3D_1cgct"] [sub_resource type="SphereShape3D" id="SphereShape3D_1cgct"]
[node name="ball" type="Ball" unique_id=816748600] [node name="ball" type="Ball" unique_id=816748600]
collision_layer = 12 collision_layer = 8
collision_mask = 7 collision_mask = 7
contact_monitor = true contact_monitor = true
max_contacts_reported = 10 max_contacts_reported = 10

View File

@ -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)

View File

@ -23,6 +23,7 @@ use crate::{
DamageSource, IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, DamageSource, IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer,
remote_player::RemotePlayer, weapon::WeaponType, remote_player::RemotePlayer, weapon::WeaponType,
}, },
replay::{ReplayPlayback, ReplayRecorder},
team_resource::TeamResource, team_resource::TeamResource,
ui::cli::{Command, CommandLinePanel}, ui::cli::{Command, CommandLinePanel},
}; };
@ -229,6 +230,9 @@ pub struct Game {
pub goals: HashMap<u8, u16>, pub goals: HashMap<u8, u16>,
pub replay_recorder: Option<Gd<ReplayRecorder>>,
pub current_replay_playback: Option<Gd<ReplayPlayback>>,
base: Base<Node>, base: Base<Node>,
} }
@ -342,6 +346,17 @@ impl Game {
.map(|g| g.cast::<Game>()) .map(|g| g.cast::<Game>())
} }
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) { pub fn start_game(&mut self) {
self.game_started = true; self.game_started = true;
if let Some(map) = &self.selected_map { 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) { pub fn reset_game(&mut self) {

View File

@ -13,6 +13,7 @@ pub mod map_resource;
pub mod net; pub mod net;
pub mod player; pub mod player;
pub mod popup_queue; pub mod popup_queue;
pub mod replay;
pub mod team_resource; pub mod team_resource;
pub mod ui; pub mod ui;

View File

@ -6,6 +6,7 @@ use crate::{
player::{ player::{
IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, remote_player::RemotePlayer, IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, remote_player::RemotePlayer,
}, },
replay::ReplayPlayerState,
}; };
#[derive(GodotClass)] #[derive(GodotClass)]
@ -20,6 +21,8 @@ pub struct Map {
#[export] #[export]
pub remote_player: Option<Gd<PackedScene>>, pub remote_player: Option<Gd<PackedScene>>,
#[export] #[export]
pub replay_player: Option<Gd<PackedScene>>,
#[export]
pub ball: Option<Gd<PackedScene>>, pub ball: Option<Gd<PackedScene>>,
base: Base<Node3D>, base: Base<Node3D>,
@ -93,6 +96,31 @@ impl Map {
} }
} }
pub fn spawn_replay_player(
&mut self,
player_id: u16,
state: ReplayPlayerState,
) -> Option<Gd<RemotePlayer>> {
if let Some(replay_player) = &self.replay_player {
let mut node: Gd<RemotePlayer> = 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( pub fn spawn_ball(
&mut self, &mut self,
transform: Option<NetTransform>, transform: Option<NetTransform>,

View File

@ -350,10 +350,10 @@ impl ICharacterBody3D for LocalPlayer {
if let Some(camera) = &self.camera { if let Some(camera) = &self.camera {
if camera.is_current() && self.dead_for > 3. { 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 Some(killer_id) = self.killer_id
{ {
let game = game.bind(); let mut game = game.bind_mut();
let remote_player = game.find_player(killer_id); let remote_player = game.find_player(killer_id);
if let Some(remote_player) = &remote_player if let Some(remote_player) = &remote_player
&& let Some(remote_character) = remote_player.character.clone() && let Some(remote_character) = remote_player.character.clone()
@ -363,6 +363,8 @@ impl ICharacterBody3D for LocalPlayer {
{ {
camera.set_current(true); camera.set_current(true);
} }
game.start_playback(-5.);
} }
} }
} }

181
rust/src/replay.rs Normal file
View File

@ -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<Gd<Map>>,
current_frame_idx: usize,
player_characters: HashMap<u16, Gd<RemotePlayer>>,
pub current_time: f64,
base: Base<Node>,
}
#[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<Node>,
}
#[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<u16, ReplayPlayer>,
frames: Vec<ReplayFrame>,
}
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<u16, ReplayPlayerState>,
events: Vec<ReplayEvent>,
}
#[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<ReplayPlayerState> {
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,
}