Godot Version
4.4.1
Question
For some reason, my AnimatedSprite2D
keeps on looping and repeating the animations I want to play, even though I have set them to not loop.
In the video attached you can see an example, where the frog keeps on repeating the attack_start
and attack_end
animations, even though looping is turned off for both. I expect the animation attack_start
to play once, the tongue to shoot out and retract and then for attack_end
to play once.
if you need the full context, you can find the game here.
Code
class_name JoltJumper
extends Enemy
@export var animated_sprite: AnimatedSprite2D
@export var border_bounce_component: BorderBounceComponent
@export var collision_box: Area2D
@export var tongue_spawn_point: Marker2D
enum State { IDLE, JUMPING, ATTACKING }
enum TonguePosition { IN, EXTENDING, OUT, RETRACTING }
const STATE_DELAY := 1.0
var state: State: set = set_state
var tongue_position: TonguePosition: set = set_tongue_position
var jump_direction: int
var random: RandomNumberGenerator
var state_timer: float
var tongue_tip_scene = preload("res://entities/enemies/jolt_jumper/tongue_tip.tscn")
var tongue_body_scene = preload("res://entities/enemies/jolt_jumper/tongue_body.tscn")
var tongue_stack: Array
func _ready() -> void:
super._ready()
state = State.IDLE
tongue_position = TonguePosition.IN
random = RandomNumberGenerator.new()
random.randomize()
tongue_stack = []
border_bounce_component.bounced.connect(func(direction: int):
jump_direction = direction
)
# THIS IS ONLY AS A FALLBACK, SINCE IT ISN'T THE CLEANEST
collision_box.area_entered.connect(_on_collision_box_entered)
func _process(delta: float) -> void:
if state == State.IDLE:
animated_sprite.play("idle")
state_timer += delta
if state_timer >= STATE_DELAY:
state_timer = 0
state = State.JUMPING
if state == State.JUMPING:
animated_sprite.play("jumping")
state_timer += delta
if state_timer >= STATE_DELAY:
state_timer = 0
if global_position.y >= Constants.SHIP_Y_POSITION - 8:
state = State.IDLE
else:
state = State.ATTACKING
if state == State.ATTACKING:
if not animated_sprite.is_playing():
animated_sprite.play("attack_start")
func _physics_process(_delta: float) -> void:
if state == State.IDLE:
_reset_move_stats()
if state == State.JUMPING:
_update_move_stats()
if state == State.ATTACKING:
_reset_move_stats()
func _reset_move_stats():
move_component.velocity = Vector2.ZERO
func _update_move_stats():
move_component.velocity = Vector2(random.randi_range(35,45) * jump_direction, random.randi_range(35,45))
func _generate_random_jump_direction() -> int:
var value = random.randf()
if value < 0.5:
return -1
else:
return 1
func set_state(new_state: State):
state = new_state
match state:
State.IDLE:
tongue_position = TonguePosition.IN
pass
State.JUMPING:
tongue_position = TonguePosition.IN
jump_direction = _generate_random_jump_direction()
State.ATTACKING:
attack()
func _on_collision_box_entered(area_2d: Area2D) -> void:
if area_2d.get_parent() is JoltJumper:
var other_jolt_jumper = area_2d.get_parent() as JoltJumper
_bounce_away_from(other_jolt_jumper)
func _bounce_away_from(other_jumper: JoltJumper):
var push_dir = (global_position - other_jumper.global_position).normalized()
var speed = move_component.velocity.length()
move_component.velocity = push_dir * speed
var separation_distance = 4.0
global_position += push_dir * separation_distance
func render_tongue() -> void:
for i in tongue_stack.size():
var scene = tongue_stack[i]
if not is_instance_valid(scene):
continue
if not scene.is_inside_tree():
add_child(scene)
scene.global_position = tongue_spawn_point.global_position + Vector2(0, i * Constants.TONGUE_BODY_HEIGHT)
func shoot_out_tongue() -> void:
if not tongue_position == TonguePosition.IN:
return
tongue_position = TonguePosition.EXTENDING
var current_length = 0
var tongue_tip = tongue_tip_scene.instantiate()
tongue_stack.push_back(tongue_tip)
render_tongue()
await get_tree().create_timer(0.015).timeout
while tongue_spawn_point.global_position.y + current_length < 216 + Constants.TONGUE_BODY_HEIGHT * 3:
if not is_inside_tree():
return
if get_tree().paused:
await get_tree().process_frame
continue
current_length += Constants.TONGUE_BODY_HEIGHT
var tongue_body = tongue_body_scene.instantiate()
tongue_stack.push_front(tongue_body)
render_tongue()
await get_tree().create_timer(0.015).timeout
tongue_position = TonguePosition.OUT
# TODO: FOR DEBUGGING, REMOVE AFTER
await get_tree().create_timer(0.5).timeout
func retract_tongue() -> void:
if not tongue_position == TonguePosition.OUT:
return
tongue_position = TonguePosition.RETRACTING
while not tongue_stack.is_empty():
if not is_inside_tree():
return
if get_tree().paused:
await get_tree().process_frame
continue
var tongue_part = tongue_stack.pop_front()
if not is_instance_valid(tongue_part):
continue
if tongue_part.is_inside_tree():
tongue_part.queue_free()
render_tongue()
await get_tree().create_timer(0.015).timeout
tongue_position = TonguePosition.IN
# Issues:
# - The tongue is serialised, but not positioned back correctly
# - The animation keeps on playing, even though I put it to a stop programatically and the animation loop is turned off
# - The tongue is like a death ray, dealing constant damage, I want the tongue to deal damage, and then be turned off for a while (I want the player to have no vulnerability for a while of the same tongue, but not from any other damage source)
func attack() -> void:
animated_sprite.play("attack_start")
animated_sprite.animation_finished.connect(func():
animated_sprite.stop()
)
await shoot_out_tongue()
await retract_tongue()
animated_sprite.play("attack_end")
animated_sprite.animation_finished.connect(func():
animated_sprite.stop()
)
state = State.IDLE
func serialise() -> Dictionary:
var data = super.serialise()
data["state"] = state
data["tongue_position"] = tongue_position
data["jump_direction"] = jump_direction
data["tongue_stack"] = tongue_stack
return data
func deserialise(data: Dictionary) -> void:
super.deserialise(data)
if data.has("state"):
state = data["state"]
if data.has("tongue_position"):
tongue_position = data["tongue_position"]
if data.has("jump_direction"):
jump_direction = data["jump_direction"]
if data.has("tongue_stack"):
tongue_stack = data["tongue_stack"]
render_tongue()
# TODO: FOR DEBUGGING, REMOVE AFTER
@onready var label: Label = $Label
func set_tongue_position(new_position: TonguePosition):
label.text = parse_enum(new_position)
tongue_position = new_position
func parse_enum(number: int) -> String:
match number:
0: return "IN"
1: return "EXTENDING"
2: return "OUT"
3: return "RETRACTING"
_: return ""