Major clean up and reorganization

- Upgraded to Godot 4
- Just remembered the basic principles are based on a tile editor, and dramatically simplified from there. Derp.
- New state machine and license display add-ons.
- Re-licensed under the GPL because Micropolis' assets aren't under a separate one.
This commit is contained in:
Tony Bark 2023-03-14 06:17:27 -04:00
parent 55ed76c914
commit c980445340
337 changed files with 5129 additions and 7661 deletions

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Addons By Aura
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,47 @@
@icon("../icons/animation_state.png")
class_name AnimationState
extends State
## Plays an animation from the linked [member animation_player].
## The name of the animation to be played comes from the name of the node.
## Emitted when the animation started by this state has finished playing.
signal animation_finished
@export_range(0, 20, 1, "or_greater")
## How many times to play before emitting [signal State.choose_new_substate_requested].
## [b]If set to zero, it will go forever.[/b]
var loops := 0
var _loops_left := 0
func _init() -> void:
set_meta(&"description", "Plays the named animation from the linked AnimationPlayer.")
func _on_animation_finished(animation_name: StringName) -> void:
if animation_name != name: return
if loops == 0:
animation_player.play(name)
elif _loops_left <= 0:
choose_new_substate_requested.emit()
else:
_loops_left -= 1
animation_player.play(name)
func enter(set_target: Node, set_animation_player: AnimationPlayer, set_debug_mode := false) -> void:
super(set_target, set_animation_player, set_debug_mode)
assert(animation_player != null, "AnimationPlayer must be set, either directly or by an ancestor.")
animation_player.animation_finished.connect(_on_animation_finished)
_loops_left = loops - 1
animation_player.play(name)
func exit() -> void:
super()
animation_player.animation_finished.disconnect(_on_animation_finished)
animation_player.stop()

View file

@ -0,0 +1,139 @@
@icon("../icons/state_machine_debugger.png")
class_name StateMachineDebugger
extends Tree
## Displays an interactive state tree.
# This source code is a mess, I'm trying to make it less so.
@export
## Root state machine to reference.
var state_machine_root : State:
set(value):
state_machine_root = value
_setup_tree()
@export
## What color to make the item when a state is active.
var active_color := Color.FOREST_GREEN
@export
## Forcefully switch states by double-clicking them.
## Due to its nature, it has the potential to be destructive
## and/ or not behave completely how one might expect.
var allow_state_switching := false:
set(value):
allow_state_switching = value
if allow_state_switching:
item_activated.connect(_on_item_activated)
else:
item_activated.disconnect(_on_item_activated)
@export_group("Signals", "signal_")
@export
## Show when a state emits a relevant signal.
var signal_show := false:
set(value):
signal_show = value
if state_machine_root == null:
return
if signal_show:
connect_signals()
else:
disconnect_signals()
@export
## Which signals to connect to on each state, as long as they exist.
var signal_connections : Array[StringName] = [
&"entered",
&"exited",
&"choose_new_substate_requested",
&"animation_finished",
]
@export
## Delay before hiding signal.
var signal_hide_delay := 1.0
func _init() -> void:
columns = 2
func change_state_by_path(path: NodePath) -> void:
if not state_machine_root.has_node(path):
return
var state := state_machine_root
for i in path.get_name_count():
var part := path.get_name(i)
state = await state.change_state_name(part)
func connect_signals(state := state_machine_root) -> void:
if not state.has_meta(&"tree_item"):
return
for signal_name in signal_connections:
if state.has_signal(signal_name) and not \
state.is_connected(signal_name, _on_state_signal):
state.connect(signal_name, _on_state_signal.bind(signal_name, state.get_meta(&"tree_item")))
for child in(state.get_children() as Array[State]):
connect_signals(child)
func disconnect_signals(state := state_machine_root) -> void:
for signal_name in signal_connections:
if state.has_signal(signal_name) and \
state.is_connected(signal_name, _on_state_signal):
state.disconnect(signal_name, _on_state_signal)
for child in (state.get_children() as Array[State]):
disconnect_signals(child)
func _setup_tree(state := state_machine_root, parent_item: TreeItem = null) -> void:
if state == state_machine_root:
if get_root() != null:
disconnect_signals()
clear()
if state_machine_root == null:
return
# state.print_tree_pretty()
# TODO: add icons
var item := create_item(parent_item)
item.set_text(0, state.name)
item.set_metadata(0, state)
state.set_meta(&"tree_item", item)
connect_signals(state)
for child in (state.get_children() as Array[State]):
_setup_tree(child, item)
func _on_item_activated() -> void:
change_state_by_path(state_machine_root.get_path_to(
get_selected().get_metadata(0) as State))
func _on_state_signal(signal_name: StringName, state_item: TreeItem) -> void:
match signal_name:
&"entered":
for i in columns:
state_item.set_custom_color(i, active_color)
&"exited":
for i in columns:
state_item.clear_custom_color(i)
if not signal_show:
return
state_item.set_text(1, signal_name)
var timer := state_item.get_metadata(1) as SceneTreeTimer
if timer != null:
timer.timeout.disconnect(state_item.set_text)
timer = get_tree().create_timer(signal_hide_delay)
timer.timeout.connect(state_item.set_text.bind(1, ""))
state_item.set_metadata(1, timer)

