Created
January 15, 2026 10:07
-
-
Save mieko/55fcc3c92862153ab8cd0f2947754c46 to your computer and use it in GitHub Desktop.
See the current and previous states of an AnimationNodeStateMachine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
sm-debug.mp4