I have an animation error affecting only one of my animations. (GODOT 3)

Godot Version

3.5

Question

Sorry for the super old godot version, just trying to plow through and finish this old project.

EDIT: OKAY I think i finally got it formatted. second comment has the enemy’s individual code.

TY anyone who can help

I am in the middle of trying to code a simple ‘mini boss’ type character, and one of his attacks works well but not the other.

It seems to be an animation error - the hit box to hurt the player is activated in the animation, and the animation itself simply won’t play.

There are some other bugs, too , but this is the one I’ve been working on debugging.

(Other bugs are that he flips his attacking ‘direction’ a lot. So he shows one direction, then flips to another direction, then flips back, for example. I haven’t tried to debug that a lot yet though since Im in the middle of fixing this one other bug.)

There’s… a lot of code here (for me) SO I would really not blame anyone who doesn’t want to read it. but i’ll post it anyway in case someone is patient and can help.

First, the ‘character parent’ code - the code all characters inherit from:



onready var immunity_timer = $ImmunityTimer
onready var interrupted_timer = $InterruptedTimer
onready var death_timer = $DeathTimer
onready var healthbar_total = $HealthBarTotal
onready var healthbar_current = $HealthBarTotal/HealthBarCurrent
onready var healthbar_new = $HealthBarNew
#thinking we're gonna need a placeholder sprite for tihs
#and then whenever we are changing the sprite in code on the player
#or enemy. we use this name, to modulate it!
onready var test_sprite = $TestSprite

# Called when the node enters the scene tree for the first time.
func _ready():
	#do we update difficulty mods here? Or manually on player and enemies?
	#maybe i DO have to do it in the parent.... (here)
	update_difficulty_lvl()

#we need to update health bar when we HEAL too!!!!
#function for taking damage should be here!
#so ... when we take damage we ALSO need to stop our current action!
func take_damage(var damage):
	currentHealth_modded -= (damage * damage_taken_modded)
	update_small_healthbar(currentHealth_modded) 
	#if we're dead, we dont need the timers!
	if currentHealth_modded <= 0:
		on_death()
	else: 
		#knockback willb e called from the player GIVING damage, not here
		#call interrupted to interrupt them
		if interrupted_timer != null:
			interrupted()
			immune()
			interrupted_timer.start()
			immunity_timer.start()
		else:
			print ("Interrupted timer was null")

#dont think i've added custom knockback functionality yet. may add later. 
#called in area entereds, on teh player/ item giving damage
func knockback(var dam_pos):
	#first we need to compare the abs value. make sure that there is big
	#enough difference to actually make it diagnonal (as opposed to just 4 directoinal)
	var x_dif = self.position.x - dam_pos.x
	var y_dif = self.position.y - dam_pos.y
	var _abs_x_dif = abs(x_dif)
	var abs_y_dif = abs(y_dif) #could do this in just two lines with abs around the expressoin
	#but i noob and wnat it readable
	if self.global_position.x < dam_pos.x: 
		#if the damage comes FROM the right. 
		if abs_y_dif <= knockback_smidge_value:
			#we are in range. knocke em to the left
			#it looks like we used move_and_slide for the player and moveable enemies
			velocity.x += -knockback_amount
		elif self.global_position.y < dam_pos.y:
			#the damage comes FROM down direction
			#knock em upleft
			velocity.x += -knockback_amount
			velocity.y += -knockback_amount
		else:
			#its not in smidge and not from down, it must be from up.
			#knock em downleft
			velocity.x += -knockback_amount
			velocity.y += knockback_amount
	elif self.global_position.x > dam_pos.x:
		#damage came from the LEFT
		if abs_y_dif <= knockback_smidge_value:
			#knock em right
			velocity.x += knockback_amount
		elif self.global_position.y < dam_pos.y:
			#damage comes FROM down directoin 
			#knock them up right
			velocity.x += knockback_amount
			velocity.y += -knockback_amount
		else:
			#dmaage should be coming from up. 
			#knock em downright
			velocity.x += knockback_amount
			velocity.y += knockback_amount
	elif self.global_postiion.y < dam_pos.y:
		#damage coming FROM down. 
		#we dont need to check for x smidge value as we started w/ x logic
		#and its elfi
		#knock em up
		velocity.y += -knockback_amount
	else:
		#knock em donw
		velocity.y += knockback_amount