View file

@ -0,0 +1,34 @@
@icon("../icons/random_state.png")
class_name RandomState
extends State
## Activates a random one of its substates.
## Useful in conjuction with [AnimationState] for random idles.
@export
## When one of its children asks for a state change,
## instead of picking another one itself, it defers that choice to its parent.
## Allows for nested random states for finer control over flow and probability.
var defer_choice := false
func _init() -> void:
set_meta(&"description", "Pseudo-randomly picks a state to start.")
func _ready() -> void:
randomize()
super()
# You can define which state is picked automatically (like on enter).
# If you would like to call it yourself, use the public version (choose_substate).
func _choose_substate() -> State:
if get_child_count() == 0:
return null
if defer_choice and _active_substate != null:
choose_new_substate_requested.emit()
return null
return get_child(randi() % get_child_count()) as State

View file

@ -0,0 +1,43 @@
@icon("../icons/sequence_state.png")
class_name SequenceState
extends State
## Executes its children in order, one after the other. Like an [Array] in [State] form!
@export_range(0, 20, 1, "or_greater")
## How many times the sequence should be looped through before emitting [signal State.choose_new_substate_requested].
## [b]If set to zero, it will go forever.[/b]
var loops := 1
var _loops_left := 0
func _init() -> void:
set_meta(&"description", "Starts its children one after the other in order, \
waiting for each one to be done before starting the next.")
# You can define which state is picked automatically (like on [method enter]).
# If you would like to call it yourself, use the public version ([method choose_substate]).
func _choose_substate() -> State:
if _active_substate == null:
return get_child(0) as State if get_child_count() > 0 else null
if _active_substate.get_index() == get_child_count() - 1:
if loops == 0:
return get_child(0) as State
elif _loops_left == 0:
choose_new_substate_requested.emit()
return null
else:
_loops_left -= 1
return get_child(0) as State
return get_child(_active_substate.get_index() + 1) as State
func enter(set_target: Node, set_animation_player: AnimationPlayer, set_debug := false) -> void:
super(set_target, set_animation_player, set_debug)
_loops_left = loops - 1

View file

