Bee State Machine

This commit is contained in:
Dan 2024-05-03 15:43:05 +01:00
parent 20bcab01b1
commit 752131c955
16 changed files with 467 additions and 13 deletions

View file

@ -1,3 +1,48 @@
[gd_scene format=3 uid="uid://deek6uv574xas"]
[gd_scene load_steps=8 format=3 uid="uid://deek6uv574xas"]
[node name="Bee" type="Node2D"]
[ext_resource type="Script" path="res://entities/scripts/bee.gd" id="1_pnu7x"]
[ext_resource type="Script" path="res://entities/scripts/finite_state_machine.gd" id="1_t3s5d"]
[ext_resource type="Script" path="res://entities/bee/states/bee_idle.gd" id="3_vasc5"]
[ext_resource type="Script" path="res://entities/bee/states/bee_traveling.gd" id="4_rr0w6"]
[ext_resource type="Script" path="res://entities/bee/states/bee_gather.gd" id="5_4vs4l"]
[ext_resource type="Script" path="res://entities/scripts/bee_hit_box.gd" id="5_agq38"]
[sub_resource type="CircleShape2D" id="CircleShape2D_86nxf"]
radius = 22.0907
[node name="Bee" type="CharacterBody2D"]
collision_mask = 0
script = ExtResource("1_pnu7x")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
light_mask = 0
shape = SubResource("CircleShape2D_86nxf")
[node name="HitBox" type="Area2D" parent="."]
collision_layer = 6
collision_mask = 7
script = ExtResource("5_agq38")
[node name="CollisionShape2D" type="CollisionShape2D" parent="HitBox"]
light_mask = 0
shape = SubResource("CircleShape2D_86nxf")
[node name="StateMachine" type="Node2D" parent="." node_paths=PackedStringArray("initial_state")]
script = ExtResource("1_t3s5d")
initial_state = NodePath("Idle")
[node name="Idle" type="Node" parent="StateMachine"]
script = ExtResource("3_vasc5")
[node name="Traveling" type="Node" parent="StateMachine"]
script = ExtResource("4_rr0w6")
[node name="Gathering" type="Node" parent="StateMachine"]
script = ExtResource("5_4vs4l")
[node name="Polygon2D" type="Polygon2D" parent="."]
color = Color(1, 1, 0.0745098, 1)
polygon = PackedVector2Array(-18, -11, -6, -21, 17, -19, 23, 1, 3, 12, -18, 7)
[connection signal="area_entered" from="HitBox" to="HitBox" method="_on_area_entered"]
[connection signal="area_exited" from="HitBox" to="HitBox" method="_on_area_exited"]

View file

@ -1,8 +1,19 @@
[gd_scene format=3 uid="uid://nxq2fd04ehcu"]
[gd_scene load_steps=2 format=3 uid="uid://nxq2fd04ehcu"]
[ext_resource type="Script" path="res://entities/scripts/director_drone.gd" id="1_3v6jp"]
[node name="DirectorDrone" type="Node2D"]
script = ExtResource("1_3v6jp")
[node name="Polygon2D" type="Polygon2D" parent="."]
position = Vector2(1, -1)
color = Color(0.703926, 0.656042, 0.441124, 1)
polygon = PackedVector2Array(-28, -25, 25, -28, 26, 33, -32, 19)
[node name="Label" type="Label" parent="."]
offset_left = -16.0
offset_top = -12.0
offset_right = 24.0
offset_bottom = 11.0
theme_override_colors/font_color = Color(0, 0, 0, 1)
text = "999"

View file

@ -0,0 +1,29 @@
extends State
class_name BeeGathering
@export var animator : AnimationPlayer
@onready var bee = get_parent().get_parent() as Bee # I think this is bad but I dont care it works
var time_at_patch : float = 0.0
func enter(_msg := {}):
Log.pr("I am going to attempt to gather some stuff!")
#animator.play("Idle")
#if !bee.in_range_of_flowers:
# ## Go home bee, you're drunk
# state_transition.emit(self, "Idle")
func update(_delta : float):
if bee.in_range_of_flowers:
#animator.play("Gathering")
time_at_patch += _delta
if time_at_patch > 5.0:
bee.nectar += 1
state_transition.emit(self, "Idle")
else:
state_transition.emit(self, "Idle")

View file

@ -0,0 +1,37 @@
extends State
class_name BeeIdle
@export var animator : AnimationPlayer
@onready var bee = get_parent().get_parent() # I think this is bad but I dont care it works
var idle_time : float = 0.0
func _ready():
Log.pr(bee, bee.nectar)
func enter(_msg := {}):
Log.pr("I sit by idl-bee")
#animator.play("Idle")
pass
func update(delta : float):
idle_time += delta
if idle_time > 2.0:
Log.pr("Been idle for 2 seconds, check for something to do...")
find_something_to_do()
idle_time = 0.0
pass
func find_something_to_do():
if bee.nectar > 0:
## Bee has pollen - head home
Log.pr(bee, "I have nectar, I should return to the hive")
state_transition.emit(self, "Traveling")
else:
## Bee has no pollen - they should move to a flower
Log.pr(bee, "I have no nectar, I should find a flower")
state_transition.emit(self, "Traveling")
pass

View file

