Advertisement
Guest User

Godot 3.1 Authoritative Server

a guest
May 31st, 2019
889
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.70 KB | None | 0 0
  1. extends KinematicBody
  2.  
  3. # FPS controller script by fornclake
  4. # Uses an authoritative server model when connected to a network
  5. # Includes client-side prediction, server reconciliation, and entity interpolation
  6.  
  7. # Ground movement has acceleration & deceleration
  8. # Currently variable jump height is defunct because of issues syncing jumps with server without jitter
  9. # Ability to jump is also currently calculated client-side, which is an avenue for cheating and should be looked at
  10.  
  11. #############
  12. # VARIABLES #
  13. #############
  14.  
  15. # PLAYER ATTRIBUTES #
  16. const SPEED = 6
  17. const ACC = 5
  18. const DEC = 5
  19. const GRAVITY = 20
  20. const JUMP_SPEED = 8
  21. const JUMP_DAMP = 0.25 # currently defunct, used for variable jump heights
  22. const MAX_HEALTH = 100
  23. const RESPAWN_TIME = 3
  24. const INVULNERABLE_TIME = 1.5
  25.  
  26. # ENGINE VARIABLES #
  27. var velocity = Vector3(0,0,0)
  28. var movedir = Vector3()
  29. var state = "default"
  30. var last_state = "default"
  31. var animation = "shoot"
  32. var can_jump = false
  33. var health = MAX_HEALTH
  34. var hitstun = 0.1
  35. sync var invulnerable = false
  36. var sensitivity = 0.001
  37.  
  38. # WEAPON STUFF
  39. var weapon = null
  40. var projectile = "res://weapons/projectiles/power.tscn"
  41.  
  42. # OTHER NODES #
  43. onready var camera = $Camera
  44. onready var controller = get_parent().get_parent()
  45.  
  46. # CLIENT #
  47. var client_timestamp = 0 # keeps track of when inputs take place
  48. var pending_inputs = [] # client-only queue of previous inputs used for server reconciliation
  49. var queued_inputs = [] # similar to above, but is sent to server and cleared once sent
  50. var server_state = [] # state received from server to sync position & velocity
  51. var game_time = 0
  52.  
  53. # SERVER #
  54. var server_timestamp = 0 # used for tracking the latest received input
  55. var client_input = [] # queue of inputs received from client
  56. var queued_states = [] # queue of states to send to client
  57.  
  58. # PUPPET #
  59. var packet_elapsed_time = 0 # how long it's been since the last packet
  60. var old_position = Vector3(0,0,0) # position to interpolate from
  61. var new_position = Vector3(0,0,0) # position to interpolate to
  62.  
  63. ###########
  64. # SIGNALS #
  65. ###########
  66.  
  67. signal hurt
  68. signal die
  69. signal respawn
  70.  
  71. #############
  72. # FUNCTIONS #
  73. #############
  74.  
  75. func initialize(): # we use this instead of _ready() so it happens once the player is fully connected
  76.     add_to_group("player")
  77.     $Hitbox.connect("body_entered", self, "collision")
  78.     $Sprite3D.set_material_override($Sprite3D.get_material_override().duplicate(true)) # prevents color changes from affecting every player
  79.    
  80.     if is_network_master():
  81.         var new_hud = preload("res://ui/hud.tscn").instance()
  82.         add_child(new_hud)
  83.         new_hud.player = self
  84.         connect("hurt", new_hud, "player_hurt")
  85.         connect("die", new_hud, "player_die")
  86.         connect("respawn", new_hud, "player_respawn")
  87.     else:
  88.         $Weapon.queue_free()
  89.  
  90. func _physics_process(delta):
  91.     set_animation()
  92.    
  93.     can_jump = is_on_floor()
  94.    
  95.     game_time = controller.game_time
  96.    
  97.     # currently playing player
  98.     if is_network_master():
  99.         camera.current = true
  100.        
  101.         allow_mouse_capture()
  102.        
  103.         if state == "default":
  104.             get_inputs()
  105.             get_weapon_inputs()
  106.             weapon_bobbing()
  107.        
  108.         move()
  109.        
  110.         # if not the server, prepare packets for server
  111.         if !get_tree().is_network_server():
  112.             create_client_input()
  113.             server_reconciliation()
  114.    
  115.     # the server
  116.     if get_tree().is_network_server():
  117.         # if not the host, process client movements
  118.         if !is_network_master():
  119.             process_client_movement()
  120.        
  121.         create_server_state() # prepare packets for client
  122.        
  123.         if health <= 0 || global_transform.origin.y <= 0:
  124.             rpc("die")
  125.    
  126.     # puppets
  127.     if !is_network_master() && !get_tree().is_network_server():
  128.         sync_entities()
  129.    
  130.     if state == "hurt":
  131.         animation = "jump"
  132.         if last_state != "hurt":
  133.             $anim_spritefx.play("hurt")
  134.    
  135.     last_state = state
  136.    
  137.  
  138. func update_tick(): # called by game.gd; sends packets
  139.     # server updates to clients
  140.     if get_tree().is_network_server():
  141.         # send all of the states that occured since the last tick
  142.         while queued_states.size() > 0:
  143.             rpc_unreliable("receive_server_state", queued_states[0])
  144.             queued_states.pop_front()
  145.    
  146.     # client updates to server
  147.     elif is_network_master():
  148.         # send all of the inputs that occured since the last tick
  149.         while queued_inputs.size() > 0:
  150.             rpc_unreliable_id(1, "receive_client_input", queued_inputs[0])
  151.             queued_inputs.pop_front()
  152.  
  153. # CLIENT FUNCTIONS #
  154. # stuff that happens on your screen (and other players' screens)
  155.  
  156. remote func receive_server_state(state): # takes received state of server & adds it to a queue (used in reconciliation)
  157.     server_state.append(state)
  158.     packet_elapsed_time = 0
  159.  
  160. remote func receive_server_weapon_state(bullet, camrot, rot):
  161.     fire_weapon(bullet, camrot, rot)
  162.  
  163. func create_client_input(): # takes inputs and adds it to two queues
  164.     var new_input = {
  165.         movedir = movedir,
  166.         rot = rotation.y,
  167.         timestamp = client_timestamp
  168.     }
  169.    
  170.     pending_inputs.append(new_input) # used in server_reconciliation
  171.     queued_inputs.append(new_input) # used to send to server
  172.     client_timestamp += 1
  173.  
  174. func server_reconciliation(): # compares latest received server state & replays unreceived movements
  175.     while server_state.size() > 0:
  176.         var msg = server_state.front() # the earliest unplayed state
  177.         server_state.pop_front()
  178.        
  179.         # sync player to server state
  180.         global_transform.origin = msg.position
  181.         velocity = msg.velocity
  182.         health = msg.health
  183.         state = msg.state
  184.        
  185.         # replay movements that the state didn't receive at the time
  186.         var current_input = 0
  187.         while current_input < pending_inputs.size():
  188.             var input = pending_inputs[current_input]
  189.             if input.timestamp <= msg.timestamp: # if the server already processed this input, remove it
  190.                 pending_inputs.remove(current_input)
  191.             else: # otherwise, reprocess the input on the client
  192.                 move(input.movedir) # takes movedir from the inputs that were unreceived by server
  193.                 current_input += 1
  194.  
  195. func get_inputs(): # gets currently pressed movement & jump keys
  196.     movedir = Vector3(0,0,0)
  197.  
  198.     movedir += camera.global_transform.basis.z * (-int(Input.is_action_pressed("up")) + int(Input.is_action_pressed("down")))
  199.     movedir += camera.global_transform.basis.x * (-int(Input.is_action_pressed("left")) + int(Input.is_action_pressed("right")))
  200.    
  201.     movedir.y = 0
  202.     movedir = movedir.normalized()
  203.    
  204.     # we just use movedir.y for jumps to save some bandwidth on a jump variable
  205.     if Input.is_action_pressed("jump") && can_jump: # !!this line is dangerous and should eventually be checked by the server!!
  206.         movedir.y = 1
  207.  
  208. func allow_mouse_capture(): # lock mouse to screen if clicked
  209.     if Input.is_mouse_button_pressed(BUTTON_LEFT):
  210.         Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
  211.  
  212. func get_weapon_inputs():
  213.     if !weapon.anim.is_playing() && Input.is_mouse_button_pressed(BUTTON_LEFT):
  214.         weapon.anim.play("shoot")
  215.         if get_tree().is_network_server():
  216.             fire_weapon()
  217.             rpc("receive_server_weapon_state", projectile, camera.rotation.x, rotation.y)
  218.         else:
  219.             #fire_weapon()
  220.             rpc_id(1, "receive_client_weapon_input", rotation.y, camera.rotation.x)
  221.  
  222. func weapon_bobbing(): # animate weapon when walking on ground
  223.     if movedir != Vector3(0,0,0) && is_grounded():
  224.         if $Weapon/anim_bobbing.current_animation != "bobbing":
  225.             $Weapon/anim_bobbing.play("bobbing")
  226.     else:
  227.         $Weapon/anim_bobbing.stop()
  228.         $Weapon.offset = $Weapon.offset.linear_interpolate(Vector2(0,0), 0.05)
  229.  
  230. sync func hurt(dmg, angle):
  231.     if is_network_master() || get_tree().is_network_server():
  232.         if !state == "default" || invulnerable:
  233.             return
  234.        
  235.         if get_tree().is_network_server():
  236.             health -= dmg
  237.        
  238.         emit_signal("hurt")
  239.         state = "hurt"
  240.         get_tree().create_timer(hitstun).connect("timeout", self, "end_hitstun")
  241.        
  242.         velocity.x = angle.x * 5
  243.         velocity.z = angle.z * 5
  244.         velocity.y = 2
  245.  
  246. func end_hitstun():
  247.     state = "default"
  248.  
  249. func _input(event): # rotate camera to mouse && press TAB to unlock mouse
  250.     if !is_network_master():
  251.         return
  252.     if event is InputEventMouseMotion && Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED && state != "dead":
  253.         rotation.y -= event.relative.x * sensitivity # left right
  254.         camera.rotation.x = clamp(camera.rotation.x - event.relative.y * sensitivity, deg2rad(-85), deg2rad(85)) # up down
  255.        
  256.     if event is InputEventKey && event.pressed:
  257.         if event.scancode == KEY_TAB:
  258.             Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
  259.  
  260. # SERVER FUNCTIONS #
  261. # processes inputs received by the client and creates the state every peer will adhere to
  262.  
  263. remote func receive_client_input(input): # takes received client movement & adds it to a queue
  264.     client_input.append(input)
  265.  
  266. remote func receive_client_weapon_input(rot, camrot):
  267.     var new_projectile = fire_weapon(projectile, camrot, rot)
  268.     rpc("receive_server_weapon_state", projectile, camrot, rot)
  269.  
  270. func process_client_movement(): # iterate through and process every input received by the client
  271.     server_timestamp = 0
  272.    
  273.     while client_input.size() > 0:
  274.         var msg = client_input[0]
  275.        
  276.         server_timestamp = max(server_timestamp, msg.timestamp)
  277.        
  278.         # make sure the client isn't trying to punk us with bogus movedirs (speedhacks)
  279.         msg.movedir.x = clamp(msg.movedir.x, -1, 1)
  280.         msg.movedir.z = clamp(msg.movedir.z, -1, 1)
  281.        
  282.         rotation.y = msg.rot
  283.        
  284.         move(msg.movedir)
  285.         client_input.pop_front()
  286.  
  287. func create_server_state(): # saves the player's current state to be sent to every other player-
  288.     var new_state = {
  289.         position = global_transform.origin,
  290.         velocity = velocity,
  291.         rot = rotation.y,
  292.         state = state,
  293.         health = health,
  294.         timestamp = server_timestamp,
  295.         }
  296.        
  297.     queued_states.append(new_state)
  298.  
  299. # PUPPET FUNCTIONS #
  300. # these are player objects that are not controlled by the client i.e. other players
  301.  
  302. func sync_entities(): # sync position & velocity to server state
  303.     packet_elapsed_time += get_physics_process_delta_time() # time since last packet (reset in update_tick)
  304.        
  305.     if server_state:
  306.         var msg = server_state.back()
  307.        
  308.         old_position = server_state.front().position # the oldest known position that will be interpolated from
  309.         new_position = msg.position # the last position that will be interpolated to
  310.        
  311.         velocity = msg.velocity
  312.         rotation.y = msg.rot
  313.         state = msg.state
  314.        
  315.         server_state.clear() # we don't care about this state anymore
  316.    
  317.     ### entity interpolation
  318.     #!# currently the host doesn't interpolate the other players on his screen
  319.     global_transform.origin = old_position.linear_interpolate(new_position, packet_elapsed_time / 0.2)
  320.  
  321. # GENERIC FUNCTIONS #
  322.  
  323. func move(dir = movedir): # process movement based off of direction & velocity
  324.     if state == "dead":
  325.         return
  326.     var delta = get_physics_process_delta_time()
  327.     var hv = Vector3(velocity.x, 0, velocity.z) # horizontal velocity
  328.     var new_pos = dir * SPEED
  329.    
  330.     var accel = DEC
  331.    
  332.     if dir.dot(hv) > 0:
  333.         accel = ACC
  334.    
  335.     hv = hv.linear_interpolate(new_pos, accel * delta) # I know lerping by delta doesn't work like this but I forgot the other way
  336.    
  337.     velocity.x = hv.x
  338.     velocity.z = hv.z
  339.    
  340.     if !is_on_floor():
  341.         velocity.y -= GRAVITY * delta
  342.    
  343.     if dir.y != 0: # on jump key press (refer to get_input)
  344.         velocity.y = JUMP_SPEED
  345.    
  346.     velocity = move_and_slide(velocity, Vector3(0,1,0))
  347.  
  348. func fire_weapon(bullet = projectile, rot_x = camera.rotation.x, rot_y = rotation.y):
  349.     var new_projectile = load(bullet).instance()
  350.     new_projectile.new_position = global_transform.origin
  351.     new_projectile.rotation.x = -rot_x
  352.     new_projectile.rotation.y = rot_y
  353.     new_projectile.shooter = self
  354.     get_parent().add_child(new_projectile)
  355.     return new_projectile
  356.  
  357. func collision(body):
  358.     if body.get_groups().has("projectile") && body.shooter != self && body.visible:
  359.         if get_tree().is_network_server():
  360.             rpc("hurt", body.DAMAGE, body.movedir)
  361.         body.explode()
  362.  
  363. sync func die():
  364.     if state == "dead":
  365.         return
  366.     if !is_network_master():
  367.         var new_death = preload("res://player/death_anim.tscn").instance()
  368.         new_death.global_transform.origin = global_transform.origin
  369.         new_death.material_override = $Sprite3D.material_override
  370.         new_death.scale = $Sprite3D.scale
  371.         get_parent().add_child(new_death)
  372.         hide()
  373.    
  374.     if get_tree().is_network_server():
  375.         get_tree().create_timer(RESPAWN_TIME).connect("timeout", self, "server_respawn")
  376.    
  377.     state = "dead"
  378.     emit_signal("die")
  379.    
  380.     $Camera/ScreenShake.start()
  381.    
  382.     set_collision_layer_bit(1, 0)
  383.     set_collision_mask_bit(1, 0)
  384.     $Hitbox/CollisionShape.disabled = true
  385.    
  386.     if is_network_master():
  387.         weapon.hide()
  388.  
  389. func server_respawn():
  390.     if state != "dead":
  391.         return
  392.     global_transform.origin = get_parent().pick_random_spawn()
  393.    
  394.     rset("global_transform", global_transform)
  395.    
  396.     yield(get_tree().create_timer(0.25), "timeout")
  397.    
  398.     rpc("client_respawn")
  399.  
  400. sync func client_respawn():
  401.     var new_respawn = preload("res://player/respawn_anim.tscn").instance()
  402.     get_parent().add_child(new_respawn)
  403.     new_respawn.global_transform.origin = global_transform.origin
  404.    
  405.     if get_tree().is_network_server():
  406.         get_tree().create_timer(INVULNERABLE_TIME).connect("timeout", self, "set_vulnerable")
  407.         rset("invulnerable", true)
  408.    
  409.     health = MAX_HEALTH
  410.     state = "default"
  411.     velocity = Vector3(0,0,0)
  412.    
  413.     set_collision_layer_bit(1, 1)
  414.     set_collision_mask_bit(1, 1)
  415.     $Hitbox/CollisionShape.disabled = false
  416.    
  417.     emit_signal("respawn")
  418.    
  419.     if is_network_master():
  420.         weapon.show()
  421.    
  422.     yield(get_tree().create_timer(0.1), "timeout")
  423.    
  424.     show()
  425.  
  426. func set_vulnerable():
  427.     rset("invulnerable", false)
  428.  
  429. func set_animation(): # set Sprite3D animation (also refer to that script for billboard sprites)
  430.     var hspeed = Vector3(velocity.x, 0, velocity.z).length() # horizontal speed
  431.    
  432.     if !is_grounded():
  433.         animation = "jump"
  434.     elif hspeed < SPEED * 0.2:
  435.         animation = "idle"
  436.     else:
  437.         animation = "walk"
  438.  
  439. func is_grounded(): # basically is_on_floor combined with a longer RayCast3D to give a little leeway
  440.     if is_on_floor() || $OnGround.is_colliding():
  441.         return true
  442.     return false
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement