Limiting enemy velocity using limit_length() not preventing massive velocities

Godot 4.2

I have a spherical flying enemy using a state machine with a couple of states. When the enemy gets near the player they transition to the chase state where they get the direction to the player before flying towards them.

Every physics frame the state machine calls the physics_process function in the current state.

Chase Script:

func physics_process( delta:float ):
	if los_shapecast.is_colliding():
		for i in los_shapecast.get_collision_count():
			var collision = los_shapecast.get_collider(i)
			if collision is Player and state_machine.get_timer("Shoot_Cooldown").is_stopped():
				state_machine.transition_to("Shoot")
				return
	los_shapecast.position = entity.position
	los_shapecast.look_at(player.global_position)
	var dir:Vector2 = entity.global_position.direction_to(player.global_position)
	current_speed += (dir * acceleration).rotated(randf_range(-random_spread, random_spread)) 
	current_speed = current_speed.limit_length(chase_speed)
	entity.velocity = current_speed
	for i in entity.get_slide_collision_count():
		var collision = entity.get_slide_collision(i)
		if collision.get_collider() is MoveableObject:
			collision.get_collider().call_deferred("apply_central_impulse", -collision.get_normal() * entity.motion.length()  ) 
	entity.move_and_slide()

Once the enemy is close enough to the player it prepares to exit the state by calling this function exit()

Chase Script:

 func exit(): 
	nav_timer.disconnect("timeout", Callable(self, "_on_nav_timer_timeout"))
	entity.velocity = entity.velocity.limit_length(chase_speed)
	current_speed = Vector2.ZERO

and enters the “Shoot” state where it plays an animation, creates a projectile at the end, then transitions back to Chase.

Shoot Script:

func _on_shoot_anim_over(anim_name):
	var fireball_instance = projectile.instantiate()
	GlobalScript.add_child(fireball_instance)
	var points:Vector2 = entity.global_position + PROJECTILE_SPAWN_LOCATION
	var push:Vector2 = fireball_instance.object_push
	if !entity.sprite.flip_h:
		points.x = abs(points.x)
		push.x = abs(push.x)
	
	var hitbox_info = {
		"global_position": points,
		"projectile_owner": entity
	}
	fireball_instance.set_parameters(hitbox_info)
	fireball_instance.set_future_collision()
	fireball_instance.set_past_collision()
	state_machine.transition_to("Chase")

then I call the exit function for shoot:

Shoot Script:

func exit() -> void:
	entity.anim_player.disconnect("animation_finished", Callable(self, "_on_shoot_anim_over"))
	shoot_cooldown.start()

and then the enter function for chase again:

Chase Script:

func enter(_msg:= {}):
	nav_timer.connect("timeout", Callable(self, "_on_nav_timer_timeout"))
	nav_timer.start()
	current_speed = Vector2.ZERO
	entity.velocity = entity.velocity.limit_length(chase_speed)

At this point for 1 physics frame, the enemy’s velocity spikes up to numbers of 25,000 when the chase_speed variable is currently set in the editor as 425,

How can I restrict the enemy’s velocity so that the length doesn’t exceed the variable chase_speed, since current_speed.limit_length(chase_speed) isn’t working?

Use clamp instead to clamp your velocity to a max value, in this case the max value you want to clamp to is chase_speed. With Clamp, the entity velocity will never go below or above whatever values you clamp it to.

Below is an example of how I clamped the y velocity of the player in my game.

agent.velocity.y = clampf(agent.velocity.y, agent.min_y_velocity, agent.max_y_velocity)

1 Like

Thanks for the reply.

In your comment you clamped the y vel to a min and max variable, but I don’t think that’ll work for this situation though:

Most platformer characters move in only cardinal directions, whereas my enemy is flying and can accelerate freely in any direction, so if I clamp the x and y values individually then it can accelerate up to a potential Vector2 (chase_speed, chase_speed) instead of having a max length of chase_speed.

I still tried it anyways, teleported again.

Second thing I tried based off your suggestion was clamping the Vector2’s value between the negative and absolute values of (direction * chase_speed) like so:

	entity.velocity = entity.velocity.clamp(-(abs(dir * chase_speed)), abs(dir * chase_speed))

and that didn’t work either. Still teleporting.

Here’s a video that shows what happens (using the code in the original post)

You’re referencing an entity node whenever you modify the enemy’s velocity, but not when you’re changing the current_speed.
Is this your own state machine system? If so, would you mind sharing the full codebase for the system? From the video, it looks like there’s an issue happening when transitioning between states.

Yeah I can share the State Machine script, no problem:

class_name EntityStateMachine extends Node

signal transitioned(new_state)

var current_state: BaseState 
var previous_state: BaseState

@export var initial_state: String

@export var machine_owner: Entity 

var debug_info: Node2D = null
var state_tracker: Label = null
var motion_tracker: Label = null

var state_nodes: = {}

var timer_nodes = {}
var ray_nodes = {}
var shapecast_nodes = {}

