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. The factory resolves keys at build time via NotificationLocalization.

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