Various enhancements, fixes, and tweaks focused on improving the inspector (#34)

* Added Dynamic Gravity Toggle

Added an editor toggle for dynamic gravity so users don't need to uncomment code in character.gd.

* Refactor controls export variables

Changed controls export variables to be a single dictionary so they can be edited completely in the editor without having to open the script. I also refactored the controller support section to allow similar functionality for controller specific controls and removed the need to uncomment/comment out lines of code to turn on/off controller support.

* Add more editor descriptions and implement camera axis inversion options

Added/tweaked export variable descriptions so everything has one. Also slightly refactored the camera input inversion code and added the ability to invert the camera x axis.

* Added direction based landing animations

Added some logic to determine which landing animation to play so it's not random anymore. Also added a land_center animation for the case of neutral landings. This can be expanded if desired, but it's pretty convincing as is and is ulitmately a small detail.

* Correct various grammar and spelling errors

A final pass to clean up any grammatical or spelling errors in the comments

* Re-organized Character.gd

Hope I'm not overstepping here, but I re-organized Character.gd to make it a bit easier to read and work with. Added editor bookmarks and collapsible code regions. It's hard to parse in the diff so I recommend you look at it in Godot proper.

* Commented out debug print statement

I assume this was left uncommented by mistake. I was wondering what was printing that to my console.

* Fixed Landing Animation Logic

I guess I was up too late on my first attempt because it straight up didn't work. This new version works as far as my testing can tell.

* Removed some unused variables
This commit is contained in:
Austin Dennis
2024-10-25 16:30:03 -04:00
committed by GitHub
parent 288618e6a3
commit 2885aadf4e
3 changed files with 275 additions and 152 deletions

View File

@ -31,7 +31,7 @@ extends Node
func _ready(): func _ready():
if !Engine.is_editor_hint(): if !Engine.is_editor_hint():
print("not editor") #print("not editor")
HEAD.rotation.y = deg_to_rad(head_y_rotation) HEAD.rotation.y = deg_to_rad(head_y_rotation)
HEAD.rotation.x = deg_to_rad(head_x_rotation) HEAD.rotation.x = deg_to_rad(head_x_rotation)

View File

@ -1,12 +1,13 @@
# COPYRIGHT Colormatic Studios # COPYRIGHT Colormatic Studios
# MIT licence # MIT license
# Quality Godot First Person Controller v2 # Quality Godot First Person Controller v2
extends CharacterBody3D extends CharacterBody3D
#region Character Export Group
## The settings for the character's movement and feel. ## The settings for the character's movement and feel.
@export_category("Character") @export_category("Character")
## The speed that the character moves at without crouching or sprinting. ## The speed that the character moves at without crouching or sprinting.
@ -22,52 +23,82 @@ extends CharacterBody3D
@export var jump_velocity : float = 4.5 @export var jump_velocity : float = 4.5
## How far the player turns when the mouse is moved. ## How far the player turns when the mouse is moved.
@export var mouse_sensitivity : float = 0.1 @export var mouse_sensitivity : float = 0.1
## Invert the Y input for mouse and joystick ## Invert the X axis input for the camera.
@export var invert_mouse_y : bool = false # Possibly add an invert mouse X in the future @export var invert_camera_x_axis : bool = false
## Wether the player can use movement inputs. Does not stop outside forces or jumping. See Jumping Enabled. ## Invert the Y axis input for the camera.
@export var invert_camera_y_axis : bool = false
## Whether the player can use movement inputs. Does not stop outside forces or jumping. See Jumping Enabled.
@export var immobile : bool = false @export var immobile : bool = false
## The reticle file to import at runtime. By default are in res://addons/fpc/reticles/. Set to an empty string to remove. ## The reticle file to import at runtime. By default are in res://addons/fpc/reticles/. Set to an empty string to remove.
@export_file var default_reticle @export_file var default_reticle
#endregion
#region Nodes Export Group
@export_group("Nodes") @export_group("Nodes")
## The node that holds the camera. This is rotated instead of the camera for mouse input. ## A reference to the camera for use in the character script. This is the parent node to the camera and is rotated instead of the camera for mouse input.
@export var HEAD : Node3D @export var HEAD : Node3D
## A reference to the camera for use in the character script.
@export var CAMERA : Camera3D @export var CAMERA : Camera3D
## A reference to the headbob animation for use in the character script.
@export var HEADBOB_ANIMATION : AnimationPlayer @export var HEADBOB_ANIMATION : AnimationPlayer
## A reference to the jump animation for use in the character script.
@export var JUMP_ANIMATION : AnimationPlayer @export var JUMP_ANIMATION : AnimationPlayer
## A reference to the crouch animation for use in the character script.
@export var CROUCH_ANIMATION : AnimationPlayer @export var CROUCH_ANIMATION : AnimationPlayer
## A reference to the the player's collision shape for use in the character script.
@export var COLLISION_MESH : CollisionShape3D @export var COLLISION_MESH : CollisionShape3D
@export_group("Controls") #endregion
# We are using UI controls because they are built into Godot Engine so they can be used right away
@export var JUMP : String = "ui_accept"
@export var LEFT : String = "ui_left"
@export var RIGHT : String = "ui_right"
@export var FORWARD : String = "ui_up"
@export var BACKWARD : String = "ui_down"
## By default this does not pause the game, but that can be changed in _process.
@export var PAUSE : String = "ui_cancel"
@export var CROUCH : String = "crouch"
@export var SPRINT : String = "sprint"
# Uncomment if you want controller support #region Controls Export Group
#@export var controller_sensitivity : float = 0.035
#@export var LOOK_LEFT : String = "look_left" # We are using UI controls because they are built into Godot Engine so they can be used right away
#@export var LOOK_RIGHT : String = "look_right" @export_group("Controls")
#@export var LOOK_UP : String = "look_up" ## Use the Input Map to map a mouse/keyboard input to an action and add a reference to it to this dictionary to be used in the script.
#@export var LOOK_DOWN : String = "look_down" @export var controls : Dictionary = {
LEFT = "ui_left",
RIGHT = "ui_right",
FORWARD = "ui_up",
BACKWARD = "ui_down",
JUMP = "ui_accept",
CROUCH = "crouch",
SPRINT = "sprint",
PAUSE = "ui_cancel"
}
@export_subgroup("Controller Specific")
## This only affects how the camera is handled, the rest should be covered by adding controller inputs to the existing actions in the Input Map.
@export var controller_support : bool = false
## Use the Input Map to map a controller input to an action and add a reference to it to this dictionary to be used in the script.
@export var controller_controls : Dictionary = {
LOOK_LEFT = "look_left",
LOOK_RIGHT = "look_right",
LOOK_UP = "look_up",
LOOK_DOWN = "look_down"
}
## The sensitivity of the analog stick that controls camera rotation. Lower is less sensitive and higher is more sensitive.
@export_range(0.001, 1, 0.001) var look_sensitivity : float = 0.035
#endregion
#region Feature Settings Export Group
@export_group("Feature Settings") @export_group("Feature Settings")
## Enable or disable jumping. Useful for restrictive storytelling environments. ## Enable or disable jumping. Useful for restrictive storytelling environments.
@export var jumping_enabled : bool = true @export var jumping_enabled : bool = true
## Wether the player can move in the air or not. ## Whether the player can move in the air or not.
@export var in_air_momentum : bool = true @export var in_air_momentum : bool = true
## Smooths the feel of walking. ## Smooths the feel of walking.
@export var motion_smoothing : bool = true @export var motion_smoothing : bool = true
## Enables or disables sprinting.
@export var sprint_enabled : bool = true @export var sprint_enabled : bool = true
@export var crouch_enabled : bool = true ## Toggles the sprinting state when button is pressed or requires the player to hold the button down to remain sprinting.
@export_enum("Hold to Crouch", "Toggle Crouch") var crouch_mode : int = 0
@export_enum("Hold to Sprint", "Toggle Sprint") var sprint_mode : int = 0 @export_enum("Hold to Sprint", "Toggle Sprint") var sprint_mode : int = 0
## Enables or disables crouching.
@export var crouch_enabled : bool = true
## Toggles the crouch state when button is pressed or requires the player to hold the button down to remain crouched.
@export_enum("Hold to Crouch", "Toggle Crouch") var crouch_mode : int = 0
## Wether sprinting should effect FOV. ## Wether sprinting should effect FOV.
@export var dynamic_fov : bool = true @export var dynamic_fov : bool = true
## If the player holds down the jump button, should the player keep hopping. ## If the player holds down the jump button, should the player keep hopping.
@ -80,14 +111,19 @@ extends CharacterBody3D
@export var pausing_enabled : bool = true @export var pausing_enabled : bool = true
## Use with caution. ## Use with caution.
@export var gravity_enabled : bool = true @export var gravity_enabled : bool = true
## If your game changes the gravity value during gameplay, check this property to allow the player to experience the change in gravity.
@export var dynamic_gravity : bool = false
#endregion
# Member variables #region Member Variable Initialization
# These are variables used in this script that don't need to be exposed in the editor.
var speed : float = base_speed var speed : float = base_speed
var current_speed : float = 0.0 var current_speed : float = 0.0
# States: normal, crouching, sprinting # States: normal, crouching, sprinting
var state : String = "normal" var state : String = "normal"
var low_ceiling : bool = false # This is for when the cieling is too low and the player needs to crouch. var low_ceiling : bool = false # This is for when the ceiling is too low and the player needs to crouch.
var was_on_floor : bool = true # Was the player on the floor last frame (for landing animation) var was_on_floor : bool = true # Was the player on the floor last frame (for landing animation)
# The reticle should always have a Control node as the root # The reticle should always have a Control node as the root
@ -96,124 +132,86 @@ var RETICLE : Control
# Get the gravity from the project settings to be synced with RigidBody nodes # Get the gravity from the project settings to be synced with RigidBody nodes
var gravity : float = ProjectSettings.get_setting("physics/3d/default_gravity") # Don't set this as a const, see the gravity section in _physics_process var gravity : float = ProjectSettings.get_setting("physics/3d/default_gravity") # Don't set this as a const, see the gravity section in _physics_process
# Stores mouse input for rotating the camera in the phyhsics process # Stores mouse input for rotating the camera in the physics process
var mouseInput : Vector2 = Vector2(0,0) var mouseInput : Vector2 = Vector2(0,0)
#endregion
#region Main Control Flow
func _ready(): func _ready():
#It is safe to comment this line if your game doesn't start with the mouse captured #It is safe to comment this line if your game doesn't start with the mouse captured
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# If the controller is rotated in a certain direction for game design purposes, redirect this rotation into the head. # If the controller is rotated in a certain direction for game design purposes, redirect this rotation into the head.
HEAD.rotation.y = rotation.y HEAD.rotation.y = rotation.y
rotation.y = 0 rotation.y = 0
if default_reticle: if default_reticle:
change_reticle(default_reticle) change_reticle(default_reticle)
# Reset the camera position initialize_animations()
# If you want to change the default head height, change these animations.
HEADBOB_ANIMATION.play("RESET")
JUMP_ANIMATION.play("RESET")
CROUCH_ANIMATION.play("RESET")
check_controls() check_controls()
enter_normal_state() enter_normal_state()
func check_controls(): # If you add a control, you might want to add a check for it here.
# The actions are being disabled so the engine doesn't halt the entire project in debug mode func _process(_delta):
if !InputMap.has_action(JUMP): if pausing_enabled:
push_error("No control mapped for jumping. Please add an input map control. Disabling jump.") handle_pausing()
jumping_enabled = false
if !InputMap.has_action(LEFT): update_debug_menu_per_frame()
push_error("No control mapped for move left. Please add an input map control. Disabling movement.")
immobile = true
if !InputMap.has_action(RIGHT):
push_error("No control mapped for move right. Please add an input map control. Disabling movement.")
immobile = true
if !InputMap.has_action(FORWARD):
push_error("No control mapped for move forward. Please add an input map control. Disabling movement.")
immobile = true
if !InputMap.has_action(BACKWARD):
push_error("No control mapped for move backward. Please add an input map control. Disabling movement.")
immobile = true
if !InputMap.has_action(PAUSE):
push_error("No control mapped for pause. Please add an input map control. Disabling pausing.")
pausing_enabled = false
if !InputMap.has_action(CROUCH):
push_error("No control mapped for crouch. Please add an input map control. Disabling crouching.")
crouch_enabled = false
if !InputMap.has_action(SPRINT):
push_error("No control mapped for sprint. Please add an input map control. Disabling sprinting.")
sprint_enabled = false
func change_reticle(reticle): # Yup, this function is kinda strange func _physics_process(delta): # Most things happen here.
if RETICLE:
RETICLE.queue_free()
RETICLE = load(reticle).instantiate()
RETICLE.character = self
$UserInterface.add_child(RETICLE)
func _physics_process(delta):
# Big thanks to github.com/LorenzoAncora for the concept of the improved debug values
current_speed = Vector3.ZERO.distance_to(get_real_velocity())
$UserInterface/DebugPanel.add_property("Speed", snappedf(current_speed, 0.001), 1)
$UserInterface/DebugPanel.add_property("Target speed", speed, 2)
var cv : Vector3 = get_real_velocity()
var vd : Array[float] = [
snappedf(cv.x, 0.001),
snappedf(cv.y, 0.001),
snappedf(cv.z, 0.001)
]
var readable_velocity : String = "X: " + str(vd[0]) + " Y: " + str(vd[1]) + " Z: " + str(vd[2])
$UserInterface/DebugPanel.add_property("Velocity", readable_velocity, 3)
# Gravity # Gravity
#gravity = ProjectSettings.get_setting("physics/3d/default_gravity") # If the gravity changes during your game, uncomment this code if dynamic_gravity:
gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
if not is_on_floor() and gravity and gravity_enabled: if not is_on_floor() and gravity and gravity_enabled:
velocity.y -= gravity * delta velocity.y -= gravity * delta
handle_jumping() handle_jumping()
var input_dir = Vector2.ZERO var input_dir = Vector2.ZERO
if !immobile: # Immobility works by interrupting user input, so other forces can still be applied to the player
input_dir = Input.get_vector(LEFT, RIGHT, FORWARD, BACKWARD) if not immobile: # Immobility works by interrupting user input, so other forces can still be applied to the player
input_dir = Input.get_vector(controls.LEFT, controls.RIGHT, controls.FORWARD, controls.BACKWARD)
handle_movement(delta, input_dir) handle_movement(delta, input_dir)
handle_head_rotation() handle_head_rotation()
# The player is not able to stand up if the ceiling is too low # The player is not able to stand up if the ceiling is too low
low_ceiling = $CrouchCeilingDetection.is_colliding() low_ceiling = $CrouchCeilingDetection.is_colliding()
handle_state(input_dir) handle_state(input_dir)
if dynamic_fov: # This may be changed to an AnimationPlayer if dynamic_fov: # This may be changed to an AnimationPlayer
update_camera_fov() update_camera_fov()
if view_bobbing: if view_bobbing:
headbob_animation(input_dir) play_headbob_animation(input_dir)
if jump_animation: if jump_animation:
if !was_on_floor and is_on_floor(): # The player just landed play_jump_animation()
match randi() % 2: #TODO: Change this to detecting velocity direction
0: update_debug_menu_per_tick()
JUMP_ANIMATION.play("land_left", 0.25)
1:
JUMP_ANIMATION.play("land_right", 0.25)
was_on_floor = is_on_floor() # This must always be at the end of physics_process was_on_floor = is_on_floor() # This must always be at the end of physics_process
#endregion
#region Input Handling
func handle_jumping(): func handle_jumping():
if jumping_enabled: if jumping_enabled:
if continuous_jumping: # Hold down the jump button if continuous_jumping: # Hold down the jump button
if Input.is_action_pressed(JUMP) and is_on_floor() and !low_ceiling: if Input.is_action_pressed(controls.JUMP) and is_on_floor() and !low_ceiling:
if jump_animation: if jump_animation:
JUMP_ANIMATION.play("jump", 0.25) JUMP_ANIMATION.play("jump", 0.25)
velocity.y += jump_velocity # Adding instead of setting so jumping on slopes works properly velocity.y += jump_velocity # Adding instead of setting so jumping on slopes works properly
else: else:
if Input.is_action_just_pressed(JUMP) and is_on_floor() and !low_ceiling: if Input.is_action_just_pressed(controls.JUMP) and is_on_floor() and !low_ceiling:
if jump_animation: if jump_animation:
JUMP_ANIMATION.play("jump", 0.25) JUMP_ANIMATION.play("jump", 0.25)
velocity.y += jump_velocity velocity.y += jump_velocity
@ -223,7 +221,7 @@ func handle_movement(delta, input_dir):
var direction = input_dir.rotated(-HEAD.rotation.y) var direction = input_dir.rotated(-HEAD.rotation.y)
direction = Vector3(direction.x, 0, direction.y) direction = Vector3(direction.x, 0, direction.y)
move_and_slide() move_and_slide()
if in_air_momentum: if in_air_momentum:
if is_on_floor(): if is_on_floor():
if motion_smoothing: if motion_smoothing:
@ -240,30 +238,69 @@ func handle_movement(delta, input_dir):
velocity.x = direction.x * speed velocity.x = direction.x * speed
velocity.z = direction.z * speed velocity.z = direction.z * speed
func handle_head_rotation(): func handle_head_rotation():
HEAD.rotation_degrees.y -= mouseInput.x * mouse_sensitivity if invert_camera_x_axis:
if invert_mouse_y: HEAD.rotation_degrees.y -= mouseInput.x * mouse_sensitivity * -1
HEAD.rotation_degrees.x -= mouseInput.y * mouse_sensitivity * -1.0 else:
HEAD.rotation_degrees.y -= mouseInput.x * mouse_sensitivity
if invert_camera_y_axis:
HEAD.rotation_degrees.x -= mouseInput.y * mouse_sensitivity * -1
else: else:
HEAD.rotation_degrees.x -= mouseInput.y * mouse_sensitivity HEAD.rotation_degrees.x -= mouseInput.y * mouse_sensitivity
# Uncomment for controller support if controller_support:
#var controller_view_rotation = Input.get_vector(LOOK_DOWN, LOOK_UP, LOOK_RIGHT, LOOK_LEFT) * controller_sensitivity # These are inverted because of the nature of 3D rotation. var controller_view_rotation = Input.get_vector(controller_controls.LOOK_DOWN, controller_controls.LOOK_UP, controller_controls.LOOK_RIGHT, controller_controls.LOOK_LEFT) * look_sensitivity # These are inverted because of the nature of 3D rotation.
#HEAD.rotation.x += controller_view_rotation.x if invert_camera_x_axis:
#if invert_mouse_y: HEAD.rotation.x += controller_view_rotation.x * -1
#HEAD.rotation.y += controller_view_rotation.y * -1.0 else:
#else: HEAD.rotation.x += controller_view_rotation.x
#HEAD.rotation.y += controller_view_rotation.y
if invert_camera_y_axis:
HEAD.rotation.y += controller_view_rotation.y * -1
else:
HEAD.rotation.y += controller_view_rotation.y
mouseInput = Vector2(0,0) mouseInput = Vector2(0,0)
HEAD.rotation.x = clamp(HEAD.rotation.x, deg_to_rad(-90), deg_to_rad(90)) HEAD.rotation.x = clamp(HEAD.rotation.x, deg_to_rad(-90), deg_to_rad(90))
func check_controls(): # If you add a control, you might want to add a check for it here.
# The actions are being disabled so the engine doesn't halt the entire project in debug mode
if !InputMap.has_action(controls.JUMP):
push_error("No control mapped for jumping. Please add an input map control. Disabling jump.")
jumping_enabled = false
if !InputMap.has_action(controls.LEFT):
push_error("No control mapped for move left. Please add an input map control. Disabling movement.")
immobile = true
if !InputMap.has_action(controls.RIGHT):
push_error("No control mapped for move right. Please add an input map control. Disabling movement.")
immobile = true
if !InputMap.has_action(controls.FORWARD):
push_error("No control mapped for move forward. Please add an input map control. Disabling movement.")
immobile = true
if !InputMap.has_action(controls.BACKWARD):
push_error("No control mapped for move backward. Please add an input map control. Disabling movement.")
immobile = true
if !InputMap.has_action(controls.PAUSE):
push_error("No control mapped for pause. Please add an input map control. Disabling pausing.")
pausing_enabled = false
if !InputMap.has_action(controls.CROUCH):
push_error("No control mapped for crouch. Please add an input map control. Disabling crouching.")
crouch_enabled = false
if !InputMap.has_action(controls.SPRINT):
push_error("No control mapped for sprint. Please add an input map control. Disabling sprinting.")
sprint_enabled = false
#endregion
#region State Handling
func handle_state(moving): func handle_state(moving):
if sprint_enabled: if sprint_enabled:
if sprint_mode == 0: if sprint_mode == 0:
if Input.is_action_pressed(SPRINT) and state != "crouching": if Input.is_action_pressed(controls.SPRINT) and state != "crouching":
if moving: if moving:
if state != "sprinting": if state != "sprinting":
enter_sprint_state() enter_sprint_state()
@ -274,10 +311,10 @@ func handle_state(moving):
enter_normal_state() enter_normal_state()
elif sprint_mode == 1: elif sprint_mode == 1:
if moving: if moving:
# If the player is holding sprint before moving, handle that cenerio # If the player is holding sprint before moving, handle that scenario
if Input.is_action_pressed(SPRINT) and state == "normal": if Input.is_action_pressed(controls.SPRINT) and state == "normal":
enter_sprint_state() enter_sprint_state()
if Input.is_action_just_pressed(SPRINT): if Input.is_action_just_pressed(controls.SPRINT):
match state: match state:
"normal": "normal":
enter_sprint_state() enter_sprint_state()
@ -285,16 +322,16 @@ func handle_state(moving):
enter_normal_state() enter_normal_state()
elif state == "sprinting": elif state == "sprinting":
enter_normal_state() enter_normal_state()
if crouch_enabled: if crouch_enabled:
if crouch_mode == 0: if crouch_mode == 0:
if Input.is_action_pressed(CROUCH) and state != "sprinting": if Input.is_action_pressed(controls.CROUCH) and state != "sprinting":
if state != "crouching": if state != "crouching":
enter_crouch_state() enter_crouch_state()
elif state == "crouching" and !$CrouchCeilingDetection.is_colliding(): elif state == "crouching" and !$CrouchCeilingDetection.is_colliding():
enter_normal_state() enter_normal_state()
elif crouch_mode == 1: elif crouch_mode == 1:
if Input.is_action_just_pressed(CROUCH): if Input.is_action_just_pressed(controls.CROUCH):
match state: match state:
"normal": "normal":
enter_crouch_state() enter_crouch_state()
@ -304,7 +341,6 @@ func handle_state(moving):
# Any enter state function should only be called once when you want to enter that state, not every frame. # Any enter state function should only be called once when you want to enter that state, not every frame.
func enter_normal_state(): func enter_normal_state():
#print("entering normal state") #print("entering normal state")
var prev_state = state var prev_state = state
@ -315,7 +351,6 @@ func enter_normal_state():
func enter_crouch_state(): func enter_crouch_state():
#print("entering crouch state") #print("entering crouch state")
var prev_state = state
state = "crouching" state = "crouching"
speed = crouch_speed speed = crouch_speed
CROUCH_ANIMATION.play("crouch") CROUCH_ANIMATION.play("crouch")
@ -328,15 +363,18 @@ func enter_sprint_state():
state = "sprinting" state = "sprinting"
speed = sprint_speed speed = sprint_speed
#endregion
func update_camera_fov(): #region Animation Handling
if state == "sprinting":
CAMERA.fov = lerp(CAMERA.fov, 85.0, 0.3)
else:
CAMERA.fov = lerp(CAMERA.fov, 75.0, 0.3)
func initialize_animations():
# Reset the camera position
# If you want to change the default head height, change these animations.
HEADBOB_ANIMATION.play("RESET")
JUMP_ANIMATION.play("RESET")
CROUCH_ANIMATION.play("RESET")
func headbob_animation(moving): func play_headbob_animation(moving):
if moving and is_on_floor(): if moving and is_on_floor():
var use_headbob_animation : String var use_headbob_animation : String
match state: match state:
@ -344,11 +382,11 @@ func headbob_animation(moving):
use_headbob_animation = "walk" use_headbob_animation = "walk"
"sprinting": "sprinting":
use_headbob_animation = "sprint" use_headbob_animation = "sprint"
var was_playing : bool = false var was_playing : bool = false
if HEADBOB_ANIMATION.current_animation == use_headbob_animation: if HEADBOB_ANIMATION.current_animation == use_headbob_animation:
was_playing = true was_playing = true
HEADBOB_ANIMATION.play(use_headbob_animation, 0.25) HEADBOB_ANIMATION.play(use_headbob_animation, 0.25)
HEADBOB_ANIMATION.speed_scale = (current_speed / base_speed) * 1.75 HEADBOB_ANIMATION.speed_scale = (current_speed / base_speed) * 1.75
if !was_playing: if !was_playing:
@ -357,30 +395,53 @@ func headbob_animation(moving):
# The headbob animation has two starting positions. One is at 0 and the other is at 1. # The headbob animation has two starting positions. One is at 0 and the other is at 1.
# randi() % 2 returns either 0 or 1, and so the animation randomly starts at one of the starting positions. # randi() % 2 returns either 0 or 1, and so the animation randomly starts at one of the starting positions.
# This code is extremely performant but it makes no sense. # This code is extremely performant but it makes no sense.
else: else:
if HEADBOB_ANIMATION.current_animation == "sprint" or HEADBOB_ANIMATION.current_animation == "walk": if HEADBOB_ANIMATION.current_animation == "sprint" or HEADBOB_ANIMATION.current_animation == "walk":
HEADBOB_ANIMATION.speed_scale = 1 HEADBOB_ANIMATION.speed_scale = 1
HEADBOB_ANIMATION.play("RESET", 1) HEADBOB_ANIMATION.play("RESET", 1)
func play_jump_animation():
if !was_on_floor and is_on_floor(): # The player just landed
var facing_direction : Vector3 = CAMERA.get_global_transform().basis.x
var facing_direction_2D : Vector2 = Vector2(facing_direction.x, facing_direction.z).normalized()
var velocity_2D : Vector2 = Vector2(velocity.x, velocity.z).normalized()
func _process(delta): # Compares velocity direction against the camera direction (via dot product) to determine which landing animation to play.
var side_landed : int = round(velocity_2D.dot(facing_direction_2D))
if side_landed > 0:
JUMP_ANIMATION.play("land_right", 0.25)
elif side_landed < 0:
JUMP_ANIMATION.play("land_left", 0.25)
else:
JUMP_ANIMATION.play("land_center", 0.25)
#endregion
#region Debug Menu
func update_debug_menu_per_frame():
$UserInterface/DebugPanel.add_property("FPS", Performance.get_monitor(Performance.TIME_FPS), 0) $UserInterface/DebugPanel.add_property("FPS", Performance.get_monitor(Performance.TIME_FPS), 0)
var status : String = state var status : String = state
if !is_on_floor(): if !is_on_floor():
status += " in the air" status += " in the air"
$UserInterface/DebugPanel.add_property("State", status, 4) $UserInterface/DebugPanel.add_property("State", status, 4)
if pausing_enabled:
if Input.is_action_just_pressed(PAUSE): func update_debug_menu_per_tick():
# You may want another node to handle pausing, because this player may get paused too. # Big thanks to github.com/LorenzoAncora for the concept of the improved debug values
match Input.mouse_mode: current_speed = Vector3.ZERO.distance_to(get_real_velocity())
Input.MOUSE_MODE_CAPTURED: $UserInterface/DebugPanel.add_property("Speed", snappedf(current_speed, 0.001), 1)
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE $UserInterface/DebugPanel.add_property("Target speed", speed, 2)
#get_tree().paused = false var cv : Vector3 = get_real_velocity()
Input.MOUSE_MODE_VISIBLE: var vd : Array[float] = [
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED snappedf(cv.x, 0.001),
#get_tree().paused = false snappedf(cv.y, 0.001),
snappedf(cv.z, 0.001)
]
var readable_velocity : String = "X: " + str(vd[0]) + " Y: " + str(vd[1]) + " Z: " + str(vd[2])
$UserInterface/DebugPanel.add_property("Velocity", readable_velocity, 3)
func _unhandled_input(event : InputEvent): func _unhandled_input(event : InputEvent):
@ -393,3 +454,35 @@ func _unhandled_input(event : InputEvent):
# Where we're going, we don't need InputMap # Where we're going, we don't need InputMap
if event.keycode == 4194338: # F7 if event.keycode == 4194338: # F7
$UserInterface/DebugPanel.visible = !$UserInterface/DebugPanel.visible $UserInterface/DebugPanel.visible = !$UserInterface/DebugPanel.visible
#endregion
#region Misc Functions
func change_reticle(reticle): # Yup, this function is kinda strange
if RETICLE:
RETICLE.queue_free()
RETICLE = load(reticle).instantiate()
RETICLE.character = self
$UserInterface.add_child(RETICLE)
func update_camera_fov():
if state == "sprinting":
CAMERA.fov = lerp(CAMERA.fov, 85.0, 0.3)
else:
CAMERA.fov = lerp(CAMERA.fov, 75.0, 0.3)
func handle_pausing():
if Input.is_action_just_pressed(controls.PAUSE):
# You may want another node to handle pausing, because this player may get paused too.
match Input.mouse_mode:
Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
#get_tree().paused = false
Input.MOUSE_MODE_VISIBLE:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
#get_tree().paused = false
#endregion

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=21 format=3 uid="uid://cc1m2a1obsyn4"] [gd_scene load_steps=22 format=3 uid="uid://cc1m2a1obsyn4"]
[ext_resource type="Script" path="res://addons/fpc/character.gd" id="1_0t4e8"] [ext_resource type="Script" path="res://addons/fpc/character.gd" id="1_0t4e8"]
[ext_resource type="Script" path="res://addons/fpc/EditorModule.gd" id="3_v3ckk"] [ext_resource type="Script" path="res://addons/fpc/EditorModule.gd" id="3_v3ckk"]
@ -335,10 +335,39 @@ tracks/1/keys = {
"values": [Vector3(0, 0, 0), Vector3(0, -0.1, 0), Vector3(0, 0, 0)] "values": [Vector3(0, 0, 0), Vector3(0, -0.1, 0), Vector3(0, 0, 0)]
} }
[sub_resource type="Animation" id="Animation_3eyjl"]
resource_name = "land_center"
length = 1.5
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Camera:rotation")
tracks/0/interp = 2
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.5, 1.5),
"transitions": PackedFloat32Array(1, 1, 1),
"update": 0,
"values": [Vector3(0, 0, 0), Vector3(-0.0349066, 0, 0), Vector3(0, 0, 0)]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Camera:position")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0, 0.5, 1.5),
"transitions": PackedFloat32Array(1, 1, 1),
"update": 0,
"values": [Vector3(0, 0, 0), Vector3(0, -0.1, 0), Vector3(0, 0, 0)]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_qeg5r"] [sub_resource type="AnimationLibrary" id="AnimationLibrary_qeg5r"]
_data = { _data = {
"RESET": SubResource("Animation_fvvjq"), "RESET": SubResource("Animation_fvvjq"),
"jump": SubResource("Animation_s07ye"), "jump": SubResource("Animation_s07ye"),
"land_center": SubResource("Animation_3eyjl"),
"land_left": SubResource("Animation_l1rph"), "land_left": SubResource("Animation_l1rph"),
"land_right": SubResource("Animation_vsknp") "land_right": SubResource("Animation_vsknp")
} }
@ -352,6 +381,7 @@ MarginContainer/constants/margin_top = 10
[sub_resource type="SphereShape3D" id="SphereShape3D_k4wwl"] [sub_resource type="SphereShape3D" id="SphereShape3D_k4wwl"]
[node name="Character" type="CharacterBody3D" node_paths=PackedStringArray("HEAD", "CAMERA", "HEADBOB_ANIMATION", "JUMP_ANIMATION", "CROUCH_ANIMATION", "COLLISION_MESH")] [node name="Character" type="CharacterBody3D" node_paths=PackedStringArray("HEAD", "CAMERA", "HEADBOB_ANIMATION", "JUMP_ANIMATION", "CROUCH_ANIMATION", "COLLISION_MESH")]
transform = Transform3D(-4.37114e-08, 0, -1, 0, 1, 0, 1, 0, -4.37114e-08, 0, 0, 0)
script = ExtResource("1_0t4e8") script = ExtResource("1_0t4e8")
default_reticle = "res://addons/fpc/reticles/reticle_1.tscn" default_reticle = "res://addons/fpc/reticles/reticle_1.tscn"
HEAD = NodePath("Head") HEAD = NodePath("Head")