Architecture

This page covers the internals of Spark's four core pillars and how they connect. Understanding this architecture will make everything else in the Developer Guide click.

The Spark Service Locator

Spark.cs is a static class that acts as the central registry for all plugins. Every plugin registers itself here at startup, and any system can retrieve a plugin by its interface type.

API

// Register a plugin instance
Spark.RegisterPlugin<IMyPlugin>(instance);

// Retrieve a plugin (returns null if not registered)
IMyPlugin plugin = Spark.GetPlugin<IMyPlugin>();

// Remove a plugin
Spark.UnregisterPlugin<IMyPlugin>();

// Access the network provider
INetworkProvider network = Spark.Network;

// Listen for network provider readiness
Spark.OnNetworkProviderReady += (provider) => { /* ... */ };

How Registration Works

Plugins register using Unity's [RuntimeInitializeOnLoadMethod] attribute, which runs automatically before any scene loads. Each plugin follows this pattern:

The two-phase approach (Reset at SubsystemRegistration, Register at BeforeSceneLoad) handles Unity's domain reload correctly. The #if UNITY_EDITOR block ensures the plugin is also available in edit mode for the Spark Editor window.

Initialization Order

Unity runs RuntimeInitializeOnLoadMethod in this order:

  1. SubsystemRegistration - Reset static state (domain reload safety)

  2. AfterAssembliesLoaded - Assemblies ready

  3. BeforeSplashScreen - Before splash

  4. BeforeSceneLoad - Plugin registration happens here

  5. AfterSceneLoad - Scene is loaded, entities are active

Spark itself initializes at SubsystemRegistration to clear its internal dictionaries, then plugins register at BeforeSceneLoad.

SparkDatabaseRegistry

The database registry is a static cache of all SparkDatabaseEntry ScriptableObjects in the project. It loads entries at startup and provides fast lookups.

How It Loads

In the editor, the registry uses AssetDatabase to find all entries. In builds, it loads from Resources/ folders. An AssetPostprocessor handles hot-reloading when entries are created, modified, or deleted in the editor.

Entries are indexed in two ways:

  • By ID: Dictionary<string, SparkDatabaseEntry> for O(1) lookup by unique ID

  • By Type: Dictionary<Type, List<SparkDatabaseEntry>> for retrieving all entries of a given type

API

Entry ID Format

Entry IDs are auto-generated in lowercase with underscores. They follow the pattern entrytype_name or similar. Once created, an ID cannot be changed. This ensures references to entries remain stable.

SparkEventBus

The event bus is the primary way plugins communicate with each other. It uses a priority-based publish/subscribe pattern.

Publishing Events

Subscribing to Events

Handler Priority

Handlers run in priority order, highest first. This lets you control execution order when multiple systems need to respond to the same event.

Event Consumption

If an event implements IConsumableEvent, a handler can stop propagation by returning true from Handle(). This prevents lower-priority handlers from receiving the event.

Type Matching

The event bus supports compatible type matching. If you subscribe to a base event class or an interface, your handler will receive all events that inherit from that class or implement that interface.

Event Contract

All events implement ISparkEvent:

The SparkEventBase abstract class provides a default implementation where Timestamp is set at construction and Priority defaults to 0.

SparkEntityRegistry

The entity registry tracks all SparkEntity instances in the scene. Entities register themselves automatically when they awaken and unregister when destroyed.

Lookup Methods

All lookups are O(1) dictionary access:

TryGet Variants

Every lookup method has a TryGet variant that returns bool and outputs the result, avoiding null checks:

Entity IDs

Each SparkEntity has an ID source that determines how its ID is generated:

  • Local: Auto-generated GUID + timestamp. Used for dynamically spawned objects.

  • Static: Manually assigned in the inspector. Used for persistent world objects (NPCs, interactables) that must be the same across sessions.

  • Networked: Assigned by the server at runtime. Used for multiplayer-synced objects.

Assembly Definitions

Spark uses assembly definitions extensively. Each plugin has its own Spark.PluginName.asmdef for the runtime code and optionally Spark.PluginName.Editor.asmdef for editor code.

This structure provides:

  • Compile isolation: Changes to one plugin don't recompile others.

  • Dependency control: Plugins declare explicit references to only the assemblies they need.

  • Optional dependencies: A plugin can check for another plugin's existence at runtime without requiring a compile-time reference.

The core assembly is Spark.Core. All plugins reference it. Plugins that need to interact with each other (like extensions) add explicit assembly references.

Optional Dependencies Pattern

When a plugin wants to interact with another plugin that may or may not be installed:

This null-check pattern is used throughout Spark. Plugins never assume other optional plugins are present.

Data Flow Summary

Here is the complete lifecycle of a typical game action:

This pattern is consistent across all of Spark. Once you understand it, you can predict how any system works.

Next Steps

Now that you understand the architecture, continue to Creating Your First Plugin to put this knowledge into practice.

Last updated

Was this helpful?