-
-
Notifications
You must be signed in to change notification settings - Fork 117
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
weighted random selector/sequence (#206)
* weighted random selector/sequence * added tests for weighted random composites * bug fixes - renaming will not reset the weight - selector composite was not reversing the children bag (weights were having the opposite effect) * added tests for both sequence and selector random composites with weights --------- Co-authored-by: miguel <miguel-gonzalez@gmx.de>
- Loading branch information
Showing
14 changed files
with
601 additions
and
33 deletions.
There are no files selected for viewing
152 changes: 152 additions & 0 deletions
152
addons/beehave/nodes/composites/randomized_composite.gd
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
@tool | ||
class_name RandomizedComposite extends Composite | ||
|
||
const WEIGHTS_PREFIX = "Weights/" | ||
|
||
## Sets a predicable seed | ||
@export var random_seed: int = 0: | ||
set(rs): | ||
random_seed = rs | ||
if random_seed != 0: | ||
seed(random_seed) | ||
else: | ||
randomize() | ||
|
||
## Wether to use weights for every child or not. | ||
@export var use_weights: bool: | ||
set(value): | ||
use_weights = value | ||
if use_weights: | ||
_update_weights(get_children()) | ||
_connect_children_changing_signals() | ||
notify_property_list_changed() | ||
|
||
var _weights: Dictionary | ||
|
||
|
||
func _ready(): | ||
_connect_children_changing_signals() | ||
|
||
|
||
func _connect_children_changing_signals(): | ||
if not child_entered_tree.is_connected(_on_child_entered_tree): | ||
child_entered_tree.connect(_on_child_entered_tree) | ||
|
||
if not child_exiting_tree.is_connected(_on_child_exiting_tree): | ||
child_exiting_tree.connect(_on_child_exiting_tree) | ||
|
||
|
||
func get_shuffled_children() -> Array[Node]: | ||
var children_bag: Array[Node] = get_children().duplicate() | ||
if use_weights: | ||
var weights: Array[int] | ||
weights.assign(children_bag.map(func (child): return _weights[child.name])) | ||
children_bag.assign(_weighted_shuffle(children_bag, weights)) | ||
else: | ||
children_bag.shuffle() | ||
return children_bag | ||
|
||
|
||
## Returns a shuffled version of a given array using the supplied array of weights. | ||
## Think of weights as the chance of a given item being the first in the array. | ||
func _weighted_shuffle(items: Array, weights: Array[int]) -> Array: | ||
if len(items) != len(weights): | ||
push_error("items and weights size mismatch: expected %d weights, got %d instead." % [len(items), len(weights)]) | ||
return items | ||
|
||
# This method is based on the weighted random sampling algorithm | ||
# by Efraimidis, Spirakis; 2005. This runs in O(n log(n)). | ||
|
||
# For each index, it will calculate random_value^(1/weight). | ||
var chance_calc = func(i): return [i, randf() ** (1.0 / weights[i])] | ||
var random_distribuition = range(len(items)).map(chance_calc) | ||
|
||
# Now we just have to order by the calculated value, descending. | ||
random_distribuition.sort_custom(func(a, b): return a[1] > b[1]) | ||
|
||
return random_distribuition.map(func(dist): return items[dist[0]]) | ||
|
||
|
||
func _get_property_list(): | ||
var properties = [] | ||
|
||
if use_weights: | ||
for key in _weights.keys(): | ||
properties.append({ | ||
"name": WEIGHTS_PREFIX + key, | ||
"type": TYPE_INT, | ||
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR, | ||
"hint": PROPERTY_HINT_RANGE, | ||
"hint_string": "1,100" | ||
}) | ||
|
||
return properties | ||
|
||
|
||
func _set(property: StringName, value: Variant) -> bool: | ||
if property.begins_with(WEIGHTS_PREFIX): | ||
var weight_name = property.trim_prefix(WEIGHTS_PREFIX) | ||
_weights[weight_name] = value | ||
return true | ||
|
||
return false | ||
|
||
|
||
func _get(property: StringName): | ||
if property.begins_with(WEIGHTS_PREFIX): | ||
var weight_name = property.trim_prefix(WEIGHTS_PREFIX) | ||
return _weights[weight_name] | ||
|
||
return null | ||
|
||
|
||
func _update_weights(children: Array[Node]) -> void: | ||
var new_weights = {} | ||
for c in children: | ||
if _weights.has(c.name): | ||
new_weights[c.name] = _weights[c.name] | ||
else: | ||
new_weights[c.name] = 1 | ||
_weights = new_weights | ||
notify_property_list_changed() | ||
|
||
|
||
func _on_child_entered_tree(node: Node): | ||
_update_weights(get_children()) | ||
|
||
var renamed_callable = _on_child_renamed.bind(node.name, node) | ||
if not node.renamed.is_connected(renamed_callable): | ||
node.renamed.connect(renamed_callable) | ||
|
||
|
||
func _on_child_exiting_tree(node: Node): | ||
var renamed_callable = _on_child_renamed.bind(node.name, node) | ||
if node.renamed.is_connected(renamed_callable): | ||
node.renamed.disconnect(renamed_callable) | ||
|
||
var children = get_children() | ||
children.erase(node) | ||
_update_weights(children) | ||
|
||
|
||
func _on_child_renamed(old_name: String, renamed_child: Node): | ||
if old_name == renamed_child.name: | ||
return # No need to update the weights. | ||
|
||
# Disconnect signal with old name... | ||
renamed_child.renamed\ | ||
.disconnect(_on_child_renamed.bind(old_name, renamed_child)) | ||
# ...and connect with the new name. | ||
renamed_child.renamed\ | ||
.connect(_on_child_renamed.bind(renamed_child.name, renamed_child)) | ||
|
||
var original_weight = _weights[old_name] | ||
_weights.erase(old_name) | ||
_weights[renamed_child.name] = original_weight | ||
notify_property_list_changed() | ||
|
||
|
||
func get_class_name() -> Array[StringName]: | ||
var classes := super() | ||
classes.push_back(&"RandomizedComposite") | ||
return classes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
test/randomized_composites/runtime_changes/RuntimeChangesTestScene.gd
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
extends Node2D | ||
|
||
@onready var sequence_random: SequenceRandomComposite = %SequenceRandom |
34 changes: 34 additions & 0 deletions
34
test/randomized_composites/runtime_changes/RuntimeChangesTestScene.tscn
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
[gd_scene load_steps=5 format=3 uid="uid://dhhw4ej2jbyha"] | ||
|
||
[ext_resource type="Script" path="res://addons/beehave/nodes/beehave_tree.gd" id="1_10c1m"] | ||
[ext_resource type="Script" path="res://test/randomized_composites/runtime_changes/RuntimeChangesTestScene.gd" id="1_folsk"] | ||
[ext_resource type="Script" path="res://addons/beehave/nodes/composites/sequence_random.gd" id="2_k8ytk"] | ||
[ext_resource type="Script" path="res://test/actions/mock_action.gd" id="3_kqvkq"] | ||
|
||
[node name="RuntimeChangesTestScene" type="Node2D"] | ||
script = ExtResource("1_folsk") | ||
|
||
[node name="BeehaveTree" type="Node" parent="."] | ||
script = ExtResource("1_10c1m") | ||
|
||
[node name="SequenceRandom" type="Node" parent="BeehaveTree"] | ||
unique_name_in_owner = true | ||
script = ExtResource("2_k8ytk") | ||
random_seed = 12345 | ||
use_weights = true | ||
Weights/Idle = 1 | ||
Weights/Run = 1 | ||
"Weights/Attack Meele" = 1 | ||
"Weights/Attack Ranged" = 1 | ||
|
||
[node name="Idle" type="Node" parent="BeehaveTree/SequenceRandom"] | ||
script = ExtResource("3_kqvkq") | ||
|
||
[node name="Run" type="Node" parent="BeehaveTree/SequenceRandom"] | ||
script = ExtResource("3_kqvkq") | ||
|
||
[node name="Attack Meele" type="Node" parent="BeehaveTree/SequenceRandom"] | ||
script = ExtResource("3_kqvkq") | ||
|
||
[node name="Attack Ranged" type="Node" parent="BeehaveTree/SequenceRandom"] | ||
script = ExtResource("3_kqvkq") |
Oops, something went wrong.