Made the popup system a lot more generic

This commit is contained in:
Sebastian Bularca
2026-04-06 20:38:58 +02:00
parent fa7659d905
commit cfe202da44
21 changed files with 840 additions and 682 deletions

View File

@@ -39,7 +39,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 300, y: 500}
m_SizeDelta: {x: 300, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!225 &1835601435911948781
CanvasGroup:
@@ -68,11 +68,6 @@ MonoBehaviour:
content: {fileID: 176628901263125209}
canvasGroup: {fileID: 1835601435911948781}
background: {fileID: 8899521584296352500}
headerPrefab: {fileID: 6612787789151041457, guid: dfc1bc0bd5b4905409615c3e770a5b77, type: 3}
textPrefab: {fileID: 2506259255305457008, guid: bfa97c92d1878cc448ddc7dc456f4b17, type: 3}
statPrefab: {fileID: 1843470073663794312, guid: 5882db210c62d8647858933649f64c29, type: 3}
imagePrefab: {fileID: 7093821785826926595, guid: 5e715f4b614d02b4fa0b4d3fcfe3c053, type: 3}
separatorPrefab: {fileID: 4190588985333916705, guid: 7ccdfa1a2079db044be4b1684303ec7f, type: 3}
--- !u!223 &3081303906751693297
Canvas:
m_ObjectHideFlags: 0
@@ -219,14 +214,14 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 0.31132078, g: 0.22780241, b: 0.16006587, a: 1}
m_Color: {r: 0.1509434, g: 0.116711326, b: 0.088999644, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Sprite: {fileID: 21300000, guid: 52125a3c3df558448a5af5a04dbf8d2d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -4,16 +4,18 @@
### Prefabs
Reference prefabs for the popup system. Copy these into your project as a starting point.
Reference prefabs for the popup system. Copy these into your project as a starting point. After copying, add entries to your `PopupSettings` asset's Element Prefabs list mapping `PopupElementType` values to these prefabs.
| Prefab | Description |
|---|---|
| `PopupReferencePrefab` | Main popup container with Canvas, CanvasGroup, Background, and Content |
| `PopupHeader` | Header text element (TMP_Text, bold, larger font) |
| `PopupText` | Body text element (TMP_Text, regular) |
| `PopupStat` | Stat row element (HorizontalLayoutGroup with label + value TMP_Text) |
| `PopupIcon` | Image element for icons or artwork |
| `PopupSeparator` | Horizontal divider line (Image, thin) |
| Prefab | PopupElementType | Description |
|---|---|---|
| `PopupReferencePrefab` | N/A | Main popup container with Canvas, CanvasGroup, Background, and Content |
| `PopupHeader` | `Header` ("header") | Header text element (TMP_Text, bold, larger font) |
| `PopupText` | `Text` ("text") | Body text element (TMP_Text, regular) |
| `PopupStat` | `LabelValueText` ("label_value_text") | Label + value row (HorizontalLayoutGroup with two TMP_Text children) |
| `PopupIcon` | `Image` ("image") | Image element for icons or artwork |
| `PopupSeparator` | `Separator` ("separator") | Horizontal divider line (Image, thin) |
To add variants, duplicate a prefab, style it differently, and register it with a variant type. Use `PopupElementType.Header.Variant("gold")` in code, and set the type to "header_gold" in the PopupSettings Inspector.
### Settings
@@ -25,13 +27,32 @@ Reference prefabs for the popup system. Copy these into your project as a starti
| Script | Description |
|---|---|
| `PopupSystemExample` | Basic setup with auto-scanned triggers, GetTrigger by name, and GetTriggers by category |
| `DynamicTriggersExample` | Setting up triggers on dynamically instantiated UI using InitializeTriggersInChildren |
| `CodeOnlyPopupExample` | Showing popups from code without PopupTrigger (anchored, fixed position, follow mouse) |
| `PopupSystemExample` | Basic setup with auto-scanned triggers, `GetTriggerHandler` by name, and `GetTriggerHandlers` by category |
| `DynamicTriggersExample` | Setting up triggers on dynamically instantiated UI using `InitializeTriggersInChildren` with (trigger, view) callback |
| `CodeOnlyPopupExample` | Showing popups from code (anchored, fixed position, follow mouse), `PopupElementType` variants, and generic `Add()` for custom types |
### Architecture Overview
```
PopupTrigger (MonoBehaviour) -- reference holder, forwards pointer events
|
PopupTriggerView (C# class) -- behavior: calls IPopupSystem.Show/Hide
|
IPopupSystem / PopupSystem -- manages categories, delays, priority, triggers
|
PopupView (C# class) -- behavior: generic element cache, positioning
|
PopupReference (MonoBehaviour) -- reference holder: content, canvasGroup, background
|
PopupSettings (ScriptableObject) -- configuration + element prefab registry (PopupElementType -> prefab)
|
PopupElementType (struct) -- type-safe element identifier (Header, Text, LabelValueText, Image, Separator + custom)
```
## How to use
1. Import the samples via the Unity Package Manager (select the package, expand Samples, click Import)
2. Copy the prefabs into your project's Prefabs folder
3. Create a PopupSettings asset or use the provided one
4. Reference the example scripts for integration patterns
4. Add Element Prefab entries to PopupSettings using the dropdown (Header, Text, LabelValueText, Image, Separator) mapped to the copied prefabs
5. Reference the example scripts for integration patterns

View File

@@ -7,6 +7,7 @@ using UnityEngine;
///
/// Use this approach for confirmation dialogs, tutorial tips, or any popup
/// that is triggered by game logic rather than hover events.
/// Also demonstrates PopupElementType variants and the generic Add() method.
/// </summary>
public class CodeOnlyPopupExample : MonoBehaviour {
[SerializeField] PopupSettings popupSettings;
@@ -14,6 +15,9 @@ public class CodeOnlyPopupExample : MonoBehaviour {
[SerializeField] Transform canvasRoot;
[SerializeField] RectTransform targetElement;
// Custom element type defined in game code
static readonly PopupElementType BadgeElement = new("badge");
IPopupSystem popupSystem;
void Start() {
@@ -24,12 +28,12 @@ public class CodeOnlyPopupExample : MonoBehaviour {
void Update() {
popupSystem?.Tick(Time.deltaTime);
// Show anchored to an element on key press
// Show anchored to an element
if(Input.GetKeyDown(KeyCode.Alpha1)) {
popupSystem.Show(PopupCategory.General, builder => {
builder
.AddHeader("Anchored Popup")
.AddText("This popup is anchored to a UI element.");
.AddText("Anchored Popup", PopupElementType.Header)
.AddText("This popup is anchored to a UI element.", PopupElementType.Text);
}, targetElement, AnchorSide.Right);
}
@@ -37,8 +41,8 @@ public class CodeOnlyPopupExample : MonoBehaviour {
if(Input.GetKeyDown(KeyCode.Alpha2)) {
popupSystem.ShowAtPosition(PopupCategory.General, builder => {
builder
.AddHeader("Fixed Position")
.AddText("This popup appears at the center of the screen.");
.AddText("Fixed Position", PopupElementType.Header)
.AddText("This popup appears at the center of the screen.", PopupElementType.Text);
}, new Vector2(Screen.width * 0.5f, Screen.height * 0.5f));
}
@@ -46,12 +50,36 @@ public class CodeOnlyPopupExample : MonoBehaviour {
if(Input.GetKeyDown(KeyCode.Alpha3)) {
popupSystem.Show(PopupCategory.General, builder => {
builder
.AddHeader("Follow Mouse")
.AddText("This popup follows the cursor.");
.AddText("Follow Mouse", PopupElementType.Header)
.AddText("This popup follows the cursor.", PopupElementType.Text);
});
}
// Hide on key press
// Demonstrate variant elements
// Requires "header_gold" and "separator_thick" entries in PopupSettings.elementPrefabs
if(Input.GetKeyDown(KeyCode.Alpha4)) {
popupSystem.Show(PopupCategory.General, builder => {
builder
.AddText("Legendary Item", PopupElementType.Header.Variant("gold"))
.AddSeparator(PopupElementType.Separator.Variant("thick"))
.AddNameValue("Damage", "150", PopupElementType.LabelValueText)
.AddText("A weapon forged in dragon fire.", "FF6600", PopupElementType.Text);
}, targetElement, AnchorSide.Below);
}
// Demonstrate generic Add() for fully custom elements
// Requires a "badge" entry in PopupSettings.elementPrefabs
if(Input.GetKeyDown(KeyCode.Alpha5)) {
popupSystem.Show(PopupCategory.General, builder => {
builder.AddText("Custom Element", PopupElementType.Header);
var badge = builder.Add(BadgeElement);
if(badge != null) {
// Access any components on the custom prefab
// badge.GetComponent<MyBadgeComponent>().SetData(...);
}
}, targetElement);
}
if(Input.GetKeyDown(KeyCode.Escape)) {
popupSystem.HideAll();
}

View File

@@ -6,7 +6,9 @@ using UnityEngine;
/// Example: Using PopupSystem with dynamically instantiated UI elements.
///
/// When UI elements are created at runtime (e.g. inventory slots, party member portraits),
/// use InitializeTriggersInChildren to scan and bind triggers after instantiation.
/// use InitializeTriggersInChildren to scan, bind, and configure triggers after instantiation.
/// The callback receives both the trigger (MonoBehaviour, for hierarchy queries) and the
/// view (behavior handler, for setting content).
/// </summary>
public class DynamicTriggersExample : MonoBehaviour {
[SerializeField] PopupSettings popupSettings;
@@ -21,21 +23,17 @@ public class DynamicTriggersExample : MonoBehaviour {
popupSystem = new PopupSystem(popupSettings, popupReferencePrefab, canvasRoot);
popupSystem.RegisterCategory(PopupCategory.Item, priority: 5);
// Simulate creating dynamic UI slots
for(int i = 0; i < 5; i++) {
Instantiate(slotPrefab, slotsContainer);
}
// Scan the container for any PopupTrigger components on the new slots.
// Each trigger is automatically bound to the popup system.
// The configure callback lets you set content per trigger.
popupSystem.InitializeTriggersInChildren(slotsContainer, trigger => {
popupSystem.InitializeTriggersInChildren(slotsContainer, (trigger, view) => {
var slotName = trigger.gameObject.name;
trigger.SetContent(builder => {
view.SetContent(builder => {
builder
.AddHeader(slotName)
.AddSeparator()
.AddText("This is a dynamically created slot.");
.AddText(slotName, PopupElementType.Header)
.AddSeparator(PopupElementType.Separator)
.AddText("This is a dynamically created slot.", PopupElementType.Text);
});
});
}

View File

@@ -9,7 +9,10 @@ using UnityEngine;
/// 2. Assign the PopupSettings asset and PopupReference prefab.
/// 3. Set canvasRoot to the root Canvas that contains your UI and PopupTrigger components.
/// 4. Place PopupTrigger components on any UI elements that should show popups on hover.
/// 5. The system auto-scans triggers on creation. Use SetContent() to provide data.
/// 5. The system auto-scans triggers on creation. Use GetTriggerHandler() to set content.
///
/// Element prefabs are configured in PopupSettings via PopupElementType.
/// Add entries mapping types (header, text, label_value_text, separator, image) to prefabs.
/// </summary>
public class PopupSystemExample : MonoBehaviour {
[SerializeField] PopupSettings popupSettings;
@@ -19,52 +22,46 @@ public class PopupSystemExample : MonoBehaviour {
IPopupSystem popupSystem;
void Start() {
// Create the system. Passing canvasRoot auto-scans all PopupTrigger components.
popupSystem = new PopupSystem(popupSettings, popupReferencePrefab, canvasRoot);
// Register categories before showing popups.
popupSystem.RegisterCategory(PopupCategory.Character, priority: 10);
popupSystem.RegisterCategory(PopupCategory.Item, priority: 5);
popupSystem.RegisterCategory(PopupCategory.General, priority: 1);
// Option A: Set content on an auto-scanned trigger by GameObject name.
var characterTrigger = popupSystem.GetTrigger("CharacterPortrait");
if(characterTrigger != null) {
characterTrigger.SetContent(builder => {
builder
.AddHeader("Kael")
.AddText("Human Warrior", "CCCCCC")
.AddSeparator()
.AddStat("Health", 55)
.AddStat("Mana", 42)
.AddStat("Level", 1)
.AddSeparator()
.AddStat("Might", 8)
.AddStat("Reflex", 2)
.AddStat("Knowledge", 5)
.AddStat("Perception", 1);
});
}
// Set content on an auto-scanned trigger by GameObject name.
var characterHandler = popupSystem.GetTriggerHandler("CharacterPortrait");
characterHandler?.SetContent(builder => {
builder
.AddText("Kael", PopupElementType.Header)
.AddText("Human Warrior", "CCCCCC", PopupElementType.Text)
.AddSeparator(PopupElementType.Separator)
.AddNameValue("Health", 55, PopupElementType.LabelValueText)
.AddNameValue("Mana", 42, PopupElementType.LabelValueText)
.AddNameValue("Level", 1, PopupElementType.LabelValueText)
.AddSeparator(PopupElementType.Separator)
.AddNameValue("Might", 8, PopupElementType.LabelValueText)
.AddNameValue("Reflex", 2, PopupElementType.LabelValueText)
.AddNameValue("Knowledge", 5, PopupElementType.LabelValueText)
.AddNameValue("Perception", 1, PopupElementType.LabelValueText);
});
// Option B: Set content on all triggers of a category.
foreach(var trigger in popupSystem.GetTriggers(PopupCategory.Item)) {
trigger.SetContent(builder => {
// Set content on all triggers of a category.
foreach(var handler in popupSystem.GetTriggerHandlers(PopupCategory.Item)) {
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 a moderate amount of health.", PopupElementType.Text)
.AddNameValue("Heal Amount", 50, PopupElementType.LabelValueText);
});
}
}
void Update() {
// Tick drives delay timers and animations.
popupSystem?.Tick(Time.deltaTime);
}
void OnDestroy() {
// Clean up all popup views.
popupSystem?.Dispose();
}
}

View File

@@ -21,6 +21,22 @@ MonoBehaviour:
followMouseOffset: {x: 15, y: -15}
touchHoldDuration: 0.6
gamepadFocusTrigger: 1
elementPrefabs:
- type:
id: header
prefab: {fileID: 7034836061828108288, guid: dfc1bc0bd5b4905409615c3e770a5b77, type: 3}
- type:
id: image
prefab: {fileID: 5887814251614319338, guid: 5e715f4b614d02b4fa0b4d3fcfe3c053, type: 3}
- type:
id: label_value_text
prefab: {fileID: 6770634903822758885, guid: 7ccdfa1a2079db044be4b1684303ec7f, type: 3}
- type:
id: separator
prefab: {fileID: 6770634903822758885, guid: 7ccdfa1a2079db044be4b1684303ec7f, type: 3}
- type:
id: text
prefab: {fileID: 3157287847714375358, guid: bfa97c92d1878cc448ddc7dc456f4b17, type: 3}
categoryPriorities:
- category:
id: Character