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