@tool @icon("spritesheet_animation.svg") class_name SpritesheetAnimation extends Node ## A node that animates a portion of a spritesheet displayed in a parent [Sprite2D]. ## ## The parent [Sprite2D] should have [member Sprite2D.hrames] and [member Sprite2D.vframes] ## properly set to match the spritesheet's grid. An animation consists of ## a coordinate (in frames, not pixels) pointing to the first frame of the animation ## and an amount of frames to include from then forward (wrapping to the next row ## if going beyond the current one). ## Emitted when a new frame is shown. signal frame_changed() ## Emitted when the animation reaches the last frame and loops. signal looped() ## Emitted when the animation reaches the last frame and stops. signal finished() @export_group("Animation Setup") ## Frame coordinates of the first animation frame. @export var first_frame: Vector2i: set(value): var sprite = get_parent() as Sprite2D if sprite: first_frame = value.clamp(Vector2i.ZERO, Vector2i(sprite.hframes - 1, sprite.vframes - 1)) else: first_frame = value ## Length of the animation in frames. @export_range(1, 1, 1, "or_greater") var frames: int = 1 ## Rate of animation in frames per second. @export_range(1, 60, 1, "or_greater") var fps: float = 1.0 ## Behavior when the last animation frame finishes. [br] ## If [code]true[/code], the animation will loop and emits [signal looped]. [br] ## If [code]false[/code], the animation will stay on the last frame and emits [signal finished]. @export var loop: bool = false @export_group("Playback") ## Whether the animation is playing. If another [SpritesheetAnimation] is ## playing through the same parent [Sprite2D], it will be stopped when this ## is set to [code]true[/code]. @export var playing: bool = false: set(value): if not playing and value and is_inside_tree(): _stop_other_animations() playing = value ## Speed scaling ration. [br] ## If set to a negative value, the animation will play backwards. @export var speed_scale: float = 1.0 var frame: int: get(): return int(_current_frame) var _current_frame: float = 0.0 func _ready() -> void: first_frame = first_frame func _physics_process(delta: float) -> void: if playing: var last_frame = frame _current_frame += delta * fps * speed_scale if _current_frame >= float(frames): if loop: _current_frame -= float(frames) looped.emit() else: playing = false _current_frame = float(frames - 1) finished.emit() return if _current_frame < 0.0: if loop: _current_frame += float(frames) looped.emit() else: playing = false _current_frame = 0.0 finished.emit() return if last_frame != frame: frame_changed.emit() _update_sprite() func _get_configuration_warnings() -> PackedStringArray: var sprite = get_parent() as Sprite2D if not sprite: return ["Must be a child of a Sprite2D in order to function."] return [] ## Plays the animation from the beginning. func play() -> void: playing = true _current_frame = 0.0 _update_sprite() ## Stops playback. func stop() -> void: playing = false func _update_sprite() -> void: var sprite = get_parent() as Sprite2D if sprite: sprite.frame_coords = first_frame sprite.frame += int(_current_frame) func _stop_other_animations() -> void: for child in get_parent().get_children().filter( func(node): return node != self and node is SpritesheetAnimation ): child.playing = false