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,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()