Skip to content

Instantly share code, notes, and snippets.

@mieko
Created January 15, 2026 10:07
Show Gist options
  • Select an option

  • Save mieko/55fcc3c92862153ab8cd0f2947754c46 to your computer and use it in GitHub Desktop.

Select an option

Save mieko/55fcc3c92862153ab8cd0f2947754c46 to your computer and use it in GitHub Desktop.
See the current and previous states of an AnimationNodeStateMachine
# StateMachineHistoryLabel.gd
# Debug UI for AnimationNodeStateMachinePlayback
# Horizontal rolling history, arrows, newest entry pulse
class_name StateMachineHistoryLabel
extends Control
@export var animation_tree: AnimationTree
@export var state_machine_path: StringName
@export var max_entries: int = 8
@export var persistent_count: int = 2
@export var fade_delay: float = 0.5
@export var fade_duration: float = 1.0
@export var bright_color: Color = Color(1, 1, 1, 1)
@export var dim_color: Color = Color(0.7, 0.7, 0.7, 1)
@export var min_alpha: float = 0.25
@export var newest_scale: float = 1.15
@export var scale_decay_duration: float = 0.2
# Internal container for labels
var _container: HBoxContainer
var _playback: AnimationNodeStateMachinePlayback
var _current_state: StringName = &""
var _entries: Array[Dictionary] = []
func _ready() -> void:
assert(animation_tree != null)
_container = HBoxContainer.new()
_container.alignment = BoxContainer.ALIGNMENT_END
_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_container.size_flags_vertical = Control.SIZE_FILL
_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_container.add_child(spacer)
add_child(_container)
# Resolve playback node
_playback = animation_tree.get(_resolve_playback_path(state_machine_path))
assert(_playback != null)
func _process(delta: float) -> void:
if _playback == null:
return
var state: StringName = _playback.get_current_node()
if state != _current_state:
_current_state = state
_push_state(state)
_update_entries(delta)
_update_labels(delta)
func _push_state(state: StringName) -> void:
_entries.append({ "state": state, "age": 0.0 })
if _entries.size() > max_entries:
_entries.pop_front()
func _update_entries(delta: float) -> void:
for entry in _entries:
entry.age += delta
func _update_labels(delta: float) -> void:
_ensure_label_count(_entries.size())
var labels: Array[RichTextLabel] = []
for child in _container.get_children():
if child is RichTextLabel:
labels.append(child)
for i in _entries.size():
var entry := _entries[i]
var label := labels[i]
var is_newest: bool = i == _entries.size() - 1
var is_persistent: bool = i >= _entries.size() - persistent_count
label.bbcode_enabled = true
label.text = _format_entry(entry.state, is_newest, i)
var alpha: float = _compute_alpha(entry.age, is_persistent)
var color: Color = bright_color if is_newest else dim_color
color.a *= alpha
label.modulate = color
_update_scale(label, entry.age, is_newest)
func _update_scale(label: Control, age: float, is_newest: bool) -> void:
if not is_newest:
label.scale = Vector2.ONE
return
var t: float = clampf(age / scale_decay_duration, 0.0, 1.0)
label.scale = Vector2.ONE.lerp(Vector2.ONE * newest_scale, 1.0 - t)
func _compute_alpha(age: float, persistent: bool) -> float:
if persistent:
return 1.0
if age <= fade_delay:
return 1.0
var t: float = (age - fade_delay) / fade_duration
return clamp(1.0 - t, min_alpha, 1.0)
func _format_entry(state: StringName, is_newest: bool, index: int) -> String:
var text: String = str(state)
if not is_newest and index < _entries.size() - 1:
text += " →"
if is_newest:
text = "[b]%s[/b]" % text
return "[font_size=24]%s[/font_size]" % text
func _ensure_label_count(count: int) -> void:
var labels: Array[RichTextLabel] = []
for child in _container.get_children():
if child is RichTextLabel:
labels.append(child)
# Add missing labels
while labels.size() < count:
var lbl := _create_label()
_container.add_child(lbl)
labels.append(lbl)
# Remove excess labels
while labels.size() > count:
var child := labels.pop_front() as RichTextLabel
_container.remove_child(child)
child.queue_free()
func _create_label() -> RichTextLabel:
var lbl := RichTextLabel.new()
lbl.fit_content = true
lbl.scroll_active = false
lbl.bbcode_enabled = true
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
lbl.autowrap_mode = TextServer.AUTOWRAP_OFF
return lbl
func _resolve_playback_path(path: StringName) -> StringName:
if path.begins_with("parameters/"):
return path if path.ends_with("/playback") else StringName("%s/playback" % path)
return "parameters/%s/playback" % path
@mieko
Copy link
Author

mieko commented Jan 15, 2026

sm-debug.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment