Jovian Popup System
A lightweight, low-allocation popup and tooltip system for Unity with category-based isolation, a fluent content builder, and extensible animations.
Requirements
- Unity 2022.3 or later
- TextMeshPro 3.0.6+
- Input System 1.18.0+
- Newtonsoft JSON 3.2.1+
Install via the Unity Package Manager by adding the package from its local path or from a git URL.
Quick Start
1. Create a PopupSettings asset
In the Unity Editor, go to Assets > Create > Jovian > Popup System > Popup Settings. Place the asset somewhere accessible (e.g. Assets/Settings/PopupSettings.asset). You can also configure settings via Project Settings > Jovian > Popup System.
2. Build a PopupReference prefab
See the Prefab Setup section below for step-by-step instructions.
3. Create and use PopupSystem
using Jovian.PopupSystem;
using Jovian.PopupSystem.UI;
// Create the system. viewPrefab is a reference to your PopupReference prefab.
var popup = new PopupSystem(settings, viewPrefab);
// Register categories you intend to use.
popup.RegisterCategory(PopupCategory.Item);
popup.RegisterCategory(PopupCategory.Character);
// Show a popup anchored to a UI element.
popup.Show(PopupCategory.Item, builder => {
builder
.AddHeader("Health Potion")
.AddSeparator()
.AddText("Restores a moderate amount of health.")
.AddStat("Heal Amount", 50)
.AddStat("Uses", "3 / 3");
}, anchorRect, AnchorSide.Right);
// Tick each frame (typically in Update or a game-state loop).
popup.Tick(Time.deltaTime);
// Clean up when the game state is torn down.
popup.Dispose();
PopupCategory
PopupCategory is a readonly struct that acts as a channel for isolating popups. Each category gets its own view instance and can have independent priority and delay settings.
Built-in categories
PopupCategory.Character // Character tooltips
PopupCategory.Item // Item tooltips
PopupCategory.Skill // Skill tooltips
PopupCategory.General // General-purpose tooltips
Custom categories
Create any number of additional categories:
var lootCategory = new PopupCategory("Loot");
popup.RegisterCategory(lootCategory, priority: 5);
Categories are compared by their string ID using ordinal comparison.
PopupSettings
PopupSettings is a ScriptableObject that holds all configuration. Create one via Assets > Create > Jovian > Popup System > Popup Settings.
| Field | Type | Default | Description |
|---|---|---|---|
popupDelay |
float | 0.4 | Seconds before the popup appears after a show request. |
fadeDuration |
float | 0.2 | Duration of the fade-in and fade-out animation. |
defaultAnchorSide |
AnchorSide | Below | Default side to anchor the popup relative to the target element. |
screenEdgePadding |
float | 10 | Minimum pixel distance from screen edges. |
maxPopupWidth |
float | 400 | Maximum width of the popup in pixels. |
sortingOrder |
int | 100 | Sorting order applied to the popup Canvas. |
followMouseOffset |
Vector2 | (15, -15) | Pixel offset from the cursor in follow-mouse mode. |
touchHoldDuration |
float | 0.6 | Seconds a touch must be held before triggering a popup. |
gamepadFocusTrigger |
bool | true | Whether gamepad focus triggers popups. |
categoryPriorities |
List | empty | Per-category priority overrides. Higher priority popups dismiss lower ones. |
categoryDelayOverrides |
List | empty | Per-category delay overrides. Overrides popupDelay for specific categories. |
PopupContentBuilder
PopupContentBuilder is a struct with a fluent API for composing popup content. You receive it in the build callback passed to Show or ShowAtPosition.
Available methods
builder.AddHeader("Fireball"); // Bold header text
builder.AddText("Deals fire damage to all enemies."); // Body text
builder.AddText("Rare", "FFD700"); // Colored text (hex, with or without #)
builder.AddStat("Damage", 120); // Label + integer value row
builder.AddStat("Range", "15m"); // Label + string value row
builder.AddImage(iconSprite, 64f); // Sprite with optional height
builder.AddSeparator(); // Horizontal divider line
Full example
popup.Show(PopupCategory.Skill, builder => {
builder
.AddHeader("Fireball")
.AddText("Hurls a ball of fire that explodes on impact.")
.AddSeparator()
.AddStat("Damage", 120)
.AddStat("Mana Cost", 35)
.AddStat("Range", "15m")
.AddSeparator()
.AddText("Requires: Level 5", "FF6666");
}, targetRect);
All elements are drawn from a grow-only pool inside PopupReference. No allocations occur once the pool is warmed.
PopupTrigger
PopupTrigger is a MonoBehaviour you attach to UI elements to get hover-based popup behavior automatically. It implements IPointerEnterHandler and IPointerExitHandler.
Inspector fields
| Field | Type | Description |
|---|---|---|
category |
PopupCategory | Which category channel to use. |
anchorSide |
AnchorSide | Which side of this element to anchor the popup. |
positionMode |
PopupPositionMode | AnchorToElement or FollowMouse. |
Initialization
PopupTrigger must be initialized from code before it will respond to pointer events:
var trigger = button.GetComponent<PopupTrigger>();
trigger.Initialize(popupSystem, builder => {
builder
.AddHeader("Attack")
.AddStat("Damage", 25);
});
You can also override the category at initialization time:
trigger.Initialize(popupSystem, PopupCategory.Skill, builder => {
builder.AddHeader("Fireball");
});
To change content without re-initializing:
trigger.UpdateContent(builder => {
builder
.AddHeader("Attack")
.AddStat("Damage", newDamageValue);
});
Code-Only Triggers
You do not need PopupTrigger to show popups. Call IPopupSystem methods directly:
Anchor to a RectTransform
popupSystem.Show(PopupCategory.Item, builder => {
builder.AddHeader("Iron Sword");
}, inventorySlotRect, AnchorSide.Right);
Follow the mouse cursor
Pass no anchor argument:
popupSystem.Show(PopupCategory.General, builder => {
builder.AddText("Click to interact");
});
Show at a fixed screen position
popupSystem.ShowAtPosition(PopupCategory.General, builder => {
builder.AddText("Tutorial tip");
}, new Vector2(Screen.width * 0.5f, Screen.height * 0.5f));
Hide
popupSystem.Hide(PopupCategory.Item); // Hide a specific category
popupSystem.HideAll(); // Hide all categories
Priority System
When a popup is shown, any visible popup from a lower-priority category is automatically dismissed. Priority is configured per category via PopupSettings.categoryPriorities or at registration time:
popup.RegisterCategory(PopupCategory.Character, priority: 10);
popup.RegisterCategory(PopupCategory.Item, priority: 5);
popup.RegisterCategory(PopupCategory.General, priority: 1);
If the player hovers a character portrait (priority 10) while an item tooltip (priority 5) is visible, the item tooltip is dismissed. Popups of equal or higher priority are not affected.
Settings-based priorities (from PopupSettings.categoryPriorities) take precedence over the priority argument in RegisterCategory.
Lifecycle Integration
The popup system is designed to be created per game state, not as a global singleton. Each game state owns its own IPopupSystem instance:
// In your game state constructor or initialization:
var popupSystem = new PopupSystem(popupSettings, popupViewPrefab);
popupSystem.RegisterCategory(PopupCategory.Character, priority: 10);
popupSystem.RegisterCategory(PopupCategory.Item, priority: 5);
// Pass to views/play modes via constructor DI
// In your game state's Tick/Update:
popupSystem.Tick(Time.deltaTime);
// When the game state exits:
popupSystem.Dispose(); // destroys all popup view GameObjects
Each category lazily creates its own PopupReference instance on first Show call. On Dispose, all views are destroyed. This ensures no leaked GameObjects when transitioning between game states.
IPopupAnimator
The popup system uses IPopupAnimator for show/hide transitions. The default implementation is FadePopupAnimator, which lerps the CanvasGroup.alpha over the configured fadeDuration.
Interface
public interface IPopupAnimator {
void Show(CanvasGroup canvasGroup, float duration, Action onComplete);
void Hide(CanvasGroup canvasGroup, float duration, Action onComplete);
void Tick(float deltaTime);
bool IsAnimating { get; }
}
Custom animator example
public sealed class ScalePopupAnimator : IPopupAnimator {
private CanvasGroup target;
private float duration;
private float elapsed;
private float startScale;
private float endScale;
private Action onComplete;
public bool IsAnimating => target != null;
public void Show(CanvasGroup canvasGroup, float duration, Action onComplete) {
target = canvasGroup;
this.duration = Mathf.Max(duration, 0.001f);
elapsed = 0f;
startScale = 0f;
endScale = 1f;
this.onComplete = onComplete;
canvasGroup.transform.localScale = Vector3.zero;
canvasGroup.alpha = 1f;
}
public void Hide(CanvasGroup canvasGroup, float duration, Action onComplete) {
target = canvasGroup;
this.duration = Mathf.Max(duration, 0.001f);
elapsed = 0f;
startScale = 1f;
endScale = 0f;
this.onComplete = onComplete;
}
public void Tick(float deltaTime) {
if(target == null) {
return;
}
elapsed += deltaTime;
var t = Mathf.Clamp01(elapsed / duration);
var scale = Mathf.Lerp(startScale, endScale, t);
target.transform.localScale = Vector3.one * scale;
if(t >= 1f) {
var callback = onComplete;
target = null;
onComplete = null;
callback?.Invoke();
}
}
}
Pass a factory function to the constructor so each category gets its own animator instance:
var popup = new PopupSystem(settings, viewPrefab, () => new ScalePopupAnimator());
Prefab Setup
Build the PopupReference prefab with the following hierarchy:
Step 1: Root GameObject
- Create a new GameObject named
PopupReference. - Add a
Canvascomponent. Set Render Mode to Screen Space - Overlay. Set Sort Order to matchPopupSettings.sortingOrder(default 100). - Add a
CanvasGroupcomponent. - Add a
PopupReferencecomponent (fromJovian.PopupSystem.UI).
Step 2: Background
- Create a child GameObject named
Background. - Add a
RectTransformand anImagecomponent. Set the image color/sprite to your desired popup background. - Add a
ContentSizeFitterwith Vertical Fit set to Preferred Size.
Step 3: Content container
- Create a child of
BackgroundnamedContent. - Add a
RectTransform. Stretch it to fill the background with appropriate padding. - Add a
VerticalLayoutGroup. Configure padding and spacing to taste. - Add a
ContentSizeFitterwith Vertical Fit set to Preferred Size.
Step 4: Element prefabs
Create these as child prefabs (or separate prefabs). Each must be a prefab reference, not an in-hierarchy instance:
| Prefab | Component | Notes |
|---|---|---|
| Header | TMP_Text |
Bold, larger font size |
| Text | TMP_Text |
Regular body text |
| Stat | RectTransform with HorizontalLayoutGroup |
Must have exactly two TMP_Text children: label and value |
| Image | Image |
For icons or artwork |
| Separator | Image |
Thin horizontal line |
Step 5: Wire references
On the PopupReference component, assign:
- Content - the Content RectTransform
- Canvas Group - the root CanvasGroup
- Background - the Background RectTransform
- Header Prefab - your header TMP_Text prefab
- Text Prefab - your body text TMP_Text prefab
- Stat Prefab - your stat row RectTransform prefab
- Image Prefab - your image prefab
- Separator Prefab - your separator Image prefab
Save as a prefab. The maxPopupWidth from PopupSettings is applied at runtime to constrain the popup's horizontal size.
Optimization Notes
The popup system is designed for minimal runtime allocation:
PopupCategoryis a readonly struct with value semantics -- zero heap allocation on pass or comparisonPopupContentBuilderis a struct that operates directly on cached UI elements -- no intermediate data structures- Content elements use a grow-only cache: elements are activated/deactivated, never instantiated or destroyed after warmup.
GetStatcaches TMP_Text references in a struct to avoidGetComponentsInChildrenon reuse - Delay timers and animation are driven by
floatfields inTick()-- no coroutines, noWaitForSeconds - Screen rect calculation uses a static
Vector3[4]buffer -- no per-frame array allocation - Layout rebuilds only occur when content changes (dirty flag), not on every position update
- Each registered category gets its own
IPopupAnimatorinstance, preventing state corruption during concurrent show/hide animations
API Reference
Core Types
| Type | Namespace | Description |
|---|---|---|
IPopupSystem |
Jovian.PopupSystem |
Main interface for showing/hiding popups. |
PopupSystem |
Jovian.PopupSystem |
Concrete implementation. Constructor: (PopupSettings, PopupReference, Func<IPopupAnimator>). |
PopupSettings |
Jovian.PopupSystem |
ScriptableObject with all configuration fields. |
PopupCategory |
Jovian.PopupSystem |
Readonly struct identifying a popup channel. |
PopupContentBuilder |
Jovian.PopupSystem |
Fluent struct for building popup content in callbacks. |
UI Types
| Type | Namespace | Description |
|---|---|---|
PopupReference |
Jovian.PopupSystem.UI |
MonoBehaviour managing popup layout, pooling, and positioning. |
PopupTrigger |
Jovian.PopupSystem.UI |
MonoBehaviour for hover-based popup triggers on UI elements. |
Animation Types
| Type | Namespace | Description |
|---|---|---|
IPopupAnimator |
Jovian.PopupSystem |
Interface for custom show/hide animations. |
FadePopupAnimator |
Jovian.PopupSystem |
Default fade animation via CanvasGroup alpha. |
Enums
| Type | Values |
|---|---|
AnchorSide |
Below, Above, Left, Right |
PopupPositionMode |
AnchorToElement, FollowMouse |
License
See the LICENSE file in the package root.