forked from Shardstone/trail-into-darkness
Made the popup system a lot more generic
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Jovian Popup System
|
||||
|
||||
A lightweight, low-allocation popup and tooltip system for Unity with category-based isolation, a fluent content builder, and extensible animations.
|
||||
A lightweight, low-allocation popup and tooltip system for Unity with category-based isolation, a generic element cache, type-safe element identifiers, and extensible animations.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -15,11 +15,11 @@ Install via the Unity Package Manager by adding the package from its local path
|
||||
|
||||
### 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**.
|
||||
In the Unity Editor, go to **Assets > Create > Jovian > Popup System > Popup Settings**. Configure element prefabs by adding entries to the Element Prefabs list, mapping `PopupElementType` values to your prefabs.
|
||||
|
||||
### 2. Build a PopupReference prefab
|
||||
|
||||
See the [Prefab Setup](#prefab-setup) section below for step-by-step instructions.
|
||||
See the [Prefab Setup](#prefab-setup) section below.
|
||||
|
||||
### 3. Create and use PopupSystem
|
||||
|
||||
@@ -27,38 +27,78 @@ See the [Prefab Setup](#prefab-setup) section below for step-by-step instruction
|
||||
using Jovian.PopupSystem;
|
||||
using Jovian.PopupSystem.UI;
|
||||
|
||||
// Create the system. Pass the scene Canvas as canvasRoot to auto-scan all PopupTrigger components.
|
||||
// Create the system. canvasRoot auto-scans all PopupTrigger components.
|
||||
var popup = new PopupSystem(settings, viewPrefab, canvasRoot);
|
||||
|
||||
// Register categories you intend to use.
|
||||
popup.RegisterCategory(PopupCategory.Item);
|
||||
popup.RegisterCategory(PopupCategory.Character);
|
||||
|
||||
// Option A: Set content on auto-scanned triggers by name.
|
||||
var trigger = popup.GetTrigger("ItemSlot");
|
||||
trigger?.SetContent(builder => {
|
||||
// Set content on auto-scanned triggers by name.
|
||||
var handler = popup.GetTriggerHandler("ItemSlot");
|
||||
handler?.SetContent(builder => {
|
||||
builder
|
||||
.AddHeader("Health Potion")
|
||||
.AddSeparator()
|
||||
.AddText("Restores a moderate amount of health.")
|
||||
.AddStat("Heal Amount", 50);
|
||||
.AddText("Health Potion", PopupElementType.Header)
|
||||
.AddSeparator(PopupElementType.Separator)
|
||||
.AddText("Restores health.", PopupElementType.Text)
|
||||
.AddNameValue("Heal Amount", 50, PopupElementType.LabelValueText);
|
||||
});
|
||||
|
||||
// Option B: Show a popup directly from code.
|
||||
// Or show a popup directly from code.
|
||||
popup.Show(PopupCategory.Item, builder => {
|
||||
builder.AddHeader("Iron Sword").AddStat("Damage", 12);
|
||||
builder
|
||||
.AddText("Iron Sword", PopupElementType.Header)
|
||||
.AddNameValue("Damage", 12, PopupElementType.LabelValueText);
|
||||
}, anchorRect, AnchorSide.Right);
|
||||
|
||||
// Tick each frame (typically in Update or a game-state loop).
|
||||
// Tick each frame.
|
||||
popup.Tick(Time.deltaTime);
|
||||
|
||||
// Clean up when the game state is torn down.
|
||||
// Clean up when the game state exits.
|
||||
popup.Dispose();
|
||||
```
|
||||
|
||||
## PopupElementType
|
||||
|
||||
`PopupElementType` is a type-safe struct identifying element prefabs in the registry. The Inspector shows a dropdown with built-in types plus a custom text field for game-specific elements.
|
||||
|
||||
### Built-in types
|
||||
|
||||
| Static Field | ID | Usage |
|
||||
|---|---|---|
|
||||
| `PopupElementType.Header` | `"header"` | Bold header text |
|
||||
| `PopupElementType.Text` | `"text"` | Body text |
|
||||
| `PopupElementType.LabelValueText` | `"label_value_text"` | Label + value row (two TMP_Text children) |
|
||||
| `PopupElementType.Image` | `"image"` | Image/icon element |
|
||||
| `PopupElementType.Separator` | `"separator"` | Horizontal divider line |
|
||||
|
||||
### Custom types
|
||||
|
||||
Define game-specific element types as static fields:
|
||||
|
||||
```csharp
|
||||
public static class MyPopupElements {
|
||||
public static readonly PopupElementType Badge = new("badge");
|
||||
public static readonly PopupElementType ProgressBar = new("progress_bar");
|
||||
}
|
||||
```
|
||||
|
||||
### Variants
|
||||
|
||||
Create variants of built-in types using `Variant()`:
|
||||
|
||||
```csharp
|
||||
// "header_gold" — a gold-styled header
|
||||
PopupElementType.Header.Variant("gold")
|
||||
|
||||
// "separator_thick" — a thicker separator
|
||||
PopupElementType.Separator.Variant("thick")
|
||||
```
|
||||
|
||||
Register variant prefabs in PopupSettings with the variant ID (e.g. "header_gold").
|
||||
|
||||
## 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.
|
||||
`PopupCategory` is a readonly struct that acts as a channel for isolating popups. Each category gets its own view instance.
|
||||
|
||||
### Built-in categories
|
||||
|
||||
@@ -71,47 +111,55 @@ PopupCategory.General // General-purpose tooltips
|
||||
|
||||
### Custom categories
|
||||
|
||||
Create any number of additional categories:
|
||||
|
||||
```csharp
|
||||
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**.
|
||||
ScriptableObject holding all configuration. Create 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. |
|
||||
| `popupDelay` | float | 0.4 | Seconds before popup appears. |
|
||||
| `fadeDuration` | float | 0.2 | Fade animation duration. |
|
||||
| `defaultAnchorSide` | AnchorSide | Below | Default anchor side. |
|
||||
| `screenEdgePadding` | float | 10 | Minimum pixels from screen edge. |
|
||||
| `maxPopupWidth` | float | 400 | Maximum popup width in pixels. |
|
||||
| `sortingOrder` | int | 100 | Canvas sorting order. |
|
||||
| `followMouseOffset` | Vector2 | (15, -15) | Cursor offset in follow mode. |
|
||||
| `touchHoldDuration` | float | 0.6 | Touch hold duration. |
|
||||
| `gamepadFocusTrigger` | bool | true | Trigger on gamepad focus. |
|
||||
| `elementPrefabs` | List | empty | Element prefab registry (PopupElementType -> prefab). |
|
||||
| `categoryPriorities` | List | empty | Per-category priority overrides. |
|
||||
| `categoryDelayOverrides` | List | empty | Per-category delay overrides. |
|
||||
|
||||
## 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`.
|
||||
Fluent API struct for composing popup content. All methods take a `PopupElementType` to identify which prefab to use.
|
||||
|
||||
### Available methods
|
||||
### Methods
|
||||
|
||||
```csharp
|
||||
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
|
||||
// Generic — returns raw GameObject for custom elements
|
||||
builder.Add(PopupElementType.Header);
|
||||
builder.Add(new PopupElementType("custom_widget"));
|
||||
|
||||
// Text — sets TMP_Text on the element
|
||||
builder.AddText("Fireball", PopupElementType.Header);
|
||||
builder.AddText("Body text here.", PopupElementType.Text);
|
||||
builder.AddText("Colored text", "#FFD700", PopupElementType.Text);
|
||||
|
||||
// Name/Value — sets two TMP_Text children (label + value)
|
||||
builder.AddNameValue("Damage", 120, PopupElementType.LabelValueText);
|
||||
builder.AddNameValue("Range", "15m", PopupElementType.LabelValueText);
|
||||
|
||||
// Image — sets sprite and height
|
||||
builder.AddImage(iconSprite, PopupElementType.Image, 64f);
|
||||
|
||||
// Separator
|
||||
builder.AddSeparator(PopupElementType.Separator);
|
||||
```
|
||||
|
||||
### Full example
|
||||
@@ -119,331 +167,143 @@ builder.AddSeparator(); // Horizontal divider lin
|
||||
```csharp
|
||||
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");
|
||||
.AddText("Fireball", PopupElementType.Header)
|
||||
.AddText("Hurls a ball of fire.", PopupElementType.Text)
|
||||
.AddSeparator(PopupElementType.Separator)
|
||||
.AddNameValue("Damage", 120, PopupElementType.LabelValueText)
|
||||
.AddNameValue("Mana Cost", 35, PopupElementType.LabelValueText)
|
||||
.AddSeparator(PopupElementType.Separator)
|
||||
.AddText("Requires: Level 5", "FF6666", PopupElementType.Text);
|
||||
}, 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`.
|
||||
MonoBehaviour attached to UI elements for hover-based popups. Forwards pointer events to a `PopupTriggerView` behavior handler.
|
||||
|
||||
### Inspector fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `category` | PopupCategory | Which category channel to use. |
|
||||
| `anchorSide` | AnchorSide | Which side of this element to anchor the popup. |
|
||||
| `anchorSide` | AnchorSide | Which side to anchor the popup. |
|
||||
| `positionMode` | PopupPositionMode | `AnchorToElement` or `FollowMouse`. |
|
||||
|
||||
### Approach 1: Auto-scanned triggers (recommended for static UI)
|
||||
|
||||
When you pass a `canvasRoot` to the `PopupSystem` constructor, it auto-scans all `PopupTrigger` components and binds them. You only need to set content:
|
||||
|
||||
```csharp
|
||||
var popup = new PopupSystem(settings, viewPrefab, canvasRoot);
|
||||
popup.RegisterCategory(PopupCategory.Character);
|
||||
|
||||
// Find a trigger by its GameObject name and set content
|
||||
var trigger = popup.GetTrigger("HeroPortrait");
|
||||
trigger?.SetContent(builder => {
|
||||
builder.AddHeader("Kael").AddStat("Health", 55);
|
||||
// GetTriggerHandler returns PopupTriggerView (the behavior handler)
|
||||
var handler = popup.GetTriggerHandler("HeroPortrait");
|
||||
handler?.SetContent(builder => {
|
||||
builder
|
||||
.AddText("Kael", PopupElementType.Header)
|
||||
.AddNameValue("Health", 55, PopupElementType.LabelValueText);
|
||||
});
|
||||
|
||||
// Or set content on all triggers of a category
|
||||
foreach(var t in popup.GetTriggers(PopupCategory.Item)) {
|
||||
t.SetContent(builder => builder.AddHeader("Item"));
|
||||
// Or all handlers for a category
|
||||
foreach(var h in popup.GetTriggerHandlers(PopupCategory.Item)) {
|
||||
h.SetContent(builder => builder.AddText("Item", PopupElementType.Header));
|
||||
}
|
||||
```
|
||||
|
||||
### Approach 2: Dynamic triggers (for runtime-created UI)
|
||||
|
||||
For UI elements instantiated at runtime (inventory slots, party portraits), use `InitializeTriggersInChildren` after instantiation:
|
||||
|
||||
```csharp
|
||||
// After instantiating slot prefabs under a container:
|
||||
popup.InitializeTriggersInChildren(slotsContainer, trigger => {
|
||||
popup.InitializeTriggersInChildren(slotsContainer, (trigger, view) => {
|
||||
var slotData = trigger.GetComponentInParent<SlotComponent>();
|
||||
trigger.SetContent(builder => {
|
||||
builder.AddHeader(slotData.itemName);
|
||||
view.SetContent(builder => {
|
||||
builder.AddText(slotData.itemName, PopupElementType.Header);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
This scans, binds, and registers all triggers under the parent, then calls the callback on each.
|
||||
|
||||
### Approach 3: Full manual initialization
|
||||
|
||||
For complete control, bypass auto-scan and initialize triggers directly:
|
||||
### Approach 3: Code-only (no PopupTrigger needed)
|
||||
|
||||
```csharp
|
||||
var trigger = button.GetComponent<PopupTrigger>();
|
||||
trigger.Initialize(popupSystem, builder => {
|
||||
builder.AddHeader("Attack").AddStat("Damage", 25);
|
||||
});
|
||||
// Anchored
|
||||
popupSystem.Show(PopupCategory.Item, builder => { ... }, targetRect, AnchorSide.Right);
|
||||
|
||||
// Or with a category override:
|
||||
trigger.Initialize(popupSystem, PopupCategory.Skill, builder => {
|
||||
builder.AddHeader("Fireball");
|
||||
});
|
||||
```
|
||||
// Follow mouse
|
||||
popupSystem.Show(PopupCategory.General, builder => { ... });
|
||||
|
||||
### Updating content
|
||||
// Fixed screen position
|
||||
popupSystem.ShowAtPosition(PopupCategory.General, builder => { ... }, screenPos);
|
||||
|
||||
To change content on an already-initialized trigger without re-binding:
|
||||
|
||||
```csharp
|
||||
trigger.SetContent(builder => {
|
||||
builder.AddHeader("Attack").AddStat("Damage", newDamageValue);
|
||||
});
|
||||
|
||||
// UpdateContent is an alias for SetContent
|
||||
trigger.UpdateContent(builder => { ... });
|
||||
```
|
||||
|
||||
## Code-Only Triggers
|
||||
|
||||
You do not need `PopupTrigger` to show popups. Call `IPopupSystem` methods directly:
|
||||
|
||||
### Anchor to a RectTransform
|
||||
|
||||
```csharp
|
||||
popupSystem.Show(PopupCategory.Item, builder => {
|
||||
builder.AddHeader("Iron Sword");
|
||||
}, inventorySlotRect, AnchorSide.Right);
|
||||
```
|
||||
|
||||
### Follow the mouse cursor
|
||||
|
||||
Pass no anchor argument:
|
||||
|
||||
```csharp
|
||||
popupSystem.Show(PopupCategory.General, builder => {
|
||||
builder.AddText("Click to interact");
|
||||
});
|
||||
```
|
||||
|
||||
### Show at a fixed screen position
|
||||
|
||||
```csharp
|
||||
popupSystem.ShowAtPosition(PopupCategory.General, builder => {
|
||||
builder.AddText("Tutorial tip");
|
||||
}, new Vector2(Screen.width * 0.5f, Screen.height * 0.5f));
|
||||
```
|
||||
|
||||
### Hide
|
||||
|
||||
```csharp
|
||||
popupSystem.Hide(PopupCategory.Item); // Hide a specific category
|
||||
popupSystem.HideAll(); // Hide all categories
|
||||
// Hide
|
||||
popupSystem.Hide(PopupCategory.Item);
|
||||
popupSystem.HideAll();
|
||||
```
|
||||
|
||||
## 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:
|
||||
Higher priority categories dismiss lower ones when shown:
|
||||
|
||||
```csharp
|
||||
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`.
|
||||
Settings-based priorities take precedence over registration arguments.
|
||||
|
||||
## 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:
|
||||
Created per game state, not as a singleton:
|
||||
|
||||
```csharp
|
||||
// In your game state initialization:
|
||||
var guiCanvas = guiReferences.GetComponentInParent<Canvas>().transform;
|
||||
var popupSystem = new PopupSystem(popupSettings, popupViewPrefab, guiCanvas);
|
||||
popupSystem.RegisterCategory(PopupCategory.Character, priority: 10);
|
||||
popupSystem.RegisterCategory(PopupCategory.Item, priority: 5);
|
||||
|
||||
// All PopupTrigger components under guiCanvas are auto-scanned and bound.
|
||||
// Pass popupSystem to views/play modes via constructor DI.
|
||||
|
||||
// In your game state's Tick/Update:
|
||||
// Tick each frame
|
||||
popupSystem.Tick(Time.deltaTime);
|
||||
|
||||
// When the game state exits:
|
||||
popupSystem.Dispose(); // destroys all popup view GameObjects
|
||||
// Dispose on state exit
|
||||
popupSystem.Dispose();
|
||||
```
|
||||
|
||||
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
|
||||
Extensible show/hide animation interface. Default: `FadePopupAnimator` (alpha lerp). Pass a factory to the constructor:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
var popup = new PopupSystem(settings, viewPrefab, () => new ScalePopupAnimator());
|
||||
var popup = new PopupSystem(settings, viewPrefab, canvasRoot, () => new ScalePopupAnimator());
|
||||
```
|
||||
|
||||
## Prefab Setup
|
||||
|
||||
Build the `PopupReference` prefab with the following hierarchy. Reference prefabs are included in the `Samples~/Prefabs` folder.
|
||||
|
||||
```
|
||||
PopupReferencePrefab Canvas, CanvasGroup, CanvasScaler, PopupReference
|
||||
Background Image (popup background), ContentSizeFitter (V=Preferred)
|
||||
Content VerticalLayoutGroup (Control Child Size Width, Child Force Expand Width)
|
||||
Background Image, ContentSizeFitter (V=Preferred)
|
||||
Content VerticalLayoutGroup (Control Child Width, Force Expand Width)
|
||||
```
|
||||
|
||||
### Step 1: Root GameObject
|
||||
|
||||
1. Create a new GameObject named `PopupReferencePrefab`
|
||||
2. Add a `Canvas` component. Set **Render Mode** to **Screen Space - Overlay**
|
||||
3. Add a `CanvasScaler` with **Scale With Screen Size**, reference resolution matching your project (e.g. 1920x1080). This is needed for prefab editing; at runtime the nested Canvas inherits the parent's scaler
|
||||
4. Add a `CanvasGroup` component
|
||||
5. Add a `PopupReference` component (from `Jovian.PopupSystem.UI`)
|
||||
6. Set anchors to a single point (e.g. Min 0,1 Max 0,1), Pivot (0,1) for top-left origin
|
||||
7. Do **not** add a `ContentSizeFitter` to the root
|
||||
|
||||
### Step 2: Background
|
||||
|
||||
1. Create a child GameObject named `Background`
|
||||
2. Add an `Image` component. Set the image sprite/color to your desired popup background
|
||||
3. Add a `ContentSizeFitter` with **Horizontal Fit = Unconstrained** and **Vertical Fit = Preferred Size**
|
||||
4. Stretch the RectTransform to fill the root (anchors 0,0 to 1,1, offsets 0) or use point anchors with the ContentSizeFitter driving the size
|
||||
|
||||
### Step 3: Content container
|
||||
|
||||
1. Create a child of `Background` named `Content`
|
||||
2. Stretch the RectTransform to fill the Background (anchors 0,0 to 1,1, offsets for padding)
|
||||
3. Add a `VerticalLayoutGroup`:
|
||||
- **Control Child Size**: Width checked, Height unchecked
|
||||
- **Child Force Expand**: Width checked, Height unchecked
|
||||
- **Spacing**: 2 (or to taste)
|
||||
- **Padding**: as needed
|
||||
4. Do **not** add a `ContentSizeFitter` to Content. The VerticalLayoutGroup reports its preferred size to the parent Background's ContentSizeFitter
|
||||
|
||||
### Step 4: Element prefabs
|
||||
|
||||
Create these as separate prefabs. Do not add `ContentSizeFitter` to any element prefab:
|
||||
|
||||
| Prefab | Component | Notes |
|
||||
|---|---|---|
|
||||
| PopupHeader | `TMP_Text` | Bold, larger font size. Add `LayoutElement` if you need minimum height |
|
||||
| PopupText | `TMP_Text` | Regular body text, rich text enabled |
|
||||
| PopupStat | `RectTransform` with `HorizontalLayoutGroup` | Must have exactly two `TMP_Text` children: label (left) and value (right) |
|
||||
| PopupIcon | `Image` | For icons or artwork. Add `LayoutElement` with preferred height |
|
||||
| PopupSeparator | `Image` | Thin horizontal line. Add `LayoutElement` with preferred height = 1 or 2 |
|
||||
|
||||
### 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 PopupHeader prefab
|
||||
- **Text Prefab**: your PopupText prefab
|
||||
- **Stat Prefab**: your PopupStat prefab
|
||||
- **Image Prefab**: your PopupIcon prefab
|
||||
- **Separator Prefab**: your PopupSeparator prefab
|
||||
|
||||
Save as a prefab. The `maxPopupWidth` from `PopupSettings` is applied at runtime to constrain the popup's horizontal size. At runtime, when parented under a scene Canvas, the popup's own Canvas becomes a nested override Canvas inheriting the parent's scaling.
|
||||
1. **Root**: Canvas (Screen Space Overlay), CanvasScaler (Scale With Screen Size), CanvasGroup, PopupReference. Point anchors, no ContentSizeFitter.
|
||||
2. **Background**: Image, ContentSizeFitter (Vertical = Preferred).
|
||||
3. **Content**: Stretched to Background, VerticalLayoutGroup (Control Child Size Width, Child Force Expand Width), no ContentSizeFitter.
|
||||
4. **Element prefabs**: Create separate prefabs, register in PopupSettings under Element Prefabs with their `PopupElementType`. No ContentSizeFitter on element prefabs.
|
||||
5. **Wire PopupReference**: Assign Content, CanvasGroup, and Background fields.
|
||||
|
||||
## Optimization Notes
|
||||
|
||||
The popup system is designed for minimal runtime allocation:
|
||||
|
||||
- `PopupCategory` is a readonly struct with value semantics -- zero heap allocation on pass or comparison
|
||||
- `PopupContentBuilder` is 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. `GetStat` caches TMP_Text references in a struct to avoid `GetComponentsInChildren` on reuse
|
||||
- Delay timers and animation are driven by `float` fields in `Tick()` -- no coroutines, no `WaitForSeconds`
|
||||
- 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 `IPopupAnimator` instance, preventing state corruption during concurrent show/hide animations
|
||||
- `PopupCategory` and `PopupElementType` are value-type structs — zero heap allocation
|
||||
- `PopupContentBuilder` is a readonly struct operating directly on cached elements
|
||||
- Content elements use a **grow-only cache** keyed by `PopupElementType` — activate/deactivate, never destroy after warmup
|
||||
- Delay timers and animation driven by float fields in `Tick()` — no coroutines
|
||||
- Screen rect uses static `Vector3[4]` buffer — no per-frame allocation
|
||||
- Layout rebuilds only on content change (dirty flag)
|
||||
- Each category gets its own `IPopupAnimator` — no concurrent animation corruption
|
||||
- MonoBehaviour (`PopupReference`, `PopupTrigger`) holds references only; all behavior in plain C# classes (`PopupView`, `PopupTriggerView`)
|
||||
|
||||
## Samples
|
||||
|
||||
The `Samples~` folder contains ready-to-use reference material:
|
||||
The `Samples~` folder contains:
|
||||
|
||||
- **Prefabs**: PopupReferencePrefab and all element prefabs (header, text, stat, icon, separator) with correct component setup
|
||||
- **Settings**: Pre-configured PopupSettings asset with sensible defaults
|
||||
- **Scripts**: Three example scripts demonstrating different integration patterns:
|
||||
- `PopupSystemExample` -- auto-scanned triggers with `GetTrigger` and `GetTriggers`
|
||||
- `DynamicTriggersExample` -- runtime-instantiated UI with `InitializeTriggersInChildren`
|
||||
- `CodeOnlyPopupExample` -- showing popups from code (anchored, fixed position, follow mouse)
|
||||
- **Prefabs**: PopupReferencePrefab and all element prefabs
|
||||
- **Settings**: Pre-configured PopupSettings asset
|
||||
- **Scripts**: Three example scripts (auto-scanned triggers, dynamic triggers, code-only with variants)
|
||||
|
||||
Import via Unity Package Manager: select the package, expand Samples, click Import.
|
||||
|
||||
@@ -454,17 +314,20 @@ Import via Unity Package Manager: select the package, expand Samples, click Impo
|
||||
| Type | Namespace | Description |
|
||||
|---|---|---|
|
||||
| `IPopupSystem` | `Jovian.PopupSystem` | Main interface for showing/hiding popups. |
|
||||
| `PopupSystem` | `Jovian.PopupSystem` | Concrete implementation. Constructor: `(PopupSettings, PopupReference, Transform canvasParent, Func<IPopupAnimator>)`. Auto-scans triggers under canvasParent. Methods: `ScanTriggers`, `GetTrigger`, `GetTriggers`, `InitializeTriggersInChildren`. |
|
||||
| `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. |
|
||||
| `PopupSystem` | `Jovian.PopupSystem` | Implementation. Constructor: `(PopupSettings, PopupReference, Transform, Func<IPopupAnimator>)`. |
|
||||
| `PopupView` | `Jovian.PopupSystem` | Behavior class: generic element cache, positioning, visibility. |
|
||||
| `PopupSettings` | `Jovian.PopupSystem` | ScriptableObject configuration + element prefab registry. |
|
||||
| `PopupCategory` | `Jovian.PopupSystem` | Struct identifying a popup channel. |
|
||||
| `PopupElementType` | `Jovian.PopupSystem` | Struct identifying an element prefab type. Built-ins + Variant(). |
|
||||
| `PopupContentBuilder` | `Jovian.PopupSystem` | Fluent readonly struct for building content. |
|
||||
|
||||
### 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. |
|
||||
| `PopupReference` | `Jovian.PopupSystem.UI` | MonoBehaviour reference holder (content, canvasGroup, background). |
|
||||
| `PopupTrigger` | `Jovian.PopupSystem.UI` | MonoBehaviour reference holder for hover triggers. |
|
||||
| `PopupTriggerView` | `Jovian.PopupSystem` | Behavior handler for triggers (SetContent, pointer forwarding). |
|
||||
|
||||
### Animation Types
|
||||
|
||||
@@ -479,7 +342,3 @@ Import via Unity Package Manager: select the package, expand Samples, click Impo
|
||||
|---|---|
|
||||
| `AnchorSide` | `Below`, `Above`, `Left`, `Right` |
|
||||
| `PopupPositionMode` | `AnchorToElement`, `FollowMouse` |
|
||||
|
||||
## License
|
||||
|
||||
See the LICENSE file in the package root.
|
||||
|
||||
Reference in New Issue
Block a user