Skip to content

Commit

Permalink
Merge branch 'master' of github.com:hpi-swa-lab/godot-pronto
Browse files Browse the repository at this point in the history
  • Loading branch information
leogeier committed Jan 17, 2024
2 parents 5f680a3 + c213047 commit 55777dc
Show file tree
Hide file tree
Showing 17 changed files with 894 additions and 44 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ The following list of behaviors manage state or communicate visual properties.
| InspectBehavior | Add as a child of a node to inspect its properties inside the game. |
| InstanceBehavior | Allows you to define a template subtree of Nodes that you want to repeat multiple times without copy-pasting. Add your template as a child of the Instance node, then hover the connection dialog and click the "Instance" button. Note: internally, this creates a "hidden" scene that you need to commit as well. You can thus use **"Editable children"** in Godot by right-clicking the instance and tweaking properties while inheriting the rest. |
| PlaceholderBehavior | Show a colored rectangle with a label. Useful as a quick means to communicate a game object's function. Functions as a collision shape, so you don't need to add another. Instead of a rectangle a placeholder can also display a sprite instead (use the Sprite Library in the Inspector to choose an existing texture or load your own). Can be `flash()`ed in a different color. |
|StateMachineBehavior|The StateMachineBehavior acts as a purely graphic hint as to which StateBehavior objects belong to the same state machine. The GroupDrawer class is used for this.|
|StateBehavior|The StateBehavior is the fundamental building block of a state machine. Each StateBehavior emits the signals `StateBehavior.entered()` and `StateBehavior.exited()` to communicate the state machine's state|
| StateMachineBehavior | A state machine to model behavior with different states. Add states by adding StateBehvaior children. Connect other nodes with the trigger method to make triggers available inside the state machine that can be used for state transitions. |
| StateBehavior | Modelling states of a state machine, use as children of StateMachineBehavior. Transition between different states using the `on_trigger_received` signal. Perform actions in states with the `entered` and `in_state` signals. |
| StoreBehavior | Use the Godot meta properties to store state. You can configure it to store values in the global dictionary `G` and access it via `G.at(prop)`. |
| ValueBehavior | Show a constant you can use in expression visually as a slider. Note that these are shared globally, so create new names if you need to use different values. |
| VisualLineBehavior | Show a colored Line between two Nodes. Useful as a quick visual connection. |
Expand Down
20 changes: 16 additions & 4 deletions addons/pronto/behaviors/ControlsBehavior.gd
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ signal mouse_drag_global(pos: Vector2)
var enable_drag = false
var held_mouse_buttons = {}

## time in ms to wait after a key input to process the next input (0 for no throttle)
@export var input_throttle_delay = 0
var last_input_time = 0

#const valid_directions = ["left", "right", "up", "down"]
func _is_key_pressed(direction):
#if not direction in valid_directions:
Expand All @@ -116,19 +120,27 @@ func _process(delta):

if Engine.is_editor_hint(): return

# If throttling is enabled, discard input events until delay has passed
var current_time = Time.get_ticks_msec()
var discard_key_events = last_input_time + input_throttle_delay > current_time

var input_direction = Vector2.ZERO # Used to allow vertical movement
if _is_key_pressed("left"):
if _is_key_pressed("left") and not discard_key_events:
left.emit()
input_direction += Vector2.LEFT
if _is_key_pressed("right"):
last_input_time = current_time
if _is_key_pressed("right") and not discard_key_events:
right.emit()
input_direction += Vector2.RIGHT
if _is_key_pressed("up"):
last_input_time = current_time
if _is_key_pressed("up") and not discard_key_events:
up.emit()
input_direction += Vector2.UP
if _is_key_pressed("down"):
last_input_time = current_time
if _is_key_pressed("down") and not discard_key_events:
down.emit()
input_direction += Vector2.DOWN
last_input_time = current_time
# Emit signals
vertical_direction.emit(Vector2(0, input_direction.y))
horizontal_direction.emit(Vector2(input_direction.x, 0))
Expand Down
10 changes: 10 additions & 0 deletions addons/pronto/behaviors/InspectBehavior.gd
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,16 @@ func _get_property_list():
if target:
var property_names = []
property_names.push_front('<statement(s)>')

var custom_class = Utils.get_custom_class_name(target)
if custom_class:
property_names.push_back("-- %s --" % custom_class)
var custom_property_names = Utils.get_script_properties(target).map(func(_property): return _property.name)
custom_property_names.sort()
for n in custom_property_names:
if n.contains(".gd"):
continue
property_names.push_back(n)
for _class in Utils.all_classes_of(target):
var new_property_names = ClassDB.class_get_property_list(_class, true)\
.map(func(_property): return _property.name)
Expand Down
79 changes: 60 additions & 19 deletions addons/pronto/behaviors/StateBehavior.gd
Original file line number Diff line number Diff line change
Expand Up @@ -12,49 +12,79 @@ class_name StateBehavior
signal entered

## Signal that gets emitted when the state becomes inactive.
## Use [param transition_id] to determine in the transitions' condition which transition to trigger.
signal exited(target_state_name: String)

