Created
February 26, 2023 00:50
-
-
Save kingthrillgore/af386533db9bdb8fb8e08c68a2b9d92a to your computer and use it in GitHub Desktop.
Modifications to pemguin005's godot third person controller. MIT licensed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extends KinematicBody | |
# Imports | |
# Allows to pick your animation tree from the inspector | |
export (NodePath) var PlayerAnimationTree | |
export onready var animation_tree = get_node(PlayerAnimationTree) | |
onready var playback = animation_tree.get("parameters/playback"); | |
# Allows to pick your chracter's mesh from the inspector | |
export (NodePath) var PlayerCharacterMesh | |
export onready var player_mesh = get_node(PlayerCharacterMesh) | |
# Gamplay mechanics and Inspector tweakables | |
export var gravity = 9.8 | |
export var jump_force = 9 | |
export var walk_speed = 1.3 | |
export var run_speed = 5.5 | |
export var dash_power = 12 # Controls roll and big attack speed boosts | |
# Animation node names | |
var roll_node_name = "Roll" | |
var idle_node_name = "Idle" | |
var walk_node_name = "Walk" | |
var run_node_name = "Run" | |
var jump_node_name = "Jump" | |
var attack1_node_name = "Attack1" | |
var attack2_node_name = "Attack2" | |
var bigattack_node_name = "BigAttack" | |
var death_node_name = "Death" | |
# Condition States | |
var is_attacking : bool = bool() | |
var is_rolling : bool = bool() | |
var is_walking : bool = bool() | |
var is_running : bool = bool() | |
# Physics values | |
var direction = Vector3() | |
var horizontal_velocity = Vector3() | |
var aim_turn = float() | |
var movement = Vector3() | |
var vertical_velocity = Vector3() | |
var movement_speed = int() | |
var angular_acceleration = int() | |
var acceleration = int() | |
# Context Panel | |
var is_context_on : bool = false | |
# Lock jumping (for various UI actions) | |
var is_jumping_locked : bool = false | |
# Lock interact (for various UI actions) | |
var is_interact_locked : bool = false | |
# Nodes in Scene | |
onready var InteractRayCast : RayCast = $Camroot/h/v/Camera/InteractRay | |
onready var poison_timer : Timer = $CharacterStatusTimers/PoisonTimer | |
onready var player_death_sound : AudioStreamPlayer3D = $Sounds/PlayerDeath | |
# Signals | |
signal looking_at_interactive | |
signal interacting_with_interactive | |
signal not_looking_at_interactive | |
signal looking_at_character_with_dialog | |
signal interacting_with_chracter_with_dialog | |
signal not_looking_at_character_with_dialog | |
# signal player_hit | |
signal free_camera | |
signal lock_camera | |
# System Methods | |
## Fired on scene load | |
func _ready() -> void: | |
# Connecting signals | |
var hud = get_node("/root/Spatial/HUD") | |
PlayerVitals.connect("player_is_dead", self, "player_has_died") | |
hud.connect("in_dialogue", self, "lock_dialog_ui_elements") | |
hud.connect("out_of_dialogue", self, "unlock_dialog_ui_elements") | |
# Camera based Rotation | |
direction = Vector3.BACK.rotated(Vector3.UP, $Camroot/h.global_transform.basis.get_euler().y) | |
## Fired with every input event | |
func _input(event) -> void: # All major mouse and button input events | |
if event is InputEventMouseMotion: | |
aim_turn = -event.relative.x * 0.015 # animates player with mouse movement while aiming | |
if event.is_action_pressed("aim"): # Aim button triggers a strafe walk and camera mechanic | |
direction = $Camroot/h.global_transform.basis.z | |
## Called every frame. 'delta' is the elapsed time since the previous frame. | |
func _process(delta) -> void: | |
check_frustrum_look() | |
## Called at every Physics tick, which is before every frame. | |
## | |
## This contains the resounding corpus of our game logic, as recommended | |
## by Godot. | |
func _physics_process(delta) -> void: | |
rollattack() | |
bigattack() | |
attack1() | |
attack2() | |
roll() | |
var on_floor = is_on_floor() # State control for is jumping/falling/landing | |
var h_rot = $Camroot/h.global_transform.basis.get_euler().y | |
movement_speed = 0 | |
angular_acceleration = 10 | |
acceleration = 15 | |
# Gravity mechanics and prevent slope-sliding | |
if not is_on_floor(): | |
vertical_velocity += Vector3.DOWN * gravity * 2 * delta | |
else: | |
vertical_velocity = -get_floor_normal() * gravity / 3 | |
# Defining attack state: Add more attacks animations here as you add more! | |
if (attack1_node_name in playback.get_current_node()) or (attack2_node_name in playback.get_current_node()) or (bigattack_node_name in playback.get_current_node()): | |
is_attacking = true | |
else: | |
is_attacking = false | |
# Return a summary of objects in the game world that intersect on where the camera is looking | |
var space_state = get_world().direct_space_state | |
var result = space_state.intersect_ray(Vector3(0, 0, 0), Vector3(100, 100, 100)) | |
# Giving BigAttack some Slide | |
if bigattack_node_name in playback.get_current_node(): | |
acceleration = 3 | |
# Defining Roll state and limiting movment during rolls | |
if roll_node_name in playback.get_current_node(): | |
is_rolling = true | |
acceleration = 2 | |
angular_acceleration = 2 | |
else: | |
is_rolling = false | |
# Jump input and Mechanics | |
if Input.is_action_just_pressed("jump") and ((is_attacking != true) and (is_rolling != true)) and is_on_floor(): | |
if not is_jumping_locked: | |
vertical_velocity = Vector3.UP * jump_force | |
# Quit Game | |
if Input.is_action_just_pressed("exit"): | |
get_tree().quit() | |
# Pause Menu | |
if Input.is_action_just_pressed("pause"): | |
pass | |
# Self Poison (Testing) | |
if Input.is_action_just_pressed("inflict_poison"): | |
if !PlayerVitals.is_poisoned: | |
var absorb_poison = PlayerEffects.calculate_poison_providence_chance() | |
if !absorb_poison: | |
PlayerVitals.is_poisoned = true | |
var poison_time = PlayerEffects.calculate_poison_time() | |
print("Poison Time: ", str(poison_time)) | |
poison_timer.set_wait_time(poison_time) | |
PlayerEffects.start_poison() | |
poison_timer.start() | |
# Context Menu | |
if Input.is_action_just_pressed("context"): | |
if (is_context_on): | |
is_context_on = false | |
emit_signal("lock_camera") | |
get_parent().get_node("ContextRoot").hide() | |
else: | |
is_context_on = true | |
emit_signal("free_camera") | |
get_parent().get_node("ContextRoot").show() | |
# Movement input, state and mechanics. *Note: movement stops if attacking | |
if (Input.is_action_pressed("forward") || Input.is_action_pressed("backward") || Input.is_action_pressed("left") || Input.is_action_pressed("right")): | |
direction = Vector3(Input.get_action_strength("left") - Input.get_action_strength("right"), | |
0, | |
Input.get_action_strength("forward") - Input.get_action_strength("backward")) | |
direction = direction.rotated(Vector3.UP, h_rot).normalized() | |
is_walking = true | |
# Sprint input, state and speed | |
if (Input.is_action_pressed("sprint")) and (is_walking == true): | |
movement_speed = run_speed | |
is_running = true | |
else: # Walk State and speed | |
movement_speed = walk_speed | |
is_running = false | |
else: | |
is_walking = false | |
is_running = false | |
if Input.is_action_pressed("aim"): # Aim/Strafe input and mechanics | |
player_mesh.rotation.y = lerp_angle(player_mesh.rotation.y, $Camroot/h.rotation.y, delta * angular_acceleration) | |
else: # Normal turn movement mechanics | |
player_mesh.rotation.y = lerp_angle(player_mesh.rotation.y, atan2(direction.x, direction.z) - rotation.y, delta * angular_acceleration) | |
# Movment mechanics with limitations during rolls/attacks | |
if ((is_attacking == true) or (is_rolling == true)): | |
horizontal_velocity = horizontal_velocity.linear_interpolate(direction.normalized() * .01 , acceleration * delta) | |
else: # Movement mechanics without limitations | |
horizontal_velocity = horizontal_velocity.linear_interpolate(direction.normalized() * movement_speed, acceleration * delta) | |
# Interact | |
if Input.is_action_just_pressed("interact"): | |
if not is_interact_locked: | |
print("Interact thrown") | |
var object = return_frustrum_item() | |
print("Object in frustrum: ", object) | |
# if object != null: | |
# TODO Send signal to handle Item | |
# object.queue_free() | |
# Subtract HP | |
if Input.is_action_just_pressed("subtract_hp"): | |
print("Subtracting HP") | |
PlayerVitals.decrease_health(5) | |
# Subtract Psi | |
if Input.is_action_just_pressed("subtract_psi"): | |
print("Subtracing Psi") | |
PlayerVitals.decrease_psi(5) | |
# The Physics Sauce. Movement, gravity and velocity in a perfect dance. | |
movement.z = horizontal_velocity.z + vertical_velocity.z | |
movement.x = horizontal_velocity.x + vertical_velocity.x | |
movement.y = vertical_velocity.y | |
move_and_slide(movement, Vector3.UP) | |
# ========= State machine controls ========= | |
# The booleans of the on_floor, is_walking etc, trigger the | |
# advanced conditions of the AnimationTree, controlling animation paths | |
# on_floor manages jumps and falls | |
animation_tree["parameters/conditions/IsOnFloor"] = on_floor | |
animation_tree["parameters/conditions/IsInAir"] = !on_floor | |
# Moving and running respectively | |
animation_tree["parameters/conditions/IsWalking"] = is_walking | |
animation_tree["parameters/conditions/IsNotWalking"] = !is_walking | |
animation_tree["parameters/conditions/IsRunning"] = is_running | |
animation_tree["parameters/conditions/IsNotRunning"] = !is_running | |
# Attacks and roll don't use these boolean conditions, instead | |
# they use "travel" or "start" to one-shot their animations. | |
# Custom Methods | |
## Rolls the player on the floor. | |
func roll() -> void: | |
## Dodge button input with dash and interruption to basic actions | |
if Input.is_action_just_pressed("roll"): | |
if !roll_node_name in playback.get_current_node() and !jump_node_name in playback.get_current_node() and !bigattack_node_name in playback.get_current_node(): | |
playback.start(roll_node_name) #"start" not "travel" to speedy teleport to the roll! | |
horizontal_velocity = direction * dash_power | |
## Executes the Short Hand Attack | |
func attack1() -> void: | |
# If not doing other things, start attack1 | |
if (idle_node_name in playback.get_current_node() or walk_node_name in playback.get_current_node()) and is_on_floor(): | |
if Input.is_action_just_pressed("attack"): | |
if (is_attacking == false): | |
playback.travel(attack1_node_name) | |
## Executes the dual hand attack | |
func attack2() -> void: | |
# If attack1 is animating, combo into attack 2 | |
if attack1_node_name in playback.get_current_node(): # Big Attack if sprinting, adds a dash | |
if Input.is_action_just_pressed("attack"): | |
playback.travel(attack2_node_name) | |
## Executes the third type of attack | |
func attack3() -> void: | |
# If attack2 is animating, combo into attack 3. This is a template. | |
if attack1_node_name in playback.get_current_node(): | |
if Input.is_action_just_pressed("attack"): | |
pass #no current animation, but add it's playback here! | |
## Executes the roll attack, typically tied in after a roll attack | |
func rollattack() -> void: | |
# If attack pressed while rolling, do a special attack afterwards. | |
if roll_node_name in playback.get_current_node(): | |
if Input.is_action_just_pressed("attack"): | |
horizontal_velocity = direction * dash_power | |
playback.travel(bigattack_node_name) #change this animation for a different attack | |
## Executes the big attack | |
func bigattack() -> void: | |
# If attack pressed while springing, do a special attack | |
if run_node_name in playback.get_current_node(): # Big Attack if sprinting, adds a dash | |
if Input.is_action_just_pressed("attack"): | |
horizontal_velocity = direction * dash_power | |
playback.travel(bigattack_node_name) #Add and Change this animation node for a different attack | |
## Checks the Interact frustrum raycast to see if any object is colliding, and returns it. | |
## | |
## This was originally written for testing and is not actively being used, it is deprecated | |
## and should be removed in the future | |
func check_collider_look() -> void: | |
var collider = InteractRayCast.get_collider() | |
if collider != null: | |
if collider.is_in_group("Environment") != true: | |
print("Floorcheck Collider hit against ", collider) | |
# TODO Send signal for other collider types | |
## Checks the Interact frustrum raycast for any colliding objects, prints a status of what item type it is | |
## (based on the group), and emits a "looking_at_interactive" signal. Similarly, if the frustrum is | |
## not looking at anything, signal "not_looking_at_interactive" is thrown. | |
func check_frustrum_look() -> void: | |
var collider = InteractRayCast.get_collider() | |
if collider != null: | |
if collider.is_in_group("Environment") != true: | |
# print("Frustrum Collider hit against ", collider) | |
# print("Groups: ", collider.get_groups()) | |
for group in collider.get_groups(): | |
match group: | |
"Item": | |
emit_signal("looking_at_interactive") | |
# print("We have an item!") | |
# collider.item_used() | |
"Dialog": | |
emit_signal("looking_at_interactive") | |
#print("We have a dialog-ready object!") | |
else: | |
emit_signal("not_looking_at_interactive") | |
## Checks the Interact frustrum raycast for any object and returns it if is an Item or Dialog. | |
## Additionally, it will emit signals "looking_at_interactive" and "interacting_with_interactive." | |
func return_frustrum_item(): | |
var collider = InteractRayCast.get_collider() | |
if collider != null: | |
if collider.is_in_group("Environment") != true: | |
if collider.is_in_group("Item") == true: | |
emit_signal("looking_at_interactive") | |
emit_signal("interacting_with_interactive") | |
collider.item_interacted() | |
return collider | |
elif collider.is_in_group("Dialog") == true: | |
emit_signal("looking_at_interactive") | |
emit_signal("interacting_with_interactive") | |
collider.item_interacted() | |
return collider | |
# Signal Methods | |
func _on_PoisonTimer_timeout(): | |
poison_timer.stop() | |
PlayerVitals.is_poisoned = false | |
PlayerEffects.end_poison() | |
func player_has_died(): | |
playback.start(death_node_name) | |
player_death_sound.play() | |
func lock_dialog_ui_elements(): | |
lock_jumping() | |
lock_interact() | |
func unlock_dialog_ui_elements(): | |
unlock_jumping() | |
unlock_interact() | |
func lock_jumping(): | |
is_jumping_locked = true | |
func unlock_jumping(): | |
is_jumping_locked = false | |
func lock_interact(): | |
is_interact_locked = true | |
func unlock_interact(): | |
is_interact_locked = false |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment