Real-time Synchronization Design
Overview
PyBulletFleet implements a real-time synchronization system to ensure smooth simulation playback at configurable speeds. This document explains the design, implementation, and best practices.
Design Philosophy
Absolute Time-based Control
The synchronization system uses absolute time calculation for stable, predictable behavior:
elapsed_time = current_time - start_time
target_sim_time = elapsed_time * speed
time_diff = target_sim_time - current_sim_time
Key Benefits:
Stable control: Fixed reference point (
start_time) prevents oscillationSimple logic: No complex history management or filtering
Predictable: Easy to reason about and debug
Efficient: Minimal computational overhead
Core Algorithm
1. Time Difference Calculation
# Calculate how much simulation should have progressed
target_sim_time = elapsed_real_time * speed_multiplier
# Compare with actual simulation progress
time_diff = target_sim_time - current_sim_time
Interpretation:
time_diff > 0: Simulation is behind → Execute multiple steps to catch uptime_diff < 0: Simulation is ahead → Sleep until next frametime_diff ≈ 0: Simulation is synchronized → Continue at current pace
2. Adaptive Step Execution
When behind target:
if time_diff > 0:
steps_needed = int(time_diff / timestep)
steps_needed = max(1, min(steps_needed, max_steps_per_frame))
for _ in range(steps_needed):
step_once()
Saturation limit (max_steps_per_frame):
Default: 10 steps per frame
Prevents sudden jumps from large
time_diffEnsures GUI responsiveness
3. Sleep Strategy
When ahead of target:
if time_diff < 0:
sleep_time = abs(time_diff) - last_step_process_time
if sleep_time > 0:
time.sleep(sleep_time) # Exact sleep
elif sleep_time > -min_sleep:
time.sleep(min_sleep) # Minimum for GUI responsiveness
GUI responsiveness:
min_sleep = 1.0 / gui_min_fps(default: 33ms for 30 FPS)Ensures smooth rendering even when simulation is ahead
Pause/Resume Handling
Reset start_time on Resume
if last_pause_state and not self._simulation_paused:
# Just resumed from pause
start_time = current_time - current_sim_time / speed
logger.info("Resumed from pause: start_time reset for smooth continuation")
Timeline visualization:
Real time: 100s -------- 105s [Pause 10s] 115s -------- 120s
Sim time: 0s ----------- 5s (stopped) 5s ---------- 10s
start_time: 100s (initial) ──────────────→ 110s (reset) ────→
After reset: target = (115 - 110) * 1.0 = 5s, actual = 5s → smooth ✅
RTF Multiplier Support
Variable RTF Control
target_rtf = 0.5 # Half speed (slow motion)
target_rtf = 1.0 # Real-time (1:1)
target_rtf = 2.0 # Double speed (fast forward)
target_rtf = 0 # Maximum speed (no sleep, as fast as possible)
Example with target_rtf=2.0:
# After 1 second of real time
elapsed_real = 1.0
target_sim_time = 1.0 * 2.0 = 2.0 seconds
# Simulation should have progressed 2 seconds
# If current_sim_time = 1.8, then time_diff = 0.2
# → Execute extra steps to catch up
Best Practices
1. Spawn Objects Before run_simulation() ✅
Recommended pattern:
# Setup phase (timing doesn't matter)
sim = MultiRobotSimulationCore.from_yaml("config.yaml")
# Spawn can take seconds - no impact on simulation time
for i in range(100):
sim.spawn_large_structure() # May take 10 seconds total
# Simulation starts HERE (start_time = time.time())
sim.run_simulation(duration=60.0) # Smooth, no spawn delay impact
Why it works:
start_timeis initialized inrun_simulation()Spawn time is completely excluded from synchronization
No accumulated
time_difffrom spawn overhead
2. Use Pause/Resume for Dynamic Spawning of large/complex objects ✅
Correct pattern:
# Programmatic pause/resume
sim.pause()
# spawn objects here. Won't affect time_diff
sim.resume() # Auto-resets start_time