@ -0,0 +1,291 @@
@icon("../icons/state.png")
class_name State
extends Node
## The bare, basic state. Use it if you want total control over the state-flow.
##
## Properties marked as [b](inherited)[/b] are passed to substates,
## meaning you don't have to set it on each individual state, only the root.
## You can override it of course, and that will be passed to all of [i]its[/i] children.
## Emitted between [method _enter] and [method _after_enter].
signal entered
## Emitted after [method _exit].
signal exited
## Emitted between [method _update] and [method _after_update]
signal updated
## Switched active substates.
signal active_substate_changed(new: State, old: State)
## A request for the parent to pick a new substate to activate.
## Mainly used by children of [RandomState], such as an [AnimationState].
signal choose_new_substate_requested
## Active or not.
enum Status {
INACTIVE, ## Inactive
ACTIVE, ## Active
}
@export
## The node that the states will act upon. [b](inherited)[/b]
## Doesn't actually get used in the addon scripts, it's just
## included for your convenience when scripting your own behaviour.
var target: Node:
set(value):
target = value
if _active_substate != null:
_active_substate.target = target
@export
## Where to play animations from. [b](inherited)[/b]
var animation_player: AnimationPlayer
@export_range(0, 120, 1, "or_greater")
## How many seconds the state should be active before emitting [signal choose_new_substate_requested].
## [b]If set to zero, it will go forever.[/b]
var timer := 0.0
@export
## Whether to force-restart the chosen substate in the callback for [signal choose_new_substate_requested] if it was already active.
var force := true
@export
## The state will not be activated under any circumstances.
var disabled := false:
set(value):
disabled = value
var root := is_root()
if root and not disabled:
enter(target, animation_player, debug_mode)
elif status == Status.ACTIVE:
exit()
@export
## Print a message avery time there is a state change. [b](inherited)[/b]
var debug_mode := false:
set(value):
debug_mode = value
if _active_substate != null:
_active_substate.debug_mode = debug_mode
## The status of this state, ie. whether it's running or not.
var status := Status.INACTIVE
# The substate that is currently active, if any.
var _active_substate: State:
set(value):
if _active_substate != null:
_active_substate.choose_new_substate_requested.disconnect(_on_choose_new_substate_requested)
active_substate_changed.emit(value, _active_substate)
_active_substate = value
if _active_substate != null:
_active_substate.choose_new_substate_requested.connect(_on_choose_new_substate_requested)
# If a timer is set, the object will be stored here.
var _timer_object: SceneTreeTimer
#########################
### VIRTUAL METHODS ###
#########################
func _init() -> void:
set_physics_process(false)
set_meta(&"description", "A bare, basic state - will only ever automatically start its first child.")
func _ready() -> void:
for child in get_children():
assert(child is State, "A State should not have any children that are not other States.")
if is_root() and not disabled:
enter(target, animation_player, debug_mode)
func _physics_process(delta: float) -> void:
if status == Status.INACTIVE:
set_physics_process(false)
return
update(delta)
## [b][parents, then children][/b] Called when the state is activated.
func _enter() -> void:
pass
## [b][children, then parents][/b] Called after the state is activated.
func _after_enter() -> void:
pass
## [b][parents, then children][/b] Called every physics frame (only when the state is active, of course).
func _update(delta: float) -> void:
pass
## [b][children, then parents][/b] Called at the end of every physics frame.
func _after_update(delta: float) -> void:
pass
## [b][parents, then children][/b] Called before the state is deactivated.
func _before_exit() -> void:
pass
## [b][children, then parents][/b] Called when the state is deactivated.
func _exit() -> void:
pass
## You can define which state is picked automatically (like on [method enter]).
## Return `null` to not change substate at all.
## If you would like to call it yourself, use the public version ([method choose_substate]).
func _choose_substate() -> State:
return get_child(0) as State if get_child_count() > 0 else null
########################
### PUBLIC METHODS ###
########################
## Switch to the specified substate by name. It is just a shortcut to [method change_state_node].
func change_state_name(name: String, force := false) -> State:
return await change_state_node(get_node_or_null(name) as State, force)
## Switch to the specified substate by node. If it is not a direct child, nothing will happen.
## If `force`, it will start a state again even if it's already running.
## It waits for the next [signal updated] to make sure it's not
## switching all over the place in one tick.
func change_state_node(node: State, force := false) -> State:
await updated
if (
node == null
or node.disabled
or (node.status != Status.INACTIVE and not force)
or node.get_parent() != self
):
return node
var old := _active_substate
_active_substate = node
if old != null:
old.exit()
_active_substate.enter(target, animation_player, debug_mode)
if debug_mode:
print(
("FORCE " if force else "") +
"STATE: " +
str(get_root().get_parent().get_path_to(_active_substate))
)
return _active_substate
## Return the currently active substate, if any.
func get_active_substate() -> State:
return _active_substate
## Public [method _choose_substate].
func choose_substate() -> State:
return _choose_substate()
## Shortcut for `change_state_node(choose_substate())`.
func change_to_next_substate(force := false) -> void:
await change_state_node(choose_substate(), force)
## Whether this state is the root of the state tree,
## ie. it is the common ancestor of all the others.
func is_root() -> bool:
# If your parent is not a state, then you are the root.
return not get_parent() is State
## Get the root state.
func get_root() -> State:
var node: State = self
while not node.is_root():
node = node.get_parent() as State
return node
## Runs [method _enter] and [method _after_enter],
## not a good idea to call it yourself unless you really know what you're doing.
func enter(set_target: Node, set_animation_player: AnimationPlayer, set_debug_mode: bool) -> void:
for child in get_children():
assert(child is State, "A State should not have any children that are not other States.")
_enter()
entered.emit()
status = Status.ACTIVE
if timer != 0:
_timer_object = get_tree().create_timer(timer)
_timer_object.timeout.connect(_on_timer_timeout)
set_physics_process(is_root())
# Only set them if they're not being overridden
if target == null:
target = set_target
if animation_player == null:
animation_player = set_animation_player
if debug_mode == false:
debug_mode = set_debug_mode
change_to_next_substate()
_after_enter()
## Runs [method _update] and [method _after_update],
## not a good idea to call it yourself unless you really know what you're doing.
func update(delta: float) -> void:
_update(delta)
updated.emit()
if _active_substate != null:
_active_substate.update(delta)
_after_update(delta)
## Runs [method _exit] and [method _before_exit],
## not a good idea to call it yourself unless you really know what you're doing.
func exit() -> void:
_before_exit()
status = Status.INACTIVE
if _active_substate != null:
_active_substate.exit()
_active_substate = null
if is_instance_valid(_timer_object):
_timer_object.timeout.disconnect(_on_timer_timeout)
_timer_object = null
_exit()
exited.emit()
set_physics_process(false)
#########################
### PRIVATE METHODS ###
#########################
#################
### CALLBACKS ###
#################
func _on_choose_new_substate_requested() -> void:
change_to_next_substate(force)
func _on_timer_timeout() -> void:
choose_new_substate_requested.emit()

