Network Integration Guide
Sirocco Race Timing is designed with a clear separation between its pure C# timing logic (the Core layer) and Unity integration (the Adapter layer). This architecture makes it straightforward to integrate with any multiplayer networking stack (Unity Netcode for GameObjects, Mirror, Photon Fusion, Fish-Net, etc.).
This guide explains how a multiplayer engineer can feed server-authoritative data into the timing system, ensuring accurate lap times, sector crossings, and standings calculations in a networked racing game.
Entry Points
The Core LapTimer class provides discrete entry points for feeding simulator or network state into the timing system. All methods require a competitorId to identify which competitor the data belongs to.
UpdatePosition
The primary method for high-fidelity timing with sub-tick interpolation.
public LapCrossingResult UpdatePosition(
int competitorId,
PositionSample current,
PositionSample previous,
double trackLength)
competitorId: The registered competitor's unique identifier.current: The position sample from this tick.previous: The position sample from the last tick. The system interpolates between these two samples to determine the exact sub-tick moment a lap or sector line was crossed.trackLength: Total track length in meters. Used to convert ratios to distance for gap calculations.- Returns: A
LapCrossingResultstruct indicating whether a lap crossing was detected, its direction, and the interpolated crossing time.
Lap crossing detection: When current.Ratio wraps (e.g., 0.98 → 0.02) and the ratio delta exceeds 0.5, the system interpolates between the two samples to find the exact crossing time at the start/finish line.
Preconditions: The competitor must be registered, the session must be in Running state, and the competitor must be Active (not disconnected). If any precondition fails, the call is silently ignored and returns CrossingDetected = false.
TriggerSector
Used for explicit sector crossings, typically when the server dictates that a competitor crossed a sector line.
public void TriggerSector(
int competitorId,
int sectorId,
double? explicitTime = null)
competitorId: The registered competitor's unique identifier.sectorId: 1-based sector index. Must be in range[1, sectorCount].explicitTime: If provided, records the crossing at this specific server-synchronized time instead of queryingITimeProvider.GetTime(). This is critical for server-authoritative race resolution where the server knows the exact crossing time.
UpdateDistance
A simpler alternative when track-projected ratios and segment indices are unavailable.
public void UpdateDistance(
int competitorId,
double distance,
double x, double y, double z)
competitorId: The registered competitor's unique identifier.distance: Total distance traveled in meters (must be monotonically increasing).x,y,z: World position coordinates (asdouble). Used for breadcrumb recording and Euclidean gap calculations.
Trade-offs vs UpdatePosition:
- No sub-tick interpolation for start/finish line crossings (lap detection must be handled externally via
TriggerLap). - No micro-sector calculations for the delta bar.
- No ratio-based gap calculations — falls back to distance-based breadcrumb interpolation only.
Use this path when your network only syncs world position and distance, without track spline data.
Data Types
PositionSample
A mutable struct with public fields. All numeric types are double (not float), matching the Core layer's precision requirements.
public struct PositionSample
{
public double Time; // Server-synchronized time of this sample
public double Ratio; // Track progress ratio (0.0 to 1.0)
public int SegmentIndex; // Current track spline segment index
public int CompletedLaps; // Number of full laps completed at this sample
public double X; // World position X
public double Y; // World position Y
public double Z; // World position Z
}
Field guidance for network consumers:
| Field | Source | Notes |
|---|---|---|
Time |
Server-synchronized clock | Must be monotonically increasing. See Timing Assumptions. |
Ratio |
Server's track projection | Normalized 0.0–1.0 along the racing line. Wraps from ~1.0 back to ~0.0 at the start/finish line. |
SegmentIndex |
Server's spline evaluation | Index into the track's spline segments. Used for segment-walking distance calculation. |
CompletedLaps |
Server's lap counter | Drives lap crossing detection. Must increment when Ratio wraps. |
X, Y, Z |
Server's authoritative position | Used for breadcrumb world positions and Euclidean fallback calculations. |
A convenience constructor is available:
new PositionSample(time, ratio, segmentIndex, completedLaps, x, y, z)
ITimeProvider
Allows the timing Core to use your server's clock instead of Unity's Time.timeAsDouble.
public interface ITimeProvider
{
double GetTime();
}
Constructor injection: Pass your implementation when creating the LapTimer:
var timeProvider = new ServerSynchronizedTimeProvider();
var config = new BreadcrumbRetentionConfig();
var lapTimer = new LapTimer(timeProvider, config);
The optional BreadcrumbRetentionConfig parameter controls breadcrumb retention, standings throttling, and gap update intervals (see Timing Assumptions).
Implementation guidance: Your ITimeProvider should return a network-synced time adjusted for latency. If the server says a lap was completed at t=124.5 and the client receives it later, the synchronized time provider ensures recorded history is consistent across all clients.
Timing Assumptions
If integrating network data, there are invariant rules you must enforce. Violations are not detected at runtime (for performance reasons) — they silently corrupt data.
Time Monotonicity
Time must never flow backward.
- If a
PositionSample.Timeis older than the previous sample's time, speed and gap calculations will produce incorrect results. - When
timeDelta <= 0, the speed calculation defaults to0.0, which means the breadcrumb system treats the competitor as stationary. - Network guidance: Discard or reorder out-of-sequence packets before feeding them into the timing system. The Core layer does not buffer or reorder inputs.
Distance Monotonicity
Total distance must always increase.
- Breadcrumb interpolation assumes later entries have greater distance values.
- A network teleport that moves a competitor backward on track will corrupt gap calculations for that competitor.
- Network guidance: If your server performs rollback/reconciliation, recalculate distance from the corrected position rather than feeding a decreased value.
Breadcrumb System
The Core uses a breadcrumb trail per competitor for gap interpolation.
- What's stored: Each breadcrumb records
(distance, racingTime, x, y, z). - Racing time vs wall-clock time: Breadcrumb timestamps use "racing time" — time that freezes when the competitor's speed falls below
StationarySpeedThreshold(default: 3.0 m/s). This prevents gap inflation during pit stops or when a car is temporarily stationary. Racing time is calculated internally from consecutiveUpdatePosition/UpdateDistancecalls. - Retention window: Breadcrumbs older than
RetentionWindowSeconds(default: 120 seconds) are pruned, with a minimum ofMinimumBreadcrumbCount(default: 10) always preserved. - Network implication: Even if your network tick rate is lower than the local physics rate, the breadcrumb system still works — it interpolates between whatever samples it has. Lower tick rates mean less precise gap interpolation, but the system degrades gracefully.
Update Frequency Tolerance
The timing system is designed to accept inputs at any reasonable frequency.
- Standings throttling: The
RaceDirectorthrottles standings recalculations independently. Default intervals: - Standings sort: every 0.1 seconds (10 Hz)
- Gap recalculation: every 0.1 seconds (10 Hz)
- Decoupled from input rate: You can feed position samples at your network tick rate (e.g., 20 Hz) independently of Unity's
FixedUpdaterate (e.g., 50 Hz). The throttling ensures that even at 50 Hz input, standings are only recalculated at 10 Hz. - Configurable: Adjust via
BreadcrumbRetentionConfig:
var config = new BreadcrumbRetentionConfig
{
StandingsUpdateIntervalSeconds = 0.2, // 5 Hz standings
GapUpdateIntervalSeconds = 0.1, // 10 Hz gaps
RetentionWindowSeconds = 120.0, // 2 minutes of breadcrumbs
StationarySpeedThreshold = 3.0 // m/s
};
Adapter Pattern
Instead of modifying the timing library, implement a NetworkRaceCompetitor MonoBehaviour in your own project. This acts as a bridge between your network state and the Core logic, following the same pattern as the built-in RaceCompetitor.
Skeleton: NetworkRaceCompetitor
using UnityEngine;
using SlowToast.RaceTiming.Core;
using SlowToast.RaceTiming.Core.Models;
/// <summary>
/// Bridges network-replicated state into the Sirocco Race Timing Core.
/// Lives in YOUR project, not in the timing library.
/// Replace NetworkBehaviour with your netcode framework's base class.
/// </summary>
public class NetworkRaceCompetitor : MonoBehaviour
{
[SerializeField] private int _competitorId;
[SerializeField] private string _driverName;
private LapTimer _lapTimer;
private PositionSample _previousSample;
private double _trackLength;
private bool _initialized;
/// <summary>
/// Call this when the network entity spawns and the timing system is ready.
/// </summary>
public void Initialize(LapTimer lapTimer, double trackLength)
{
_lapTimer = lapTimer;
_trackLength = trackLength;
// Register with the Core timing system
_lapTimer.RegisterCompetitor(new Competitor
{
Id = _competitorId,
Name = _driverName
});
_initialized = true;
}
/// <summary>
/// Call this when your network layer delivers a new authoritative state update.
/// </summary>
public void OnServerStateReceived(
double serverTime,
double trackRatio,
int segmentIndex,
int completedLaps,
double x, double y, double z)
{
if (!_initialized) return;
var current = new PositionSample(
time: serverTime,
ratio: trackRatio,
segmentIndex: segmentIndex,
completedLaps: completedLaps,
x: x, y: y, z: z
);
// Feed both current and previous sample for sub-tick interpolation
_lapTimer.UpdatePosition(_competitorId, current, _previousSample, _trackLength);
_previousSample = current;
}
/// <summary>
/// Call this when the network client disconnects.
/// Session data is preserved for potential reconnection.
/// </summary>
public void OnNetworkDisconnect()
{
if (!_initialized) return;
_lapTimer.DisconnectCompetitor(_competitorId);
}
/// <summary>
/// Call this when a previously disconnected client rejoins.
/// Reactivates timing updates and resets gap tracking timestamps.
/// </summary>
public void OnNetworkReconnect()
{
if (!_initialized) return;
_lapTimer.ReconnectCompetitor(_competitorId);
}
}
Competitor Lifecycle
Map your network events to the Core lifecycle methods on LapTimer:
| Network Event | Core Method | Behavior |
|---|---|---|
| Player joins / race starts | RegisterCompetitor(Competitor) |
Creates session data. Must happen before any position updates. |
| Player disconnects | DisconnectCompetitor(int competitorId) |
Marks as disconnected. Session data (laps, times) is preserved. Position updates are silently ignored. |
| Player rejoins | ReconnectCompetitor(int competitorId) |
Reactivates the competitor. Gap tracking timestamps are reset to prevent stale calculations. |
| Player permanently leaves | WithdrawCompetitor(int competitorId) |
Permanently removes the competitor. All session data is deleted. |
Important: Competitors must be registered before receiving any position or sector updates. Calls to UpdatePosition, TriggerSector, or UpdateDistance for unregistered competitors are silently ignored.
Responsibility Boundary
Timing Library Owns
- Lap and sector timing — sub-tick interpolated crossing times, split times, cumulative times.
- Gap calculations — breadcrumb-based track intervals, distance gaps, and time gaps between competitors.
- Standings — position sorting respecting laps completed, distance, and penalties.
- Penalties — time penalties, disqualifications, lap invalidation, and penalty-aware ranking.
- Best times — personal best and session best tracking for laps and sectors (excluding invalid laps).
Network Layer Must Own
- Position authority — the server decides where cars are, resolving physics interactions and collisions.
- Clock synchronization — aligning client and server clocks so that timestamps from
ITimeProviderare meaningful and comparable across machines. - State reconciliation — handling out-of-order packets, deduplication, and rubber-banding. The timing system expects clean, ordered inputs.
- Interpolation / prediction — smoothing visual car movement between network ticks is a rendering concern, not a timing concern.
- Anti-cheat — the timing library does not validate that position data is physically plausible. Verifying that distance correlates to elapsed time, or that sector triggers aren't fabricated, is the server's responsibility.