func init(debug_node: Node2D = null):
	for nodes in get_node("Timers").get_children():
		timer_nodes[nodes.name] = nodes
	for nodes in get_node("Raycasts").get_children():
		ray_nodes[nodes.name] = nodes
	var shape_cast_node:Node = get_node_or_null("ShapeCasts")
	if shape_cast_node:
		for nodes in shape_cast_node.get_children():
			shapecast_nodes[nodes.name] = nodes
			
	for nodes in get_children():
		if nodes is BaseState:
			state_nodes[nodes.name] = nodes
	for states in state_nodes.keys():
		get_node(states).init(machine_owner, self)
	current_state = get_node(initial_state)
	current_state.enter()
	debug_info = debug_node
	if debug_node:
		state_tracker = debug_node.get_node("StateTracker")
		motion_tracker = debug_node.get_node("MotionTracker")
		state_tracker.text = initial_state
	state_nodes.make_read_only()
	timer_nodes.make_read_only()
	ray_nodes.make_read_only()

func physics_update(delta:float):
	current_state.physics_process(delta)
	motion_tracker.text ="Speed: " + str(round(machine_owner.motion.x)) + "," + str(round(machine_owner.motion.y))
	
func update(delta: float):
	current_state.process(delta)

func input(event: InputEvent):
	current_state.input(event)
	
func transition_to(target_state_name: String = "", msg: = {}, trans_anim: String = ""):
	if !state_nodes.has(target_state_name):
		push_error("Cannot find state name " + target_state_name)
		return
	if target_state_name != current_state.name:
		current_state.exit()
		previous_state = current_state
		current_state = state_nodes[target_state_name]
		if trans_anim:
			machine_owner.anim_player.play(trans_anim)
			await machine_owner.anim_player.animation_finished
		current_state.enter(msg)
		state_tracker.text = "State: " + str(current_state.name)
		emit_signal("transitioned", current_state)
	

func find_state(state:String) -> BaseState:
	var desired_state = state_nodes[state]
	if desired_state:
		return desired_state
	return 

func set_timer(timer:String, wait_time: float):
	var desired_timer = timer_nodes[timer]
	if desired_timer:
		desired_timer.wait_time = wait_time

func get_timer(timer:String) -> Timer:
	if timer_nodes.has(timer):
		return timer_nodes[timer]
	return

func get_raycast(raycast:String) -> RayCast2D:
	if ray_nodes.has(raycast):
		return ray_nodes[raycast]
	return

func get_shapecast(shapecast:String) -> ShapeCast2D:
	if shapecast_nodes.has(shapecast):
		return shapecast_nodes[shapecast]
	return

func get_current_state():
	return current_state

func get_all_states():
	return state_nodes

Based of a GD Quest tutorial on how to make a state machine, but I added some stuff, and I cache the nodes to a dictionary because I think it’s faster than calling get_node() all the time.

The player’s state machine uses the same script with no problems, so I don’t think its the problem though.

As for not referencing current_speed, I do it in the Chase physics_process script, on lines 10 to 12:

current_speed += (dir * acceleration).rotated(randf_range(-random_spread, random_spread)) 
	current_speed = current_speed.limit_length(chase_speed)
	entity.velocity = current_speed

I never said you didn’t use current_speed. I was just a little confused as to why it wasn’t a part of the entity. Of course I now see why it’s not. Its naming is slightly confusing as it is a vector, not a scalar (which “speed” implies).

I’ve looked at your script(s) extensively now, and I don’t see anything inherently wrong. One thing you could try is to print() the state, transition point, and velocity at every major location in your code to try and extract where the velocity goes wrong. We can both agree that the bug occurs when the “Chase” state starts, but is it before, during, or after running the “Chase” state’s enter()? This would be nice information for you to know in order to narrow down the location of the issue.

Another thing that may be helpful for yourself, if you haven’t already, is to compare your current modified code to the source code of the tutorial. This may illuminate some differences and, hopefully, the issue.

P.S. I just noticed that the tutorial is 6 years old. Not saying that the tutorial is invalid but Godot has changed a lot since.

The state machine code is pretty old in my project, I did it like half a year ago so I haven’t really changed anything about it. I think something strange is happening with the return value of limit length so I’m going to try to make my own function to limit the vector’s length myself.

As for entity, its a generic script extending CharacterBody2D that holds common functions and variables for the player and enemies, like changing health bar values, my game’s timeline gimmick, and state machine setters and getters.

Also for when it happens, I’m 99% sure this is true every time it teleports now:

  1. It teleports in the opposite direction of the player (if the player’s to the right, it goes to the left, and vice versa) and a little upwards
  2. It happens in the first physics_process call of chase after entering shoot
  3. It will never happen the first time Chase is called, which is from a seperate state called Idle, which is even more reading sorry lol
extends BaseState

@onready var detection_sphere:Area2D
@onready var direction_timer: Timer
@onready var worldscanner_shapecast: ShapeCast2D
@onready var wander_area:CollisionShape2D
@onready var los_raycast:ShapeCast2D

@export var direction_cooldown: float = 0.4
@export var idle_speed: int = 55
@export var max_wander_distance:int = 300
@export var acceleration:float = 15
@export var worldscanner_length: int = 25

@export var bounce_rand:float = 0.9
@export var target_reached_range: Vector2 = Vector2(10,10)

@onready var direction:Vector2
@onready var target_position: Vector2
@onready var old_worldscanner_length

@onready var speed:float

func init(current_entity, s_machine: EntityStateMachine):
	super.init(current_entity, s_machine)
	detection_sphere = current_entity.detection_sphere
	direction_timer = state_machine.get_timer("New_Random_Direction")
	worldscanner_shapecast = state_machine.get_shapecast("WorldScanner")
	wander_area = entity.wander_area
	direction_timer.wait_time = direction_cooldown
	los_raycast = state_machine.get_shapecast("LOS")
func enter(_msg:= {}):
	super.enter()
	detection_sphere.connect("body_entered", Callable(self, "_on_player_near"))
	get_new_target()
func _on_player_near(body):
	state_machine.transition_to("Chase")

func get_new_target():
	var possible_range = (wander_area.shape.radius / 2)
	target_position.x = randf_range(entity.spawn_location.x - possible_range, entity.spawn_location.x + possible_range)
	target_position.y =  randf_range(entity.spawn_location.y - possible_range, entity.spawn_location.y + possible_range)
	direction_timer.start()
	set_raycast_position()

func bounce_target(point:Vector2):
	target_position *= randf_range( 1- bounce_rand, 1 + bounce_rand)
	
func physics_process(delta):
	if direction_timer.is_stopped() or abs(target_position - entity.global_position) <= target_reached_range:
		get_new_target()
	apply_speed(delta)
	default_move_and_slide()
	if worldscanner_shapecast.is_colliding():
		var collision = worldscanner_shapecast.get_collider(0)
		if collision is TileMap:
			bounce_target(worldscanner_shapecast.get_collision_point(0) * worldscanner_shapecast.get_collision_normal(0))
			direction_timer.start()
		else:
			get_new_target()
	set_raycast_position()

func apply_speed(delta:float):
	var dir_to_target = entity.global_position.direction_to(target_position)
	speed += acceleration
	if speed > idle_speed:
		speed = idle_speed
	entity.motion = (dir_to_target * speed) 

func set_raycast_position():
	worldscanner_shapecast.position = entity.position
	worldscanner_shapecast.look_at(target_position)
	los_raycast.position = entity.position
	los_raycast.look_at(target_position)
	
func exit():
	detection_sphere.disconnect("body_entered", Callable(self, "_on_player_near"))

I don’t think its relevant. It doesn’t do anything unique compared to shoot.

By the way, thanks for helping me with this for so long, appreciate it.

Given this information I want to point out this function in your “Shoot” state:

I’m not an expert on 2D stuff (or GDScript for that matter), but it may have something to do with when this function gets fired. To provide the context as I see it, you have the following two transitions:

  1. Idle → Chase
  2. Shoot → Chase

Transition 1 is triggered via a Area2D signal (body_entered).
Transition 2 is triggered via a… Animation?
The point I’m trying to make is that transition 1 occurs as physics are being processed while transition 2 is, likely, not. Perhaps this is your issue? Errors with physics usually occur when manipulating physics-related variables (such as velocity) outside of the physics step (_physics_process()).

It’s just a guess though.

I thought since the same logic of exiting when animation’s over worked in my player’s attack script, it’ll work in the enemy’s attack script. Don’t want the enemy to hang around in the state after the attack’s finished. Also, in that script I don’t touch any of the entity’s properties in that state, just spawning in the fireball.

Maybe I could call advance() in the physics process and manually progress the animation, until it’s over.

Not entirely true as state_machine.transition_to("Chase") modifies velocity by implication.

But yeah I don’t know, dude. I think I have less of a clue than you do at this point.
Hopefully someone comes along and knows a little more about this subject.

My guess is that a collision occurs between the enemy and his own projectile. In this case, the move_and_slide function changes the velocity after you have limited it.

Sorry if it’s an obvious question but are the bullets on a different collision layer than the enemy?

No need to apologize for your question, you’re good.

Here’s the fireball colliison:
image
Items it’s scanning for:

and here’s the enemy collision:
image
Item’s it’s scanning for:

This is taken while the game is running using the remote tab, so this is what’s happening currently.

Objects are things like boxes and switches, and hook is the player’s grappling hook.

Entities are enemies right now.

I would try to remove layer 17 from enemy collision mask to see if the problem is related to that collision.

Tried it, no luck.

Not exactly sure why it works, but waiting for a 0.001 length timer before calling move_and_slide always ensures that the limit_length() function does its thing.

Thanks for all the suggestions.