From 0b1128d20909798a4c29359f52d2fce1321a672b Mon Sep 17 00:00:00 2001 From: Lefteris Notas Date: Wed, 20 May 2026 22:52:32 +0300 Subject: [PATCH] Refactor code structure for improved readability and maintainability --- .../01-core/02a_ML_GameUtilities.md | 1482 +++++++++++++++++ docs/blueprints/INDEX.md | 5 +- 2 files changed, 1485 insertions(+), 2 deletions(-) create mode 100644 docs/blueprints/01-core/02a_ML_GameUtilities.md diff --git a/docs/blueprints/01-core/02a_ML_GameUtilities.md b/docs/blueprints/01-core/02a_ML_GameUtilities.md new file mode 100644 index 0000000..2d1858e --- /dev/null +++ b/docs/blueprints/01-core/02a_ML_GameUtilities.md @@ -0,0 +1,1482 @@ +# ML_GameUtilities — Blueprint Macro Library (100% BP, No C++) + +> **This file is the pure-Blueprint equivalent of `FL_GameUtilities`.** +> Every function from the C++ Function Library is re-implemented as a Blueprint Macro Library macro using only engine-provided Blueprint nodes. No C++ compilation required. +> +> **C++ counterpart:** [`02_FL_GameUtilities.md`](02_FL_GameUtilities.md) | [`FL_GameUtilities.h`](../../Source/Framework/Public/Core/FL_GameUtilities.h) + +## Asset Details + +| Property | Value | +|----------|-------| +| **Asset Type** | `Macro Library` | +| **Name** | `ML_GameUtilities` | +| **Path** | `Content/Framework/Core/ML_GameUtilities` | +| **Categorization** | Framework\|Utilities | + +## Purpose + +A collection of reusable Blueprint Macro nodes providing common utility functions for the entire project. All macros use only built-in Unreal Engine Blueprint nodes — **zero C++ required**. Any Blueprint, component, widget, or AI asset can call these macros by searching the context menu. + +## Macro Library vs Function Library + +| Aspect | FL_GameUtilities (C++) | ML_GameUtilities (Pure BP) | +|--------|------------------------|----------------------------| +| **Requires C++?** | Yes — BlueprintFunctionLibrary must be authored in C++ | No — Macro Library is a native UE5 asset type | +| **Can be pure (no exec pin)?** | Yes — `BlueprintPure` functions return inline | Macros can be pure, but complex logic needs exec pins | +| **Multi-output pins?** | Only with `UFUNCTION` output params | Yes — any number of output pins | +| **World context support?** | Via `meta = (WorldContext = "...")` | Via `WorldContext` pin on the macro | +| **Called from Construction Script?** | Pure functions: yes | Pure macros: yes. Impure macros: no (construction scripts reject exec pins) | +| **Where to find in context menu?** | Under `Framework\|Utilities\|...` | Under `Framework\|Utilities\|...` (same categories) | + +## Macro Categories + +| Category | Macros | Pure / Impure | +|----------|--------|---------------| +| **Subsystem Access** | `GetSubsystemSafe`, `GetGameFramework`, `GetPlayerController`, `GetSubsystemByClass` | Impure (need exec pins for null-check branching) | +| **Actor Utilities** | `FindComponentByInterface`, `FindNearestActorWithTag` | Impure | +| **Math** | `RemapFloat`, `LerpClamped`, `VectorToAngle2D`, `AngleDifference` | Pure (no exec pins) | +| **Gameplay Tags** | `HasGameplayTag`, `AddGameplayTagToActor`, `RemoveGameplayTagFromActor`, `MakeTagFromString`, `TagContainerHasAny` | Mixed (queries pure, mutations impure) | +| **Text Formatting** | `FormatTime`, `Pluralise`, `OrdinalSuffix`, `TruncateText` | Pure | +| **Screen / Viewport** | `WorldToScreenSafe`, `ClampToViewport` | Impure | +| **Debug** | `LogDebug`, `DrawDebugSphere`, `DrawDebugString3D` | Impure | + +--- + +## 1. Enums + +No enums are defined in this library. + +--- + +## 2. Structs + +No structs are defined in this library. All inputs/outputs use native Blueprint types. + +--- + +## 3. Variables + +This is a Macro Library. It has **no variables, no event dispatchers, no tick**. All macros are self-contained — they only use their input pins and output their results. + +--- + +## 4. Macros (Function Definitions) + +--- + +### 4.1 Subsystem Access Macros + +#### Macro: `GetSubsystemSafe` +- **Category:** `Framework|Utilities|Subsystems` +- **Description:** Null-safe subsystem retrieval. Returns `None` instead of crashing if the GameInstance or subsystem doesn't exist. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input (impure) | + | `WorldContextObject` | Object | World context for finding the GameInstance | + | `SubsystemClass` | Class (GameInstanceSubsystem) | The subsystem class to retrieve | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Subsystem` | GameInstanceSubsystem | The found subsystem, or None if not found | +- **Node-by-Node Logic:** + ``` + [Macro Body: GetSubsystemSafe] + Inputs: WorldContextObject (Object), SubsystemClass (Class) + + Step 1: Call "Get Game Instance" (WorldContextObject) → GameInstance (GameInstance) + Step 2: Branch on "Is Valid" (GameInstance) + True → + Step 2a: Call "Get Subsystem (Class)" on GameInstance, passing SubsystemClass → Subsystem (GameInstanceSubsystem) + Step 2b: Return Subsystem + False → + Step 2c: Return None + ``` +- **Nodes to Search For:** + - `Get Game Instance` + - `Is Valid` (Object) + - `Branch` + - `Get Subsystem (Class)` +- **Macro Graph Setup:** + 1. Add `WorldContextObject` input pin (type: `Object`). + 2. Add `SubsystemClass` input pin (type: `Class Reference` → `GameInstanceSubsystem`). + 3. Add `Subsystem` output pin (type: `GameInstanceSubsystem`). + 4. Wire exec from input → `Get Game Instance` → `Branch` on validity → true branch calls `Get Subsystem (Class)` and returns. + +--- + +#### Macro: `GetGameFramework` +- **Category:** `Framework|Utilities|Subsystems` +- **Description:** Typed retrieval of `GI_GameFramework` Game Instance. Returns `None` if cast fails or GameInstance doesn't exist. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `WorldContextObject` | Object | World context | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `GameFramework` | GI_GameFramework | The Game Framework instance, or None | +- **Node-by-Node Logic:** + ``` + [Macro Body: GetGameFramework] + Step 1: Call "Get Game Instance" (WorldContextObject) → GameInstance (GameInstance) + Step 2: Branch on "Is Valid" (GameInstance) + True → + Step 2a: "Cast to GI_GameFramework" (GameInstance) → CastResult (GI_GameFramework) + Step 2b: Branch on "Is Valid" (CastResult) + True → Return CastResult + False → Print String Warning: "ML_GameUtilities: GetGameFramework failed — wrong GameInstance type" → Return None + False → + Step 2c: Print String Warning: "ML_GameUtilities: No GameInstance found" → Return None + ``` +- **Nodes to Search For:** + - `Get Game Instance` + - `Is Valid` + - `Branch` + - `Cast to GI_GameFramework` + - `Print String` +- **Note:** Replace `GI_GameFramework` with your actual Game Instance class reference. If your Game Instance has a different name, cast to that instead. + +--- + +#### Macro: `GetPlayerController` +- **Category:** `Framework|Utilities|Subsystems` +- **Description:** Typed retrieval of `PC_CoreController` for player index 0. Returns `None` if cast fails or controller doesn't exist. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `WorldContextObject` | Object | World context | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `PlayerController` | PC_CoreController | The player controller, or None | +- **Node-by-Node Logic:** + ``` + [Macro Body: GetPlayerController] + Step 1: Call "Get Player Controller" (Player Index 0, WorldContextObject) → PC (PlayerController) + Step 2: Branch on "Is Valid" (PC) + True → + Step 2a: "Cast to PC_CoreController" (PC) → CoreController (PC_CoreController) + Step 2b: Return CoreController + False → + Step 2c: Return None + ``` +- **Nodes to Search For:** + - `Get Player Controller` + - `Is Valid` + - `Branch` + - `Cast to PC_CoreController` + +--- + +#### Macro: `GetSubsystemByClass` +- **Category:** `Framework|Utilities|Subsystems` +- **Description:** Generic subsystem retrieval by class. Returns the subsystem cast to the specified class. Returns `None` if not found or cast fails. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `WorldContextObject` | Object | World context | + | `SubsystemClass` | Class (GameInstanceSubsystem) | The subsystem class to retrieve | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Subsystem` | GameInstanceSubsystem | The found subsystem, or None | +- **Node-by-Node Logic:** + ``` + [Macro Body: GetSubsystemByClass] + Step 1: Call "GetSubsystemSafe" macro (WorldContextObject, SubsystemClass) → Subsystem + Step 2: Return Subsystem + ``` +- **Nodes to Search For:** + - `GetSubsystemSafe` (internal macro reuse — drag in the macro from this same library) +- **Note:** This is a thin wrapper that delegates to `GetSubsystemSafe`. Since Blueprint macros can call other macros, you can simply insert the `GetSubsystemSafe` macro directly inside this one. This avoids duplicating the null-safety logic. + +--- + +### 4.2 Actor Utility Macros + +#### Macro: `FindComponentByInterface` +- **Category:** `Framework|Utilities|Actor` +- **Description:** Finds the first component on an actor that implements a specified interface. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `TargetActor` | Actor | Actor to search | + | `InterfaceClass` | Interface | The interface to check for | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `FoundComponent` | ActorComponent | The first matching component, or None | + | `bFound` | Boolean | True if a component was found | +- **Node-by-Node Logic:** + ``` + [Macro Body: FindComponentByInterface] + Step 1: Branch on "Is Valid" (TargetActor) + False → Return None, bFound=False + + Step 2: Call "Get Components by Class" (TargetActor, class: ActorComponent) → Components (Array) + + Step 3: For Each Loop (Components) + Loop Body: + Step 3a: Call "Does Implement Interface" (Array Element, InterfaceClass) → bImplements (Bool) + Step 3b: Branch on bImplements + True → Set FoundComponent = Array Element, Set bFound = True, break the loop (use "For Loop with Break" instead) + False → Continue + + Step 4: Return FoundComponent, bFound + ``` +- **Nodes to Search For:** + - `Is Valid` + - `Branch` + - `Get Components by Class` + - `For Loop with Break` (right-click → "For Loop with Break") + - `Does Implement Interface` +- **Things to Watch:** + - Use `For Loop with Break` instead of `For Each Loop` — you need to stop when you find the first match. + - Cast the array element to `ActorComponent` before passing to `Does Implement Interface` — UE5 may need the explicit cast. + - `Get Components by Class` returns components from Blueprint-defined components AND runtime-added components. + +--- + +#### Macro: `FindNearestActorWithTag` +- **Category:** `Framework|Utilities|Actor` +- **Description:** Finds the nearest actor within a radius that has a specific gameplay tag. Returns `None` if no matching actor found. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `Origin` | Vector | World position to search from | + | `Radius` | Float | Search radius in world units | + | `RequiredTag` | GameplayTag (Struct) | The tag an actor must have | + | `ActorsToIgnore` | Array of Actor | Actors to exclude from search | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `NearestActor` | Actor | The closest matching actor, or None | + | `Distance` | Float | Distance to the nearest actor, or -1 if none found | +- **Node-by-Node Logic:** + ``` + [Macro Body: FindNearestActorWithTag] + Step 1: Set local vars: NearestActor = None, NearestDist = -1 (or Max Float) + + Step 2: Call "Get All Actors with Tag" (RequiredTag.TagName as Name) → TaggedActors (Array) + ⚠️ "Get All Actors with Tag" takes an FName, not an FGameplayTag. + Workaround: Use "Break GameplayTag" to get TagName, or use "Get All Actors of Class with Tag" if available. + Alternative (more robust): Use "Get All Actors of Class" (Actor) → For Loop → check "Does Actor Have Tag" on each. + + Step 3: For Each Loop (TaggedActors) + Array Element → Branch on "Is Valid" + True → + Step 3a: Check if Element is in "ActorsToIgnore" (use "Array Contains Item") → Branch + True (skip) → Continue + False → + Step 3b: Call "Get Distance To" (Element, Origin) → Dist (Float) + Step 3c: Branch: Dist <= Radius + True → Branch: Dist < NearestDist OR NearestDist == -1 + True → Set NearestActor = Element, NearestDist = Dist + False → Continue + False → Continue + + Step 4: Branch on "Is Valid" (NearestActor) + True → Return NearestActor, Distance = NearestDist + False → Return None, Distance = -1 + ``` +- **Nodes to Search For:** + - `Get All Actors of Class with Tag` (if your engine version supports it with GameplayTags) + - **Better alternative:** `Get All Actors of Class` (Actor) + `Does Actor Have Tag` (for each) + - Or: `Sphere Overlap Actors` (more performant for radius-based search) + - `For Each Loop` + - `Get Distance To` (Vector) + - `Array Contains Item` + - `Branch` + - `Break GameplayTag` (to get TagName) +- **Performance Note:** For large levels, `Get All Actors of Class` is expensive. Consider using `Sphere Overlap Actors` with the Origin and Radius as the sphere — it's faster and respects collision channels. + +**Optimized Alternative Flow (using Sphere Overlap):** + ``` + Step 1: Create a temporary "Sphere Collision" trace → use "Sphere Overlap Actors" (Origin, Radius) → OverlappedActors + Step 2: For each overlapped actor → call "Does Actor Have Tag" (RequiredTag) → filter + Step 3: For each tagged actor → "Get Distance To" → track nearest + ``` + +--- + +### 4.3 Math Macros (All Pure) + +#### Macro: `RemapFloat` +- **Category:** `Framework|Utilities|Math` +- **Description:** Remaps a value from [InMin, InMax] to [OutMin, OutMax]. Clamped to output range. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Value` | Float | Value to remap | + | `InMin` | Float | Input range minimum | + | `InMax` | Float | Input range maximum | + | `OutMin` | Float | Output range minimum | + | `OutMax` | Float | Output range maximum | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Result` | Float | Remapped value | +- **Node-by-Node Logic:** + ``` + [Macro Body: RemapFloat] (PURE — check "Pure" in macro settings) + Step 1: Call "Map Range Clamped" (Value, InRangeA=InMin, InRangeB=InMax, OutRangeA=OutMin, OutRangeB=OutMax) → Result + Step 2: Return Result + ``` +- **Nodes to Search For:** + - `Map Range Clamped` (built-in math node — "Map Range" then set Clamped to true) +- **One-Liner:** This macro is a direct wrapper around `Map Range Clamped`. You can even skip this macro and use `Map Range Clamped` directly. + +--- + +#### Macro: `LerpClamped` +- **Category:** `Framework|Utilities|Math` +- **Description:** Linear interpolation with alpha clamped to [0, 1]. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `A` | Float | Start value | + | `B` | Float | End value | + | `Alpha` | Float | Interpolation factor (clamped 0-1) | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Result` | Float | Interpolated value | +- **Node-by-Node Logic:** + ``` + [Macro Body: LerpClamped] (PURE) + Step 1: Call "Clamp (Float)" (Alpha, Min=0.0, Max=1.0) → ClampedAlpha (Float) + Step 2: Call "Lerp (Float)" (A, B, ClampedAlpha) → Result + Step 3: Return Result + ``` +- **Nodes to Search For:** + - `Clamp (Float)` + - `Lerp (Float)` + +--- + +#### Macro: `VectorToAngle2D` +- **Category:** `Framework|Utilities|Math` +- **Description:** Converts a 2D direction vector (X, Y) to an angle in degrees (0-360), where 0° = +X, 90° = +Y. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Direction` | Vector2D | 2D direction vector | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Angle` | Float | Angle in degrees (0-360) | +- **Node-by-Node Logic:** + ``` + [Macro Body: VectorToAngle2D] (PURE) + Step 1: Call "Deg Atan2" (Y=Direction.Y, X=Direction.X) → RawAngle (Float, degrees, range -180 to 180) + Step 2: Call "Select Float" (Condition = RawAngle < 0, True=RawAngle + 360, False=RawAngle) → NormalizedAngle + Step 3: Return NormalizedAngle + ``` +- **Nodes to Search For:** + - `Deg Atan2` (or `Atan2 (degrees)`) + - `Select Float` + - `Float + Float` +- **If `DegAtan2` doesn't exist:** Use `Atan2 (Radians)` → `Radians to Degrees` → then normalize. + +--- + +#### Macro: `AngleDifference` +- **Category:** `Framework|Utilities|Math` +- **Description:** Returns the shortest signed angle difference between two angles in degrees. Result is in range [-180, 180]. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `AngleA` | Float | First angle in degrees | + | `AngleB` | Float | Second angle in degrees | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Difference` | Float | Signed difference in degrees [-180, 180] | +- **Node-by-Node Logic:** + ``` + [Macro Body: AngleDifference] (PURE) + Step 1: Float subtraction: Diff = AngleB - AngleA + + Step 2: Call "FMod" (Diff, 360.0) → WrappedDiff (Float) + (or use: Diff - Floor(Diff / 360) * 360) + + Step 3: Branch: WrappedDiff > 180 + True → Return WrappedDiff - 360 + False → + Branch: WrappedDiff < -180 + True → Return WrappedDiff + 360 + False → Return WrappedDiff + ``` +- **Nodes to Search For:** + - `Float - Float` + - `FMod` (Float modulo) — or use `%` modulo node (search "modulo" or "%") + - `Select Float` (for the conditional logic) +- **Simpler Alternative:** + If your engine has `Delta (Rotator)` or similar: Convert both angles to rotators → `Delta (Rotator)` → extract Yaw → return. + However, the FMod approach shown above is the most reliable. + +--- + +### 4.4 Gameplay Tag Macros + +#### Macro: `HasGameplayTag` +- **Category:** `Framework|Utilities|Tags` +- **Description:** Safe check if an actor's gameplay tag container includes the specified tag. Returns `False` if actor is invalid. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `TargetActor` | Actor | The actor to check | + | `Tag` | GameplayTag (Struct) | The tag to look for | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `bHasTag` | Boolean | True if the actor has the tag | +- **Node-by-Node Logic:** + ``` + [Macro Body: HasGameplayTag] (PURE) + Step 1: Branch on "Is Valid" (TargetActor) + False → Return False + + Step 2: Call "Does Actor Have Tag" (TargetActor, Tag) → bHasTag (Bool) + (search in context menu: "Does Actor Have Tag" or "Actor Has Tag") + Step 3: Return bHasTag + ``` +- **Nodes to Search For:** + - `Does Actor Have Tag` + - `Is Valid` + - `Select Bool` (for the invalid-actor → false path); or in pure mode use a `Branch` setup... + + **⚠️ Pure Mode Branching Issue:** Pure macros can't use `Branch` nodes. Use this workaround: + ``` + Step 1: "Does Actor Have Tag" (TargetActor, Tag) → bResult + Step 2: "And" logic: "Is Valid" (TargetActor) AND bResult → Return + ``` + This works because `Does Actor Have Tag` on an invalid actor simply returns `False` in UE5. + + **Simpler:** Just call `Does Actor Have Tag` directly — it's already null-safe. The macro is just a convenience wrapper. + +--- + +#### Macro: `AddGameplayTagToActor` +- **Category:** `Framework|Utilities|Tags` +- **Description:** Adds a gameplay tag to an actor's tag container. Logs a warning if the actor doesn't have a tag container. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `TargetActor` | Actor | The actor to modify | + | `Tag` | GameplayTag (Struct) | The tag to add | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution output | +- **Node-by-Node Logic:** + ``` + [Macro Body: AddGameplayTagToActor] + Step 1: Branch on "Is Valid" (TargetActor) + False → Print String Warning: "AddGameplayTagToActor: TargetActor is None" → Return + + Step 2: Call "Get Actor Gameplay Tag Container" (TargetActor) → TagContainer (GameplayTagContainer) + (search: "Get Actor Gameplay Tag Container") + + Step 3: Branch on "Is Valid" (TagContainer reference — actually UE5 returns by ref, so skip this check) + → Call "Add Gameplay Tag" (TagContainer, Tag) + → Return + ``` +- **Nodes to Search For:** + - `Get Actor Gameplay Tag Container` + - `Add Gameplay Tag` (GameplayTagContainer function) + - `Is Valid` + - `Branch` + - `Print String` +- **Alternative:** Use the `I_GameplayTagAssetInterface` if your actors implement it. Call `Add Gameplay Tag` on the interface reference. + +--- + +#### Macro: `RemoveGameplayTagFromActor` +- **Category:** `Framework|Utilities|Tags` +- **Description:** Removes a gameplay tag from an actor's tag container. No-op if actor is invalid. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `TargetActor` | Actor | The actor to modify | + | `Tag` | GameplayTag (Struct) | The tag to remove | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution output | +- **Node-by-Node Logic:** + ``` + [Macro Body: RemoveGameplayTagFromActor] + Step 1: Branch on "Is Valid" (TargetActor) + False → Return + + Step 2: Call "Get Actor Gameplay Tag Container" (TargetActor) → TagContainer + Step 3: Call "Remove Gameplay Tag" (TagContainer, Tag) + Step 4: Return + ``` +- **Nodes to Search For:** + - `Get Actor Gameplay Tag Container` + - `Remove Gameplay Tag` + +--- + +#### Macro: `MakeTagFromString` +- **Category:** `Framework|Utilities|Tags` +- **Description:** Creates a gameplay tag from a string. Returns `EmptyTag` if the tag is not registered in the tag table. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `TagString` | String or Name | The tag string (e.g., "State.Action.Reloading") | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Tag` | GameplayTag (Struct) | The resulting gameplay tag | +- **Node-by-Node Logic:** + ``` + [Macro Body: MakeTagFromString] (PURE) + Step 1: Call "Make Literal GameplayTag" (Value=TagString) → Tag + (search: "Make Literal GameplayTag") + Step 2: Return Tag + ``` +- **Nodes to Search For:** + - `Make Literal GameplayTag` +- **Note:** `Make Literal GameplayTag` returns an empty tag if the string doesn't match any registered tag. This is identical behavior to the `RequestGameplayTag` C++ function. +- **Alternative:** If you have a `Name` instead of a `String`, use `Make GameplayTag from Name`. Convert String → Name first if needed: `String to Name`. +- **⚠️ Gotcha:** Your GameplayTags must be registered in the project's `DefaultGameplayTags.ini` or an `IniSettings` DataTable. If the tag isn't registered, the result is silently empty. Always validate critical tags at startup. + +--- + +#### Macro: `TagContainerHasAny` +- **Category:** `Framework|Utilities|Tags` +- **Description:** Returns `True` if the container has any of the tags in the `TagsToCheck` container. This is an OR query. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Container` | GameplayTagContainer (Struct) | The container to search in | + | `TagsToCheck` | GameplayTagContainer (Struct) | The tags to look for | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `bHasAny` | Boolean | True if container has at least one of TagsToCheck | +- **Node-by-Node Logic:** + ``` + [Macro Body: TagContainerHasAny] (PURE) + Step 1: Call "Has Any" (Container, TagsToCheck) → bHasAny + (on the GameplayTagContainer struct, search: "Has Any" or "Has Any Gameplay Tags") + Step 2: Return bHasAny + ``` +- **Nodes to Search For:** + - `Has Any` (GameplayTagContainer function) +- **One-Liner:** This is a direct wrapper around the built-in `Has Any` node on GameplayTagContainer. You can use `Has Any` directly without this macro. + +--- + +### 4.5 Text Formatting Macros + +#### Macro: `FormatTime` +- **Category:** `Framework|Utilities|Text` +- **Description:** Converts total seconds into a formatted time string. Formats as `HH:MM:SS` if >= 1 hour, otherwise `MM:SS`. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `TotalSeconds` | Float | Total time in seconds | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `FormattedText` | Text | Formatted time string | +- **Node-by-Node Logic:** + ``` + [Macro Body: FormatTime] (PURE) + Step 1: Call "Truncate" (TotalSeconds) → TotalInt (Integer) + (or use "Floor" then "Integer to Integer64" if needed) + + Step 2: Integer division: Hours = TotalInt / 3600 + Step 3: Integer division: RemainingAfterHours = TotalInt % 3600 + Minutes = RemainingAfterHours / 60 + Seconds = RemainingAfterHours % 60 + + Step 4: Convert each to String: HoursStr, MinutesStr, SecondsStr + Use "Format Text" with {0}:D2 padding, or manually pad: + - "To String (Integer)" + "Append" + - Use "Cull" + "Branch" to check if < 10 and prepend "0" + + Step 5: Branch: Hours > 0 + True → "Format Text" ("{0}:{1}:{2}", HoursStr, PaddedMinutes, PaddedSeconds) → Return + False → "Format Text" ("{0}:{1}", PaddedMinutes, PaddedSeconds) → Return + ``` +- **Nodes to Search For:** + - `Truncate` (Float → Integer) + - `/` (Integer division) + - `%` (Modulo / remainder) + - `To String (Integer)` + - `Format Text` + - `Branch` +- **Simplified Padding Approach:** + ``` + To pad a number to 2 digits: + Branch: Number < 10 + True → "Format Text" ("0{0}", NumberStr) → Padded + False → Use NumberStr as-is → Padded + ``` +- **Full Implementation Detail (node by node):** + ``` + Node 1: "Truncate" (Float: TotalSeconds) → TotalInt (Int) + Node 2: "Int / Int" (TotalInt / 3600) → Hours (Int) + Node 3: "Int % Int" (TotalInt % 3600) → RemainderHours (Int) + Node 4: "Int / Int" (RemainderHours / 60) → Minutes (Int) + Node 5: "Int % Int" (RemainderHours % 60) → Seconds (Int) + + Node 6: "To String (Integer)" (Hours) → HoursStr (String) + Node 7: "To String (Integer)" (Minutes) → MinutesStr (String) + Node 8: "To String (Integer)" (Seconds) → SecondsStr (String) + + // Pad minutes and seconds + Node 9: "Int > Int" (Minutes < 10) → Branch + True → "Format Text" ("0{0}", MinutesStr) → PaddedMin (Text) + False → "To Text" (MinutesStr) → PaddedMin (Text) + + Node 10: Same for seconds → PaddedSec (Text) + + Node 11: "Int > Int" (Hours > 0) → Branch + True → "Format Text" ("{0}:{1}:{2}", HoursStr, PaddedMin, PaddedSec) → FormattedText + False → "Format Text" ("{0}:{1}", PaddedMin, PaddedSec) → FormattedText + + Return FormattedText + ``` + +--- + +#### Macro: `Pluralise` +- **Category:** `Framework|Utilities|Text` +- **Description:** Returns singular or plural form based on count (count == 1 returns singular, otherwise plural). +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Count` | Integer | The count | + | `Singular` | Text | Singular form text | + | `Plural` | Text | Plural form text | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Result` | Text | Singular or plural text | +- **Node-by-Node Logic:** + ``` + [Macro Body: Pluralise] (PURE) + Step 1: Branch: Count == 1 + True → Return Singular + False → Return Plural + ``` +- **Nodes to Search For:** + - `Equal (Integer)` + - `Select Text` (condition → two Text options → output) +- **Quick Implementation:** + ``` + "Equal (Integer)" (Count, 1) → bIsSingular (Bool) + "Select (Text)" (bIsSingular, OptionA=Singular, OptionB=Plural) → Result + Return Result + ``` + +--- + +#### Macro: `OrdinalSuffix` +- **Category:** `Framework|Utilities|Text` +- **Description:** Returns the English ordinal suffix for a number: "st", "nd", "rd", or "th". +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Number` | Integer | The number (e.g., 1, 2, 3, 4, 11, 12, 13, 21, 22, 23) | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Suffix` | String | The suffix ("st", "nd", "rd", "th") | +- **Node-by-Node Logic:** + ``` + [Macro Body: OrdinalSuffix] (PURE) + Step 1: Mod11 = Number % 100 // Get last two digits + Step 2: Branch: (Mod11 >= 11 AND Mod11 <= 13) + True → Return "th" // Special case: 11th, 12th, 13th + + Step 3: Mod10 = Number % 10 // Get last digit + Step 4: Switch on Mod10 + 1 → Return "st" + 2 → Return "nd" + 3 → Return "rd" + Default → Return "th" + ``` +- **Nodes to Search For:** + - `%` (Integer modulo) + - `And` (Boolean) + - `>=`, `<=` (Integer comparison) + - `Switch on Int` (or chain of `Select String` nodes) + - `Select String` +- **Full Implementation Detail (node by node):** + ``` + Node 1: "Int % Int" (Number % 100) → Mod11 (Int) + Node 2: "Int >= Int" AND "Int <= Int" (Mod11 >= 11, Mod11 <= 13) → bSpecial (Bool) + Node 3: If bSpecial → Return "th" + Node 4: "Int % Int" (Number % 10) → Mod10 (Int) + Node 5: "Switch on Int" (Mod10) + Case 1 → Return "st" + Case 2 → Return "nd" + Case 3 → Return "rd" + Default → Return "th" + ``` + **If `Switch on Int` isn't available as a pure node**, use a chain of `Select String` nodes: + ``` + "Equal (Integer)" (Mod10, 1) → "Select String" (True="st", False=next check) + next check: "Equal (Integer)" (Mod10, 2) → "Select String" (True="nd", False=next check) + next check: "Equal (Integer)" (Mod10, 3) → "Select String" (True="rd", False="th") + ``` + +--- + +#### Macro: `TruncateText` +- **Category:** `Framework|Utilities|Text` +- **Description:** Truncates text to `MaxChars` characters with "..." ellipsis if truncated. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Input` | Text | The text to potentially truncate | + | `MaxChars` | Integer | Maximum characters before truncation | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `Output` | Text | Truncated text (or original if within limit) | + | `bTruncated` | Boolean | True if text was truncated | +- **Node-by-Node Logic:** + ``` + [Macro Body: TruncateText] (PURE) + Step 1: Convert Input to String: "Text to String" (Input) → InputStr (String) + Step 2: Call "Len" (InputStr) → CharCount (Integer) + Step 3: Branch: CharCount > MaxChars + True → + Step 3a: Call "Left" (InputStr, MaxChars) → TruncatedStr (String) + Step 3b: Call "Append" (TruncatedStr, "...") → FinalStr (String) + Step 3c: Convert to Text: "String to Text" (FinalStr) → Output (Text) + Step 3d: Set bTruncated = True + Step 3e: Return Output, bTruncated + False → + Step 3f: Return Input (unchanged), bTruncated = False + ``` +- **Nodes to Search For:** + - `Text to String` + - `Len` (String length) + - `Left` (String function — gets left N characters) + - `Append` (String concatenation) + - `String to Text` + - `Select Text` + - `Select Bool` +- **Quick Implementation:** + ``` + "Text to String" (Input) → InputStr + "Len" (InputStr) → CharCount + "Int > Int" (CharCount > MaxChars) → bShouldTruncate (Bool) + + "Left" (InputStr, MaxChars) → TruncatedStr + "Append" (TruncatedStr, "...") → FinalStr + "String to Text" (FinalStr) → TruncatedText + + // Select final output + "Select Text" (bShouldTruncate, True=TruncatedText, False=Input) → Output + Return Output, bTruncated + ``` + +--- + +### 4.6 Screen & Viewport Macros + +#### Macro: `WorldToScreenSafe` +- **Category:** `Framework|Utilities|Screen` +- **Description:** Projects a world position to screen coordinates. Returns `False` if off-screen or behind camera. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `WorldContextObject` | Object | World context for camera | + | `WorldLocation` | Vector | World position to project | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution output | + | `ScreenPosition` | Vector2D | Screen position in pixels | + | `bIsOnScreen` | Boolean | True if on screen and in front of camera | +- **Node-by-Node Logic:** + ``` + [Macro Body: WorldToScreenSafe] + Step 1: Call "Get Player Controller" (PlayerIndex=0) → PC (PlayerController) + Step 2: Branch on "Is Valid" (PC) + False → Return ScreenPosition=(0,0), bIsOnScreen=False + + Step 3: Call "Project World Location to Screen" (PC, WorldLocation, + bPlayerViewportRelative=False) → ScreenPos (Vector2D), bSuccess (Bool) + + Step 4: Return ScreenPos, bIsOnScreen=bSuccess + ``` +- **Nodes to Search For:** + - `Get Player Controller` + - `Project World Location to Screen` + - `Is Valid` + - `Branch` +- **Note:** `Project World Location to Screen` returns `False` if the point is behind the camera. The `bIsOnScreen` output only tells you if it's in front — to check if it's within the viewport bounds, use `ClampToViewport` after this macro. + +--- + +#### Macro: `ClampToViewport` +- **Category:** `Framework|Utilities|Screen` +- **Description:** Clamps a widget position (Vector2D in screen pixel coordinates) to stay within the viewport bounds, with optional padding. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `WidgetPosition` | Vector2D | The position to clamp | + | `Padding` | Float | Padding from viewport edges in pixels | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution output | + | `ClampedPosition` | Vector2D | Clamped position | +- **Node-by-Node Logic:** + ``` + [Macro Body: ClampToViewport] + Step 1: Call "Get Viewport Size" → ViewportSize (Vector2D) + Step 2: Clamp X: + "Clamp Float" (WidgetPosition.X, Min=Padding, Max=ViewportSize.X - Padding) → ClampedX + Step 3: Clamp Y: + "Clamp Float" (WidgetPosition.Y, Min=Padding, Max=ViewportSize.Y - Padding) → ClampedY + Step 4: Make Vector2D from ClampedX, ClampedY → ClampedPosition + Step 5: Return ClampedPosition + ``` +- **Nodes to Search For:** + - `Get Viewport Size` + - `Clamp (Float)` — or `FClamp` + - `Make Vector2D` + - `Subtract Float - Float` + - `Break Vector2D` (to get X and Y) + +--- + +### 4.7 Debug Macros (Shipping-Safe) + +These macros are automatically stripped from **Shipping** builds because the underlying engine nodes (`Print String`, `Draw Debug Sphere`, `Draw Debug String`) are no-ops in shipping builds. You wrap them with a `Branch: Is Editor Build` check for extra safety. + +#### Macro: `LogDebug` +- **Category:** `Framework|Utilities|Debug` +- **Description:** Conditional debug output. Prints to output log AND viewport. Stripped in shipping builds. +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `Category` | Name | Log category (any name) | + | `Message` | String | Message to log | + | `Color` | Linear Color | Text color on screen | + | `Duration` | Float | Screen display duration (seconds, default 4.0) | + | `WorldContextObject` | Object | World context | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution output | +- **Node-by-Node Logic:** + ``` + [Macro Body: LogDebug] + Step 1: Call "Is Editor Build" → bIsEditor (Bool) + (search: "Is Editor Build" — development-only macro) + + Step 2: Branch on bIsEditor + True → + Step 2a: Call "Print String" (Message, TextColor=Color, Duration=Duration) + Step 2b: Call "Log String" (Message, Category) + (search: "Log String" — this goes to Output Log) + Step 2c: Return + False → + Return (no-op in shipping) + ``` +- **Nodes to Search For:** + - `Is Editor Build` + - `Branch` + - `Print String` (set `Screen Color` to the `Color` input) + - `Log String` (blueprint node for `UE_LOG` equivalent — search "Log String" in Utilities) +- **Note:** If `Log String` doesn't appear in context menu, you can use `Print String` with `Print to Log` checked ✓ instead. Or use the `Log` node from the `Developer` category. + +--- + +#### Macro: `DrawDebugSphere` +- **Category:** `Framework|Utilities|Debug` +- **Description:** Draws a debug sphere in the world. Stripped in shipping builds. **This is a wrapper around the built-in `Draw Debug Sphere` node.** +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `WorldContextObject` | Object | World context | + | `Center` | Vector | Sphere center world position | + | `Radius` | Float | Sphere radius (default 50) | + | `Color` | Linear Color | Sphere color (default green) | + | `Duration` | Float | How long the sphere persists (default 5.0) | + | `Thickness` | Float | Line thickness (default 0.0 = thin) | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution output | +- **Node-by-Node Logic:** + ``` + [Macro Body: DrawDebugSphere] + Step 1: Branch on "Is Editor Build" + True → + Call "Draw Debug Sphere" (Center, Radius, Segments=12, Color, Duration, Thickness) + Return + False → + Return + ``` +- **Nodes to Search For:** + - `Draw Debug Sphere` + - `Is Editor Build` + - `Branch` + +--- + +#### Macro: `DrawDebugString3D` +- **Category:** `Framework|Utilities|Debug` +- **Description:** Draws 3D world-space debug text. Stripped in shipping builds. **Wrapper around the built-in `Draw Debug String` node.** +- **Input Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution input | + | `WorldContextObject` | Object | World context | + | `Location` | Vector | 3D world position for the text | + | `Text` | String | Text to display | + | `Color` | Linear Color | Text color (default white) | + | `Duration` | Float | Display duration (default 4.0) | + | `Scale` | Float | Font scale (default 1.0) | +- **Output Pins:** + | Name | Type | Description | + |------|------|-------------| + | `exec` | Exec | Execution output | +- **Node-by-Node Logic:** + ``` + [Macro Body: DrawDebugString3D] + Step 1: Branch on "Is Editor Build" + True → + Call "Draw Debug String" (Location, Text, Color=nullptr (use default), Duration, Scale) + (search: "Draw Debug String" — select the version with WorldContextObject pin) + Return + False → + Return + ``` +- **Nodes to Search For:** + - `Draw Debug String` + - `Is Editor Build` + - `Branch` +- **Note:** The built-in `Draw Debug String` node may have different pin names depending on your UE5 version (5.2+ vs 5.5+). Look for the variant that takes `WorldContextObject`, `Text Location`, `Draw Color`, and `Duration`. + +--- + +## 5. Event Dispatchers + +None. This is a Macro Library — no event dispatchers. + +--- + +## 6. Overridden Events / Custom Events + +None. Macros have no `BeginPlay` or lifecycle events. + +--- + +## 7. Blueprint Graph Logic Flow + +```mermaid +flowchart TD + A[Any Blueprint calls ML_GameUtilities macro] --> B{Macro Type?} + B -->|Pure Macro| C[Executes inline as part of caller's expression] + B -->|Impure Macro| D[Receives exec pin flow] + C --> E[Returns value to caller's node] + D --> F[Checks input validity] + F -->|Valid| G[Performs operation] + F -->|Invalid| H[Returns None / False / no-op] + G --> E + H --> I[Outputs exec pin to continue] +``` + +Every macro is self-contained. The caller simply drops the node and wires inputs/outputs. + +--- + +## 8. Communication Matrix + +| Macro | Calls | Purpose | +|-------|-------|---------| +| `GetSubsystemSafe` | `Get Game Instance` → `Get Subsystem (Class)` | Safe subsystem lookup | +| `GetGameFramework` | `GetSubsystemSafe` or `Get Game Instance` → `Cast to GI_GameFramework` | Typed Game Instance access | +| `GetPlayerController` | `Get Player Controller` → `Cast to PC_CoreController` | Typed controller access | +| `FindComponentByInterface` | `Get Components by Class` + `Does Implement Interface` | Interface component search | +| `FindNearestActorWithTag` | `Sphere Overlap Actors` / `Get All Actors with Tag` | Tagged actor spatial search | +| `RemapFloat` | `Map Range Clamped` | Math remap | +| `LerpClamped` | `Clamp` + `Lerp` | Clamped interpolation | +| `VectorToAngle2D` | `Atan2` (degrees) | Direction → angle | +| `AngleDifference` | `%` modulo + branch | Shortest angle delta | +| `HasGameplayTag` | `Does Actor Have Tag` | Tag presence check | +| `AddGameplayTagToActor` | `Get Actor Gameplay Tag Container` + `Add Gameplay Tag` | Tag mutation | +| `RemoveGameplayTagFromActor` | `Get Actor Gameplay Tag Container` + `Remove Gameplay Tag` | Tag mutation | +| `MakeTagFromString` | `Make Literal GameplayTag` | String → tag conversion | +| `TagContainerHasAny` | `Has Any` (container function) | Tag container query | +| `FormatTime` | `Truncate`, `/`, `%`, `Format Text` | Time display | +| `Pluralise` | `Equal (Int)` + `Select Text` | Singular/plural | +| `OrdinalSuffix` | `% Mod`, `Select String` | "st"/"nd"/"rd"/"th" | +| `TruncateText` | `Len`, `Left`, `Append` | Text truncation | +| `WorldToScreenSafe` | `Project World Location to Screen` | Screen projection | +| `ClampToViewport` | `Get Viewport Size` + `Clamp Float` | Screen clamping | +| `LogDebug` | `Print String` + `Log String` (guarded by `Is Editor Build`) | Debug output | +| `DrawDebugSphere` | `Draw Debug Sphere` (guarded) | Debug sphere | +| `DrawDebugString3D` | `Draw Debug String` (guarded) | Debug 3D text | + +--- + +## 9. Validation / Testing Checklist + +- [ ] Macro Library created at `Content/Framework/Core/ML_GameUtilities` +- [ ] All 24 macros present and named correctly +- [ ] Pure macros (math, text, tag queries) have `Pure` checked ✓ in macro settings +- [ ] Impure macros (subsystem, actor, debug, screen) have exec input/output pins +- [ ] `GetSubsystemSafe` returns `None` instead of crashing when Subsystem doesn't exist +- [ ] `GetGameFramework` returns `None` when GameInstance is wrong type +- [ ] `GetPlayerController` returns `None` when no controller +- [ ] `FormatTime(3661.0)` returns `"1:01:01"` or `"01:01:01"` +- [ ] `FormatTime(125.0)` returns `"2:05"` (no hours) +- [ ] `Pluralise(1, "item", "items")` returns `"item"` and `Pluralise(3, "item", "items")` returns `"items"` +- [ ] `OrdinalSuffix(1)` = `"st"`, `OrdinalSuffix(2)` = `"nd"`, `OrdinalSuffix(11)` = `"th"` +- [ ] `TruncateText("Hello World", 5)` returns `"Hello..."`, `bTruncated=True` +- [ ] `HasGameplayTag` returns `False` for invalid actor (no crash) +- [ ] `RemapFloat(0.5, 0, 1, 0, 100)` returns `50.0` +- [ ] `LerpClamped(0, 100, 0.5)` returns `50.0` +- [ ] `VectorToAngle2D(1, 0)` returns `0.0`, `VectorToAngle2D(0, 1)` returns `90.0` +- [ ] `AngleDifference(10, 350)` returns `-20` (or `-20.0`) +- [ ] Debug macros produce output in Editor; no output in Shipping build (test `Is Editor Build` branch) +- [ ] `WorldToScreenSafe` returns `bIsOnScreen=False` for point behind camera +- [ ] `ClampToViewport` keeps position within viewport bounds +- [ ] `TagContainerHasAny` correctly reports OR logic for overlapping containers +- [ ] Edge case: `GetSubsystemSafe` with `None` WorldContextObject → returns `None` (no crash) +- [ ] Edge case: `FindComponentByInterface` on actor with zero components → returns `None`, `bFound=False` +- [ ] Edge case: `FormatTime(0)` returns `"0:00"` +- [ ] Edge case: `TruncateText("", 10)` returns `""`, `bTruncated=False` + +--- + +## 10. Reuse Notes + +- **This is the Blueprint-only replacement for `FL_GameUtilities`.** Use this when you can't compile C++. +- All macros use only built-in engine nodes. No plugins or custom C++ required. +- **When to use `ML_GameUtilities` vs raw engine nodes:** + - Use `GetSubsystemSafe` whenever you need a null-safe subsystem lookup (instead of raw `Get Subsystem`). + - Use `FormatTime` instead of writing your own time formatting logic. + - For simple math like `RemapFloat`, you can use `Map Range Clamped` directly — the macro is just a convenience alias. + - For debug macros, the `Is Editor Build` guard ensures zero shipping overhead. +- **Extending:** If you need project-specific utilities, create your own Macro Library (e.g., `ML_ProjectUtilities`) and add macros there. Do NOT modify `ML_GameUtilities` — it should remain a generic utility library. +- **Construction Script compatibility:** Pure macros can be called from construction scripts. Impure macros cannot (construction scripts don't support exec pins). +- **Macro vs Function:** Macros are "copy-pasted" into every caller graph at compile time. They have zero function call overhead but increase Blueprint graph size. For frequently-used macros (like `FormatTime`), this is fine. For very complex macros (`FindNearestActorWithTag`), consider creating a `BPC_` component with a function instead. + +--- + +## 11. Manual Implementation Guide + +> **This section is for a human implementer building this Macro Library manually in UE5.** +> Follow these steps IN ORDER. Each step creates one macro. + +### 11.1 Class Setup + +1. **Create the Macro Library asset:** + - In the Content Browser, navigate to `Content/Framework/Core/`. + - Right-click → **Miscellaneous** → **Blueprint Macro Library**. + - Name it: `ML_GameUtilities`. + - Double-click to open. + +2. **Set up the macro categories:** + - This step is optional but helps with organization. + - In the Macro Library window, you'll see a list of macros on the left panel. + - Macros will appear in the context menu under `Framework|Utilities|{Category}` based on what you type in the **Category** field of each macro. + +### 11.2 Create Each Macro — Step-by-Step + +For **each macro** listed below, follow this creation process: + +1. In the `ML_GameUtilities` window, click **+ Add Macro** in the "My Blueprint" panel. +2. Name it exactly as specified. +3. In the **Details** panel, set: + - **Category:** (as listed) + - **Keywords:** (add search keywords like `utility`, `safe`, `subsystem`) + - **Description:** (as listed) +4. For **pure** macros: check ✓ **Pure** in the macro's Details panel. +5. Add input and output pins (click the **+** button on the macro's header, or right-click → "Add Input" / "Add Output"). +6. Open the macro graph and wire the nodes as described in each section. + +#### Step 1: `GetSubsystemSafe` (Impure) + +1. Add macro → name `GetSubsystemSafe`, Category = `Subsystems`. +2. Add input pins: + - `WorldContextObject` — Type: `Object` + - `SubsystemClass` — Type: `GameInstanceSubsystem` (Class Reference) +3. Add output pin: `Subsystem` — Type: `GameInstanceSubsystem` +4. In the graph: + ``` + [Input exec] → [Get Game Instance] (WorldContextObject) → [Is Valid] → [Branch] + True → [Get Subsystem (Class)] (GameInstance, SubsystemClass) → Return node → [Output Subsystem] + False → Return node → leave Subsystem pin unconnected (returns None) + ``` +5. **Nodes to drop:** `Get Game Instance`, `Is Valid`, `Branch`, `Get Subsystem (Class)` (from the `Subsystems` submenu). + +#### Step 2: `GetGameFramework` (Impure) + +1. Add macro → name `GetGameFramework`, Category = `Subsystems`. +2. Add input pin: `WorldContextObject` — Type: `Object`. +3. Add output pin: `GameFramework` — Type: `GI_GameFramework` (or your actual game instance class). +4. In the graph: + ``` + [Input exec] → [Get Game Instance] → [Cast to GI_GameFramework] + Success → Return → [GameFramework] + Failed → [Print String] (Warning: "No GameFramework") → Return → None + ``` +5. **Nodes to drop:** `Get Game Instance`, `Cast to GI_GameFramework`, `Print String`, `Branch`. + +#### Step 3: `GetPlayerController` (Impure) + +1. Add macro → name `GetPlayerController`, Category = `Subsystems`. +2. Add input pin: `WorldContextObject` — Type: `Object`. +3. Add output pin: `PlayerController` — Type: `PC_CoreController` (or your actual PC class). +4. In the graph: + ``` + [Input exec] → [Get Player Controller] (Player Index 0) → [Cast to PC_CoreController] + Success → Return → [PlayerController] + Failed → Return → None + ``` +5. **Nodes to drop:** `Get Player Controller`, `Cast to PC_CoreController`, `Branch`. + +#### Step 4: `GetSubsystemByClass` (Impure) + +1. Add macro → name `GetSubsystemByClass`, Category = `Subsystems`. +2. Add input pins: `WorldContextObject` (Object), `SubsystemClass` (Class Ref → GameInstanceSubsystem). +3. Add output pin: `Subsystem` (GameInstanceSubsystem). +4. In the graph: + ``` + [Input exec] → [GetSubsystemSafe macro] (WorldContextObject, SubsystemClass) → [Subsystem output] + Return Subsystem + ``` +5. **Nodes to drop:** The `GetSubsystemSafe` macro itself (drag from "My Blueprint" panel into this graph). + +#### Step 5: `FindComponentByInterface` (Impure) + +1. Add macro → name `FindComponentByInterface`, Category = `Actor`. +2. Add input pins: `TargetActor` (Actor), `InterfaceClass` (Class Ref → Interface). +3. Add output pins: `FoundComponent` (ActorComponent), `bFound` (Boolean). +4. In the graph: + - `Is Valid` (TargetActor) → `Branch` → False path: return None, bFound=False. + - True path: `Get Components by Class` (TargetActor, ActorComponent) → `For Loop with Break`: + - Loop body: `Does Implement Interface` (array element, InterfaceClass) → `Branch` → True: set FoundComponent, set bFound=True, break. False: continue. + - After loop: return FoundComponent, bFound. +5. **Nodes to drop:** `Is Valid`, `Branch`, `Get Components by Class`, `For Loop with Break`, `Does Implement Interface`. + +#### Step 6: `FindNearestActorWithTag` (Impure) + +1. Add macro → name `FindNearestActorWithTag`, Category = `Actor`. +2. Add input pins: `Origin` (Vector), `Radius` (Float), `RequiredTag` (GameplayTag), `ActorsToIgnore` (Array of Actor). +3. Add output pins: `NearestActor` (Actor), `Distance` (Float). +4. In the graph: + - `Sphere Overlap Actors` (Origin, Radius, Object Type: WorldDynamic or All) → Array of Actors. + - Local variables: `NearestActor` (Actor, default None), `NearestDist` (Float, default -1 or a large number). + - `For Each Loop` over OverlappedActors: + - `Does Actor Have Tag` (Array Element, RequiredTag) → `Branch`: + - True: Check if element is in `ActorsToIgnore` via `Array Contains Item` → skip if true. + - If not ignored: `Get Distance To` (Element, Origin) → Dist. + - Branch: Dist <= NearestDist OR NearestDist < 0 → set NearestActor & NearestDist. + - False: continue. + - Return NearestActor, NearestDist. +5. **Nodes to drop:** `Sphere Overlap Actors`, `For Each Loop`, `Does Actor Have Tag`, `Array Contains Item`, `Get Distance To`, `Branch`, `Print String` (warning). + +#### Step 7: `RemapFloat` (Pure) + +1. Add macro → name `RemapFloat`, Category = `Math`. Check ✓ **Pure**. +2. Add input pins: `Value`, `InMin`, `InMax`, `OutMin`, `OutMax` (all Float). +3. Add output pin: `Result` (Float). +4. In the graph: + ``` + [Map Range Clamped] (In Value=Value, In Range A=InMin, In Range B=InMax, Out Range A=OutMin, Out Range B=OutMax, Clamped=True) → Result + Return Result + ``` +5. **Nodes to drop:** `Map Range Clamped`. + +#### Step 8: `LerpClamped` (Pure) + +1. Add macro → name `LerpClamped`, Category = `Math`. Check ✓ **Pure**. +2. Add input pins: `A`, `B`, `Alpha` (all Float). +3. Add output pin: `Result` (Float). +4. In the graph: + ``` + [Clamp (Float)] (Alpha, 0.0, 1.0) → ClampedAlpha + [Lerp (Float)] (A, B, ClampedAlpha) → Result + Return Result + ``` +5. **Nodes to drop:** `Clamp (Float)`, `Lerp (Float)`. + +#### Step 9: `VectorToAngle2D` (Pure) + +1. Add macro → name `VectorToAngle2D`, Category = `Math`. Check ✓ **Pure**. +2. Add input pin: `Direction` (Vector2D). +3. Add output pin: `Angle` (Float). +4. In the graph: + ``` + [Break Vector2D] (Direction) → X, Y + [Deg Atan2] (Y, X) → RawAngle (-180 to 180) + // Normalize to 0-360: + [Select Float] (RawAngle < 0, True=RawAngle + 360, False=RawAngle) → Angle + Return Angle + ``` +5. **Nodes to drop:** `Break Vector2D`, `Deg Atan2` (or `Atan2 (degrees)`), `Select Float`, `Float + Float`. + +#### Step 10: `AngleDifference` (Pure) + +1. Add macro → name `AngleDifference`, Category = `Math`. Check ✓ **Pure**. +2. Add input pins: `AngleA`, `AngleB` (both Float). +3. Add output pin: `Difference` (Float). +4. In the graph: + ``` + [Float B - Float A] → Diff + [FMod] (Diff, 360.0) → WrappedDiff + [Select Float] (WrappedDiff > 180.0, True=WrappedDiff - 360.0, False= + [Select Float] (WrappedDiff < -180.0, True=WrappedDiff + 360.0, False=WrappedDiff) + ) → Difference + Return Difference + ``` +5. **Nodes to drop:** `Float - Float`, `FMod` (search "fmod" or "modulo"), `Select Float`. + +#### Step 11: `HasGameplayTag` (Pure) + +1. Add macro → name `HasGameplayTag`, Category = `Tags`. Check ✓ **Pure**. +2. Add input pins: `TargetActor` (Actor), `Tag` (GameplayTag, struct by value). +3. Add output pin: `bHasTag` (Boolean). +4. In the graph: + ``` + [Is Valid] (TargetActor) → ValidBool + [Does Actor Have Tag] (TargetActor, Tag) → HasTagBool + [AND Boolean] (ValidBool, HasTagBool) → bHasTag + Return bHasTag + ``` +5. **Nodes to drop:** `Is Valid`, `Does Actor Have Tag`, `AND Boolean`. +6. **Note:** You can actually skip `Is Valid` — `Does Actor Have Tag` on `None` returns `False` safely. The extra check is insurance. + +#### Step 12: `AddGameplayTagToActor` (Impure) + +1. Add macro → name `AddGameplayTagToActor`, Category = `Tags`. +2. Add input pins: `TargetActor` (Actor), `Tag` (GameplayTag). +3. Add no output pins (just exec). +4. In the graph: + ``` + [Input exec] → [Is Valid] (TargetActor) → [Branch] + True → [Get Actor Gameplay Tag Container] (TargetActor) → TagContainer + → [Add Gameplay Tag] (TagContainer, Tag) → Return node + False → [Print String] ("TargetActor is invalid") → Return node + ``` +5. **Nodes to drop:** `Is Valid`, `Branch`, `Get Actor Gameplay Tag Container`, `Add Gameplay Tag`, `Print String`. + +#### Step 13: `RemoveGameplayTagFromActor` (Impure) + +1. Same as Step 12 but use `Remove Gameplay Tag` instead of `Add Gameplay Tag`. +2. Name: `RemoveGameplayTagFromActor`. + +#### Step 14: `MakeTagFromString` (Pure) + +1. Add macro → name `MakeTagFromString`, Category = `Tags`. Check ✓ **Pure**. +2. Add input pin: `TagString` (String). +3. Add output pin: `Tag` (GameplayTag). +4. In the graph: + ``` + [String to Name] (TagString) → TagName (Name) + [Make GameplayTag from Name] (TagName) → Tag + Return Tag + ``` +5. **Nodes to drop:** `String to Name`, `Make GameplayTag from Name`. +6. **Alternative:** Use `Make Literal GameplayTag` directly — it takes a `Name` not a `String`. You may need to convert String→Name first. + +#### Step 15: `TagContainerHasAny` (Pure) + +1. Add macro → name `TagContainerHasAny`, Category = `Tags`. Check ✓ **Pure**. +2. Add input pins: `Container` (GameplayTagContainer), `TagsToCheck` (GameplayTagContainer). +3. Add output pin: `bHasAny` (Boolean). +4. In the graph: + ``` + [Has Any] (Target=Container, Other=TagsToCheck) → bHasAny + Return bHasAny + ``` +5. **Nodes to drop:** `Has Any` (search on GameplayTagContainer → "Has Any Gameplay Tags"). + +#### Step 16: `FormatTime` (Pure) + +1. Add macro → name `FormatTime`, Category = `Text`. Check ✓ **Pure**. +2. Add input pin: `TotalSeconds` (Float). +3. Add output pin: `FormattedText` (Text). +4. In the graph — follow the detailed node-by-node logic from Section 4.5: + ``` + 1. Truncate(TotalSeconds) → TotalInt + 2. TotalInt / 3600 → Hours + 3. TotalInt % 3600 → Remainder + 4. Remainder / 60 → Minutes + 5. Remainder % 60 → Seconds + 6. To String (Hours) → HStr + 7. To String (Minutes) → MStr + 8. To String (Seconds) → SStr + 9. Pad MStr and SStr (Branch < 10 → prepend "0") + 10. Hours > 0 → Format Text("{0}:{1}:{2}", HStr, PaddedM, PaddedS) + Else → Format Text("{0}:{1}", PaddedM, PaddedS) + ``` +5. **Key nodes:** `Truncate`, `/` (int division), `%` (mod), `To String (Integer)`, `Format Text`, `Select Text`. + +#### Step 17: `Pluralise` (Pure) + +1. Add macro → name `Pluralise`, Category = `Text`. Check ✓ **Pure**. +2. Inputs: `Count` (Integer), `Singular` (Text), `Plural` (Text). +3. Outputs: `Result` (Text). +4. Graph: `Equal (Int)` (Count, 1) → `Select Text` (True=Singular, False=Plural) → Return. + +#### Step 18: `OrdinalSuffix` (Pure) + +1. Add macro → name `OrdinalSuffix`, Category = `Text`. Check ✓ **Pure**. +2. Input: `Number` (Integer). Output: `Suffix` (String). +3. Follow Section 4.5 node-by-node: %100 → check 11-13 → %10 → switch on 1/2/3 → return. +4. **Key nodes:** `%` (mod), `AND (bool)`, `>=`, `<=`, `Select String` (chained). + +#### Step 19: `TruncateText` (Pure) + +1. Add macro → name `TruncateText`, Category = `Text`. Check ✓ **Pure**. +2. Inputs: `Input` (Text), `MaxChars` (Integer). Outputs: `Output` (Text), `bTruncated` (Boolean). +3. Follow Section 4.5: Text→String → Len → Branch (Len > MaxChars) → Left(str, MaxChars) + "..." → String→Text → Select Text. + +#### Step 20: `WorldToScreenSafe` (Impure) + +1. Add macro → name `WorldToScreenSafe`, Category = `Screen`. +2. Inputs: `WorldContextObject` (Object), `WorldLocation` (Vector). +3. Outputs: `exec`, `ScreenPosition` (Vector2D), `bIsOnScreen` (Boolean). +4. Graph: `Get Player Controller` → `Is Valid` → `Branch` → `Project World Location to Screen` → Return. +5. **Nodes:** `Get Player Controller`, `Is Valid`, `Branch`, `Project World Location to Screen`. + +#### Step 21: `ClampToViewport` (Impure) + +1. Add macro → name `ClampToViewport`, Category = `Screen`. +2. Inputs: `WidgetPosition` (Vector2D), `Padding` (Float). +3. Outputs: `exec`, `ClampedPosition` (Vector2D). +4. Graph: `Get Viewport Size` → `Break Vector2D` → Clamp X, Clamp Y each with `Padding` min and `ViewportSize - Padding` max → `Make Vector2D` → Return. +5. **Nodes:** `Get Viewport Size`, `Break Vector2D`, `Clamp (Float)`, `Make Vector2D`. + +#### Step 22: `LogDebug` (Impure) + +1. Add macro → name `LogDebug`, Category = `Debug`. +2. Inputs: `Category` (Name), `Message` (String), `Color` (LinearColor), `Duration` (Float, default 4.0), `WorldContextObject` (Object). +3. No output pins (just exec). +4. Graph: `Is Editor Build` → `Branch` → True: `Print String` (Message, TextColor=Color, Duration=Duration) + `Log String` (Message, Category) → Return. False: Return. + +#### Step 23: `DrawDebugSphere` (Impure) + +1. Add macro → name `DrawDebugSphere`, Category = `Debug`. +2. Inputs: `WorldContextObject` (Object), `Center` (Vector), `Radius` (Float, default 50), `Color` (LinearColor, default Green), `Duration` (Float, default 5.0), `Thickness` (Float, default 0.0). +3. Graph: `Is Editor Build` → `Branch` → True: `Draw Debug Sphere` → Return. + +#### Step 24: `DrawDebugString3D` (Impure) + +1. Add macro → name `DrawDebugString3D`, Category = `Debug`. +2. Inputs: `WorldContextObject` (Object), `Location` (Vector), `Text` (String), `Color` (LinearColor, default White), `Duration` (Float, default 4.0), `Scale` (Float, default 1.0). +3. Graph: `Is Editor Build` → `Branch` → True: `Draw Debug String` → Return. + +--- + +### 11.3 Quick Node Reference + +Common UE5 Blueprint nodes used across macros: + +| Node | Where to Find | Used In | +|------|---------------|---------| +| `Get Game Instance` | Right-click → "Get Game Instance" | Subsystem macros | +| `Get Subsystem (Class)` | Context menu → Subsystems | `GetSubsystemSafe` | +| `Cast To` | Right-click → "Cast To {ClassName}" | `GetGameFramework`, `GetPlayerController` | +| `Get Player Controller` | Right-click → "Get Player Controller" (Player Index 0) | `GetPlayerController`, `WorldToScreenSafe` | +| `Is Valid` | Right-click → "Is Valid" (Object) | Nearly all macros | +| `Branch` | Right-click → "Branch" | All impure macros | +| `Map Range Clamped` | Right-click → "Map Range Clamped" | `RemapFloat` | +| `Clamp (Float)` | Right-click → "Clamp (Float)" | `LerpClamped`, `ClampToViewport` | +| `Lerp (Float)` | Right-click → "Lerp (Float)" | `LerpClamped` | +| `Deg Atan2` | Right-click → "Deg Atan2" | `VectorToAngle2D` | +| `FMod` | Right-click → "FMod" or "% (Float)" | `AngleDifference` | +| `Does Actor Have Tag` | Right-click → "Does Actor Have Tag" | `HasGameplayTag`, `FindNearestActorWithTag` | +| `Get Actor Gameplay Tag Container` | Right-click → "Get Actor Gameplay Tag Container" | Tag mutation macros | +| `Make Literal GameplayTag` | Right-click → "Make Literal GameplayTag" | `MakeTagFromString` | +| `Has Any` | Right-click on GameplayTagContainer → "Has Any" | `TagContainerHasAny` | +| `For Loop with Break` | Right-click → "For Loop with Break" | `FindComponentByInterface` | +| `Get Components by Class` | Right-click → "Get Components by Class" | `FindComponentByInterface` | +| `Does Implement Interface` | Right-click → "Does Implement Interface" | `FindComponentByInterface` | +| `Sphere Overlap Actors` | Right-click → "Sphere Overlap Actors" | `FindNearestActorWithTag` | +| `Get Distance To` | Right-click on Actor → "Get Distance To" | `FindNearestActorWithTag` | +| `Truncate` | Right-click → "Truncate (Float)" | `FormatTime` | +| `Format Text` | Right-click → "Format Text" | `FormatTime` | +| `Select Text` | Right-click → "Select" | `Pluralise`, `TruncateText` | +| `Select Float` | Right-click → "Select" | `AngleDifference`, `VectorToAngle2D` | +| `Select String` | Right-click → "Select" | `OrdinalSuffix` | +| `Left` (String) | Right-click → "Left (String)" | `TruncateText` | +| `Len` | Right-click → "Len" | `TruncateText` | +| `Project World Location to Screen` | Right-click → "Project World Location to Screen" | `WorldToScreenSafe` | +| `Get Viewport Size` | Right-click → "Get Viewport Size" | `ClampToViewport` | +| `Is Editor Build` | Right-click → "Is Editor Build" | All debug macros | +| `Print String` | Right-click → "Print String" | `LogDebug` | +| `Draw Debug Sphere` | Right-click → "Draw Debug Sphere" | `DrawDebugSphere` | +| `Draw Debug String` | Right-click → "Draw Debug String" | `DrawDebugString3D` | + +--- + +## 12. Blueprint Build Checklist + +Complete these steps in order when implementing this Macro Library: + +- [ ] Create Macro Library asset at `Content/Framework/Core/ML_GameUtilities` +- [ ] Create `GetSubsystemSafe` macro (Subsystems, impure) — 3 ins, 1 out +- [ ] Create `GetGameFramework` macro (Subsystems, impure) — 1 in, 1 out +- [ ] Create `GetPlayerController` macro (Subsystems, impure) — 1 in, 1 out +- [ ] Create `GetSubsystemByClass` macro (Subsystems, impure) — wraps `GetSubsystemSafe` +- [ ] Create `FindComponentByInterface` macro (Actor, impure) — 2 ins, 2 outs +- [ ] Create `FindNearestActorWithTag` macro (Actor, impure) — 4 ins, 2 outs +- [ ] Create `RemapFloat` macro (Math, pure ✓) — 5 ins, 1 out +- [ ] Create `LerpClamped` macro (Math, pure ✓) — 3 ins, 1 out +- [ ] Create `VectorToAngle2D` macro (Math, pure ✓) — 1 in, 1 out +- [ ] Create `AngleDifference` macro (Math, pure ✓) — 2 ins, 1 out +- [ ] Create `HasGameplayTag` macro (Tags, pure ✓) — 2 ins, 1 out +- [ ] Create `AddGameplayTagToActor` macro (Tags, impure) — 2 ins +- [ ] Create `RemoveGameplayTagFromActor` macro (Tags, impure) — 2 ins +- [ ] Create `MakeTagFromString` macro (Tags, pure ✓) — 1 in, 1 out +- [ ] Create `TagContainerHasAny` macro (Tags, pure ✓) — 2 ins, 1 out +- [ ] Create `FormatTime` macro (Text, pure ✓) — 1 in, 1 out +- [ ] Create `Pluralise` macro (Text, pure ✓) — 3 ins, 1 out +- [ ] Create `OrdinalSuffix` macro (Text, pure ✓) — 1 in, 1 out +- [ ] Create `TruncateText` macro (Text, pure ✓) — 2 ins, 2 outs +- [ ] Create `WorldToScreenSafe` macro (Screen, impure) — 2 ins, 3 outs (2 data + exec) +- [ ] Create `ClampToViewport` macro (Screen, impure) — 2 ins, 2 outs (1 data + exec) +- [ ] Create `LogDebug` macro (Debug, impure) — 5 ins +- [ ] Create `DrawDebugSphere` macro (Debug, impure) — 5 ins +- [ ] Create `DrawDebugString3D` macro (Debug, impure) — 6 ins +- [ ] Run validation checklist (Section 9) — all 24 items pass +- [ ] Test from a test Blueprint Actor: call each macro and verify outputs +- [ ] Save macro library + +--- + +## Multiplayer Networking + +**Replication: None needed.** Macro Libraries are called locally per-client or server. The macros themselves have no state and make no RPCs. Functions like `GetPlayerController` return the local player controller on clients and the first player controller on the server (for the server's "local" player). + +**Authority notes:** +- Subsystem access macros return local-side subsystems. Server-only subsystems won't be available on clients. +- `FindNearestActorWithTag` and `AddGameplayTagToActor` operate on whatever actors exist on the calling side. On a dedicated server, all actors exist. On a client, only replicated actors exist. + +--- + +*Blueprint-only Macro Library specification for ML_GameUtilities — 24 macros, zero C++ required.* diff --git a/docs/blueprints/INDEX.md b/docs/blueprints/INDEX.md index 68b5c69..63d67ef 100644 --- a/docs/blueprints/INDEX.md +++ b/docs/blueprints/INDEX.md @@ -1,6 +1,6 @@ # Master Blueprint Index — UE5 Modular Game Framework -**Version:** 3.1 | **Generated:** 2026-05-20 | **Total Files:** 135 numbered + 1 starter (136 total specs) +**Version:** 3.2 | **Generated:** 2026-05-20 | **Total Files:** 135 numbered + 1 starter + 1 supplementary (137 total specs) This document is the canonical index of every Blueprint specification file in the framework. Each entry links to its full spec document and includes: file name, asset type, parent class, purpose summary, and key dependencies. @@ -16,7 +16,7 @@ docs/blueprints/ │ ├── 00-project-setup/ ← Project Setup & Starter Assets (1 file) │ └── GI_StarterGameInstance.md ← Minimal GameInstance; tag validation entry point -├── 01-core/ ← Foundation (7 files + 11 Data Table CSVs) +├── 01-core/ ← Foundation (7 files + 11 Data Table CSVs + 1 Macro Library) │ ├── data-tables/ ← Per-category Gameplay Tag Data Tables (NEW — replaces DT_ProjectTags.csv) │ │ ├── DT_Tags_Player.csv (34 tags) │ │ ├── DT_Tags_Interaction.csv (36 tags) @@ -56,6 +56,7 @@ docs/blueprints/ | — | [`GI_StarterGameInstance`](00-project-setup/GI_StarterGameInstance.md) | `GI_` Game Instance | `GameInstance` | Minimal GameInstance; loads DA_GameTagRegistry, validates tags, broadcasts OnFrameworkReady | 00-project-setup | | 01 | [`01_DA_GameTagRegistry`](01-core/01_DA_GameTagRegistry.md) | `DA_` Data Asset | `PrimaryDataAsset` | Central gameplay tag registry; maps tags to data/assets | 01-core | | 02 | [`02_FL_GameUtilities`](01-core/02_FL_GameUtilities.md) | `FL_` Function Library | `BlueprintFunctionLibrary` | Shared utility functions (logging, tag queries, math) | 01-core | +| — | [`02a_ML_GameUtilities`](01-core/02a_ML_GameUtilities.md) | `ML_` Macro Library | `MacroLibrary` | **BP-Only companion:** 24 macros (no C++ required) — subsystem access, math, tags, text, screen, debug | 01-core | | 03 | [`03_I_InterfaceLibrary`](01-core/03_I_InterfaceLibrary.md) | `I_` Interface | `Interface` | All framework interfaces (I_Damageable, I_Interactable, I_Persistable, etc.) | 01-core | | 04 | [`04_GI_GameFramework`](01-core/04_GI_GameFramework.md) | `GI_` Game Instance | `GameInstance` | Application kernel; owns subsystems, game phases, platform init | 01-core | | 05 | [`05_GM_CoreGameMode`](01-core/05_GM_CoreGameMode.md) | `GM_` Game Mode | `GameModeBase` | Core game mode; spawns player, sets default classes, pause control | 01-core |