Repository: godot
Issue: NavigationAgent2D.velocity_computed emits zero vector when NavigationAgent2D.set_velocity not called.
Labels: discussion,documentation,topic:navigation

### Tested versions

Using Godot Engine v4.3.stable.official [77dcf97d8] and bug appears both on my MacOS as well as Android exports.

### System information

Godot Engine v4.3.stable.official [77dcf97d8]

### Issue description

I have found that if I add the ability to disable navigation in my entity (while still using it to detect arrival), I must also disconnect the `NavigationAgent2D.velocity_computed` callback, even if I never call `NavigationAgent2D.set_velocity`, otherwise Vector2D.ZERO is pass to my callback function.

It dose set the owner CharacterBody2D velocity to zero and strangely this does not effect the movement of the entity. But it does prevent me from using the owner velocity to chose the sprite, which is how I found the bug.





### Steps to reproduce

Abbreviated code:
```
## Unit no longer follows navigation, but goes streight to target.
## %NavigationAgent2D is still used to detect when arrived.
@export var navigation_disabled: bool = false :
    set(value):
        navigation_disabled = value
        if navigation_disabled:
            %NavigationAgent2D.velocity_computed.disconnect(_set_owner_velocity)
        else:
            %NavigationAgent2D.velocity_computed.connect(_set_owner_velocity)

func _ready():
    # ...
    self.navigation_disabled = self.navigation_disabled

func _set_owner_velocity(safe_velocity: Vector2):
    if not self.movement_disabled and self.target != Vector2.ZERO:
        self.owner.velocity = safe_velocity
    else:
        self.owner.velocity = Vector2.ZERO
    # ...

func _physics_process(delta):
    # ...

    if %NavigationAgent2D.is_navigation_finished():
        self.stop()
        self.arrived.emit()
    else:
        var current_pos = self.owner.global_position
        if self.navigation_disabled:
            var direction = current_pos.direction_to(self.target)
            var new_velocity = direction * self.get_total_speed()
            self._set_owner_velocity(new_velocity)
        else:
            var next_path_pos = %NavigationAgent2D.get_next_path_position()
            var direction = current_pos.direction_to(next_path_pos)
            var new_velocity = direction * self.get_total_speed()
            if %NavigationAgent2D.avoidance_enabled:
                %NavigationAgent2D.set_velocity(new_velocity)
            else:
                self._set_owner_velocity(new_velocity)
```



### Minimal reproduction project (MRP)

Complete code:
```
extends Node2D

class_name Movement

signal stopped
signal stuck
signal arrived

## Unit no longer follows navigation, but goes streight to target.
## %NavigationAgent2D is still used to detect when arrived.
@export var navigation_disabled: bool = false :
    set(value):
        navigation_disabled = value
        if navigation_disabled:
            %NavigationAgent2D.velocity_computed.disconnect(_set_owner_velocity)
        else:
            %NavigationAgent2D.velocity_computed.connect(_set_owner_velocity)

@export var movement_disabled: bool = false
@export var stuck_time_limit: float = 0.333

@export var knockback_disabled: bool = false
@export var knockback_factor: float = 1.0
@export var knockback_stacked: bool = true

var block_finished: bool = false
var knockback_vec: Vector2 = Vector2.ZERO
var knockback_ttl: float = 0.0
var speed: float = 0.0
var stuck_time: float = 0.0
var target: Vector2 = Vector2.ZERO

## bufs and nerfs to speed {owner -> float value}
var speed_factors: Dictionary = {}  

func _ready():
    %NavigationAgent2D.radius = %CollisionShape2D.shape.radius
    self.navigation_disabled = self.navigation_disabled

    # TODO set %NavigationAgent2D.radius based on statsheet.size instead?

    self.owner.statsheet_updated.connect(self._set_statsheet_data)
    self._set_statsheet_data(self.owner.statsheet)

func _set_statsheet_data(statsheet: Statsheet):
    self.speed = statsheet.speed * 16.0
    %NavigationAgent2D.max_speed = self.owner.statsheet.speed * 16
    %NavigationAgent2D.neighbor_distance = self.owner.statsheet.vision * 16
    %NavigationAgent2D.navigation_layers = self.owner.statsheet.navigation_layers
    %NavigationAgent2D.avoidance_layers = self.owner.statsheet.avoidance_layers
    %NavigationAgent2D.avoidance_mask = self.owner.statsheet.avoidance_mask

func on_damaged(src_hitbox: HitBox):
    # TODO based on hitbox speed & dammage vs unit max health
    var knockback_speed: float = 16 * 8
    var knockback_time: float = 1.0 / 32.0

    if not self.knockback_disabled:
        var direction = src_hitbox.owner.velocity.normalized()
        var hit_knockback_vec = (
            direction * knockback_speed * self.knockback_factor
        )
        if self.knockback_ttl > 0.0 and self.knockback_stacked:
            self.knockback_vec += hit_knockback_vec
            self.knockback_ttl += knockback_time
        else:
            self.knockback_vec = hit_knockback_vec
            self.knockback_ttl = knockback_time

func start(target: Vector2):
    self.movement_disabled = false
    self.target = target

func stop():
    print("Movement stop")
    self.movement_disabled = true
    self.stuck_time = 0.0
    self.owner.velocity = Vector2.ZERO
    self.stopped.emit()

func _set_owner_velocity(safe_velocity: Vector2):
    if not self.movement_disabled and self.target != Vector2.ZERO:
        self.owner.velocity = safe_velocity
    else:
        self.owner.velocity = Vector2.ZERO
    if not self.knockback_disabled and self.knockback_ttl > 0.0:
        self.owner.velocity += self.knockback_vec

func get_agent_velocity():
    return self.owner.velocity - self.knockback_vec

func get_total_speed():
    var total = self.speed
    for owner_key in self.speed_factors:
        total *= self.speed_factors[owner_key]
    return total

func _physics_process(delta):
    if not self.movement_disabled and self.target != Vector2.ZERO:
        %NavigationAgent2D.target_position = self.target
        if not self.block_finished and %NavigationAgent2D.is_navigation_finished():
            print("Movement navigation finished")
            self.stop()
            self.arrived.emit()
        else:
            var current_pos = self.owner.global_position
            if self.navigation_disabled:
                var direction = current_pos.direction_to(self.target)
                var new_velocity = direction * self.get_total_speed()
                self._set_owner_velocity(new_velocity)
            else:
                var next_path_pos = %NavigationAgent2D.get_next_path_position()
                var direction = current_pos.direction_to(next_path_pos)
                var new_velocity = direction * self.get_total_speed()
                if %NavigationAgent2D.avoidance_enabled:
                    %NavigationAgent2D.set_velocity(new_velocity)
                else:
                    self._set_owner_velocity(new_velocity)
    else:
        self._set_owner_velocity(Vector2.ZERO)

    if self.owner.velocity != Vector2.ZERO:
        var prev_pos = self.owner.global_position
        self.owner.move_and_slide()

        # check if overshot target but only within close distancet to it
        # as to not break navigating around things from a distance
        var dist_moved = self.owner.global_position.distance_to(prev_pos)
        var dist_after = self.owner.global_position.distance_to(self.target)
        if (
            not self.block_finished and dist_after < dist_moved and
            Utils.is_between(self.target, prev_pos, self.owner.global_position)
        ):
            print("Movement arrived and overshot")
            self.stop()
            self.arrived.emit()

    # TODO stuck detection via initial distance / speed vs total moving time?

    self.knockback_ttl -= delta

```