• Godot Help
  • Parent Class Leaking on Application Close, Godot 3.4 stable

Hello all, I'm new here.

Recently I've been trying Godot and have run into a problem. A quick synopsis:

I've been testing out different movement types for a top-down game and have recently run into an issue with the second one I tried. The Parent States of the States found in my character's Scene Tree aren't being removed from memory upon closing the (DEBUG) application.

I've done a lot of reading so far and believe I understand the difference between the Reference (when all references to an instance of it are removed, it removes the Reference from memory) and Object (when it is removed from the Scene Tree, it removes the Object from memory) classes, but I'm unsure of how to solve the issue. My States are inheriting from Node, so the parent classes that aren't in the Scene Tree (and one Node that is in the Scene Tree but isn't a State) aren't being freed from memory.

The Code for the leaking scripts is as follows:
playerV2.gd (this one is the Root Node of the scene tree)

class_name PlayerV2
extends KinematicBody2D

var speed = 20
var velocity = Vector2.ZERO
var facing = Vector2.ZERO.angle()

onready var animations = $animations
onready var facing_sprites = $facing
onready var states = $state_manager


func _ready() -> void:
	# Initialize the state machine, passing a reference of the player to the states,
	# that way they can move and react accordingly
	states.init(self)


func _unhandled_input(event: InputEvent) -> void:
	states.input(event)


func _physics_process(delta: float) -> void:
	states.physics_process(delta)
	sprite_control()


func _process(delta: float) -> void:
	states.process(delta)


func sprite_control():
	# Set Player State Sprite
	var state_sprite: String = states.current_state.name
	animations.play(state_sprite)
	
	# Set Player Direction Sprite
	if velocity:
		facing = velocity.angle()
	var direction_name: String
	var frame_number = facing_sprites.frame
	
	if facing == Vector2(1, 0).angle():
		direction_name = "right"
	if facing == Vector2(1, 1).angle():
		direction_name = "down_right"
	if facing == Vector2(0, 1).angle():
		direction_name = "down"
	if facing == Vector2(-1, 1).angle():
		direction_name = "down_left"
	if facing == Vector2(-1, 0).angle():
		direction_name = "left"
	if facing == Vector2(-1, -1).angle():
		direction_name = "up_left"
	if facing == Vector2(0, -1).angle():
		direction_name = "up"
	if facing == Vector2(1, -1).angle():
		direction_name = "up_right"
	
	facing_sprites.play(direction_name)
	facing_sprites.frame = frame_number

base_state.gd (all states inherit from this, which in turn inherits from Node)

class_name BaseState
extends Node

# Pass in a reference to the player's kinematic body so that it can be used by the state
var player: PlayerV2


func enter() -> void:
	pass


func exit() -> void:
	pass


func input(event: InputEvent) -> BaseState:
	return null


func process(delta: float) -> BaseState:
	return null


func physics_process(delta: float) -> BaseState:
	return null

move.gd (idle, walk, and dodge inherit from this)

extends BaseState
class_name MoveState

# Variables set in the Inspector (under Script Variables).
export (NodePath) var idle_node
export (NodePath) var walk_node
export (NodePath) var jog_node
export (NodePath) var sprint_node
export (NodePath) var dodge_node

onready var idle_state: MoveState = get_node(idle_node)
onready var walk_state: MoveState = get_node(walk_node)
onready var jog_state: MoveState = get_node(jog_node)
onready var sprint_state: MoveState = get_node(sprint_node)
onready var dodge_state: MoveState = get_node(dodge_node)

# Variables set on State Entry (using enter() method in child States).
var max_speed: float		# Maximum Speed possible within the State.
var max_speed_reached: bool	# Keeps track of whether the max speed has been reached.
var acceleration: float		# Speed at which the object will accelerate.
var deceleration: float		# Speed at which the object will decelerate.
var break_speed: Vector2	# The Speed the object needs to fall to change to the Break State.
var break_state: MoveState	# The State the object returns to when it loses enough speed.


func enter() -> void:
	max_speed_reached = false
	break_speed = Vector2(max_speed / 2, 0)