View file

@ -0,0 +1,14 @@
extends State
const NEXT_STATE_ACTION = "demo_next_state"
func _enter() -> void:
if InputMap.has_action(NEXT_STATE_ACTION):
return
var input_event := InputEventKey.new()
input_event.keycode = KEY_TAB
InputMap.add_action(NEXT_STATE_ACTION)
InputMap.action_add_event(NEXT_STATE_ACTION, input_event)

View file

@ -0,0 +1,218 @@
[gd_scene load_steps=17 format=3 uid="uid://clnliyc6fmqy6"]
[ext_resource type="Script" path="res://addons/simple-state/classes/debugger.gd" id="1_rqf1w"]
[ext_resource type="Script" path="res://addons/simple-state/demo/description_box.gd" id="2_gktik"]
[ext_resource type="Script" path="res://addons/simple-state/classes/sequence_state.gd" id="3_4afa7"]
[ext_resource type="Script" path="res://addons/simple-state/demo/demo.gd" id="3_x0hcs"]
[ext_resource type="Script" path="res://addons/simple-state/classes/random_state.gd" id="4_wxjoe"]
[ext_resource type="Script" path="res://addons/simple-state/demo/emit_next.gd" id="6_kxcgl"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ysqm7"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_wxupu"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7gdgn"]
draw_center = false
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
[sub_resource type="Animation" id="Animation_5pgem"]
resource_name = "RESET"
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("ColorRect:color")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 1)]
}
[sub_resource type="Animation" id="Animation_mwhj1"]
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("ColorRect:color")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(0, 0, 0, 1)]
}
[sub_resource type="Animation" id="Animation_yyph0"]
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("ColorRect:color")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(0, 0, 1, 1)]
}
[sub_resource type="Animation" id="Animation_xmkhy"]
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("ColorRect:color")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(0, 1, 0, 1)]
}
[sub_resource type="Animation" id="Animation_4pwkk"]
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("ColorRect:color")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 0, 0, 1)]
}
[sub_resource type="Animation" id="Animation_gt7hj"]
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("ColorRect:color")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 0, 1)]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_01hf1"]
_data = {
"RESET": SubResource("Animation_5pgem"),
"black": SubResource("Animation_mwhj1"),
"blue": SubResource("Animation_yyph0"),
"green": SubResource("Animation_xmkhy"),
"red": SubResource("Animation_4pwkk"),
"yellow": SubResource("Animation_gt7hj")
}
[node name="SimpleStateDemo" type="Panel"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="StateMachineDebugger" type="Tree" parent="MarginContainer/HBoxContainer" node_paths=PackedStringArray("state_machine_root")]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/relationship_line_width = 2
theme_override_constants/draw_guides = 0
theme_override_styles/panel = SubResource("StyleBoxEmpty_ysqm7")
theme_override_styles/focus = SubResource("StyleBoxEmpty_wxupu")
theme_override_styles/selected = SubResource("StyleBoxFlat_7gdgn")
theme_override_styles/selected_focus = SubResource("StyleBoxFlat_7gdgn")
select_mode = 1
script = ExtResource("1_rqf1w")
state_machine_root = NodePath("../../Root")
allow_state_switching = true
signal_show = true
[node name="DescriptionBox" type="Label" parent="MarginContainer/HBoxContainer" node_paths=PackedStringArray("tree")]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
autowrap_mode = 3
script = ExtResource("2_gktik")
tree = NodePath("../StateMachineDebugger")
show_descriptions = 2
[node name="InputInfo" type="Label" parent="MarginContainer/HBoxContainer/DescriptionBox"]
layout_mode = 1
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -349.0
offset_top = -78.0
grow_horizontal = 0
grow_vertical = 0
text = "MOUSE CLICK on state: select
DOUBLE MOUSE CLICK on state: start
TAB: next substate (only has effect on leaves)"
horizontal_alignment = 2
vertical_alignment = 2
[node name="ColorRect" type="ColorRect" parent="MarginContainer"]
custom_minimum_size = Vector2(50, 50)
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 8
[node name="AnimationPlayer" type="AnimationPlayer" parent="MarginContainer"]
libraries = {
"": SubResource("AnimationLibrary_01hf1")
}
[node name="Root" type="Node" parent="MarginContainer" node_paths=PackedStringArray("target", "animation_player")]
script = ExtResource("3_x0hcs")
target = NodePath("")
animation_player = NodePath("../AnimationPlayer")
[node name="SequenceState" type="Node" parent="MarginContainer/Root"]
script = ExtResource("3_4afa7")
[node name="RandomState" type="Node" parent="MarginContainer/Root/SequenceState"]
script = ExtResource("4_wxjoe")
defer_choice = true
[node name="red" type="Node" parent="MarginContainer/Root/SequenceState/RandomState"]
script = ExtResource("6_kxcgl")
[node name="yellow" type="Node" parent="MarginContainer/Root/SequenceState/RandomState"]
script = ExtResource("6_kxcgl")
[node name="green" type="Node" parent="MarginContainer/Root/SequenceState"]
script = ExtResource("6_kxcgl")
[node name="blue" type="Node" parent="MarginContainer/Root/SequenceState"]
script = ExtResource("6_kxcgl")
[node name="black" type="Node" parent="MarginContainer/Root"]
script = ExtResource("6_kxcgl")
timer = 1.0
metadata/description = "Will never start automatically, due to its parent being only a normal state."