#this is the func we call to make it so that whoever was hit cannot do another hting
#until their interrupted timer passes
func interrupted():
	var sprites_to_freeze = [0] #fuck it it doesn't even need to be all sprites
	#we need to freeze the node
	#NOW I am thinking lets freeze indivdiual parts...
	#if we want a knockback... 
	can_move = false
	sprites_to_freeze = check_nodes_types_and_return_sprites()
	#okay this dont work in an array :X so we must loop
	for i in sprites_to_freeze.size():
		sprites_to_freeze[i].self_modulate = Color(0, 0, 1)

#This lasts longer than interrupted
func immune():
	can_take_damage = false
	var sprites = [0]
	sprites = check_nodes_types_and_return_sprites()
	for i in sprites.size():
		sprites[i].self_modulate = Color(0, 0, 1, .5)
	# we need to make the node see through or a slightly different color
	#to clarify they cannot take damage now

func update_small_healthbar(new_value):
	healthbar_new.value = new_value

#for destroying ourself if we die?
#or at least stopping our movement
func on_death():
	set_physics_process(false) 
	set_process(false)
	var sprites = [0]
	sprites = check_nodes_types_and_return_sprites()
	for i in sprites.size():
		#this null check is very important. otherwise it bugs 
		#cuz of the first null sprite!
		if sprites[i] != null:
			var tween = get_tree().create_tween()
			tween.tween_property(sprites[i], "modulate", Color(1, 1, 1, 0), 1.0)
	death_timer.start()
	
func _on_ImmunityTimer_timeout():
	can_take_damage = true #now we can take damage again
	var sprites = [0]
	sprites = check_nodes_types_and_return_sprites()
	for i in sprites.size():
		#this null check is very important. otherwise it bugs 
		#cuz of the first null sprite!
		if sprites[i] != null:
			sprites[i].self_modulate = Color(1, 1, 1, 1)
	
#so ...w e need to get ourself and all our children
#and see which are sprite nodes
#nad then return tehm!
func check_nodes_types_and_return_sprites():
	var array_of_sprites = [test_sprite]
	#cuz of godots weird way to set arrays
	for i in self.get_children():
		#array_of_children.append(i)
		if i is Sprite:
			array_of_sprites.append(i)
	return array_of_sprites


#DONT Delete this
#okay im a dumb dumb. i acutally need to get facing dir for raycasting code to work
#as it uses the facing dir to decide which way to cast the ray
#so this won't work on enemies at times?
#but it always works on player 
#It LOOKS like when the enemy collides with the player, then they cannot get facing!
func get_facing_dir():
	#if we are moving, we can change the facing dir
	if velocity != Vector2(0,0):
		#if we are moving positive in x dir
		if velocity.x > 0.1:
			#and not moving on y dir
			if velocity.y == 0:
				facing = directions.RIGHT
			#if we are moving positive in y and x (remmeber, pos y is down)
			elif velocity.y > 0.1:
				facing = directions.DOWNRIGHT
			elif velocity.y < -0.1:
				facing = directions.UPRIGHT
		#if we are moving left
		elif velocity.x < -0.1:
			if velocity.y == 0:
				facing = directions.LEFT
			elif velocity.y < -0.1:
				facing = directions.UPLEFT
			elif velocity.y > 0.1:
				facing = directions.DOWNLEFT
		elif velocity.y < -0.1 and velocity.x == 0:
			facing = directions.UP
		elif velocity.y > 0.1 and velocity.x == 0:
			facing = directions.DOWN
		#we enter else sometimes. need to find out why!
#		else: 
#			print("could not get facing direction")

