A plugin for Godot Engine 4.1+ that functions as a Rule-Based System framework. This was developed as the capstone project for my Computer Science bachelor degree at USP.
Using gd-plug
Supposing the plugin manager is already installed (check its documentation for details), you need only to:
- Add the
plug.gd
file below in the project root. If the file already exists, add the new line in the _plugging() function:
# plug.gd
extends "res://addons/gd-plug/plug.gd"
func _plugging():
plug("rvbatt/rule-based-godot", {"include": ["addons/rule_based_godot/", "test_scenes/", "script_templates/"]})
- Still on the project root, run the following command in the terminal:
godot --no−window -s plug.gd install
- On Godot's Editor, enter the Project menu, click in ProjectSettings, got to the Plugins tab and enable the Rule-Based Godot plugin.
Project > ProjectSettings > Plugins > Rule-Based Godot (Enable) |
-
Download or clone this repository. Use the last version on the main branch. If you decide to download, the content will come in a .zip file, so you will need to unpack it
-
Copy the
addons/rule-based-godot/
folder to your Godot's project root. If the addons folder already exists, copy only the rule_based_godot subfolder and place it there -
(Recommend) Copy the
test_scenes
andscript_templates
folders as well. If the templates folder already exists, copy only its subfolders -
Follow the same instructions given on the Step 3 of Installation Using gd-plug
- Add a RuleBasedSystem node to the scene
Recomendation: children of the RuleBasedSystem will have a smaller relative path, so make the system node be the parent of the nodes you want to control
- Set the type of Iteration Update that the system will use:
Every frame
: iterates every physics frame. Use this with caution, a large set of rules can take a while to run throughOn Timer
: iterates every wait_time seconds, which can be defined in the Timer category on the InspectorOn Call
: only iterates when the method iterate() is called explicity. You can connect external signals to this method if you want to have a better control over it
- Add an Arbiter. Don't choose the AbstractArbiter, because that's the abstract class and it doesn't implement the necessary function
Recomendation: save and
Quick Load
a resource file instead of creating a new arbiter
Inspector after following steps 1 to 3 |
Now you can choose to use either the Inspector, with a graphical interface, or the Rules Editor bottom panel, with a code driven approach, to declare the rules
Since the RuleList and all of its components are resources, you can save and load any part of the data structure, sharing common rules, conditions or actions between several systems. The following steps talk about creating new ones from scratch, but you can skip any creation step if you load an existing resource instead.
- Create a new RuleList and start adding rules on the array, as many as you need
- For each Rule, create its components (never choose the ones that start with Abstract):
- Create a Condition. If you choose a boolean match (NOT, OR, AND), you can then create its subconditions, repeating this step. If you choose a datum match, edit its properties, and don't forget to expand the groups to check them out
- Add an Action to the array, then edit its properties, including the ones on the groups. You can repeat this step as many times as you want
- (optional) Once you have defined the behavior you want, you can save a part or all of the list of rules as a resource file. You can save a Condition, an individual Action, a Rule or the whole RuleList
Obs.: You can give Rules, Matches and Actions a name, by editing the Name property on the Resource group (it's actually resource_name)
Inspector after following steps 4 and 5 |
You can define rules in the Rules Editor bottom panel using a JSON syntax. The panel connects itself to the last RuleBasedSystem that you've clicked, which will be the one showing on the Inspector.
- Open the Rules Editor and use the
Reset
button if there is some leftover text in the editor
RulesEditor after step 4 |
- Add new rules using the
New Rule
button, as many as you need. Be careful with the position of the cursor, because the template will be inserted right at that position - For each rule, replace the "condition" and "actions" placeholders, using the corresponding buttons. Always be mindful of the cursor position
- Delete the "condition", keep the cursor at that position and click the
New Match
button. Choose one of the options that popped up and click it. The format of the selected match will be inserted in the editor, where the cursor was - Follow the syntax explained in the documentation and replace the match's placeholders with the configuration you want. If you chose a boolean match, there will be another "condition" or "conditions", so repeat the previous step
- Delete "actions" and click the
New action
button. Click one option. The JSON format of that type of action will be inserted where the cursor was - Replace the action's placeholders with the action properties, following the syntax
- Repeat steps iii. (without erasing the placeholder) and iv. to add more actions
- Delete the "condition", keep the cursor at that position and click the
RulesEditor in the middle of step 6 |
- When your rules are done, click the
Apply
button to set the current RuleBasedSystem's rule list. If the syntax is wrong, a JSON parsing error will appear and the "apply" will abort - (optional) Save the created text in a .json file in case you want to reuse some part later
RulesEditor after steps 6 and 7 |
Type | Identifier | Description |
---|---|---|
Arbiters | FirstApplicable | selects the first satisfied rule (assumes they are ordered by priority) |
LeastRecentlyUsed | selects the satisfied rule that was triggered the longest time ago | |
Boolean Matches | NOT | logic gate |
AND | multiple-input logic gate | |
OR | multiple-input logic gate | |
Atomic Matches | Numeric | tests if a numeric value, obtained through a property or method call, is in an interval |
String | tests if a string, obtained through a property or method call, is equal to a constant | |
Hierarchy | tests if two nodes have a certain relation: the first is "Parent of" the second, the first is "Sibling of" the second or the first is "Child of" the second | |
Distance | tests if the distance between the origin of two nodes is in an interval | |
AreaDetection | tests if there are objects (specific ones, or any) in an area | |
DistinctVariables | applies a substitution that makes sure every listed variable has a distinct value | |
Actions | SetProperty | sets the property of a node |
CallMethod | calls the method of a node passing the arguments in a vector | |
EmitSignal | adds a signal to a Node, if it doesn't have it, and emits it, passing the arguments in a vector |
To add new types of: Arbiters, Boolean Matches, Atomic Matches and/or Actions
- Copy the
script_templates
folder to your Godot's project root. If this folder already exists, copy all of its subfolders - Create a new script and select the appropriate class to inherit from. Then, check the template box and select the "New ___"
- Arbiter strategy: inherits from:
AbstractArbiter
- Multiple-entry Boolean Match: inherits from
AbstractBooleanMatch
- Atomic Match: inherits from
AbstractAtomicMatch
- Action command: inherits from
AbstractAction
- Arbiter strategy: inherits from:
Template option when creating a script |
-
Follow the instructions given on the template. For actions and matches, there are some flags that define which functions need to be implemented:
- Actions:
Action Flag Methods that MUST be implemented Methods that could be overriden trigger(bindings)
Agent Nodes trigger_node(agent_node, bindings)
get_agent_nodes(bindings)
- Atomic Matches:
Atomic Match Flags Methods that MUST be implemented Methods that could be overriden is_satisfied(bindings)
Tester Node node_satisfies_match(node, bindings)
get_candidates()
Tester Node, Data Based Node get_data(node)
,data_satisfies_match(data)
get_candidates()
Tester Node, Data Based Node, Get Node Data Preset data_satisfies_match(data)
get_candidates()
Extends Timer
iteration_update
: IterationUpdate
Should the system iterate ON_CALL, ON_TIMER or EVERY_FRAME
arbiter
: AbstractArbiter
The type of rule arbitration strategy used
rule_list
: RuleList
The list of rules
iterate()
-> Array
Iterates through the
rule_list
, gathers the satisfied ones, asks thearbiter
to select one of them, triggers the selected rule and returns the results
Extends Resource
Receives an array of satisfied rules and returns one of them
Extends AbstractMatch
subconditions
: Array[AbstractMatch]
The matches children of this boolean operator
Extends AbstractMatch
Tester_Node
: bool
Configuration flag: is this match node-based?
Data_Based_Node
: bool
Configuration flag: is this match based on the data from a node? Can only be active if
Tester_Node
= true
Get_Node_Data_Preset
: bool
Configuration flag: should this match use preset ways of getting data from a node? Can only be active if
Data_Based_Node
= true
_preset_node_path(path_variable: StringName, node_variable: StringName)
-> void
Sets the
node_variable
with the node found on thepath_variable
at the moment the system is ready and begins setup
_pre_connect(node_variable: StringName, signal_name: StringName, function: Callable)
-> void
Connects the signal
signal_name
to the node's method defined bynode_variable
.function
. Does this soon after_preset_node_path
_get_candidates()
-> Array[Node]
Returns the first batch of candidates for this match. If
Tester_Node
= true and the target is a wildcard, use the node search groups. If there are no groups, defaults to children of the RuleBasedSystem
_node_satisfies_match(tester_node: Node, bindings: Dictionary)
-> bool
If
Data_Based_Node
= true, uses_get_data
and_data_satisfies_match
to return true if the node satisfies the match, and false otherwise. Also saves the data variable, if Should Retrieve Data option was on
_data_satisfies_match(data: Variant)
-> bool:
If
Data_Based_Node
= true, return true if thedata
satisfies the math and false otherwise
_get_data(tester_node: Node)
-> Variant:
If
Get_Node_Data_Preset
= true, extracts the data from thetester_node
, whether by getting a property value or the return of a mehod call
Extends RuleBasedResource
is_satisfied(bindings)
-> bool
Receives the Dictionary of bindings between variable names and possible candidates. Returns true if the match is satisfied with current variable substitutions, and false otherwise
Extends RuleBasedResource
Agent_Nodes
: bool
Configuration flag: is this action node-based?
_pre_add_signal(name_var: StringName, param_to_type_var: StringName)
-> void
Adds a user signal with the name defined in the variable named
name_var
and parameters defined by the variable namedparam_to_type_var
. SeeObject.add_user_signal
for more information
trigger(bindings: Dictionary)
-> Array
Receives the Dictionary of bindings between variable names and possible candidates. Returns the results from triggering this action. If
Agent_Nodes
= true, uses_get_agent_nodes
and_trigger_node
to trigger all necesary nodes
_trigger_node(agent_node: Node, bindings: Dictionary)
-> Variant:
If
Agent_Nodes
= true, triggers theagent_node
using the substitutions defined bybindings
. Returns a value that represents the action's result
_get_agent_nodes(bindings: Dictionary)
-> Array
If
Agent_Nodes
= true, receives the Dictionary of bindings between variable names and possible candidates. Returns all the nodes that must perform the action
Extends RuleBasedResource
condition
: AbstractMatch
Points to the root of the tree of matches that defines the condition
actions
: Array[Action]
The actions triggered by this rule. Follows execution order
_bindings
: Dictionary
Associations between variable names and possible substitutions
condition_satisfied()
-> bool
Clears the
_bindings
and returns whether thecondition
is satisfied
trigger_actions()
-> Array
Triggers all the
actions
in the order they appear in the array. Returns the list of all actions results
Extends RuleBasedResource
rules
: Array[Rule]
The rules, ordered by priority (descending)
satisfied_rules()
-> Array[Rule]
Returns all the rules that are satisfied in the current system iteration, ordered by priority (descending)
Extends Resource
_system_node
: RuleBasedSystem
A reference to the node of the system this resource belongs to
_rule_db
: RuleDB
A reference to the Rules DataBase used in the current scope (belongs to the RuleList)
json_format()
-> String
Returns a String with a template for this resource's JSON representation format
to_json_repr()
-> Variant
Returns the JSON representation of this resource, which can be a Dictionary (RuleList and Rule) or an Array (AbstractBooleanMatch, AbstractAtomicMatch and AbstractAction)
build_from_repr(json_repr)
-> void
Receives a JSON representation equal to the one defined in
to_json_repr()
and builds itself with it, setting the corresponding properties
Extends Object
actions
: Dictionary
Associations between the action identifier (without suffix "Action") and the script path
matches
: Dictionary
Associations between the match identifier (without suffix "Match") and the script path
match_from_json(json_repr: Array)
-> AbstractMatch:
Receives the JSON representation of a match and returns the corresponding object. Uses recursion for boolean matches
action_from_json(json_repr: Array)
-> AbstractAction:
Receives the JSON representation of a action and returns the corresponding object
rule_from_json(json_repr: Dictionary)
-> Rule:
Receives the JSON representation of a rule and returns the corresponding object. Uses
match_from_json
andaction_from_json
-
The term condition can be replaced by a NOTMatch, subtypes of MultiBoolMatch or subtypes of DatumMatch.
-
The ID of a component is its class_name without the "Action" or "Match" suffix.
Here are the patterns adopted to represent the syntax:
-
"constant"
: if something is between " ", then it is a constant and it should be written the exact same way, including the quotation marksEx.: "Fixed" should be copied as "Fixed" and not be replaced
-
?variable
: any name prefixed by an ? represents a variable, or wild card. It can renamed to anything you want, but the string must contain the question markEx.: ?data represents a variable named data, but it can be replaced by ?example
-
[items]
: represents an array of arbitrary length, including empty, containing elements of type item separated by commasEx.: [fruits] can be replaced by [], [apple] or [apple, banana, mango]
-
items...
: represents an arbitrary number (which can be zero) of elements of type item separated by commas, but not inside an arrayEx.: colors... can be replaced by red or red, green, blue or by nothing
-
{keys: values}
: represents a dictionary where the keys have the type key and the values are of the type value, with arbitrary size, including emptyEx.: {characters: classes} can be replaced by {} or by {Alice: mage, Bob: fighter}
Note: If the terms inside { } are on the singular form (don't end with s), then the dictionary must contain only one entry
-
<optional>
: if something is between < >, you can choose to include it or notEx.: <adjective>, noun can be replaced by red, ball or just ball
Note: When used on templates, it indicates that the subtypes may or may not have this element
-
(choiceA|choiceB|choiceC.1, choiceC.2)
: you need to pick one of the choices separated by | and between ( ). One choice can have several items separated by commasEx.: (melee_weapon|ranged_weapon, ammunition) can be replaced by sword or by bow, arrow
- Rule List:
{"Rules": [rules]}
- Rule:
{"if": condition, "then": [actions]}
- Matches:
- NOT:
["NOT", condition]
- (Multiple-entry) Boolean template: [ID, [conditions]]
- AND:
["AND", [conditions]]
- OR:
["OR", [conditions]]
- AND:
- Atomic template: [ID, <?data>, vars..., (tester_path|?wild, [groups]) <, (prop|method, [args])>]
- DistinctVariables:
["DistinctVariables", [distinct_variables]]
- Area Detection:
["AreaDetection", area_path, (tester_path|?wild, [groups])]
Obs: area_path must be the path to either an Area2D or Area3D
- Distance:
["Distance", <?dist,> source_path, min_distance, max_distance, (tester_path|?wild, [groups])]
Obs: min_distance e max_distance are float number, "inf" or "-inf"
- Hierarchy:
["Hierarchy", source_path, ("Parent of"|"Sibling of"|"Child of"), (tester_path|?wild, [groups])]
- Numeric:
["Numeric", <?number,> min_value, max_value, (tester_path|?wild, [groups]), (prop|method, [args])
Obs: min_value e max_value are float number, "inf" or "-inf"
- String:
["String", <?string,> string_value, (tester_path|?wild, [groups]), (prop|method, [args])
- DistinctVariables:
- NOT:
- Action template: [ID, (agent_path|?wild|[groups]|), vars...]
- Set Property:
["SetProperty", (agent_path|?wild|[groups]|), {property: value}]
Obs: {property: value} only has only one entry
- Call Method:
["CallMethod", (agent_path|?wild|[groups]|), method, [args]]
- Emit Signal:
["EmitSignal", (agent_path|?wild|[groups]|), signal <, {params: types}, [args]>]
Obs: {params: types} is a dictionary with the name of the signal parameters as keys and an arbitrary element with the same type as the corresponding argument as value. For example: {number_param: 1, string_param: ""}
- Set Property: