A complete in-game notification system: transient toasts that slide in and auto-dismiss, a history modal listing everything that happened, and an unread badge. Push notifications the quick way (build a NotificationData and push it) or the data-driven way (author NotificationDefinition assets and push by key through a service). Everything lives in com.cupkekgames.luna under the CupkekGames.Luna.Notifications namespace.
timeScale = 0, no coroutines.AutoDismissMs, 0 = manual).DedupKey bump a single toast's ×N count instead of stacking.MaxOnScreen, stagger, Fifo / PriorityThenFifo ordering, and Queue / DropOldest / DropNewest overflow.NotificationHistoryView) + unread badge (NotificationBadge) bound to the same feed.NotificationDefinition assets + a catalog + a push service, so gameplay code pushes by key and never hard-codes copy or icons.NotificationHistory round-trips through the save system (see Serialization).The system is three cooperating layers, all inside Luna:
| Layer | Types | Role |
|---|---|---|
| Model / feed | NotificationData, INotificationFeed, NotificationHistory | The data and the store. UI-agnostic. |
| UI | NotificationToastController, NotificationToastItem, NotificationBadge, NotificationHistoryView | Renders the feed as toasts + history + badge. |
| Data-driven | NotificationDefinition(SO), NotificationDefinitionCatalog, NotificationFactory, INotificationService | Authored content → runtime notifications, resolved by key. Mirrors the Data-Driven Service Flow. |
You can use just the model + toast controller (raw pushes) and ignore the data-driven layer entirely, or lean on the service for authored, localizable, icon-cataloged notifications.
The minimum: a NotificationToastController bound to a feed.
NotificationToastController to a UIViewComponent-style view whose UXML contains a NotificationContainer element (and optionally a NotificationModalButton + NotificationBadge label). The promoted Runtime/UI/Notification/NotificationView.uxml already has all three.Runtime/UI/Notification/Notification.uxml (the toast row the pool instantiates).using CupkekGames.Luna.Notifications;
[SerializeField] private NotificationToastController _toasts;
private readonly NotificationHistory _feed = new();
void Start()
{
_toasts.WhenUILoaded(() => _toasts.Bind(_feed));
}
void OnLootPickup(string item)
{
_feed.Push(new NotificationData
{
Title = "Loot",
Body = $"Picked up {item}",
Severity = NotificationSeverity.Success,
Category = "loot",
AutoDismissMs = 5000, // 0 = sticky (manual dismiss)
DedupKey = $"loot:{item}", // repeats bump ×N instead of stacking
});
}The controller only shows toasts while its view is visible. Notifications pushed while it is faded out are buffered and replayed (newest-first, capped at
MaxOnScreen) on fade-in — they always land in history regardless.
For real projects, author notifications as assets and push by key so gameplay code carries no copy, icons, or tuning.
NotificationDefinition assets (Create ▸ Luna ▸ Notifications ▸ Definition) and add them to a NotificationDefinitionCatalog (catalogId = "notification-definitions").SpriteCatalog (catalogId = "notification-icons") for the icons definitions reference by CatalogKey.NotificationServiceProviderSO + both catalogs via a ServiceRegistrySO (or a runtime ServiceRegistry component).var svc = ServiceLocator.Get<INotificationService>();
// "loot" resolves a NotificationDefinition; BodyArgs format its "Picked up {0}" body.
svc.Push(new CatalogKey { Catalog = NotificationConstants.DefinitionsCatalogId, Key = "loot" },
new NotificationArgs { BodyArgs = new[] { item } });NotificationFactory builds the NotificationData from the definition: it resolves the icon from the notification-icons catalog, formats title/body from *Fallback (or localized *Key, see Localization), stamps the timestamp, applies DedupKeyTemplate, and builds each runtime feature.
| Field | Purpose |
|---|---|
TitleKey / BodyKey | Localization keys ("table/entry"). |
TitleFallback / BodyFallback | Raw text when localization is absent; also the string.Format template for Args. |
IconKey | CatalogKey into the notification-icons catalog. |
Severity / Category / Priority / Size | Presentation + ordering. |
AutoDismissMs | 0 = sticky. |
DedupKeyTemplate | e.g. "loot:{0}", formatted with BodyArgs to produce the DedupKey. |
Features | INotificationDefinitionFeature list (e.g. NavPushDefinitionFeature). |
NotificationToastController is a UIViewComponent. Key serialized fields:
| Field | Description |
|---|---|
Notification Template | The toast-row VisualTreeAsset (Notification.uxml). Required. |
Policy | NotificationQueuePolicy — see below. Also settable at runtime. |
Modal Dest | CatalogKey whose Key is the nav-node id of the history modal; the bell button pushes it. |
Audio Source / Push Sound / Dismiss Sound | Optional toast SFX. |
| Element names | NotificationContainer, NotificationModalButton, NotificationBadge (defaults match NotificationView.uxml). |
_toasts.Policy = new NotificationQueuePolicy
{
MaxOnScreen = 5,
StaggerMs = 100,
Ordering = NotificationQueueOrdering.PriorityThenFifo, // or Fifo
Overflow = NotificationOverflowBehavior.Queue, // DropOldest | DropNewest
};Priority first; Fifo is arrival order.Each toast row (Notification.uxml) has two hit-zones: the card (ActivateButton) runs the notification's features and marks it read; the ✕ (DismissButton) just dismisses. Both animate the row out and, being "seen", mark it read.
NotificationHistoryView is a UIViewComponent over NotificationModal.uxml (a NotificationList ListView + ReturnButton). Open it by pushing its nav destination with a NotificationHistoryArgs:
LunaNavigation.Push(historyDest, new NotificationHistoryArgs
{
Feed = ServiceLocator.Get<INotificationService>().Feed,
MarkAllReadOnOpen = true, // clears the unread badge
});The NotificationToastController's bell button (NotificationModalButton) does exactly this using its Modal Dest. NotificationBadge binds a Label to INotificationFeed.UnreadCount and hides itself at zero — the controller wires it automatically from the NotificationBadge element in its view.
Attach INotificationFeatures to a notification to run behavior when its card is activated. The shipped one navigates:
// Authoring: add a NavPushDefinitionFeature to a NotificationDefinition,
// targeting a nav destination. Activating the toast pushes it.
data.Features.Add(new NavPushFeature { Destination = questBoardDest });Runtime features implement INotificationFeature { CloneFeature(); Activate(NotificationData); }; authoring features implement INotificationDefinitionFeature : IFeature and build the runtime feature in BuildRuntimeFeature(NotificationArgs).
NotificationHistory is the save-owned store. It round-trips through both Unity serialization and the Newtonsoft save path — its list is exposed as a concrete List<NotificationData> so the reflection serializer can populate it in place. Persist keys, not resolved values: NotificationData stores its CatalogNotificationImageSource (a CatalogKey), Category/Severity/ReadState/TimestampUtcTicks, and [SerializeReference] features — never a baked Sprite or localized string — so a reload re-resolves the icon and re-localizes in the current locale.
Register the persisted polymorphic types (NotificationData, CatalogNotificationImageSource, and every concrete feature) with your save system's known-types list. On load, hand the active save's history to the service once:
ServiceLocator.Get<INotificationService>().Bind(save.NotificationHistory);INotificationService.Feed is a stable façade (NotificationFeedRelay) — bind consumers (toast controller, badge, history) to it once; Bind repoints the backing store on save switch with no re-subscription.
When com.unity.localization is present (UNITY_LOCALIZATION), NotificationData.TitleKey/BodyKey ("table/entry") bind live and re-localize on locale change; otherwise the *Fallback text is used, formatted with Args. Under the hood this is the shared localized text API (BindLocalizedText for the views, LunaLocalization.Resolve for the baked snapshot the factory writes at build time). A non-empty key that is not in "table/entry" form logs a warning and falls back.
Applied to the toast/history row root (.notification):
| Class | Applied when |
|---|---|
.notification--info / --success / --warning / --error | matches Severity (built-in border colors). |
.notification--{category} | free-string Category (you author the color — e.g. .notification--loot). |
.notification-sm / .notification-lg | Size Small / Large. |
.active / .removing / .remove | pump lifecycle (slide-in / slide-out / collapse). |
The Notifications entry in the LunaUI Storybook sample (Samples ▸ Showcase) is a full playground: a control panel drives severity, category, size, sticky/auto-dismiss, priority, overflow policy, dedup grouping, the history modal + badge, NavPush, and both the raw and data-driven push paths.
Settings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction