Hello, I'm trying to create a homing missile that automatically locks on the closest possible target and flies towards it. I managed to get it to work by following the instructions in the "Godot Recipes: Homing Missiles" tutorial by KidsCanCode. However, I'm not happy with the way the missile flies. It acts more like it's attracted towards the target via gravity rather than steering towards it. When I shoot the missile away from the target, it flies straight, decelerates, then flips around and accelerates towards the target. That's not how missiles fly, at least not in atmosphere. However, I can't figure out how to change it so the missile keeps its speed and actually steers towards the target and I couldn't find any other tutorial. Can anyone help me?

Here's the code for the missile. I removed some parts not related to the movement to make it shorter. If required I can post the entire script.

extends Area2D

export (int) var damage = 10
export (int) var speed = 500
export (float) var recoil = 0 #50
export (float) var steer_force = 15.0
export (int) var step_size

export (PackedScene) var explosion
export (PackedScene) var splash_damage

onready var life_time = $LifeTime
onready var raycast = $FindTargets

var target : Node2D = null

var direction = Vector2.ZERO

var current_step : int

var velocity = Vector2.ZERO
var acceleration = Vector2.ZERO
	
func start(_transform):
	global_transform = _transform
	velocity = transform.x * speed

func _physics_process(delta):
	if current_step < (360/step_size):
		target = get_target(detect_targets())

	acceleration = seek()
	velocity += acceleration * delta
	velocity = velocity.clamped(speed)
	rotation = velocity.angle()
	position += velocity * delta * 100 # *100 not in original code but without it missile is slow af

func _on_HomingMissile_body_entered(body):
	#damage and explosion code, removed to shorten post, is already long enough
	
func _on_LifeTime_timeout():
	queue_free()

func detect_targets() -> Array:
	#code to detect all possible targets, removed to shorten post
	
func get_target(target_array) -> Node2D:
	#code to find closest target, removed to shorten post
	
func seek():
	var steer = Vector2.ZERO
	if target:
		var desired = (target.position - position).normalized() * speed
		steer = (desired - velocity).normalized() * steer_force
	return steer

Instead of using acceleration to steer, just rotate current velocity vector towards the desired velocity vector by a small amount proportional to delta time. This may give you more natural looking behavior.

Also, existing model may look better if velocity magnitude is kept constant. Using only clamp() will not ensure that as in some situations velocity could end up being smaller than speed. So try to replace:

velocity = velocity.clamped(speed)

with

velocity = velocity.normalized() * speed

@xyz said: Instead of using acceleration to steer, just rotate current velocity vector towards the desired velocity vector by a small amount proportional to delta time. This may give you more natural looking behavior.

Also, existing model may look better if velocity magnitude is kept constant. Using only clamp() will not ensure that as in some situations velocity could end up being smaller than speed. So try to replace:

velocity = velocity.clamped(speed)

with

velocity = velocity.normalized() * speed

I actually tried that before. However, that didn't really work out either. The result is... difficult to describe. When the player almost faces the enemy, the missile makes a very narrow turn towards the enemy. However, the more the player faces away, the larger the turn radius of the missile becomes. When the player faces completely away from the enemy, the turn radius of the missile becomes so large that it basically flies in a straight line (at least it looks like it on the limited screen). So, the greater the turn the missile has to make, the greater its turn radius becomes. If it has to make a 180° turn, it seems its turn radius becomes infintely large.

For steering behavior to work right, the missile must always be moving forward (relative to itself) like it would in real life. For example:

position += transform.xform(Vector2.RIGHT) * speed * delta

Then when you call steer, it finds the angle it should be rotated by to point in the correct direction. For example, you could take the vector of the current direction (based on the rotation) and find the angle to the enemy (using angle_to). If you set the rotation to this angle, the missile would instantly face the target, which is not what we want. So instead, you only rotate a portion of this angle, which will get the missile to slowly rotate in an arc until it is moving directly to the target. Usually it looks better if the rotation is a fixed amount, so you can check if the angle is positive or negative and then just add some float value you set.

rotation += sign(steer_angle) * rotation_speed * delta

Or you could use a percentage like

rotation += steer_angle * 0.3 * delta

Which would rotate 30% of the way every second.

@Irolan I suggested two different things. Didn't quite get which one did you try before.

Rotating the velocity vector should work fine for simple missile simulation. If you want it to steer faster when facing more away from the target just let your rotation amount be proportional to angle towards wanted direction.

If you still find you can't adapt this for your needs, you can try to implement proportional navigation which is how actual real world homing missiles steer. It's still based on rotating the velocity vector. It just calculates the rate of rotation in more sophisticated way that ensures the most efficient collision course.

