initial commit: canny cat basic movement, bouncing, some gridmap tiles for levels
This commit is contained in:
commit
e1b43c8bc5
120 changed files with 5785 additions and 0 deletions
58
addons/godot_state_charts/utilities/debugger_history.gd
Normal file
58
addons/godot_state_charts/utilities/debugger_history.gd
Normal file
|
@ -0,0 +1,58 @@
|
|||
const RingBuffer = preload("ring_buffer.gd")
|
||||
|
||||
var _buffer:RingBuffer = null
|
||||
|
||||
var _dirty:bool = false
|
||||
|
||||
## Whether the history has changed since the full
|
||||
## history string was last requested.
|
||||
var dirty:bool:
|
||||
get: return _dirty
|
||||
|
||||
func _init(maximum_lines:int = 500):
|
||||
_buffer = RingBuffer.new(maximum_lines)
|
||||
_dirty = false
|
||||
|
||||
|
||||
## Sets the maximum number of lines to store in the history.
|
||||
## This will clear the history.
|
||||
func set_maximum_lines(maximum_lines:int) -> void:
|
||||
_buffer.set_maximum_lines(maximum_lines)
|
||||
|
||||
|
||||
## Adds an item to the history list.
|
||||
func add_history_entry(frame:int, text:String) -> void:
|
||||
_buffer.append("[%s]: %s \n" % [frame, text])
|
||||
_dirty = true
|
||||
|
||||
|
||||
## Adds a transition to the history list.
|
||||
func add_transition(frame:int, name:String, from:String, to:String) -> void:
|
||||
add_history_entry(frame, "[»] Transition: %s from %s to %s" % [name, from, to])
|
||||
|
||||
|
||||
## Adds an event to the history list.
|
||||
func add_event(frame:int, event:StringName) -> void:
|
||||
add_history_entry(frame, "[!] Event received: %s" % event)
|
||||
|
||||
|
||||
## Adds a state entered event to the history list.
|
||||
func add_state_entered(frame:int, name:StringName) -> void:
|
||||
add_history_entry(frame, "[>] Enter: %s" % name)
|
||||
|
||||
|
||||
## Adds a state exited event to the history list.
|
||||
func add_state_exited(frame:int, name:StringName) -> void:
|
||||
add_history_entry(frame, "[<] Exit: %s" % name)
|
||||
|
||||
|
||||
## Clears the history.
|
||||
func clear() -> void:
|
||||
_buffer.clear()
|
||||
_dirty = true
|
||||
|
||||
|
||||
## Returns the full history as a string.
|
||||
func get_history_text() -> String:
|
||||
_dirty = false
|
||||
return _buffer.join()
|
|
@ -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)
|
|
@ -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"]
|
|
@ -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])
|
||||
|
||||
|
||||
|
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
99
addons/godot_state_charts/utilities/editor_sidebar.gd
Normal file
99
addons/godot_state_charts/utilities/editor_sidebar.gd
Normal file
|
@ -0,0 +1,99 @@
|
|||
@tool
|
||||
extends Control
|
||||
|
||||
## Emitted when the user requests to toggle the sidebar.
|
||||
signal sidebar_toggle_requested()
|
||||
|
||||
## The currently selected node or null
|
||||
var _selected_node:Node
|
||||
## The editor interface
|
||||
var _editor_interface:EditorInterface
|
||||
## The undo/redo facility
|
||||
var _undo_redo:EditorUndoRedoManager
|
||||
|
||||
@onready var _add_section:Control = %AddSection
|
||||
@onready var _no_options_label:Control = %NoOptionsLabel
|
||||
@onready var _add_node_name_line_edit:LineEdit = %AddNodeNameLineEdit
|
||||
@onready var _add_grid_container:Control = %AddGridContainer
|
||||
|
||||
|
||||
func setup(editor_interface:EditorInterface, undo_redo:EditorUndoRedoManager):
|
||||
_editor_interface = editor_interface
|
||||
_undo_redo = undo_redo
|
||||
|
||||
|
||||
func change_selected_node(node):
|
||||
_selected_node = node
|
||||
_repaint()
|
||||
|
||||
func _repaint():
|
||||
# we can add states to all composite states and to the
|
||||
# root if the root has no child state yet.
|
||||
var can_add_states = \
|
||||
( _selected_node is StateChart and _selected_node.get_child_count() == 0 ) \
|
||||
or _selected_node is ParallelState \
|
||||
or _selected_node is CompoundState
|
||||
|
||||
# we can add transitions to all states
|
||||
var can_add_transitions = \
|
||||
_selected_node is StateChartState
|
||||
|
||||
_add_section.visible = can_add_states or can_add_transitions
|
||||
_no_options_label.visible = not (can_add_states or can_add_transitions)
|
||||
|
||||
|
||||
for btn in _add_grid_container.get_children():
|
||||
if btn.is_in_group("statebutton"):
|
||||
btn.visible = can_add_states
|
||||
else:
|
||||
btn.visible = can_add_transitions
|
||||
|
||||
|
||||
func _create_node(type, name:StringName):
|
||||
|
||||
var final_name = _add_node_name_line_edit.text.strip_edges()
|
||||
if final_name.length() == 0:
|
||||
final_name = name
|
||||
|
||||
var new_node = type.new()
|
||||
_undo_redo.create_action("Add " + final_name)
|
||||
_undo_redo.add_do_method(_selected_node, "add_child", new_node)
|
||||
_undo_redo.add_undo_method(_selected_node, "remove_child", new_node)
|
||||
_undo_redo.add_do_reference(new_node)
|
||||
_undo_redo.add_do_method(new_node, "set_owner", _selected_node.get_tree().edited_scene_root)
|
||||
_undo_redo.add_do_property(new_node, "name", final_name)
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
if Input.is_key_pressed(KEY_SHIFT):
|
||||
_editor_interface.get_selection().clear()
|
||||
_editor_interface.get_selection().add_node(new_node)
|
||||
|
||||
_add_node_name_line_edit.grab_focus()
|
||||
_editor_interface.edit_node(new_node)
|
||||
_repaint()
|
||||
|
||||
|
||||
|
||||
func _on_atomic_state_pressed():
|
||||
_create_node(AtomicState, "AtomicState")
|
||||
|
||||
|
||||
func _on_compound_state_pressed():
|
||||
_create_node(CompoundState, "CompoundState")
|
||||
|
||||
|
||||
func _on_parallel_state_pressed():
|
||||
_create_node(ParallelState, "ParallelState")
|
||||
|
||||
|
||||
func _on_history_state_pressed():
|
||||
_create_node(HistoryState, "HistoryState")
|
||||
|
||||
|
||||
func _on_transition_pressed():
|
||||
_create_node(Transition, "Transition")
|
||||
|
||||
|
||||
func _on_toggle_sidebar_button_pressed():
|
||||
sidebar_toggle_requested.emit()
|
117
addons/godot_state_charts/utilities/editor_sidebar.tscn
Normal file
117
addons/godot_state_charts/utilities/editor_sidebar.tscn
Normal file
|
@ -0,0 +1,117 @@
|
|||
[gd_scene load_steps=8 format=3 uid="uid://bephgxrkhh3e2"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/editor_sidebar.gd" id="1_7kcy8"]
|
||||
[ext_resource type="Texture2D" uid="uid://c4ojtah20jtxc" path="res://addons/godot_state_charts/atomic_state.svg" id="2_0k4pg"]
|
||||
[ext_resource type="Texture2D" uid="uid://bbudjoa3ds4qj" path="res://addons/godot_state_charts/compound_state.svg" id="3_b4okj"]
|
||||
[ext_resource type="Texture2D" uid="uid://dsa1nco51br8d" path="res://addons/godot_state_charts/parallel_state.svg" id="4_lmfic"]
|
||||
[ext_resource type="Texture2D" uid="uid://bkf1e240ouleb" path="res://addons/godot_state_charts/history_state.svg" id="5_oj1t0"]
|
||||
[ext_resource type="Texture2D" uid="uid://chb8tq62aj2b2" path="res://addons/godot_state_charts/transition.svg" id="6_72e5q"]
|
||||
[ext_resource type="Texture2D" uid="uid://vga3avpb4gyh" path="res://addons/godot_state_charts/toggle_sidebar.svg" id="9_dqcj0"]
|
||||
|
||||
[node name="EditorSidebar" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 4
|
||||
theme_override_constants/margin_top = 4
|
||||
theme_override_constants/margin_bottom = 4
|
||||
script = ExtResource("1_7kcy8")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="AddSection" type="VBoxContainer" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="AddLabel" type="Label" parent="VBoxContainer/AddSection"]
|
||||
layout_mode = 2
|
||||
text = "Add"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="AddNodeNameLineEdit" type="LineEdit" parent="VBoxContainer/AddSection"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
placeholder_text = "Name"
|
||||
alignment = 1
|
||||
expand_to_text_length = true
|
||||
select_all_on_focus = true
|
||||
caret_blink = true
|
||||
caret_blink_interval = 0.5
|
||||
|
||||
[node name="AddGridContainer" type="HFlowContainer" parent="VBoxContainer/AddSection"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_constants/h_separation = 5
|
||||
theme_override_constants/v_separation = 5
|
||||
alignment = 1
|
||||
|
||||
[node name="CompoundState" type="Button" parent="VBoxContainer/AddSection/AddGridContainer" groups=["statebutton"]]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 0
|
||||
tooltip_text = "CompoundState"
|
||||
icon = ExtResource("3_b4okj")
|
||||
icon_alignment = 1
|
||||
|
||||
[node name="ParallelState" type="Button" parent="VBoxContainer/AddSection/AddGridContainer" groups=["statebutton"]]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 0
|
||||
tooltip_text = "ParallelState"
|
||||
icon = ExtResource("4_lmfic")
|
||||
icon_alignment = 1
|
||||
|
||||
[node name="AtomicState" type="Button" parent="VBoxContainer/AddSection/AddGridContainer" groups=["statebutton"]]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 0
|
||||
tooltip_text = "AtomicState"
|
||||
icon = ExtResource("2_0k4pg")
|
||||
icon_alignment = 1
|
||||
|
||||
[node name="HistoryState" type="Button" parent="VBoxContainer/AddSection/AddGridContainer" groups=["statebutton"]]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 0
|
||||
tooltip_text = "HistoryState"
|
||||
icon = ExtResource("5_oj1t0")
|
||||
icon_alignment = 1
|
||||
|
||||
[node name="Transition" type="Button" parent="VBoxContainer/AddSection/AddGridContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 0
|
||||
tooltip_text = "Transition"
|
||||
icon = ExtResource("6_72e5q")
|
||||
icon_alignment = 1
|
||||
|
||||
[node name="NoOptionsLabel" type="Label" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 0)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
text = "This node cannot have further child nodes."
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="Spacer" type="Control" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ToggleSidebarButton" type="Button" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
tooltip_text = "Toggle sidebar location"
|
||||
icon = ExtResource("9_dqcj0")
|
||||
|
||||
[connection signal="pressed" from="VBoxContainer/AddSection/AddGridContainer/CompoundState" to="." method="_on_compound_state_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/AddSection/AddGridContainer/ParallelState" to="." method="_on_parallel_state_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/AddSection/AddGridContainer/AtomicState" to="." method="_on_atomic_state_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/AddSection/AddGridContainer/HistoryState" to="." method="_on_history_state_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/AddSection/AddGridContainer/Transition" to="." method="_on_transition_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/ToggleSidebarButton" to="." method="_on_toggle_sidebar_button_pressed"]
|
100
addons/godot_state_charts/utilities/event_editor/event_editor.gd
Normal file
100
addons/godot_state_charts/utilities/event_editor/event_editor.gd
Normal file
|
@ -0,0 +1,100 @@
|
|||
@tool
|
||||
extends EditorProperty
|
||||
|
||||
const StateChartUtil = preload("../state_chart_util.gd")
|
||||
var _refactor_window_scene:PackedScene = preload("../event_refactor/event_refactor.tscn")
|
||||
|
||||
|
||||
# The main control for editing the property.
|
||||
var _property_control:LineEdit = LineEdit.new()
|
||||
# drop down button for the popup menu
|
||||
var _dropdown_button:Button = Button.new()
|
||||
# popup menu with event names
|
||||
var _popup_menu:PopupMenu = PopupMenu.new()
|
||||
|
||||
# the state chart we are currently editing
|
||||
var _chart:StateChart
|
||||
# the undo redo manager
|
||||
var _undo_redo:EditorUndoRedoManager
|
||||
|
||||
|
||||
func _init(transition:Transition, undo_redo:EditorUndoRedoManager):
|
||||
|
||||
# save the variables
|
||||
_chart = StateChartUtil.find_parent_state_chart(transition)
|
||||
_undo_redo = undo_redo
|
||||
|
||||
# setup the ui
|
||||
_popup_menu.index_pressed.connect(_on_event_selected)
|
||||
|
||||
_dropdown_button.icon = get_theme_icon("arrow", "OptionButton")
|
||||
_dropdown_button.flat = true
|
||||
_dropdown_button.pressed.connect(_show_popup)
|
||||
|
||||
# build the actual editor
|
||||
var hbox := HBoxContainer.new()
|
||||
hbox.add_child(_property_control)
|
||||
hbox.add_child(_dropdown_button)
|
||||
_property_control.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
# Add the control as a direct child of EditorProperty node.
|
||||
add_child(hbox)
|
||||
add_child(_popup_menu)
|
||||
|
||||
# Make sure the control is able to retain the focus.
|
||||
add_focusable(_property_control)
|
||||
_property_control.text_changed.connect(_on_text_changed)
|
||||
|
||||
## Shows the popup when the user clicks the button.
|
||||
func _show_popup():
|
||||
# always show up-to-date information in selector
|
||||
var known_events = StateChartUtil.events_of(_chart)
|
||||
|
||||
_popup_menu.clear()
|
||||
_popup_menu.add_item("<empty>")
|
||||
_popup_menu.add_icon_item(get_theme_icon("Tools", "EditorIcons"), "Manage...")
|
||||
|
||||
if known_events.size() > 0:
|
||||
_popup_menu.add_separator()
|
||||
|
||||
for event in known_events:
|
||||
_popup_menu.add_item(event)
|
||||
|
||||
# and show it relative to the dropdown button
|
||||
var gt := _dropdown_button.get_global_rect()
|
||||
_popup_menu.reset_size()
|
||||
var ms := _popup_menu.get_contents_minimum_size().x
|
||||
var popup_pos := gt.end - Vector2(ms, 0) + Vector2(DisplayServer.window_get_position())
|
||||
_popup_menu.set_position(popup_pos)
|
||||
_popup_menu.popup()
|
||||
|
||||
|
||||
func _on_event_selected(index:int) -> void:
|
||||
# index 1 == "Manage"
|
||||
if index == 1:
|
||||
# open refactor window
|
||||
var window = _refactor_window_scene.instantiate()
|
||||
add_child(window)
|
||||
window.open(_chart, _undo_redo)
|
||||
return
|
||||
|
||||
# replace content with selection from popup
|
||||
var event := _popup_menu.get_item_text(index) if index > 0 else ""
|
||||
_property_control.text = event
|
||||
_on_text_changed(event)
|
||||
_property_control.grab_focus()
|
||||
|
||||
|
||||
func _on_text_changed(new_text:String):
|
||||
emit_changed(get_edited_property(), new_text)
|
||||
|
||||
|
||||
func _update_property() -> void:
|
||||
# Read the current value from the property.
|
||||
var new_value = get_edited_object()[get_edited_property()]
|
||||
|
||||
# if the text is already correct, don't change it.
|
||||
if new_value == _property_control.text:
|
||||
return
|
||||
|
||||
_property_control.text = new_value
|
|
@ -0,0 +1,29 @@
|
|||
@tool
|
||||
extends EditorInspectorPlugin
|
||||
|
||||
const EventEditor = preload("event_editor.gd")
|
||||
|
||||
var _undo_redo:EditorUndoRedoManager
|
||||
|
||||
|
||||
func setup(undo_redo:EditorUndoRedoManager):
|
||||
_undo_redo = undo_redo
|
||||
|
||||
|
||||
func _can_handle(_object):
|
||||
# We support all objects in this example.
|
||||
return true
|
||||
|
||||
|
||||
func _parse_property(object, type, name, _hint_type, _hint_string, _usage_flags, _wide):
|
||||
# We handle properties of type integer.
|
||||
if object is Transition and name == "event" and type == TYPE_STRING_NAME:
|
||||
# Create an instance of the custom property editor and register
|
||||
# it to a specific property path.
|
||||
var editor = EventEditor.new(object as Transition, _undo_redo)
|
||||
add_property_editor(name, editor)
|
||||
# Inform the editor to remove the default property editor for
|
||||
# this property type.
|
||||
return true
|
||||
else:
|
||||
return false
|
|
@ -0,0 +1,52 @@
|
|||
@tool
|
||||
|
||||
extends ConfirmationDialog
|
||||
|
||||
const StateChartUtil = preload("../state_chart_util.gd")
|
||||
|
||||
@onready var _event_list:ItemList = %EventList
|
||||
@onready var _event_name_edit:LineEdit = %EventNameEdit
|
||||
|
||||
var _chart:StateChart
|
||||
var _undo_redo:EditorUndoRedoManager
|
||||
var _current_event_name:StringName = ""
|
||||
|
||||
func open(chart:StateChart, undo_redo:EditorUndoRedoManager):
|
||||
title = "Events of " + chart.name
|
||||
_chart = chart
|
||||
_refresh_events()
|
||||
_undo_redo = undo_redo
|
||||
|
||||
|
||||
func _refresh_events():
|
||||
_event_list.clear()
|
||||
for item in StateChartUtil.events_of(_chart):
|
||||
_event_list.add_item(item)
|
||||
|
||||
func _close():
|
||||
hide()
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_event_list_item_selected(index:int):
|
||||
_current_event_name = _event_list.get_item_text(index)
|
||||
_event_name_edit.text = _current_event_name
|
||||
_on_event_name_edit_text_changed(_current_event_name)
|
||||
|
||||
|
||||
func _on_event_name_edit_text_changed(new_text):
|
||||
# disable rename button if the event name is the same as the
|
||||
# currently selected event
|
||||
get_ok_button().disabled = new_text == _current_event_name
|
||||
|
||||
|
||||
func _on_confirmed():
|
||||
var new_event_name = _event_name_edit.text
|
||||
var transitions = StateChartUtil.transitions_of(_chart)
|
||||
_undo_redo.create_action("Rename state chart event")
|
||||
for transition in transitions:
|
||||
if transition.event == _current_event_name:
|
||||
_undo_redo.add_do_property(transition, "event", new_event_name)
|
||||
_undo_redo.add_undo_property(transition, "event", _current_event_name)
|
||||
_undo_redo.commit_action()
|
||||
_close()
|
|
@ -0,0 +1,55 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://cvlabg8e2qbk3"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/event_refactor/event_refactor.gd" id="1_hh1x6"]
|
||||
|
||||
[node name="event_refactor" type="ConfirmationDialog"]
|
||||
initial_position = 1
|
||||
title = "Rename Event"
|
||||
size = Vector2i(586, 562)
|
||||
visible = true
|
||||
ok_button_text = "Rename"
|
||||
dialog_autowrap = true
|
||||
script = ExtResource("1_hh1x6")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = 578.0
|
||||
offset_bottom = 513.0
|
||||
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="MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Event"
|
||||
|
||||
[node name="EventList" type="ItemList" parent="MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(560, 330)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Label2" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "New name"
|
||||
|
||||
[node name="EventNameEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
caret_blink = true
|
||||
caret_blink_interval = 0.5
|
||||
|
||||
[connection signal="canceled" from="." to="." method="_close"]
|
||||
[connection signal="confirmed" from="." to="." method="_on_confirmed"]
|
||||
[connection signal="item_selected" from="MarginContainer/VBoxContainer/EventList" to="." method="_on_event_list_item_selected"]
|
||||
[connection signal="text_changed" from="MarginContainer/VBoxContainer/HBoxContainer/EventNameEdit" to="." method="_on_event_name_edit_text_changed"]
|
54
addons/godot_state_charts/utilities/ring_buffer.gd
Normal file
54
addons/godot_state_charts/utilities/ring_buffer.gd
Normal file
|
@ -0,0 +1,54 @@
|
|||
|
||||
|
||||
## The content of the ring buffer
|
||||
var _content:Array[String] = []
|
||||
|
||||
## The current index in the ring buffer
|
||||
var _index = 0
|
||||
|
||||
## The size of the ring buffer
|
||||
var _size = 0
|
||||
|
||||
## Whether the buffer is fully populated
|
||||
var _filled = false
|
||||
|
||||
|
||||
func _init(size:int = 300):
|
||||
_size = size
|
||||
_content.resize(size)
|
||||
|
||||
|
||||
## Sets the maximum number of lines to store. This clears the buffer.
|
||||
func set_maximum_lines(lines:int):
|
||||
_size = lines
|
||||
_content.resize(lines)
|
||||
clear()
|
||||
|
||||
## Adds an item to the ring buffer
|
||||
func append(value:String):
|
||||
_content[_index] = value
|
||||
if _index + 1 < _size:
|
||||
_index += 1
|
||||
else:
|
||||
_index = 0
|
||||
_filled = true
|
||||
|
||||
|
||||
## Joins the items of the ring buffer into a big string
|
||||
func join():
|
||||
var result = ""
|
||||
if _filled:
|
||||
# start by _index + 1, run to the end and then continue from the start
|
||||
for i in range(_index, _size):
|
||||
result += _content[i]
|
||||
|
||||
# when not filled, just start at the beginning
|
||||
for i in _index:
|
||||
result += _content[i]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
func clear():
|
||||
_index = 0
|
||||
_filled = false
|
280
addons/godot_state_charts/utilities/state_chart_debugger.gd
Normal file
280
addons/godot_state_charts/utilities/state_chart_debugger.gd
Normal file
|
@ -0,0 +1,280 @@
|
|||
@icon("state_chart_debugger.svg")
|
||||
extends Control
|
||||
|
||||
const DebuggerHistory = preload("debugger_history.gd")
|
||||
|
||||
## Whether or not the debugger is enabled.
|
||||
@export var enabled:bool = true:
|
||||
set(value):
|
||||
enabled = value
|
||||
if not Engine.is_editor_hint():
|
||||
_setup_processing(enabled)
|
||||
|
||||
|
||||
## The initial node that should be watched. Optional, if not set
|
||||
## then no node will be watched. You can set the node that should
|
||||
## be watched at runtime by calling debug_node().
|
||||
@export var initial_node_to_watch:NodePath
|
||||
|
||||
## Maximum lines to display in the history. Keep at 300 or below
|
||||
## for best performance.
|
||||
@export var maximum_lines:int = 300
|
||||
|
||||
## If set to true, events will not be printed in the history panel.
|
||||
## If you send a large amount of events then this may clutter the
|
||||
## output so you can disable it here.
|
||||
@export var ignore_events:bool = false
|
||||
|
||||
## If set to true, state changes will not be printed in the history
|
||||
## panel. If you have a large amount of state changes, this may clutter
|
||||
## the output so you can disable it here.
|
||||
@export var ignore_state_changes:bool = false
|
||||
|
||||
## If set to true, transitions will not be printed in the history.
|
||||
@export var ignore_transitions:bool = false
|
||||
|
||||
## The tree that shows the state chart.
|
||||
@onready var _tree:Tree = %Tree
|
||||
## The text field with the history.
|
||||
@onready var _history_edit:TextEdit = %HistoryEdit
|
||||
|
||||
# the state chart we track
|
||||
var _state_chart:StateChart
|
||||
var _root:Node
|
||||
|
||||
# the states we are currently connected to
|
||||
var _connected_states:Array[StateChartState] = []
|
||||
|
||||
# the transitions we are currently connected to
|
||||
# key is the transition, value is the callable
|
||||
var _connected_transitions:Dictionary = {}
|
||||
|
||||
# the debugger history in text form
|
||||
var _history:DebuggerHistory = null
|
||||
|
||||
func _ready():
|
||||
# always run, even if the game is paused
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
|
||||
# initialize the buffer
|
||||
_history = DebuggerHistory.new(maximum_lines)
|
||||
|
||||
%CopyToClipboardButton.pressed.connect(func (): DisplayServer.clipboard_set(_history_edit.text))
|
||||
%ClearButton.pressed.connect(_clear_history)
|
||||
|
||||
var to_watch = get_node_or_null(initial_node_to_watch)
|
||||
if is_instance_valid(to_watch):
|
||||
debug_node(to_watch)
|
||||
|
||||
# mirror the editor settings
|
||||
%IgnoreEventsCheckbox.set_pressed_no_signal(ignore_events)
|
||||
%IgnoreStateChangesCheckbox.set_pressed_no_signal(ignore_state_changes)
|
||||
%IgnoreTransitionsCheckbox.set_pressed_no_signal(ignore_transitions)
|
||||
|
||||
|
||||
|
||||
## Adds an item to the history list.
|
||||
func add_history_entry(text:String):
|
||||
_history.add_history_entry(Engine.get_process_frames(), text)
|
||||
|
||||
## Sets up the debugger to track the given state chart. If the given node is not
|
||||
## a state chart, it will search the children for a state chart. If no state chart
|
||||
## is found, the debugger will be disabled.
|
||||
func debug_node(root:Node) -> bool:
|
||||
# if we are not enabled, we do nothing
|
||||
if not enabled:
|
||||
return false
|
||||
|
||||
_root = root
|
||||
|
||||
# disconnect all existing signals
|
||||
_disconnect_all_signals()
|
||||
|
||||
var success = _debug_node(root)
|
||||
|
||||
|
||||
# if we have no success, we disable the debugger
|
||||
if not success:
|
||||
push_warning("No state chart found. Disabling debugger.")
|
||||
_setup_processing(false)
|
||||
_state_chart = null
|
||||
else:
|
||||
# find all state nodes below the state chart and connect their signals
|
||||
_connect_all_signals()
|
||||
_clear_history()
|
||||
_setup_processing(true)
|
||||
|
||||
return success
|
||||
|
||||
|
||||
func _debug_node(root:Node) -> bool:
|
||||
# if we have no root, we use the scene root
|
||||
if not is_instance_valid(root):
|
||||
return false
|
||||
|
||||
if root is StateChart:
|
||||
_state_chart = root
|
||||
return true
|
||||
|
||||
# no luck, search the children
|
||||
for child in root.get_children():
|
||||
if _debug_node(child):
|
||||
# found one, return
|
||||
return true
|
||||
|
||||
# no luck, return false
|
||||
return false
|
||||
|
||||
|
||||
func _setup_processing(enabled:bool):
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS if enabled else Node.PROCESS_MODE_DISABLED
|
||||
visible = enabled
|
||||
|
||||
|
||||
## Disconnects all signals from the currently connected states.
|
||||
func _disconnect_all_signals():
|
||||
if is_instance_valid(_state_chart):
|
||||
if not ignore_events:
|
||||
_state_chart.event_received.disconnect(_on_event_received)
|
||||
|
||||
for state in _connected_states:
|
||||
# in case the state has been destroyed meanwhile
|
||||
if is_instance_valid(state):
|
||||
state.state_entered.disconnect(_on_state_entered)
|
||||
state.state_exited.disconnect(_on_state_exited)
|
||||
|
||||
for transition in _connected_transitions.keys():
|
||||
# in case the transition has been destroyed meanwhile
|
||||
if is_instance_valid(transition):
|
||||
transition.taken.disconnect(_connected_transitions.get(transition))
|
||||
|
||||
|
||||
|
||||
## Connects all signals from the currently processing state chart
|
||||
func _connect_all_signals():
|
||||
_connected_states.clear()
|
||||
_connected_transitions.clear()
|
||||
|
||||
if not is_instance_valid(_state_chart):
|
||||
return
|
||||
|
||||
_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:
|
||||
_connect_signals(child)
|
||||
|
||||
|
||||
func _connect_signals(state:StateChartState):
|
||||
state.state_entered.connect(_on_state_entered.bind(state))
|
||||
state.state_exited.connect(_on_state_exited.bind(state))
|
||||
_connected_states.append(state)
|
||||
|
||||
# recurse into children
|
||||
for child in state.get_children():
|
||||
if child is StateChartState:
|
||||
_connect_signals(child)
|
||||
if child is Transition:
|
||||
var callable = _on_before_transition.bind(child, state)
|
||||
child.taken.connect(callable)
|
||||
_connected_transitions[child] = callable
|
||||
|
||||
|
||||
func _process(delta):
|
||||
# Clear contents
|
||||
_tree.clear()
|
||||
|
||||
if not is_instance_valid(_state_chart):
|
||||
return
|
||||
|
||||
var root = _tree.create_item()
|
||||
root.set_text(0, _root.name)
|
||||
|
||||
# walk over the state chart and find all active states
|
||||
_collect_active_states(_state_chart, root )
|
||||
|
||||
# also show the values of all variables
|
||||
var items = _state_chart._expression_properties.keys()
|
||||
|
||||
if items.size() <= 0:
|
||||
return # nothing to show
|
||||
|
||||
# sort by name so it doesn't flicker all the time
|
||||
items.sort()
|
||||
|
||||
var properties_root = root.create_child()
|
||||
properties_root.set_text(0, "< Expression properties >")
|
||||
|
||||
for item in items:
|
||||
var value = str(_state_chart._expression_properties.get(item))
|
||||
|
||||
var property_line = properties_root.create_child()
|
||||
property_line.set_text(0, "%s = %s" % [item, value])
|
||||
|
||||
|
||||
func _collect_active_states(root:Node, parent:TreeItem):
|
||||
for child in root.get_children():
|
||||
if child is StateChartState:
|
||||
if child.active:
|
||||
var state_item:TreeItem = _tree.create_item(parent)
|
||||
state_item.set_text(0, child.name)
|
||||
|
||||
if is_instance_valid(child._pending_transition):
|
||||
var transition_item: TreeItem = state_item.create_child()
|
||||
transition_item.set_text(0, ">> %s (%.2f)" % [child._pending_transition.name, child._pending_transition_remaining_delay])
|
||||
|
||||
_collect_active_states(child, state_item)
|
||||
|
||||
|
||||
func _clear_history():
|
||||
_history_edit.text = ""
|
||||
_history.clear()
|
||||
|
||||
func _on_before_transition(transition:Transition, source:StateChartState):
|
||||
if ignore_transitions:
|
||||
return
|
||||
|
||||
_history.add_transition(Engine.get_process_frames(), transition.name, _state_chart.get_path_to(source), _state_chart.get_path_to(transition.resolve_target()))
|
||||
|
||||
|
||||
func _on_event_received(event:StringName):
|
||||
if ignore_events:
|
||||
return
|
||||
|
||||
_history.add_event(Engine.get_process_frames(), event)
|
||||
|
||||
|
||||
func _on_state_entered(state:StateChartState):
|
||||
if ignore_state_changes:
|
||||
return
|
||||
|
||||
_history.add_state_entered(Engine.get_process_frames(), state.name)
|
||||
|
||||
|
||||
func _on_state_exited(state:StateChartState):
|
||||
if ignore_state_changes:
|
||||
return
|
||||
|
||||
_history.add_state_exited(Engine.get_process_frames(), state.name)
|
||||
|
||||
|
||||
func _on_timer_timeout():
|
||||
# ignore the timer if the history edit isn't visible
|
||||
if not _history_edit.visible or not _history.dirty:
|
||||
return
|
||||
|
||||
# fill the history field
|
||||
_history_edit.text = _history.get_history_text()
|
||||
_history_edit.scroll_vertical = _history_edit.get_line_count() - 1
|
||||
|
||||
|
||||
func _on_ignore_events_checkbox_toggled(button_pressed):
|
||||
ignore_events = button_pressed
|
||||
|
||||
|
||||
func _on_ignore_state_changes_checkbox_toggled(button_pressed):
|
||||
ignore_state_changes = button_pressed
|
||||
|
||||
func _on_ignore_transitions_checkbox_toggled(button_pressed):
|
||||
ignore_transitions = button_pressed
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#8eef97" d="M 3 0 C 1.3380034 0 0 1.3380034 0 3 L 0 13 C 0 14.661996 1.3380034 16 3 16 L 13 16 C 14.661996 16 16 14.661996 16 13 L 16 3 C 16 1.3380034 14.661996 0 13 0 L 3 0 z M 3 1 L 13 1 C 14.107998 1 15 1.8920022 15 3 L 15 13 C 15 14.107998 14.107998 15 13 15 L 3 15 C 1.8920022 15 1 14.107998 1 13 L 1 3 C 1 1.8920022 1.8920022 1 3 1 z M 7 3 L 7 4 L 6 4 L 6 5 L 10 5 L 10 4 L 9 4 L 9 3 L 7 3 z M 2 5 L 2 6 L 3 6 L 3 5 L 2 5 z M 3 6 L 3 7 L 4 7 L 4 6 L 3 6 z M 13 5 L 13 6 L 14 6 L 14 5 L 13 5 z M 13 6 L 12 6 L 12 7 L 13 7 L 13 6 z M 6 6 L 6 7 L 5 7 L 5 11 L 6 11 L 6 12 L 10 12 L 10 11 L 11 11 L 11 7 L 10 7 L 10 6 L 6 6 z M 2 8 L 2 9 L 4 9 L 4 8 L 2 8 z M 12 8 L 12 9 L 14 9 L 14 8 L 12 8 z M 3 10 L 3 11 L 4 11 L 4 10 L 3 10 z M 3 11 L 2 11 L 2 12 L 3 12 L 3 11 z M 12 10 L 12 11 L 13 11 L 13 10 L 12 10 z M 13 11 L 13 12 L 14 12 L 14 11 L 13 11 z "/></svg>
|
After Width: | Height: | Size: 939 B |
|
@ -0,0 +1,38 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bnsri4fr2bbu0"
|
||||
path="res://.godot/imported/state_chart_debugger.svg-84b90904efaf4dffb8ff9ef4bed14dd2.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/godot_state_charts/utilities/state_chart_debugger.svg"
|
||||
dest_files=["res://.godot/imported/state_chart_debugger.svg-84b90904efaf4dffb8ff9ef4bed14dd2.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=false
|
|
@ -0,0 +1,97 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://bcwkugn6v3oy7"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/state_chart_debugger.gd" id="1_i74os"]
|
||||
|
||||
[node name="StateChartDebugger" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_i74os")
|
||||
maximum_lines = 50
|
||||
|
||||
[node name="TabContainer" type="TabContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="StateChart" type="MarginContainer" parent="TabContainer"]
|
||||
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="Tree" type="Tree" parent="TabContainer/StateChart"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
scroll_horizontal_enabled = false
|
||||
scroll_vertical_enabled = false
|
||||
|
||||
[node name="History" type="MarginContainer" parent="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="TabContainer/History"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="HistoryEdit" type="TextEdit" parent="TabContainer/History/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="TabContainer/History/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ClearButton" type="Button" parent="TabContainer/History/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Clear"
|
||||
|
||||
[node name="CopyToClipboardButton" type="Button" parent="TabContainer/History/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Copy to Clipboard"
|
||||
|
||||
[node name="Settings" type="MarginContainer" parent="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="TabContainer/Settings"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="IgnoreEventsCheckbox" type="CheckBox" parent="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="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="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="Timer" type="Timer" parent="."]
|
||||
wait_time = 0.5
|
||||
autostart = true
|
||||
|
||||
[connection signal="toggled" from="TabContainer/Settings/VBoxContainer/IgnoreEventsCheckbox" to="." method="_on_ignore_events_checkbox_toggled"]
|
||||
[connection signal="toggled" from="TabContainer/Settings/VBoxContainer/IgnoreStateChangesCheckbox" to="." method="_on_ignore_state_changes_checkbox_toggled"]
|
||||
[connection signal="toggled" from="TabContainer/Settings/VBoxContainer/IgnoreTransitionsCheckbox" to="." method="_on_ignore_transitions_checkbox_toggled"]
|
||||
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]
|
49
addons/godot_state_charts/utilities/state_chart_util.gd
Normal file
49
addons/godot_state_charts/utilities/state_chart_util.gd
Normal file
|
@ -0,0 +1,49 @@
|
|||
@tool
|
||||
|
||||
## Finds the first ancestor of the given node which is a state
|
||||
## chart. Returns null when none is found.
|
||||
static func find_parent_state_chart(node:Node) -> StateChart:
|
||||
if node is StateChart:
|
||||
return node
|
||||
|
||||
var parent = node.get_parent()
|
||||
while parent != null:
|
||||
if parent is StateChart:
|
||||
return parent
|
||||
|
||||
parent = parent.get_parent()
|
||||
return null
|
||||
|
||||
|
||||
## Returns an array with all event names currently used in the given
|
||||
## state chart.
|
||||
static func events_of(chart:StateChart) -> Array[StringName]:
|
||||
var result:Array[StringName] = []
|
||||
# now collect all events below the state chart
|
||||
_collect_events(chart, result)
|
||||
result.sort_custom(func(a, b): return a.naturalnocasecmp_to(b) < 0)
|
||||
return result
|
||||
|
||||
|
||||
static func _collect_events(node:Node, events:Array[StringName]):
|
||||
if node is Transition:
|
||||
if node.event != "" and not events.has(node.event):
|
||||
events.append(node.event)
|
||||
|
||||
for child in node.get_children():
|
||||
_collect_events(child, events)
|
||||
|
||||
|
||||
## Returns all transitions of the given state chart.
|
||||
static func transitions_of(chart:StateChart) -> Array[Transition]:
|
||||
var result:Array[Transition] = []
|
||||
_collect_transitions(chart, result)
|
||||
return result
|
||||
|
||||
|
||||
static func _collect_transitions(node:Node, result:Array[Transition]):
|
||||
if node is Transition:
|
||||
result.append(node)
|
||||
|
||||
for child in node.get_children():
|
||||
_collect_transitions(child, result)
|
Loading…
Add table
Add a link
Reference in a new issue