#we cannot simply use velocity
#because the entity may have zero velocity
#but still be facing in a direction
func get_facing_dir_as_vector():
	match facing:
		directions.DOWN:
			return Vector2(0,1)
		directions.UP:
			return Vector2(0,-1)
		directions.RIGHT:
			return Vector2(1,0)
		directions.LEFT:
			return Vector2(-1,0)
		directions.UPRIGHT:
			return Vector2(1,-1).normalized()
		directions.UPLEFT:
			return Vector2(-1,-1).normalized()
		directions.DOWNRIGHT:	
			return Vector2(1,1).normalized()
		directions.DOWNLEFT:	
			return Vector2(-1,1).normalized()


#we need a way to update our variables.... for difficulty mods
func update_difficulty_lvl():
	if team == "player":
		#do player stuff
		#do we have to load current health and max health differently?
		#based on IF the player or enemy already has helath off or not?
		#I THINK SO
		if currentHealth_modded == maxHealth_modded:
			maxHealth_modded = maxHealth * DifficultyManager.p_health_mod
			currentHealth_modded = maxHealth
			healthbar_new.max_value = maxHealth_modded
			healthbar_new.value = currentHealth_modded
		#we need to check for IF they have already been modified... hmmm 
		#maybe i need hte bases as variables and hten teh modified variants as varialbes
		#that seems overly complicated but lets try it
		damage_done_modded = damage_done * DifficultyManager.p_damage_mod
		damage_taken_modded = damage_taken * DifficultyManager.p_damage_per_taken
		knockback_modded = knockback_amount * DifficultyManager.p_knocked_back
		attack_speed_modded = 1.0 #multiplicative
		charge_speed_modded = 1.0
	elif team == "enemy":
		#do enemy stuff:
		if currentHealth_modded == maxHealth_modded:
			maxHealth_modded = maxHealth * DifficultyManager.e_health_mod
			currentHealth_modded = maxHealth_modded
			healthbar_new.max_value = maxHealth_modded
			healthbar_new.value = currentHealth_modded
		damage_done_modded = damage_done * DifficultyManager.e_damage_mod
		damage_taken_modded = damage_taken * DifficultyManager.e_damage_per_taken
		knockback_modded = knockback_amount * DifficultyManager.e_knocked_back
		#for some reason attack speed modded is being an ITEM/ node, NOT a number!!!!
		#so is charge speed, fyi
		attack_speed_modded = DifficultyManager.e_attack_speed
		charge_speed_modded = DifficultyManager.e_charge_speed
	else:
		#probably frienld. do nothing
		pass
	
#our save dict func
func save():
	var save_dict = {
		"filename" : get_filename(),
		"parent" : get_parent().get_path(),
		"pos_x" : position.x, #Vector 2 is not supported by JSON
		"pos_y" : position.y,
		"maxHealth" : maxHealth,
		"currentHealth" : currentHealth,
		"maxHealth_modded": maxHealth_modded,
		"currentHealth_modded": currentHealth_modded,
		"team" : team
	}
	return save_dict


func _on_InteruptedTimer_timeout():
	var sprites_to_unfreeze = [0] #fuck it it doesn't even need to be all sprites
	can_move = true
	sprites_to_unfreeze = check_nodes_types_and_return_sprites()
	set_process(true)
	set_physics_process(true)
	for i in sprites_to_unfreeze.size():
		sprites_to_unfreeze[i].self_modulate = Color(1, 1, 1, .5)


func _on_DeathTimer_timeout():
	self.queue_free()


THen, th enemy’s individualized code

I’ve already tried checking the animation player and animation tree. Animation tree WILL show basic attack play from ‘charge basic attack’ but it won’t happen in game. So the error could be there soemwhere?

Sorry for the long post.

extends "res://Combatants/CharacterParent.gd"


