When Should You Use Reactive Planning?
Reactive methods are online algorithms. The robot doesn’t precompute a full path—it reacts to immediate surroundings.
Suitable for:
- Unknown environments (discovery during navigation)
- Real-time constraints (no time to compute global path)
- Simple robots (limited computational power)
- Dynamic obstacles (moving targets)
Not suitable for:
- Complex navigation (many obstacles)
- Optimal paths (reactive ≠ optimal)
- Guaranteed convergence (local methods can get stuck)
Planning Method Comparison
Time Optimality Memory Guarantees Adaptivity
Grid-Based Good Optimal High Complete Low (static)
RRT/RRT* Fair Fair/Good Low Probabilistic Low
Reactive Fast Suboptimal Very Low Partial High
Hybrid Good Good Medium High High
Part 1: Bug Algorithms
Bug algorithms are simple: move toward goal, when you hit an obstacle, follow its boundary.
Figure 1: Bug Algorithm Navigation Strategy
BUG ALGORITHM: Three Phases
Goal (G)
*
/│
/ │ Direct approach
/ │ blocked
/ │
/ ╱════════╲
╱─────── ╲
│ Obstacle │
│ (hit point→) H │
│╱─────────────────╲│
│ │ ← Follow boundary
│ │ with left-hand rule
│ │
Start (S) │
Phase 1: Move directly toward G
Phase 2: Hit obstacle at H, start boundary following
Phase 3: Exit boundary when closer to G than H
Three Variants:
- Bug0: Simple, fast, can get stuck in U-shapes
- Bug1: Slower, guarantees convergence, complete
- Bug2: Uses M-line trick, usually fastest, mostly works
Bug0 (Simplest)
Algorithm:
1. Move directly toward goal
2. If no obstacle in path:
- Move straight to goal
3. If obstacle encountered:
a. Enter boundary-following (left-hand rule)
b. Exit when:
- Can move toward goal AND
- Have made progress past hit point
Left-Hand Rule: Keep your left hand on the wall, follow it.
Goal
*
|
___|(hit obstacle)
/ \
| ← follow boundary with left hand
\____/
|
Start
Bug0 Code
class Bug0:
def __init__(self, robot, goal):
self.robot = robot
self.goal = goal
self.hit_point = None
self.following_boundary = False
def next_move(self):
current_pos = self.robot.position
# Try direct path to goal
if not self.following_boundary:
direction = self.goal - current_pos
# Check if direct path is free
if self.is_path_clear(current_pos, self.goal):
return direction.normalize()
# Hit obstacle, enter boundary following
self.hit_point = current_pos
self.following_boundary = True
# Boundary following (left-hand rule)
if self.following_boundary:
boundary_move = self.follow_left_wall()
# Check if we can exit
if self.can_reach_goal_from_here():
if self.is_path_clear(current_pos, self.goal):
self.following_boundary = False
return (self.goal - current_pos).normalize()
return boundary_move
def follow_left_wall(self):
# Simplified: try moving forward
# If blocked, turn left and try again
current_pos = self.robot.position
# Left-hand rule: keep obstacle on right
for angle in [0, -45, -90, -135, 180]:
direction = self.get_direction_relative_to_heading(angle)
if self.is_path_clear(current_pos, current_pos + direction):
return direction
return (0, 0) # StuckBug0 Pros & Cons
✅ Pros:
- Very simple (easy to implement)
- Works with unknown environments
- Fast (no global planning)
❌ Cons:
- Gets stuck in U-shaped obstacles
- Not optimal (long paths around obstacles)
- Only works with point robots
Bug1 (Better)
Improves Bug0 by fully circling obstacles.
Algorithm:
1. Move directly toward goal
2. If hit obstacle:
a. Fully circumnavigate obstacle (complete circle)
b. Record closest point to goal (closest_point)
c. Return to closest_point
d. Resume direct motion
Key improvement: Doesn’t exit too early. Guarantees finding goal if reachable.
Hit point → Full circle → Closest point → Direct to goal
*───────────────────────*
/╱╲ ╲
/ │ │ (full boundary │
│ │ │ traversal) │
│ │ │ │
╱ │ │ ╱
╲_│_│_________________╱
Bug1 Code
class Bug1:
def __init__(self, robot, goal):
self.robot = robot
self.goal = goal
self.hit_point = None
self.closest_point = None
self.closest_distance = float('inf')
self.circumnavigating = False
def next_move(self):
current_pos = self.robot.position
if not self.circumnavigating:
# Try direct approach
if self.is_path_clear(current_pos, self.goal):
return (self.goal - current_pos).normalize()
# Hit obstacle, start circumnavigation
self.hit_point = current_pos
self.closest_point = current_pos
self.closest_distance = distance(current_pos, self.goal)
self.circumnavigating = True
# Circumnavigation phase
boundary_move = self.follow_left_wall()
# Update closest point
dist_to_goal = distance(current_pos, self.goal)
if dist_to_goal < self.closest_distance:
self.closest_distance = dist_to_goal
self.closest_point = current_pos
# Exit condition: returned to hit point after circumnavigation
if distance(current_pos, self.hit_point) < 0.1 and self.circumnavigating:
# Move to closest point
if distance(current_pos, self.closest_point) < 0.1:
# Now try direct approach again
self.circumnavigating = False
return (self.goal - current_pos).normalize()
else:
return (self.closest_point - current_pos).normalize()
return boundary_moveBug1 Guarantee
Bug1 GUARANTEES finding goal if reachable (complete).
Cost: May traverse boundaries multiple times.
Bug2 (Optimal)
Uses M-line strategy—usually finds path faster than Bug1.
Algorithm:
1. Define M-line: straight line from start to goal
2. Move toward goal
3. If hit obstacle:
a. Follow boundary
b. Exit when:
- Cross M-line AND
- Closer to goal than hit point
4. If return to hit point without crossing M-line:
- Goal unreachable
Start ─ M-line ─ Goal
\ │ /
\ (obstacle) /
\ │ /
╲ │ ╱
╲ │ ╱ (boundary follow)
╲│╱
● (exit when cross M-line closer to goal)
Bug2 Code
class Bug2:
def __init__(self, robot, goal):
self.robot = robot
self.goal = goal
self.start = robot.position.copy()
self.hit_point = None
self.on_boundary = False
def next_move(self):
current_pos = self.robot.position
if not self.on_boundary:
# Try direct approach
if self.is_path_clear(current_pos, self.goal):
return (self.goal - current_pos).normalize()
# Hit obstacle
self.hit_point = current_pos
self.on_boundary = True
# Boundary following
if self.on_boundary:
boundary_move = self.follow_left_wall()
# Check exit condition
if self.is_on_m_line(current_pos) and \
distance(current_pos, self.goal) < distance(self.hit_point, self.goal):
# Exit boundary following
self.on_boundary = False
return (self.goal - current_pos).normalize()
# Check if unreachable
if distance(current_pos, self.hit_point) < 0.01 and self.on_boundary:
return None # Goal unreachable
return boundary_move
def is_on_m_line(self, pos):
# Check if position is on line from start to goal
# Simplified check using cross product
v1 = self.goal - self.start
v2 = pos - self.start
cross = v1[0]*v2[1] - v1[1]*v2[0]
return abs(cross) < 0.1Bug Algorithm Comparison
Obstacle Type Bug0 Bug1 Bug2
Simple (open) ✓ Works ✓ Works ✓ Works
U-shaped ✗ Stuck ✓ Works ✓ Works
Spiral ✗ Stuck ✓ Works ✗ Issues
Steps explored Least Most Medium
Guaranteed? No Yes Mostly
Practical use:
- Unknown environment, simple geometry → Bug0
- Guaranteed convergence needed → Bug1
- Balanced approach → Bug2
Part 2: Potential Fields
Potential fields treat obstacles as repelling forces and goal as attracting force.
Figure 2: Potential Field Force Visualization
ATTRACTIVE FORCE (Goal) REPULSIVE FORCE (Obstacle)
↑ ↑ ↑ ← ← ← ←
↑ ↑ ← ↓ ←
↑ G ↑ + ← O ← =
↑ ↑ ← ↑ ←
↑ ↑ ↑ ← ← ← ←
Goal attracts Obstacles repel
(like magnet) (like opposing magnets)
COMBINED FIELD:
↑ ↓ ↓ ↓
↑ ↓ ↓ (robot moves here,
↑ R → around obstacle)
↑ ↑
↑ ↑ ↑ ← ← ←
Robot follows resultant force vector
Method:
Total Force = Attractive Force + Repulsive Forces
Robot Velocity = K_p * Total_Force (proportional control)
Pros: Very fast, works with dynamic obstacles
Cons: Can get stuck in local minima (concave obstacles)
The Concept
Total force = Attractive (toward goal) + Repulsive (away from obstacles)
Robot moves in direction of total force
Attractive Potential
Goal attracts robot:
F_attractive = -k_a * (robot_pos - goal_pos)
More distance → Stronger force.
Repulsive Potential
Obstacles repel robot:
F_repulsive = 0 if distance > d_0
F_repulsive = k_r * (1/distance - 1/d_0) * (robot_pos - obstacle_pos) / distance
if distance < d_0
Where d_0 is influence radius.
Visualization
Goal (attractive) Obstacles (repulsive)
↑ ↑ ↑ ← ← ←
↑ ↑ ← ↓ ←
↑ R ↑ vs. ← O ←
← ↑ ←
Robot sees combined field:
↑ ↓ ↓
↑ ↓ (attraction weaker than repulsion)
↑ R → (move around obstacle)
Potential Field Code
import numpy as np
class PotentialField:
def __init__(self, k_attractive=1.0, k_repulsive=1.0, influence_radius=2.0):
self.k_a = k_attractive
self.k_r = k_repulsive
self.d_0 = influence_radius
def attractive_force(self, robot_pos, goal):
return -self.k_a * (robot_pos - goal)
def repulsive_force(self, robot_pos, obstacles):
force = np.array([0.0, 0.0])
for obs_pos, obs_radius in obstacles:
distance = np.linalg.norm(robot_pos - obs_pos)
if distance < self.d_0:
repulsion = (self.k_r *
(1.0/distance - 1.0/self.d_0) / (distance**2) *
(robot_pos - obs_pos))
force += repulsion
return force
def total_force(self, robot_pos, goal, obstacles):
f_attr = self.attractive_force(robot_pos, goal)
f_rep = self.repulsive_force(robot_pos, obstacles)
return f_attr + f_rep
def next_move(self, robot_pos, goal, obstacles):
force = self.total_force(robot_pos, goal, obstacles)
if np.linalg.norm(force) < 0.01:
return np.array([0.0, 0.0]) # Stuck
direction = force / np.linalg.norm(force)
step_size = 0.1
new_pos = robot_pos + step_size * direction
return new_pos
# Usage
pf = PotentialField(k_attractive=1.0, k_repulsive=2.0)
robot_pos = np.array([0.0, 0.0])
goal = np.array([10.0, 10.0])
obstacles = [
(np.array([5.0, 5.0]), 0.5), # (center, radius)
(np.array([7.0, 3.0]), 0.3)
]
for _ in range(100):
new_pos = pf.next_move(robot_pos, goal, obstacles)
robot_pos = new_pos
if np.linalg.norm(robot_pos - goal) < 0.1:
print("Goal reached!")
breakPotential Field Pros & Cons
✅ Pros:
- Very fast (one force calculation per step)
- Works with dynamic obstacles
- Smooth trajectories
- Simple to implement
❌ Cons:
- Gets stuck in local minima (concave obstacles)
- No global optimality
- Parameter tuning critical (k_a, k_r)
- Jittery at obstacle boundaries
Local Minimum Problem
Potential field stuck in local minima:
Goal
G
|
├─ ○ (obstacle creates local minimum)
|
R (robot stuck here)
Robot can't escape—forces balanced.
Part 3: Hybrid Approaches
Combine strengths of different methods.
RRT-Connect (Bidirectional RRT)
Grows trees from both start AND goal simultaneously.
Figure 4: RRT-Connect: Bidirectional Growth
UNIDIRECTIONAL RRT BIDIRECTIONAL RRT-CONNECT
(Traditional) (Faster - trees meet)
G G
* *
/ │ \ / ║ \
/ │ \ / T \
/ │ \ / 2 \
/ │ \ / \
│ │ │ │ │
│ Tree│grows │ │ T2 T1 │
│ from│start │ │ |│ │
│ │ │ │ ││ │
└──┬───┴──┬───┘ └──┬──┴┴──┬───┘
│ │ │Meeting│
S 1000+ iters S point
Growth expands Both trees
from start toward grow toward
goal (slow) each other
(fast!)
TIME COMPARISON:
Unidirectional: Need 1000+ iterations to reach goal
Bidirectional: Trees meet after ~50-100 iterations (10× faster!)
Start Goal
S ←RRT1 RRT2→ G
Tree1 expands from start
Tree2 expands from goal
Trees meet → Path found!
Why faster:
- Trees grow toward each other (smaller growth radius)
- Meets in middle (half the exploration space)
Time improvement: Often 10-100x faster than unidirectional RRT.
Pseudocode
RRTConnect(q_start, q_goal):
T1.add_node(q_start)
T2.add_node(q_goal)
for i = 1 to max_iterations:
// Grow T1 toward random sample
q_rand = random_config()
q_new1 = extend(T1, q_rand)
if collision_free(q_new1):
T1.add_node(q_new1)
// Try connecting to T2
q_nearest2 = T2.nearest(q_new1)
q_new2 = extend(T2, q_new1)
if collision_free(q_new2):
T2.add_node(q_new2)
// Try direct connection
if collision_free(q_new1, q_new2):
return Path(T1, T2, q_new1, q_new2)
// Swap trees to balance growth
swap(T1, T2)
return NULLRRT-Connect Performance
Time (seconds)
|
| RRT (unidirectional)
5 ├─────
| /
2 ├ /
| / RRT-Connect (bidirectional)
0.5 ├
|/
└──────→ Workspace Complexity
Informed RRT*
Combines RRT* with informed sampling.
Idea: After finding first path, only sample configurations that could improve it.
Phase 1: RRT* finds path with cost C_best
Phase 2: Sample only from region where:
g_cost + h_cost < C_best
(could improve best solution)
Dramatically reduces unnecessary samples.
When to Use Each
Problem Characteristics Use This Algorithm
────────────────────────────────────────────────────
Known static environment Grid-based / RRT*
Unknown/partially known Bug algorithms
Real-time, low-power device Potential fields
High-DOF manipulator RRT*
Need reusable path database PRM
Dynamic obstacles Potential fields + reactive
Optimal path critical RRT* or informed RRT*
Bidirectional growth needed RRT-Connect
Hybrid Strategy: Multilevel Planning
Combine multiple methods.
Figure 5: Hierarchical Planning Architecture
┌──────────────────────────────────────────────────┐
│ LEVEL 1: GLOBAL PLANNER │
│ (5-10 Hz, RRT* / Grid-based) │
│ Output: Rough waypoint path │
│ Time budget: 1-10 seconds │
└────────────────────┬─────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────┐
│ LEVEL 2: LOCAL PLANNER │
│ (20-50 Hz, Potential fields / MPC) │
│ Output: Smooth collision-free trajectory │
│ Time budget: 20-50ms per step │
└────────────────────┬─────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────┐
│ LEVEL 3: REACTIVE LAYER │
│ (100+ Hz, Bug algo / Obstacle avoidance) │
│ Output: Immediate velocity commands │
│ Time budget: 10ms per cycle │
└────────────────────┬─────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────┐
│ ROBOT EXECUTION │
│ Motor commands, wheel speeds, joint angles │
└──────────────────────────────────────────────────┘
Advantages of Layered Approach:
├─ Global: Optimizes overall mission
├─ Local: Handles local obstacles/constraints
└─ Reactive: Ensures real-time safety
Level 1: Global Path (RRT* or grid-based)
└─ Rough waypoints
Level 2: Local Refinement (potential fields)
└─ Smooth trajectories
Level 3: Reactive Correction (Bug algorithms)
└─ Handle unexpected obstacles
Workflow:
- Compute global path with RRT*
- Smooth with potential fields
- If obstacle encountered, apply Bug algorithm locally
- Resume global path
Real-World Example: Navigation Stack
Navigation Pipeline
─────────────────────────────────────
Start ──→ [Global Planner] ──→ Global Path
(RRT* or grid)
↓
[Local Planner] ──→ Local Trajectory
(potential fields)
↓
[Reactive Layer] ──→ Final Velocity Command
(Bug algorithm)
↓
[Motor Controller] ──→ Robot Motion
Each layer handles different time scales:
- Global: seconds (rough path)
- Local: 100ms (smooth trajectory)
- Reactive: 10ms (obstacle avoidance)
Key Takeaways
-
Reactive methods for online planning
- Bug algorithms, potential fields
- Fast but not optimal
-
Bug algorithms guarantee convergence
- Bug0: Simplest
- Bug1: Complete
- Bug2: Usually fastest
-
Potential fields smooth but local minima
- Adjust k_a, k_r for balance
- Works with dynamic obstacles
-
Hybrid approaches combine strengths
- RRT-Connect faster than RRT
- Multilevel planning for robustness
- Practical real-world systems use combinations
-
Choose algorithm by requirements
- Time constraint?
- Optimality needed?
- Environment known?
Next: Part 4
In Part 4, we’ll explore advanced topics: trajectory optimization, MPC (Model Predictive Control), and practical deployment strategies.
References
-
Khatib, O. (1986). “Real-time obstacle avoidance for manipulators and mobile robots.” IJRR, 5(1), 90-98.
-
Lumelsky, V. J., Skewis, S. E. (1990). “Incorporating range sensing in the robot navigation function.” IEEE Trans. Systems, Man, and Cybernetics, 20(5), 1058-1068.
-
Kuffner, J. J., & LaValle, S. M. (2000). “RRT-Connect: An efficient approach to single-query path planning.” ICRA, 995-1001.
-
Gammell, J. D., Srinivasa, S. S., & Barfoot, T. D. (2015). “Informed RRT*: Optimal sampling-based path planning focused via direct sampling of an admissible ellipsoidal heuristic.” IROS.
Last updated: June 2026 | Tested on ground robots and manipulators