View file

@ -0,0 +1,98 @@
extends Label
## Mode of description box rendering.
enum DisplayModes {
NONE, ## Description box completely hidden.
ACTIVE, ## Show descriptions of all active states.
SELECTION, ## Show description of last selected state (also includes manual switches).
}
@export
## [StateMachineDebugger] to reference.
var tree : StateMachineDebugger:
set(value):
tree = value
if show_descriptions == DisplayModes.SELECTION and \
not tree.item_selected.is_connected(_on_tree_item_selected):
tree.item_selected.connect(_on_tree_item_selected)
@export
## Show a description of a state.
## Looks for a string metadata value by the name of [code]description[/code] on each state.
var show_descriptions := DisplayModes.NONE:
set(value):
show_descriptions = value
match show_descriptions:
DisplayModes.NONE:
visible = false
DisplayModes.ACTIVE:
visible = true
if not is_instance_valid(tree):
return
if tree.item_selected.is_connected(_on_tree_item_selected):
tree.item_selected.disconnect(_on_tree_item_selected)
DisplayModes.SELECTION:
visible = true
if not is_instance_valid(tree):
return
if not tree.item_selected.is_connected(_on_tree_item_selected):
tree.item_selected.connect(_on_tree_item_selected)
var _active_states : Array[State] = []
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
visible = show_descriptions != DisplayModes.NONE
connect_signals()
func connect_signals(state := tree.state_machine_root) -> void:
if not state.has_meta(&"tree_item"):
return
for signal_name in tree.signal_connections:
if state.has_signal(signal_name) and not \
state.is_connected(signal_name, _on_state_signal):
state.connect(signal_name, _on_state_signal.bind(signal_name, state))
for child in(state.get_children() as Array[State]):
connect_signals(child)
func disconnect_signals(state := tree.state_machine_root) -> void:
for signal_name in tree.signal_connections:
if state.has_signal(signal_name) and \
state.is_connected(signal_name, _on_state_signal):
state.disconnect(signal_name, _on_state_signal)
for child in (state.get_children() as Array[State]):
disconnect_signals(child)
func _set_description_from_active_states() -> void:
text = ""
for state in _active_states:
text += ("" if state.is_root() else "\n\n") + \
state.name as String + \
": " + \
state.get_meta(&"description", "") as String
func _on_tree_item_selected() -> void:
text = tree.get_selected() \
.get_metadata(0).get_meta(&"description", "")
func _on_state_signal(signal_name: StringName, state: State) -> void:
match signal_name:
&"entered":
if show_descriptions == DisplayModes.ACTIVE:
_active_states.push_back(state)
_set_description_from_active_states()
&"exited":
if show_descriptions == DisplayModes.ACTIVE:
_active_states.pop_back()
_set_description_from_active_states()

