State driven anim

Idle, windup, dash and recover, each with its own tween. Entering a state kills the tween of the previous one so they never fight over the same property.

The editor above is running this project. Change a line and it reloads.

Godot classes used

TweenPolygon2DLabel

The code

scripts/main.gd
extends Node

enum State { IDLE, WINDUP, DASH, RECOVER }

const HOME: float = 760.0
const ACTOR_Y: float = 380.0
const SIZE: float = 76.0
const NAMES: Array = ['IDLE', 'WINDUP', 'DASH', 'RECOVER']
const COLORS: Array = ['#8da5f3', '#f3d48d', '#8eef97', '#fc7f7f']
const DIM: Color = Color('#4a5162')

var state: State = State.IDLE
var tween: Tween
var actor: Polygon2D
var rows: Array[Label] = []
var pointer: Polygon2D


func _ready() -> void:
	_add_background()
	_add_label('one enum, four tweens, the state picks which one runs', Vector2(40, 40), Color('#999999'))

	for i in NAMES.size():
		var row: Label = _add_label(NAMES[i], Vector2(110, 130 + i * 60), DIM)
		row.add_theme_font_size_override('font_size', 30)
		rows.append(row)

	pointer = Polygon2D.new()
	pointer.polygon = PackedVector2Array([Vector2(0, -13), Vector2(21, 0), Vector2(0, 13)])
	pointer.position = Vector2(70, 152)
	add_child(pointer)

	var rail: ColorRect = ColorRect.new()
	rail.color = Color('#2a2f3d')
	rail.position = Vector2(600, ACTOR_Y + SIZE * 0.5)
	rail.size = Vector2(600, 4)
	add_child(rail)

	var home_mark: ColorRect = ColorRect.new()
	home_mark.color = Color('#4a5162')
	home_mark.position = Vector2(HOME - 2, ACTOR_Y + SIZE * 0.5 - 10)
	home_mark.size = Vector2(4, 24)
	add_child(home_mark)

	actor = _make_square()
	actor.position = Vector2(HOME, ACTOR_Y)
	add_child(actor)

	_set_state(State.IDLE)

	print('the cycle runs on its own, each state kills the tween of the previous one')


func _set_state(next: State) -> void:
	state = next
	actor.color = Color(COLORS[state])
	_highlight()

	if tween and tween.is_valid():
		tween.kill()

	tween = create_tween()

	var following: State

	match state:
		State.IDLE:
			tween.tween_property(actor, 'scale', Vector2(1.08, 0.92), 0.3).set_trans(Tween.TRANS_SINE)
			tween.tween_property(actor, 'scale', Vector2.ONE, 0.3).set_trans(Tween.TRANS_SINE)
			following = State.WINDUP
		State.WINDUP:
			tween.tween_property(actor, 'position:x', HOME - 90.0, 0.3).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
			tween.parallel().tween_property(actor, 'scale', Vector2(0.78, 1.22), 0.3)
			following = State.DASH
		State.DASH:
			tween.tween_property(actor, 'position:x', 1150.0, 0.4).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
			tween.parallel().tween_property(actor, 'scale', Vector2(1.5, 0.7), 0.2)
			following = State.RECOVER
		State.RECOVER:
			tween.tween_property(actor, 'position:x', HOME, 0.55).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN_OUT)
			tween.parallel().tween_property(actor, 'scale', Vector2.ONE, 0.55)
			following = State.IDLE

	tween.finished.connect(_set_state.bind(following), CONNECT_ONE_SHOT)


func _highlight() -> void:
	var active: Color = Color(COLORS[state])

	for i in rows.size():
		rows[i].add_theme_color_override('font_color', active if i == state else DIM)

	pointer.color = active
	pointer.position.y = 152 + state * 60


func _make_square() -> Polygon2D:
	var square: Polygon2D = Polygon2D.new()
	square.polygon = PackedVector2Array(
		[
			Vector2(-SIZE, -SIZE) * 0.5,
			Vector2(SIZE, -SIZE) * 0.5,
			Vector2(SIZE, SIZE) * 0.5,
			Vector2(-SIZE, SIZE) * 0.5,
		]
	)

	return square


func _add_background() -> void:
	var background: ColorRect = ColorRect.new()
	background.color = Color('#12141a')
	add_child(background)
	background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)


func _add_label(text: String, at: Vector2, color: Color) -> Label:
	var label: Label = Label.new()
	label.text = text
	label.position = at
	label.add_theme_color_override('font_color', color)
	add_child(label)

	return label

More Animation examples