initial commit: canny cat basic movement, bouncing, some gridmap tiles for levels

This commit is contained in:
Haze Weathers 2025-02-22 16:48:31 -05:00
commit e1b43c8bc5
120 changed files with 5785 additions and 0 deletions

View file

@ -0,0 +1,388 @@
## UI for the in-editor state debugger
@tool
extends Control
## Utility class for holding state info
const DebuggerStateInfo = preload("editor_debugger_state_info.gd")
## Debugger history wrapper. Shared with in-game debugger.
const DebuggerHistory = preload("../debugger_history.gd")
## The debugger message
const DebuggerMessage = preload("editor_debugger_message.gd")
## The settings propapagor
const SettingsPropagator = preload("editor_debugger_settings_propagator.gd")
## Constants for the settings
const SETTINGS_ROOT:String = "godot_state_charts/debugger/"
const SETTINGS_IGNORE_EVENTS:String = SETTINGS_ROOT + "ignore_events"
const SETTINGS_IGNORE_STATE_CHANGES:String = SETTINGS_ROOT + "ignore_state_changes"
const SETTINGS_IGNORE_TRANSITIONS:String = SETTINGS_ROOT + "ignore_transitions"
const SETTINGS_MAXIMUM_LINES:String = SETTINGS_ROOT + "maximum_lines"
const SETTINGS_SPLIT_OFFSET:String = SETTINGS_ROOT + "split_offset"
## The tree that shows all state charts
@onready var _all_state_charts_tree:Tree = %AllStateChartsTree
## The tree that shows the current state chart
@onready var _current_state_chart_tree:Tree = %CurrentStateChartTree
## The history edit
@onready var _history_edit:TextEdit = %HistoryEdit
## The settings UI
@onready var _ignore_events_checkbox:CheckBox = %IgnoreEventsCheckbox
@onready var _ignore_state_changes_checkbox:CheckBox = %IgnoreStateChangesCheckbox
@onready var _ignore_transitions_checkbox:CheckBox = %IgnoreTransitionsCheckbox
@onready var _maximum_lines_spin_box:SpinBox = %MaximumLinesSpinBox
@onready var _split_container:HSplitContainer = %SplitContainer
## The actual settings
var _ignore_events:bool = true
var _ignore_state_changes:bool = false
var _ignore_transitions:bool = true
## The editor settings for storing all the settings across sessions
var _settings:EditorSettings = null
## The current session (EditorDebuggerSession)
## this does not exist in exported games, so this is deliberately not
## typed, to avoid compile errors after exporting
var _session = null
## Dictionary of all state charts and their states. Key is the path to the
## state chart, value is a dictionary of states. Key is the path to the state,
## value is the state info (an array).
var _state_infos:Dictionary = {}
## Dictionary of all state charts and their histories. Key is the path to the
## state chart, value is the history.
var _chart_histories:Dictionary = {}
## Path to the currently selected state chart.
var _current_chart:NodePath = ""
## Helper variable for debouncing the maximum lines setting. When
## the value is -1, the setting hasn't been changed yet. When it's
## >= 0, the setting has been changed and the timer is waiting for
## the next timeout to update the setting. The debouncing is done
## in the same function that updates the text edit.
var _debounced_maximum_lines:int = -1
## Initializes the debugger UI using the editor settings.
func initialize(settings:EditorSettings, session:EditorDebuggerSession):
clear()
_settings = settings
_session = session
# restore editor settings
_ignore_events = _get_setting_or_default(SETTINGS_IGNORE_EVENTS, true)
_ignore_state_changes = _get_setting_or_default(SETTINGS_IGNORE_STATE_CHANGES, false)
_ignore_transitions = _get_setting_or_default(SETTINGS_IGNORE_TRANSITIONS, true)
# initialize UI elements, so they match the settings
_ignore_events_checkbox.set_pressed_no_signal(_ignore_events)
_ignore_state_changes_checkbox.set_pressed_no_signal(_ignore_state_changes)
_ignore_transitions_checkbox.set_pressed_no_signal(_ignore_transitions)
_maximum_lines_spin_box.value = _get_setting_or_default(SETTINGS_MAXIMUM_LINES, 300)
_split_container.split_offset = _get_setting_or_default(SETTINGS_SPLIT_OFFSET, 0)
## Returns the given setting or the default value if the setting is not set.
## No clue, why this isn't a built-in function.
func _get_setting_or_default(key, default) -> Variant :
if _settings == null:
return default
if not _settings.has_setting(key):
return default
return _settings.get_setting(key)
## Sets the given setting and marks it as changed.
func _set_setting(key, value) -> void:
if _settings == null:
return
_settings.set_setting(key, value)
_settings.mark_setting_changed(key)
## Clears all state charts and state trees.
func clear():
_clear_all()
## Clears all state charts and state trees.
func _clear_all():
_state_infos.clear()
_chart_histories.clear()
_all_state_charts_tree.clear()
var root := _all_state_charts_tree.create_item()
root.set_text(0, "State Charts")
root.set_selectable(0, false)
_clear_current()
## Clears all data about the current chart from the ui
func _clear_current():
_current_chart = ""
_current_state_chart_tree.clear()
_history_edit.clear()
var root := _current_state_chart_tree.create_item()
root.set_text(0, "States")
root.set_selectable(0, false)
## Adds a new state chart to the debugger.
func add_chart(path:NodePath):
_state_infos[path] = {}
_chart_histories[path] = DebuggerHistory.new()
_repaint_charts()
# push the settings to the new chart remote
SettingsPropagator.send_settings_update(_session, path, _ignore_events, _ignore_transitions)
if _current_chart.is_empty():
_current_chart = path
_select_current_chart()
## Removes a state chart from the debugger.
func remove_chart(path:NodePath):
_state_infos.erase(path)
if _current_chart == path:
_clear_current()
_repaint_charts()
## Updates state information for a state chart.
func update_state(_frame:int, state_info:Array) -> void:
var chart := DebuggerStateInfo.get_chart(state_info)
var path := DebuggerStateInfo.get_state(state_info)
if not _state_infos.has(chart):
push_error("Probable bug: Received state info for unknown chart %s" % [chart])
return
_state_infos[chart][path] = state_info
## Called when a state is entered.
func state_entered(frame:int, chart:NodePath, state:NodePath) -> void:
if not _state_infos.has(chart):
return
if not _ignore_state_changes:
var history:DebuggerHistory = _chart_histories[chart]
history.add_state_entered(frame, _get_node_name(state))
var state_info = _state_infos[chart][state]
DebuggerStateInfo.set_active(state_info, true)
## Called when a state is exited.
func state_exited(frame:int, chart:NodePath, state:NodePath) -> void:
if not _state_infos.has(chart):
return
if not _ignore_state_changes:
var history:DebuggerHistory = _chart_histories[chart]
history.add_state_exited(frame, _get_node_name(state))
var state_info = _state_infos[chart][state]
DebuggerStateInfo.set_active(state_info, false)
## Called when an event is received.
func event_received(frame:int, chart:NodePath, event:StringName) -> void:
var history:DebuggerHistory = _chart_histories.get(chart, null)
history.add_event(frame, event)
## Called when a transition is pending
func transition_pending(_frame:int, chart:NodePath, state:NodePath, transition:NodePath, pending_time:float) -> void:
var state_info = _state_infos[chart][state]
DebuggerStateInfo.set_transition_pending(state_info, transition, pending_time)
func transition_taken(frame:int, chart:NodePath, transition:NodePath, source:NodePath, destination:NodePath) -> void:
var history:DebuggerHistory = _chart_histories.get(chart, null)
history.add_transition(frame, _get_node_name(transition), _get_node_name(source), _get_node_name(destination))
## Repaints the tree of all state charts.
func _repaint_charts() -> void:
for chart in _state_infos.keys():
_add_to_tree(_all_state_charts_tree, chart, preload("../../state_chart.svg"))
_clear_unused_items(_all_state_charts_tree.get_root())
## Selects the current chart in the main tree.
func _select_current_chart(item:TreeItem = _all_state_charts_tree.get_root()) -> void:
if item.has_meta("__path") and item.get_meta("__path") == _current_chart:
_all_state_charts_tree.set_selected(item, 0)
return
var first_child := item.get_first_child()
if first_child != null:
_select_current_chart(first_child)
var next := item.get_next()
while next != null:
_select_current_chart(next)
next = next.get_next()
## Repaints the tree of the currently selected state chart.
func _repaint_current_chart(force:bool = false) -> void:
if _current_chart.is_empty():
return
# get the history for this chart and update the history text edit
var history:DebuggerHistory = _chart_histories[_current_chart]
if history != null and (history.dirty or force):
_history_edit.text = history.get_history_text()
_history_edit.scroll_vertical = _history_edit.get_line_count() - 1
# update the tree
for state_info in _state_infos[_current_chart].values():
if DebuggerStateInfo.get_active(state_info):
_add_to_tree(_current_state_chart_tree, DebuggerStateInfo.get_state(state_info), DebuggerStateInfo.get_state_icon(state_info))
if DebuggerStateInfo.get_transition_pending(state_info):
var transition_path := DebuggerStateInfo.get_transition_path(state_info)
var transition_delay := DebuggerStateInfo.get_transition_delay(state_info)
var name := _get_node_name(transition_path)
_add_to_tree(_current_state_chart_tree, DebuggerStateInfo.get_transition_path(state_info), preload("../../transition.svg"), "%s (%.1fs)" % [name, transition_delay])
_clear_unused_items(_current_state_chart_tree.get_root())
## Walks over the tree and removes all items that are not marked as in use
## removes the "in-use" marker from all remaining items
func _clear_unused_items(root:TreeItem) -> void:
if root == null:
return
for child in root.get_children():
if not child.has_meta("__in_use"):
root.remove_child(child)
_free_all(child)
else:
child.remove_meta("__in_use")
_clear_unused_items(child)
## Frees this tree item and all its children
func _free_all(root:TreeItem) -> void:
if root == null:
return
for child in root.get_children():
root.remove_child(child)
_free_all(child)
root.free()
## Adds an item to the tree. Will re-use existing items if possible.
## The node path will be used as structure for the tree. The created
## leaf will have the given icon and text.
func _add_to_tree(tree:Tree, path:NodePath, icon:Texture2D, text:String = ""):
var ref := tree.get_root()
for i in path.get_name_count():
var segment := path.get_name(i)
# do we need to add a new child?
var needs_new := true
if ref != null:
for child in ref.get_children():
# re-use child if it exists
if child.get_text(0) == segment:
ref = child
ref.set_meta("__in_use", true)
needs_new = false
break
if needs_new:
ref = tree.create_item(ref)
ref.set_text(0, segment)
ref.set_meta("__in_use", true)
ref.set_selectable(0, false)
ref.set_meta("__path", path)
if text != "":
ref.set_text(0, text)
ref.set_icon(0, icon)
ref.set_selectable(0, true)
## Called when a state chart is selected in the tree.
func _on_all_state_charts_tree_item_selected():
var item := _all_state_charts_tree.get_selected()
if item == null:
return
if not item.has_meta("__path"):
return
var path = item.get_meta("__path")
_current_chart = path
_repaint_current_chart(true)
## Called every 0.5 seconds to update the history text edit and the maximum lines setting.
func _on_timer_timeout():
# update the maximum lines setting if it has changed
if _debounced_maximum_lines >= 0:
_set_setting(SETTINGS_MAXIMUM_LINES, _debounced_maximum_lines)
# walk over all histories and update their maximum lines
for history in _chart_histories.values():
history.set_maximum_lines(_debounced_maximum_lines)
# and reset the debounced value
_debounced_maximum_lines = -1
# repaint the current chart
_repaint_current_chart()
## Called when the ignore events checkbox is toggled.
func _on_ignore_events_checkbox_toggled(button_pressed:bool) -> void:
_ignore_events = button_pressed
_set_setting(SETTINGS_IGNORE_EVENTS, button_pressed)
# push the new setting to all remote charts
for chart in _state_infos.keys():
SettingsPropagator.send_settings_update(_session, chart, _ignore_events, _ignore_transitions)
## Called when the ignore state changes checkbox is toggled.
func _on_ignore_state_changes_checkbox_toggled(button_pressed:bool) -> void:
_ignore_state_changes = button_pressed
_set_setting(SETTINGS_IGNORE_STATE_CHANGES, button_pressed)
## Called when the ignore transitions checkbox is toggled.
func _on_ignore_transitions_checkbox_toggled(button_pressed:bool) -> void:
_ignore_transitions = button_pressed
_set_setting(SETTINGS_IGNORE_TRANSITIONS, button_pressed)
# push the new setting to all remote charts
for chart in _state_infos.keys():
SettingsPropagator.send_settings_update(_session, chart, _ignore_events, _ignore_transitions)
## Called when the maximum lines spin box value is changed.
func _on_maximum_lines_spin_box_value_changed(value:int) -> void:
_debounced_maximum_lines = value
## Called when the split container is dragged.
func _on_split_container_dragged(offset:int) -> void:
_set_setting(SETTINGS_SPLIT_OFFSET, offset)
## Helper to get the last element of a node path
func _get_node_name(path:NodePath) -> StringName:
return path.get_name(path.get_name_count() - 1)
## Called when the clear button is pressed.
func _on_clear_button_pressed() -> void:
_history_edit.text = ""
if _chart_histories.has(_current_chart):
var history:DebuggerHistory = _chart_histories[_current_chart]
history.clear()
## Called when the copy to clipboard button is pressed.
func _on_copy_to_clipboard_button_pressed() -> void:
DisplayServer.clipboard_set(_history_edit.text)