# Declare member variables here. Examples:
export var damage = 4
export var speed = 60
var player = null
#we need a variable to hold the amount of damage we will do... 
#gonna try using the damage modifier here. sintead of straight up damage
#so id ont change the base damage value all the time
var damage_mod = 1.0 
#gonna start with a timer for power attack
#and we will ALWAYS power attack unless it is on cooldown
#so eyah
var power_attack_cooldown = 5 #guessed a good number
var attack_cooldown = 1
var attack_radius = 35 #guessed. check if it works well at this number
var approach_radius = 225 #guessed a good number again. double check it
var sight_angle = 1.85 # radians
var can_power_attack_after_time = power_attack_cooldown
var can_attack_after_time = attack_cooldown
var elapsed_time = 0
var last_time_seen_player = 0
var chase_time = 5.0
var power_attack_speed = 1.0 #in seconds
var charge_power_speed = 0.6
var basic_attack_speed = 0.6 #I THINK
var charge_basic_speed = 0.6
var is_charging = false
var is_attacking = false #atm trying to do these so htey'll work for both charges and attacks
enum states {IDLE, APPROACH, CHARGE_BASIC, BASIC, CHARGE_POWER, POWER, DEAD}
var current_state = states.IDLE
export (NodePath) var door_guarded_path
var door_guarded
onready var hurtbox = $HurtBox2D/CollisionShape2D
onready var animation_player = $AnimationPlayer
onready var animation_tree = $AnimationTree
onready var walk_sprites = $Walk
onready var basic_sprites = $BasicAttack
onready var charge_basic_sprites = $ChargeBasicAttack
onready var power_sprites = $PowerAttack
onready var charge_power_sprites = $ChargePowerAttack
onready var animation_state = animation_tree.get("parameters/playback")

# Called when the node enters the scene tree for the first time.
func _ready():
	door_guarded = get_node(door_guarded_path)
	current_state = states.IDLE


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _physics_process(delta):
	#i hate htat this is my way of getting the player but lets just do this atm
	if player == null: #fuck it. trying to only assessing if we DONT have player
		player = get_tree().get_nodes_in_group("Player")[0]
	elapsed_time += delta
	get_facing_dir()
	if can_move:
		velocity = Vector2(0,0)
	#State machine
	match int(current_state):
		states.DEAD:
			on_death()
		states.IDLE:
			idle()
		states.APPROACH:
			if can_move:
				approach_player()
		states.CHARGE_BASIC:
			charge_basic()
		states.CHARGE_POWER:
			charge_power()
		states.BASIC:
			basic_attack()
		states.POWER:
			power_attack()
	velocity = move_and_slide(velocity)

func idle():
	if can_see_player():
		current_state = states.APPROACH
		charge_basic_sprites.visible = false
		walk_sprites.visible = true
		charge_power_sprites.visible = false
		basic_sprites.visible = false
		basic_sprites.visible = false
		power_sprites.visible = false
		animation_tree.set("parameters/Walk/blend_position", velocity)
		animation_state.travel("Walk")
	else: 
		#show idle sprite sheet (1 frame each) that i will totally add if he's not moving
		charge_basic_sprites.visible = false
		walk_sprites.visible = true
		charge_power_sprites.visible = false
		basic_sprites.visible = false
		basic_sprites.visible = false
		power_sprites.visible = false
		animation_tree.set("parameters/Walk/blend_position", velocity)
		animation_state.travel("Walk")
		velocity = position.direction_to(door_guarded.position).normalized() * speed

#copied from guard one script
func can_see_player():
	#okay so. the VECTOR seems to get the player position minus self
	var vector = player.get_global_position() - self.get_global_position()
	if vector.length() < approach_radius:
		#FACING vector is a vectorifed version of facing (just passes in facing)
		var facing_vector = get_facing_dir_as_vector()
		var dot_product =  facing_vector.dot(vector)
		var angle = acos(dot_product/(vector.length()))
		return angle < sight_angle
	else: 
		return false

