Implemented physics interpolation

This commit is contained in:
Eduard Urbach 2024-02-27 21:05:55 +01:00
parent ca7fa120ea
commit 489a14061a
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
23 changed files with 226 additions and 68 deletions

View File

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

View File

@ -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"]

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
class_name Character
extends CharacterBody3D
extends Node3D
signal controlled(Controller)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
body.move_and_slide()

View File

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

View File

@ -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 []

View File

@ -4,4 +4,4 @@ func _process(_delta):
if Global.player == null:
return
text = str(Global.player.velocity)
text = str(Global.player.physics.velocity)

View File

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

View File

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