View file

@ -0,0 +1,120 @@
[gd_scene load_steps=2 format=3 uid="uid://donfbhh5giyfy"]
[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/editor_debugger/editor_debugger.gd" id="1_ia1de"]
[node name="State Charts" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_ia1de")
[node name="SplitContainer" type="HSplitContainer" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
split_offset = 300
[node name="AllStateChartsTree" type="Tree" parent="SplitContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="TabContainer" type="TabContainer" parent="SplitContainer"]
layout_mode = 2
[node name="State Chart" type="MarginContainer" parent="SplitContainer/TabContainer"]
layout_mode = 2
[node name="CurrentStateChartTree" type="Tree" parent="SplitContainer/TabContainer/State Chart"]
unique_name_in_owner = true
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
size_flags_vertical = 3
[node name="History" type="MarginContainer" parent="SplitContainer/TabContainer"]
visible = false
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="VBoxContainer" type="VBoxContainer" parent="SplitContainer/TabContainer/History"]
layout_mode = 2
theme_override_constants/separation = 4
[node name="HistoryEdit" type="TextEdit" parent="SplitContainer/TabContainer/History/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="SplitContainer/TabContainer/History/VBoxContainer"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="SplitContainer/TabContainer/History/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Clear"
[node name="CopyToClipboardButton" type="Button" parent="SplitContainer/TabContainer/History/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Copy to Clipboard"
[node name="Settings" type="MarginContainer" parent="SplitContainer/TabContainer"]
visible = false
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="VBoxContainer" type="VBoxContainer" parent="SplitContainer/TabContainer/Settings"]
layout_mode = 2
theme_override_constants/separation = 4
[node name="IgnoreEventsCheckbox" type="CheckBox" parent="SplitContainer/TabContainer/Settings/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Do not show events in the history."
text = "Ignore events"
[node name="IgnoreStateChangesCheckbox" type="CheckBox" parent="SplitContainer/TabContainer/Settings/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Do not show state changes in the history."
text = "Ignore state changes"
[node name="IgnoreTransitionsCheckbox" type="CheckBox" parent="SplitContainer/TabContainer/Settings/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Do not show transitions in the history."
text = "Ignore transitions"
[node name="Label" type="Label" parent="SplitContainer/TabContainer/Settings/VBoxContainer"]
layout_mode = 2
text = "Maximum lines in history"
[node name="MaximumLinesSpinBox" type="SpinBox" parent="SplitContainer/TabContainer/Settings/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
min_value = 50.0
max_value = 1000.0
value = 300.0
rounded = true
[node name="Timer" type="Timer" parent="."]
wait_time = 0.2
autostart = true
[connection signal="dragged" from="SplitContainer" to="." method="_on_split_container_dragged"]
[connection signal="item_selected" from="SplitContainer/AllStateChartsTree" to="." method="_on_all_state_charts_tree_item_selected"]
[connection signal="pressed" from="SplitContainer/TabContainer/History/VBoxContainer/HBoxContainer/ClearButton" to="." method="_on_clear_button_pressed"]
[connection signal="pressed" from="SplitContainer/TabContainer/History/VBoxContainer/HBoxContainer/CopyToClipboardButton" to="." method="_on_copy_to_clipboard_button_pressed"]
[connection signal="toggled" from="SplitContainer/TabContainer/Settings/VBoxContainer/IgnoreEventsCheckbox" to="." method="_on_ignore_events_checkbox_toggled"]
[connection signal="toggled" from="SplitContainer/TabContainer/Settings/VBoxContainer/IgnoreStateChangesCheckbox" to="." method="_on_ignore_state_changes_checkbox_toggled"]
[connection signal="toggled" from="SplitContainer/TabContainer/Settings/VBoxContainer/IgnoreTransitionsCheckbox" to="." method="_on_ignore_transitions_checkbox_toggled"]
[connection signal="value_changed" from="SplitContainer/TabContainer/Settings/VBoxContainer/MaximumLinesSpinBox" to="." method="_on_maximum_lines_spin_box_value_changed"]
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]

View file

@ -0,0 +1,117 @@
@tool
const MESSAGE_PREFIX = "gds"
const STATE_CHART_ADDED_MESSAGE = MESSAGE_PREFIX + ":sca"
const STATE_CHART_REMOVED_MESSAGE = MESSAGE_PREFIX + ":scr"
const STATE_UPDATED_MESSAGE = MESSAGE_PREFIX + ":stu"
const STATE_ENTERED_MESSAGE = MESSAGE_PREFIX + ":sten"
const STATE_EXITED_MESSAGE = MESSAGE_PREFIX + ":stex"
const TRANSITION_PENDING_MESSAGE = MESSAGE_PREFIX + ":trp"
const TRANSITION_TAKEN_MESSAGE = MESSAGE_PREFIX + ":trf"
const STATE_CHART_EVENT_RECEIVED_MESSAGE = MESSAGE_PREFIX + ":scev"
const DebuggerStateInfo = preload("editor_debugger_state_info.gd")
## Whether we can currently send debugger messages.
static func _can_send() -> bool:
return not Engine.is_editor_hint() and OS.has_feature("editor")
## Sends a state_chart_added message.
static func state_chart_added(chart:StateChart) -> void:
if not _can_send():
return
if not chart.is_inside_tree():
return
EngineDebugger.send_message(STATE_CHART_ADDED_MESSAGE, [chart.get_path()])
## Sends a state_chart_removed message.
static func state_chart_removed(chart:StateChart) -> void:
if not _can_send():
return
if not chart.is_inside_tree():
return
EngineDebugger.send_message(STATE_CHART_REMOVED_MESSAGE, [chart.get_path()])
## Sends a state_updated message
static func state_updated(chart:StateChart, state:StateChartState) -> void:
if not _can_send():
return
if not state.is_inside_tree() or not chart.is_inside_tree():
return
var transition_path:NodePath = NodePath()
if is_instance_valid(state._pending_transition) and state._pending_transition.is_inside_tree():
transition_path = chart.get_path_to(state._pending_transition)
EngineDebugger.send_message(STATE_UPDATED_MESSAGE, [Engine.get_process_frames(), DebuggerStateInfo.make_array( \
chart.get_path(), \
chart.get_path_to(state), \
state.active, \
is_instance_valid(state._pending_transition), \
transition_path, \
state._pending_transition_remaining_delay, \
state)]
)
## Sends a state_entered message
static func state_entered(chart:StateChart, state:StateChartState) -> void:
if not _can_send():
return
if not state.is_inside_tree() or not chart.is_inside_tree():
return
EngineDebugger.send_message(STATE_ENTERED_MESSAGE,[Engine.get_process_frames(), chart.get_path(), chart.get_path_to(state)])
## Sends a state_exited message
static func state_exited(chart:StateChart, state:StateChartState) -> void:
if not _can_send():
return
if not state.is_inside_tree() or not chart.is_inside_tree():
return
EngineDebugger.send_message(STATE_EXITED_MESSAGE,[Engine.get_process_frames(), chart.get_path(), chart.get_path_to(state)])
## Sends a transition taken message
static func transition_taken(chart:StateChart, source:StateChartState, transition:Transition) -> void:
if not _can_send():
return
var target:StateChartState = transition.resolve_target()
if not source.is_inside_tree() or not chart.is_inside_tree() or not transition.is_inside_tree() or target == null or not target.is_inside_tree():
return
EngineDebugger.send_message(TRANSITION_TAKEN_MESSAGE,[Engine.get_process_frames(), chart.get_path(), chart.get_path_to(transition), chart.get_path_to(source), chart.get_path_to(target)])
## Sends an event received message
static func event_received(chart:StateChart, event_name:StringName) -> void:
if not _can_send():
return
if not chart.is_inside_tree():
return
EngineDebugger.send_message(STATE_CHART_EVENT_RECEIVED_MESSAGE, [Engine.get_process_frames(), chart.get_path(), event_name])
## Sends a transition pending message
static func transition_pending(chart:StateChart, source:StateChartState, transition:Transition, pending_transition_remaining_delay:float) -> void:
if not _can_send():
return
if not source.is_inside_tree() or not chart.is_inside_tree() or not transition.is_inside_tree():
return
EngineDebugger.send_message(TRANSITION_PENDING_MESSAGE, [Engine.get_process_frames(), chart.get_path(), chart.get_path_to(source), chart.get_path_to(transition), pending_transition_remaining_delay])

View file

@ -0,0 +1,53 @@
## Debugger plugin to show state charts in the editor UI.
extends EditorDebuggerPlugin
## Debugger message network protocol
const DebuggerMessage = preload("editor_debugger_message.gd")
const DebuggerUI = preload("editor_debugger.gd")
## The UI scene holding the debugger UI
var _debugger_ui_scene:PackedScene = preload("editor_debugger.tscn")
## Current editor settings
var _settings:EditorSettings = null
func initialize(settings:EditorSettings):
_settings = settings
func _has_capture(prefix):
return prefix == DebuggerMessage.MESSAGE_PREFIX
func _capture(message, data, session_id):
var ui:DebuggerUI = get_session(session_id).get_meta("__state_charts_debugger_ui")
match(message):
DebuggerMessage.STATE_CHART_EVENT_RECEIVED_MESSAGE:
ui.event_received(data[0], data[1], data[2])
DebuggerMessage.STATE_CHART_ADDED_MESSAGE:
ui.add_chart(data[0])
DebuggerMessage.STATE_CHART_REMOVED_MESSAGE:
ui.remove_chart(data[0])
DebuggerMessage.STATE_UPDATED_MESSAGE:
ui.update_state(data[0], data[1])
DebuggerMessage.STATE_CHART_EVENT_RECEIVED_MESSAGE:
ui.event_received(data[0], data[1], data[2])
DebuggerMessage.STATE_ENTERED_MESSAGE:
ui.state_entered(data[0], data[1], data[2])
DebuggerMessage.STATE_EXITED_MESSAGE:
ui.state_exited(data[0], data[1], data[2])
DebuggerMessage.TRANSITION_PENDING_MESSAGE:
ui.transition_pending(data[0], data[1], data[2], data[3], data[4])
DebuggerMessage.TRANSITION_TAKEN_MESSAGE:
ui.transition_taken(data[0], data[1], data[2], data[3], data[4])
return true
func _setup_session(session_id):
# get the session
var session = get_session(session_id)
# Add a new tab in the debugger session UI containing a label.
var debugger_ui:DebuggerUI = _debugger_ui_scene.instantiate()
# add the session tab
session.add_session_tab(debugger_ui)
session.stopped.connect(debugger_ui.clear)
session.set_meta("__state_charts_debugger_ui", debugger_ui)
debugger_ui.initialize(_settings, session)

View file

@ -0,0 +1,102 @@
## This is the remote part of the editor debugger. It attaches to a state
## chart similar to the in-game debugger and forwards signals and debug
## information to the editor.
extends Node
const DebuggerMessage = preload("editor_debugger_message.gd")
const SettingsPropagator = preload("editor_debugger_settings_propagator.gd")
# The scene tree, needed to interface with the settings propagator
var _tree:SceneTree
# the state chart we track
var _state_chart:StateChart
# whether to send transitions to the editor
var _ignore_transitions:bool = true
# whether to send events to the editor
var _ignore_events:bool = true
## Sets up the debugger remote to track the given state chart.
func _init(state_chart:StateChart):
if not is_instance_valid(state_chart):
push_error("Probable bug: State chart is not valid. Please report this bug.")
return
if not state_chart.is_inside_tree():
push_error("Probably bug: State chart is not in tree. Please report this bug.")
return
_state_chart = state_chart
func _ready():
# subscribe to settings changes coming from the editor debugger
# will auto-unsubscribe when the chart goes away. We do this before
# sending state_chart_added, so we get the initial settings update delivered
SettingsPropagator.get_instance(get_tree()).settings_updated.connect(_on_settings_updated)
# send initial state chart
DebuggerMessage.state_chart_added(_state_chart)
# prepare signals and send initial state of all states
_prepare()
func _on_settings_updated(chart:NodePath, ignore_events:bool, ignore_transitions:bool):
if _state_chart.get_path() != chart:
return # doesn't affect this chart
_ignore_events = ignore_events
_ignore_transitions = ignore_transitions
## Connects all signals from the currently processing state chart
func _prepare():
_state_chart.event_received.connect(_on_event_received)
# find all state nodes below the state chart and connect their signals
for child in _state_chart.get_children():
if child is StateChartState:
_prepare_state(child)
func _prepare_state(state:StateChartState):
state.state_entered.connect(_on_state_entered.bind(state))
state.state_exited.connect(_on_state_exited.bind(state))
state.transition_pending.connect(_on_transition_pending.bind(state))
# send initial state
DebuggerMessage.state_updated(_state_chart, state)
# recurse into children
for child in state.get_children():
if child is StateChartState:
_prepare_state(child)
if child is Transition:
child.taken.connect(_on_transition_taken.bind(state, child))
func _on_transition_taken(source:StateChartState, transition:Transition):
if _ignore_transitions:
return
DebuggerMessage.transition_taken(_state_chart, source, transition)
func _on_event_received(event:StringName):
if _ignore_events:
return
DebuggerMessage.event_received(_state_chart, event)
func _on_state_entered(state:StateChartState):
DebuggerMessage.state_entered(_state_chart, state)
func _on_state_exited(state:StateChartState):
DebuggerMessage.state_exited(_state_chart, state)
func _on_transition_pending(num1, remaining, state:StateChartState):
DebuggerMessage.transition_pending(_state_chart, state, state._pending_transition, remaining)

View file

@ -0,0 +1,54 @@
@tool
## This node receives messages from the editor debugger and forwards them to the state chart. It
## the reverse of EditorDebuggerMessage.
extends Node
const DebuggerMessage = preload("editor_debugger_message.gd")
const SETTINGS_UPDATED_MESSAGE = DebuggerMessage.MESSAGE_PREFIX + ":scsu"
const NAME = "StateChartEditorRemoteControl"
signal settings_updated(chart:NodePath, ignore_events:bool, ignore_transitions:bool)
static func get_instance(tree:SceneTree):
# because we add the node deferred, callers in the same frame would not see
# the node. Therefore we put it as metadata on the tree root. If we set the
# minimum to Godot 4.2, we can use a static var for this which will
# avoid this trickery.
var result = tree.root.get_meta(NAME) if tree.root.has_meta(NAME) else null
if not is_instance_valid(result):
# we want to avoid using class_name to avoid polluting the namespace with
# classes that are internals. we cannot use preload (cylic dependency)
# so we have to do this abomination
result = load("res://addons/godot_state_charts/utilities/editor_debugger/editor_debugger_settings_propagator.gd").new()
result.name = NAME
tree.root.set_meta(NAME, result)
tree.root.add_child.call_deferred(result)
return result
## Sends a settings updated message.
## session is an EditorDebuggerSession but this does not exist after export
## so its not statically typed here. This code won't run after export anyways.
static func send_settings_update(session, chart:NodePath, ignore_events:bool, ignore_transitions:bool) -> void:
session.send_message(SETTINGS_UPDATED_MESSAGE, [chart, ignore_events, ignore_transitions])
func _enter_tree():
# When the node enters the tree, register a message capture to receive
# settings updates from the editor.
EngineDebugger.register_message_capture(DebuggerMessage.MESSAGE_PREFIX, _on_settings_updated)
func _exit_tree():
# When the node exits the tree, shut down the message capture.
EngineDebugger.unregister_message_capture(DebuggerMessage.MESSAGE_PREFIX)
func _on_settings_updated(key:String, data:Array) -> bool:
# Inform interested parties that
settings_updated.emit(data[0], data[1], data[2])
# accept the message
return true

View file

@ -0,0 +1,104 @@
@tool
## Helper class for serializing/deserializing state information from the game
## into a format that can be used by the editor.
## State types that can be serialized
enum StateTypes {
AtomicState = 1,
CompoundState = 2,
ParallelState = 3,
AnimationPlayerState = 4,
AnimationTreeState = 5
}
## Create an array from the given state information.
static func make_array( \
## The owning chart
chart:NodePath, \
## Path of the state
path:NodePath, \
## Whether it is currently active
active:bool, \
## Whether a transition is currently pending for this state
transition_pending:bool, \
## The path of the pending transition if any.
transition_path:NodePath, \
## The remaining transition delay for the pending transition if any.
transition_delay:float, \
## The kind of state
state:StateChartState \
) -> Array:
return [ \
chart, \
path, \
active, \
transition_pending, \
transition_path, \
transition_delay, \
type_for_state(state) ]
## Get the state type for the given state.
static func type_for_state(state:StateChartState) -> StateTypes:
if state is CompoundState:
return StateTypes.CompoundState
elif state is ParallelState:
return StateTypes.ParallelState
elif state is AnimationPlayerState:
return StateTypes.AnimationPlayerState
elif state is AnimationTreeState:
return StateTypes.AnimationTreeState
else:
return StateTypes.AtomicState
## Accessors for the array.
static func get_chart(array:Array) -> NodePath:
return array[0]
static func get_state(array:Array) -> NodePath:
return array[1]
static func get_active(array:Array) -> bool:
return array[2]
static func get_transition_pending(array:Array) -> bool:
return array[3]
static func get_transition_path(array:Array) -> NodePath:
return array[4]
static func get_transition_delay(array:Array) -> float:
return array[5]
static func get_state_type(array:Array) -> StateTypes:
return array[6]
## Returns an icon for the state type of the given array.
static func get_state_icon(array:Array) -> Texture2D:
var type = get_state_type(array)
if type == StateTypes.AtomicState:
return preload("../../atomic_state.svg")
elif type == StateTypes.CompoundState:
return preload("../../compound_state.svg")
elif type == StateTypes.ParallelState:
return preload("../../parallel_state.svg")
elif type == StateTypes.AnimationPlayerState:
return preload("../../animation_player_state.svg")
elif type == StateTypes.AnimationTreeState:
return preload("../../animation_tree_state.svg")
else:
return null
static func set_active(array:Array, active:bool) -> void:
array[2] = active
# if no longer active, clear the pending transition
if not active:
array[3] = false
array[4] = null
array[5] = 0.0
static func set_transition_pending(array:Array, transition:NodePath, pending_time:float) -> void:
array[3] = true
array[4] = transition
array[5] = pending_time