Basic flying enemy with navigation

This commit is contained in:
Jakob Feldmann 2023-02-21 18:37:10 +01:00
parent d3df3b1424
commit 9bb25a7cbe
6 changed files with 153 additions and 388 deletions

View File

@ -3,382 +3,66 @@ const PhysicsFunc = preload("res://src/Utilities/Physic/PhysicsFunc.gd")
onready var players = get_tree().get_nodes_in_group("player")
onready var vision_raycast: RayCast2D = $VisionRayCast
onready var orientation: RayCast2D = $Orientation
onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
onready var feeler_raycast: RayCast2D = $FeelerRayCast
onready var tilemap: TileMap = $"../%TileMap"
onready var jump_timer: Timer
onready var target_lost_timer: Timer
onready var rng = RandomNumberGenerator.new()
export var score := 100
# Is given in blocks
export var vision_distance := 6.0
# Jump distance in blocks
export var default_jump_distance := 3.0
export var default_jump_angle := 70.0
export var jump_time_search := 0.7
export var jump_time_hunt := 0.3
export var jump_time_standard_deviation := 0.1
# Also in blocks
var movement_radius: float
var anchor: Node2D
var is_bound := false
var was_restricted := false
var has_reversed := false
var target: Object = null
var start_x := 0.0
var in_air := false
var is_hurt := false
var stored_x_vel = 0.0
var has_reversed := false
var avoidance_raycasts := []
var current_delta = 0.0
var reversing_possible_searching := true
# TODO Make parameters tunable!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1111!!
func _ready():
default_jump_distance = default_jump_distance * tilemap.cell_size.x
jump_timer = Timer.new()
jump_timer.set_one_shot(true)
jump_timer.connect("timeout", self, "jump")
target_lost_timer = Timer.new()
target_lost_timer.set_one_shot(true)
target_lost_timer.connect("timeout", self, "lose_target")
add_child(jump_timer)
add_child(target_lost_timer)
func bind_to_anchor(anchor_node: Node2D, radius: float ) -> void:
anchor = anchor_node
movement_radius = radius * 24
is_bound = true
func _on_StompDetector_body_entered(body: Node) -> void:
if body.global_position.y > get_node("StompDetector").global_position.y:
return
if body.is_in_group("player"):
remove_from_group("harmful")
is_hurt = true
func execute_movement(delta: float) -> void:
# Navigation2DServer.map_get_path()
current_delta = delta
velocity.y += _gravity * delta
if(is_bound):
var next_position = global_position + velocity * current_delta
var current_distance = global_position.distance_to(anchor.global_position)
var new_distance = next_position.distance_to(anchor.global_position)
# TODO Fix this in respects to x and y distances and movement dampening
# Maybe use mathemathematics or something idfc
if(current_distance >= movement_radius && new_distance > current_distance):
velocity.x = velocity.x * 0.8
velocity.y = velocity.y * 0.8
was_restricted = true
velocity = move_and_slide(velocity, FLOOR_NORMAL, false, 4, 0.785398,false)
if(is_on_floor()):
velocity = Vector2(0,0)
# Reverse direction when hitting limit
func die() -> void:
GlobalState.score += score
queue_free()
func _on_EnemySkin_area_entered(area:Area2D) -> void:
if area.is_in_group("harmful"):
get_node("EnemyBody").disabled = true
die()
spawn_avoidance_raycasts(32, 20)
pass
func searching() -> Vector2:
detect_player()
# TODO Less often!
return nav_agent.get_next_location()
if(is_on_floor()):
if(jump_timer.is_stopped()):
jump_timer.start(rng.randfn(jump_time_search, jump_time_standard_deviation))
if(in_air):
in_air = false
else:
if(!in_air):
start_x = global_position.x
reversing_possible_searching = true
jump_timer.stop()
in_air = true
func execute_movement(_delta: float) -> void:
nav_agent.set_target_location(players[0].global_position)
var next_pos = velocity - global_position
var avoidance_obstacle_distance = average_collision_vector(avoidance_raycasts)
next_pos = next_pos.normalized() + avoidance_obstacle_distance.rotated(PI).normalized()
check_feeler((next_pos * 100))
#TODO
velocity = move_and_slide(next_pos.normalized() * 50, FLOOR_NORMAL, false, 4, 0.785398,false)
return velocity
func hunting() -> Vector2:
detect_player()
if(is_on_floor()):
if(jump_timer.is_stopped()):
jump_timer.start(rng.randfn(jump_time_hunt, jump_time_standard_deviation))
if(in_air):
in_air = false
else:
if(!in_air):
start_x = global_position.x
reversing_possible_searching = true
jump_timer.stop()
in_air = true
return velocity
func detect_player() -> void:
var player
if(players.empty()):
print("no player found")
return
player = players[0]
vision_raycast.cast_to = (player.global_position - global_position).normalized() * 24 * vision_distance
var ray_angle_to_facing = vision_raycast.cast_to.angle_to(orientation.cast_to)
vision_raycast.force_raycast_update()
var collider = vision_raycast.get_collider()
if(target == null && abs(ray_angle_to_facing) < PI/4 && collider != null && collider.is_in_group("player")):
target_lost_timer.stop()
target = collider
print("target found")
elif(target != null && target_lost_timer.is_stopped()):
target_lost_timer.start(3.0)
func sleeping() -> Vector2:
jump_timer.stop()
detect_player()
return velocity
func lose_target() -> void:
print("target lost")
target = null
func jump():
# print("jump calculation initiated")
# Can only reverse once per jump calculation
has_reversed = false
var zero_vector = Vector2(0,0)
var v: Vector2 = velocity_for_jump_distance(default_jump_distance, deg2rad(default_jump_angle))
v = correct_jump_direction(v)
if(is_bound):
var next_position = global_position + v * current_delta
var current_distance = global_position.distance_to(anchor.global_position)
var new_distance = next_position.distance_to(anchor.global_position)
# print(current_distance)
# print(new_distance)
if((new_distance >= movement_radius && new_distance > current_distance) || (new_distance > current_distance && was_restricted)):
if can_reverse_facing_direction():
reverse_facing_direction()
was_restricted = false
if (get_facing_direction() < 0 && $Left_Wallcast.is_colliding()):
v = zero_vector
if (get_facing_direction() > 0 && $Right_Wallcast.is_colliding()):
v = zero_vector
if ($Right_Wallcast.is_colliding() && $Left_Wallcast.is_colliding()):
print("help this is a really tight space :(")
return velocity
v = correct_jump_direction(v)
if(v != zero_vector):
v = consider_jump_headspace(v)
if(v != zero_vector):
v = consider_jump_landing_space(v)
if(v == zero_vector):
# TODO fix that you could call jump from jumping on top
# and let it fail if the top is dangerous for jump height or not safe
v = consider_jumping_on_top()
if(v == zero_vector && can_reverse_facing_direction()):
reverse_facing_direction()
jump()
velocity = v
func correct_jump_direction(v: Vector2) -> Vector2:
if sign(v.x) != get_facing_direction():
v.x *= -1
return v
# Cast a ray to the highest point of the jump
# Check the highest point for collision
# Calculate safe jump height and then a safe jump velocity
func consider_jump_headspace(v: Vector2) -> Vector2:
var height = calculate_jump_height(v)
var distance = calculate_jump_distance(v)
# Half distance is an estimate of the jumps apex()
var height_collider = check_feeler(Vector2(get_facing_direction()*(distance/2), (-height)), Vector2(0,-9))
if(height_collider != null):
# check half jump height
var half_height_v = jump_height_to_velocity(height/3, v)
var half_height = calculate_jump_height(half_height_v)
height_collider = check_feeler(Vector2(get_facing_direction()*(distance/2), (-half_height)), Vector2(0,-9))
if(height_collider != null && can_reverse_facing_direction()):
print("no safe height for frog jump")
return Vector2(0,0)
else:
var collision_point = feeler_raycast.get_collision_point()
#TODO Consider sprite size for height
var target_height = collision_point.y - (feeler_raycast.global_position.y - 9)
v = jump_height_to_velocity(abs(target_height), v)
return v
# Check the block in jump distance for danger or height
# If danger check neighboring blocks: if still danger, then jump closer (or jump over)
# If height move to distance which allows 1 block high jump
func consider_jump_landing_space(v: Vector2) -> Vector2:
var jump_distance = calculate_jump_distance(v)
var jump_height = calculate_jump_height(v)
var collider = check_feeler(Vector2(jump_distance * get_facing_direction(), - jump_height/2))
# TODO Unpacked loop, make function or something?
# Shortens the jump in steps to make it more safe
if(!is_jump_path_safe(v, global_position) || collider != null):
jump_distance = calculate_jump_distance(v) - 24
v = change_jump_distance(jump_distance, v)
jump_height = calculate_jump_height(v)
v = correct_jump_direction(v)
collider = check_feeler(Vector2(jump_distance * get_facing_direction(), - jump_height/2))
if(!is_jump_path_safe(v, global_position) || collider != null):
jump_distance = calculate_jump_distance(v) - 12
v = change_jump_distance(jump_distance, v)
jump_height = calculate_jump_height(v)
v = correct_jump_direction(v)
collider = check_feeler(Vector2(jump_distance * get_facing_direction(), - jump_height/2))
if((!is_jump_path_safe(v, global_position) || collider != null) && can_reverse_facing_direction()):
return Vector2(0,0)
return v
# Tries to shorten the jump, so that it lands in a tiles center
func jump_to_tile_center(v: Vector2) -> Vector2:
var distance = stepify(calculate_jump_distance(v), 0.01)
if !is_equal_approx(fmod(abs(global_position.x + distance * get_facing_direction()), 24), 12):
# print(distance)
# print(global_position.x + distance)
# print(fmod((global_position.x + distance), 24))
var new_distance = distance
if(get_facing_direction() < 0):
new_distance = fmod((global_position.x + distance), 24) - 12 + distance
else:
new_distance = distance + 12 - fmod((global_position.x + distance), 24)
# print("centering distance")
# print(new_distance)
v = change_jump_distance(abs(new_distance), v)
v = correct_jump_direction(v)
return v
# TODO Depends on Frog Shape and Tile Shape
func is_jump_path_safe(v: Vector2, pos: Vector2) -> bool:
var v0 = v.length()
var angle = v.angle()
var jump_distance = calculate_jump_distance(v)
var harmful_nodes = get_tree().get_nodes_in_group("harmful")
for node in harmful_nodes:
var node_pos = node.global_position
if abs(node_pos.x - pos.x) > abs(jump_distance) * 3 || abs(node_pos.x - pos.x) < 1:
func average_collision_vector(var raycasts: Array) -> Vector2:
var total_distances = Vector2()
for raycast in raycasts:
if !raycast.is_colliding():
continue
var node_y = node_pos.y - 12
var initial_throw_height = node_y - (global_position.y + 9)
var term1 = (pow(v0, 2) * sin(2 * angle)) / (2 * _gravity)
var term2 = ((v0 * cos(angle))/_gravity) * sqrt(pow(v0, 2) * pow(sin(angle), 2) + 2 * _gravity * initial_throw_height)
var distance = abs(term1) + abs(term2)
# print("distance to next spike")
# print(pos.x + sign(v.x) * distance - node_pos.x)
var safe_distance = 12
if (sign(initial_throw_height) < 0):
safe_distance = 24
if(abs(pos.x + sign(v.x) * distance - node_pos.x) < safe_distance):
return false
return true
func calculate_jump_height(v: Vector2) -> float:
return abs((pow(v.length(), 2) * pow(sin(v.angle()), 2))/(2*_gravity))
func consider_jumping_on_top() -> Vector2:
var collider = check_feeler(Vector2(36 * get_facing_direction(),0))
var facing = 0 if get_facing_direction() >= 0 else - 1
if (collider == null):
return Vector2(0,0)
var local_position = tilemap.to_local(feeler_raycast.get_collision_point())
var map_position = tilemap.world_to_map(local_position)
var tile_position = Vector2(map_position.x + facing, map_position.y)
# print(tile_position)
# TODO Here the climb height of frog is limited to one constantly
if (tilemap.get_cell(tile_position.x, tile_position.y - 1) != -1):
# print("wall is more than one high")
return Vector2(0,0)
# print("wall is only one high")
var tile_upper_left_corner = tilemap.to_global(tilemap.map_to_world(tile_position))
var tile_upper_right_corner = Vector2(tile_upper_left_corner.x + tilemap.cell_size.x, tile_upper_left_corner.y)
var jump_angle = 0
if(facing < 0):
var frog_bottom_left_corner = Vector2($EnemyBody.global_position.x - $EnemyBody.shape.extents.x,
$EnemyBody.global_position.y + $EnemyBody.shape.extents.y)
jump_angle = frog_bottom_left_corner.angle_to_point(tile_upper_right_corner)
else:
var frog_bottom_right_corner = Vector2($EnemyBody.global_position.x + $EnemyBody.shape.extents.x,
$EnemyBody.global_position.y + $EnemyBody.shape.extents.y)
jump_angle = frog_bottom_right_corner.angle_to_point(tile_upper_left_corner) - PI
# print(rad2deg(jump_angle))
# if(abs(rad2deg(jump_angle)) < default_jump_angle):
# return correct_jump_direction(velocity_for_jump_distance(default_jump_distance/2, abs(deg2rad(default_jump_angle))))
if(abs(rad2deg(jump_angle)) < 78):
return correct_jump_direction(velocity_for_jump_distance(default_jump_distance/2, abs(deg2rad(80))))
else:
return velocity_for_jump_distance(8, abs(deg2rad(45))) * -1 * facing
return Vector2(0,0)
# Check if there is another obstacle above the block or do a regular jump with adjusted parameters instead(for checking)
# Cast from bottom corner to upper tile corner
# Check the angle of the raycast
# Return small jump backwards if the angle is too steep
# Make the angle 1 deg steeper
# Return a jump along the angled raycast
# Only works for jumps on straight ground
func calculate_jump_distance(v: Vector2) -> float:
return abs((pow(v.length(), 2) * sin(-1 * 2 * v.angle()))/(_gravity))
func jump_height_to_velocity(target_height: float, v: Vector2) -> Vector2:
var initial_height = calculate_jump_height(v)
return v.normalized() * sqrt(pow(v.length(),2)/(initial_height/target_height))
# Changes a Vector for a jump to the targeted distance, keeping the angle
func change_jump_distance(target_distance: float, v: Vector2) -> Vector2:
var initial_distance = calculate_jump_distance(v)
return v.normalized() * sqrt(pow(v.length(),2)/(initial_distance/target_distance))
# Takes an angle and a distance to calculate a jump launching at that angle and covering the distance
func velocity_for_jump_distance(distance: float = 3*24, angle: float = deg2rad(65)) -> Vector2:
var abs_velocity = sqrt((distance * _gravity)/sin(2*angle))
return Vector2(abs_velocity,0).rotated(-1*angle)
var collision_point = self.to_local(raycast.get_collision_point())
total_distances += collision_point
return total_distances/raycasts.size()
func can_reverse_facing_direction() -> bool:
if(is_on_floor() && !has_reversed):
return true
return false
func spawn_avoidance_raycasts(var raycount: int, var length: float = 24) -> void:
var direction: float = 0
while direction <= 2*PI:
var raycast: RayCast2D = RayCast2D.new()
raycast.enabled = true
raycast.exclude_parent = true
raycast.collide_with_areas = true
raycast.collide_with_bodies = true
# Layers 4, 5 & 6
raycast.collision_mask = 56
raycast.cast_to = Vector2(length, 0).rotated(direction)
add_child(raycast)
avoidance_raycasts.append(raycast)
direction += (2*PI)/raycount
# Checks the feeler ray for collisions and returns collision or null
func check_feeler(v: Vector2, _offset = Vector2(0,0)) -> Object:
@ -388,14 +72,3 @@ func check_feeler(v: Vector2, _offset = Vector2(0,0)) -> Object:
feeler_raycast.force_raycast_update()
feeler_raycast.position = prev_position
return feeler_raycast.get_collider()
func reverse_facing_direction() -> void:
has_reversed = true
print("reversing direction")
orientation.cast_to.x *= -1
pass
func get_facing_direction() -> float:
return orientation.cast_to.x

View File

@ -1,12 +1,12 @@
[gd_scene load_steps=8 format=2]
[ext_resource path="res://src/Actors/Enemies/Beings/WhatAreFrog.gd" type="Script" id=1]
[ext_resource path="res://src/Actors/Enemies/Beings/Flyer.gd" type="Script" id=1]
[ext_resource path="res://src/Actors/Enemies/Beings/FlyerStateMachine.gd" type="Script" id=2]
[ext_resource path="res://assets/meta/new_dynamicfont.tres" type="DynamicFont" id=3]
[ext_resource path="res://assets/enemy/enemy.png" type="Texture" id=4]
[ext_resource path="res://assets/blobby/idle/blobby1.png" type="Texture" id=4]
[sub_resource type="RectangleShape2D" id=1]
extents = Vector2( 12, 7 )
extents = Vector2( 12, 9 )
[sub_resource type="RectangleShape2D" id=2]
extents = Vector2( 15, 5.12039 )
@ -17,13 +17,13 @@ extents = Vector2( 18.2143, 14.3338 )
[node name="Flyer" type="KinematicBody2D" groups=["harmful"]]
collision_layer = 2
collision_mask = 9
collision/safe_margin = 0.001
script = ExtResource( 1 )
[node name="Statemachine" type="Node2D" parent="."]
script = ExtResource( 2 )
[node name="StateLabel" type="Label" parent="."]
visible = false
show_behind_parent = true
margin_left = -36.0
margin_top = -30.0
@ -37,8 +37,8 @@ align = 1
valign = 1
[node name="FlyerSprite" type="Sprite" parent="."]
position = Vector2( 0, -1.90735e-06 )
scale = Vector2( 0.201, 0.193 )
position = Vector2( -1, -1 )
scale = Vector2( 1, 1.34375 )
texture = ExtResource( 4 )
[node name="VisibilityEnabler2D" type="VisibilityEnabler2D" parent="."]
@ -54,15 +54,12 @@ cast_to = Vector2( 0, -1 )
collision_mask = 56
collide_with_areas = true
[node name="Orientation" type="RayCast2D" parent="."]
cast_to = Vector2( -1, 0 )
collision_mask = 0
collide_with_bodies = false
[node name="EnemyBody" type="CollisionShape2D" parent="." groups=["harmful"]]
position = Vector2( 0, 4.5 )
shape = SubResource( 1 )
[node name="NavigationAgent2D" type="NavigationAgent2D" parent="."]
path_max_distance = 100.0
[node name="cshape" type="Node2D" parent="."]
position = Vector2( 0, -3.8147e-06 )
scale = Vector2( 0.1, 0.1 )
@ -74,7 +71,7 @@ softness = 0.1
[node name="StompDetector" type="Area2D" parent="." groups=["weakpoint"]]
modulate = Color( 0, 0.0392157, 1, 1 )
position = Vector2( 0, -6.44095 )
position = Vector2( 0, -9 )
scale = Vector2( 0.7, 0.7 )
collision_layer = 2
input_pickable = false
@ -83,14 +80,13 @@ input_pickable = false
position = Vector2( -4.76837e-07, 1.56134 )
shape = SubResource( 2 )
[node name="EnemySkin" type="Area2D" parent="." groups=["player"]]
[node name="EnemySkin" type="Area2D" parent="."]
process_priority = -1
scale = Vector2( 0.7, 0.7 )
collision_layer = 2
collision_mask = 126
[node name="CollisionPolygon2D" type="CollisionShape2D" parent="EnemySkin"]
position = Vector2( 0, 2.85714 )
shape = SubResource( 3 )
[connection signal="body_entered" from="StompDetector" to="." method="_on_StompDetector_body_entered"]