func approach_player():
	if can_move: 
		velocity = Vector2(0, 0)
		charge_basic_sprites.visible = false
		walk_sprites.visible = true
		charge_power_sprites.visible = false
		basic_sprites.visible = false
		basic_sprites.visible = false
		power_sprites.visible = false
		velocity = position.direction_to(player.position).normalized() * speed
		animation_tree.set("parameters/Walk/blend_position", velocity)
		animation_state.travel("Walk")
	var vector = self.get_global_position() - player.get_global_position()
	if player.is_in_safezone == true:
		current_state = states.IDLE
	elif vector.length() < attack_radius:
		if can_attack():
			if can_power_attack():
				current_state = states.CHARGE_POWER
			else:
				current_state = states.CHARGE_BASIC
	else:
		#....how does this differ from the above IF statement?
		#keep moving
		charge_basic_sprites.visible = false
		walk_sprites.visible = true
		charge_power_sprites.visible = false
		basic_sprites.visible = false
		basic_sprites.visible = false
		power_sprites.visible = false
		velocity = position.direction_to(player.position).normalized() * speed
		animation_tree.set("parameters/Walk/blend_position", velocity)
		animation_state.travel("Walk")
	if !can_see_player():
		if can_stop_chase():
			current_state = states.IDLE
	else:
		last_time_seen_player = elapsed_time + chase_time

func can_stop_chase():
	return elapsed_time >= last_time_seen_player

func can_attack():
	return elapsed_time >= can_attack_after_time

func can_power_attack():
	return elapsed_time >= can_power_attack_after_time

func charge_basic():
	if player:
		if !is_charging:
			is_charging = true
			can_move = false
			#add animation stuff
			charge_basic_sprites.visible = true
			walk_sprites.visible = false
			charge_power_sprites.visible = false
			basic_sprites.visible = false
			power_sprites.visible = false
			animation_tree.set("parameters/ChargeBasicAttack/blend_position", velocity)
			animation_state.travel("ChargeBasicAttack")
			animation_player.set_speed_scale(charge_basic_speed * attack_speed_modded)
			var timer_speed = match_scale_math_for_timer(charge_basic_speed, charge_speed_modded)
			yield(get_tree().create_timer(timer_speed), "timeout")
			is_charging = false
			#this triggers fine. So WHERE IS MY PROBLEM :C
			current_state = states.BASIC
			animation_player.set_speed_scale(1)

func basic_attack():
	if is_attacking:
		return
	is_attacking = true
	damage_mod = 1.0
	can_attack_after_time = elapsed_time + attack_cooldown
	#animation stuff
	charge_basic_sprites.visible = false
	walk_sprites.visible = false
	charge_power_sprites.visible = false
	basic_sprites.visible = true
	power_sprites.visible = false
	animation_player.set_speed_scale(basic_attack_speed * attack_speed_modded)
	animation_tree.set("parameters/BasicAttack/blend_position", velocity)
	animation_state.travel("BasicAttack")
	var timer_speed = match_scale_math_for_timer(basic_attack_speed, charge_speed_modded)
	yield(get_tree().create_timer(timer_speed), "timeout")
	is_attacking = false
	can_move = true
	current_state = states.APPROACH
	#approach to attack state comes next
	animation_player.set_speed_scale(1)

func charge_power():
	if !can_power_attack():
		return
	if player:
		if !is_charging:
			is_charging = true
			can_move = false
			#add animation stuff
			charge_basic_sprites.visible = false
			walk_sprites.visible = false
			charge_power_sprites.visible = true
			basic_sprites.visible = false
			power_sprites.visible = false
			animation_player.set_speed_scale(charge_power_speed * attack_speed_modded)
			animation_tree.set("parameters/ChargePowerAttack/blend_position", velocity)
			animation_state.travel("ChargePowerAttack")
			var timer_speed = match_scale_math_for_timer(charge_power_speed, charge_speed_modded)
			yield(get_tree().create_timer(timer_speed), "timeout")#replace 0.6 with a variable
			is_charging = false
			current_state = states.POWER
			animation_player.set_speed_scale(1)

