From 3ac12a8491f1ee8e8484ab65b54fc1fd152c65d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agnis=20Aldi=C5=86=C5=A1=20=22NeZv=C4=93rs?= Date: Sat, 14 Sep 2024 01:56:45 +0300 Subject: [PATCH] simple bot navigation --- .../static/GameMath/GameMath.gd | 11 +++ scenes/actors/ai/enemy_ai.tscn | 51 ++++++++++++++ scenes/actors/zombie.tscn | 33 ++------- scripts/actor/ZombieInput.gd | 68 ------------------- scripts/actor/bots/BotInput.gd | 29 ++++++++ scripts/actor/bots/ProximityAttack.gd | 16 +++++ scripts/actor/bots/TargetDirection.gd | 40 +++++++++++ scripts/actor/bots/TargetFinder.gd | 19 ++++++ 8 files changed, 171 insertions(+), 96 deletions(-) create mode 100644 scenes/actors/ai/enemy_ai.tscn delete mode 100644 scripts/actor/ZombieInput.gd create mode 100644 scripts/actor/bots/BotInput.gd create mode 100644 scripts/actor/bots/ProximityAttack.gd create mode 100644 scripts/actor/bots/TargetDirection.gd create mode 100644 scripts/actor/bots/TargetFinder.gd diff --git a/addons/nezvers_library/static/GameMath/GameMath.gd b/addons/nezvers_library/static/GameMath/GameMath.gd index 6460df34d..e41dcf5f6 100644 --- a/addons/nezvers_library/static/GameMath/GameMath.gd +++ b/addons/nezvers_library/static/GameMath/GameMath.gd @@ -38,6 +38,17 @@ static func dist_to_line_3d(pos:Vector3, line_start:Vector3, line_end:Vector3)-> return bv.length() return ab.cross(ac).length() / ab.length() +static func get_closest_node_2d(point:Vector2, body_list:Array[Node2D])->Node2D: + if body_list.is_empty(): + return null + var closest:Node2D = body_list.front() + var dist:float = (closest.global_position - point).length_squared() + for body:Node2D in body_list: + var _dist:float = (body.global_position - point).length_squared() + if _dist < dist: + dist = _dist + closest = body + return closest ## Outline of the formula static func dampened_spring(displacement, damp, velocity, spring:float, delta:float): diff --git a/scenes/actors/ai/enemy_ai.tscn b/scenes/actors/ai/enemy_ai.tscn new file mode 100644 index 000000000..a3263065a --- /dev/null +++ b/scenes/actors/ai/enemy_ai.tscn @@ -0,0 +1,51 @@ +[gd_scene load_steps=7 format=3 uid="uid://cenq1bawfywu8"] + +[ext_resource type="Script" path="res://scripts/actor/bots/BotInput.gd" id="1_qo7hy"] +[ext_resource type="Script" path="res://scripts/actor/bots/NavigationVisualizer2D.gd" id="2_2yt0t"] +[ext_resource type="Script" path="res://scripts/actor/bots/ProximityAttack.gd" id="3_4n00q"] +[ext_resource type="Script" path="res://scripts/actor/bots/TargetFinder.gd" id="4_y3qb6"] +[ext_resource type="Script" path="res://scripts/actor/bots/TargetDirection.gd" id="5_ekfo4"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_ixqjj"] +size = Vector2(1000, 500) + +[node name="EnemyAi" type="Node2D"] +script = ExtResource("1_qo7hy") + +[node name="Area2D" type="Area2D" parent="."] +collision_layer = 0 +collision_mask = 2 +monitorable = false + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"] +shape = SubResource("RectangleShape2D_ixqjj") +debug_color = Color(0, 0.6, 0.701961, 0) + +[node name="RayCast2D" type="RayCast2D" parent="."] +enabled = false + +[node name="NavigationAgent2D" type="NavigationAgent2D" parent="."] +path_desired_distance = 5.0 +radius = 16.0 + +[node name="NavigationVisualizer" type="Line2D" parent="NavigationAgent2D" node_paths=PackedStringArray("navigation_agent")] +width = 1.0 +script = ExtResource("2_2yt0t") +navigation_agent = NodePath("..") + +[node name="TargetFinder" type="Node" parent="." node_paths=PackedStringArray("area", "bot_input")] +script = ExtResource("4_y3qb6") +area = NodePath("../Area2D") +bot_input = NodePath("..") + +[node name="ProximityAttack" type="Node" parent="." node_paths=PackedStringArray("target_finder", "bot_input")] +script = ExtResource("3_4n00q") +target_finder = NodePath("../TargetFinder") +bot_input = NodePath("..") + +[node name="TargetDirection" type="Node" parent="." node_paths=PackedStringArray("target_finder", "bot_input", "raycast", "navigation_agent")] +script = ExtResource("5_ekfo4") +target_finder = NodePath("../TargetFinder") +bot_input = NodePath("..") +raycast = NodePath("../RayCast2D") +navigation_agent = NodePath("../NavigationAgent2D") diff --git a/scenes/actors/zombie.tscn b/scenes/actors/zombie.tscn index f5038cf05..3a394b69a 100644 --- a/scenes/actors/zombie.tscn +++ b/scenes/actors/zombie.tscn @@ -1,9 +1,8 @@ -[gd_scene load_steps=20 format=3 uid="uid://62j3fy0o3uwl"] +[gd_scene load_steps=18 format=3 uid="uid://62j3fy0o3uwl"] [ext_resource type="PackedScene" uid="uid://botai66n8rwt3" path="res://scenes/actors/actor.tscn" id="1_fkm2k"] [ext_resource type="Shader" path="res://scripts/shaders/color_flash.gdshader" id="2_duvie"] [ext_resource type="Texture2D" uid="uid://dqqfb3tyiyk2v" path="res://assets/images/characters/zombie_16x16_strip8.png" id="2_giu7j"] -[ext_resource type="Script" path="res://scripts/actor/ZombieInput.gd" id="2_yis6g"] [ext_resource type="Resource" uid="uid://k6kulmw3ddq8" path="res://resources/health_resource/zombie_health.tres" id="2_ysqwl"] [ext_resource type="Resource" uid="uid://bgj1j2b6mc1u7" path="res://resources/ActorStatsResource/zombie_stats.tres" id="5_srg4e"] [ext_resource type="Resource" uid="uid://dq8mhmimpehoi" path="res://resources/InputResource/zombie_input_resource.tres" id="6_o1vf4"] @@ -11,8 +10,8 @@ [ext_resource type="Resource" uid="uid://bmurrqosq7bec" path="res://resources/sounds/kill_zombie.tres" id="7_uiw1f"] [ext_resource type="Script" path="res://scripts/actor/AddPoint.gd" id="9_3rsmj"] [ext_resource type="PackedScene" uid="uid://bu2n5cvym7bse" path="res://scenes/vfx/zombie_body.tscn" id="9_3xjhw"] +[ext_resource type="PackedScene" uid="uid://cenq1bawfywu8" path="res://scenes/actors/ai/enemy_ai.tscn" id="9_4t8nd"] [ext_resource type="Resource" uid="uid://5emqdks7qgbm" path="res://resources/score_resource.tres" id="10_3mlgp"] -[ext_resource type="Script" path="res://scripts/actor/bots/NavigationVisualizer2D.gd" id="10_ouyos"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_mtjxa"] resource_local_to_scene = true @@ -100,9 +99,6 @@ _data = { "walk": SubResource("Animation_d7gcn") } -[sub_resource type="RectangleShape2D" id="RectangleShape2D_ixqjj"] -size = Vector2(1000, 500) - [node name="Zombie" instance=ExtResource("1_fkm2k")] collision_layer = 4 collision_mask = 7 @@ -128,33 +124,14 @@ input_resource = ExtResource("6_o1vf4") sound_resource_dead = ExtResource("7_uiw1f") dead_vfx_scene = ExtResource("9_3xjhw") -[node name="PlayerDetector" type="Area2D" parent="." index="8"] - -[node name="CollisionShape2D" type="CollisionShape2D" parent="PlayerDetector" index="0"] -shape = SubResource("RectangleShape2D_ixqjj") -debug_color = Color(0, 0.6, 0.701961, 0) - -[node name="ZombieInput" type="Node2D" parent="." index="9" node_paths=PackedStringArray("mover", "player_detector", "weapon")] -script = ExtResource("2_yis6g") +[node name="ZombieInput" parent="." index="8" node_paths=PackedStringArray("mover") instance=ExtResource("9_4t8nd")] mover = NodePath("../MoverTopDown2D") -player_detector = NodePath("../PlayerDetector") -minimal_distance = Vector2(12, 6) -weapon = NodePath("../SlashAttack") - -[node name="RayCast2D" type="RayCast2D" parent="ZombieInput" index="0"] -enabled = false - -[node name="NavigationAgent2D" type="NavigationAgent2D" parent="ZombieInput" index="1"] - -[node name="NavigationVisualizer" type="Line2D" parent="ZombieInput/NavigationAgent2D" index="0" node_paths=PackedStringArray("navigation_agent")] -script = ExtResource("10_ouyos") -navigation_agent = NodePath("..") -[node name="SlashAttack" parent="." index="10" node_paths=PackedStringArray("mover") instance=ExtResource("7_tyqu3")] +[node name="SlashAttack" parent="." index="9" node_paths=PackedStringArray("mover") instance=ExtResource("7_tyqu3")] collision_mask = 2 mover = NodePath("../MoverTopDown2D") -[node name="AddPoint" type="Node" parent="." index="11" node_paths=PackedStringArray("damage_receiver")] +[node name="AddPoint" type="Node" parent="." index="10" node_paths=PackedStringArray("damage_receiver")] script = ExtResource("9_3rsmj") damage_receiver = NodePath("../DamageReceiver") score_resource = ExtResource("10_3mlgp") diff --git a/scripts/actor/ZombieInput.gd b/scripts/actor/ZombieInput.gd deleted file mode 100644 index 47f16b8f5..000000000 --- a/scripts/actor/ZombieInput.gd +++ /dev/null @@ -1,68 +0,0 @@ -class_name ZombieInput -extends Node2D - -signal state_changed - -@export var enabled:bool = true -## Commands the movement -@export var mover:MoverTopDown2D -## Used to detect player -@export var player_detector:Area2D -## How close to walk to start an attack -@export var minimal_distance:Vector2 = Vector2(20.0, 20.0) -## Weapon will execute an attack and spawning projectiles -@export var weapon:Weapon - -var axis_compensation:Vector2 # top down movement can use different speed for X&Y axis -enum StateType {NONE, IDLE, CHASE, ATTACK} -var state: = StateType.IDLE - -## A place to react on state change -func set_state(value:StateType)->void: - if value == state: - return - state = value - state_changed.emit() - -## Not using automatic setter functions because they are called before _ready during initialization -func _ready()->void: - # Set to run before mover - process_physics_priority -= 1 - axis_compensation = Vector2.ONE/mover.axis_multiplier - set_enabled(enabled) - player_detector.monitorable = false - player_detector.collision_layer = 0 - player_detector.collision_mask = 2 - -## Toggle processing for animation state machine -func set_enabled(value:bool)->void: - enabled = value - set_physics_process(enabled) - if !enabled: - mover.input_resource.axis = Vector2.ZERO - set_state(StateType.NONE) - #print("ZombieInput [INFO]: set_enabled = ", enabled) - -func _physics_process(_delta:float)->void: - var body_list:Array[Node2D] = player_detector.get_overlapping_bodies() - if body_list.is_empty(): - mover.input_resource.axis = Vector2.ZERO - set_state(StateType.IDLE) - return - - # TODO: decision logic for multiple possible targets - var target:Node2D = body_list.front() - var direction:Vector2 = target.global_position - global_position - - if abs(direction.x) < minimal_distance.x && abs(direction.y) < minimal_distance.y: - # close enough - mover.input_resource.axis = Vector2.ZERO - set_state(StateType.ATTACK) - mover.input_resource.aim_direction = direction.normalized() - mover.input_resource.set_action(weapon.enabled) - return - - # compensate if using different axis speed multipliers - direction *= axis_compensation - mover.input_resource.axis = direction.normalized() - set_state(StateType.CHASE) diff --git a/scripts/actor/bots/BotInput.gd b/scripts/actor/bots/BotInput.gd new file mode 100644 index 000000000..5613fffc7 --- /dev/null +++ b/scripts/actor/bots/BotInput.gd @@ -0,0 +1,29 @@ +class_name BotInput +extends Node2D + +signal input_update + +@export var enabled:bool = true +## Commands the movement +@export var mover:MoverTopDown2D + +var axis_compensation:Vector2 # top down movement can use different speed for X&Y axis + +## Not using automatic setter functions because they are called before _ready during initialization +func _ready()->void: + # Set to run before mover + process_physics_priority -= 1 + axis_compensation = Vector2.ONE/mover.axis_multiplier + set_enabled(enabled) + +## Toggle processing for animation state machine +func set_enabled(value:bool)->void: + enabled = value + set_physics_process(enabled) + if !enabled: + mover.input_resource.axis = Vector2.ZERO + +## Inputs need to be manipulated here +## Modules use this to time their functions +func _physics_process(_delta:float)->void: + input_update.emit() diff --git a/scripts/actor/bots/ProximityAttack.gd b/scripts/actor/bots/ProximityAttack.gd new file mode 100644 index 000000000..d98ebbf50 --- /dev/null +++ b/scripts/actor/bots/ProximityAttack.gd @@ -0,0 +1,16 @@ +class_name ProximityAttack +extends Node + +@export var attack_range:float = 16.0 +@export var target_finder:TargetFinder +@export var bot_input:BotInput + +func _ready()->void: + target_finder.target_update.connect(on_target_update) + +func on_target_update()->void: + if target_finder.closest == null: + bot_input.mover.input_resource.set_action(false) + return + var distance:float = (target_finder.closest.global_position - bot_input.global_position).length() + bot_input.mover.input_resource.set_action(distance <= attack_range) diff --git a/scripts/actor/bots/TargetDirection.gd b/scripts/actor/bots/TargetDirection.gd new file mode 100644 index 000000000..572a15931 --- /dev/null +++ b/scripts/actor/bots/TargetDirection.gd @@ -0,0 +1,40 @@ +class_name TargetDirection +extends Node + +@export var target_finder:TargetFinder +@export var bot_input:BotInput +@export var attack_distance:float = 16.0 +@export var raycast:RayCast2D +@export var navigation_agent:NavigationAgent2D + +var detected: = false +var last_target_position:Vector2 +var local_direction:Vector2 + +func _ready()->void: + target_finder.target_update.connect(on_target_update) + + +func on_target_update()->void: + if target_finder.closest == null: + return + local_direction = target_finder.closest.global_position - bot_input.global_position + if local_direction.length() > attack_distance && line_of_sight(): + bot_input.mover.input_resource.set_axis((local_direction * bot_input.axis_compensation).normalized()) + bot_input.mover.input_resource.set_aim_direction(bot_input.mover.input_resource.axis) + return + navigation_update() + +## Raycast checks if anything from environment is in the way +func line_of_sight()->bool: + raycast.target_position = local_direction + raycast.force_raycast_update() + return !raycast.is_colliding() + +func navigation_update()->void: + if (navigation_agent.target_position - bot_input.global_position).length() > 16.0: + navigation_agent.target_position = target_finder.closest.global_position + var point:Vector2 = navigation_agent.get_next_path_position() + var direction:Vector2 = (point - bot_input.global_position).normalized() + bot_input.mover.input_resource.set_axis((direction * bot_input.axis_compensation).normalized()) + bot_input.mover.input_resource.set_aim_direction(bot_input.mover.input_resource.axis) diff --git a/scripts/actor/bots/TargetFinder.gd b/scripts/actor/bots/TargetFinder.gd new file mode 100644 index 000000000..660155980 --- /dev/null +++ b/scripts/actor/bots/TargetFinder.gd @@ -0,0 +1,19 @@ +class_name TargetFinder +extends Node + +signal target_update + +@export var area:Area2D +@export var bot_input:BotInput + +var target_list:Array[Node2D] +var closest:Node2D + +func _ready()->void: + bot_input.input_update.connect(on_input_update) + +func on_input_update()->void: + target_list = area.get_overlapping_bodies() + closest = GameMath.get_closest_node_2d(bot_input.global_position, target_list) + + target_update.emit()