From 489a14061a821e6759d6215e1b73dd93e3435b8d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Feb 2024 21:05:55 +0100 Subject: [PATCH] Implemented physics interpolation --- client/Global.gd | 2 +- client/Main.tscn | 15 +++- client/enemy/death/DeathComponent.gd | 16 ++--- client/item/soul/Soul.gd | 2 +- client/math/Math.gd | 11 +-- client/player/Character.gd | 2 +- client/player/CharacterComponent.gd | 9 +++ client/player/Player.gd | 6 ++ client/player/Player.tscn | 25 ++++--- client/player/animation/AnimationComponent.gd | 17 ++++- client/player/controller/ProxyController.gd | 13 ++-- client/player/movement/MovementComponent.gd | 41 +++++++---- .../performance/PerformanceComponent.gd | 72 +++++++++++++++++++ .../performance/PerformanceComponent.tscn | 7 ++ client/player/rotation/RotationComponent.gd | 7 +- client/player/skills/SkillsComponent.gd | 7 +- client/player/state/StateComponent.gd | 4 +- client/skill/dash/dash.gd | 2 +- client/ui/chat/ChatInput.gd | 4 +- client/ui/debug/DebugLabelText.gd | 2 +- client/ui/debug/VelocityLabel.gd | 2 +- client/world/PlayerManager.gd | 20 ++++-- stresstest/main.go | 8 ++- 23 files changed, 226 insertions(+), 68 deletions(-) create mode 100644 client/player/CharacterComponent.gd create mode 100644 client/player/performance/PerformanceComponent.gd create mode 100644 client/player/performance/PerformanceComponent.tscn diff --git a/client/Global.gd b/client/Global.gd index 5d756f8..762d4db 100644 --- a/client/Global.gd +++ b/client/Global.gd @@ -11,6 +11,6 @@ var terrain: Terrain3D var instance_id: int func _enter_tree(): - instance_id = 0 + instance_id = OS.get_process_id() % 2 account = Account.new() account.name = "user%d" % instance_id diff --git a/client/Main.tscn b/client/Main.tscn index 12b9da0..b145c95 100644 --- a/client/Main.tscn +++ b/client/Main.tscn @@ -28,6 +28,7 @@ process_thread_group = 2 process_thread_group_order = 0 process_thread_messages = 0 script = ExtResource("2_8hxcx") +host = "akyoto.dev" [node name="Ping" type="Node" parent="Client"] unique_name_in_owner = true @@ -84,8 +85,6 @@ process_thread_group_order = 0 process_thread_messages = 0 autostart = true -[node name="Camera" parent="." instance=ExtResource("12_aljdh")] - [node name="World" parent="." instance=ExtResource("13_sqmhj")] process_thread_group = 2 process_thread_group_order = 0 @@ -95,8 +94,20 @@ process_thread_messages = 0 unique_name_in_owner = true script = ExtResource("16_dp6bj") +[node name="UpdatePlayers" type="Timer" parent="Players"] +unique_name_in_owner = true +process_priority = 1 +wait_time = 0.25 +autostart = true + +[node name="Camera" parent="." instance=ExtResource("12_aljdh")] +process_priority = 100 +process_physics_priority = 100 + [node name="UI" parent="." instance=ExtResource("17_43qhq")] process_mode = 3 +process_priority = 100 +process_physics_priority = 100 [connection signal="timeout" from="Client/Ping/Timer" to="Client/Ping" method="send_ping"] [connection signal="timeout" from="Client/Login/Timer" to="Client/Login" method="send_login"] diff --git a/client/enemy/death/DeathComponent.gd b/client/enemy/death/DeathComponent.gd index 0d77c08..5b5e92a 100644 --- a/client/enemy/death/DeathComponent.gd +++ b/client/enemy/death/DeathComponent.gd @@ -1,5 +1,5 @@ class_name DeathComponent -extends Node +extends CharacterComponent @export var health: HealthComponent @export var hud: HUDComponent @@ -10,7 +10,7 @@ extends Node var respawn_position: Vector3 func _ready(): - respawn_position = owner.global_position + respawn_position = character.global_position health.death.connect(on_death) func on_death(): @@ -21,20 +21,20 @@ func on_death(): func drop_loot(): var loot := drop.instantiate() as Node3D - owner.get_parent().add_child(loot) - loot.global_position = owner.global_position + loot.position + character.get_parent().add_child(loot) + loot.global_position = character.global_position + loot.position func die(): - DeathComponent.set_physics(owner, false) + DeathComponent.set_physics(character, false) hud.visible = false animation.play("slime_death") func revive(): health.restore() - DeathComponent.set_physics(owner, true) + DeathComponent.set_physics(character, true) hud.visible = true - (owner as Node3D).transform = Transform3D.IDENTITY - owner.global_position = respawn_position + character.transform = Transform3D.IDENTITY + character.global_position = respawn_position animation.play("slime_idle") static func set_physics(obj: Node3D, alive: bool): diff --git a/client/item/soul/Soul.gd b/client/item/soul/Soul.gd index cf78462..edc5973 100644 --- a/client/item/soul/Soul.gd +++ b/client/item/soul/Soul.gd @@ -24,7 +24,7 @@ func _process(delta): if !collected_by: return - global_position = Math.damp(global_position, Global.player.global_position + Vector3.UP, 0.75 * delta) + global_position = Math.damp_vector(global_position, Global.player.global_position + Vector3.UP, 0.75 * delta) func on_body_entered(body: Node3D): if not body is Player: diff --git a/client/math/Math.gd b/client/math/Math.gd index 7d55539..ebfe33b 100644 --- a/client/math/Math.gd +++ b/client/math/Math.gd @@ -1,17 +1,20 @@ class_name Math -static func damp(from: Variant, to: Variant, weight: float): +static func damp(from: Variant, to: Variant, weight: float) -> Variant: return lerp(from, to, 1 - exp(-weight)) -static func dampf(from: float, to: float, weight: float): +static func dampf(from: float, to: float, weight: float) -> float: return lerpf(from, to, 1 - exp(-weight)) -static func damp_angle(from: float, to: float, weight: float): +static func damp_angle(from: float, to: float, weight: float) -> float: return lerp_angle(from, to, 1 - exp(-weight)) -static func damp_spherical(from: Vector3, to: Vector3, weight: float): +static func damp_spherical(from: Vector3, to: Vector3, weight: float) -> Vector3: return from.slerp(to, 1 - exp(-weight)) +static func damp_vector(from: Vector3, to: Vector3, weight: float) -> Vector3: + return from.lerp(to, 1 - exp(-weight)) + static func from_to_rotation(from: Vector3, to: Vector3) -> Quaternion: var axis := from.cross(to).normalized() var angle := from.angle_to(to) diff --git a/client/player/Character.gd b/client/player/Character.gd index 87d4511..41998ea 100644 --- a/client/player/Character.gd +++ b/client/player/Character.gd @@ -1,5 +1,5 @@ class_name Character -extends CharacterBody3D +extends Node3D signal controlled(Controller) diff --git a/client/player/CharacterComponent.gd b/client/player/CharacterComponent.gd new file mode 100644 index 0000000..5044723 --- /dev/null +++ b/client/player/CharacterComponent.gd @@ -0,0 +1,9 @@ +class_name CharacterComponent +extends Node + +var character: Character + +func _enter_tree(): + assert(owner, "owner not set") + assert(owner is Character, "owner must be a character") + character = owner \ No newline at end of file diff --git a/client/player/Player.gd b/client/player/Player.gd index 8106355..7563d10 100644 --- a/client/player/Player.gd +++ b/client/player/Player.gd @@ -7,10 +7,16 @@ var id: String ## Components var movement: MovementComponent var state: StateComponent +var performance: PerformanceComponent +var animation: AnimationComponent +var physics: CharacterBody3D func _enter_tree(): movement = $Movement state = $State + performance = $Performance + animation = $Animation + physics = $Physics ## Name signal name_changed(new_name: String) diff --git a/client/player/Player.tscn b/client/player/Player.tscn index eb22c7b..d74b878 100644 --- a/client/player/Player.tscn +++ b/client/player/Player.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=31 format=3 uid="uid://2lcnu3dy54lx"] +[gd_scene load_steps=32 format=3 uid="uid://2lcnu3dy54lx"] [ext_resource type="Script" path="res://player/Player.gd" id="1_8gebs"] [ext_resource type="PackedScene" uid="uid://c8j7t4yg7anb0" path="res://assets/female/Female.blend" id="2_8nah6"] @@ -26,6 +26,7 @@ [ext_resource type="AudioStream" uid="uid://cdywep3dxm0y3" path="res://assets/audio/footsteps/Footsteps-Human-Dirt-07.wav" id="19_vvmo0"] [ext_resource type="AudioStream" uid="uid://bp8pka7qhcetq" path="res://assets/audio/footsteps/Footsteps-Human-Dirt-08.wav" id="20_srjtb"] [ext_resource type="AudioStream" uid="uid://bgdodasvt7ier" path="res://assets/audio/footsteps/Footsteps-Human-Dirt-01.wav" id="21_a8ikg"] +[ext_resource type="PackedScene" uid="uid://b5yfm1sektkv" path="res://player/performance/PerformanceComponent.tscn" id="22_iqcgj"] [ext_resource type="PackedScene" uid="uid://sx4ein0bju6m" path="res://player/state/StateComponent.tscn" id="28_0i0of"] [ext_resource type="PackedScene" uid="uid://csidusk3jpq0m" path="res://player/voice/VoiceComponent.tscn" id="28_iolce"] @@ -58,10 +59,16 @@ stream_8/weight = 1.0 stream_9/stream = ExtResource("21_a8ikg") stream_9/weight = 1.0 -[node name="Player" type="CharacterBody3D" groups=["player"]] +[node name="Player" type="Node3D"] +script = ExtResource("1_8gebs") + +[node name="Physics" type="CharacterBody3D" parent="."] collision_layer = 512 collision_mask = 769 -script = ExtResource("1_8gebs") + +[node name="Collision" type="CollisionShape3D" parent="Physics"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0) +shape = SubResource("CapsuleShape3D_2f50n") [node name="Model" type="Node3D" parent="."] @@ -97,10 +104,6 @@ bone_idx = 44 [node name="Heirloom" parent="Model/Female/Armature/GeneralSkeleton/Weapon" instance=ExtResource("7_u8433")] transform = Transform3D(-4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 0, 0, 1, 0.197644, 0, 0) -[node name="Collision" type="CollisionShape3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0) -shape = SubResource("CapsuleShape3D_2f50n") - [node name="HUD" parent="." node_paths=PackedStringArray("health") instance=ExtResource("7_fwgtd")] layers = 512 visibility_range_end = 50.0 @@ -129,10 +132,14 @@ footsteps = SubResource("AudioStreamRandomizer_4yj1k") [node name="Health" parent="." instance=ExtResource("2_np5ag")] max_value = 100.0 -[node name="Movement" parent="." instance=ExtResource("8_25qd0")] +[node name="Movement" parent="." node_paths=PackedStringArray("body") instance=ExtResource("8_25qd0")] +body = NodePath("../Physics") + +[node name="Performance" parent="." node_paths=PackedStringArray("animation") instance=ExtResource("22_iqcgj")] +animation = NodePath("../Animation") [node name="Rotation" parent="." node_paths=PackedStringArray("root") instance=ExtResource("9_agxqu")] -root = NodePath("..") +root = NodePath("../Model") [node name="Skills" parent="." instance=ExtResource("14_6idcf")] skills = Array[Resource("res://skill/Skill.gd")]([ExtResource("2_x58e1"), ExtResource("3_l76ly"), null, ExtResource("5_pnues")]) diff --git a/client/player/animation/AnimationComponent.gd b/client/player/animation/AnimationComponent.gd index 9890754..4d70874 100644 --- a/client/player/animation/AnimationComponent.gd +++ b/client/player/animation/AnimationComponent.gd @@ -1,16 +1,27 @@ class_name AnimationComponent -extends Node +extends CharacterComponent + +@export var skip_frames: int = 0 var animations: AnimationPlayer var state: StateComponent +var accumulated_delta: float +var skipped_frames: int func _ready(): - state = owner.get_node("State") + state = character.get_node("State") state.transitioned.connect(on_transition) animations = %AnimationPlayer func _process(delta): - animations.advance(delta) + accumulated_delta += delta + + if skipped_frames >= skip_frames: + animations.advance(accumulated_delta) + accumulated_delta = 0 + skipped_frames = 0 + else: + skipped_frames += 1 func on_transition(_from: StateComponent.State, to: StateComponent.State): match to: diff --git a/client/player/controller/ProxyController.gd b/client/player/controller/ProxyController.gd index b88a5f3..dfcbe6b 100644 --- a/client/player/controller/ProxyController.gd +++ b/client/player/controller/ProxyController.gd @@ -1,22 +1,25 @@ class_name ProxyController extends Controller -@export var interpolation_speed := 5.0 +@export var interpolation_speed := 20.0 var player: Player +var movement: MovementComponent var server_position: Vector3 func _init(new_player: Player): player = new_player + movement = player.movement name = "Controller" + process_physics_priority = 1 func _ready(): server_position = player.position func _physics_process(delta: float): - if absf(server_position.x - player.position.x) < 0.001 && absf(server_position.z - player.position.z) < 0.001: + if absf(server_position.x - movement.physics_position.x) < 0.001 && absf(server_position.z - movement.physics_position.z) < 0.001: return - + var time := interpolation_speed * delta - player.position.x = Math.dampf(player.position.x, server_position.x, time) - player.position.z = Math.dampf(player.position.z, server_position.z, time) \ No newline at end of file + movement.physics_position.x = Math.dampf(movement.physics_position.x, server_position.x, time) + movement.physics_position.z = Math.dampf(movement.physics_position.z, server_position.z, time) \ No newline at end of file diff --git a/client/player/movement/MovementComponent.gd b/client/player/movement/MovementComponent.gd index 228b84f..8656e95 100644 --- a/client/player/movement/MovementComponent.gd +++ b/client/player/movement/MovementComponent.gd @@ -1,24 +1,30 @@ class_name MovementComponent -extends Node +extends CharacterComponent +static var ticks_per_second := ProjectSettings.get_setting("physics/common/physics_ticks_per_second") as float +static var gravity := ProjectSettings.get_setting("physics/3d/default_gravity") as float + +@export var body: CharacterBody3D @export var move_speed := 4.5 @export var jump_velocity := 4.5 @export var deceleration_speed := 20 -var body: Character var direction: Vector3 -var gravity: float +var physics_position: Vector3 func _ready(): - gravity = ProjectSettings.get_setting("physics/3d/default_gravity") - body = owner as Character - body.controlled.connect(on_controlled) + physics_position = character.position + character.controlled.connect(on_controlled) -func on_controlled(controller: Controller): - controller.direction_changed.connect(on_direction_changed) - controller.jumped.connect(jump) +func _process(delta: float): + character.position = Math.damp_vector(character.position, physics_position, ticks_per_second * delta) -func _physics_process(delta): +func _physics_process(delta: float): + begin_physics() + move(delta) + end_physics() + +func move(delta: float): if direction: body.velocity.x = direction.x * move_speed body.velocity.z = direction.z * move_speed @@ -34,11 +40,22 @@ func _physics_process(delta): body.move_and_slide() -func on_direction_changed(new_direction: Vector3): - direction = new_direction +func begin_physics(): + body.global_position = physics_position + +func end_physics(): + physics_position = body.global_position + body.position = Vector3.ZERO func can_jump() -> bool: return body.is_on_floor() func jump(): body.velocity.y = jump_velocity + +func on_controlled(controller: Controller): + controller.direction_changed.connect(on_direction_changed) + controller.jumped.connect(jump) + +func on_direction_changed(new_direction: Vector3): + direction = new_direction diff --git a/client/player/performance/PerformanceComponent.gd b/client/player/performance/PerformanceComponent.gd new file mode 100644 index 0000000..93a5ffb --- /dev/null +++ b/client/player/performance/PerformanceComponent.gd @@ -0,0 +1,72 @@ +class_name PerformanceComponent +extends VisibleOnScreenNotifier3D + +const SKIP_FRAMES_INVISIBLE := 16 +const SKIP_FRAMES_VISIBLE := 8 + +static var quality_budget: Array[int] = [ + 10, # 0 skipped frames + 10, # 1 skipped frame + 10, # 2 skipped frames + 10, # 3 skipped frames + 10, # 4 skipped frames + 10, # 5 skipped frames + 10, # 6 skipped frames + 10, # 7 skipped frames +] + +@export var animation: AnimationComponent + +var camera_distance_squared: float +var on_screen: bool +var skip_frames_visible: int + +func _ready(): + assert(animation) + screen_entered.connect(on_screen_entered) + screen_exited.connect(on_screen_exited) + +func on_screen_entered(): + on_screen = true + animation.skip_frames = skip_frames_visible + +func on_screen_exited(): + on_screen = false + animation.skip_frames = SKIP_FRAMES_INVISIBLE + +static func update_all_animations(): + var visible_players: Array[Player] = [] + + for player in Global.players.id_to_player.values(): + player.performance.camera_distance_squared = player.global_position.distance_squared_to(Global.camera.global_position) + + if player.performance.on_screen: + visible_players.append(player) + else: + player.performance.animation.skip_frames = SKIP_FRAMES_INVISIBLE + + if Global.player: + Global.player.performance.camera_distance_squared = 0 + + visible_players.sort_custom(PerformanceComponent.distance_sort) + + var skip_frames := 0 + var count := 0 + + for player in visible_players: + if skip_frames >= quality_budget.size(): + player.performance.skip_frames_visible = SKIP_FRAMES_VISIBLE + player.animation.skip_frames = SKIP_FRAMES_VISIBLE + continue + + player.performance.skip_frames_visible = skip_frames + player.animation.skip_frames = skip_frames + count += 1 + + if count >= quality_budget[skip_frames]: + skip_frames += 1 + count = 0 + +static func distance_sort(a: Player, b: Player) -> bool: + return a.performance.camera_distance_squared < b.performance.camera_distance_squared + \ No newline at end of file diff --git a/client/player/performance/PerformanceComponent.tscn b/client/player/performance/PerformanceComponent.tscn new file mode 100644 index 0000000..efda985 --- /dev/null +++ b/client/player/performance/PerformanceComponent.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=3 uid="uid://b5yfm1sektkv"] + +[ext_resource type="Script" path="res://player/performance/PerformanceComponent.gd" id="1_vp0dv"] + +[node name="Performance" type="VisibleOnScreenNotifier3D"] +aabb = AABB(-1, 0, -1, 2, 2, 2) +script = ExtResource("1_vp0dv") diff --git a/client/player/rotation/RotationComponent.gd b/client/player/rotation/RotationComponent.gd index d05adda..b56732d 100644 --- a/client/player/rotation/RotationComponent.gd +++ b/client/player/rotation/RotationComponent.gd @@ -1,5 +1,5 @@ class_name RotationComponent -extends Node +extends CharacterComponent @export var root: Node3D @export var rotation_speed: float = 15.0 @@ -8,8 +8,9 @@ var direction: Vector3 var angle: float func _ready(): - assert(root, "Rotation root needs to be set") - (owner as Character).controlled.connect(on_controlled) + assert(root, "rotation root needs to be set") + assert(rotation_speed > 0, "rotation speed must be greater than zero") + character.controlled.connect(on_controlled) func _process(delta): if absf(angle_difference(root.rotation.y, angle)) < 0.001: diff --git a/client/player/skills/SkillsComponent.gd b/client/player/skills/SkillsComponent.gd index 2832970..d72a41a 100644 --- a/client/player/skills/SkillsComponent.gd +++ b/client/player/skills/SkillsComponent.gd @@ -1,12 +1,9 @@ class_name SkillsComponent -extends Node +extends CharacterComponent @export var skills: Array[Skill] -var character: Character - func _ready(): - character = owner character.controlled.connect(on_controlled) func on_controlled(controller: Controller): @@ -25,4 +22,4 @@ func use_skill(slot: int): return var scene := skill.scene.instantiate() - owner.add_child(scene) + character.add_child(scene) diff --git a/client/player/state/StateComponent.gd b/client/player/state/StateComponent.gd index 21ea77f..ba8517f 100644 --- a/client/player/state/StateComponent.gd +++ b/client/player/state/StateComponent.gd @@ -1,5 +1,5 @@ class_name StateComponent -extends Node +extends CharacterComponent enum State { None, @@ -17,7 +17,7 @@ var _current: State func _ready(): current = State.Idle - movement = owner.get_node("Movement") + movement = character.get_node("Movement") func _process(_delta): if current == State.Skill: diff --git a/client/skill/dash/dash.gd b/client/skill/dash/dash.gd index c3013ea..704b744 100644 --- a/client/skill/dash/dash.gd +++ b/client/skill/dash/dash.gd @@ -24,4 +24,4 @@ func _physics_process(_delta): body.velocity.x = direction.x * movement.move_speed * 3.0 body.velocity.y = 0 body.velocity.z = direction.z * movement.move_speed * 3.0 - body.move_and_slide() \ No newline at end of file + body.move_and_slide() diff --git a/client/ui/chat/ChatInput.gd b/client/ui/chat/ChatInput.gd index 09f571c..babc092 100644 --- a/client/ui/chat/ChatInput.gd +++ b/client/ui/chat/ChatInput.gd @@ -25,4 +25,6 @@ func on_focus_exited(): func on_text_submitted(message: String): text = "" release_focus() - (owner as Chat).message_submitted.emit(message) + + var chat := owner as Chat + chat.message_submitted.emit(message) diff --git a/client/ui/debug/DebugLabelText.gd b/client/ui/debug/DebugLabelText.gd index 4de2109..c09793b 100644 --- a/client/ui/debug/DebugLabelText.gd +++ b/client/ui/debug/DebugLabelText.gd @@ -4,6 +4,6 @@ extends Label func _ready(): text = get_parent().name + ":" -func _get_configuration_warnings(): +func _get_configuration_warnings() -> PackedStringArray: text = get_parent().name + ":" return [] diff --git a/client/ui/debug/VelocityLabel.gd b/client/ui/debug/VelocityLabel.gd index c229092..eb4822e 100644 --- a/client/ui/debug/VelocityLabel.gd +++ b/client/ui/debug/VelocityLabel.gd @@ -4,4 +4,4 @@ func _process(_delta): if Global.player == null: return - text = str(Global.player.velocity) + text = str(Global.player.physics.velocity) diff --git a/client/world/PlayerManager.gd b/client/world/PlayerManager.gd index 10e984c..e319efe 100644 --- a/client/world/PlayerManager.gd +++ b/client/world/PlayerManager.gd @@ -1,27 +1,33 @@ class_name PlayerManager extends Node3D -var players = {} +var id_to_player = {} func _ready(): Global.players = self + var timer := %UpdatePlayers as Timer + timer.timeout.connect(tick) + +func tick(): + PerformanceComponent.update_all_animations() + func add(player: Player): if has(player.id): return add_child(player) - players[player.id] = player + id_to_player[player.id] = player func get_player(id: String) -> Player: - return players[id] as Player + return id_to_player[id] as Player func has(id: String) -> bool: - return players.has(id) + return id_to_player.has(id) func remove(id: String): - if !players.has(id): + if !id_to_player.has(id): return - players[id].queue_free() - players.erase(id) + id_to_player[id].queue_free() + id_to_player.erase(id) diff --git a/stresstest/main.go b/stresstest/main.go index 1fde340..3f6159e 100644 --- a/stresstest/main.go +++ b/stresstest/main.go @@ -6,6 +6,7 @@ import ( "encoding/json" "flag" "fmt" + "math/rand" "net" "sync" "time" @@ -49,7 +50,8 @@ func udpClient(wg *sync.WaitGroup, id int) { defer conn.Close() - loginRequest := [2]string{fmt.Sprintf("user%d", id+1), sha256Text("password")} + username := fmt.Sprintf("user%d", id+1) + loginRequest := [2]string{username, sha256Text("password")} data, err := json.Marshal(loginRequest) if err != nil { @@ -69,6 +71,10 @@ func udpClient(wg *sync.WaitGroup, id int) { return } + if rand.Float32() > 0.5 { + conn.Write([]byte{13}) + } + time.Sleep(*sleepTime) } }