func power_attack():
	if is_attacking:
		return
	is_attacking = true
	damage_mod = 2.0 #might change value later. but atm. it is twice the regular attack
	can_power_attack_after_time = elapsed_time + power_attack_cooldown
	can_attack_after_time = elapsed_time + attack_cooldown #update both. oen is global
	#animation stuff
	charge_basic_sprites.visible = false
	walk_sprites.visible = false
	charge_power_sprites.visible = false
	basic_sprites.visible = false
	power_sprites.visible = true
	animation_player.set_speed_scale(power_attack_speed * attack_speed_modded)
	animation_tree.set("parameters/PowerAttack/blend_position", velocity)
	animation_state.travel("PowerAttack")
	var timer_speed = match_scale_math_for_timer(power_attack_speed, charge_speed_modded)
	yield(get_tree().create_timer(timer_speed), "timeout") #replace 0.6 with a variable
	is_attacking = false
	can_move = true
	#approach to attack state comes next
	current_state = states.APPROACH
	animation_player.set_speed_scale(1)

#stolen from guard one. oop sforgot to include at firstr
func match_scale_math_for_timer(anim_speed, speed_modded):
	var new_speed
	if speed_modded < 1:
		new_speed = anim_speed / speed_modded
	elif speed_modded > 1:
		new_speed = anim_speed / speed_modded
	else: #speed must be exactly 1
		new_speed = anim_speed
	return new_speed

#until their interrupted timer passes
#for the guard we must send them back to patrol state!
func interrupted():
	current_state = states.APPROACH
	#animation_tree.advance(0)#trhis doens't seem to be working :< 
	#(hit box is in the attack anim)
	var sprites_to_freeze = [] #fuck it it doesn't even need to be all sprites
	#we need to freeze the node
	can_move = false
	sprites_to_freeze = check_nodes_types_and_return_sprites()
	#okay this dont work in an array :X so we must loop
	for i in sprites_to_freeze.size():
		sprites_to_freeze[i].self_modulate = Color(0, 0, 1)

func save():
	var save_dict = {
	"filename" : get_filename(),
	"parent" : get_parent().get_path(),
	"pos_x" : position.x, #Vector 2 is not supported by JSON
	"pos_y" : position.y,
	"maxHealth" : maxHealth,
	"currentHealth" : currentHealth,
	"maxHealth_modded": maxHealth_modded,
	"currentHealth_modded": currentHealth_modded,
	"team" : team,
	"damage" : damage,
	"speed" : speed,
	"state" : current_state
	}
	return save_dict


func _on_HurtBox2D_area_entered(area):
	#we need to see waht attack we are doing and adjust damage accordingly!
	if area.is_in_group("player_hitbox"):
		if area.get_parent().can_take_damage:
			if area.get_parent().is_pushable:
				#now we need to pass in teh direciton OUR attack came from
				#this works for melee attacks, not ranged attacks
				var push_vector = Vector2(global_position.x, global_position.y)
				area.get_parent().knockback(push_vector)
			area.get_parent().take_damage(damage)

Which animation works and which one doesn’t? Seems like you are using an Animation Tree state machine can you show that graph?

Is there any specific part of the code you think should be looked at? Have you tried adding prints before traveling the animation state, just to make sure the code is reached? Is there any code that would interrupt the animation?

Hi thanks for the reply!

The basic attack doesn’t work atm - it plays the charge, but never the rest of the attack.

I FEEL like the error must be somewhere in the basic attack function: I have used the debug tool where you pause at certain lines to look in (sorry brain DYING and i forgot the name). the f unction itself seems to be being entered, just the animation is not playing.

Ill get a pic of the animation tree machine right now. TY again

there may be more that would be useful to see? I’m not sure where else to show it though

It DOES play correctly when I hit play on it in the anim tree (manually, not in game) if that helps anything.

The travel state APPEARS to be the same on the charge power attack to power attack as it is on charge basic to basic.

… Not sure that was the best way to set it up, but like I said its a bit of an old project I’m just trying to finish up quick

looked over the power attack and basic attack code more today, along with th state machine.

i THINK the problem might somehow be in animations (either animation player, animation tree, or the animation itself), does anyone have a tip on how to debug those?

Sorry for being dense

bug seems to have stopped. Just stopped when i toggled on/ off a …toggle… that affected whether the animation plays automatically after the charge animation.

which is … weird. Cuz i hit toggle twice, so it went back to original state?
But it seems to work now :>