Skip to content

Advanced Customization (Code)

This guide is for developers who want to customize behavior, build custom UIs, or implement their own race rules.

1) Prefer Core for race logic

  • Core (Assets/RaceTiming/Core/) is pure C# and testable outside Unity.
  • Adapter/UI are Unity-specific. Keep them thin.

If you’re adding: - race rules, ranking logic, finish rules → Core - MonoBehaviour glue, transforms, colliders → Adapter - display-only code → UI


2) Subscribe to events (C#)

You can subscribe directly to Core events via the LapTimer:

using UnityEngine;
using RaceTiming.Adapter;
using RaceTiming.Core;
using RaceTiming.Core.Events;

public class RaceAnnouncer : MonoBehaviour
{
    private LapTimer _timer;

    void Start()
    {
        _timer = RaceTimingManager.Instance.LapTimer;
        _timer.LapCompleted += OnLapCompleted;
        _timer.NewSessionBestLap += OnNewSessionBestLap;
    }

    void OnDestroy()
    {
        if (_timer == null) return;
        _timer.LapCompleted -= OnLapCompleted;
        _timer.NewSessionBestLap -= OnNewSessionBestLap;
    }

    private void OnLapCompleted(LapCompletedEventArgs e)
        => Debug.Log($"Car {e.CompetitorId} lap {e.LapData.LapNumber} in {e.LapData.Duration:F3}s");

    private void OnNewSessionBestLap(SessionBestLapEventArgs e)
        => Debug.Log($"SESSION BEST: {e.NewSessionBestLap.Duration:F3}s by {e.CompetitorId}");
}

UnityEvents alternative

If you prefer Inspector wiring, RaceTimingManager exposes UnityEvents for these same events.


3) Build your own UI

A common “hybrid” approach: - use events for discrete updates (lap complete, position changed) - poll session data for continuously changing values (live gaps)

Get the current session snapshot

var session = RaceTimingManager.Instance.LapTimer.GetSessionData();

On-track interval ordering

var intervals = RaceTimingManager.Instance.LapTimer.QueryTrackPositionIntervals(referenceDriverId);

4) Custom ranking strategies

Ranking is pluggable via IRaceRankingStrategy (Core):

public interface IRaceRankingStrategy
{
    void UpdateStandings(Session session);
}

Built-in strategies: - StandardRaceRanking (started first, then laps desc, then distance desc) - BestLapRanking (best lap asc)

To add a new strategy: 1. Implement IRaceRankingStrategy in Core. 2. Set it on the RaceDirector (Core).

Note: RaceDirector is responsible for standings updates and also triggers lapped detection + gap calculation.


5) Custom finish conditions

Finish conditions are pluggable via ISessionFinishCondition (Core):

public interface ISessionFinishCondition
{
    bool IsComplete(Session session, int competitorId);
    IReadOnlyList<int> GetFinalOrder(Session session);
    SessionProgress GetProgress(Session session);
}

Built-in: - LapCountFinishCondition

Important implementation note

RaceTimingManager currently sets the finish condition using reflection to reach LapTimer’s internal session. If you plan to extend finish logic heavily, consider adding a proper public API in Core (e.g., LapTimer.SetFinishCondition(...)) to avoid reflection.


6) Custom sector definitions

Sectors are defined by ratios around the lap.

  • The default list is generated by SectorDefinition.GenerateDefault(sectorCount).
  • You can manually edit sectors on the track asset (TrackMarkerScriptableObject.sectors) in the Inspector, or by dragging the yellow sector handles in the Scene View.

Sector timing is triggered by the adapter calling: - LapTimer.TriggerSector(competitorId, sectorId)


7) Testing recommendation

Core is designed to be testable via dotnet test (see tests/RaceTiming.Core.Tests/). If you add race logic, prefer adding unit tests there.