CharacterBody2d's child node sometimes get set to null

Godot Version

4.2.1

Question

Github project here: GitHub - joebog1/GodotTest

I’ve recently been trying to implement basic sprite movement where the correct animation plays based on input direction. Currently it correctly walks in whatever direction I move the player in (despite some errors I made while adding frames from a sprite sheet, that’s not the issue here)

I’ve noticed that when I stop moving my code should set the animation to the respective Idle animation based off of which direction the player was moving. However, if it’s not the UP direction the code crashes with the error “Attempt to call function ‘play’ in base ‘null instance’ on a null instance.”

This is weird because I have an assert just before calling that where I assert that the $AnimatedSprite2D is a valid instance but I still see this error.

I’ve ensured that the IdleDown, IdleLeft and IdleRight animations do exist on the AnimatedSprite2D node but the error appears to be with AnimatedSprite2D suddenly becoming null.

The relevant code is here:

extends CharacterBody2D

const BASE_SPEED = 300.0
# Enum represents the direction the Runner is facing.
enum {UP, DOWN, LEFT, RIGHT}

# To be expanded later.
enum {IDLE, WALKING}

# Can be changed
var current_state = movement_state.new(UP, IDLE)

class movement_state:
	# :NOTE: Access modifiers for classes in GDScript are public by default.
	# That is fine for this use case as I want a struct essentially.
	func _init(Direction, State): 
		direction = Direction
		state = State
		
	# :NOTE: I would like to statically type these to be their respective enums.
	# However currently enums aren't considered types in GDScript. 
	# https://github.com/godotengine/godot/issues/20368
	var direction
	var state
	
	# This function updates the direction and state given a Vector2 of a 
	# newDirection
	func update_state(new_direction: Vector2):
		if(new_direction != Vector2.ZERO):
			state = WALKING
			if(new_direction.x > 0):
				direction = RIGHT
			elif(new_direction.x < 0):
				direction = LEFT
			elif(new_direction.y > 0):
				direction = DOWN
			elif(new_direction.y < 0):
				direction = UP
		else:
			# We keep the previous direction so that the idle animation faces 
			# the correct direction.
			state = IDLE

func _physics_process(_delta):
	
	velocity = Input.get_vector("left","right", "up", "down") * BASE_SPEED
	
	# :NOTE: An argument could be made here that Input.get_vector should be used
	# instead depending on velocity regarding animations we want in the future.
	# Currently I'm thinking we stay with velocity as if we allow signals to 
	# change the player's velocity (such as when they take a hit) then it'd be 
	# Best if we use velocity to determine which animation to take based on
	# the player's final movement direction.
	current_state.update_state(velocity)
	if(current_state.state == WALKING):	
		if(current_state.direction == RIGHT):
			$AnimatedSprite2D.play("WalkRight")
		elif(current_state.direction == LEFT):
			$AnimatedSprite2D.play("WalkLeft")
		elif(current_state.direction == DOWN):
			$AnimatedSprite2D.play("WalkDown")
		elif(current_state.direction == UP):
			$AnimatedSprite2D.play("WalkUp")
	else: # The player is idling, choose the correct idle animation to match.
		assert(current_state.state == IDLE)
		# Prove that $AnimatedSprite2D is not a null instance
		assert(is_instance_valid($AnimatedSprite2D))
		if(current_state.direction == UP):
			$AnimatedSprite2D.play("IdleUp")
		elif(current_state.direction == DOWN):
			$AnimaDtedSprite2D.play("IdleDown")
		elif(current_state.direction == LEFT):
			$AnimaDtedSprite2D.play("IdleLeft")
		elif(current_state.direction == RIGHT):
			$AnimaDtedSprite2D.play("IdleRight")
	
	# :NOTE:
	# delta is automatically incorporated in move_and_slide.
	# see https://github.com/godotengine/godot-proposals/issues/1192
	# Rather inconsistent with move_and_collide which requires delta 
	# (and an input argument that isn't needed as move_and_slide automatically
	# grabs velocity).
	move_and_slide()

I just swapped out the if elif statements with a switch and it’s all fine now. Does anybody know why exactly???

func _physics_process(_delta):
	
	velocity = Input.get_vector("left","right", "up", "down") * BASE_SPEED
	
	# :NOTE: An argument could be made here that Input.get_vector should be used
	# instead depending on velocity regarding animations we want in the future.
	# Currently I'm thinking we stay with velocity as if we allow signals to 
	# change the player's velocity (such as when they take a hit) then it'd be 
	# Best if we use velocity to determine which animation to take based on
	# the player's final movement direction.
	current_state.update_state(velocity)
	if(current_state.state == WALKING):	
		match(current_state.direction):
			RIGHT:
				$AnimatedSprite2D.play("WalkRight")
			LEFT:
				$AnimatedSprite2D.play("WalkLeft")
			DOWN:
				$AnimatedSprite2D.play("WalkDown")
			UP:
				$AnimatedSprite2D.play("WalkUp")
	else: # The player is idling, choose the correct idle animation to match.
		assert(current_state.state == IDLE)
		# Prove that $AnimatedSprite2D is not a null instance
		assert(is_instance_valid($AnimatedSprite2D))
		match(current_state.direction):
			RIGHT:
				$AnimatedSprite2D.play("IdleRight")
			LEFT:
				$AnimatedSprite2D.play("IdleLeft")
			DOWN:
				$AnimatedSprite2D.play("IdleDown")
			UP:
				$AnimatedSprite2D.play("IdleUp")
	
	# :NOTE:
	# delta is automatically incorporated in move_and_slide.
	# see https://github.com/godotengine/godot-proposals/issues/1192
	# Rather inconsistent with move_and_collide which requires delta 
	# (and an input argument that isn't needed as move_and_slide automatically
	# grabs velocity).
	move_and_slide()

You had a typo in the first example that was pointing to a node that didn’t exist:

...
elif(current_state.direction == DOWN):
  $AnimaDtedSprite2D.play("IdleDown")
...

$AnimaDtedSprite2D would have returned null.

2 Likes

Ah, wow I would expect the linter to pick up on that :sweat_smile:. My bad

Just as an additional explanation: The linter can’t pick up on that due to the highly dynamic nature of the node tree. The linter only sees the script itself, and it must assume that the script could be attached to whatever at runtime. It can not know that your scene tree won’t have a "AnimaDtedSprite2D" named node.

For example, the script would be perfectly valid if another script created an "AnimaDtedSprite2D" node in _ready and added it as a child of your node.

That’s the reason why it is a runtime warning, where it tries to resolve the node path (which fails and gives you null) and then tries to call the play() method on that result. Now with keeping in mind how it works internally, the error message makes more sense:

Attempt to call function ‘play’ in base ‘null instance’ on a null instance.

(also the $NodeName syntax is just a shorthand for get_node("NodeName") which makes it a bit more obvious what’s happening: resolving a node path)

2 Likes

I see, I was under the impression that a script was attached to a specific scene (as that’s the only kind of thing I’ve seen so far). Glad to know that’s not the case. I’ll have to look for examples where scripts are applied to different scenes at run time as that might be useful to know later as I get more familiar with Godot.

Yeah I didn’t notice my typo and thought my assert covered the case it ran into.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.