func input(_event: InputEvent) -> BaseState:
	if Input.is_action_just_pressed("move_dodge"):
		return dodge_state
	
	return null


func physics_process(_delta: float) -> BaseState:
	move(get_movement_input(), max_speed)
	
	return check_for_break_speed()


func get_movement_input() -> Vector2:
	var movement_direction = Vector2(
		Input.get_action_strength("move_right") - Input.get_action_strength("move_left"),
		Input.get_action_strength("move_down") - Input.get_action_strength("move_up")
	)
	return movement_direction.normalized()


func move(direction: Vector2, speed: float):
	var max_velocity_length: float = get_max_velocity_length_squared(direction, speed)
	
	if direction && player.velocity.length_squared() <= max_velocity_length:
		player.velocity = speed_control(direction, speed, acceleration)
	else:
		player.velocity = speed_control(direction, speed, deceleration)
	
	check_for_max_speed(direction, speed)
	
	player.velocity = player.move_and_slide(player.velocity)


func speed_control(direction: Vector2, speed: float, momentum: float) -> Vector2:
	return player.velocity.move_toward(direction * speed, momentum)


func check_for_max_speed(direction: Vector2, speed: float):
	var max_velocity_length: float = get_max_velocity_length_squared(direction, speed)
	if max_speed_reached == false && player.velocity.length_squared() >= max_velocity_length:
		print("Max Speed Reached.")
		max_speed_reached = true


func check_for_break_speed() -> MoveState:
	if player.velocity.length_squared() <= break_speed.length_squared() && max_speed_reached:
#		print("Break Speed: " + player.velocity)
		
		return break_state
	
	return null

func get_max_velocity_length_squared(direction: Vector2, speed: float) -> float:
	var max_velocity = direction * speed
	return max_velocity.length_squared()

run_state.gd (jog and sprint inherit from this)

extends MoveState
class_name RunState

var toggle_state: RunState		# State to toggle to when switching between run states.
var allow_toggle: bool			# Switch that determines whether the state can toggle.
var previous_direction: Vector2	# Keep track of the last made input direction.
var timer: float = 0.0			# Timer used to reset the allow_toggle variable to false.
var reset_time: float = 1.0		# Target time the timer variable needs to reach to trigger the reset.

func enter() -> void:
	.enter()
	allow_toggle = false

func input(event: InputEvent) -> BaseState:
	# First run parent code and make sure we don't need to exit early
	# based on its logic
	var new_state = .input(event)
	if new_state:
		return new_state
	
	if Input.is_action_just_released("move_run"):
		return walk_state
	
	return null


func process(delta: float):
	if allow_toggle:
		timer += delta
	elif timer > 0.0:
		timer = 0.0
	
	if timer >= reset_time:
		allow_toggle = false


func physics_process(delta: float) -> BaseState:
	var new_state = .physics_process(delta)
	if new_state:
		return new_state
	
	if get_movement_input() && allow_toggle == false:
		previous_direction = get_movement_input()
	
	if !get_movement_input() && allow_toggle == false:
		allow_toggle = true
	
	if allow_toggle && get_movement_input() == previous_direction:
		return toggle_state
	
	return null


func _exit_tree():
	self.queue_free()


func check_for_break_speed():
	if !Input.is_action_pressed("move_run") or !get_movement_input():
		return .check_for_break_speed()

Console Output:

Running: PathToGodot --path PathToProject --remote-debug 127.0.0.1:6007 --allow_focus_steal_pid 55720 --position 320,180
Godot Engine v3.4.stable.official.206ba70f4 - https://godotengine.org
Using GLES3 video driver
OpenGL ES 3.0 Renderer: NVIDIA GeForce GTX 1650/PCIe/SSE2
OpenGL ES Batching: ON
        OPTIONS
        max_join_item_commands 16
        colored_vertex_format_threshold 0.25
        batch_buffer_size 16384
        light_scissor_area_threshold 1
        item_reordering_lookahead 4
        light_max_join_items 32
        single_rect_fallback False
        debug_flash False
        diagnose_frame False
