Compare commits

...

3 Commits

Author SHA1 Message Date
698c9067a7 Implement spectator camera 2026-07-20 23:48:07 +03:00
82fcfe0ea0 Make sure killer and damage-dealer player id is propagated 2026-07-20 23:36:46 +03:00
c6d59ffb04 Add respawn timer ui element 2026-07-20 23:30:49 +03:00
7 changed files with 126 additions and 45 deletions

View File

@ -3,5 +3,5 @@
[ext_resource type="MapResource" uid="uid://c7n1tacc7a73t" path="res://maps/default_map.tres" id="1_glms7"] [ext_resource type="MapResource" uid="uid://c7n1tacc7a73t" path="res://maps/default_map.tres" id="1_glms7"]
[node name="game_manager" type="GameManager" unique_id=1442044769] [node name="game_manager" type="GameManager" unique_id=1442044769]
respawn_timer = 5.0 respawn_timer = 10.0
maps = Array[MapResource]([ExtResource("1_glms7")]) maps = Array[MapResource]([ExtResource("1_glms7")])

View File

@ -8,7 +8,7 @@ glow_enabled = true
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_oh8vq"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_oh8vq"]
[node name="player" type="LocalPlayer" unique_id=863055906 node_paths=PackedStringArray("camera", "soldier_mesh", "collider", "splorch", "dead_cam_target")] [node name="player" type="LocalPlayer" unique_id=863055906 node_paths=PackedStringArray("camera", "soldier_mesh", "collider", "splorch", "dead_cam_target", "respawn_label")]
look_sensitivity = 1.5 look_sensitivity = 1.5
camera = NodePath("camera_3d") camera = NodePath("camera_3d")
move_speed = 5.0 move_speed = 5.0
@ -16,6 +16,7 @@ soldier_mesh = NodePath("soldier_m")
collider = NodePath("collision_shape_3d2") collider = NodePath("collision_shape_3d2")
splorch = NodePath("splorch") splorch = NodePath("splorch")
dead_cam_target = NodePath("dead_camera_target") dead_cam_target = NodePath("dead_camera_target")
respawn_label = NodePath("respawn_label")
health = 100 health = 100
collision_mask = 4 collision_mask = 4
@ -37,3 +38,17 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.16157162, 1.4223524, 0)
[node name="dead_camera_target" type="Node3D" parent="." unique_id=559969559] [node name="dead_camera_target" type="Node3D" parent="." unique_id=559969559]
transform = Transform3D(1, 0, 0, 0, 0.9069022, 0.4213412, 0, -0.4213412, 0.9069022, 0, 2.3258846, 1.5254744) transform = Transform3D(1, 0, 0, 0, 0.9069022, 0.4213412, 0, -0.4213412, 0.9069022, 0, 2.3258846, 1.5254744)
[node name="respawn_label" type="Label" parent="." unique_id=595642200]
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -20.0
offset_top = -11.5
offset_right = 20.0
offset_bottom = 11.5
grow_horizontal = 2
grow_vertical = 2
horizontal_alignment = 1

View File

@ -5,12 +5,13 @@
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1cgct"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1cgct"]
[node name="player" type="RemotePlayer" unique_id=845968872 node_paths=PackedStringArray("soldier_mesh", "collider", "splorch")] [node name="player" type="RemotePlayer" unique_id=845968872 node_paths=PackedStringArray("soldier_mesh", "collider", "splorch", "spectator_camera")]
move_speed = 5.0 move_speed = 5.0
soldier_mesh = NodePath("soldier_m") soldier_mesh = NodePath("soldier_m")
collider = NodePath("collision_shape_3d") collider = NodePath("collision_shape_3d")
splorch = NodePath("gpu_particles_3d") splorch = NodePath("gpu_particles_3d")
health = 100 health = 100
spectator_camera = NodePath("spectate_root/spectate_camera")
collision_layer = 2 collision_layer = 2
collision_mask = 4 collision_mask = 4
@ -23,3 +24,9 @@ shape = SubResource("CapsuleShape3D_1cgct")
[node name="gpu_particles_3d" parent="." unique_id=986677517 instance=ExtResource("2_orxjf")] [node name="gpu_particles_3d" parent="." unique_id=986677517 instance=ExtResource("2_orxjf")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4540546, 0) 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)

View File

@ -324,23 +324,27 @@ impl Game {
.bind_mut() .bind_mut()
.signals() .signals()
.on_die() .on_die()
.connect_other(self, move |s| s.on_death(id)); .connect_other(self, move |s, spid| s.on_death(spid, id));
local_player local_player
.bind_mut() .bind_mut()
.signals() .signals()
.on_take_damage() .on_take_damage()
.connect_other(self, move |s, damage| s.on_take_damage(id, damage)); .connect_other(self, move |s, spid, damage| {
s.on_take_damage(spid, id, damage)
});
} else if let Ok(mut remote_player) = character.clone().try_cast::<RemotePlayer>() { } else if let Ok(mut remote_player) = character.clone().try_cast::<RemotePlayer>() {
remote_player remote_player
.bind_mut() .bind_mut()
.signals() .signals()
.on_die() .on_die()
.connect_other(self, move |s| s.on_death(id)); .connect_other(self, move |s, spid| s.on_death(spid, id));
remote_player remote_player
.bind_mut() .bind_mut()
.signals() .signals()
.on_take_damage() .on_take_damage()
.connect_other(self, move |s, damage| s.on_take_damage(id, damage)); .connect_other(self, move |s, spid, damage| {
s.on_take_damage(spid, id, damage)
});
} }
} }
@ -434,38 +438,39 @@ impl Game {
} }
// Handle taking damage, from server to client // Handle taking damage, from server to client
pub fn handle_take_damage(&mut self, player_id: u16, damage: i32) { pub fn handle_take_damage(&mut self, source_player_id: u16, player_id: u16, damage: i32) {
if let Some(player) = self.find_player_mut(player_id) { if let Some(player) = self.find_player_mut(player_id) {
if let Some(character) = &mut player.character { if let Some(character) = &mut player.character {
character.dyn_bind_mut().take_damage(damage, false); character
.dyn_bind_mut()
.take_damage(source_player_id, damage, false);
} }
} }
} }
// Handle dying, from server to client // Handle dying, from server to client
pub fn handle_kill(&mut self, player_id: u16) { pub fn handle_kill(&mut self, source_player_id: u16, player_id: u16) {
if let Some(player) = self.find_player_mut(player_id) { if let Some(player) = self.find_player_mut(player_id) {
if let Some(character) = &mut player.character { if let Some(character) = &mut player.character {
character.dyn_bind_mut().kill(); character.dyn_bind_mut().kill(source_player_id);
} }
} }
} }
// Called whenever a player takes damage on the server-side // Called whenever a player takes damage on the server-side
pub fn on_take_damage(&mut self, player_id: u16, damage: i32) { pub fn on_take_damage(&mut self, source_player_id: u16, target_player_id: u16, damage: i32) {
if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer { if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer {
if let PeerKind::Server(peer, _) = peer { if let PeerKind::Server(peer, _) = peer {
peer.broadcast_reliable(TakeDamage(player_id, damage)); peer.broadcast_reliable(TakeDamage(source_player_id, target_player_id, damage));
} }
} }
} }
// Called whenever a player dies on the server-side // Called whenever a player dies on the server-side
pub fn on_death(&mut self, player_id: u16) { pub fn on_death(&mut self, source_player_id: u16, target_player_id: u16) {
if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer { if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer {
if let PeerKind::Server(peer, _) = peer { if let PeerKind::Server(peer, _) = peer {
godot_print!("Sent kill!"); peer.broadcast_reliable(Kill(source_player_id, target_player_id));
peer.broadcast_reliable(Kill(player_id));
} }
} }
} }

