Files

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)