Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- extends KinematicBody
- # FPS controller script by fornclake
- # Uses an authoritative server model when connected to a network
- # Includes client-side prediction, server reconciliation, and entity interpolation
- # Ground movement has acceleration & deceleration
- # Currently variable jump height is defunct because of issues syncing jumps with server without jitter
- # Ability to jump is also currently calculated client-side, which is an avenue for cheating and should be looked at
- #############
- # VARIABLES #
- #############
- # PLAYER ATTRIBUTES #
- const SPEED = 6
- const ACC = 5
- const DEC = 5
- const GRAVITY = 20
- const JUMP_SPEED = 8
- const JUMP_DAMP = 0.25 # currently defunct, used for variable jump heights
- const MAX_HEALTH = 100
- const RESPAWN_TIME = 3
- const INVULNERABLE_TIME = 1.5
- # ENGINE VARIABLES #
- var velocity = Vector3(0,0,0)
- var movedir = Vector3()
- var state = "default"
- var last_state = "default"
- var animation = "shoot"
- var can_jump = false
- var health = MAX_HEALTH
- var hitstun = 0.1
- sync var invulnerable = false
- var sensitivity = 0.001
- # WEAPON STUFF
- var weapon = null
- var projectile = "res://weapons/projectiles/power.tscn"
- # OTHER NODES #
- onready var camera = $Camera
- onready var controller = get_parent().get_parent()
- # CLIENT #
- var client_timestamp = 0 # keeps track of when inputs take place
- var pending_inputs = [] # client-only queue of previous inputs used for server reconciliation
- var queued_inputs = [] # similar to above, but is sent to server and cleared once sent
- var server_state = [] # state received from server to sync position & velocity
- var game_time = 0
- # SERVER #
- var server_timestamp = 0 # used for tracking the latest received input
- var client_input = [] # queue of inputs received from client
- var queued_states = [] # queue of states to send to client
- # PUPPET #
- var packet_elapsed_time = 0 # how long it's been since the last packet
- var old_position = Vector3(0,0,0) # position to interpolate from
- var new_position = Vector3(0,0,0) # position to interpolate to
- ###########
- # SIGNALS #
- ###########
- signal hurt
- signal die
- signal respawn
- #############
- # FUNCTIONS #
- #############
- func initialize(): # we use this instead of _ready() so it happens once the player is fully connected
- add_to_group("player")
- $Hitbox.connect("body_entered", self, "collision")
- $Sprite3D.set_material_override($Sprite3D.get_material_override().duplicate(true)) # prevents color changes from affecting every player
- if is_network_master():
- var new_hud = preload("res://ui/hud.tscn").instance()
- add_child(new_hud)
- new_hud.player = self
- connect("hurt", new_hud, "player_hurt")
- connect("die", new_hud, "player_die")
- connect("respawn", new_hud, "player_respawn")
- else:
- $Weapon.queue_free()
- func _physics_process(delta):
- set_animation()
- can_jump = is_on_floor()
- game_time = controller.game_time
- # currently playing player
- if is_network_master():
- camera.current = true
- allow_mouse_capture()
- if state == "default":
- get_inputs()
- get_weapon_inputs()
- weapon_bobbing()
- move()
- # if not the server, prepare packets for server
- if !get_tree().is_network_server():
- create_client_input()
- server_reconciliation()
- # the server
- if get_tree().is_network_server():
- # if not the host, process client movements
- if !is_network_master():
- process_client_movement()
- create_server_state() # prepare packets for client
- if health <= 0 || global_transform.origin.y <= 0:
- rpc("die")
- # puppets
- if !is_network_master() && !get_tree().is_network_server():
- sync_entities()
- if state == "hurt":
- animation = "jump"
- if last_state != "hurt":
- $anim_spritefx.play("hurt")
- last_state = state
- func update_tick(): # called by game.gd; sends packets
- # server updates to clients
- if get_tree().is_network_server():
- # send all of the states that occured since the last tick
- while queued_states.size() > 0:
- rpc_unreliable("receive_server_state", queued_states[0])
- queued_states.pop_front()
- # client updates to server
- elif is_network_master():
- # send all of the inputs that occured since the last tick
- while queued_inputs.size() > 0:
- rpc_unreliable_id(1, "receive_client_input", queued_inputs[0])
- queued_inputs.pop_front()
- # CLIENT FUNCTIONS #
- # stuff that happens on your screen (and other players' screens)
- remote func receive_server_state(state): # takes received state of server & adds it to a queue (used in reconciliation)
- server_state.append(state)
- packet_elapsed_time = 0
- remote func receive_server_weapon_state(bullet, camrot, rot):
- fire_weapon(bullet, camrot, rot)
- func create_client_input(): # takes inputs and adds it to two queues
- var new_input = {
- movedir = movedir,
- rot = rotation.y,
- timestamp = client_timestamp
- }
- pending_inputs.append(new_input) # used in server_reconciliation
- queued_inputs.append(new_input) # used to send to server
- client_timestamp += 1
- func server_reconciliation(): # compares latest received server state & replays unreceived movements
- while server_state.size() > 0:
- var msg = server_state.front() # the earliest unplayed state
- server_state.pop_front()
- # sync player to server state
- global_transform.origin = msg.position
- velocity = msg.velocity
- health = msg.health
- state = msg.state
- # replay movements that the state didn't receive at the time
- var current_input = 0
- while current_input < pending_inputs.size():
- var input = pending_inputs[current_input]
- if input.timestamp <= msg.timestamp: # if the server already processed this input, remove it
- pending_inputs.remove(current_input)
- else: # otherwise, reprocess the input on the client
- move(input.movedir) # takes movedir from the inputs that were unreceived by server
- current_input += 1
- func get_inputs(): # gets currently pressed movement & jump keys
- movedir = Vector3(0,0,0)
- movedir += camera.global_transform.basis.z * (-int(Input.is_action_pressed("up")) + int(Input.is_action_pressed("down")))
- movedir += camera.global_transform.basis.x * (-int(Input.is_action_pressed("left")) + int(Input.is_action_pressed("right")))
- movedir.y = 0
- movedir = movedir.normalized()
- # we just use movedir.y for jumps to save some bandwidth on a jump variable
- if Input.is_action_pressed("jump") && can_jump: # !!this line is dangerous and should eventually be checked by the server!!
- movedir.y = 1
- func allow_mouse_capture(): # lock mouse to screen if clicked
- if Input.is_mouse_button_pressed(BUTTON_LEFT):
- Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
- func get_weapon_inputs():
- if !weapon.anim.is_playing() && Input.is_mouse_button_pressed(BUTTON_LEFT):
- weapon.anim.play("shoot")
- if get_tree().is_network_server():
- fire_weapon()
- rpc("receive_server_weapon_state", projectile, camera.rotation.x, rotation.y)
- else:
- #fire_weapon()
- rpc_id(1, "receive_client_weapon_input", rotation.y, camera.rotation.x)
- func weapon_bobbing(): # animate weapon when walking on ground
- if movedir != Vector3(0,0,0) && is_grounded():
- if $Weapon/anim_bobbing.current_animation != "bobbing":
- $Weapon/anim_bobbing.play("bobbing")
- else:
- $Weapon/anim_bobbing.stop()
- $Weapon.offset = $Weapon.offset.linear_interpolate(Vector2(0,0), 0.05)
- sync func hurt(dmg, angle):
- if is_network_master() || get_tree().is_network_server():
- if !state == "default" || invulnerable:
- return
- if get_tree().is_network_server():
- health -= dmg
- emit_signal("hurt")
- state = "hurt"
- get_tree().create_timer(hitstun).connect("timeout", self, "end_hitstun")
- velocity.x = angle.x * 5
- velocity.z = angle.z * 5
- velocity.y = 2
- func end_hitstun():
- state = "default"
- func _input(event): # rotate camera to mouse && press TAB to unlock mouse
- if !is_network_master():
- return
- if event is InputEventMouseMotion && Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED && state != "dead":
- rotation.y -= event.relative.x * sensitivity # left right
- camera.rotation.x = clamp(camera.rotation.x - event.relative.y * sensitivity, deg2rad(-85), deg2rad(85)) # up down
- if event is InputEventKey && event.pressed:
- if event.scancode == KEY_TAB:
- Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
- # SERVER FUNCTIONS #
- # processes inputs received by the client and creates the state every peer will adhere to
- remote func receive_client_input(input): # takes received client movement & adds it to a queue
- client_input.append(input)
- remote func receive_client_weapon_input(rot, camrot):
- var new_projectile = fire_weapon(projectile, camrot, rot)
- rpc("receive_server_weapon_state", projectile, camrot, rot)
- func process_client_movement(): # iterate through and process every input received by the client
- server_timestamp = 0
- while client_input.size() > 0:
- var msg = client_input[0]
- server_timestamp = max(server_timestamp, msg.timestamp)
- # make sure the client isn't trying to punk us with bogus movedirs (speedhacks)
- msg.movedir.x = clamp(msg.movedir.x, -1, 1)
- msg.movedir.z = clamp(msg.movedir.z, -1, 1)
- rotation.y = msg.rot
- move(msg.movedir)
- client_input.pop_front()
- func create_server_state(): # saves the player's current state to be sent to every other player-
- var new_state = {
- position = global_transform.origin,
- velocity = velocity,
- rot = rotation.y,
- state = state,
- health = health,
- timestamp = server_timestamp,
- }
- queued_states.append(new_state)
- # PUPPET FUNCTIONS #
- # these are player objects that are not controlled by the client i.e. other players
- func sync_entities(): # sync position & velocity to server state
- packet_elapsed_time += get_physics_process_delta_time() # time since last packet (reset in update_tick)
- if server_state:
- var msg = server_state.back()
- old_position = server_state.front().position # the oldest known position that will be interpolated from
- new_position = msg.position # the last position that will be interpolated to
- velocity = msg.velocity
- rotation.y = msg.rot
- state = msg.state
- server_state.clear() # we don't care about this state anymore
- ### entity interpolation
- #!# currently the host doesn't interpolate the other players on his screen
- global_transform.origin = old_position.linear_interpolate(new_position, packet_elapsed_time / 0.2)
- # GENERIC FUNCTIONS #
- func move(dir = movedir): # process movement based off of direction & velocity
- if state == "dead":
- return
- var delta = get_physics_process_delta_time()
- var hv = Vector3(velocity.x, 0, velocity.z) # horizontal velocity
- var new_pos = dir * SPEED
- var accel = DEC
- if dir.dot(hv) > 0:
- accel = ACC
- hv = hv.linear_interpolate(new_pos, accel * delta) # I know lerping by delta doesn't work like this but I forgot the other way
- velocity.x = hv.x
- velocity.z = hv.z
- if !is_on_floor():
- velocity.y -= GRAVITY * delta
- if dir.y != 0: # on jump key press (refer to get_input)
- velocity.y = JUMP_SPEED
- velocity = move_and_slide(velocity, Vector3(0,1,0))
- func fire_weapon(bullet = projectile, rot_x = camera.rotation.x, rot_y = rotation.y):
- var new_projectile = load(bullet).instance()
- new_projectile.new_position = global_transform.origin
- new_projectile.rotation.x = -rot_x
- new_projectile.rotation.y = rot_y
- new_projectile.shooter = self
- get_parent().add_child(new_projectile)
- return new_projectile
- func collision(body):
- if body.get_groups().has("projectile") && body.shooter != self && body.visible:
- if get_tree().is_network_server():
- rpc("hurt", body.DAMAGE, body.movedir)
- body.explode()
- sync func die():
- if state == "dead":
- return
- if !is_network_master():
- var new_death = preload("res://player/death_anim.tscn").instance()
- new_death.global_transform.origin = global_transform.origin
- new_death.material_override = $Sprite3D.material_override
- new_death.scale = $Sprite3D.scale
- get_parent().add_child(new_death)
- hide()
- if get_tree().is_network_server():
- get_tree().create_timer(RESPAWN_TIME).connect("timeout", self, "server_respawn")
- state = "dead"
- emit_signal("die")
- $Camera/ScreenShake.start()
- set_collision_layer_bit(1, 0)
- set_collision_mask_bit(1, 0)
- $Hitbox/CollisionShape.disabled = true
- if is_network_master():
- weapon.hide()
- func server_respawn():
- if state != "dead":
- return
- global_transform.origin = get_parent().pick_random_spawn()
- rset("global_transform", global_transform)
- yield(get_tree().create_timer(0.25), "timeout")
- rpc("client_respawn")
- sync func client_respawn():
- var new_respawn = preload("res://player/respawn_anim.tscn").instance()
- get_parent().add_child(new_respawn)
- new_respawn.global_transform.origin = global_transform.origin
- if get_tree().is_network_server():
- get_tree().create_timer(INVULNERABLE_TIME).connect("timeout", self, "set_vulnerable")
- rset("invulnerable", true)
- health = MAX_HEALTH
- state = "default"
- velocity = Vector3(0,0,0)
- set_collision_layer_bit(1, 1)
- set_collision_mask_bit(1, 1)
- $Hitbox/CollisionShape.disabled = false
- emit_signal("respawn")
- if is_network_master():
- weapon.show()
- yield(get_tree().create_timer(0.1), "timeout")
- show()
- func set_vulnerable():
- rset("invulnerable", false)
- func set_animation(): # set Sprite3D animation (also refer to that script for billboard sprites)
- var hspeed = Vector3(velocity.x, 0, velocity.z).length() # horizontal speed
- if !is_grounded():
- animation = "jump"
- elif hspeed < SPEED * 0.2:
- animation = "idle"
- else:
- animation = "walk"
- func is_grounded(): # basically is_on_floor combined with a longer RayCast3D to give a little leeway
- if is_on_floor() || $OnGround.is_colliding():
- return true
- return false
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement