I have created a script for my “Enemy” scene, for an enemy NPC. Admittedly, I used AI to generate most of this, however, it is clearly far from perfect or I am doing something wrong.
Enemy Script:
extends CharacterBody2D
# Enemy movement and behavior settings
@export var speed = 100 # Enemy walking speed
@export var detection_range = 150 # Range within which the enemy can detect the player
@export var attack_range = 50 # Range within which the enemy can attack the player
@export var stop_chase_range = 300 # Range beyond which the enemy stops chasing the player
@export var idle_time = 1.0 # Time to spend in idle state
@export var rotation_speed = 5.0 # Speed of rotation for smooth turning
# State management
enum State {IDLE, WALK, CHASE, ATTACK}
var current_state = State.IDLE
var target_position = Vector2.ZERO
# References
@onready var animated_sprite = $AnimatedSprite2D
@onready var raycast = $RayCast2D
# Timer for idle state
var idle_timer = 0.0
# Player reference
var player = null
func _ready():
# Initialize the player reference
player = get_parent().get_node("Player") # Adjust the path based on your scene structure
_pick_new_target_position() # Pick an initial target position for patrolling
raycast.enabled = true # Enable the RayCast2D
raycast.target_position = Vector2(150, 0) # Set the target position for the ray (150 pixels forward)
raycast.add_exception(self) # Avoid colliding with itself
func _physics_process(delta):
# Check for player proximity using RayCast2D
_check_player_proximity()
# Handle enemy behavior based on the current state
match current_state:
State.IDLE:
_handle_idle_state(delta)
State.WALK:
_handle_walk_state(delta)
State.CHASE:
_handle_chase_state(delta)
State.ATTACK:
_handle_attack_state(delta)
# State Handling Functions
func _handle_idle_state(delta):
# Increment the idle timer and transition to walking state if time is up
idle_timer += delta
if idle_timer >= idle_time:
current_state = State.WALK
idle_timer = 0.0
_pick_new_target_position() # Choose a new position to walk towards
animated_sprite.play("Idle") # Play idle animation
func _handle_walk_state(delta):
# Move the enemy towards the target position
var direction = (target_position - global_position).normalized()
velocity = direction * speed
move_and_slide() # Apply the velocity to move the enemy
animated_sprite.play("Walk") # Play walking animation
# Rotate smoothly towards the target direction
_rotate_towards_direction(direction, delta)
# If the enemy reaches the target position, switch to idle state
if global_position.distance_to(target_position) < 5:
current_state = State.IDLE
func _handle_chase_state(delta):
if player:
# Move the enemy towards the player
var direction = (player.global_position - global_position).normalized()
velocity = direction * speed * 1.5 # Increase speed while chasing
move_and_slide() # Apply the velocity to move the enemy
animated_sprite.play("Walk") # Play walking animation
# Rotate smoothly towards the player
_rotate_towards_direction(direction, delta)
# Transition to attack state if close enough to the player
if global_position.distance_to(player.global_position) < attack_range:
current_state = State.ATTACK
# Stop chasing if the player is too far away or blocked
if global_position.distance_to(player.global_position) > stop_chase_range or !_can_see_player():
current_state = State.IDLE
func _handle_attack_state(_delta):
animated_sprite.play("Idle") # Play idle animation (could be replaced with attack animation)
# Implement attack logic here (e.g., damage player)
# If the player moves out of attack range, switch back to chase state
if global_position.distance_to(player.global_position) > attack_range:
current_state = State.CHASE
# Helper Functions
func _check_player_proximity():
# Check if the player is within detection range and has line of sight
if player:
# Update the RayCast2D's target position to the player
raycast.target_position = (player.global_position - global_position).normalized() * detection_range
if _can_see_player():
# Player detected, switch to chase state
if current_state != State.CHASE:
print("Chasing the player!") # Debug: Print when starting the chase
current_state = State.CHASE
func _can_see_player():
return raycast.is_colliding() and raycast.get_collider() == player
func _pick_new_target_position():
# Choose a random position nearby for the enemy to walk towards
var random_distance = randf_range(50, 150) # Random distance for wandering
var random_angle = randf() * 2 * PI # Random angle for direction
target_position = global_position + Vector2(cos(random_angle), sin(random_angle)) * random_distance
func _rotate_towards_direction(direction: Vector2, delta: float):
# Calculate the target angle from the direction
var target_angle = direction.angle()
# Smooth rotation using angular interpolation (lerp_angle)
animated_sprite.rotation = lerp_angle(animated_sprite.rotation, target_angle + PI / 2, rotation_speed * delta)
Here are my scene setups:
“Main” Scene:
-Main_tscn (Node2D)
____-Player (CharacterBody2D): Instance: res://Player.tscn
____-ColorRect (ColorRect)
____TileMap (TileMap)
________-LightOccluder2D (LightOccluder2D)
________-LightOccluder2D2 (LightOccluder2D)
________-LightOccluder2D3 (LightOccluder2D)
____-Enemy (CharacterBody2D) res://Enemy.tscn
____-StaticBody2D (StaticBody2D)
________-CollisionPolygon2D (CollisionPolygon2D)
________-CollisionPolygon2D2 (CollisionPolygon2D)
________-CollisionPolygon2D3 (CollisionPolygon2D)
“Player” Scene:
-Player (CharacterBody2D)
____-AnimatedSprite2D (AnimatedSprite2D)
____-CollisionShape2D (CollisionShape2D)
____-Camera2D (Camera2D)
____-PointLight2D (PointLight2D)
“Enemy” Scene:
-Enemy (CharacterBody2D)
____-AnimatedSprite2D (AnimatedSprite2D)
____-CollisionShape2D (CollisionShape2D)
____-RayCast2D (RayCast2D)
The enemy seems to work fine, however, I have moments where the enemy will randomly start chasing the wall and will not stop. I cannot explain it.
The idea is that the enemy will wander about, until it sees the player then it will chase. If it loses line of sight due to a wall (Walls set by StaticDody2D and CollisionPolygon2D), then it should stop chasing.
Somebody help!