How to make "weighted randomness" for an enemy wander state

Godot Version

4.2.1

Question

I have an enemy and I want it to wander around randomly. I’ve already achieved this to some degree however i’d like for the enemy to be more likely to wander in certain direction at certain times.

basically whenever the enemy is far away from the “wander_center” the enemy should be more likely to wander towards the center and vice versa. When the enemy is close to the center it should be more likely to move away from the center.

I’m not sure how to get started on this

Enemy movement code:

extends State

const SPEED = 100.0
const WANDER_RADIUS = 100

@onready var animation_tree = $"../../AnimationTree"
@onready var timer = $movement_timer

@export var idle_state : State
@export var dead_state : State
@export var hurt_state : State

var target_position: Vector2 = Vector2.ZERO

func on_enter(): # is triggered everytime the enemy enters this state
	timer.start(randf_range(1,2.5))
	target_position = Vector2(randf_range(-WANDER_RADIUS,WANDER_RADIUS),randf_range(-WANDER_RADIUS,WANDER_RADIUS))

func on_exit(): #triggered just before exiting this state
	pass

func state_process(_delta): #returns a vector from global_position to target_point
	var direction = character.global_position.direction_to(target_position) 

	if character.global_position.distance_to(target_position) >= 30: 
		character.velocity = character.global_position.direction_to(target_position) * SPEED
	else: # if not moving and timer has not stopped - transition early
		timer.stop() 
		next_state = idle_state
		print("has arrived")

	if direction != Vector2.ZERO: #handles latest direction
		character.latest_direction = direction

	if character.health <= 0: #transition to dead state
		character.dead = true
		next_state = dead_state

	if character.hurt: # transition to hurt state
		next_state = hurt_state

	character.move_and_slide()

func _on_movement_timer_timeout(): #transition to idle state
	next_state = idle_state

There are lots ways you could do this. Probably the easiest looks like this:

func pick_next_random_wander_target(center: Vector2, radius: float) -> Vector2:
    var dist: float = randf_range(0.0, radius) # Random distance from center.
    var ang:  float = randf_range(0.0, TAU)    # Random angle 0..360
    return center + (Vector2(dist, 0.0).rotated(ang))

That will always pick targets within the circle, and if the enemy is near the edge of the circle they will tend to wander back towards the center. If they’re near the center they’ll probably wind up heading further out.

If you want them to tend to wander out to the edge then back near the center in a cycle, you could always play with radius; maybe store the last radius and angle picked, hand them in to the pick function and have it bias away from those values:

func pick_next_with_bias(center: Vector2, radius: float, last_angle: float, last_dist: float) -> Vector2:
    var dist: float
    var ang:  float = fposmod(PI + randf_range(-last_angle, angle), TAU)

    if last_dist < (radius * 0.5): dist = randf_range(radius * 0.5, radius)
    else:                          dist = randf_range(0.0, radius * 0.5)

    return center + (Vector2(dist, 0.0).rotated(ang)

You could probably do nicer biasing, particularly by using ease functions or sin() or cos(), but the basic idea is the same.

You could also go oldschool; make an array of Vector2 patrol positions that are relative to the wander center, and have the enemy walk that array picking out positions. Use some number you can extract from them (like, say, their initial int(global_position.x) & 0x15) to determine how many steps forward they move in the array to get their next position, and some similarly generated number (maybe .y and masked with something near the size of the position list length) to determine their initial offset in the list. Wrap if they walk off the end of the list.

That lets a lot of enemies operate off the same relatively small table of positions without making them all look like they’re doing the same thing, and it lets you tailor things a bit. With a slightly more complicated list you can even add behaviours, so that (say) if they walk to a certain location they then do a specific animation, like pulling out binoculars or brandishing a sword.

4 Likes

Another way to do this could be to have it pick new wander target location in a two step process:

  • When picking a new wander target location, first decide if they should return towards the center or not, have this be a random chance that is based on their distance to the center.
  • If they decide to return to the center, that’s the new wander target location. If not, just pick a new location at random in your allowed area as usual.

This way when they’re further away from your center they will more often decide to wander back to the center, but not always.