WASAPI: wFormatTag = 65534
WASAPI: nChannels = 2
WASAPI: nSamplesPerSec = 48000
WASAPI: nAvgBytesPerSec = 384000
WASAPI: nBlockAlign = 8
WASAPI: wBitsPerSample = 32
WASAPI: cbSize = 22
WASAPI: detected 2 channels
WASAPI: audio buffer frames: 1962 calculated latency: 44ms

CORE API HASH: 15843999809385478705
EDITOR API HASH: 5561212406590411750
Loading resource: res://default_env.tres
Loaded builtin certs
Loading resource: res://player/player_v2.tscn
Loading resource: res://player/player_sprites/player_states_spritesheet.bmp
Loading resource: res://player/playerV2.gd
Loading resource: res://state_machine_v2/state_manager.gd
Loading resource: res://state_machine_v2/base_state.gd
Loading resource: res://state_machine_v2/idle.gd
Loading resource: res://state_machine_v2/move.gd
Loading resource: res://state_machine_v2/walk.gd
Loading resource: res://state_machine_v2/jog.gd
Loading resource: res://state_machine_v2/run_state.gd
Loading resource: res://state_machine_v2/sprint.gd
Loading resource: res://state_machine_v2/dodge.gd
Loading resource: res://player/player_sprites/player_direction_spritesheet.bmp
State Changed: idle	NOTE: These lines are printed to the console from the Project
Max Speed Reached.	NOTE: These lines are printed to the console from the Project
ERROR: Condition "_first != nullptr" is true.
   at: ~List (./core/self_list.h:108)
ERROR: Condition "_first != nullptr" is true.
   at: ~List (./core/self_list.h:108)
WARNING: ObjectDB instances leaked at exit (run with --verbose for details).
     at: cleanup (core/object.cpp:2064)
Leaked instance: GDScript:1240 - Resource path: res://player/playerV2.gd
Leaked instance: GDScript:1244 - Resource path: res://state_machine_v2/move.gd
Leaked instance: GDScriptNativeClass:612
Leaked instance: GDScriptNativeClass:973
Leaked instance: GDScript:1247 - Resource path: res://state_machine_v2/run_state.gd
Leaked instance: GDScript:1242 - Resource path: res://state_machine_v2/base_state.gd
Hint: Leaked instances typically happen when nodes are removed from the scene tree (with `remove_child()`) but not freed (with `free()` or `queue_free()`).
ERROR: Resources still in use at exit (run with --verbose for details).
   at: clear (core/resource.cpp:417)
Resource still in use: res://state_machine_v2/run_state.gd (GDScript)
Resource still in use: res://player/playerV2.gd (GDScript)
Resource still in use: res://state_machine_v2/move.gd (GDScript)
Resource still in use: res://state_machine_v2/base_state.gd (GDScript)
Orphan StringName: res://state_machine_v2/run_state.gd
Orphan StringName: res://state_machine_v2/run_state.gd::10::RunState.enter
Orphan StringName: res://state_machine_v2/move.gd::31::MoveState.input
Orphan StringName: res://state_machine_v2/move.gd::44::MoveState.get_movement_input
Orphan StringName: direction_name
Orphan StringName: is_action_just_pressed
Orphan StringName: res://state_machine_v2/move.gd::84::MoveState.get_max_velocity_len

I'm not sure what "GDScriptNativeClass:612" and "GDScriptNativeClass:973" refers to, but I don't recall them showing up before the other leaks occurred.

EDIT: I noticed the part at the end with the Orphan StringNames changes every time I close the DEBUG window.

Additionally, this is what my Scene Tree looks like:

I based the code itself off of this video by the youtuber The Shaggy Dev, in case that's relevant.

I've done a lot of looking around, but almost everything I've found has pretty much amounted to a description of the Reference and Object classes. I've also found videos describing memory leak in Godot. As I've mentioned, that hasn't helped me solve the issue. Maybe my lack of experience is preventing me from seeing some solution to this problem.

