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).