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:
SubsystemRegistration- Reset static state (domain reload safety)AfterAssembliesLoaded- Assemblies readyBeforeSplashScreen- Before splashBeforeSceneLoad- Plugin registration happens hereAfterSceneLoad- 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 IDBy 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?
