> For the complete documentation index, see [llms.txt](https://malbersanimations.gitbook.io/animal-controller/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://malbersanimations.gitbook.io/animal-controller/malbers-questforge/npc-speech-bubbles/npc-speech-bubble-component.md).

# NPC Speech Bubble Component

### Overview

NPC Speech Bubbles are world-space UI panels that float above characters and display short contextual text when the player is nearby. They provide ambient storytelling and gameplay signposting without requiring a full dialogue interaction — an NPC might say *"Have you seen the blacksmith?"* when you walk past, shift to *"Please, find my missing cart!"* once a relevant quest is active, and go silent after you've already spoken to them.

The system is built from two cooperating parts:

* **NPCSpeechBubble** — a per-NPC MonoBehaviour that owns the bubble content, proximity detection, and conditions for what text to show.
* **SpeechBubbleManager** — a scene singleton that coordinates all active bubbles, caps how many are visible at once, and handles object pooling so bubble UI instances are reused rather than instantiated and destroyed at runtime.

Text content is driven by **Text Variants** — a list of conditional text entries evaluated in priority order each time the bubble activates. Conditions can gate variants on quest state, dialogue flags, and objective progress, so the same NPC can say something different depending on where the player is in the story. If no variant condition matches, a configurable **Default Text** is shown instead.

Bubbles respond to **proximity**: each NPC defines its own activation and deactivation distances, with optional hysteresis to prevent flickering. Line-of-sight checking is also supported. The Manager enforces a global cap on how many bubbles can be visible simultaneously, prioritising the NPCs closest to the player when the limit is reached.

<figure><img src="/files/UCOUhbeTmJbhq3rZIc3s" alt=""><figcaption></figcaption></figure>

### Properties

| Property             | Type                 | Description                                                                                                                                                              |
| -------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Dialogue Trigger** | `DialogueTrigger`    | Optional link to a `DialogueTrigger` on the same NPC. Auto-detected via `GetComponent` at Start if not assigned. Used by the quest indicator and auto-hide logic.        |
| **Player Transform** | `TransformReference` | The player transform used for distance calculations. Falls back to `GameObject.FindGameObjectWithTag("Player")` at Start if left empty.                                  |
| **Bubble Anchor**    | `Transform`          | The world-space transform the bubble is parented to. If unset, the NPC's own transform is used. The **Bubble Offset** is applied in local space relative to this anchor. |
| **NPC Aligner**      | `Aligner`            | Optional Malbers `Aligner` component. When assigned, the NPC rotates to face the player the moment they enter activation range.                                          |

***

**Proximity Settings**

| Property                  | Type           | Default | Description                                                                                                                                                                                                                                                                          |
| ------------------------- | -------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Activation Distance**   | Float *(1–50)* | `4`     | Player must be within this distance (metres) for the bubble to become eligible to show.                                                                                                                                                                                              |
| **Deactivation Distance** | Float *(1–55)* | `5`     | Player must move beyond this distance for the bubble to become ineligible. Must be ≥ Activation Distance — auto-corrected with a warning if not. The gap between the two values provides **hysteresis**: the bubble won't flicker on and off as the player hovers right at the edge. |
| **Require Line of Sight** | Bool           | `false` | When enabled, a Physics raycast is cast from the NPC to the player each frame. If an obstacle on **Obstacle Layers** blocks it, the NPC is treated as out of range even if within distance.                                                                                          |
| **Obstacle Layers**       | LayerMask      | All     | The layers that count as obstructions for line-of-sight checks. Only relevant when **Require Line of Sight** is `true`.                                                                                                                                                              |

***

**Text Content**

| Property                | Type                        | Default          | Description                                                                                                                                                                                     |
| ----------------------- | --------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Text Variants**       | `SpeechBubbleTextVariant[]` | Empty            | Conditional text entries evaluated in priority order. See **Text Variants** section below.                                                                                                      |
| **Default Text**        | String                      | `"Hello there!"` | Shown when no Text Variant's condition is met, or when the variants list is empty. At Start, if no variants are configured, a single `Always` variant is automatically created from this value. |
| **Randomize Text**      | Bool                        | `false`          | When `true`, selects randomly from all *matching* variants rather than taking the highest-priority one.                                                                                         |
| **Cycle Text**          | Bool                        | `false`          | When `true`, re-evaluates and updates the displayed text every **Text Cycle Interval** seconds while the bubble is visible.                                                                     |
| **Text Cycle Interval** | Float                       | `5`              | Seconds between text cycles. Only used when **Cycle Text** is enabled.                                                                                                                          |

***

**Quest Integration**

| Property                     | Type             | Default | Description                                                                                                               |
| ---------------------------- | ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Linked Quest**             | `Quest`          | None    | The quest this NPC is associated with. Drives the quest state indicator icon and the hide-on-complete behaviour.          |
| **Linked Objective**         | `QuestObjective` | None    | A specific objective within the linked quest. Reserved for fine-grained per-objective indicator logic.                    |
| **Hide When Quest Complete** | Bool             | `true`  | When `true`, the bubble stops showing entirely once the linked quest reaches the completed state.                         |
| **Show Quest Indicator**     | Bool             | `true`  | When `true`, an icon is overlaid on the bubble reflecting the quest's current state. See **Quest State Indicator** below. |

***

**Visual Settings**

| Property                 | Type         | Default     | Description                                                                                                                                                                     |
| ------------------------ | ------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Custom Bubble Prefab** | `GameObject` | None        | Override the SpeechBubbleManager's default prefab for this specific NPC. The prefab must have a `SpeechBubbleUI` component. If left blank, the Manager's pooled prefab is used. |
| **Bubble Offset**        | `Vector3`    | `(0, 2, 0)` | Local-space offset from the anchor transform. Adjust Y to position the bubble above the NPC's head.                                                                             |
| **Billboard to Camera**  | Bool         | `true`      | Passes this flag to the `SpeechBubbleUI` component, which rotates the bubble to face the camera each frame.                                                                     |
| **Fade In Duration**     | Float        | `0.3`       | Duration in seconds for the bubble's appear fade.                                                                                                                               |
| **Fade Out Duration**    | Float        | `0.2`       | Duration in seconds for the bubble's disappear fade. The bubble instance is not returned to the pool until this duration has elapsed.                                           |
| **Use Typing Effect**    | Bool         | `false`     | When `true`, text is revealed character by character at **Typing Speed** rather than appearing all at once.                                                                     |
| **Typing Speed**         | Float        | `0.05`      | Seconds per character for the typing effect. Only used when **Use Typing Effect** is `true`.                                                                                    |

***

**Audio**

| Property                 | Type                 | Description                                                                                                                                                    |
| ------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Audio Source**         | `AudioSource`        | The AudioSource used for all bubble sounds. Assign for spatialised (3D) audio anchored to the NPC. If unassigned, no sounds will play.                         |
| **Default Bubble Sound** | `AudioClipReference` | Sound played when the bubble shows, unless the active Text Variant has its own `variantSound` assigned. Variant sounds always take priority over this default. |

***

**Interaction**

| Property                    | Type             | Default             | Description                                                                                                                                        |
| --------------------------- | ---------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Interaction Distance**    | Float *(0.5–10)* | `2`                 | When the player is within this distance, the **Interaction Prompt** is displayed on the bubble via `SpeechBubbleUI.ShowInteractionPrompt()`.       |
| **Interaction Prompt**      | String           | `"Press E to talk"` | The text shown in the interaction prompt area of the bubble UI when the player is close enough.                                                    |
| **Auto Hide on Dialogue**   | Bool             | `true`              | Hides the bubble for *all* NPCs whenever any dialogue begins. The bubble re-appears after dialogue ends, subject to the cooldown below.            |
| **Cooldown After Dialogue** | Float            | `2`                 | Seconds after a dialogue ends before this bubble is allowed to show again. Prevents bubbles from immediately popping back up after a conversation. |

***

**Quest State Styling**

Controls the colours and icons shown in the quest state indicator when **Show Quest Indicator** is enabled.

| Property                  | Type   | Default | Description                                                                                |
| ------------------------- | ------ | ------- | ------------------------------------------------------------------------------------------ |
| **Quest Available Color** | Color  | Yellow  | Tint applied to the indicator icon when the linked quest is available but not yet started. |
| **Quest Active Color**    | Color  | White   | Tint applied when the quest is in progress but not yet completable.                        |
| **Quest Complete Color**  | Color  | Cyan    | Tint applied when all objectives are done and the quest is ready to turn in.               |
| **Quest Available Icon**  | Sprite | None    | Icon sprite for the available state. Conventionally an exclamation mark (`!`).             |
| **Quest Active Icon**     | Sprite | None    | Icon sprite for the in-progress state. Conventionally a question mark (`?`).               |
| **Quest Complete Icon**   | Sprite | None    | Icon sprite for the ready-to-turn-in state.                                                |

***

**Events**

| Event                  | When It Fires                                                                                                           |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **OnBubbleShown**      | Immediately after `ShowBubble()` makes the bubble visible and fires the fade-in.                                        |
| **OnBubbleHidden**     | Immediately after `HideBubble()` is called, before the fade-out completes.                                              |
| **OnPlayerEnterRange** | The first frame the player's distance drops to or below **Activation Distance** (and passes line-of-sight if required). |
| **OnPlayerExitRange**  | The first frame the player's distance exceeds **Deactivation Distance**.                                                |

***

### Text Variants

**Text Variants** (`SpeechBubbleTextVariant`) are the core of the contextual text system. Each variant is an independent entry in the list that carries its own display text, a condition, and a priority score.

At Start, all variants are sorted by **Priority** (highest first) and this order is retained for the lifetime of the component.

When the bubble is about to show, `GetCurrentVariant()` runs `EvaluateConditions()` on each variant in priority order and collects all that pass. The winner is:

* The **first match** (highest priority) — normal mode.
* A **random pick** from all matches — when **Randomize Text** is enabled.

If nothing matches, **Default Text** is shown instead.

**Text Variant Fields**

| Field                      | Type                 | Default    | Description                                                                                                                                                   |
| -------------------------- | -------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Text**                   | String               | `"Hello!"` | The speech bubble text displayed when this variant is selected.                                                                                               |
| **Condition Type**         | Enum                 | `Always`   | Which condition must pass. See condition types below.                                                                                                         |
| **Required Quest**         | `Quest`              | None       | The quest referenced by quest-based condition types.                                                                                                          |
| **Quest State Condition**  | Enum                 | `Active`   | The required state. Used by some condition types for context (see below).                                                                                     |
| **Required Objective**     | `QuestObjective`     | None       | A specific objective that must be completed. Only evaluated by `QuestObjectiveComplete`.                                                                      |
| **Required Dialogue Flag** | String               | `""`       | The flag name to check in `DialogueManager`. Only evaluated by `DialogueFlag`.                                                                                |
| **Expected Flag Value**    | Bool                 | `true`     | Whether the flag must be `true` or `false`. Only evaluated by `DialogueFlag`.                                                                                 |
| **Variant Sound**          | `AudioClipReference` | None       | Per-variant audio override. When assigned, this plays instead of the component-level **Default Bubble Sound** whenever this variant is selected or cycled to. |
| **Priority**               | Int *(0–100)*        | `0`        | Higher numbers are evaluated first. When multiple variants match, the highest-priority one wins (unless **Randomize Text** is on).                            |

**Condition Types**

| Condition Type             | Passes When                                                                                           |
| -------------------------- | ----------------------------------------------------------------------------------------------------- |
| **Always**                 | Always passes. Use for default ambient chatter that shows when no other condition matches.            |
| **QuestAvailable**         | The **Required Quest** is available (not yet started, not completed).                                 |
| **QuestActive**            | The **Required Quest** is currently active.                                                           |
| **QuestCompleted**         | The **Required Quest** has been completed.                                                            |
| **QuestObjectiveComplete** | The **Required Quest** is active *and* the **Required Objective** has been completed within it.       |
| **DialogueFlag**           | The flag named **Required Dialogue Flag** in `DialogueManager` equals **Expected Flag Value**.        |
| **NoQuestActive**          | The **Required Quest** is *not* currently active (available or completed — anything but in-progress). |

> **Tip:** Stack multiple variants with increasing priority to create layered NPC awareness. A priority-0 `Always` variant provides ambient dialogue; a priority-10 `QuestActive` variant overrides it once the quest begins; a priority-20 `QuestObjectiveComplete` variant overrides that once a key task is done.

***

### Quest State Indicator

When **Show Quest Indicator** is enabled and a **Linked Quest** is assigned, the indicator logic evaluates quest state each time `ShowBubble()` is called:

| Quest State                                                     | Icon Shown               | Colour                    |
| --------------------------------------------------------------- | ------------------------ | ------------------------- |
| Quest **available** (not yet started)                           | **Quest Available Icon** | **Quest Available Color** |
| Quest **active** and objectives incomplete                      | **Quest Active Icon**    | **Quest Active Color**    |
| Quest **active** and all objectives complete (ready to turn in) | **Quest Complete Icon**  | **Quest Complete Color**  |
| Quest **completed**                                             | Icon hidden              | —                         |
| Quest not found / no quest assigned                             | Icon hidden              | —                         |

***

### Visibility Gate Order

The Manager calls `CanShowBubble` before deciding to show any bubble. All of the following must pass:

1. Not **Force Hidden** (via `ForceHide()`).
2. Player is **in range** — distance ≤ activation threshold and line-of-sight passes if required.
3. No dialogue is active, or **Auto Hide on Dialogue** is `false`.
4. **Cooldown after dialogue** has elapsed.
5. Linked quest is not completed, or **Hide When Quest Complete** is `false`.

If all five pass, the Manager then applies its own global cap (see **Speech Bubble Manager** below). A bubble that passes all local conditions but is pushed out by the cap is neither shown nor hidden — it waits for the next priority update cycle.

***

### Public API

| Method                               | Description                                                                                                                                                |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ShowBubble()`                       | Called by the Manager. Acquires a pooled instance, resolves the current text variant, applies visual settings, and triggers the fade-in.                   |
| `HideBubble(bool immediate = false)` | Called by the Manager or internally. Triggers fade-out. The pool release is delayed by `fadeOutDuration + 0.1s` unless `immediate` is `true`.              |
| `ForceHide(bool immediate = false)`  | Sets the `isForcedHidden` flag and hides the bubble. The bubble will not show again until `ReEnableAfterForceHide()` is called.                            |
| `ReEnableAfterForceHide()`           | Clears the forced-hide flag and requests a Manager visibility update.                                                                                      |
| `ManualShow()`                       | Clears the forced-hide flag and requests a Manager visibility update. Does not bypass proximity or condition checks — the Manager decides whether to show. |
| `ManualHide()`                       | Calls `HideBubble()` with the default fade.                                                                                                                |
| `RefreshText()`                      | If the bubble is currently visible, re-evaluates the variant conditions and updates the displayed text immediately.                                        |
| `UpdateDistanceToPlayer()`           | Recalculates `cachedDistanceToPlayer` and updates `isInRange`. Called every frame in `Update()` and can also be called externally.                         |
| `IsVisible()`                        | Returns `true` if the bubble is currently shown.                                                                                                           |

**Read-only properties:**

| Property                | Type      | Description                                                                         |
| ----------------------- | --------- | ----------------------------------------------------------------------------------- |
| `IsBubbleVisible`       | Bool      | Whether the bubble is currently in the shown state.                                 |
| `IsInRange`             | Bool      | Whether the player is currently within the activation/deactivation hysteresis zone. |
| `DistanceToPlayer`      | Float     | Last cached world-space distance to the player. Updated every frame.                |
| `CanShowBubble`         | Bool      | Evaluates all five local visibility gates. Does not account for Manager limits.     |
| `BubbleAnchorTransform` | Transform | The resolved anchor — `bubbleAnchor` if set, otherwise the NPC's own transform.     |

***

### Scene Gizmos

Gizmos are only drawn when **Debug Mode** is enabled on the component.

| Gizmo                       | Colour | What It Represents                           |
| --------------------------- | ------ | -------------------------------------------- |
| Wire sphere (always)        | Green  | Activation distance                          |
| Wire sphere (always)        | Yellow | Deactivation distance                        |
| Wire sphere (always)        | Blue   | Interaction distance                         |
| Wire cube (selected only)   | Cyan   | World position of the bubble anchor + offset |
| Scene label (selected only) | Cyan   | `"Speech Bubble\nN variants"`                |

### Usage Tips

* **Hysteresis distance** — always set **Deactivation Distance** 1–2 metres above **Activation Distance**. A player standing exactly at the activation boundary will constantly cross it if both values are equal, causing the bubble to flicker.
* **Layered variants** — use `Always` at priority 0 as a fallback, then override it with quest-aware variants at higher priorities. The NPC appears aware of the player's progress without any extra scripting.
* **Cycling ambient chatter** — enable **Cycle Text** with several `Always` variants at equal priority and **Randomize Text** enabled. The NPC will rotate through different lines of ambient speech while the player is nearby.
* **Post-dialogue cooldown** — set **Cooldown After Dialogue** to at least 2–3 seconds. This prevents the speech bubble from immediately reappearing right after the player closes a full dialogue conversation.
* **Per-NPC custom prefabs** — if a particular NPC needs a stylistically different bubble (e.g. a monster growling instead of talking), assign a **Custom Bubble Prefab** directly on that NPC's component. All other NPCs continue using the Manager's pool.
* **Pool size** — set **Initial Pool Size** to match or exceed **Max Visible Bubbles**. If the pool runs dry at runtime, a new instance is instantiated on the fly, which causes a brief hitch.
* **Global hide for cutscenes** — call `SpeechBubbleManager.Instance.HideAllBubbles()` at the start of a cutscene or menu, and `ShowAllBubbles()` when returning to gameplay.
* **Debug overlay** — enable **Debug Mode** on the Manager during development to see a live stats overlay in Play Mode showing exactly how many bubbles are registered, eligible, and visible, alongside pool usage.