@ -0,0 +1,37 @@
extends State
class_name BeeTraveling
@export var animator : AnimationPlayer
@export var target : Drone = null
@onready var bee = get_parent().get_parent() as Bee # I think this is bad but I dont care it works
var t = 0
func enter(_msg := {}):
Log.pr("I am on the move!")
## Get the next target location from the bee
target = bee.get_next_target()
#animator.play("Idle")
pass
func update(delta : float):
if target:
if bee.position.distance_to(target.position) > 3:
bee.velocity = (target.get_global_position() - bee.position).normalized() * bee.speed * delta
bee.move_and_collide(bee.velocity)
bee.look_at(target.position)
else:
# Bee has arrived at location, if its the hive or a collector drone do the things
if(target.name == "CollectorDrone"):
state_transition.emit(self, "Gathering")
else:
state_transition.emit(self, "Idle")
pass

55
entities/scripts/bee.gd Normal file
View file

@ -0,0 +1,55 @@
extends Node2D
class_name Bee
@onready var fsm = $StateMachine as FiniteStateMachine
@onready var drone_manager = get_tree().get_first_node_in_group("dronemanager") as DroneManager
@export var nectar : int = 0
@export var speed : int = 30
var latest_target_director : int = 0
# This is updated when the bee enters or exits a flower patch
var in_range_of_flowers : bool = false
func _ready():
Log.pr("I have never bee-n so ready!")
Log.pr(fsm.current_state.name)
## Get the next target to move to
## If we have no nectar, we need to go up the director list
## If we have nectar, we need to go down the director list
## If we are at the lower director, we need to go the hive
## If we are at the highest director, we need to go to a flower
func get_next_target():
if nectar == 0:
Log.pr("I have no nectar")
## If there is a next directory drone, lets go to it
var next_drone = drone_manager.get_next_director(latest_target_director)
if next_drone:
Log.pr("Next drone target", next_drone)
latest_target_director = next_drone.visit_order
return next_drone
## If there is no next drone, check for a collector drone
var collector_drone = drone_manager.get_collector()
if collector_drone:
Log.pr("Collector drone target", collector_drone)
return collector_drone
else:
Log.pr("I have nectar")
## Let's go home, we need the previous director drones location
var previous_drone = drone_manager.get_previous_director(latest_target_director)
if previous_drone:
Log.pr("Previous drone target", previous_drone)
latest_target_director = previous_drone.visit_order
return previous_drone
pass

View file

@ -0,0 +1,19 @@
extends Area2D
@onready var bee = get_parent() as Bee
func _ready():
pass
func _on_area_entered(area:Area2D):
## Check if the area entered is a flower patch
if area.is_in_group("flowers"):
bee.in_range_of_flowers = true
Log.pr("I found some flowers!")
func _on_area_exited(area:Area2D):
## Check if the area exited is a flower patch
if area.is_in_group("flowers"):
bee.in_range_of_flowers = false
Log.pr("I left the flowers!")

View file

@ -1,11 +1,17 @@
class_name DirectorDrone extends Drone
@export var visit_order : int = 0 :
get:
return visit_order
set(value):
visit_order = value
update_label_value()
# Called when the node enters the scene tree for the first time.
func _ready():
pass # Replace with function body.
pass
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta):
pass
func update_label_value():
$Label.text = str(visit_order)

View file

@ -0,0 +1,69 @@
@icon("res://resources/icons/fsm.png")
extends Node
class_name FiniteStateMachine
var states : Dictionary = {}
var current_state : State
@export var initial_state : State
#NOTE This is a generic finite_state_machine, it handles all states, changes to this code will affect
# everything that uses a state machine!
func _ready():
Log.pr("FSM Ready")
for child in get_children():
if child is State:
states[child.name.to_lower()] = child
child.state_transition.connect(change_state)
Log.pr(states)
if initial_state:
initial_state.enter()
current_state = initial_state
# Call the current states update function
func _process(delta):
if current_state:
current_state.update(delta)
# Use force_change_state cautiously, it immediately switches to a state regardless of any transitions.
# This is used to force us into a 'death state' when killed
func force_change_state(new_state : String):
var newState = states.get(new_state.to_lower())
if !newState:
print(new_state + " does not exist in the dictionary of states")
return
if current_state == newState:
print("State is same, aborting")
return
# NOTE Calling exit like so: (current_state.Exit()) may cause warnings when flushing queries, like when the enemy is being removed after death.
# call_deferred is safe and prevents this from occuring. We get the Exit function from the state as a callable and then call it in a thread-safe manner
if current_state:
var exit_callable = Callable(current_state, "exit")
exit_callable.call_deferred()
newState.enter()
current_state = newState
func change_state(source_state : State, new_state_name : String):
if source_state != current_state:
#print("Invalid change_state trying from: " + source_state.name + " but currently in: " + current_state.name)
#This typically only happens when trying to switch from death state following a force_change
return
var new_state = states.get(new_state_name.to_lower())
if !new_state:
print("New state is empty")
return
if current_state:
current_state.exit()
new_state.enter()
current_state = new_state

15
entities/scripts/state.gd Normal file
View file

@ -0,0 +1,15 @@
@icon("res://resources/icons/fsm.png")
extends Node
class_name State
signal state_transition
func enter(_msg := {}):
pass
func exit():
pass
func update(_delta:float):
pass