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.

Features

  • Toast feed with a single-serial pump on unscaled time — animates correctly at timeScale = 0, no coroutines.
  • Severities (Info / Success / Warning / Error) drive built-in border colors; free-string categories drive your own USS classes.
  • Auto-dismiss or sticky per notification (AutoDismissMs, 0 = manual).
  • Dedup / grouping — repeated pushes that share a DedupKey bump a single toast's ×N count instead of stacking.
  • Queue policyMaxOnScreen, stagger, Fifo / PriorityThenFifo ordering, and Queue / DropOldest / DropNewest overflow.
  • History modal (NotificationHistoryView) + unread badge (NotificationBadge) bound to the same feed.
  • Object pooling for toast rows.
  • Data-driven layerNotificationDefinition assets + a catalog + a push service, so gameplay code pushes by key and never hard-codes copy or icons.
  • PersistableNotificationHistory round-trips through the save system (see Serialization).

Architecture

The system is three cooperating layers, all inside Luna:

LayerTypesRole
Model / feedNotificationData, INotificationFeed, NotificationHistoryThe data and the store. UI-agnostic.
UINotificationToastController, NotificationToastItem, NotificationBadge, NotificationHistoryViewRenders the feed as toasts + history + badge.
Data-drivenNotificationDefinition(SO), NotificationDefinitionCatalog, NotificationFactory, INotificationServiceAuthored 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.

Quick start (raw)

The minimum: a NotificationToastController bound to a feed.

  1. Add a 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.
  2. Set its Notification Template to Runtime/UI/Notification/Notification.uxml (the toast row the pool instantiates).
  3. Bind a feed and push:
csharp
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.

Data-driven pushes

For real projects, author notifications as assets and push by key so gameplay code carries no copy, icons, or tuning.

  1. Create NotificationDefinition assets (Create ▸ Luna ▸ Notifications ▸ Definition) and add them to a NotificationDefinitionCatalog (catalogId = "notification-definitions").
  2. Create a SpriteCatalog (catalogId = "notification-icons") for the icons definitions reference by CatalogKey.
  3. Register the NotificationServiceProviderSO + both catalogs via a ServiceRegistrySO (or a runtime ServiceRegistry component).
  4. Push by key:
csharp
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.

NotificationDefinition fields

FieldPurpose
TitleKey / BodyKeyLocalization keys ("table/entry").
TitleFallback / BodyFallbackRaw text when localization is absent; also the string.Format template for Args.
IconKeyCatalogKey into the notification-icons catalog.
Severity / Category / Priority / SizePresentation + ordering.
AutoDismissMs0 = sticky.
DedupKeyTemplatee.g. "loot:{0}", formatted with BodyArgs to produce the DedupKey.
FeaturesINotificationDefinitionFeature list (e.g. NavPushDefinitionFeature).

Toast controller

NotificationToastController is a UIViewComponent. Key serialized fields:

FieldDescription
Notification TemplateThe toast-row VisualTreeAsset (Notification.uxml). Required.
PolicyNotificationQueuePolicy — see below. Also settable at runtime.
Modal DestCatalogKey whose Key is the nav-node id of the history modal; the bell button pushes it.
Audio Source / Push Sound / Dismiss SoundOptional toast SFX.
Element namesNotificationContainer, NotificationModalButton, NotificationBadge (defaults match NotificationView.uxml).

Queue policy

csharp
_toasts.Policy = new NotificationQueuePolicy { MaxOnScreen = 5, StaggerMs = 100, Ordering = NotificationQueueOrdering.PriorityThenFifo, // or Fifo Overflow = NotificationOverflowBehavior.Queue, // DropOldest | DropNewest };
  • PriorityThenFifo surfaces higher Priority first; Fifo is arrival order.
  • Queue holds overflow until a slot frees; DropOldest slides the oldest toast out; DropNewest discards the incoming toast (it still lands in history and the badge).

Toast interaction

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.

History modal & badge

NotificationHistoryView is a UIViewComponent over NotificationModal.uxml (a NotificationList ListView + ReturnButton). Open it by pushing its nav destination with a NotificationHistoryArgs:

csharp
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.

Features (NavPush)

Attach INotificationFeatures to a notification to run behavior when its card is activated. The shipped one navigates:

csharp
// 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).

Serialization & persistence

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:

csharp
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.

Localization

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.

CSS classes

Applied to the toast/history row root (.notification):

ClassApplied when
.notification--info / --success / --warning / --errormatches Severity (built-in border colors).
.notification--{category}free-string Category (you author the color — e.g. .notification--loot).
.notification-sm / .notification-lgSize Small / Large.
.active / .removing / .removepump lifecycle (slide-in / slide-out / collapse).

Sample

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.

See also

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL