Start adding replays
This commit is contained in:
parent
b8578a0a95
commit
7a9292242f
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
98
godot/scenes/player/replay_player.tscn
Normal file
98
godot/scenes/player/replay_player.tscn
Normal 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)
|
||||
@ -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<u8, u16>,
|
||||
|
||||
pub replay_recorder: Option<Gd<ReplayRecorder>>,
|
||||
pub current_replay_playback: Option<Gd<ReplayPlayback>>,
|
||||
|
||||
base: Base<Node>,
|
||||
}
|
||||
|
||||
@ -342,6 +346,17 @@ impl 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) {
|
||||
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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<Gd<PackedScene>>,
|
||||
#[export]
|
||||
pub replay_player: Option<Gd<PackedScene>>,
|
||||
#[export]
|
||||
pub ball: Option<Gd<PackedScene>>,
|
||||
|
||||
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(
|
||||
&mut self,
|
||||
transform: Option<NetTransform>,
|
||||
|
||||
@ -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.);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
181
rust/src/replay.rs
Normal file
181
rust/src/replay.rs
Normal 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,
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user