Okay, so after having a friend look over the code who is way more knowledgeable than me in programming, calculations and stuff, turns out the script in the tutorial works only for a very specific application: if the missiles are fired from guns that already point at the target, at a target that is moving with a constant velocity. The missiles will always be slightly faster than the target, and the strange flight behaviour is not noticeable because they start out flying roughly in the target's direction. Simply put, it does not work for what I want. You're both right, I need to rotate the angle bit by bit. So, I tried that... here's what the flight path of the missile looked like when I was done (P = player, E = enemy, red line = missile path): My missile turned into a boomerang doing a sine wave trajectory. This about sums up my skill as a programmer, So yeah, I need to work on that a bit more. :)

I didn't test the code I posted, but the general idea should work. You probably just need to mess with it more, good luck.

Okay, this was a doozy. In case anyone finds this thread looking for an answer, here's how I did it eventually. Not saying it's the best way to do it, but after almost four hours of "what the fluff is even happening?!?" it works.

This first function calculates the angle from the missile to its target. I'm converting from rad to degree because I can't think in rad. It should be noted that if the target is below the missile, the angle is between 0 and 180, if it is above, the angle will be between 0 and -180. This caused me some issues. The target_path check is just there in case the missile is still there but the target is destroyed prematurely. It is filled in the function that selects a target. If anyone asks, I tried the angle_to() function, but for some reason the angles it returned were all over the place and never correct. Also, apparently markdown is bugged, when I put something in a code block, it removes all line breaks. func get_angle_to_target() -> float: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var angle = 0.0 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if has_node(target_path): &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angle = rad2deg((target.position - position).angle()) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return angle

The second function calculates the difference between the angle to the target and the angle the missile is facing. func get_angle_delta() -> float: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var angle_delta = 0.0 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if get_angle_to_target() < 0: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angle_delta = global_rotation_degrees - get_angle_to_target() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;angle_delta = get_angle_to_target() - global_rotation_degrees &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return angle_delta

This is the actual rotation and movement: func _physics_process(delta): &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if detect_targets().size() > 0: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if !has_node(target_path): &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;target = get_target(detect_targets()) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if get_angle_to_target() < 0: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if get_angle_delta() <= 0 || get_angle_delta() > 180: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rotation_degrees += delta * turn_rate &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rotation_degrees -= delta * turn_rate &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if get_angle_delta() <= 0 || get_angle_delta() > 180: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rotation_degrees -= delta * turn_rate &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rotation_degrees += delta * turn_rate &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;velocity = get_transform().x.normalized() * speed &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;position += velocity

a year later
4 months later

know this is an old post, but I stumbled across it when I was looking for a solution to a similar issue and it works great for the most part. Thanks for sharing it! The only issue I'm having is that when a missile is headed straight for an enemy it jitters back and forth rapidly. I've tinkered around with it and it seems like get_angle_delta is the culprit. It bounces back and forth rapidly between very small negative and positive numbers when the missile is moving on a straight line toward a target but I'm not sure how to smooth that out. For reference, this is the relevant bit of the code I'm using. Any thoughts or advice would be awesome. Thank you!

func _physics_process(delta) -> void:
	var _collision = move_and_collide(velocity * delta)
	velocity = seek(delta)

func seek(delta) -> Vector2:
	if is_instance_valid(target):
		if get_angle_to_target() < 0:
			if get_angle_delta() <= 0 or get_angle_delta() > 180:
				rotation += turn_speed * delta
			else:
				rotation -= turn_speed * delta
		else:
			if get_angle_delta() <= 0 or get_angle_delta() > 180:
				rotation -= turn_speed * delta
			else:
				rotation += turn_speed * delta
	velocity = get_transform().x.normalized() * speed
	return velocity

func get_angle_to_target() -> float:
	var angle = 0.0
	if is_instance_valid(target):
		angle = rad2deg((target.global_position - global_position).angle())
	return angle

func get_angle_delta() -> float:
	var angle_delta = 0.0
	if get_angle_to_target() < 0:
		angle_delta = rad2deg(global_rotation) - get_angle_to_target()        
	else:
		angle_delta = get_angle_to_target() - rad2deg(global_rotation)
	return angle_delta

Check if abs(angle_delta) < turn_speed*delta. If yes, set the missile rotation so it aims directly at the target.

    xyz
    Thanks for the suggestion. I played around with it a little yesterday and wound up declaring a deadzone variable because turn_speed * delta wasn't quite catching it. If abs(angle_delta) < deadzone is true, I just set the missile to look_at(target.global_position). I also added an is_approx_equal(angle_delta, 0) condition to that block of code to set it to 0 if it has a tiny value. It feels a little hack-y but it works and seems to have smoothed the movement out, plus I can refine it some more as I go.