277 lines
8.5 KiB
GDScript
277 lines
8.5 KiB
GDScript
extends CharacterBody2D
|
|
|
|
signal clicked(node:Node2D)
|
|
|
|
@export var marker_scene:PackedScene
|
|
|
|
## The navigation agent we use.
|
|
@onready var navigation_agent:NavigationAgent2D = $NavigationAgent2D
|
|
|
|
## The state chart
|
|
@onready var state_chart:StateChart = $StateChart
|
|
|
|
## Set of close food markers
|
|
var food_markers:Dictionary = {}
|
|
|
|
## Set of close nest markers
|
|
var nest_markers:Dictionary = {}
|
|
|
|
## Set of food nearby
|
|
var food:Dictionary = {}
|
|
|
|
## The nest, if nearby
|
|
var nest:Node2D = null
|
|
|
|
## The currently carried food
|
|
var carried_food:Node = null
|
|
|
|
const SEGMENT_LENGTH = 150
|
|
|
|
func _ready():
|
|
# start the state chart
|
|
state_chart.send_event.call_deferred("initialized")
|
|
|
|
|
|
## Called when we are seeking for food and need a new target.
|
|
func _on_idle_seeking_food():
|
|
|
|
var current_position := get_global_position()
|
|
|
|
# if we have food nearby grab it
|
|
if food.size() > 0:
|
|
state_chart.send_event("food_detected")
|
|
return
|
|
|
|
var target_position := Vector2()
|
|
# if we have food markers nearby travel into the general direction of the closest one points
|
|
if food_markers.size() > 0:
|
|
var closest_food_marker := _find_closest(food_markers.keys(), current_position)
|
|
var direction = Vector2.RIGHT.rotated(closest_food_marker.get_rotation())
|
|
target_position = current_position + (direction * SEGMENT_LENGTH)
|
|
|
|
# otherwise or if we couldn't reach the last target position, pick a random
|
|
# direction
|
|
if food_markers.size() == 0 or not navigation_agent.is_target_reachable():
|
|
# otherwise pick a random position in a radius of SEGMENT_LENGTH pixels
|
|
# first calculate a random angle in radians
|
|
var random_angle := randf() * 2 * PI
|
|
# then calculate the x and y components of the vector
|
|
var random_x := cos(random_angle) * SEGMENT_LENGTH
|
|
var random_y := sin(random_angle) * SEGMENT_LENGTH
|
|
|
|
# add the random vector to the current position
|
|
target_position = current_position + Vector2(random_x, random_y)
|
|
|
|
|
|
navigation_agent.set_target_position(target_position)
|
|
state_chart.set_expression_property("target_position", target_position)
|
|
state_chart.send_event("destination_set")
|
|
|
|
|
|
## Called when we have found food nearby and want to go to it
|
|
func _on_food_detected():
|
|
# set the target position to the closest food
|
|
var closest_food_position = _find_closest(food.keys(), get_global_position()).global_position
|
|
navigation_agent.set_target_position(closest_food_position)
|
|
|
|
|
|
## Called when we arrived at the food and want to pick it up
|
|
func _on_food_reached():
|
|
var closest_food = _find_closest(food.keys(), get_global_position())
|
|
if not is_instance_valid(closest_food):
|
|
# some other ant must have picked it up
|
|
state_chart.send_event("food_vanished")
|
|
return
|
|
|
|
closest_food.get_parent().remove_child(closest_food)
|
|
carried_food = closest_food
|
|
# remove it from the food set
|
|
food.erase(closest_food)
|
|
# it's collected, so remove it from the food group
|
|
closest_food.remove_from_group("food")
|
|
# add it to our ant so it moves with us
|
|
add_child(closest_food)
|
|
closest_food.position = Vector2.ZERO
|
|
closest_food.scale = Vector2(0.5, 0.5)
|
|
|
|
# place a marker pointing to the food (0 means point into the current direction)
|
|
var marker = _place_marker(Marker.MarkerType.FOOD, global_position, 0)
|
|
food_markers[marker] = true
|
|
|
|
# notify the state chart that we picked up food
|
|
state_chart.send_event("food_picked_up")
|
|
|
|
|
|
|
|
## Called when we are returning home and need a new target.
|
|
func _on_idle_returning_home():
|
|
var current_position := get_global_position()
|
|
|
|
# if the nest is nearby, drop off the food
|
|
if nest != null:
|
|
state_chart.send_event("nest_detected")
|
|
return
|
|
|
|
var target_position := Vector2()
|
|
# if we have nest markers nearby travel into the general direction of the closest one points
|
|
if nest_markers.size() > 0:
|
|
# refresh them
|
|
for marker in nest_markers.keys():
|
|
marker.refresh()
|
|
|
|
var closest_nest_marker := _find_closest(nest_markers.keys(), current_position)
|
|
var direction = Vector2.RIGHT.rotated(closest_nest_marker.get_rotation())
|
|
target_position = current_position + (direction * SEGMENT_LENGTH)
|
|
|
|
# if we have no nest markers or the navigation agent couldn't reach
|
|
# the position of the last target pick a random direction
|
|
if nest_markers.size() == 0 or not navigation_agent.is_target_reachable():
|
|
var random_angle := randf() * 2 * PI
|
|
# then calculate the x and y components of the vector
|
|
var random_x := cos(random_angle) * SEGMENT_LENGTH
|
|
var random_y := sin(random_angle) * SEGMENT_LENGTH
|
|
|
|
# add the random vector to the current position
|
|
target_position = current_position + Vector2(random_x, random_y)
|
|
|
|
navigation_agent.set_target_position(target_position)
|
|
state_chart.set_expression_property("target_position", target_position)
|
|
state_chart.send_event("destination_set")
|
|
return
|
|
|
|
## Called when we are returning home and detected the nest
|
|
func _on_nest_detected():
|
|
# travel to the nest
|
|
navigation_agent.set_target_position(nest.global_position)
|
|
state_chart.set_expression_property("target_position", nest.global_position)
|
|
|
|
|
|
## Called when we have arrived at the nest and want to drop off the food
|
|
func _on_nest_reached():
|
|
# drop off the food
|
|
carried_food.get_parent().remove_child(carried_food)
|
|
carried_food.queue_free()
|
|
carried_food = null
|
|
# notify the state chart that we dropped off the food
|
|
state_chart.send_event("food_dropped")
|
|
|
|
|
|
## Called while travelling to a destination
|
|
func _on_travelling_state_physics_processing(_delta):
|
|
# get the next position on the path
|
|
var path_position = navigation_agent.get_next_path_position()
|
|
# and move towards it
|
|
velocity = (path_position - get_global_position()).normalized() * navigation_agent.max_speed
|
|
look_at(path_position)
|
|
move_and_slide()
|
|
|
|
|
|
func _on_input_event(_viewport, event, _shape_idx):
|
|
# if the left mouse button is up emit the clicked signal
|
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed() == false:
|
|
# print("clicked")
|
|
clicked.emit(self)
|
|
|
|
|
|
## Called when the ant is sensing something nearby.
|
|
func _on_sensor_area_area_entered(area:Area2D):
|
|
var node = area
|
|
if area.has_meta("owner"):
|
|
node = area.get_node(area.get_meta("owner"))
|
|
|
|
|
|
if node.is_in_group("marker"):
|
|
# it's a marker
|
|
if node.is_in_group("food"):
|
|
food_markers[node] = true
|
|
elif node.is_in_group("nest"):
|
|
nest_markers[node] = true
|
|
elif node.is_in_group("food"):
|
|
# it's food
|
|
food[node] = true
|
|
elif node.is_in_group("nest"):
|
|
# it's the nest
|
|
nest = node
|
|
|
|
state_chart.set_expression_property("nest_markers", nest_markers.size())
|
|
state_chart.set_expression_property("food_markers", food_markers.size())
|
|
|
|
|
|
|
|
func _on_sensor_area_area_exited(area:Area2D):
|
|
var node = area
|
|
if area.has_meta("owner"):
|
|
node = area.get_node(area.get_meta("owner"))
|
|
|
|
if node.is_in_group("marker"):
|
|
# it's a marker
|
|
if node.is_in_group("food"):
|
|
food_markers.erase(node)
|
|
elif node.is_in_group("nest"):
|
|
nest_markers.erase(node)
|
|
elif node.is_in_group("food"):
|
|
# it's food
|
|
food.erase(node)
|
|
elif node.is_in_group("nest"):
|
|
# it's the nest
|
|
nest = null
|
|
|
|
state_chart.set_expression_property("nest_markers", nest_markers.size())
|
|
state_chart.set_expression_property("food_markers", nest_markers.size())
|
|
|
|
|
|
|
|
|
|
## Finds the closest position to the given position from the given list of nodes.
|
|
func _find_closest(targets:Array, from:Vector2) -> Node2D:
|
|
var shortest_distance := 99999999.00
|
|
var result = null
|
|
|
|
for target in targets:
|
|
var distance := from.distance_squared_to(target.get_global_position())
|
|
if distance < shortest_distance:
|
|
shortest_distance = distance
|
|
result = target
|
|
|
|
return result
|
|
|
|
|
|
## Places a marker of the given type at the given position
|
|
func _place_marker(type:Marker.MarkerType, target_position:Vector2, offset:float = PI) -> Marker:
|
|
var marker = marker_scene.instantiate()
|
|
marker.initialize(type)
|
|
# add to the tree on our parent
|
|
get_parent().add_child.call_deferred(marker)
|
|
# set the position to our current position
|
|
marker.set_global_position(target_position)
|
|
# set the marker rotation to look opposite to the direction we are facing
|
|
marker.set_rotation(get_rotation() + offset)
|
|
return marker
|
|
|
|
|
|
func _place_nest_marker():
|
|
# if there are already nest markers around, just refresh them
|
|
if nest_markers.size() > 0:
|
|
for marker in nest_markers:
|
|
marker.refresh()
|
|
else:
|
|
# otherwise place a new one
|
|
_place_marker(Marker.MarkerType.NEST, global_position)
|
|
|
|
|
|
func _place_food_marker():
|
|
_place_marker(Marker.MarkerType.FOOD, global_position)
|
|
|
|
|
|
func _maintenance(_delta):
|
|
# remove all markers which are no longer valid
|
|
for marker in food_markers.keys():
|
|
if not is_instance_valid(marker):
|
|
food_markers.erase(marker)
|
|
|
|
for marker in nest_markers.keys():
|
|
if not is_instance_valid(marker):
|
|
nest_markers.erase(marker)
|
|
|
|
|