View file

@ -0,0 +1,6 @@
extends AnimationState
func _update(_delta: float) -> void:
if Input.is_action_just_pressed(get_root().NEXT_STATE_ACTION):
choose_new_substate_requested.emit()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cna1s8hi8xc58"
path="res://.godot/imported/animation_state.png-5f10255295e45d80e555d2885de83e22.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/simple-state/icons/animation_state.png"
dest_files=["res://.godot/imported/animation_state.png-5f10255295e45d80e555d2885de83e22.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

View file

@ -0,0 +1,2 @@
[InternetShortcut]
URL=http://www.kenney.nl/

View file

@ -0,0 +1,2 @@
[InternetShortcut]
URL=https://www.patreon.com/kenney/

View file

@ -0,0 +1,22 @@
Board Game Icons (1.0)
Created/distributed by Kenney (www.kenney.nl)
Creation date: 10-01-2022
------------------------------
License: (Creative Commons Zero, CC0)
http://creativecommons.org/publicdomain/zero/1.0/
This content is free to use in personal, educational and commercial projects.
Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
------------------------------
Donate: http://support.kenney.nl
Patreon: http://patreon.com/kenney/
Follow on Twitter for updates:
http://twitter.com/KenneyNL

View file

@ -0,0 +1,14 @@
###############################################################################
Game icon pack by Kenney Vleugels (www.kenney.nl)
------------------------------
License (CC0)
http://creativecommons.org/publicdomain/zero/1.0/
You may use these graphics in personal and commercial projects.
Credit (Kenney or www.kenney.nl) would be nice but is not mandatory.
###############################################################################

View file

@ -0,0 +1,20 @@
Pictogrammers Free License
--------------------------
This icon collection is released as free, open source, and GPL friendly by
the [Pictogrammers](http://pictogrammers.com/). You may use it
for commercial projects, open source projects, or anything really.
# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
Some of the icons are redistributed under the Apache 2.0 license. All other
icons are either redistributed under their respective licenses or are
distributed under the Apache 2.0 license.
# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
All web and desktop fonts are distributed under the Apache 2.0 license. Web
and desktop fonts contain some icons that are redistributed under the Apache
2.0 license. All other icons are either redistributed under their respective
licenses or are distributed under the Apache 2.0 license.
# Code: MIT (https://opensource.org/licenses/MIT)
The MIT license applies to all non-font and non-icon files.

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cgoc214akn2d6"
path="res://.godot/imported/random_state.png-0878745fbdc123f3f0d51012c73a7024.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/simple-state/icons/random_state.png"
dest_files=["res://.godot/imported/random_state.png-0878745fbdc123f3f0d51012c73a7024.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bcviu3dccvvm2"
path="res://.godot/imported/sequence_state.png-4718f2c926301319d99a0ccd15ff62fb.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/simple-state/icons/sequence_state.png"
dest_files=["res://.godot/imported/sequence_state.png-4718f2c926301319d99a0ccd15ff62fb.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

View file

@ -0,0 +1,12 @@
Icon Sources
-----------
[0] Board Game Icons: https://kenney.nl/assets/board-game-icons
- state.png
- random_state.png
- sequence_state.png
[1] Game Icons: https://kenney.nl/assets/game-icons
- animation_state.png
[2] Pictogrammers: <https://pictogrammers.com/library/mdi/>
- state_machine_debugger.png (tinted)

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://msu07hn5ewo5"
path="res://.godot/imported/state.png-fa3dd722682a28f890f138dcc6e162af.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/simple-state/icons/state.png"
dest_files=["res://.godot/imported/state.png-fa3dd722682a28f890f138dcc6e162af.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b32u1sa3robj0"
path="res://.godot/imported/state_fullsize.png-41a4827bc36d7745ba55c17c0bc213e9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/simple-state/icons/state_fullsize.png"
dest_files=["res://.godot/imported/state_fullsize.png-41a4827bc36d7745ba55c17c0bc213e9.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dsglk01amsawf"
path="res://.godot/imported/state_machine_debugger.png-4af22f2c577f0756aa0b1d50ce679700.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/simple-state/icons/state_machine_debugger.png"
dest_files=["res://.godot/imported/state_machine_debugger.png-4af22f2c577f0756aa0b1d50ce679700.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

View file

@ -0,0 +1,7 @@
[plugin]
name="SimpleState"
description="A super-simple state machine. Lightweight and (hopefully) reliable."
author="AuraTheEnby"
version="1.3.2"
script="plugin.gd"

View file

@ -0,0 +1,25 @@
@tool
extends EditorPlugin
## It uses the icons provided by the scripts anyway, so
## we don't really need to specify the real ones here.
## Plus, it might help with enabling it before the project
## has been reloaded for the first time.
var placeholder_texture := PlaceholderTexture2D.new()
func _enter_tree() -> void:
add_custom_type("State", "Node", State, placeholder_texture)
add_custom_type("RandomState", "Node", RandomState, placeholder_texture)
add_custom_type("AnimationState", "Node", AnimationState, placeholder_texture)
add_custom_type("SequenceState", "Node", SequenceState, placeholder_texture)
add_custom_type("StateMachineDebugger", "Tree", StateMachineDebugger, placeholder_texture)
func _exit_tree() -> void:
remove_custom_type("State")
remove_custom_type("RandomState")
remove_custom_type("AnimationState")
remove_custom_type("SequenceState")
remove_custom_type("StateMachineDebugger")

View file

@ -0,0 +1,33 @@
# meta-default: true
extends _BASE_
# Called when the state is activated. (parents, then children)
func _enter() -> void:
pass
# Called after the state is activated. (children, then parents)
func _after_enter() -> void:
pass
# Called every physics frame (only when the state is active, of course). (parents, then children)
func _update(delta: float) -> void:
pass
# Called at the end of every physics frame. (children, then parents)
func _after_update(delta: float) -> void:
pass
# Called before the state is deactivated. (parents, then children)
func _before_exit() -> void:
pass
# Called when the state is deactivated. (children, then parents)
func _exit() -> void:
pass