I've tried adding an _exit_tree() method to explicitly call self.queue_free() in base_state.gd and playerV2.gd, but this hasn't solved anything (I figure it's doing nothing for base_state.gd because only its children are in the Scene Tree, but I'm not sure what's going on with playerV2.gd).

  • Okay, I've fixed it.

    It turns out it was a Circular Referencing Issue. After going through the scripts (base_state.gd, move.gd, run_state.gd, state_manager.gd, and the State scripts themselves) that referenced the Nodes and replacing any references to the Nodes with Strings instead it now works without any memory leak.

    The Scripts now look like this, in case anyone else might need this kind of info:
    base_state.gd
    I had to change the return type for input(), process(), and physics_process() to String. Originally it was BaseState.

    class_name BaseState
    extends Node
    
    # Pass in a reference to the player's kinematic body so that it can be used by the state
    var player: PlayerV2
    
    
    func enter() -> void:
    	pass
    
    
    func exit() -> void:
    	pass
    
    
    func input(_event: InputEvent) -> String:
    	return ""
    
    
    func process(_delta: float) -> String:
    	return ""
    
    
    func physics_process(_delta: float) -> String:
    	return ""

    move.gd
    I removed the variables that referenced the various states (every instance of the variables in the code was replaced with an equivalent String), and I also changed the return types of the methods that returned States to Strings instead.

    extends BaseState
    class_name MoveState
    
    # Variables set on State Entry (using enter() method in child States).
    var max_speed: float			# Maximum Speed possible within the State.
    var max_speed_reached: bool	# Keeps track of whether the max speed has been reached.
    var acceleration: float			# Speed at which the object will accelerate.
    var deceleration: float			# Speed at which the object will decelerate.
    var break_speed: Vector2		# The Speed the object needs to fall to change to the Break State.
    var break_state: String			# The State the object returns to when it loses enough speed.
    
    
    func enter() -> void:
    	max_speed_reached = false
    	break_speed = Vector2(max_speed / 2, 0)
    
    
    func input(_event: InputEvent) -> String:
    	if Input.is_action_just_pressed("move_dodge"):
    		return "dodge"
    	
    	return ""
    
    
    func physics_process(_delta: float) -> String:
    	move(get_movement_input(), max_speed)
    	
    	return check_for_break_speed()
    
    
    func get_movement_input() -> Vector2:
    	var movement_direction = Vector2(
    		Input.get_action_strength("move_right") - Input.get_action_strength("move_left"),
    		Input.get_action_strength("move_down") - Input.get_action_strength("move_up")
    	)
    	return movement_direction.normalized()
    
    
    func move(direction: Vector2, speed: float):
    	var max_velocity_length: float = get_max_velocity_length_squared(direction, speed)
    	
    	if direction && player.velocity.length_squared() <= max_velocity_length:
    		player.velocity = speed_control(direction, speed, acceleration)
    	else:
    		player.velocity = speed_control(direction, speed, deceleration)
    	
    	check_for_max_speed(direction, speed)
    	
    	player.velocity = player.move_and_slide(player.velocity)
    
    
    func speed_control(direction: Vector2, speed: float, momentum: float) -> Vector2:
    	return player.velocity.move_toward(direction * speed, momentum)
    
    
    func check_for_max_speed(direction: Vector2, speed: float):
    	var max_velocity_length: float = get_max_velocity_length_squared(direction, speed)
    	if max_speed_reached == false && player.velocity.length_squared() >= max_velocity_length:
    		print("Max Speed Reached.")
    		max_speed_reached = true
    
    
    func check_for_break_speed() -> String:
    	if player.velocity.length_squared() <= break_speed.length_squared() && max_speed_reached:
    #		print("Break Speed: " + player.velocity)
    		
    		return break_state
    	
    	return ""
    
    func get_max_velocity_length_squared(direction: Vector2, speed: float) -> float:
    	var max_velocity = direction * speed
    	return max_velocity.length_squared()

    run_state.gd
    I did the same here that I did in move.gd and base_state.gd.

    extends MoveState
    class_name RunState
    
    var toggle_state: String			# State to toggle to when switching between run states.
    var allow_toggle: bool			# Switch that determines whether the state can toggle.
    var previous_direction: Vector2	# Keep track of the last made input direction.
    var timer: float = 0.0			# Timer used to reset the allow_toggle variable to false.
    var reset_time: float = 1.0		# Target time the timer variable needs to reach to trigger the reset.
    
    func enter() -> void:
    	.enter()
    	allow_toggle = false
    
    func input(event: InputEvent) -> String:
    	# First run parent code and make sure we don't need to exit early
    	# based on its logic
    	var new_state = .input(event)
    	if bool(new_state):
    		return new_state
    	
    	if Input.is_action_just_released("move_run"):
    		return "walk"
    	
    	return ""
    
    
    func process(delta: float):
    	if allow_toggle:
    		timer += delta
    	elif timer > 0.0:
    		timer = 0.0
    	
    	if timer >= reset_time:
    		allow_toggle = false
    
    
    func physics_process(delta: float) -> String:
    	var new_state = .physics_process(delta)
    	if new_state:
    		return new_state
    	
    	if get_movement_input() && allow_toggle == false:
    		previous_direction = get_movement_input()
    	
    	if !get_movement_input() && allow_toggle == false:
    		allow_toggle = true
    	
    	if allow_toggle && get_movement_input() == previous_direction:
    		return toggle_state
    	
    	return ""
    
    
    func check_for_break_speed():
    	if !Input.is_action_pressed("move_run") or !get_movement_input():
    		return .check_for_break_speed()

    state_manager.gd
    With the exception of the current_state variable I did essentially the same thing here as I've done in the preceding scripts. I had to pass the String values to get_node() in order for this to work.

    extends Node
    
    export var starting_state: String
    
    var current_state: BaseState
    
    func change_state(new_state: String) -> void:
    	if current_state:
    		current_state.exit()
    	
    	current_state = get_node(new_state)
    	current_state.enter()
    	print("State Changed: " + new_state)
    
    
    # Initialize the state machine by giving each state a reference to the objects
    # owned by the parent that they should be able to take control of
    # and set a default state
    # argument for init() is [player: PlayerV2]
    func init(player: PlayerV2) -> void:
    	for child in get_children():
    		child.player = player
    	
    	# Initialize with a default state of idle
    	change_state(starting_state)
    
    
    # Pass through functions for the Player to call,
    # handling state changes as needed
    func physics_process(delta: float) -> void:
    	var new_state = current_state.physics_process(delta)
    	if bool(new_state):
    		change_state(new_state)
    
    
    func input(event: InputEvent) -> void:
    	var new_state = current_state.input(event)
    	if bool(new_state):
    		change_state(new_state)
    
    
    func process(delta: float) -> void:
    	var new_state = current_state.process(delta)
    	if new_state:
    		change_state(new_state)

    Additionally, I ran into problems with bool(string_variable) causing crashes in some places and not in others. I don't know why this is, but if anyone out there is planning on doing something similar to me and finds that the remaining bool(string_variable) methods in state_manager.gd are giving them problems it seems it's okay to remove them.

    Also, for anyone who doesn't know, "" (an empty String), like a null value, returns false in a boolean expression (such as the if() statements). That's why "" is used to replace null return values throughout the scripts.

    I hope this helps someone out there.

I've found that objects deleting themselves is not reliable. It's best to delete them from a main script. You can also try call_deferred, sometimes this helps as well.

func _exit_tree():
	call_deferred("queue_free")

    cybereality

    That doesn't seem to be working (I tried adding it to each of the scripts displayed in the original post). I'm guessing it's not working for the same reasons the below doesn't work.

    func _exit_tree():
    	self.queue_free()

    Unfortunately, I'm not sure what exactly those reasons are.

    ========== EDIT: I tried adding the following to each script: ==========

    func _exit_tree():
    	print("Exiting Tree: " + self.to_string())
    	call_deferred("queue_free")

    I also tested it with self.queue_free().

    This is the added output for both:

    Exiting Tree: dodge:[Node:1297]
    Exiting Tree: dodge:[Node:1297]
    Exiting Tree: sprint:[Node:1296]
    Exiting Tree: sprint:[Node:1296]
    Exiting Tree: sprint:[Node:1296]
    Exiting Tree: jog:[Node:1295]
    Exiting Tree: jog:[Node:1295]
    Exiting Tree: jog:[Node:1295]
    Exiting Tree: walk:[Node:1294]
    Exiting Tree: walk:[Node:1294]
    Exiting Tree: idle:[Node:1293]
    Exiting Tree: idle:[Node:1293]
    Exiting Tree: playerV2:[KinematicBody2D:1288]

    No clue why there were multiple references to the same Nodes.

    You can probably solve this just by making BaseState extend Reference.

      duane

      Doing this doesn't work. There are a lot of functions that are required from the Node Class that aren't available within the Reference Class.

      Currently I'm thinking about making a separate project and adding the scripts and objects to the Scene Tree piece by piece to see when this problem crops up. I'm currently thinking that it might have something to do with the variables declared in BaseState and the other non-Node classes, but I'm not sure.

      Someone on reddit suggested it might be a cyclical reference issue in the move.gd and run_state.gd files, so I'm going to try and test for that... Somehow.

      They also suggested that I go to the Github Issues page and post about it, so I'm going to attempt that later as well.

      Yes, I was considering cyclic reference.

      Here's a link to the reddit post I made in case anyone wants to take a look.

      Next time I post here I hope to have a solution to share.

      Okay, I've fixed it.

      It turns out it was a Circular Referencing Issue. After going through the scripts (base_state.gd, move.gd, run_state.gd, state_manager.gd, and the State scripts themselves) that referenced the Nodes and replacing any references to the Nodes with Strings instead it now works without any memory leak.

      The Scripts now look like this, in case anyone else might need this kind of info:
      base_state.gd
      I had to change the return type for input(), process(), and physics_process() to String. Originally it was BaseState.

      class_name BaseState
      extends Node
      
      # Pass in a reference to the player's kinematic body so that it can be used by the state
      var player: PlayerV2
      
      
      func enter() -> void:
      	pass
      
      
      func exit() -> void:
      	pass
      
      
      func input(_event: InputEvent) -> String:
      	return ""
      
      
      func process(_delta: float) -> String:
      	return ""
      
      
      func physics_process(_delta: float) -> String:
      	return ""

      move.gd
      I removed the variables that referenced the various states (every instance of the variables in the code was replaced with an equivalent String), and I also changed the return types of the methods that returned States to Strings instead.

      extends BaseState
      class_name MoveState
      
      # Variables set on State Entry (using enter() method in child States).
      var max_speed: float			# Maximum Speed possible within the State.
      var max_speed_reached: bool	# Keeps track of whether the max speed has been reached.
      var acceleration: float			# Speed at which the object will accelerate.
      var deceleration: float			# Speed at which the object will decelerate.
      var break_speed: Vector2		# The Speed the object needs to fall to change to the Break State.
      var break_state: String			# The State the object returns to when it loses enough speed.
      
      
      func enter() -> void:
      	max_speed_reached = false
      	break_speed = Vector2(max_speed / 2, 0)
      
      
      func input(_event: InputEvent) -> String:
      	if Input.is_action_just_pressed("move_dodge"):
      		return "dodge"
      	
      	return ""
      
      
      func physics_process(_delta: float) -> String:
      	move(get_movement_input(), max_speed)
      	
      	return check_for_break_speed()
      
      
      func get_movement_input() -> Vector2:
      	var movement_direction = Vector2(
      		Input.get_action_strength("move_right") - Input.get_action_strength("move_left"),
      		Input.get_action_strength("move_down") - Input.get_action_strength("move_up")
      	)
      	return movement_direction.normalized()
      
      
      func move(direction: Vector2, speed: float):
      	var max_velocity_length: float = get_max_velocity_length_squared(direction, speed)
      	
      	if direction && player.velocity.length_squared() <= max_velocity_length:
      		player.velocity = speed_control(direction, speed, acceleration)
      	else:
      		player.velocity = speed_control(direction, speed, deceleration)
      	
      	check_for_max_speed(direction, speed)
      	
      	player.velocity = player.move_and_slide(player.velocity)
      
      
      func speed_control(direction: Vector2, speed: float, momentum: float) -> Vector2:
      	return player.velocity.move_toward(direction * speed, momentum)
      
      
      func check_for_max_speed(direction: Vector2, speed: float):
      	var max_velocity_length: float = get_max_velocity_length_squared(direction, speed)
      	if max_speed_reached == false && player.velocity.length_squared() >= max_velocity_length:
      		print("Max Speed Reached.")
      		max_speed_reached = true
      
      
      func check_for_break_speed() -> String:
      	if player.velocity.length_squared() <= break_speed.length_squared() && max_speed_reached:
      #		print("Break Speed: " + player.velocity)
      		
      		return break_state
      	
      	return ""
      
      func get_max_velocity_length_squared(direction: Vector2, speed: float) -> float:
      	var max_velocity = direction * speed
      	return max_velocity.length_squared()

      run_state.gd
      I did the same here that I did in move.gd and base_state.gd.

      extends MoveState
      class_name RunState
      
      var toggle_state: String			# State to toggle to when switching between run states.
      var allow_toggle: bool			# Switch that determines whether the state can toggle.
      var previous_direction: Vector2	# Keep track of the last made input direction.
      var timer: float = 0.0			# Timer used to reset the allow_toggle variable to false.
      var reset_time: float = 1.0		# Target time the timer variable needs to reach to trigger the reset.
      
      func enter() -> void:
      	.enter()
      	allow_toggle = false
      
      func input(event: InputEvent) -> String:
      	# First run parent code and make sure we don't need to exit early
      	# based on its logic
      	var new_state = .input(event)
      	if bool(new_state):
      		return new_state
      	
      	if Input.is_action_just_released("move_run"):
      		return "walk"
      	
      	return ""
      
      
      func process(delta: float):
      	if allow_toggle:
      		timer += delta
      	elif timer > 0.0:
      		timer = 0.0
      	
      	if timer >= reset_time:
      		allow_toggle = false
      
      
      func physics_process(delta: float) -> String:
      	var new_state = .physics_process(delta)
      	if new_state:
      		return new_state
      	
      	if get_movement_input() && allow_toggle == false:
      		previous_direction = get_movement_input()
      	
      	if !get_movement_input() && allow_toggle == false:
      		allow_toggle = true
      	
      	if allow_toggle && get_movement_input() == previous_direction:
      		return toggle_state
      	
      	return ""
      
      
      func check_for_break_speed():
      	if !Input.is_action_pressed("move_run") or !get_movement_input():
      		return .check_for_break_speed()

      state_manager.gd
      With the exception of the current_state variable I did essentially the same thing here as I've done in the preceding scripts. I had to pass the String values to get_node() in order for this to work.

      extends Node
      
      export var starting_state: String
      
      var current_state: BaseState
      
      func change_state(new_state: String) -> void:
      	if current_state:
      		current_state.exit()
      	
      	current_state = get_node(new_state)
      	current_state.enter()
      	print("State Changed: " + new_state)
      
      
      # Initialize the state machine by giving each state a reference to the objects
      # owned by the parent that they should be able to take control of
      # and set a default state
      # argument for init() is [player: PlayerV2]
      func init(player: PlayerV2) -> void:
      	for child in get_children():
      		child.player = player
      	
      	# Initialize with a default state of idle
      	change_state(starting_state)
      
      
      # Pass through functions for the Player to call,
      # handling state changes as needed
      func physics_process(delta: float) -> void:
      	var new_state = current_state.physics_process(delta)
      	if bool(new_state):
      		change_state(new_state)
      
      
      func input(event: InputEvent) -> void:
      	var new_state = current_state.input(event)
      	if bool(new_state):
      		change_state(new_state)
      
      
      func process(delta: float) -> void:
      	var new_state = current_state.process(delta)
      	if new_state:
      		change_state(new_state)

      Additionally, I ran into problems with bool(string_variable) causing crashes in some places and not in others. I don't know why this is, but if anyone out there is planning on doing something similar to me and finds that the remaining bool(string_variable) methods in state_manager.gd are giving them problems it seems it's okay to remove them.

      Also, for anyone who doesn't know, "" (an empty String), like a null value, returns false in a boolean expression (such as the if() statements). That's why "" is used to replace null return values throughout the scripts.

      I hope this helps someone out there.