View File

@ -238,14 +238,23 @@ impl INode for NetworkManager {
); );
} }
} }
Package::TakeDamage(player_id, damage) => { Package::TakeDamage(
source_player_id,
target_player_id,
damage,
) => {
if let Some(game) = &mut Game::singleton() { if let Some(game) = &mut Game::singleton() {
game.bind_mut().handle_take_damage(player_id, damage); game.bind_mut().handle_take_damage(
source_player_id,
target_player_id,
damage,
);
} }
} }
Package::Kill(player_id) => { Package::Kill(source_player_id, target_player_id) => {
if let Some(game) = &mut Game::singleton() { if let Some(game) = &mut Game::singleton() {
game.bind_mut().handle_kill(player_id); game.bind_mut()
.handle_kill(source_player_id, target_player_id);
} }
} }
_ => {} _ => {}
@ -551,8 +560,8 @@ pub enum Package {
SelfSync(SyncPackage), SelfSync(SyncPackage),
Jump(u16), Jump(u16),
Shoot(u16, NetVector3, NetPlayerTransform, f32), Shoot(u16, NetVector3, NetPlayerTransform, f32),
TakeDamage(u16, i32), TakeDamage(u16, u16, i32),
Kill(u16), Kill(u16, u16),
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -3,8 +3,8 @@ use std::{f32::consts::PI, ops::Neg};
use godot::{ use godot::{
classes::{ classes::{
AudioStreamPlayer3D, Camera3D, CharacterBody3D, CollisionShape3D, GpuParticles3D, AudioStreamPlayer3D, Camera3D, CharacterBody3D, CollisionShape3D, GpuParticles3D,
ICharacterBody3D, Input, InputEvent, InputEventMouseMotion, PhysicsRayQueryParameters3D, ICharacterBody3D, Input, InputEvent, InputEventMouseMotion, Label,
input::MouseMode, PhysicsRayQueryParameters3D, input::MouseMode,
}, },
obj::WithBaseField, obj::WithBaseField,
prelude::*, prelude::*,
@ -12,6 +12,7 @@ use godot::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
game_manager::Game,
net::{ net::{
network_manager::{ network_manager::{
NetworkManager, NetworkManager,
@ -45,9 +46,9 @@ pub trait IPlayer {
fn get_transform(&self) -> NetPlayerTransform; fn get_transform(&self) -> NetPlayerTransform;
fn jump(&mut self); fn jump(&mut self);
fn try_shoot(&mut self, to: Vector3, deal_damage: bool) -> bool; fn try_shoot(&mut self, to: Vector3, deal_damage: bool) -> bool;
fn take_damage(&mut self, damage: i32, kill: bool); fn take_damage(&mut self, source_player: u16, damage: i32, kill: bool);
fn is_dead(&self) -> bool; fn is_dead(&self) -> bool;
fn kill(&mut self); fn kill(&mut self, source_player: u16);
fn dead_for(&self) -> f64; fn dead_for(&self) -> f64;
} }
@ -97,6 +98,8 @@ pub struct LocalPlayer {
#[export] #[export]
dead_cam_target: Option<Gd<Node3D>>, dead_cam_target: Option<Gd<Node3D>>,
#[export] #[export]
respawn_label: Option<Gd<Label>>,
#[export]
health: i32, health: i32,
pub player_id: Option<u16>, pub player_id: Option<u16>,
@ -110,6 +113,7 @@ pub struct LocalPlayer {
is_dead: bool, is_dead: bool,
dead_for: f64, dead_for: f64,
killer_id: Option<u16>,
locked: bool, locked: bool,
@ -182,7 +186,9 @@ impl IPlayer for LocalPlayer {
if self.shoot_cd <= 0. { if self.shoot_cd <= 0. {
let rid = self.base().get_rid(); let rid = self.base().get_rid();
if let Some(weapon) = &mut self.weapon { if let Some(weapon) = &mut self.weapon {
weapon.dyn_bind_mut().shoot(rid, to, deal_damage); weapon
.dyn_bind_mut()
.shoot(rid, self.player_id.unwrap_or(0), to, deal_damage);
} }
self.shoot_cd = 0.2; self.shoot_cd = 0.2;
true true
@ -191,11 +197,11 @@ impl IPlayer for LocalPlayer {
} }
} }
fn take_damage(&mut self, damage: i32, kill: bool) { fn take_damage(&mut self, source_player: u16, damage: i32, kill: bool) {
self.run_deferred(move |s| s.signals().on_take_damage().emit(damage)); self.run_deferred(move |s| s.signals().on_take_damage().emit(source_player, damage));
self.health -= damage; self.health -= damage;
if kill && self.health <= 0 { if kill && self.health <= 0 {
self.kill(); self.kill(source_player);
} }
} }
@ -207,8 +213,8 @@ impl IPlayer for LocalPlayer {
self.dead_for self.dead_for
} }
fn kill(&mut self) { fn kill(&mut self, source_player: u16) {
self.run_deferred(move |s| s.signals().on_die().emit()); self.run_deferred(move |s| s.signals().on_die().emit(source_player));
if let Some(mut mesh) = self.soldier_mesh.take() { if let Some(mut mesh) = self.soldier_mesh.take() {
mesh.queue_free(); mesh.queue_free();
} }
@ -222,6 +228,7 @@ impl IPlayer for LocalPlayer {
splorch.set_emitting(true); splorch.set_emitting(true);
} }
self.is_dead = true; self.is_dead = true;
self.killer_id = Some(source_player);
} }
} }
@ -246,11 +253,45 @@ impl ICharacterBody3D for LocalPlayer {
if let Some(mesh) = &mut self.soldier_mesh { if let Some(mesh) = &mut self.soldier_mesh {
self.weapon = mesh.bind_mut().spawn_raygun(); self.weapon = mesh.bind_mut().spawn_raygun();
} }
if let Some(camera) = &mut self.camera {
camera.set_current(true);
}
} }
fn process(&mut self, delta: f64) { fn process(&mut self, delta: f64) {
if self.is_dead { if self.is_dead {
self.dead_for += delta; self.dead_for += delta;
if let Some(label) = &mut self.respawn_label {
let respawn_timer = if let Some(game) = Game::singleton() {
game.bind().respawn_timer
} else {
0.
};
let time_remaining = respawn_timer - self.dead_for;
label.set_text(&format!("Respawn in\n{:.2}", time_remaining));
}
if let Some(camera) = &self.camera {
if camera.is_current() && self.dead_for > 3. {
if let Some(game) = Game::singleton()
&& let Some(killer_id) = self.killer_id
{
let game = game.bind();
let remote_player = game.find_player(killer_id);
if let Some(remote_player) = &remote_player
&& let Some(remote_character) = remote_player.character.clone()
&& let Ok(mut remote_character) =
remote_character.try_cast::<RemotePlayer>()
&& let Some(camera) = &mut remote_character.bind_mut().spectator_camera
{
camera.set_current(true);
}
}
}
}
} }
self.shoot_cd -= delta; self.shoot_cd -= delta;
@ -355,9 +396,9 @@ impl ICharacterBody3D for LocalPlayer {
#[godot_api] #[godot_api]
impl LocalPlayer { impl LocalPlayer {
#[signal] #[signal]
pub fn on_die(); pub fn on_die(source_player: u16);
#[signal] #[signal]
pub fn on_take_damage(damage: i32); pub fn on_take_damage(source_player: u16, damage: i32);
fn set_lock(&mut self, locked: bool) { fn set_lock(&mut self, locked: bool) {
self.locked = locked; self.locked = locked;
@ -382,6 +423,8 @@ pub struct RemotePlayer {
splorch: Option<Gd<GpuParticles3D>>, splorch: Option<Gd<GpuParticles3D>>,
#[export] #[export]
health: i32, health: i32,
#[export]
spectator_camera: Option<Gd<Camera3D>>,
pub player_id: Option<u16>, pub player_id: Option<u16>,
pub weapon: Option<DynGd<Node3D, dyn Weapon>>, pub weapon: Option<DynGd<Node3D, dyn Weapon>>,
@ -462,7 +505,9 @@ impl IPlayer for RemotePlayer {
if self.shoot_cd <= 0. { if self.shoot_cd <= 0. {
let rid = self.base().get_rid(); let rid = self.base().get_rid();
if let Some(weapon) = &mut self.weapon { if let Some(weapon) = &mut self.weapon {
weapon.dyn_bind_mut().shoot(rid, to, deal_damage); weapon
.dyn_bind_mut()
.shoot(rid, self.player_id.unwrap_or(0), to, deal_damage);
} }
self.shoot_cd = 0.2; self.shoot_cd = 0.2;
true true
@ -471,11 +516,11 @@ impl IPlayer for RemotePlayer {
} }
} }
fn take_damage(&mut self, damage: i32, kill: bool) { fn take_damage(&mut self, source_player: u16, damage: i32, kill: bool) {
self.run_deferred(move |s| s.signals().on_take_damage().emit(damage)); self.run_deferred(move |s| s.signals().on_take_damage().emit(source_player, damage));
self.health -= damage; self.health -= damage;
if kill && self.health <= 0 { if kill && self.health <= 0 {
self.kill(); self.kill(source_player);
} }
} }
@ -487,8 +532,8 @@ impl IPlayer for RemotePlayer {
self.dead_for self.dead_for
} }
fn kill(&mut self) { fn kill(&mut self, source_player: u16) {
self.run_deferred(move |s| s.signals().on_die().emit()); self.run_deferred(move |s| s.signals().on_die().emit(source_player));
if let Some(mut mesh) = self.soldier_mesh.take() { if let Some(mut mesh) = self.soldier_mesh.take() {
mesh.queue_free(); mesh.queue_free();
} }
@ -534,7 +579,7 @@ impl ICharacterBody3D for RemotePlayer {
#[godot_api] #[godot_api]
impl RemotePlayer { impl RemotePlayer {
#[signal] #[signal]
pub fn on_die(); pub fn on_die(source_player: u16);
#[signal] #[signal]
pub fn on_take_damage(damage: i32); pub fn on_take_damage(source_player: u16, damage: i32);
} }

View File

@ -6,7 +6,7 @@ use godot::{
use crate::{net::util::cast_ray, player::IPlayer}; use crate::{net::util::cast_ray, player::IPlayer};
pub trait Weapon { pub trait Weapon {
fn shoot(&mut self, player: Rid, to: Vector3, deal_damage: bool); fn shoot(&mut self, player_rid: Rid, player_id: u16, to: Vector3, deal_damage: bool);
} }
#[derive(GodotClass)] #[derive(GodotClass)]
@ -24,7 +24,7 @@ pub struct Raygun {
#[godot_dyn] #[godot_dyn]
impl Weapon for Raygun { impl Weapon for Raygun {
fn shoot(&mut self, player: Rid, to: Vector3, deal_damage: bool) { fn shoot(&mut self, player: Rid, player_id: u16, to: Vector3, deal_damage: bool) {
if let Some(emitter) = &mut self.sound_emitter { if let Some(emitter) = &mut self.sound_emitter {
emitter.play(); emitter.play();
} }
@ -56,7 +56,7 @@ impl Weapon for Raygun {
if deal_damage && let Some(target) = target { if deal_damage && let Some(target) = target {
if let Ok(mut player) = target.collider.try_dynify::<dyn IPlayer>() { if let Ok(mut player) = target.collider.try_dynify::<dyn IPlayer>() {
player.dyn_bind_mut().take_damage(100, true); player.dyn_bind_mut().take_damage(player_id, 100, true);
} }
} }
} }