docs
Systems
Infrastructure
ApplicationSystem
Architecture and Design

Design patterns in use

  • Singleton (enforced in Awake): ApplicationCoordinator exposes a static Instance field. Awake assigns it on the first instance, calls DontDestroyOnLoad, and destroys any duplicate. The code carries a TODO to remove this singleton in favor of runtime initialization.
  • Service locator: ServiceLocator holds one BusRegistry reference set during bootstrap with Initialize, and other classes retrieve it with GetBusRegistry. FlowController and ApplicationFlowController use it to reach the command and query buses.
  • Factory: ApplicationEventFactory builds and raises Event messages on Domain.Application. ApplicationQueryFactory builds and sends Query messages on the same domain.
  • Adapter: IApplicationLifecycleAdapter is the contract for bridging an engine's lifecycle callbacks. UnityLifecycleAdapter implements it for Unity.
  • Template method: ApplicationLifecycleHandler.OnFocusChanged is not overridable. It raises the standard ApplicationEventFactory.OnFocusChanged event, then calls the abstract ProcessFocusChanged hook that each project implements.
  • Abstract base with hooks: ApplicationCoordinator defines abstract hooks (RegisterPlatformModules, RunPlatformModules, InitializeStartupPresentation, InitializeVolumeState, CreateLifecycleHandler, CreateLifecycleAdapter) that each project fills in.
  • Partial static class: ApplicationFlowService is declared partial, so project code can add content-specific methods in separate files.

How the system connects to dependencies

Initialization system. ApplicationCoordinator implements IInitializable and IAsyncInitializable. Its Awake registers it with InitializationRegistry, which places it in the sync list (it does not implement the event receiver interface) and the async list. InitializationManager.Start runs Init on the sync list, then awaits InitAsync on the async list, clears the registry, and raises SystemsReady.

Signal system. InitAsync calls WhenRaised on SaffronLibrarySignals.SystemsReady with InitializeApplication as the callback. If SystemsReady has already been raised, the callback runs immediately. After that, InitializeApplication runs the platform module, presentation, and lifecycle steps.

Bus system. Events are published through BusUtility.Raise(Event), which forwards to the EventBus. Queries are sent through BusUtility.AskAndGet, which forwards to the QueryBus. ApplicationFlowController registers OnQuery with queryBus.Register(Domain.Application, ...) during Init, so the query bus routes Application-domain queries to the coordinator.

Command system. ApplicationFlowController extends FlowController, which registers HandleCommand on the CommandBus for Domain.Application during Init. This system's HandleCommand is empty, so no commands are handled for this domain.

Param and query contracts. Messages carry data in Params. ApplicationParamKeys defines the typed keys (ParamKey<bool> for focus, ParamKey<ApplicationSubjects> for the subject). The focus-loss answer is written to CoreParamKeys.Duration, a ParamKey<float> defined in the Contracts system.

Contracts. Domain.Application (value 7) and QueryVerb.Get route the messages. CoreParamKeys.Duration is the result key for focus-loss queries.

Key concepts and terminology

  • Subject: ApplicationSubjects (Focus, Application) identifies which application topic a query or event is about. It is stored under ApplicationParamKeys.ApplicationSubject.
  • Verb: ApplicationEventVerb (Changed, Quit) identifies what happened in an event. The Event verb is set to the enum's integer value.
  • Focus-loss time: ApplicationLogicManager stores a single float timestamp, in engine seconds, from SetFocusLossTime. GetFocusLossDuration returns the current time minus that timestamp. The stored value starts at 0, so before any call to SetFocusLossTime the duration equals the time since startup.
  • Realtime clock: UnityLifecycleAdapter.GetRealtimeSinceStartup returns Time.realtimeSinceStartup. The same clock must be used for both SetFocusLossTime and GetFocusLossDuration.
  • Lifecycle handler: IApplicationLifecycleHandler receives forwarded focus changes. Its implementation, ApplicationLifecycleHandler, is engine-agnostic.

Event and data flow

Focus flow:

  1. Unity calls the private OnApplicationFocus method on UnityLifecycleAdapter.
  2. The adapter calls handler.OnFocusChanged(hasFocus) on the injected handler, if one has been set.
  3. ApplicationLifecycleHandler.OnFocusChanged calls ApplicationEventFactory.OnFocusChanged(hasFocus), which raises an Event (verb Changed, subject Focus, HasFocus param) on Domain.Application through the EventBus.
  4. The handler calls ProcessFocusChanged(hasFocus), the project-specific hook.

Quit flow:

  1. Unity calls the private OnApplicationQuit method on UnityLifecycleAdapter.
  2. The adapter calls ApplicationEventFactory.OnApplicationQuit, which raises an Event (verb Quit, subject Application) on Domain.Application.

Focus-loss query flow:

  1. A caller calls ApplicationQueryFactory.GetFocusLossDuration, which builds a Query for Domain.Application with verb Get and subject Focus, then calls BusUtility.AskAndGet with CoreParamKeys.Duration.
  2. The QueryBus routes the query to ApplicationFlowController.OnQuery, which calls ApplicationCoordinator.ProcessQuery.
  3. ProcessQuery reads the subject, calls logicManager.GetFocusLossDuration(lifecycleAdapter.GetRealtimeSinceStartup()), and writes the result to CoreParamKeys.Duration.
  4. The answer returns to the caller through the same query object.

Startup flow:

  1. Awake registers the coordinator with InitializationRegistry.
  2. InitializationManager.Start calls Init, which creates ApplicationLogicManager.
  3. InitializationManager.Start awaits InitAsync, which subscribes InitializeApplication to SystemsReady and then awaits InitializeVolumeState.
  4. When SystemsReady is raised, InitializeApplication calls RegisterPlatformModules, RunPlatformModules, InitializeStartupPresentation, and InitializeLifecycle.
  5. InitializeLifecycle creates the handler and adapter through the abstract factory hooks, stores them, and calls lifecycleAdapter.Initialize(lifecycleHandler).