View File

@ -271,14 +271,11 @@ func consider_jumping_on_top() -> Vector2:
var local_position = tilemap.to_local(feeler_raycast.get_collision_point())
var map_position = tilemap.world_to_map(local_position)
var tile_position = Vector2(map_position.x + facing, map_position.y)
print(tile_position)
# TODO Here the climb height of frog is limited to one constantly
if (tilemap.get_cell(tile_position.x, tile_position.y - 1) != -1 &&
#TODO 9 is the navigation tile!
tilemap.get_cell(tile_position.x, tile_position.y - 1) != 9):
print("wall is more than one high")
return Vector2(0,0)
print("wall is only one high")
var tile_upper_left_corner = tilemap.to_global(tilemap.map_to_world(tile_position))
var tile_upper_right_corner = Vector2(tile_upper_left_corner.x + tilemap.cell_size.x, tile_upper_left_corner.y)
@ -287,12 +284,10 @@ func consider_jumping_on_top() -> Vector2:
var frog_bottom_left_corner = Vector2($EnemyBody.global_position.x - $EnemyBody.shape.extents.x,
$EnemyBody.global_position.y + $EnemyBody.shape.extents.y)
jump_angle = frog_bottom_left_corner.angle_to_point(tile_upper_right_corner)
print(rad2deg(jump_angle))
else:
var frog_bottom_right_corner = Vector2($EnemyBody.global_position.x + $EnemyBody.shape.extents.x,
$EnemyBody.global_position.y + $EnemyBody.shape.extents.y)
jump_angle = frog_bottom_right_corner.angle_to_point(tile_upper_left_corner) - PI
print(rad2deg(jump_angle))
if(abs(rad2deg(jump_angle)) < 78):
return correct_jump_direction(velocity_for_jump_distance(default_jump_distance/2, abs(deg2rad(80))))

View File

@ -461,7 +461,7 @@ input_pickable = false
position = Vector2( 1.19209e-07, -1.42857 )
shape = SubResource( 2 )
[node name="EnemySkin" type="Area2D" parent="." groups=["player"]]
[node name="EnemySkin" type="Area2D" parent="."]
process_priority = -1
scale = Vector2( 0.7, 0.7 )
collision_layer = 2
@ -469,7 +469,6 @@ collision_mask = 126
[node name="CollisionPolygon2D" type="CollisionShape2D" parent="EnemySkin"]
position = Vector2( 0, -0.738329 )
scale = Vector2( 1, 1 )
shape = SubResource( 3 )
[connection signal="body_entered" from="StompDetector" to="." method="_on_StompDetector_body_entered"]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long