## Modeles whether the state reacts to transitions at all.
## The sum of all [code]active[/code] variables is the state of the state machine
## Signal that gets emitted every frame while the state is active.
signal in_state(delta: float)

## Signal that gets emitted when the state machine receives a trigger and this
## is the active state.
## Use this to tranistion to different states.
signal on_trigger_received(trigger: String)

## Use this variable to determine the initial state.
@export var active: bool = false:
get: return active
set(value):
@export var is_initial_state: bool = false:
set(value):
active = value
reload_icon()
is_initial_state = value

## Models whether the state reacts to transitions at all.
var active: bool = false:
get:
if get_parent():
return get_parent().active_state == self
else:
return false
set(value):
if get_parent():
get_parent().set_active_state(self, value)

var _active_texture = load("res://addons/pronto/icons/StateActive.svg")
var _inactive_texture = load("res://addons/pronto/icons/StateInactive.svg")

## Function that tells the state to become active. Works only if the state is not active yet.
func _get_configuration_warnings() -> PackedStringArray:
if not get_parent() is StateMachineBehavior:
return ["StateBehavior must be child of a StateMachineBehavior"]
return []

## Function that tells the state to become active.
## Will not do anything if the state is already active.
func enter():
if not active:
active = true
entered.emit()

## Function that tells the state to become inactive. Works only if the state is active.
## The [param transition_id] is forwarded to the [signal StateBehavior.exited] signal and
## can thus be used to determine which transition to trigger.
## DEPRECATED
func exit(target_state_name: String):
reload_icon()
if active:
active = false
exited.emit(target_state_name)

## Override of [method Behavior.line_text_function].
## Used to display the node name of a target StateBehavior on a line
## Used to display special text on transitions.
func line_text_function(connection: Connection) -> Callable:
var addendum = ""
if get_node(connection.to) is StateBehavior:
addendum = "\ntransition to '%s'" % connection.to.get_name(connection.to.get_name_count() - 1)
if connection.trigger != "":
addendum = "\ntransition on '%s'" % connection.trigger
var only_if_source_code = connection.only_if.source_code
if only_if_source_code != "true":
if Utils.count_lines(only_if_source_code) == 1:
addendum += " if " + only_if_source_code
else:
addendum += " if [?]"

return func(flipped):
return connection.print(flipped) + addendum

## Override of [method Behavior.lines]
## Used to add the State name below the icon
## Used to add the State name below the icon and change the color.
func lines():
return super.lines() + [Lines.BottomText.new(self, str(name))]
var connection_lines = super.lines()
# Color state transitions specially
for line in connection_lines:
if line.to is StateBehavior and line.from is StateBehavior:
line.color = Color.LIGHT_GREEN
return connection_lines + [Lines.BottomText.new(self, str(name))]

func _get_connected_states(seen_nodes = []):
seen_nodes.append(self)
Expand All @@ -68,9 +98,20 @@ func _get_connected_states(seen_nodes = []):
## Used to display the correct icon when the StateBehavior is active or inactive
func icon_texture():
return _active_texture if active else _inactive_texture

func _reload_icon_from_game(value: bool):
var icon = _active_texture if value else _inactive_texture
self.reload_icon(icon)

func _ready():
super._ready()

if active:
entered.emit()
if is_initial_state:
if get_parent():
get_parent().set_active_state(self, true)

func _process(delta):
super._process(delta)

if not Engine.is_editor_hint() and active:
in_state.emit(delta)
52 changes: 52 additions & 0 deletions addons/pronto/behaviors/StateMachineBehavior.gd
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,59 @@ class_name StateMachineBehavior
## hint as to which [class StateBehavior] objects belong to the same state machine.
## The [class GroupDrawer] is used for this.

signal triggered(trigger: String)

var active_state: StateBehavior = null
const always_trigger = "ε"
@export var triggers: Array[String] = [always_trigger]

## When true, the state machine will trigger the "ε" trigger on every frame,
## allowing state transitions without other triggers.
@export var trigger_epsilon: bool = true

## Exits the current active state and enters the new active state.
## Is usually called by StateBehavior/enter.
func set_active_state(state: StateBehavior, is_active: bool):
if active_state:
active_state.exit(state.name)
if is_active:
active_state = state
state.enter() # Since set_active_state is usually called from a state's enter(), this won't do anything.
if EngineDebugger.is_active():
EngineDebugger.send_message("pronto:state_activation", [get_path(), state.get_path()])

var _state_machine_info = null

func _ready():
super._ready()
if Engine.is_editor_hint():
add_child(preload("res://addons/pronto/helpers/GroupDrawer.tscn").instantiate(), false, INTERNAL_MODE_BACK)
_state_machine_info = preload("res://addons/pronto/helpers/StateMachineInfo.tscn").instantiate()
add_child(_state_machine_info, false, INTERNAL_MODE_BACK)

## List of all StateBehavior nodes in this StateMachineBehavior
func states():
return get_children().filter(func (c): c is StateBehavior)

## Provide a trigger to the State Machine. This will trigger the active state
## which may lead to a transition via "on_trigger_received".
func trigger(trigger: String):
if active_state:
active_state.on_trigger_received.emit(trigger)
triggered.emit(trigger)
if trigger != always_trigger:
EngineDebugger.send_message("pronto:state_machine_trigger", [get_path(),trigger])

func _redraw_states_from_game(active_state: StateBehavior):
for c in get_children():
if c.has_method("_reload_icon_from_game"):
c._reload_icon_from_game(active_state == c)

func _redraw_info_from_game(trigger: String):
if _state_machine_info:
_state_machine_info._redraw_with_trigger(trigger)

func _process(delta):
super._process(delta)
if trigger_epsilon and not Engine.is_editor_hint():
trigger(always_trigger)
4 changes: 2 additions & 2 deletions addons/pronto/helpers/Behavior.gd
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ var _lines := Lines.new()
## a non-Behavior subclass. See pronto.gd:get_behavor() for more context.
var hidden_child = false

func reload_icon():
_icon.texture = icon_texture()
func reload_icon(override_texture = null):
_icon.texture = override_texture if override_texture else icon_texture()
_icon.queue_redraw()

func icon_texture():
Expand Down
42 changes: 34 additions & 8 deletions addons/pronto/helpers/Connection.gd
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ static func connect_expr(from: Node, signal_name: String, to: NodePath, expressi
c.only_if = only_if
c._store(from, undo_redo)
return c

static func create_transition(from: Node, signal_name: String, to: NodePath, invoke: String, more_references: Array, only_if: ConnectionScript, trigger: String, undo_redo = null):
var c = Connection.new()
c.signal_name = signal_name
c.to = to
c.invoke = invoke
c.trigger = trigger
#c.arguments = arguments
c.more_references = more_references
c.only_if = only_if
c._store(from, undo_redo)
return c

## Returns list of all connections from [param node]
static func get_connections(node: Node) -> Array:
Expand Down Expand Up @@ -73,6 +85,8 @@ static func get_connections(node: Node) -> Array:
return enabled
set(new_value):
enabled = new_value
## Used to trigger state transitions (connections between StateBehaviors)
@export var trigger: String = ""

## Return whether this connection will execute an expression.
func is_expression() -> bool:
Expand Down Expand Up @@ -193,15 +207,15 @@ func _trigger(from: Object, signal_name: String, argument_names: Array, argument
target = from.get_node(c.to)
names.append("to")
values.append(target)

if not c.should_trigger(names, values, from):
continue

for i in len(self.more_references):
var ref_path = self.more_references[i]

for i in len(c.more_references):
var ref_path = c.more_references[i]
var ref_node = from.get_node(ref_path)
names.append("ref" + str(i))
values.append(ref_node)

if not c.should_trigger(names, values, from):
continue

var args_string
if deferred: await ConnectionsList.get_tree().process_frame
Expand All @@ -225,9 +239,16 @@ func _trigger(from: Object, signal_name: String, argument_names: Array, argument

func has_condition():
return only_if.source_code != "true"

func state_transition_should_trigger(names: Array, values: Array):
if trigger == "":
return true
var trigger_idx = names.find("trigger")
var trigger_value = values[trigger_idx]
return trigger == trigger_value

func should_trigger(names, values, from):
return not has_condition() or await _run_script(from, only_if, values)
func should_trigger(names: Array, values: Array, from):
return (not has_condition() or await _run_script(from, only_if, values)) and state_transition_should_trigger(names, values)

func make_unique(from: Node, undo_redo):
var old = _ensure_connections(from)
Expand Down Expand Up @@ -268,6 +289,11 @@ func print(flip = false, shorten = true, single_line = false, show_disabled = fa
assert(is_expression())
return "{2}{0}{1}{3}".format([signal_name, Utils.ellipsize(expression.source_code.split('\n')[0], 16 if shorten else -1), prefix, suffix]).replace("\n" if single_line else "", "")

## Capture all information as a string. Not used to deserialize, but to identify
## connections (as opposed to print()).
func serialize_as_string():
return "{0}/{1}/{2}".format([signal_name, to, invoke])

## Iterate over connections and check whether the target still exists for them.
## If not, remove the connection.
static func garbage_collect(from: Node):
Expand Down
10 changes: 10 additions & 0 deletions addons/pronto/helpers/ConnectionDebug.gd
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ func _capture(message, data, session_id):
if message == "pronto:value_set":
_sync_value_change(data)
return true
if message == "pronto:state_activation":
var state_machine = editor_interface.get_edited_scene_root().get_parent().get_node_or_null(str(data[0]).substr(6))
var active_state = editor_interface.get_edited_scene_root().get_parent().get_node_or_null(str(data[1]).substr(6))
state_machine._redraw_states_from_game(active_state)
return true
if message == "pronto:state_machine_trigger":
var state_machine = editor_interface.get_edited_scene_root().get_parent().get_node_or_null(str(data[0]).substr(6))
if state_machine:
state_machine._redraw_info_from_game(data[1])
return true
return true

func _sync_value_change(info: Array):
Expand Down
Loading

0 comments on commit 55777dc

Please sign in to comment.