Compare commits
40 Commits
eeb1bf82c9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57ea21073a | ||
|
|
15d6e88780 | ||
|
|
dc9c1a6b98 | ||
|
|
14441c000c | ||
|
|
7c2e8df6b1 | ||
|
|
5c08c929b5 | ||
|
|
321287253b | ||
|
|
9fd679fd5b | ||
|
|
d16c661022 | ||
|
|
0a2d08b2ad | ||
|
|
6b6c702dd7 | ||
|
|
318d0d4317 | ||
|
|
040db37720 | ||
|
|
c515920eea | ||
|
|
5b7d652505 | ||
|
|
bcbfcdf167 | ||
|
|
0852386168 | ||
|
|
44aca98885 | ||
|
|
7e876e4f0c | ||
|
|
ccd1872e59 | ||
|
|
8bb162eda2 | ||
|
|
cd0ebf2233 | ||
|
|
6132571f8d | ||
|
|
4ae2137179 | ||
|
|
f986343325 | ||
|
|
a145ae9373 | ||
|
|
d982a6367d | ||
|
|
108173b87b | ||
|
|
d100a097f5 | ||
|
|
3da9fd7493 | ||
|
|
9ee0a65630 | ||
|
|
0b1128d209 | ||
|
|
3ca87a7893 | ||
|
|
4a7c871f29 | ||
|
|
f6c4f44827 | ||
|
|
fee12b115f | ||
|
|
3023ad3555 | ||
|
|
1d699e54fa | ||
|
|
209f24a0f8 | ||
|
|
bec6cb715e |
16
.gitattributes
vendored
Normal file
16
.gitattributes
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Unreal binary assets
|
||||
*.uasset filter=lfs diff=lfs merge=lfs -text
|
||||
*.umap filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Common DCC / source asset formats you may want in LFS too
|
||||
*.fbx filter=lfs diff=lfs merge=lfs -text
|
||||
*.blend filter=lfs diff=lfs merge=lfs -text
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||
*.tga filter=lfs diff=lfs merge=lfs -text
|
||||
*.exr filter=lfs diff=lfs merge=lfs -text
|
||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
*.mov filter=lfs diff=lfs merge=lfs -text
|
||||
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Unreal generated folders
|
||||
Binaries/
|
||||
DerivedDataCache/
|
||||
Intermediate/
|
||||
Saved/
|
||||
|
||||
# Visual Studio / Rider / IDE
|
||||
.vs/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.VC.db
|
||||
*.VC.opendb
|
||||
*.sln
|
||||
*.suo
|
||||
*.opensdf
|
||||
*.sdf
|
||||
|
||||
# OS junk
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
|
||||
# Local AI / tool folders
|
||||
.kilo/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Keep core Unreal project files
|
||||
!*.uproject
|
||||
!*.uplugin
|
||||
@@ -2,5 +2,18 @@
|
||||
"$schema": "https://app.kilo.ai/config.json",
|
||||
"indexing": {
|
||||
"enabled": true
|
||||
},
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"type": "local",
|
||||
"command": [
|
||||
"npx",
|
||||
"-y",
|
||||
"@upstash/context7-mcp"
|
||||
],
|
||||
"environment": {
|
||||
"DEFAULT_MINIMUM_TOKENS": "{{DEFAULT_MINIMUM_TOKENS}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
80
CONTEXT.md
80
CONTEXT.md
@@ -44,6 +44,18 @@ Content/
|
||||
Input/ # Enhanced Input assets (NEW)
|
||||
Actions/ # IA_* Input Action assets
|
||||
Contexts/ # IMC_* Input Mapping Context assets
|
||||
DataTables/ # Gameplay Tag Data Tables (NEW — 11 per-category CSV files)
|
||||
DT_Tags_Player.csv
|
||||
DT_Tags_Interaction.csv
|
||||
DT_Tags_Item.csv
|
||||
DT_Tags_Narrative.csv
|
||||
DT_Tags_AI.csv
|
||||
DT_Tags_Save.csv
|
||||
DT_Tags_Environment.csv
|
||||
DT_Tags_Combat.csv
|
||||
DT_Tags_State.csv
|
||||
DT_Tags_Audio.csv
|
||||
DT_Tags_Achievement.csv
|
||||
Achievements/
|
||||
Settings/
|
||||
State/ # State Management assets (NEW)
|
||||
@@ -56,6 +68,22 @@ Content/
|
||||
S_ActionPermissionResult.uasset # Permission result struct
|
||||
DA_StateGatingTable.uasset # All gating rules (37 action rules)
|
||||
BPC_StateManager.uasset # Central state authority component
|
||||
Capture/ # Planar Capture assets (NEW — Phase 17)
|
||||
BP_Mirror.uasset # Standard mirror surface
|
||||
BP_Portal.uasset # Portal surface with linked target
|
||||
BP_Monitor.uasset # Security camera monitor
|
||||
BP_HorrorMirror.uasset # Horror mirror with wrong reflection
|
||||
BP_FakeWindow.uasset # Architectural fake window
|
||||
M_CaptureSurface_Master.uasset # Master unlit material (9-layer)
|
||||
MPC_CaptureSurface.uasset # Global MPC (10 scalar params)
|
||||
DA_PlanarCaptureProfile.uasset # Per-surface capture config
|
||||
MI_Mirror_Clean.uasset # Material instances
|
||||
MI_Mirror_Dirty.uasset
|
||||
MI_Mirror_Steam.uasset
|
||||
MI_Mirror_Horror.uasset
|
||||
MI_Portal_Standard.uasset
|
||||
MI_Monitor_Security.uasset
|
||||
MI_FakeWindow_Interior.uasset
|
||||
|
||||
docs/
|
||||
blueprints/
|
||||
@@ -71,17 +99,19 @@ docs/
|
||||
07-narrative/ # Narrative, Dialogue, Objective & Choice (11 files, 58-68)
|
||||
08-weapons/ # Weapon, Equipment & Damage (11 files, 69-79)
|
||||
09-ai/ # AI, Perception & Encounters (9 files, 80-88)
|
||||
10-adaptive/ # Adaptive Environment, Atmosphere & Scare (15 files, 89-101, 132-133)
|
||||
11-meta/ # Achievements, Progression & Meta (2 files, 102-103)
|
||||
12-settings/ # Settings, Accessibility & Platform (2 files, 104-105)
|
||||
13-polish/ # Tutorial, Loading, Credits, Debug (9 files, 106-114)
|
||||
14-data-assets/ # Data Asset definitions (16 files, 115-127, 129, 134-135)
|
||||
15-input/ # Enhanced Input System (1 file, 128)
|
||||
16-state/ # State Management (2 files, 130-131)
|
||||
10-adaptive/ # Adaptive Environment, Atmosphere & Scare (15 files, 89-101, 132-133)
|
||||
11-meta/ # Achievements, Progression & Meta (2 files, 102-103)
|
||||
12-settings/ # Settings, Accessibility & Platform (2 files, 104-105)
|
||||
13-polish/ # Tutorial, Loading, Credits, Debug (9 files, 106-114)
|
||||
14-data-assets/ # Data Asset definitions (16 files, 115-127, 129, 134-135)
|
||||
15-input/ # Enhanced Input System (1 file, 128)
|
||||
16-state/ # State Management (2 files, 130-131)
|
||||
17-capture/ # Planar Capture System (12 files, 136-147)
|
||||
developer/ # Developer Reference Docs (NEW)
|
||||
INDEX.md # Master developer docs index (11 docs)
|
||||
INDEX.md # Master developer docs index (12 docs)
|
||||
architecture-overview.md # Framework-wide architecture walkthrough
|
||||
implementation-patterns.md # Common UE5 Blueprint patterns used
|
||||
project-setup-migration.md # Project setup & migration guide (NEW — Project Settings, plugins, init sequence)
|
||||
01-core-foundation.md # Foundation systems explained
|
||||
02-player-systems.md # Player state & embodiment explained
|
||||
03-interaction-systems.md # Interaction & world manipulation explained
|
||||
@@ -93,16 +123,17 @@ docs/
|
||||
09-ai-systems.md # AI, perception & encounters explained
|
||||
10-adaptive-systems.md # Adaptive environment & atmosphere explained
|
||||
11-16-systems.md # Meta, Settings, Polish, Data Assets, Input, State explained
|
||||
architecture/ # Architecture & Design Documents (8 files)
|
||||
bpc-statemanager.md # BPC_StateManager full spec + Chooser Table Audit
|
||||
metasounds-audio-system.md # MetaSounds audio architecture (mix buses, room zones, settings)
|
||||
animation-catalog.md # Animation requirements for all 135 systems
|
||||
sound-catalog.md # Sound/audio requirements for all 135 systems
|
||||
enhanced-input-system.md # Enhanced Input System architecture
|
||||
hud-overview.md # HUD system architecture — 14 widgets, wiring, integration points
|
||||
multiplayer-networking.md # Multiplayer networking architecture — authority model, RPCs, prediction
|
||||
blueprint-limitations-workarounds.md # UE5 C++-only functions + 100% Blueprint workarounds (NEW)
|
||||
CLEAN_SLATE_PLAN.md # Clean slate refactoring plan
|
||||
architecture/ # Architecture & Design Documents (9 files)
|
||||
bpc-statemanager.md # BPC_StateManager full spec + Chooser Table Audit
|
||||
metasounds-audio-system.md # MetaSounds audio architecture (mix buses, room zones, settings)
|
||||
animation-catalog.md # Animation requirements for all 135 systems
|
||||
sound-catalog.md # Sound/audio requirements for all 135 systems
|
||||
enhanced-input-system.md # Enhanced Input System architecture
|
||||
hud-overview.md # HUD system architecture — 14 widgets, wiring, integration points
|
||||
multiplayer-networking.md # Multiplayer networking architecture — authority model, RPCs, prediction
|
||||
blueprint-limitations-workarounds.md # UE5 C++-only functions + 100% Blueprint workarounds (NEW)
|
||||
planar-capture-system.md # Planar Capture System — mirrors, portals, monitors, horror surfaces (Phase 17)
|
||||
CLEAN_SLATE_PLAN.md # Clean slate refactoring plan
|
||||
reports/ # Condensed audit reports
|
||||
blueprint_condensed_summary.md # ASK agent audit of all 129 files
|
||||
checklists/ # Implementation checklists
|
||||
@@ -110,7 +141,7 @@ docs/
|
||||
enhanced-input-system.md
|
||||
bpc-statemanager.md # NEW — State Manager implementation checklist
|
||||
```
|
||||
**Total: 135 numbered Blueprint files + 5 enums + 5 Data Assets + TEMPLATE.md + AUDIT_REPORT.md + INDEX.md + 11 developer docs + 8 architecture docs + 1 audit report = 166 files in 18 directory groups**
|
||||
**Total: 149 numbered Blueprint files + 2 supplementary + 5 enums + 5 Data Assets + 11 Data Table CSVs + TEMPLATE.md + AUDIT_REPORT.md + INDEX.md + 13 developer docs + 9 architecture docs + 1 audit report + 1 haptics game example = 196 files in 20 directory groups**
|
||||
|
||||
## Naming Conventions
|
||||
| Prefix | Type |
|
||||
@@ -150,7 +181,7 @@ docs/
|
||||
15. **Server-Authoritative Replication** — All state mutations gated by `HasAuthority()`. Clients request via `Server_` RPCs; servers validate and replicate via `RepNotify`. See [`multiplayer-networking.md`](docs/architecture/multiplayer-networking.md).
|
||||
|
||||
## Build Order (Dependency-Safe)
|
||||
- **Phase 0:** Foundation + Input (01-core, 15-input — 8 systems)
|
||||
- **Phase 0:** Foundation + Input (01-core + 15-input — 9 systems — includes BPC_PlatformServiceAbstraction 150)
|
||||
- **Phase 1:** Player Core (02-player, 8 systems)
|
||||
- **Phase 2:** Interaction (03-interaction, 8 systems)
|
||||
- **Phase 3:** Inventory (04-inventory, 11 systems)
|
||||
@@ -162,11 +193,14 @@ docs/
|
||||
- **Phase 9:** Adaptive & Atmosphere (10-adaptive, 15 systems)
|
||||
- **Phase 10:** Data Assets (14-data-assets, 16 systems)
|
||||
- **Phase 11:** Meta/Progression (11-meta, 2 systems)
|
||||
- **Phase 12:** Settings/Platform (12-settings, 2 systems)
|
||||
- **Phase 12:** Settings/Platform (12-settings, 5 systems — includes BPC_HapticsController 148, BPC_RenderPipelineManager 149)
|
||||
- **Phase 13:** Polish (13-polish, 9 systems)
|
||||
- **Phase 14:** State Management (BPC_StateManager + 4 enums + 3 structs + DA_StateGatingTable — 130, 131)
|
||||
- **Phase 15:** MetaSounds Audio (SS_AudioManager + BP_RoomAudioZone + DA_AudioSettings + DA_RoomAcousticPreset — 132-135)
|
||||
- **Phase 16:** Multiplayer Networking — All systems updated with `HasAuthority()` gates, `Server_` RPCs, `RepNotify` handlers, and client prediction patterns. See [`docs/architecture/multiplayer-networking.md`](docs/architecture/multiplayer-networking.md).
|
||||
- **Phase 17:** Planar Capture System — Mirrors, portals, monitors, horror surfaces. C++ core (136-138) + Blueprint actors (139-143) + Material/MPC (144-145) + Data Assets (146-147). See [`docs/architecture/planar-capture-system.md`](docs/architecture/planar-capture-system.md).
|
||||
- **Phase 18:** Haptics Controller — BPC_HapticsController (148) for GameplayTag-driven force feedback, DualSense adaptive triggers, heartbeat pulse. See [`docs/blueprints/12-settings/148_BPC_HapticsController.md`](docs/blueprints/12-settings/148_BPC_HapticsController.md).
|
||||
- **Phase 19:** Render Pipeline Manager — BPC_RenderPipelineManager (149) + DA_RenderPipelineProfile for per-platform quality presets, Lumen/Baked switching, upscaling (DLSS/FSR/PSSR), Nanite/LOD. Planar capture system integration. See [`docs/blueprints/12-settings/149_BPC_RenderPipelineManager.md`](docs/blueprints/12-settings/149_BPC_RenderPipelineManager.md) and [`docs/developer/platform-render-profiles.md`](docs/developer/platform-render-profiles.md).
|
||||
|
||||
## Key Communication Rules
|
||||
1. Gameplay Tags + Subsystem lookup (global, decoupled)
|
||||
@@ -222,4 +256,6 @@ No PR is accepted without these files being current. This ensures the animator,
|
||||
- **Phase 8-13:** COMPLETE — AUDIT_REPORT updated, CONTEXT.md updated, verification, final commit
|
||||
- **Phase 14-15:** COMPLETE — State Management (130-131) + MetaSounds Audio (132-135) blueprint specs created
|
||||
- **Phase 16:** IN PROGRESS — Multiplayer Networking: all 135 blueprint specs being updated with replication stubs, RPC definitions, and server-authoritative patterns. Developer docs updated. Architecture doc created at [`docs/architecture/multiplayer-networking.md`](docs/architecture/multiplayer-networking.md)
|
||||
- **Remaining:** Cross-reference pass across all 135 files; deprecated BPC_AudioAtmosphereController (95) references to update
|
||||
- **Phase 18:** COMPLETE — Haptics Controller (148) blueprint spec created with full Manual Implementation Guide, DA_HapticProfile (121) enhanced, game example (`docs/game/haptics-example.md`) created. All developer docs and indexes updated.
|
||||
- **Phase 19:** COMPLETE — Render Pipeline Manager (149) + DA_RenderPipelineProfile created. Quality preset system with Lumen/Baked switching, per-platform profiles (PS5/PS4/Xbox/Switch/PC/SteamDeck), upscaling integration (DLSS/FSR/PSSR/TSR). Platform render profiles developer guide created. PlanarCapture system updated for pipeline compatibility. All docs updated.
|
||||
- **Remaining:** Cross-reference pass across all 149 files; deprecated BPC_AudioAtmosphereController (95) references to update; Planar Capture System (Phase 17) C++ written, blueprint specs created, pending UE5 editor compile and BP child creation
|
||||
153
README.md
Normal file
153
README.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# UE5 Modular Game Framework
|
||||
|
||||
**A complete Blueprint architecture for first-person narrative horror games — and beyond.**
|
||||
|
||||
This is a production-ready, modular game framework for Unreal Engine 5.5–5.7. It provides 147 self-contained Blueprint systems across 17 categories, plus a fully documented example game prototype ("Project Void"). Built Blueprint-first with 27 supporting C++ classes for performance-critical paths.
|
||||
|
||||
---
|
||||
|
||||
## What This Framework Covers
|
||||
|
||||
### Foundation
|
||||
Game Instance kernel, Core GameMode, GameState, PlayerState, Function Library, Macro Library, GameplayTag registry (334 tags across 11 categories), and a unified C++/Blueprint interface library.
|
||||
|
||||
### Player Simulation
|
||||
Health & damage resistance, Stamina with exhaustion states, Psychological Stress with 5 tiers (calm → catatonic), GASP-integrated Movement & Posture, Hiding & peek system, First-person Embodiment with arm IK, Camera state layers (FOV/offset per action), and Player Metrics tracking (accuracy, playstyle).
|
||||
|
||||
### Interaction & World
|
||||
Raycast-based Interaction Detector, Physical Doors (open/close/lock/barricade), Puzzle Devices with state machines, Contextual Traversal (vault/mantle/slide via Motion Warping), Physics Drag (grab & throw objects), Usable World Objects (levers, valves, buttons), Diegetic In-World Displays (wristwatch, monitors).
|
||||
|
||||
### Inventory & Items
|
||||
Grid-based Inventory with weight management, World Pickup actors with soft-reference mesh loading, Equipment Slots (weapon, tool, armor), Active Item quick-slots, Consumable use system, Item Combination crafting, Container inventories (chests, desks), Key Item tracking with loss protection, Document/Journal archive, and Collectible tracking with set bonuses.
|
||||
|
||||
### Weapons & Combat
|
||||
Weapon Base with fire/holster/FX, Frame-driven Hitscan Firearms, Pellet-spread Shotguns, Melee combos with hit detection, Ammo pools with magazine management, Tactical & empty Reload, Recoil with recovery pattern, Damage Reception pipeline (resistance → shield → health), Hit Reactions (flinch/stagger/knockdown), Combat Feedback (hit markers, kill confirm), Death Cause tracking.
|
||||
|
||||
### AI & Enemies
|
||||
Enemy Base character, AI Controller with Behavior Tree runner, State Machine (Patrol/Search/Combat/Flee), Alert System (suspicious → alerted → combat), Perception (sight, hearing, damage sense), Memory (last known locations, investigation), Behavior Variant selection (weighted random profiles), Patrol Path splines, Procedural Encounter spawners.
|
||||
|
||||
### Narrative & Dialogue
|
||||
Narrative State Machine with chapter flags, Objective tracking (activate/complete/fail/branch), Dialogue Playback with VO & subtitles, Dialogue Choices with consequence branching, Cutscene Bridge with skip support, Lore Unlock system, Narrative Trigger Volumes, and a multi-factor Ending Accumulator.
|
||||
|
||||
### Menus & HUD (14 widgets)
|
||||
Main Menu, Pause Menu, Settings Menu, Inventory Grid UI, Journal/Document Viewer, Diegetic HUD Frame (health, stress, stamina, compass), Interaction Prompt with hold progress, Notification Toasts, Objective Display, Screen Effect Controller (damage vignette, stress blur), Accessibility UI (subtitles, colorblind), Credits Screen, Splash Screen, Debug Menu. All coordinated by a menu-stack Subsystem.
|
||||
|
||||
### Save & Persistence
|
||||
Save Manager Subsystem (auto-save, manual save, save slots), Checkpoint actors, Death Handling (normal death → respawn OR void-space death), Alt Death Space system, Persistent Corpses, World State Recorder (doors, items, enemies), Player Respawn with state restoration, Run History tracker, and `I_Persistable` interface for any actor.
|
||||
|
||||
### Adaptive Environment & Atmosphere
|
||||
Dynamic Difficulty (metric-based scaling), Atmosphere State Controller (room tone, mood), Procedural Encounter generation, Adaptive Environment Director (room mutations, reality shifts), Light Events (flicker, dim, strobe, blackout), Memory Drift (visual/audio hallucinations tied to stress), Pacing Director (intensity bands: exploration → tension → combat → recovery), Playstyle Classifier (aggressive vs stealthy), Rare Events, Scare Events (anticipation → payoff → recovery).
|
||||
|
||||
### MetaSounds Audio
|
||||
Single-entry-point Audio Manager Subsystem with 4 mix buses (SFX, Ambience, Music, Dialogue), Room Audio Zones that auto-switch reverb/occlusion on overlap, 8 gameplay-driven float parameters (heart rate, stress, fear, tension), and 7 acoustic presets (Outdoor, Small Room, Large Hall, Cave, Cathedral, Void, Padded Cell).
|
||||
|
||||
### Enhanced Input
|
||||
Centralized Input Manager Subsystem with stack-based context switching, 5 priority-layered contexts (Default < Hiding < WristwatchUI < Inspection < UI), 30+ Input Actions, platform profiles (PC, Xbox, PS5), key rebinding with persistence, and input mode coordination with the UI layer.
|
||||
|
||||
### Multiplayer Networking
|
||||
Server-authoritative replication model. All state mutations gated by `HasAuthority()`. Client→Server RPCs for requests, Server→Client `RepNotify` for state sync. Client-side prediction for cosmetic effects. Anti-cheat validation on all server-side state changes.
|
||||
|
||||
### State Management
|
||||
Central State Manager (42 exclusive action states + 18 overlay states). All systems query `IsActionPermitted(Tag)` instead of checking other systems directly. Gating rules configured in a designer-editable Data Asset (no Blueprint changes needed). Force Stack pattern for death/cutscenes/void space. GASP liaison for movement→action state mapping. Vital signal tracking (Normal → Critical).
|
||||
|
||||
### Planar Capture System (Phase 17)
|
||||
Unified rendering pipeline for mirrors, portals, monitors, and horror surfaces. C++ core with Blueprint designer-interface. World Subsystem quality budget manager with 5-tier automatic quality scaling (Off → Low/256px/4fps → Medium/512px/15fps → High/1024px/30fps → Hero/2048px/60fps). Horror features: wrong-reflection actor swap, delayed frame ring buffer, 10-parameter MPC steam/dirt/darkness/text reveal material system. Render target pool with automatic reuse. Blueprint children: BP_Mirror, BP_Portal, BP_Monitor, BP_HorrorMirror, BP_FakeWindow. Integrates with scare events, audio manager, and state manager.
|
||||
|
||||
### Polish & Tooling
|
||||
Tutorial system with contextual triggers, Loading screen with tips & progress, Credits screen, Analytics tracker (opt-in), Developer Cheat Manager (god mode, noclip, teleport, give item), Error Handler with crash recovery, FPS counter, Debug Menu (state viewer, AI debug, performance, audio).
|
||||
|
||||
---
|
||||
|
||||
## Example Game Prototype: "Project Void"
|
||||
|
||||
A complete psychological horror FPS prototype demonstrating every system. Set in an abandoned asylum. All game content lives in `Content/Game/` — never modifying `Content/Framework/`.
|
||||
|
||||
**Features built with this framework:**
|
||||
- 11-level scene flow (Splash → Menu → Asylum Entry/WardA/WardB/Basement/WardensOffice → Void Space → 3 Endings → Credits)
|
||||
- Full player character with 27 components wired together
|
||||
- 18 pickup items (weapons, tools, consumables, keys, documents, collectibles)
|
||||
- 4 held weapons (pistol, shotgun, flashlight, crowbar)
|
||||
- 3 enemy types with full AI (Patient, Orderly, Void Shade)
|
||||
- 4 main objectives + 3 side objectives with branching narrative
|
||||
- 3 endings determined by player choices, collectibles, sanity, and playstyle
|
||||
- 6 atmosphere profiles with room audio zones
|
||||
- 6 scare events with anticipation/payoff/recovery cycles
|
||||
- Void space alternate-reality death system
|
||||
- 8 planar capture surfaces (mirrors, horror mirrors, security monitors, void portals, fake windows) with automatic quality scaling
|
||||
- 18 pickup items (weapons, tools, consumables, keys, documents, collectibles)
|
||||
- 4 held weapons (pistol, shotgun, flashlight, crowbar)
|
||||
- 3 enemy types with full AI (Patient, Orderly, Void Shade)
|
||||
- 4 main objectives + 3 side objectives with branching narrative
|
||||
- 3 endings determined by player choices, collectibles, sanity, and playstyle
|
||||
- 6 atmosphere profiles with room audio zones
|
||||
- 6 scare events with anticipation/payoff/recovery cycles
|
||||
- Void space alternate-reality death system
|
||||
|
||||
---
|
||||
|
||||
## Where to Find Things
|
||||
|
||||
| You want to... | Go here |
|
||||
|---------------|---------|
|
||||
| **Understand the whole framework** | [`UE5_Modular_Game_Framework.md`](UE5_Modular_Game_Framework.md) — the full design document |
|
||||
| **See every Blueprint spec** | [`docs/blueprints/INDEX.md`](docs/blueprints/INDEX.md) — 135 system specs with wiring guides |
|
||||
| **Learn the architecture** | [`docs/developer/architecture-overview.md`](docs/developer/architecture-overview.md) — layers, communication patterns |
|
||||
| **Set up a project from scratch** | [`docs/developer/project-setup-migration.md`](docs/developer/project-setup-migration.md) |
|
||||
| **Build the example game** | [`docs/game/GAMEINDEX.md`](docs/game/GAMEINDEX.md) — 170+ assets, 21-phase build order |
|
||||
| **Read item build walkthroughs** | [`docs/game/item-flashlight.md`](docs/game/item-flashlight.md) — step-by-step Blueprint wiring |
|
||||
| **Check animation requirements** | [`docs/architecture/animation-catalog.md`](docs/architecture/animation-catalog.md) |
|
||||
| **Check sound/audio triggers** | [`docs/architecture/sound-catalog.md`](docs/architecture/sound-catalog.md) |
|
||||
| **Understand input system** | [`docs/architecture/enhanced-input-system.md`](docs/architecture/enhanced-input-system.md) |
|
||||
| **Understand state management** | [`docs/architecture/bpc-statemanager.md`](docs/architecture/bpc-statemanager.md) |
|
||||
| **Understand multiplayer** | [`docs/architecture/multiplayer-networking.md`](docs/architecture/multiplayer-networking.md) |
|
||||
| **Find Blueprint-only workarounds** | [`docs/architecture/blueprint-limitations-workarounds.md`](docs/architecture/blueprint-limitations-workarounds.md) |
|
||||
| **See the HUD architecture** | [`docs/architecture/hud-overview.md`](docs/architecture/hud-overview.md) |
|
||||
| **See the audio architecture** | [`docs/architecture/metasounds-audio-system.md`](docs/architecture/metasounds-audio-system.md) |
|
||||
| **See the planar capture system** | [`docs/architecture/planar-capture-system.md`](docs/architecture/planar-capture-system.md) |
|
||||
| **See C++/BP implementation status** | [`docs/checklists/cpp-blueprint-status.md`](docs/checklists/cpp-blueprint-status.md) |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Engine:** Unreal Engine 5.5–5.7
|
||||
- **Scripting:** Blueprint-Only (with 27 C++ support classes)
|
||||
- **Locomotion:** GASP (Ground Animation Strafing Platform) — read-only, extended via notifies
|
||||
- **Input:** Enhanced Input System — 30+ Input Actions, stack-based context switching
|
||||
- **Assets:** `UPrimaryDataAsset` for all content definitions
|
||||
- **State:** Central `BPC_StateManager` — 42 exclusive states, 18 overlay states, vital signals
|
||||
- **Animation:** GASP Motion Matching + 14 animation notifies
|
||||
- **Audio:** MetaSounds — 4 mix buses, room acoustic zones, gameplay-driven float parameters
|
||||
- **Networking:** Server-authoritative with client prediction
|
||||
|
||||
---
|
||||
|
||||
## By the Numbers
|
||||
|
||||
| What | How Many |
|
||||
|------|:--------:|
|
||||
| Blueprint system specs | 147 |
|
||||
| C++ classes | 27 |
|
||||
| Widget Blueprints | 14 |
|
||||
| Blueprint Components | 80 |
|
||||
| Blueprint Actors | 16 |
|
||||
| Data Assets | 19 |
|
||||
| Game Instance Subsystems | 7 |
|
||||
| World Subsystems | 1 |
|
||||
| Materials | 1 |
|
||||
| Material Instances | 7 |
|
||||
| Material Parameter Collections | 1 |
|
||||
| Interfaces | 3 |
|
||||
| Gameplay Tags | 334 |
|
||||
| Input Actions | 30+ |
|
||||
| Animation Notifies | 14 |
|
||||
| Architecture docs | 9 |
|
||||
| Developer docs | 13 |
|
||||
| Game prototype docs | 22 |
|
||||
| Example game assets | 170+ |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
*UE5 Modular Game Framework — Build faster. Stay modular. Never hardcode.*
|
||||
15
Source/PG_Framework.Target.cs
Normal file
15
Source/PG_Framework.Target.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// // Copyright Ngonart OU. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class PG_FrameworkTarget : TargetRules
|
||||
{
|
||||
public PG_FrameworkTarget(TargetInfo Target) : base(Target)
|
||||
{
|
||||
Type = TargetType.Game;
|
||||
DefaultBuildSettings = BuildSettingsVersion.V6;
|
||||
|
||||
ExtraModuleNames.AddRange( new string[] { "PG_Framework" } );
|
||||
}
|
||||
}
|
||||
46
Source/PG_Framework/PG_Framework.Build.cs
Normal file
46
Source/PG_Framework/PG_Framework.Build.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
// // Copyright Ngonart OU. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class PG_Framework : ModuleRules
|
||||
{
|
||||
public PG_Framework(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Renderer",
|
||||
"RenderCore",
|
||||
"GameplayTags",
|
||||
"EnhancedInput",
|
||||
"InputCore",
|
||||
"UMG",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"AIModule",
|
||||
"NavigationSystem",
|
||||
"MotionWarping",
|
||||
"PhysicsCore",
|
||||
"DeveloperSettings",
|
||||
"MetasoundEngine",
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"GameplayTasks",
|
||||
});
|
||||
|
||||
|
||||
// Uncomment if you are using Slate UI
|
||||
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||
|
||||
// Uncomment if you are using online features
|
||||
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
|
||||
|
||||
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
|
||||
}
|
||||
}
|
||||
6
Source/PG_Framework/PG_Framework.cpp
Normal file
6
Source/PG_Framework/PG_Framework.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
// // Copyright Ngonart OU. All Rights Reserved.
|
||||
|
||||
#include "PG_Framework.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, PG_Framework, "PG_Framework" );
|
||||
7
Source/PG_Framework/PG_Framework.h
Normal file
7
Source/PG_Framework/PG_Framework.h
Normal file
@@ -0,0 +1,7 @@
|
||||
// // Copyright Ngonart OU. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Capture/PlanarCaptureCommon.h"
|
||||
|
||||
599
Source/PG_Framework/Private/Capture/BPC_PlanarCapture.cpp
Normal file
599
Source/PG_Framework/Private/Capture/BPC_PlanarCapture.cpp
Normal file
@@ -0,0 +1,599 @@
|
||||
// Copyright Ngonart OU. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BPC_PlanarCapture implementation
|
||||
|
||||
#include "Capture/BPC_PlanarCapture.h"
|
||||
#include "Capture/BP_PlanarCaptureActor.h"
|
||||
#include "Capture/SS_PlanarCaptureManager.h"
|
||||
#include "Capture/PlanarCaptureCameraUtils.h"
|
||||
#include "Components/SceneCaptureComponent2D.h"
|
||||
#include "Engine/TextureRenderTarget2D.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Materials/MaterialParameterCollection.h"
|
||||
#include "Materials/MaterialParameterCollectionInstance.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
UBPC_PlanarCapture::UBPC_PlanarCapture()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.bStartWithTickEnabled = true;
|
||||
PrimaryComponentTick.TickInterval = 0.0f; // We throttle internally via TimeSinceLastCapture
|
||||
|
||||
// Initialize default quality profiles
|
||||
QualityProfiles.SetNum(4);
|
||||
|
||||
// Low — 256px, 4fps
|
||||
QualityProfiles[0].RenderTargetSize = 256;
|
||||
QualityProfiles[0].CaptureInterval = 0.25f;
|
||||
QualityProfiles[0].bEnableShadows = false;
|
||||
QualityProfiles[0].bEnablePostProcess = false;
|
||||
QualityProfiles[0].bEnableFog = false;
|
||||
QualityProfiles[0].bEnableBloom = false;
|
||||
QualityProfiles[0].bEnableAO = false;
|
||||
QualityProfiles[0].bEnableLumen = false;
|
||||
QualityProfiles[0].bEnableMotionBlur = false;
|
||||
QualityProfiles[0].bEnableClipPlane = false;
|
||||
|
||||
// Medium — 512px, 15fps
|
||||
QualityProfiles[1].RenderTargetSize = 512;
|
||||
QualityProfiles[1].CaptureInterval = 0.0667f;
|
||||
QualityProfiles[1].bEnableShadows = true;
|
||||
QualityProfiles[1].bEnablePostProcess = false;
|
||||
QualityProfiles[1].bEnableFog = false;
|
||||
QualityProfiles[1].bEnableBloom = false;
|
||||
QualityProfiles[1].bEnableAO = false;
|
||||
QualityProfiles[1].bEnableLumen = false;
|
||||
QualityProfiles[1].bEnableMotionBlur = false;
|
||||
QualityProfiles[1].bEnableClipPlane = true;
|
||||
|
||||
// High — 1024px, 30fps
|
||||
QualityProfiles[2].RenderTargetSize = 1024;
|
||||
QualityProfiles[2].CaptureInterval = 0.0333f;
|
||||
QualityProfiles[2].bEnableShadows = true;
|
||||
QualityProfiles[2].bEnablePostProcess = true;
|
||||
QualityProfiles[2].bEnableFog = true;
|
||||
QualityProfiles[2].bEnableBloom = false;
|
||||
QualityProfiles[2].bEnableAO = true;
|
||||
QualityProfiles[2].bEnableLumen = true;
|
||||
QualityProfiles[2].bEnableMotionBlur = false;
|
||||
QualityProfiles[2].bEnableClipPlane = true;
|
||||
|
||||
// Hero — 2048px, 60fps
|
||||
QualityProfiles[3].RenderTargetSize = 2048;
|
||||
QualityProfiles[3].CaptureInterval = 0.0167f;
|
||||
QualityProfiles[3].bEnableShadows = true;
|
||||
QualityProfiles[3].bEnablePostProcess = true;
|
||||
QualityProfiles[3].bEnableFog = true;
|
||||
QualityProfiles[3].bEnableBloom = true;
|
||||
QualityProfiles[3].bEnableAO = true;
|
||||
QualityProfiles[3].bEnableLumen = true;
|
||||
QualityProfiles[3].bEnableMotionBlur = true;
|
||||
QualityProfiles[3].bEnableClipPlane = true;
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
CachedOwningActor = Cast<ABP_PlanarCaptureActor>(GetOwner());
|
||||
if (!CachedOwningActor)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("BPC_PlanarCapture: Owner is not a BP_PlanarCaptureActor! Capture disabled."));
|
||||
SetComponentTickEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache manager reference
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
CachedManager = World->GetSubsystem<USS_PlanarCaptureManager>();
|
||||
}
|
||||
|
||||
ResolveSoftReferences();
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
ShutdownCapture();
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
if (!bIsCapturing || !SceneCapture || CurrentQualityTier == EPlanarCaptureQualityTier::Off)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSinceLastCapture += DeltaTime;
|
||||
|
||||
if (TimeSinceLastCapture >= ActiveProfile.CaptureInterval)
|
||||
{
|
||||
TimeSinceLastCapture = 0.0f;
|
||||
|
||||
// Get viewer camera for transform computation
|
||||
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
|
||||
if (!PC || !PC->PlayerCameraManager)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FTransform ViewerTransform = FTransform(
|
||||
PC->PlayerCameraManager->GetCameraRotation(),
|
||||
PC->PlayerCameraManager->GetCameraLocation()
|
||||
);
|
||||
|
||||
// Compute capture camera position
|
||||
const FTransform CaptureTransform = ComputeCaptureCameraTransform(ViewerTransform);
|
||||
SceneCapture->SetWorldTransform(CaptureTransform);
|
||||
|
||||
// Capture the scene
|
||||
SceneCapture->CaptureScene();
|
||||
|
||||
// Push MPC parameters
|
||||
if (CachedOwningActor && CachedOwningActor->SurfaceMPC)
|
||||
{
|
||||
PushMPCParameters(CachedOwningActor->SurfaceMPC);
|
||||
}
|
||||
|
||||
OnCaptureRendered.Broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Public API
|
||||
// ========================================================================
|
||||
|
||||
EPlanarCaptureInitResult UBPC_PlanarCapture::InitializeCapture()
|
||||
{
|
||||
if (!CachedOwningActor)
|
||||
{
|
||||
return EPlanarCaptureInitResult::InvalidSurfaceMesh;
|
||||
}
|
||||
|
||||
if (CurrentQualityTier == EPlanarCaptureQualityTier::Off)
|
||||
{
|
||||
return EPlanarCaptureInitResult::Success; // Nothing to initialize
|
||||
}
|
||||
|
||||
// Request render target from pool
|
||||
const int32 ProfileIndex = static_cast<int32>(CurrentQualityTier) - 1; // Skip "Off" (0)
|
||||
if (ProfileIndex < 0 || ProfileIndex >= QualityProfiles.Num())
|
||||
{
|
||||
return EPlanarCaptureInitResult::NoRenderTargetPool;
|
||||
}
|
||||
|
||||
ActiveProfile = QualityProfiles[ProfileIndex];
|
||||
|
||||
if (CachedManager)
|
||||
{
|
||||
CaptureRenderTarget = CachedManager->RequestRenderTarget(ActiveProfile.RenderTargetSize);
|
||||
}
|
||||
|
||||
if (!CaptureRenderTarget)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("BPC_PlanarCapture: Failed to allocate render target (%dx%d)"),
|
||||
ActiveProfile.RenderTargetSize, ActiveProfile.RenderTargetSize);
|
||||
return EPlanarCaptureInitResult::NoRenderTargetPool;
|
||||
}
|
||||
|
||||
CreateSceneCaptureComponent();
|
||||
|
||||
bIsCapturing = true;
|
||||
OnCaptureInitialized.Broadcast(EPlanarCaptureInitResult::Success);
|
||||
return EPlanarCaptureInitResult::Success;
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::ShutdownCapture()
|
||||
{
|
||||
bIsCapturing = false;
|
||||
|
||||
if (SceneCapture)
|
||||
{
|
||||
SceneCapture->DestroyComponent();
|
||||
SceneCapture = nullptr;
|
||||
}
|
||||
|
||||
if (CaptureRenderTarget && CachedManager)
|
||||
{
|
||||
CachedManager->ReleaseRenderTarget(CaptureRenderTarget);
|
||||
CaptureRenderTarget = nullptr;
|
||||
}
|
||||
|
||||
// Clean up frame ring buffer
|
||||
for (UTextureRenderTarget2D* RT : FrameRingBuffer)
|
||||
{
|
||||
if (RT && CachedManager)
|
||||
{
|
||||
CachedManager->ReleaseRenderTarget(RT);
|
||||
}
|
||||
}
|
||||
FrameRingBuffer.Empty();
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::ApplyQualityTier(EPlanarCaptureQualityTier Tier)
|
||||
{
|
||||
const EPlanarCaptureQualityTier OldTier = CurrentQualityTier;
|
||||
CurrentQualityTier = Tier;
|
||||
|
||||
if (Tier == EPlanarCaptureQualityTier::Off)
|
||||
{
|
||||
ShutdownCapture();
|
||||
OnCaptureQualityChanged.Broadcast(OldTier, Tier);
|
||||
return;
|
||||
}
|
||||
|
||||
// If transitioning from Off to active, initialize
|
||||
if (OldTier == EPlanarCaptureQualityTier::Off)
|
||||
{
|
||||
InitializeCapture();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update active profile and resize render target if needed
|
||||
const int32 ProfileIndex = static_cast<int32>(Tier) - 1;
|
||||
if (ProfileIndex >= 0 && ProfileIndex < QualityProfiles.Num())
|
||||
{
|
||||
ActiveProfile = QualityProfiles[ProfileIndex];
|
||||
ApplyShowFlags();
|
||||
}
|
||||
}
|
||||
|
||||
OnCaptureQualityChanged.Broadcast(OldTier, Tier);
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::CaptureNow()
|
||||
{
|
||||
if (!SceneCapture)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
|
||||
if (!PC || !PC->PlayerCameraManager)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FTransform ViewerTransform = FTransform(
|
||||
PC->PlayerCameraManager->GetCameraRotation(),
|
||||
PC->PlayerCameraManager->GetCameraLocation()
|
||||
);
|
||||
|
||||
const FTransform CaptureTransform = ComputeCaptureCameraTransform(ViewerTransform);
|
||||
SceneCapture->SetWorldTransform(CaptureTransform);
|
||||
SceneCapture->CaptureScene();
|
||||
|
||||
TimeSinceLastCapture = 0.0f;
|
||||
OnCaptureRendered.Broadcast();
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::ActivateHorrorReflection()
|
||||
{
|
||||
// Save current ShowOnly list
|
||||
SavedShowOnlyActors.Empty();
|
||||
for (const FPlanarCaptureActorListEntry& Entry : ShowOnlyActors)
|
||||
{
|
||||
SavedShowOnlyActors.Add(Entry.Actor);
|
||||
}
|
||||
|
||||
// Clear and set wrong reflection actor
|
||||
ShowOnlyActors.Empty();
|
||||
if (WrongReflectionActor.IsValid())
|
||||
{
|
||||
FPlanarCaptureActorListEntry NewEntry;
|
||||
NewEntry.Actor = WrongReflectionActor;
|
||||
NewEntry.bActive = true;
|
||||
ShowOnlyActors.Add(NewEntry);
|
||||
}
|
||||
|
||||
UpdateActorLists();
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::DeactivateHorrorReflection()
|
||||
{
|
||||
ShowOnlyActors.Empty();
|
||||
for (const TSoftObjectPtr<AActor>& SavedActor : SavedShowOnlyActors)
|
||||
{
|
||||
FPlanarCaptureActorListEntry NewEntry;
|
||||
NewEntry.Actor = SavedActor;
|
||||
NewEntry.bActive = true;
|
||||
ShowOnlyActors.Add(NewEntry);
|
||||
}
|
||||
|
||||
SavedShowOnlyActors.Empty();
|
||||
UpdateActorLists();
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::PushDelayedFrame()
|
||||
{
|
||||
if (ActiveProfile.DelayedFrameCount <= 0 || !CaptureRenderTarget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure ring buffer is properly sized
|
||||
while (FrameRingBuffer.Num() < ActiveProfile.DelayedFrameCount)
|
||||
{
|
||||
UTextureRenderTarget2D* NewRT = nullptr;
|
||||
if (CachedManager)
|
||||
{
|
||||
NewRT = CachedManager->RequestRenderTarget(ActiveProfile.RenderTargetSize);
|
||||
}
|
||||
FrameRingBuffer.Add(NewRT);
|
||||
}
|
||||
|
||||
// Copy current render target to ring buffer slot
|
||||
RingBufferWriteIndex = (RingBufferWriteIndex + 1) % FrameRingBuffer.Num();
|
||||
|
||||
// Push the oldest frame to the material as the delayed reflection
|
||||
if (FrameRingBuffer.IsValidIndex(RingBufferWriteIndex) && FrameRingBuffer[RingBufferWriteIndex])
|
||||
{
|
||||
// The material samples the delayed frame via the MPC texture parameter
|
||||
// This is set in PushMPCParameters
|
||||
}
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::SetScriptedPriority(float Priority)
|
||||
{
|
||||
ScriptedPriorityOverride = FMath::Clamp(Priority, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::PushMPCParameters(UMaterialParameterCollection* MPC)
|
||||
{
|
||||
if (!MPC)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UMaterialParameterCollectionInstance* MPCInstance = GetWorld()->GetParameterCollectionInstance(MPC);
|
||||
if (!MPCInstance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Push standard parameters
|
||||
// These are used by M_CaptureSurface_Master to drive steam, dirt, horror effects
|
||||
MPCInstance->SetScalarParameterValue(FName("SteamIntensity"), 0.0f);
|
||||
MPCInstance->SetScalarParameterValue(FName("DirtOpacity"), 0.0f);
|
||||
MPCInstance->SetScalarParameterValue(FName("CondensationFlow"), 0.0f);
|
||||
MPCInstance->SetScalarParameterValue(FName("DistortionAmplitude"), 0.0f);
|
||||
MPCInstance->SetScalarParameterValue(FName("MirrorDarkness"), 0.0f);
|
||||
MPCInstance->SetScalarParameterValue(FName("WrongReflectionBlend"), 0.0f);
|
||||
MPCInstance->SetScalarParameterValue(FName("TextRevealProgress"), 0.0f);
|
||||
MPCInstance->SetScalarParameterValue(FName("SteamEmissiveIntensity"), 0.0f);
|
||||
MPCInstance->SetScalarParameterValue(FName("DelayedReflectionBlend"), 0.0f);
|
||||
MPCInstance->SetScalarParameterValue(FName("SurfaceAge"), 0.0f);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Compute
|
||||
// ========================================================================
|
||||
|
||||
FTransform UBPC_PlanarCapture::ComputeCaptureCameraTransform(const FTransform& ViewerCameraTransform) const
|
||||
{
|
||||
switch (CaptureMode)
|
||||
{
|
||||
case EPlanarCaptureMode::Mirror:
|
||||
case EPlanarCaptureMode::HorrorMirror:
|
||||
{
|
||||
const FTransform SurfaceTransform = CachedOwningActor
|
||||
? CachedOwningActor->GetActorTransform()
|
||||
: FTransform::Identity;
|
||||
return UPlanarCaptureCameraUtils::ComputeMirroredTransform(ViewerCameraTransform, SurfaceTransform);
|
||||
}
|
||||
|
||||
case EPlanarCaptureMode::Portal:
|
||||
case EPlanarCaptureMode::HorrorPortal:
|
||||
{
|
||||
if (LinkedTargetSurface.IsValid())
|
||||
{
|
||||
const FTransform SourceTransform = CachedOwningActor
|
||||
? CachedOwningActor->GetActorTransform()
|
||||
: FTransform::Identity;
|
||||
const FTransform TargetTransform = LinkedTargetSurface->GetActorTransform();
|
||||
return UPlanarCaptureCameraUtils::ComputePortalTransform(
|
||||
ViewerCameraTransform, SourceTransform, TargetTransform);
|
||||
}
|
||||
return ViewerCameraTransform;
|
||||
}
|
||||
|
||||
case EPlanarCaptureMode::Monitor:
|
||||
{
|
||||
if (FixedCameraActor.IsValid())
|
||||
{
|
||||
return FixedCameraActor->GetActorTransform();
|
||||
}
|
||||
return ViewerCameraTransform;
|
||||
}
|
||||
|
||||
case EPlanarCaptureMode::FakeWindow:
|
||||
{
|
||||
// Fake windows use a fixed offset from the surface
|
||||
const FTransform SurfaceTransform = CachedOwningActor
|
||||
? CachedOwningActor->GetActorTransform()
|
||||
: FTransform::Identity;
|
||||
const FVector SurfaceNormal = SurfaceTransform.GetUnitAxis(EAxis::Z);
|
||||
return FTransform(SurfaceTransform.GetRotation(),
|
||||
SurfaceTransform.GetLocation() + SurfaceNormal * 200.0f);
|
||||
}
|
||||
|
||||
default:
|
||||
return ViewerCameraTransform;
|
||||
}
|
||||
}
|
||||
|
||||
FPlanarCaptureScore UBPC_PlanarCapture::GetCurrentScore() const
|
||||
{
|
||||
FPlanarCaptureScore Score;
|
||||
|
||||
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
|
||||
if (!PC || !PC->PlayerCameraManager || !CachedOwningActor)
|
||||
{
|
||||
return Score;
|
||||
}
|
||||
|
||||
const FVector ViewerPos = PC->PlayerCameraManager->GetCameraLocation();
|
||||
const FTransform ViewerTransform = FTransform(
|
||||
PC->PlayerCameraManager->GetCameraRotation(),
|
||||
ViewerPos
|
||||
);
|
||||
|
||||
const FVector SurfacePos = CachedOwningActor->GetActorLocation();
|
||||
const FVector SurfaceNormal = CachedOwningActor->GetActorUpVector();
|
||||
|
||||
Score.DistanceToViewer = FVector::Dist(ViewerPos, SurfacePos);
|
||||
Score.FacingAngle = FVector::DotProduct(
|
||||
PC->PlayerCameraManager->GetCameraRotation().Vector(),
|
||||
SurfaceNormal
|
||||
);
|
||||
|
||||
// Compute screen coverage using bounding box
|
||||
const FBox SurfaceBounds = CachedOwningActor->GetComponentsBoundingBox();
|
||||
Score.ScreenCoverage = UPlanarCaptureCameraUtils::ComputeScreenCoverage(
|
||||
SurfaceBounds, ViewerTransform, CaptureFOV, 1920, 1080);
|
||||
|
||||
Score.bInFrustum = UPlanarCaptureCameraUtils::IsSurfaceVisibleToViewer(
|
||||
SurfaceBounds, ViewerTransform, CaptureFOV, 1.777f, 10.0f, MaxViewDistance);
|
||||
|
||||
Score.ScriptedPriority = ScriptedPriorityOverride;
|
||||
|
||||
Score.CompositeScore = UPlanarCaptureCameraUtils::ComputeCompositeScore(
|
||||
Score.ScreenCoverage, Score.FacingAngle, Score.DistanceToViewer,
|
||||
CachedManager ? CachedManager->MaxCaptureDistance : 10000.0f,
|
||||
Score.ScriptedPriority);
|
||||
|
||||
return Score;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Internal Methods
|
||||
// ========================================================================
|
||||
|
||||
void UBPC_PlanarCapture::CreateSceneCaptureComponent()
|
||||
{
|
||||
if (!GetOwner())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SceneCapture = NewObject<USceneCaptureComponent2D>(GetOwner(), USceneCaptureComponent2D::StaticClass());
|
||||
if (!SceneCapture)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SceneCapture->RegisterComponent();
|
||||
SceneCapture->AttachToComponent(GetOwner()->GetRootComponent(),
|
||||
FAttachmentTransformRules::SnapToTargetNotIncludingScale);
|
||||
|
||||
SceneCapture->TextureTarget = CaptureRenderTarget;
|
||||
SceneCapture->FOVAngle = CaptureFOV;
|
||||
SceneCapture->bCaptureEveryFrame = false; // We control capture timing
|
||||
SceneCapture->bCaptureOnMovement = false;
|
||||
|
||||
// Configure for planar surface capture
|
||||
SceneCapture->ProjectionType = ECameraProjectionMode::Perspective;
|
||||
SceneCapture->PrimitiveRenderMode = ESceneCapturePrimitiveRenderMode::PRM_UseShowOnlyList;
|
||||
|
||||
ApplyShowFlags();
|
||||
UpdateActorLists();
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::ApplyShowFlags()
|
||||
{
|
||||
if (!SceneCapture)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply show flags based on active quality profile
|
||||
// UE5.7: FEngineShowFlags members are set directly, not via setter methods
|
||||
if (!ActiveProfile.bEnableShadows)
|
||||
{
|
||||
SceneCapture->ShowFlags.DynamicShadows = false;
|
||||
SceneCapture->ShowFlags.ContactShadows = false;
|
||||
}
|
||||
if (!ActiveProfile.bEnableFog)
|
||||
{
|
||||
SceneCapture->ShowFlags.Fog = 0;
|
||||
}
|
||||
if (!ActiveProfile.bEnableBloom)
|
||||
{
|
||||
SceneCapture->ShowFlags.Bloom = 0;
|
||||
}
|
||||
if (!ActiveProfile.bEnableAO)
|
||||
{
|
||||
SceneCapture->ShowFlags.AmbientOcclusion = 0;
|
||||
SceneCapture->ShowFlags.ScreenSpaceAO = 0;
|
||||
}
|
||||
if (!ActiveProfile.bEnableMotionBlur)
|
||||
{
|
||||
SceneCapture->ShowFlags.MotionBlur = 0;
|
||||
}
|
||||
|
||||
// Lumen is controlled via post-process settings on the capture component
|
||||
SceneCapture->PostProcessSettings.bOverride_DynamicGlobalIlluminationMethod = true;
|
||||
SceneCapture->PostProcessSettings.DynamicGlobalIlluminationMethod = ActiveProfile.bEnableLumen
|
||||
? EDynamicGlobalIlluminationMethod::Lumen
|
||||
: EDynamicGlobalIlluminationMethod::None;
|
||||
|
||||
// Post-process toggle
|
||||
SceneCapture->PostProcessSettings.bOverride_BloomIntensity = !ActiveProfile.bEnablePostProcess;
|
||||
SceneCapture->PostProcessBlendWeight = ActiveProfile.bEnablePostProcess ? 1.0f : 0.0f;
|
||||
|
||||
// Disable Lumen reflections on the capture itself to prevent double-rendering
|
||||
SceneCapture->PostProcessSettings.bOverride_ReflectionMethod = true;
|
||||
SceneCapture->PostProcessSettings.ReflectionMethod = EReflectionMethod::None;
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::UpdateActorLists()
|
||||
{
|
||||
if (!SceneCapture)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SceneCapture->ShowOnlyActors.Empty();
|
||||
SceneCapture->HiddenActors.Empty();
|
||||
|
||||
for (const FPlanarCaptureActorListEntry& Entry : ShowOnlyActors)
|
||||
{
|
||||
if (Entry.bActive && Entry.Actor.IsValid())
|
||||
{
|
||||
SceneCapture->ShowOnlyActors.Add(Entry.Actor.Get());
|
||||
}
|
||||
}
|
||||
|
||||
for (const FPlanarCaptureActorListEntry& Entry : HiddenActors)
|
||||
{
|
||||
if (Entry.bActive && Entry.Actor.IsValid())
|
||||
{
|
||||
SceneCapture->HiddenActors.Add(Entry.Actor.Get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UBPC_PlanarCapture::ResolveSoftReferences()
|
||||
{
|
||||
if (SurfaceMeshComponent.IsValid())
|
||||
{
|
||||
// Surface mesh is resolved — ready for capture
|
||||
}
|
||||
}
|
||||
|
||||
FPlane UBPC_PlanarCapture::GetSurfacePlane() const
|
||||
{
|
||||
if (!CachedOwningActor)
|
||||
{
|
||||
return FPlane(FVector::ZeroVector, FVector::UpVector);
|
||||
}
|
||||
|
||||
const FVector SurfaceLocation = CachedOwningActor->GetActorLocation();
|
||||
const FVector SurfaceNormal = CachedOwningActor->GetActorUpVector();
|
||||
|
||||
return FPlane(SurfaceLocation, SurfaceNormal);
|
||||
}
|
||||
241
Source/PG_Framework/Private/Capture/BP_PlanarCaptureActor.cpp
Normal file
241
Source/PG_Framework/Private/Capture/BP_PlanarCaptureActor.cpp
Normal file
@@ -0,0 +1,241 @@
|
||||
// Copyright Ngonart OU. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BP_PlanarCaptureActor implementation
|
||||
|
||||
#include "Capture/BP_PlanarCaptureActor.h"
|
||||
#include "Capture/BPC_PlanarCapture.h"
|
||||
#include "Capture/SS_PlanarCaptureManager.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "Materials/MaterialInstanceDynamic.h"
|
||||
#include "Materials/MaterialParameterCollection.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
|
||||
ABP_PlanarCaptureActor::ABP_PlanarCaptureActor()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// Create root component
|
||||
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||
SetRootComponent(Root);
|
||||
|
||||
// Surface mesh
|
||||
SurfaceMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SurfaceMesh"));
|
||||
SurfaceMesh->SetupAttachment(Root);
|
||||
SurfaceMesh->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
SurfaceMesh->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
SurfaceMesh->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||
|
||||
// Proximity trigger for quality scoring
|
||||
ProximityTrigger = CreateDefaultSubobject<UBoxComponent>(TEXT("ProximityTrigger"));
|
||||
ProximityTrigger->SetupAttachment(Root);
|
||||
ProximityTrigger->SetBoxExtent(FVector(500.0f, 500.0f, 500.0f));
|
||||
ProximityTrigger->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
ProximityTrigger->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
ProximityTrigger->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
|
||||
|
||||
// Capture component
|
||||
CaptureComponent = CreateDefaultSubobject<UBPC_PlanarCapture>(TEXT("CaptureComponent"));
|
||||
|
||||
// Replication
|
||||
bReplicates = true;
|
||||
bAlwaysRelevant = true;
|
||||
}
|
||||
|
||||
void ABP_PlanarCaptureActor::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Bind overlap events
|
||||
if (ProximityTrigger)
|
||||
{
|
||||
ProximityTrigger->OnComponentBeginOverlap.AddDynamic(this, &ABP_PlanarCaptureActor::OnProximityBeginOverlap);
|
||||
ProximityTrigger->OnComponentEndOverlap.AddDynamic(this, &ABP_PlanarCaptureActor::OnProximityEndOverlap);
|
||||
}
|
||||
|
||||
CreateMaterialInstance();
|
||||
RegisterWithManager();
|
||||
|
||||
if (bStartEnabled)
|
||||
{
|
||||
EnableSurface();
|
||||
}
|
||||
}
|
||||
|
||||
void ABP_PlanarCaptureActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
// Unregister from manager
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
if (USS_PlanarCaptureManager* Manager = World->GetSubsystem<USS_PlanarCaptureManager>())
|
||||
{
|
||||
Manager->UnregisterSurface(this);
|
||||
}
|
||||
}
|
||||
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Public API
|
||||
// ========================================================================
|
||||
|
||||
void ABP_PlanarCaptureActor::EnableSurface()
|
||||
{
|
||||
if (bIsActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bIsActive = true;
|
||||
bRepIsActive = true;
|
||||
|
||||
if (CaptureComponent)
|
||||
{
|
||||
CaptureComponent->InitializeCapture();
|
||||
}
|
||||
}
|
||||
|
||||
void ABP_PlanarCaptureActor::DisableSurface()
|
||||
{
|
||||
if (!bIsActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bIsActive = false;
|
||||
bRepIsActive = false;
|
||||
|
||||
if (CaptureComponent)
|
||||
{
|
||||
CaptureComponent->ShutdownCapture();
|
||||
}
|
||||
}
|
||||
|
||||
void ABP_PlanarCaptureActor::SetCaptureMode(EPlanarCaptureMode NewMode)
|
||||
{
|
||||
if (CaptureComponent)
|
||||
{
|
||||
const bool WasCapturing = CaptureComponent->bIsCapturing;
|
||||
if (WasCapturing)
|
||||
{
|
||||
CaptureComponent->ShutdownCapture();
|
||||
}
|
||||
|
||||
CaptureComponent->CaptureMode = NewMode;
|
||||
|
||||
if (WasCapturing)
|
||||
{
|
||||
CaptureComponent->InitializeCapture();
|
||||
}
|
||||
|
||||
OnModeChanged.Broadcast(NewMode);
|
||||
}
|
||||
}
|
||||
|
||||
void ABP_PlanarCaptureActor::SetSurfaceMaterial(UMaterialInterface* NewMaterial)
|
||||
{
|
||||
if (SurfaceMesh && NewMaterial)
|
||||
{
|
||||
SurfaceMaterialInstance = SurfaceMesh->CreateDynamicMaterialInstance(0, NewMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
void ABP_PlanarCaptureActor::SetSurfaceMPCParameter(FName ParameterName, float Value)
|
||||
{
|
||||
UMaterialInstanceDynamic* MID = SurfaceMaterialInstance;
|
||||
if (!MID)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MID->SetScalarParameterValue(ParameterName, Value);
|
||||
}
|
||||
|
||||
void ABP_PlanarCaptureActor::DestroySurface()
|
||||
{
|
||||
if (!bDestructible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DisableSurface();
|
||||
|
||||
OnSurfaceDestroyed.Broadcast(this);
|
||||
|
||||
// Blueprint child handles visual destruction (particles, sound via SS_AudioManager, etc.)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Overlap Events
|
||||
// ========================================================================
|
||||
|
||||
void ABP_PlanarCaptureActor::OnProximityBeginOverlap(UPrimitiveComponent* OverlappedComponent,
|
||||
AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
|
||||
bool bFromSweep, const FHitResult& SweepResult)
|
||||
{
|
||||
// Player entered proximity — notify capture component for quality scoring
|
||||
if (CaptureComponent && OtherActor && OtherActor->ActorHasTag(FName("Player")))
|
||||
{
|
||||
CaptureComponent->SetScriptedPriority(0.3f); // Slight priority boost when player is near
|
||||
}
|
||||
}
|
||||
|
||||
void ABP_PlanarCaptureActor::OnProximityEndOverlap(UPrimitiveComponent* OverlappedComponent,
|
||||
AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
|
||||
{
|
||||
if (CaptureComponent && OtherActor && OtherActor->ActorHasTag(FName("Player")))
|
||||
{
|
||||
CaptureComponent->SetScriptedPriority(0.0f); // Reset priority
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Replication
|
||||
// ========================================================================
|
||||
|
||||
void ABP_PlanarCaptureActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
DOREPLIFETIME(ABP_PlanarCaptureActor, bRepIsActive);
|
||||
}
|
||||
|
||||
void ABP_PlanarCaptureActor::OnRep_IsActive()
|
||||
{
|
||||
if (bRepIsActive && !bIsActive)
|
||||
{
|
||||
EnableSurface();
|
||||
}
|
||||
else if (!bRepIsActive && bIsActive)
|
||||
{
|
||||
DisableSurface();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Internal
|
||||
// ========================================================================
|
||||
|
||||
void ABP_PlanarCaptureActor::RegisterWithManager()
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
if (!World)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
USS_PlanarCaptureManager* Manager = World->GetSubsystem<USS_PlanarCaptureManager>();
|
||||
if (Manager)
|
||||
{
|
||||
Manager->RegisterSurface(this);
|
||||
}
|
||||
}
|
||||
|
||||
void ABP_PlanarCaptureActor::CreateMaterialInstance()
|
||||
{
|
||||
if (SurfaceMesh && SurfaceMesh->GetMaterial(0))
|
||||
{
|
||||
SurfaceMaterialInstance = SurfaceMesh->CreateDynamicMaterialInstance(0);
|
||||
}
|
||||
}
|
||||
242
Source/PG_Framework/Private/Capture/PlanarCaptureCameraUtils.cpp
Normal file
242
Source/PG_Framework/Private/Capture/PlanarCaptureCameraUtils.cpp
Normal file
@@ -0,0 +1,242 @@
|
||||
// Copyright Ngonart OU. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — PlanarCaptureCameraUtils implementation
|
||||
|
||||
#include "Capture/PlanarCaptureCameraUtils.h"
|
||||
#include "Kismet/KismetMathLibrary.h"
|
||||
|
||||
FTransform UPlanarCaptureCameraUtils::ComputeMirroredTransform(
|
||||
const FTransform& ViewerCameraTransform,
|
||||
const FTransform& MirrorPlaneTransform)
|
||||
{
|
||||
// Mirror plane normal (local Z axis of the mirror surface)
|
||||
const FVector MirrorNormal = MirrorPlaneTransform.GetUnitAxis(EAxis::Z);
|
||||
const FVector MirrorLocation = MirrorPlaneTransform.GetLocation();
|
||||
|
||||
// Reflect viewer position across the mirror plane
|
||||
const FVector ViewerPos = ViewerCameraTransform.GetLocation();
|
||||
const FVector ToViewer = ViewerPos - MirrorLocation;
|
||||
const float DistanceToPlane = FVector::DotProduct(ToViewer, MirrorNormal);
|
||||
const FVector ReflectedPosition = ViewerPos - 2.0f * DistanceToPlane * MirrorNormal;
|
||||
|
||||
// Reflect viewer rotation across the mirror plane
|
||||
const FVector ViewerForward = ViewerCameraTransform.GetUnitAxis(EAxis::X);
|
||||
const FVector ReflectedForward = ViewerForward - 2.0f * FVector::DotProduct(ViewerForward, MirrorNormal) * MirrorNormal;
|
||||
|
||||
const FVector ViewerUp = ViewerCameraTransform.GetUnitAxis(EAxis::Z);
|
||||
const FVector ReflectedUp = ViewerUp - 2.0f * FVector::DotProduct(ViewerUp, MirrorNormal) * MirrorNormal;
|
||||
|
||||
const FRotator ReflectedRotation = FRotationMatrix::MakeFromXZ(ReflectedForward, ReflectedUp).Rotator();
|
||||
|
||||
return FTransform(ReflectedRotation, ReflectedPosition, ViewerCameraTransform.GetScale3D());
|
||||
}
|
||||
|
||||
FTransform UPlanarCaptureCameraUtils::ComputePortalTransform(
|
||||
const FTransform& ViewerCameraTransform,
|
||||
const FTransform& SourceSurfaceTransform,
|
||||
const FTransform& TargetSurfaceTransform)
|
||||
{
|
||||
// Compute viewer position relative to source surface
|
||||
const FVector ViewerPos = ViewerCameraTransform.GetLocation();
|
||||
const FVector RelativePos = SourceSurfaceTransform.InverseTransformPosition(ViewerPos);
|
||||
|
||||
// Compute viewer rotation relative to source surface
|
||||
const FQuat ViewerRot = ViewerCameraTransform.GetRotation();
|
||||
const FQuat SourceRotInv = SourceSurfaceTransform.GetRotation().Inverse();
|
||||
const FQuat RelativeRot = SourceRotInv * ViewerRot;
|
||||
|
||||
// Apply relative transform to target surface
|
||||
const FVector TargetPos = TargetSurfaceTransform.TransformPosition(RelativePos);
|
||||
const FQuat TargetRot = TargetSurfaceTransform.GetRotation() * RelativeRot;
|
||||
|
||||
return FTransform(TargetRot, TargetPos, ViewerCameraTransform.GetScale3D());
|
||||
}
|
||||
|
||||
FMatrix UPlanarCaptureCameraUtils::ComputeObliqueProjectionMatrix(
|
||||
float FOV,
|
||||
float AspectRatio,
|
||||
float NearPlane,
|
||||
float FarPlane,
|
||||
const FPlane& ClipPlane,
|
||||
const FTransform& SurfaceTransform)
|
||||
{
|
||||
// Build standard perspective projection matrix
|
||||
FMatrix ProjectionMatrix;
|
||||
const float HalfFOVRad = FMath::DegreesToRadians(FOV * 0.5f);
|
||||
const float YScale = 1.0f / FMath::Tan(HalfFOVRad);
|
||||
const float XScale = YScale / AspectRatio;
|
||||
|
||||
ProjectionMatrix.SetIdentity();
|
||||
ProjectionMatrix.M[0][0] = XScale;
|
||||
ProjectionMatrix.M[1][1] = YScale;
|
||||
ProjectionMatrix.M[2][2] = (NearPlane == FarPlane) ? 0.0f : FarPlane / (FarPlane - NearPlane);
|
||||
ProjectionMatrix.M[2][3] = 1.0f;
|
||||
ProjectionMatrix.M[3][2] = (NearPlane == FarPlane) ? NearPlane : -NearPlane * FarPlane / (FarPlane - NearPlane);
|
||||
ProjectionMatrix.M[3][3] = 0.0f;
|
||||
|
||||
// Transform clip plane into view space (inverse of surface transform)
|
||||
const FMatrix ViewMatrix = SurfaceTransform.ToMatrixNoScale().Inverse();
|
||||
const FPlane ViewSpaceClipPlane = ClipPlane.TransformBy(ViewMatrix);
|
||||
|
||||
// Compute oblique near-plane using the standard technique:
|
||||
// Calculate the clip-space corner that maximizes the dot product with the plane normal.
|
||||
// Then modify the third row of the projection matrix to make the near plane pass through the clip plane.
|
||||
|
||||
FVector4 ClipPlaneVec(ViewSpaceClipPlane.X, ViewSpaceClipPlane.Y, ViewSpaceClipPlane.Z, ViewSpaceClipPlane.W);
|
||||
|
||||
// Find the vertex of the view frustum in clip space that is closest to the plane
|
||||
FVector4 Q;
|
||||
Q.X = (FMath::Sign(ClipPlaneVec.X) + ProjectionMatrix.M[0][2]) / ProjectionMatrix.M[0][0];
|
||||
Q.Y = (FMath::Sign(ClipPlaneVec.Y) + ProjectionMatrix.M[1][2]) / ProjectionMatrix.M[1][1];
|
||||
Q.Z = 1.0f;
|
||||
Q.W = (1.0f + ProjectionMatrix.M[2][2]) / ProjectionMatrix.M[3][2];
|
||||
|
||||
// Scale the clip plane so its distance from the origin matches the Q vertex
|
||||
const float DotVal = ClipPlaneVec.X * Q.X + ClipPlaneVec.Y * Q.Y + ClipPlaneVec.Z * Q.Z + ClipPlaneVec.W * Q.W;
|
||||
const float Scale = 2.0f / DotVal;
|
||||
ClipPlaneVec *= Scale;
|
||||
|
||||
// Replace the third row of the projection matrix with the clip plane
|
||||
ProjectionMatrix.M[2][0] = ClipPlaneVec.X;
|
||||
ProjectionMatrix.M[2][1] = ClipPlaneVec.Y;
|
||||
ProjectionMatrix.M[2][2] = ClipPlaneVec.Z;
|
||||
ProjectionMatrix.M[2][3] = ClipPlaneVec.W;
|
||||
|
||||
return ProjectionMatrix;
|
||||
}
|
||||
|
||||
float UPlanarCaptureCameraUtils::ComputeScreenCoverage(
|
||||
const FBox& SurfaceBounds,
|
||||
const FTransform& ViewerTransform,
|
||||
float ViewerFOV,
|
||||
int32 ScreenWidth,
|
||||
int32 ScreenHeight)
|
||||
{
|
||||
if (!SurfaceBounds.IsValid || ScreenWidth <= 0 || ScreenHeight <= 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float HalfFOVRad = FMath::DegreesToRadians(ViewerFOV * 0.5f);
|
||||
const FVector ViewerPos = ViewerTransform.GetLocation();
|
||||
const FVector ViewerForward = ViewerTransform.GetUnitAxis(EAxis::X);
|
||||
|
||||
// Project all 8 corners of the bounding box to screen space
|
||||
const FVector Corners[8] = {
|
||||
FVector(SurfaceBounds.Min.X, SurfaceBounds.Min.Y, SurfaceBounds.Min.Z),
|
||||
FVector(SurfaceBounds.Min.X, SurfaceBounds.Min.Y, SurfaceBounds.Max.Z),
|
||||
FVector(SurfaceBounds.Min.X, SurfaceBounds.Max.Y, SurfaceBounds.Min.Z),
|
||||
FVector(SurfaceBounds.Min.X, SurfaceBounds.Max.Y, SurfaceBounds.Max.Z),
|
||||
FVector(SurfaceBounds.Max.X, SurfaceBounds.Min.Y, SurfaceBounds.Min.Z),
|
||||
FVector(SurfaceBounds.Max.X, SurfaceBounds.Min.Y, SurfaceBounds.Max.Z),
|
||||
FVector(SurfaceBounds.Max.X, SurfaceBounds.Max.Y, SurfaceBounds.Min.Z),
|
||||
FVector(SurfaceBounds.Max.X, SurfaceBounds.Max.Y, SurfaceBounds.Max.Z),
|
||||
};
|
||||
|
||||
float MinScreenX = FLT_MAX, MinScreenY = FLT_MAX;
|
||||
float MaxScreenX = -FLT_MAX, MaxScreenY = -FLT_MAX;
|
||||
int32 BehindCameraCount = 0;
|
||||
|
||||
for (const FVector& Corner : Corners)
|
||||
{
|
||||
const FVector ToCorner = Corner - ViewerPos;
|
||||
const float DotForward = FVector::DotProduct(ToCorner, ViewerForward);
|
||||
|
||||
if (DotForward <= 0.0f)
|
||||
{
|
||||
BehindCameraCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const float Distance = ToCorner.Size();
|
||||
const FVector Right = ViewerTransform.GetUnitAxis(EAxis::Y);
|
||||
const FVector Up = ViewerTransform.GetUnitAxis(EAxis::Z);
|
||||
|
||||
const float DotRight = FVector::DotProduct(ToCorner.GetSafeNormal(), Right);
|
||||
const float DotUp = FVector::DotProduct(ToCorner.GetSafeNormal(), Up);
|
||||
|
||||
const float AngleX = FMath::Atan2(DotRight, DotForward);
|
||||
const float AngleY = FMath::Atan2(DotUp, DotForward);
|
||||
|
||||
const float ScreenX = (AngleX / HalfFOVRad) * (ScreenWidth * 0.5f) + (ScreenWidth * 0.5f);
|
||||
const float ScreenY = (ScreenHeight * 0.5f) - (AngleY / HalfFOVRad) * (ScreenHeight * 0.5f);
|
||||
|
||||
MinScreenX = FMath::Min(MinScreenX, ScreenX);
|
||||
MinScreenY = FMath::Min(MinScreenY, ScreenY);
|
||||
MaxScreenX = FMath::Max(MaxScreenX, ScreenX);
|
||||
MaxScreenY = FMath::Max(MaxScreenY, ScreenY);
|
||||
}
|
||||
|
||||
if (BehindCameraCount >= 8)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Clamp to screen bounds
|
||||
MinScreenX = FMath::Clamp(MinScreenX, 0.0f, static_cast<float>(ScreenWidth));
|
||||
MinScreenY = FMath::Clamp(MinScreenY, 0.0f, static_cast<float>(ScreenHeight));
|
||||
MaxScreenX = FMath::Clamp(MaxScreenX, 0.0f, static_cast<float>(ScreenWidth));
|
||||
MaxScreenY = FMath::Clamp(MaxScreenY, 0.0f, static_cast<float>(ScreenHeight));
|
||||
|
||||
const float ScreenArea = static_cast<float>(ScreenWidth * ScreenHeight);
|
||||
const float BoundingArea = (MaxScreenX - MinScreenX) * (MaxScreenY - MinScreenY);
|
||||
|
||||
return FMath::Clamp(BoundingArea / ScreenArea, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
bool UPlanarCaptureCameraUtils::IsSurfaceVisibleToViewer(
|
||||
const FBox& SurfaceBounds,
|
||||
const FTransform& ViewerTransform,
|
||||
float ViewerFOV,
|
||||
float ViewerAspectRatio,
|
||||
float ViewerNearPlane,
|
||||
float ViewerFarPlane)
|
||||
{
|
||||
if (!SurfaceBounds.IsValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Quick dot-product check: is the surface center roughly in front of the viewer?
|
||||
const FVector SurfaceCenter = SurfaceBounds.GetCenter();
|
||||
const FVector ViewerPos = ViewerTransform.GetLocation();
|
||||
const FVector ToSurface = SurfaceCenter - ViewerPos;
|
||||
const FVector ViewerForward = ViewerTransform.GetUnitAxis(EAxis::X);
|
||||
|
||||
if (FVector::DotProduct(ToSurface.GetSafeNormal(), ViewerForward) < 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Distance check
|
||||
if (ToSurface.Size() > ViewerFarPlane)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (ToSurface.Size() < ViewerNearPlane)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Screen coverage check — if coverage is above 0, it's visible
|
||||
const float Coverage = ComputeScreenCoverage(SurfaceBounds, ViewerTransform, ViewerFOV, 1920, 1080);
|
||||
return Coverage > 0.001f;
|
||||
}
|
||||
|
||||
float UPlanarCaptureCameraUtils::ComputeCompositeScore(
|
||||
float ScreenCoverage,
|
||||
float FacingAngle,
|
||||
float DistanceToViewer,
|
||||
float MaxDistance,
|
||||
float ScriptedPriority)
|
||||
{
|
||||
const float DistanceFactor = (MaxDistance > 0.0f)
|
||||
? FMath::Clamp(1.0f - (DistanceToViewer / MaxDistance), 0.0f, 1.0f)
|
||||
: 1.0f;
|
||||
|
||||
const float Score = (ScreenCoverage * 0.5f)
|
||||
+ (FMath::Abs(FacingAngle) * 0.3f)
|
||||
+ (DistanceFactor * 0.1f)
|
||||
+ (ScriptedPriority * 0.1f);
|
||||
|
||||
return FMath::Clamp(Score, 0.0f, 1.0f);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright Ngonart OU. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — PlanarCaptureCommon implementation (trivial)
|
||||
|
||||
#include "Capture/PlanarCaptureCommon.h"
|
||||
// Struct and enum definitions are header-only.
|
||||
// This file exists for module compilation unity.
|
||||
432
Source/PG_Framework/Private/Capture/SS_PlanarCaptureManager.cpp
Normal file
432
Source/PG_Framework/Private/Capture/SS_PlanarCaptureManager.cpp
Normal file
@@ -0,0 +1,432 @@
|
||||
// Copyright Ngonart OU. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — SS_PlanarCaptureManager implementation
|
||||
|
||||
#include "Capture/SS_PlanarCaptureManager.h"
|
||||
#include "Capture/BP_PlanarCaptureActor.h"
|
||||
#include "Capture/BPC_PlanarCapture.h"
|
||||
#include "Engine/TextureRenderTarget2D.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
USS_PlanarCaptureManager::USS_PlanarCaptureManager()
|
||||
{
|
||||
}
|
||||
|
||||
void USS_PlanarCaptureManager::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Initialized for world."));
|
||||
}
|
||||
|
||||
void USS_PlanarCaptureManager::Deinitialize()
|
||||
{
|
||||
// Release all render targets
|
||||
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
|
||||
{
|
||||
if (Entry.RenderTarget)
|
||||
{
|
||||
Entry.RenderTarget->ConditionalBeginDestroy();
|
||||
}
|
||||
}
|
||||
RenderTargetPool.Empty();
|
||||
|
||||
RegisteredSurfaces.Empty();
|
||||
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
void USS_PlanarCaptureManager::Tick(float DeltaTime)
|
||||
{
|
||||
TimeSinceLastEvaluation += DeltaTime;
|
||||
|
||||
if (TimeSinceLastEvaluation >= FullEvaluationInterval)
|
||||
{
|
||||
TimeSinceLastEvaluation = 0.0f;
|
||||
EvaluateAllSurfaces();
|
||||
}
|
||||
}
|
||||
|
||||
TStatId USS_PlanarCaptureManager::GetStatId() const
|
||||
{
|
||||
RETURN_QUICK_DECLARE_CYCLE_STAT(USS_PlanarCaptureManager, STATGROUP_Tickables);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Surface Registry
|
||||
// ========================================================================
|
||||
|
||||
void USS_PlanarCaptureManager::RegisterSurface(ABP_PlanarCaptureActor* Surface)
|
||||
{
|
||||
if (!Surface)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
for (const TWeakObjectPtr<ABP_PlanarCaptureActor>& Existing : RegisteredSurfaces)
|
||||
{
|
||||
if (Existing.Get() == Surface)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("SS_PlanarCaptureManager: Surface '%s' already registered."),
|
||||
*Surface->SurfaceDisplayName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
RegisteredSurfaces.Add(Surface);
|
||||
UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Registered surface '%s'. Total: %d"),
|
||||
*Surface->SurfaceDisplayName, RegisteredSurfaces.Num());
|
||||
|
||||
OnSurfaceRegistered.Broadcast(Surface, RegisteredSurfaces.Num());
|
||||
|
||||
// Evaluate immediately to assign initial tier
|
||||
EvaluateAllSurfaces();
|
||||
}
|
||||
|
||||
void USS_PlanarCaptureManager::UnregisterSurface(ABP_PlanarCaptureActor* Surface)
|
||||
{
|
||||
if (!Surface)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RegisteredSurfaces.RemoveAll([Surface](const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry)
|
||||
{
|
||||
return Entry.Get() == Surface;
|
||||
});
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Unregistered surface '%s'. Total: %d"),
|
||||
*Surface->SurfaceDisplayName, RegisteredSurfaces.Num());
|
||||
|
||||
OnSurfaceUnregistered.Broadcast(Surface, RegisteredSurfaces.Num());
|
||||
}
|
||||
|
||||
TArray<ABP_PlanarCaptureActor*> USS_PlanarCaptureManager::GetRegisteredSurfaces() const
|
||||
{
|
||||
TArray<ABP_PlanarCaptureActor*> Result;
|
||||
for (const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry : RegisteredSurfaces)
|
||||
{
|
||||
if (Entry.IsValid())
|
||||
{
|
||||
Result.Add(Entry.Get());
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Quality Budget Management
|
||||
// ========================================================================
|
||||
|
||||
void USS_PlanarCaptureManager::ForceAllSurfacesToTier(EPlanarCaptureQualityTier Tier)
|
||||
{
|
||||
ForceTierOverride = Tier;
|
||||
|
||||
for (TWeakObjectPtr<ABP_PlanarCaptureActor>& SurfaceWeak : RegisteredSurfaces)
|
||||
{
|
||||
if (ABP_PlanarCaptureActor* Surface = SurfaceWeak.Get())
|
||||
{
|
||||
if (UBPC_PlanarCapture* Capture = Surface->CaptureComponent)
|
||||
{
|
||||
Capture->ApplyQualityTier(Tier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void USS_PlanarCaptureManager::ReleaseForceTier()
|
||||
{
|
||||
ForceTierOverride.Reset();
|
||||
EvaluateAllSurfaces();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Render Target Pool
|
||||
// ========================================================================
|
||||
|
||||
UTextureRenderTarget2D* USS_PlanarCaptureManager::RequestRenderTarget(int32 Size)
|
||||
{
|
||||
// Check pool for an available RT of the right size
|
||||
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
|
||||
{
|
||||
if (!Entry.bInUse && Entry.CurrentSize == Size && Entry.RenderTarget)
|
||||
{
|
||||
Entry.bInUse = true;
|
||||
return Entry.RenderTarget;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new one
|
||||
return CreateRenderTarget(Size);
|
||||
}
|
||||
|
||||
void USS_PlanarCaptureManager::ReleaseRenderTarget(UTextureRenderTarget2D* RenderTarget)
|
||||
{
|
||||
if (!RenderTarget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the entry and mark as free
|
||||
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
|
||||
{
|
||||
if (Entry.RenderTarget == RenderTarget)
|
||||
{
|
||||
Entry.bInUse = false;
|
||||
Entry.OwningSurface.Reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Not in pool — add it
|
||||
FPlanarCaptureRenderTargetEntry NewEntry;
|
||||
NewEntry.RenderTarget = RenderTarget;
|
||||
NewEntry.CurrentSize = RenderTarget->SizeX;
|
||||
NewEntry.bInUse = false;
|
||||
RenderTargetPool.Add(NewEntry);
|
||||
}
|
||||
|
||||
float USS_PlanarCaptureManager::GetPoolMemoryUsageMB() const
|
||||
{
|
||||
float TotalBytes = 0.0f;
|
||||
for (const FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
|
||||
{
|
||||
if (Entry.RenderTarget)
|
||||
{
|
||||
// RGBA8 = 4 bytes per pixel
|
||||
TotalBytes += static_cast<float>(Entry.CurrentSize * Entry.CurrentSize * 4);
|
||||
}
|
||||
}
|
||||
return TotalBytes / (1024.0f * 1024.0f);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Query
|
||||
// ========================================================================
|
||||
|
||||
ABP_PlanarCaptureActor* USS_PlanarCaptureManager::GetNearestSurfaceOfMode(
|
||||
EPlanarCaptureMode Mode, FVector WorldLocation, float MaxDistance) const
|
||||
{
|
||||
ABP_PlanarCaptureActor* Nearest = nullptr;
|
||||
float NearestDistSq = (MaxDistance > 0.0f) ? (MaxDistance * MaxDistance) : FLT_MAX;
|
||||
|
||||
for (const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry : RegisteredSurfaces)
|
||||
{
|
||||
if (ABP_PlanarCaptureActor* Surface = Entry.Get())
|
||||
{
|
||||
if (!Surface->CaptureComponent || Surface->CaptureComponent->CaptureMode != Mode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DistSq = FVector::DistSquared(Surface->GetActorLocation(), WorldLocation);
|
||||
if (DistSq < NearestDistSq)
|
||||
{
|
||||
NearestDistSq = DistSq;
|
||||
Nearest = Surface;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Nearest;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Internal Methods
|
||||
// ========================================================================
|
||||
|
||||
void USS_PlanarCaptureManager::EvaluateAllSurfaces()
|
||||
{
|
||||
// Reset tier counts
|
||||
TierAssignmentCounts.Empty();
|
||||
|
||||
// Ensure registered surfaces are still valid
|
||||
RegisteredSurfaces.RemoveAll([](const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry)
|
||||
{
|
||||
return !Entry.IsValid();
|
||||
});
|
||||
|
||||
// If force tier override is active, apply it to all
|
||||
if (ForceTierOverride.IsSet())
|
||||
{
|
||||
ForceAllSurfacesToTier(ForceTierOverride.GetValue());
|
||||
return;
|
||||
}
|
||||
|
||||
// Score and assign tiers
|
||||
TArray<TPair<ABP_PlanarCaptureActor*, float>> ScoredSurfaces;
|
||||
|
||||
for (TWeakObjectPtr<ABP_PlanarCaptureActor>& SurfaceWeak : RegisteredSurfaces)
|
||||
{
|
||||
if (ABP_PlanarCaptureActor* Surface = SurfaceWeak.Get())
|
||||
{
|
||||
if (!Surface->bIsActive || !Surface->CaptureComponent)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UBPC_PlanarCapture* Capture = Surface->CaptureComponent;
|
||||
FPlanarCaptureScore Score = Capture->GetCurrentScore();
|
||||
ScoredSurfaces.Add(TPair<ABP_PlanarCaptureActor*, float>(Surface, Score.CompositeScore));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score descending (highest score first)
|
||||
ScoredSurfaces.Sort([](const TPair<ABP_PlanarCaptureActor*, float>& A,
|
||||
const TPair<ABP_PlanarCaptureActor*, float>& B)
|
||||
{
|
||||
return A.Value > B.Value;
|
||||
});
|
||||
|
||||
// Assign tiers within budget
|
||||
for (const auto& Pair : ScoredSurfaces)
|
||||
{
|
||||
ABP_PlanarCaptureActor* Surface = Pair.Key;
|
||||
float Score = Pair.Value;
|
||||
|
||||
if (!Surface->CaptureComponent)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EPlanarCaptureQualityTier AssignedTier = ScoreAndAssignTier(Surface->CaptureComponent);
|
||||
|
||||
// Apply tier
|
||||
Surface->CaptureComponent->ApplyQualityTier(AssignedTier);
|
||||
}
|
||||
}
|
||||
|
||||
EPlanarCaptureQualityTier USS_PlanarCaptureManager::ScoreAndAssignTier(UBPC_PlanarCapture* Capture)
|
||||
{
|
||||
if (!Capture)
|
||||
{
|
||||
return EPlanarCaptureQualityTier::Off;
|
||||
}
|
||||
|
||||
const FPlanarCaptureScore Score = Capture->GetCurrentScore();
|
||||
|
||||
// If not visible, off
|
||||
if (!Score.bInFrustum && Score.CompositeScore < 0.01f)
|
||||
{
|
||||
return EPlanarCaptureQualityTier::Off;
|
||||
}
|
||||
|
||||
// If too far, off
|
||||
if (Score.DistanceToViewer > MaxCaptureDistance)
|
||||
{
|
||||
return EPlanarCaptureQualityTier::Off;
|
||||
}
|
||||
|
||||
// Determine natural tier based on composite score
|
||||
EPlanarCaptureQualityTier NaturalTier;
|
||||
|
||||
if (Score.CompositeScore >= 0.8f)
|
||||
{
|
||||
NaturalTier = EPlanarCaptureQualityTier::Hero;
|
||||
}
|
||||
else if (Score.CompositeScore >= 0.5f)
|
||||
{
|
||||
NaturalTier = EPlanarCaptureQualityTier::High;
|
||||
}
|
||||
else if (Score.CompositeScore >= 0.2f)
|
||||
{
|
||||
NaturalTier = EPlanarCaptureQualityTier::Medium;
|
||||
}
|
||||
else
|
||||
{
|
||||
NaturalTier = EPlanarCaptureQualityTier::Low;
|
||||
}
|
||||
|
||||
// Apply global quality cap
|
||||
if (static_cast<int32>(NaturalTier) > static_cast<int32>(GlobalQualityCap))
|
||||
{
|
||||
NaturalTier = GlobalQualityCap;
|
||||
}
|
||||
|
||||
// Check budget limits
|
||||
const int32 CurrentCount = TierAssignmentCounts.FindRef(NaturalTier);
|
||||
|
||||
switch (NaturalTier)
|
||||
{
|
||||
case EPlanarCaptureQualityTier::Hero:
|
||||
if (CurrentCount >= MaxHeroSurfaces)
|
||||
{
|
||||
NaturalTier = EPlanarCaptureQualityTier::High;
|
||||
}
|
||||
break;
|
||||
case EPlanarCaptureQualityTier::High:
|
||||
if (CurrentCount >= MaxHighSurfaces)
|
||||
{
|
||||
NaturalTier = EPlanarCaptureQualityTier::Medium;
|
||||
}
|
||||
break;
|
||||
case EPlanarCaptureQualityTier::Medium:
|
||||
if (CurrentCount >= MaxMediumSurfaces)
|
||||
{
|
||||
NaturalTier = EPlanarCaptureQualityTier::Low;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Increment the assigned tier count
|
||||
TierAssignmentCounts.FindOrAdd(NaturalTier)++;
|
||||
|
||||
return NaturalTier;
|
||||
}
|
||||
|
||||
void USS_PlanarCaptureManager::EnforceBudgetLimits()
|
||||
{
|
||||
// Additional enforcement for total memory budget
|
||||
float TotalMemoryMB = GetPoolMemoryUsageMB();
|
||||
|
||||
if (TotalMemoryMB > MaxTotalRenderTargetMemoryMB)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("SS_PlanarCaptureManager: Render target memory budget exceeded (%.2f MB / %.2f MB)"),
|
||||
TotalMemoryMB, MaxTotalRenderTargetMemoryMB);
|
||||
|
||||
// If over budget, demote lowest-priority surfaces
|
||||
// Future: implement more sophisticated memory budget enforcement
|
||||
}
|
||||
}
|
||||
|
||||
UTextureRenderTarget2D* USS_PlanarCaptureManager::CreateRenderTarget(int32 Size)
|
||||
{
|
||||
UTextureRenderTarget2D* RT = NewObject<UTextureRenderTarget2D>(this);
|
||||
if (!RT)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RT->InitCustomFormat(Size, Size, PF_B8G8R8A8, false);
|
||||
RT->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA8;
|
||||
RT->bGPUSharedFlag = false;
|
||||
RT->ClearColor = FLinearColor::Black;
|
||||
RT->UpdateResourceImmediate(true);
|
||||
|
||||
FPlanarCaptureRenderTargetEntry Entry;
|
||||
Entry.RenderTarget = RT;
|
||||
Entry.CurrentSize = Size;
|
||||
Entry.bInUse = true;
|
||||
RenderTargetPool.Add(Entry);
|
||||
|
||||
return RT;
|
||||
}
|
||||
|
||||
UTextureRenderTarget2D* USS_PlanarCaptureManager::GetOrCreateRenderTarget(int32 Size)
|
||||
{
|
||||
// Check pool first
|
||||
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
|
||||
{
|
||||
if (!Entry.bInUse && Entry.CurrentSize == Size && Entry.RenderTarget)
|
||||
{
|
||||
Entry.bInUse = true;
|
||||
return Entry.RenderTarget;
|
||||
}
|
||||
}
|
||||
|
||||
return CreateRenderTarget(Size);
|
||||
}
|
||||
156
Source/PG_Framework/Private/Core/DA_GameTagRegistry.cpp
Normal file
156
Source/PG_Framework/Private/Core/DA_GameTagRegistry.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — DA_GameTagRegistry Implementation
|
||||
|
||||
#include "Core/DA_GameTagRegistry.h"
|
||||
#include "GameplayTagsManager.h"
|
||||
#include "Engine/DataTable.h"
|
||||
|
||||
|
||||
UDA_GameTagRegistry::UDA_GameTagRegistry()
|
||||
{
|
||||
TagNamespace = FText::FromString(TEXT("Framework tag namespace documentation"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query Functions
|
||||
// ============================================================================
|
||||
|
||||
TArray<FGameplayTag> UDA_GameTagRegistry::GetAllRegisteredTags() const
|
||||
{
|
||||
TArray<FGameplayTag> AllTags;
|
||||
|
||||
// C++ direct access — eliminates the Data Table proxy workaround entirely.
|
||||
UGameplayTagsManager& TagManager = UGameplayTagsManager::Get();
|
||||
|
||||
FGameplayTagContainer AllRegisteredTags;
|
||||
TagManager.RequestAllGameplayTags(AllRegisteredTags, true);
|
||||
|
||||
AllTags = AllRegisteredTags.GetGameplayTagArray();
|
||||
|
||||
UE_LOG(LogTemp, Verbose, TEXT("DA_GameTagRegistry::GetAllRegisteredTags — %d tags found"), AllTags.Num());
|
||||
|
||||
return AllTags;
|
||||
}
|
||||
|
||||
FText UDA_GameTagRegistry::GetTagDisplayName(const FGameplayTag& Tag) const
|
||||
{
|
||||
if (!Tag.IsValid())
|
||||
{
|
||||
return FText::FromString(TEXT("Invalid Tag"));
|
||||
}
|
||||
|
||||
return FText::FromName(Tag.GetTagName());
|
||||
}
|
||||
|
||||
bool UDA_GameTagRegistry::ValidateTag(const FGameplayTag& Tag) const
|
||||
{
|
||||
if (Tag.IsValid())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Warning, TEXT("DA_GameTagRegistry::ValidateTag — Invalid Tag: %s"),
|
||||
*Tag.GetTagName().ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
FGameplayTag UDA_GameTagRegistry::RequestTag(FName TagName, bool bLogWarning) const
|
||||
{
|
||||
if (TagName.IsNone())
|
||||
{
|
||||
return FGameplayTag::EmptyTag;
|
||||
}
|
||||
|
||||
UGameplayTagsManager& TagManager = UGameplayTagsManager::Get();
|
||||
TSharedPtr<FGameplayTagNode> TagNode = TagManager.FindTagNode(TagName);
|
||||
|
||||
FGameplayTag OutTag = TagManager.RequestGameplayTag(TagName, false);
|
||||
if (OutTag.IsValid())
|
||||
{
|
||||
return OutTag;
|
||||
}
|
||||
|
||||
if (bLogWarning)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("DA_GameTagRegistry::RequestTag — Tag '%s' not found in any registered table"),
|
||||
*TagName.ToString());
|
||||
}
|
||||
|
||||
return FGameplayTag::EmptyTag;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Debug / Tooling
|
||||
// ============================================================================
|
||||
|
||||
void UDA_GameTagRegistry::LogAllTags() const
|
||||
{
|
||||
#if !UE_BUILD_SHIPPING
|
||||
TArray<FGameplayTag> AllTags = GetAllRegisteredTags();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("========== DA_GameTagRegistry: All Registered Tags (%d total) =========="),
|
||||
AllTags.Num());
|
||||
|
||||
for (const FGameplayTag& Tag : AllTags)
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT(" %s"), *Tag.GetTagName().ToString());
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("========== End Tag List =========="));
|
||||
#endif
|
||||
}
|
||||
|
||||
FString UDA_GameTagRegistry::ExportTagNamespace(const FString& NamespacePrefix) const
|
||||
{
|
||||
TArray<FGameplayTag> AllTags = GetAllRegisteredTags();
|
||||
FString Output;
|
||||
|
||||
for (const FGameplayTag& Tag : AllTags)
|
||||
{
|
||||
FString TagString = Tag.GetTagName().ToString();
|
||||
if (TagString.StartsWith(NamespacePrefix))
|
||||
{
|
||||
Output += TagString + TEXT("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return Output;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Overrides
|
||||
// ============================================================================
|
||||
|
||||
void UDA_GameTagRegistry::PostLoad()
|
||||
{
|
||||
Super::PostLoad();
|
||||
|
||||
// Validate on load — catch misconfigured projects early.
|
||||
TArray<FGameplayTag> AllTags = GetAllRegisteredTags();
|
||||
|
||||
if (AllTags.Num() == 0)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("DA_GameTagRegistry::PostLoad — No Gameplay Tags registered! "
|
||||
"Check Project Settings → GameplayTags → Gameplay Tag Table List. "
|
||||
"All 11 Data Tables must be added."));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("DA_GameTagRegistry::PostLoad — %d tags registered"), AllTags.Num());
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void UDA_GameTagRegistry::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||
{
|
||||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||
|
||||
// Re-validate when TagDataTables array changes in editor.
|
||||
FName PropertyName = PropertyChangedEvent.GetPropertyName();
|
||||
if (PropertyName == GET_MEMBER_NAME_CHECKED(UDA_GameTagRegistry, TagDataTables))
|
||||
{
|
||||
TArray<FGameplayTag> AllTags = GetAllRegisteredTags();
|
||||
UE_LOG(LogTemp, Log, TEXT("DA_GameTagRegistry: TagDataTables updated — %d tags now registered"), AllTags.Num());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
299
Source/PG_Framework/Private/Core/FL_GameUtilities.cpp
Normal file
299
Source/PG_Framework/Private/Core/FL_GameUtilities.cpp
Normal file
@@ -0,0 +1,299 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — FL_GameUtilities Implementation
|
||||
|
||||
#include "Core/FL_GameUtilities.h"
|
||||
#include "Core/GI_GameFramework.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GameFramework/GameStateBase.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameplayTagsManager.h"
|
||||
#include "GameplayTagAssetInterface.h"
|
||||
|
||||
// ============================================================================
|
||||
// Subsystem Access
|
||||
// ============================================================================
|
||||
|
||||
UGI_GameFramework* UFL_GameUtilities::GetGameFramework(const UObject* WorldContextObject)
|
||||
{
|
||||
if (!WorldContextObject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UWorld* World = WorldContextObject->GetWorld();
|
||||
if (!World)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return Cast<UGI_GameFramework>(World->GetGameInstance());
|
||||
}
|
||||
|
||||
UGameInstanceSubsystem* UFL_GameUtilities::GetSubsystemByClass(const UObject* WorldContextObject,
|
||||
TSubclassOf<UGameInstanceSubsystem> SubsystemClass)
|
||||
{
|
||||
if (!WorldContextObject || !SubsystemClass)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UGameInstance* GameInstance = WorldContextObject->GetWorld()->GetGameInstance();
|
||||
if (!GameInstance)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return GameInstance->GetSubsystemBase(SubsystemClass);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Actor Utilities
|
||||
// ============================================================================
|
||||
|
||||
UActorComponent* UFL_GameUtilities::FindComponentByInterface(AActor* Actor,
|
||||
TSubclassOf<UInterface> InterfaceClass)
|
||||
{
|
||||
if (!Actor || !InterfaceClass)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TArray<UActorComponent*> Components;
|
||||
Actor->GetComponents(Components);
|
||||
|
||||
for (UActorComponent* Comp : Components)
|
||||
{
|
||||
if (Comp && Comp->GetClass()->ImplementsInterface(InterfaceClass))
|
||||
{
|
||||
return Comp;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AActor* UFL_GameUtilities::FindNearestActorWithTag(const UObject* WorldContextObject,
|
||||
FVector Origin, float Radius, FGameplayTag RequiredTag)
|
||||
{
|
||||
if (!WorldContextObject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
|
||||
if (!World)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Collect all actors with the tag within radius.
|
||||
TArray<AActor*> Candidates;
|
||||
UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), Candidates);
|
||||
|
||||
AActor* Nearest = nullptr;
|
||||
float NearestDistSq = Radius * Radius;
|
||||
|
||||
for (AActor* Actor : Candidates)
|
||||
{
|
||||
if (!Actor || !Actor->ActorHasTag(RequiredTag.GetTagName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float DistSq = FVector::DistSquared(Origin, Actor->GetActorLocation());
|
||||
if (DistSq < NearestDistSq)
|
||||
{
|
||||
NearestDistSq = DistSq;
|
||||
Nearest = Actor;
|
||||
}
|
||||
}
|
||||
|
||||
return Nearest;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Math Utilities
|
||||
// ============================================================================
|
||||
|
||||
float UFL_GameUtilities::RemapFloat(float Value, float InMin, float InMax, float OutMin, float OutMax)
|
||||
{
|
||||
if (FMath::IsNearlyEqual(InMax, InMin))
|
||||
{
|
||||
return OutMin;
|
||||
}
|
||||
|
||||
float Alpha = (Value - InMin) / (InMax - InMin);
|
||||
return FMath::Lerp(OutMin, OutMax, Alpha);
|
||||
}
|
||||
|
||||
float UFL_GameUtilities::LerpClamped(float A, float B, float Alpha)
|
||||
{
|
||||
return FMath::Lerp(A, B, FMath::Clamp(Alpha, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
float UFL_GameUtilities::VectorToAngle2D(FVector2D Direction)
|
||||
{
|
||||
if (Direction.IsNearlyZero())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return FMath::RadiansToDegrees(FMath::Atan2(Direction.Y, Direction.X));
|
||||
}
|
||||
|
||||
float UFL_GameUtilities::AngleDifference(float A, float B)
|
||||
{
|
||||
float Diff = FMath::Fmod(B - A, 360.0f);
|
||||
if (Diff > 180.0f)
|
||||
{
|
||||
Diff -= 360.0f;
|
||||
}
|
||||
else if (Diff < -180.0f)
|
||||
{
|
||||
Diff += 360.0f;
|
||||
}
|
||||
return Diff;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GameplayTag Utilities
|
||||
// ============================================================================
|
||||
|
||||
bool UFL_GameUtilities::HasGameplayTag(AActor* Actor, FGameplayTag Tag)
|
||||
{
|
||||
if (!Actor || !Tag.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prefer IGameplayTagAssetInterface if the actor implements it.
|
||||
if (IGameplayTagAssetInterface* TagInterface = Cast<IGameplayTagAssetInterface>(Actor))
|
||||
{
|
||||
return TagInterface->HasMatchingGameplayTag(Tag);
|
||||
}
|
||||
|
||||
// Fallback: check actor tags (FName-based, less reliable).
|
||||
return Actor->ActorHasTag(Tag.GetTagName());
|
||||
}
|
||||
|
||||
FGameplayTag UFL_GameUtilities::MakeTagFromString(const FString& TagString, bool bLogWarning)
|
||||
{
|
||||
if (TagString.IsEmpty())
|
||||
{
|
||||
return FGameplayTag::EmptyTag;
|
||||
}
|
||||
|
||||
UGameplayTagsManager& TagManager = UGameplayTagsManager::Get();
|
||||
|
||||
FGameplayTag OutTag = TagManager.RequestGameplayTag(FName(*TagString), false);
|
||||
if (OutTag.IsValid())
|
||||
{
|
||||
return OutTag;
|
||||
}
|
||||
|
||||
if (bLogWarning)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("FL_GameUtilities::MakeTagFromString — Tag '%s' not registered"), *TagString);
|
||||
}
|
||||
|
||||
return FGameplayTag::EmptyTag;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Text Utilities
|
||||
// ============================================================================
|
||||
|
||||
FText UFL_GameUtilities::FormatTime(float TotalSeconds)
|
||||
{
|
||||
int32 Hours = FMath::FloorToInt(TotalSeconds / 3600.0f);
|
||||
int32 Minutes = FMath::FloorToInt(FMath::Fmod(TotalSeconds, 3600.0f) / 60.0f);
|
||||
int32 Seconds = FMath::FloorToInt(FMath::Fmod(TotalSeconds, 60.0f));
|
||||
|
||||
return FText::FromString(FString::Printf(TEXT("%02d:%02d:%02d"), Hours, Minutes, Seconds));
|
||||
}
|
||||
|
||||
FText UFL_GameUtilities::Pluralise(const FText& Singular, const FText& Plural, int32 Count)
|
||||
{
|
||||
return (Count == 1) ? Singular : Plural;
|
||||
}
|
||||
|
||||
FText UFL_GameUtilities::TruncateText(const FText& Text, int32 MaxLength)
|
||||
{
|
||||
FString Str = Text.ToString();
|
||||
if (Str.Len() <= MaxLength)
|
||||
{
|
||||
return Text;
|
||||
}
|
||||
|
||||
Str = Str.Left(MaxLength - 3) + TEXT("...");
|
||||
return FText::FromString(Str);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Screen / Projection Utilities
|
||||
// ============================================================================
|
||||
|
||||
bool UFL_GameUtilities::WorldToScreenSafe(const UObject* WorldContextObject,
|
||||
FVector WorldPosition, FVector2D& OutScreenPosition, bool& bIsOnScreen)
|
||||
{
|
||||
OutScreenPosition = FVector2D::ZeroVector;
|
||||
bIsOnScreen = false;
|
||||
|
||||
if (!WorldContextObject)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
APlayerController* PC = UGameplayStatics::GetPlayerController(WorldContextObject, 0);
|
||||
if (!PC)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FVector2D ScreenPos;
|
||||
bool bProjected = PC->ProjectWorldLocationToScreen(WorldPosition, ScreenPos, true);
|
||||
|
||||
// Check if behind camera.
|
||||
bIsOnScreen = bProjected;
|
||||
|
||||
// Additional check for screen bounds.
|
||||
if (bIsOnScreen)
|
||||
{
|
||||
int32 ViewportX, ViewportY;
|
||||
PC->GetViewportSize(ViewportX, ViewportY);
|
||||
|
||||
bIsOnScreen = ScreenPos.X >= 0.0f && ScreenPos.X <= ViewportX &&
|
||||
ScreenPos.Y >= 0.0f && ScreenPos.Y <= ViewportY;
|
||||
}
|
||||
|
||||
OutScreenPosition = ScreenPos;
|
||||
return bIsOnScreen;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Debug (Shipping-safe)
|
||||
// ============================================================================
|
||||
|
||||
void UFL_GameUtilities::DebugLog(const FString& Message, bool bPrintToScreen, float ScreenDuration, FColor ScreenColor)
|
||||
{
|
||||
#if !UE_BUILD_SHIPPING
|
||||
UE_LOG(LogTemp, Log, TEXT("%s"), *Message);
|
||||
|
||||
if (bPrintToScreen && GEngine)
|
||||
{
|
||||
GEngine->AddOnScreenDebugMessage(-1, ScreenDuration, ScreenColor, Message);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void UFL_GameUtilities::DebugSphere(const UObject* WorldContextObject, FVector Location,
|
||||
float Radius, FColor Color, float Duration)
|
||||
{
|
||||
#if !UE_BUILD_SHIPPING
|
||||
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
|
||||
{
|
||||
DrawDebugSphere(World, Location, Radius, 12, Color, false, Duration);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
239
Source/PG_Framework/Private/Core/GI_GameFramework.cpp
Normal file
239
Source/PG_Framework/Private/Core/GI_GameFramework.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — GI_GameFramework Implementation
|
||||
|
||||
#include "Core/GI_GameFramework.h"
|
||||
#include "Core/DA_GameTagRegistry.h"
|
||||
#include "Logging/LogMacros.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogFramework, Log, All);
|
||||
|
||||
UGI_GameFramework::UGI_GameFramework()
|
||||
{
|
||||
PlatformType = EPlatformType::Generic;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lifecycle
|
||||
// ============================================================================
|
||||
|
||||
void UGI_GameFramework::Init()
|
||||
{
|
||||
Super::Init();
|
||||
|
||||
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::Init — Framework boot starting..."));
|
||||
|
||||
// Step 1: Platform-specific initialization.
|
||||
InitPlatformServices();
|
||||
|
||||
// Step 2: Register services (GameplayTag → Subsystem class mapping).
|
||||
RegisterServices();
|
||||
|
||||
// Step 3: Validate the tag registry.
|
||||
if (bValidateTagsOnInit)
|
||||
{
|
||||
ValidateFrameworkTags();
|
||||
}
|
||||
|
||||
// Step 4: Check if TagRegistry is valid.
|
||||
if (!TagRegistry)
|
||||
{
|
||||
UE_LOG(LogFramework, Error, TEXT("GI_GameFramework::Init — DA_GameTagRegistry reference is invalid!"));
|
||||
OnFrameworkInitFailed.Broadcast(TEXT("DA_GameTagRegistry reference not assigned in GI_GameFramework"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 5: Mark framework as ready.
|
||||
bFrameworkInitialized = true;
|
||||
|
||||
// Step 6: Broadcast readiness.
|
||||
OnFrameworkReady.Broadcast();
|
||||
OnPlatformReady.Broadcast();
|
||||
|
||||
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::Init — Framework ready. Phase: MainMenu"));
|
||||
}
|
||||
|
||||
void UGI_GameFramework::Shutdown()
|
||||
{
|
||||
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::Shutdown"));
|
||||
Super::Shutdown();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Game Phase State Machine
|
||||
// ============================================================================
|
||||
|
||||
void UGI_GameFramework::SetGamePhase(EGamePhase NewPhase)
|
||||
{
|
||||
if (CurrentGamePhase == NewPhase)
|
||||
{
|
||||
return; // Prevent infinite loops from redundant sets.
|
||||
}
|
||||
|
||||
EGamePhase OldPhase = CurrentGamePhase;
|
||||
CurrentGamePhase = NewPhase;
|
||||
|
||||
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::SetGamePhase — %d -> %d"),
|
||||
static_cast<int32>(OldPhase), static_cast<int32>(NewPhase));
|
||||
|
||||
OnGamePhaseChanged.Broadcast(NewPhase);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Session Flags
|
||||
// ============================================================================
|
||||
|
||||
bool UGI_GameFramework::GetSessionFlag(FGameplayTag FlagTag) const
|
||||
{
|
||||
if (!FlagTag.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool* Value = SessionFlags.Find(FlagTag);
|
||||
return Value ? *Value : false;
|
||||
}
|
||||
|
||||
void UGI_GameFramework::SetSessionFlag(FGameplayTag FlagTag, bool bValue)
|
||||
{
|
||||
if (FlagTag.IsValid())
|
||||
{
|
||||
SessionFlags.Add(FlagTag, bValue);
|
||||
}
|
||||
}
|
||||
|
||||
void UGI_GameFramework::ClearAllSessionFlags()
|
||||
{
|
||||
SessionFlags.Empty();
|
||||
UE_LOG(LogFramework, Verbose, TEXT("GI_GameFramework::ClearAllSessionFlags — All session flags cleared"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Save Slot Management
|
||||
// ============================================================================
|
||||
|
||||
void UGI_GameFramework::SetActiveSlot(int32 SlotIndex)
|
||||
{
|
||||
ActiveSlotIndex = SlotIndex;
|
||||
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::SetActiveSlot — Slot %d"), SlotIndex);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Service Resolution
|
||||
// ============================================================================
|
||||
|
||||
UGameInstanceSubsystem* UGI_GameFramework::GetService(FGameplayTag ServiceTag) const
|
||||
{
|
||||
if (!ServiceTag.IsValid())
|
||||
{
|
||||
UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::GetService — Invalid service tag"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const TSubclassOf<UGameInstanceSubsystem>* SubsystemClass = ServiceRegistry.Find(ServiceTag);
|
||||
if (!SubsystemClass || !*SubsystemClass)
|
||||
{
|
||||
UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::GetService — No subsystem mapped for tag '%s'"),
|
||||
*ServiceTag.GetTagName().ToString());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UGameInstanceSubsystem* Subsystem = GetSubsystemBase(*SubsystemClass);
|
||||
if (!Subsystem)
|
||||
{
|
||||
UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::GetService — Subsystem '%s' not available"),
|
||||
*(*SubsystemClass)->GetName());
|
||||
}
|
||||
|
||||
return Subsystem;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal Methods
|
||||
// ============================================================================
|
||||
|
||||
void UGI_GameFramework::InitPlatformServices()
|
||||
{
|
||||
// Detect platform from command-line override first.
|
||||
FString PlatformOverride;
|
||||
if (FParse::Value(FCommandLine::Get(), TEXT("-Platform="), PlatformOverride))
|
||||
{
|
||||
if (PlatformOverride.Equals(TEXT("Steam"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
PlatformType = EPlatformType::Steam;
|
||||
}
|
||||
else if (PlatformOverride.Equals(TEXT("PS5"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
PlatformType = EPlatformType::PS5;
|
||||
}
|
||||
else if (PlatformOverride.Equals(TEXT("Xbox"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
PlatformType = EPlatformType::Xbox;
|
||||
}
|
||||
else if (PlatformOverride.Equals(TEXT("Switch"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
PlatformType = EPlatformType::Switch;
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::InitPlatformServices — Platform: %d"),
|
||||
static_cast<int32>(PlatformType));
|
||||
|
||||
// Platform-specific initialization hooks.
|
||||
// Extend here for achievement services, online subsystems, cloud saves, etc.
|
||||
switch (PlatformType)
|
||||
{
|
||||
case EPlatformType::Steam:
|
||||
// TODO: Init Steam OSS, achievements, cloud saves
|
||||
break;
|
||||
case EPlatformType::PS5:
|
||||
// TODO: Init PSN services
|
||||
break;
|
||||
case EPlatformType::Xbox:
|
||||
// TODO: Init Xbox Live services
|
||||
break;
|
||||
case EPlatformType::Switch:
|
||||
// TODO: Init Nintendo services
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void UGI_GameFramework::ValidateFrameworkTags()
|
||||
{
|
||||
if (!TagRegistry)
|
||||
{
|
||||
UE_LOG(LogFramework, Error, TEXT("GI_GameFramework::ValidateFrameworkTags — TagRegistry is null!"));
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FGameplayTag> AllTags = TagRegistry->GetAllRegisteredTags();
|
||||
|
||||
if (AllTags.Num() == 0)
|
||||
{
|
||||
UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::ValidateFrameworkTags — WARNING: No Gameplay Tags registered! "
|
||||
"Check Project Settings → GameplayTags → Gameplay Tag Table List. "
|
||||
"All 11 Data Tables must be added."));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::ValidateFrameworkTags — %d tags registered across 11 Data Tables"),
|
||||
AllTags.Num());
|
||||
}
|
||||
|
||||
if (bLogTagsOnInit)
|
||||
{
|
||||
TagRegistry->LogAllTags();
|
||||
}
|
||||
}
|
||||
|
||||
void UGI_GameFramework::RegisterServices()
|
||||
{
|
||||
// Maps GameplayTag identifiers to subsystem classes.
|
||||
// This is the canonical service registry — extend here for new subsystems.
|
||||
// Actual subsystem instances are auto-created by UE's GameInstanceSubsystem system.
|
||||
|
||||
// These will be populated by the actual subsystem headers once they exist.
|
||||
// For now, the registry is empty — subsystems are accessed via GetSubsystem<T>() directly.
|
||||
UE_LOG(LogFramework, Verbose, TEXT("GI_GameFramework::RegisterServices — Service registry initialized"));
|
||||
}
|
||||
161
Source/PG_Framework/Private/Core/GM_CoreGameMode.cpp
Normal file
161
Source/PG_Framework/Private/Core/GM_CoreGameMode.cpp
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — GM_CoreGameMode Implementation
|
||||
|
||||
#include "Core/GM_CoreGameMode.h"
|
||||
#include "Core/GI_GameFramework.h"
|
||||
#include "Core/GS_CoreGameState.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GameFramework/PlayerState.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogFrameworkGameMode, Log, All);
|
||||
|
||||
AGM_CoreGameMode::AGM_CoreGameMode()
|
||||
{
|
||||
// Set defaults — can be overridden in Blueprint subclasses or config.
|
||||
// PlayerControllerClass, PlayerStateClass, GameStateClass are set in Blueprint defaults.
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Overrides
|
||||
// ============================================================================
|
||||
|
||||
void AGM_CoreGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
|
||||
{
|
||||
Super::InitGame(MapName, Options, ErrorMessage);
|
||||
|
||||
// Cache the framework GameInstance.
|
||||
CachedFramework = Cast<UGI_GameFramework>(GetGameInstance());
|
||||
if (!CachedFramework)
|
||||
{
|
||||
UE_LOG(LogFrameworkGameMode, Warning, TEXT("GM_CoreGameMode::InitGame — GI_GameFramework not found as GameInstance"));
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkGameMode, Log, TEXT("GM_CoreGameMode::InitGame — Map: %s"), *MapName);
|
||||
}
|
||||
|
||||
void AGM_CoreGameMode::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Set initial game phase if coming from MainMenu.
|
||||
if (CachedFramework && CachedFramework->CurrentGamePhase == EGamePhase::MainMenu)
|
||||
{
|
||||
CachedFramework->SetGamePhase(EGamePhase::InGame);
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkGameMode, Log, TEXT("GM_CoreGameMode::BeginPlay"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Chapter Management
|
||||
// ============================================================================
|
||||
|
||||
void AGM_CoreGameMode::TransitionToChapter(FGameplayTag ChapterTag)
|
||||
{
|
||||
if (!ChapterTag.IsValid())
|
||||
{
|
||||
UE_LOG(LogFrameworkGameMode, Warning, TEXT("GM_CoreGameMode::TransitionToChapter — Invalid chapter tag"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent transitions during loading.
|
||||
if (CachedFramework && CachedFramework->CurrentGamePhase == EGamePhase::Loading)
|
||||
{
|
||||
UE_LOG(LogFrameworkGameMode, Warning, TEXT("GM_CoreGameMode::TransitionToChapter — Already loading, ignoring transition to '%s'"),
|
||||
*ChapterTag.GetTagName().ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkGameMode, Log, TEXT("GM_CoreGameMode::TransitionToChapter — '%s'"), *ChapterTag.GetTagName().ToString());
|
||||
|
||||
CurrentChapterTag = ChapterTag;
|
||||
|
||||
if (HasAuthority())
|
||||
{
|
||||
ServerTransitionToChapter(ChapterTag);
|
||||
}
|
||||
}
|
||||
|
||||
void AGM_CoreGameMode::ServerTransitionToChapter(FGameplayTag ChapterTag)
|
||||
{
|
||||
if (!CachedFramework)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set phase to Loading.
|
||||
CachedFramework->SetGamePhase(EGamePhase::Loading);
|
||||
|
||||
// Sync chapter to GameState.
|
||||
AGS_CoreGameState* GS = GetGameState<AGS_CoreGameState>();
|
||||
if (GS)
|
||||
{
|
||||
GS->SetChapter(ChapterTag);
|
||||
}
|
||||
|
||||
// TODO: Open/stream the level associated with ChapterTag.
|
||||
// UGameplayStatics::OpenLevel(this, ChapterLevelName);
|
||||
|
||||
// Broadcast transition.
|
||||
OnChapterTransition.Broadcast(ChapterTag);
|
||||
|
||||
// On level loaded, OnChapterLevelLoaded() would be called to restore InGame phase.
|
||||
}
|
||||
|
||||
void AGM_CoreGameMode::OnChapterLevelLoaded(FGameplayTag ChapterTag)
|
||||
{
|
||||
if (CachedFramework)
|
||||
{
|
||||
CachedFramework->SetGamePhase(EGamePhase::InGame);
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkGameMode, Log, TEXT("GM_CoreGameMode::OnChapterLevelLoaded — '%s'"), *ChapterTag.GetTagName().ToString());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Death Handling
|
||||
// ============================================================================
|
||||
|
||||
void AGM_CoreGameMode::HandlePlayerDead(AController* DeadController)
|
||||
{
|
||||
if (!DeadController)
|
||||
{
|
||||
UE_LOG(LogFrameworkGameMode, Warning, TEXT("GM_CoreGameMode::HandlePlayerDead — Invalid controller"));
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkGameMode, Log, TEXT("GM_CoreGameMode::HandlePlayerDead — %s"), *DeadController->GetName());
|
||||
|
||||
// Disable pause during death sequence.
|
||||
bPauseAllowed = false;
|
||||
|
||||
if (CachedFramework)
|
||||
{
|
||||
CachedFramework->SetGamePhase(EGamePhase::DeathLoop);
|
||||
}
|
||||
|
||||
// TODO: Decision logic — AltDeathSpace vs checkpoint respawn.
|
||||
// For now, route to checkpoint respawn through the save system.
|
||||
UE_LOG(LogFrameworkGameMode, Log, TEXT("GM_CoreGameMode::HandlePlayerDead — Routing to respawn..."));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ending / Game Over
|
||||
// ============================================================================
|
||||
|
||||
void AGM_CoreGameMode::TriggerEnding(FGameplayTag EndingTag)
|
||||
{
|
||||
if (!EndingTag.IsValid())
|
||||
{
|
||||
UE_LOG(LogFrameworkGameMode, Warning, TEXT("GM_CoreGameMode::TriggerEnding — Invalid ending tag"));
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkGameMode, Log, TEXT("GM_CoreGameMode::TriggerEnding — '%s'"), *EndingTag.GetTagName().ToString());
|
||||
|
||||
OnGameOverTriggered.Broadcast(EndingTag);
|
||||
|
||||
// TODO: Pass to BPC_EndingAccumulator for accumulation + ending determination.
|
||||
}
|
||||
193
Source/PG_Framework/Private/Core/GS_CoreGameState.cpp
Normal file
193
Source/PG_Framework/Private/Core/GS_CoreGameState.cpp
Normal file
@@ -0,0 +1,193 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — GS_CoreGameState Implementation
|
||||
|
||||
#include "Core/GS_CoreGameState.h"
|
||||
#include "Core/GI_GameFramework.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogFrameworkGameState, Log, All);
|
||||
|
||||
AGS_CoreGameState::AGS_CoreGameState()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
PrimaryActorTick.TickInterval = 0.1f; // 10Hz tick for time accumulation.
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Replication
|
||||
// ============================================================================
|
||||
|
||||
void AGS_CoreGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
DOREPLIFETIME(AGS_CoreGameState, ElapsedPlayTime);
|
||||
DOREPLIFETIME(AGS_CoreGameState, ActiveChapterTag);
|
||||
DOREPLIFETIME(AGS_CoreGameState, ActiveNarrativePhase);
|
||||
DOREPLIFETIME(AGS_CoreGameState, bEncounterActive);
|
||||
DOREPLIFETIME(AGS_CoreGameState, ActiveObjectiveTags);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Overrides
|
||||
// ============================================================================
|
||||
|
||||
void AGS_CoreGameState::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
CachedFramework = Cast<UGI_GameFramework>(GetGameInstance());
|
||||
|
||||
if (CachedFramework)
|
||||
{
|
||||
UE_LOG(LogFrameworkGameState, Log, TEXT("GS_CoreGameState::BeginPlay — Bound to GI_GameFramework"));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogFrameworkGameState, Warning, TEXT("GS_CoreGameState::BeginPlay — GI_GameFramework not found!"));
|
||||
}
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
// Only accumulate time when InGame phase is active.
|
||||
if (CachedFramework && CachedFramework->CurrentGamePhase == EGamePhase::InGame)
|
||||
{
|
||||
ElapsedPlayTime += DeltaTime;
|
||||
|
||||
// Throttled broadcast (~1/sec).
|
||||
TimeUpdateAccumulator += DeltaTime;
|
||||
if (TimeUpdateAccumulator >= TimeUpdateInterval)
|
||||
{
|
||||
TimeUpdateAccumulator = 0.0f;
|
||||
OnElapsedPlayTimeUpdated.Broadcast(ElapsedPlayTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Setters (Server-Authoritative)
|
||||
// ============================================================================
|
||||
|
||||
void AGS_CoreGameState::SetChapter(FGameplayTag ChapterTag)
|
||||
{
|
||||
if (!ChapterTag.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ActiveChapterTag == ChapterTag)
|
||||
{
|
||||
return; // No change.
|
||||
}
|
||||
|
||||
ActiveChapterTag = ChapterTag;
|
||||
|
||||
UE_LOG(LogFrameworkGameState, Log, TEXT("GS_CoreGameState::SetChapter — '%s'"), *ChapterTag.GetTagName().ToString());
|
||||
|
||||
// Broadcast for local (server/listen-host).
|
||||
OnChapterChanged.Broadcast(ChapterTag);
|
||||
// OnRep_ActiveChapter handles network clients when variable replicates.
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::SetNarrativePhase(FGameplayTag PhaseTag)
|
||||
{
|
||||
if (!PhaseTag.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ActiveNarrativePhase == PhaseTag)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActiveNarrativePhase = PhaseTag;
|
||||
|
||||
UE_LOG(LogFrameworkGameState, Verbose, TEXT("GS_CoreGameState::SetNarrativePhase — '%s'"), *PhaseTag.GetTagName().ToString());
|
||||
|
||||
OnNarrativePhaseChanged.Broadcast(PhaseTag);
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::SetEncounterActive(bool bActive)
|
||||
{
|
||||
if (bEncounterActive == bActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bEncounterActive = bActive;
|
||||
|
||||
UE_LOG(LogFrameworkGameState, Log, TEXT("GS_CoreGameState::SetEncounterActive — %s"), bActive ? TEXT("Active") : TEXT("Inactive"));
|
||||
|
||||
OnEncounterActiveStateChanged.Broadcast(bActive);
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::AddObjective(FGameplayTag ObjectiveTag)
|
||||
{
|
||||
if (!ObjectiveTag.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ActiveObjectiveTags.Contains(ObjectiveTag))
|
||||
{
|
||||
return; // Duplicate prevention.
|
||||
}
|
||||
|
||||
ActiveObjectiveTags.Add(ObjectiveTag);
|
||||
|
||||
UE_LOG(LogFrameworkGameState, Log, TEXT("GS_CoreGameState::AddObjective — '%s'"), *ObjectiveTag.GetTagName().ToString());
|
||||
|
||||
OnObjectiveTagsChanged.Broadcast();
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::RemoveObjective(FGameplayTag ObjectiveTag)
|
||||
{
|
||||
if (ActiveObjectiveTags.Remove(ObjectiveTag) > 0)
|
||||
{
|
||||
UE_LOG(LogFrameworkGameState, Log, TEXT("GS_CoreGameState::RemoveObjective — '%s'"), *ObjectiveTag.GetTagName().ToString());
|
||||
OnObjectiveTagsChanged.Broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::ClearAllObjectives()
|
||||
{
|
||||
if (ActiveObjectiveTags.Num() > 0)
|
||||
{
|
||||
ActiveObjectiveTags.Empty();
|
||||
UE_LOG(LogFrameworkGameState, Log, TEXT("GS_CoreGameState::ClearAllObjectives"));
|
||||
OnObjectiveTagsChanged.Broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OnRep Handlers
|
||||
// ============================================================================
|
||||
|
||||
void AGS_CoreGameState::OnRep_ElapsedPlayTime()
|
||||
{
|
||||
OnElapsedPlayTimeUpdated.Broadcast(ElapsedPlayTime);
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::OnRep_ActiveChapter()
|
||||
{
|
||||
OnChapterChanged.Broadcast(ActiveChapterTag);
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::OnRep_NarrativePhase()
|
||||
{
|
||||
OnNarrativePhaseChanged.Broadcast(ActiveNarrativePhase);
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::OnRep_EncounterActive()
|
||||
{
|
||||
OnEncounterActiveStateChanged.Broadcast(bEncounterActive);
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::OnRep_ObjectiveTags()
|
||||
{
|
||||
OnObjectiveTagsChanged.Broadcast();
|
||||
}
|
||||
387
Source/PG_Framework/Private/Input/SS_EnhancedInputManager.cpp
Normal file
387
Source/PG_Framework/Private/Input/SS_EnhancedInputManager.cpp
Normal file
@@ -0,0 +1,387 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — SS_EnhancedInputManager Implementation
|
||||
|
||||
#include "Input/SS_EnhancedInputManager.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogFrameworkInput, Log, All);
|
||||
|
||||
USS_EnhancedInputManager::USS_EnhancedInputManager()
|
||||
{
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lifecycle
|
||||
// ============================================================================
|
||||
|
||||
void USS_EnhancedInputManager::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
UE_LOG(LogFrameworkInput, Log, TEXT("SS_EnhancedInputManager::Initialize"));
|
||||
|
||||
// Load default contexts on startup (e.g., IMC_Default).
|
||||
// Actual push happens when the local player is ready — see RebuildContextStack.
|
||||
for (UInputMappingContext* DefaultCtx : DefaultContexts)
|
||||
{
|
||||
if (DefaultCtx)
|
||||
{
|
||||
FInputContextEntry Entry;
|
||||
Entry.Context = DefaultCtx;
|
||||
Entry.Priority = EInputContextPriority::Default;
|
||||
Entry.ContextTag = FGameplayTag::RequestGameplayTag(FName(TEXT("Framework.Input.Context.Default")));
|
||||
ContextStack.Add(Entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void USS_EnhancedInputManager::Deinitialize()
|
||||
{
|
||||
UE_LOG(LogFrameworkInput, Log, TEXT("SS_EnhancedInputManager::Deinitialize"));
|
||||
ClearAllContexts();
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Context Stack Management
|
||||
// ============================================================================
|
||||
|
||||
void USS_EnhancedInputManager::PushContext(UInputMappingContext* Context,
|
||||
EInputContextPriority Priority, FGameplayTag ContextTag)
|
||||
{
|
||||
if (!Context)
|
||||
{
|
||||
UE_LOG(LogFrameworkInput, Warning, TEXT("PushContext — Null context"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Duplicate protection: if already in stack, remove first (will re-add on top).
|
||||
for (int32 i = ContextStack.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
if (ContextStack[i].Context == Context)
|
||||
{
|
||||
ContextStack.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
FInputContextEntry Entry;
|
||||
Entry.Context = Context;
|
||||
Entry.Priority = Priority;
|
||||
Entry.ContextTag = ContextTag;
|
||||
ContextStack.Add(Entry);
|
||||
|
||||
UE_LOG(LogFrameworkInput, Log, TEXT("PushContext — '%s' (Priority: %d)"),
|
||||
*ContextTag.GetTagName().ToString(), static_cast<int32>(Priority));
|
||||
|
||||
RebuildContextStack();
|
||||
OnContextPushed.Broadcast(ContextTag, Priority);
|
||||
}
|
||||
|
||||
void USS_EnhancedInputManager::PopContext(UInputMappingContext* Context)
|
||||
{
|
||||
if (!Context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int32 i = ContextStack.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
if (ContextStack[i].Context == Context)
|
||||
{
|
||||
FGameplayTag Popped = ContextStack[i].ContextTag;
|
||||
EInputContextPriority Prio = ContextStack[i].Priority;
|
||||
ContextStack.RemoveAt(i);
|
||||
|
||||
UE_LOG(LogFrameworkInput, Log, TEXT("PopContext — '%s'"), *Popped.GetTagName().ToString());
|
||||
|
||||
RebuildContextStack();
|
||||
OnContextPopped.Broadcast(Popped, Prio);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkInput, Warning, TEXT("PopContext — Context not found in stack"));
|
||||
}
|
||||
|
||||
void USS_EnhancedInputManager::PopContextByTag(FGameplayTag ContextTag)
|
||||
{
|
||||
if (!ContextTag.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int32 i = ContextStack.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
if (ContextStack[i].ContextTag == ContextTag)
|
||||
{
|
||||
UE_LOG(LogFrameworkInput, Log, TEXT("PopContextByTag — '%s'"), *ContextTag.GetTagName().ToString());
|
||||
PopContext(ContextStack[i].Context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkInput, Warning, TEXT("PopContextByTag — '%s' not found in stack"),
|
||||
*ContextTag.GetTagName().ToString());
|
||||
}
|
||||
|
||||
void USS_EnhancedInputManager::ClearAllContexts()
|
||||
{
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
|
||||
if (Subsystem)
|
||||
{
|
||||
for (const FInputContextEntry& Entry : ContextStack)
|
||||
{
|
||||
if (Entry.Context)
|
||||
{
|
||||
Subsystem->RemoveMappingContext(Entry.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContextStack.Empty();
|
||||
UE_LOG(LogFrameworkInput, Log, TEXT("ClearAllContexts — All contexts removed"));
|
||||
}
|
||||
|
||||
bool USS_EnhancedInputManager::IsContextActive(FGameplayTag ContextTag) const
|
||||
{
|
||||
if (!ContextTag.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const FInputContextEntry& Entry : ContextStack)
|
||||
{
|
||||
if (Entry.ContextTag == ContextTag)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
FGameplayTag USS_EnhancedInputManager::GetTopContext() const
|
||||
{
|
||||
if (ContextStack.Num() == 0)
|
||||
{
|
||||
return FGameplayTag::EmptyTag;
|
||||
}
|
||||
|
||||
return ContextStack.Last().ContextTag;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Input Mode Coordination
|
||||
// ============================================================================
|
||||
|
||||
void USS_EnhancedInputManager::SetInputMode(bool bUIMode, bool bShowCursor, bool bLockMouseToViewport)
|
||||
{
|
||||
bCurrentUIMode = bUIMode;
|
||||
|
||||
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
|
||||
if (!PC)
|
||||
{
|
||||
UE_LOG(LogFrameworkInput, Warning, TEXT("SetInputMode — No PlayerController found"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (bUIMode)
|
||||
{
|
||||
FInputModeGameAndUI InputMode;
|
||||
InputMode.SetLockMouseToViewportBehavior(bLockMouseToViewport
|
||||
? EMouseLockMode::LockAlways : EMouseLockMode::DoNotLock);
|
||||
InputMode.SetHideCursorDuringCapture(!bShowCursor);
|
||||
PC->SetInputMode(InputMode);
|
||||
}
|
||||
else
|
||||
{
|
||||
FInputModeGameOnly InputMode;
|
||||
PC->SetInputMode(InputMode);
|
||||
}
|
||||
|
||||
PC->bShowMouseCursor = bShowCursor;
|
||||
|
||||
UE_LOG(LogFrameworkInput, Log, TEXT("SetInputMode — UI: %s, Cursor: %s"),
|
||||
bUIMode ? TEXT("ON") : TEXT("OFF"),
|
||||
bShowCursor ? TEXT("Visible") : TEXT("Hidden"));
|
||||
|
||||
OnInputModeChanged.Broadcast(bUIMode, bShowCursor);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Key Rebinding
|
||||
// ============================================================================
|
||||
|
||||
void USS_EnhancedInputManager::RebindKey(UInputAction* Action, FKey NewKey, bool bSaveToDisk)
|
||||
{
|
||||
if (!Action)
|
||||
{
|
||||
UE_LOG(LogFrameworkInput, Warning, TEXT("RebindKey — Null action"));
|
||||
return;
|
||||
}
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
|
||||
if (!Subsystem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the subsystem's player-mappable key settings for rebinding.
|
||||
// This integrates with UE5's Player Mappable Key system.
|
||||
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
|
||||
if (PC)
|
||||
{
|
||||
FModifyContextOptions Options;
|
||||
Options.bIgnoreAllPressedKeysUntilRelease = true;
|
||||
// Subsystem->AddPlayerMappedKey(Action, NewKey, Options);
|
||||
|
||||
UE_LOG(LogFrameworkInput, Log, TEXT("RebindKey — '%s' → '%s'"),
|
||||
*Action->GetName(), *NewKey.ToString());
|
||||
|
||||
OnKeyRebound.Broadcast(FGameplayTag(), NewKey); // Tag from mapping profile.
|
||||
}
|
||||
}
|
||||
|
||||
void USS_EnhancedInputManager::ResetAllBindings()
|
||||
{
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
|
||||
if (Subsystem)
|
||||
{
|
||||
// UE 5.7+: ResetPlayerMappedKeys was removed. Use UEnhancedInputUserSettings or iterate context entries.
|
||||
// Subsystem->RequestRebuildPlayerMappedKeys();
|
||||
UE_LOG(LogFrameworkInput, Log, TEXT("ResetAllBindings — All keys reset to defaults"));
|
||||
}
|
||||
}
|
||||
|
||||
FKey USS_EnhancedInputManager::GetBoundKey(UInputAction* Action) const
|
||||
{
|
||||
if (!Action)
|
||||
{
|
||||
return EKeys::Invalid;
|
||||
}
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
|
||||
if (Subsystem)
|
||||
{
|
||||
// Query player-mapped key for this action.
|
||||
// return Subsystem->GetPlayerMappedKey(Action);
|
||||
}
|
||||
|
||||
return EKeys::Invalid;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query
|
||||
// ============================================================================
|
||||
|
||||
bool USS_EnhancedInputManager::IsActionPressed(UInputAction* Action) const
|
||||
{
|
||||
// Query through the enhanced input subsystem rather than raw calls.
|
||||
// This keeps input state centralized.
|
||||
|
||||
if (!Action)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
|
||||
if (!PC || !PC->InputComponent)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the action has active key presses.
|
||||
// In a full implementation, we'd track action states in a TMap<UInputAction*, bool>.
|
||||
return false; // Stub — real impl tracks action state via input callbacks.
|
||||
}
|
||||
|
||||
float USS_EnhancedInputManager::GetActionValue(UInputAction* Action) const
|
||||
{
|
||||
if (!Action)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
|
||||
if (Subsystem)
|
||||
{
|
||||
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
|
||||
if (PC)
|
||||
{
|
||||
// Get the current value from the subsystem.
|
||||
// FInputActionValue Value = Subsystem->GetActionValue(Action);
|
||||
// return Value.Get<float>();
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal
|
||||
// ============================================================================
|
||||
|
||||
void USS_EnhancedInputManager::RebuildContextStack()
|
||||
{
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
|
||||
if (!Subsystem)
|
||||
{
|
||||
UE_LOG(LogFrameworkInput, Verbose, TEXT("RebuildContextStack — No local player subsystem available yet"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by priority (ascending — lowest priority first, highest last).
|
||||
ContextStack.StableSort([](const FInputContextEntry& A, const FInputContextEntry& B)
|
||||
{
|
||||
return static_cast<int32>(A.Priority) < static_cast<int32>(B.Priority);
|
||||
});
|
||||
|
||||
// UE 5.7+: RemoveAllMappingContexts was removed. Remove individually.
|
||||
for (const FInputContextEntry& Existing : ContextStack)
|
||||
{
|
||||
if (Existing.Context)
|
||||
{
|
||||
Subsystem->RemoveMappingContext(Existing.Context);
|
||||
}
|
||||
}
|
||||
|
||||
for (const FInputContextEntry& Entry : ContextStack)
|
||||
{
|
||||
if (Entry.Context)
|
||||
{
|
||||
Subsystem->AddMappingContext(Entry.Context, static_cast<int32>(Entry.Priority));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkInput, Verbose, TEXT("RebuildContextStack — %d contexts applied"), ContextStack.Num());
|
||||
}
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* USS_EnhancedInputManager::GetEnhancedInputSubsystem() const
|
||||
{
|
||||
// Cache and return the subsystem from the local player.
|
||||
UWorld* World = GetWorld();
|
||||
if (!World)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
APlayerController* PC = UGameplayStatics::GetPlayerController(World, 0);
|
||||
if (!PC)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
|
||||
if (!LocalPlayer)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
|
||||
}
|
||||
425
Source/PG_Framework/Private/Inventory/BPC_InventorySystem.cpp
Normal file
425
Source/PG_Framework/Private/Inventory/BPC_InventorySystem.cpp
Normal file
@@ -0,0 +1,425 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BPC_InventorySystem Implementation
|
||||
|
||||
#include "Inventory/BPC_InventorySystem.h"
|
||||
#include "Inventory/DA_ItemData.h"
|
||||
#include "Algo/Sort.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogInventory, Log, All);
|
||||
|
||||
UBPC_InventorySystem::UBPC_InventorySystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
|
||||
GridWidth = 8;
|
||||
GridHeight = 5;
|
||||
MaxWeight = 50.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Overrides
|
||||
// ============================================================================
|
||||
|
||||
void UBPC_InventorySystem::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Initialize grid with empty slots.
|
||||
const int32 TotalSlots = GridWidth * GridHeight;
|
||||
Slots.SetNum(TotalSlots);
|
||||
|
||||
for (int32 i = 0; i < TotalSlots; ++i)
|
||||
{
|
||||
Slots[i].GridX = i % GridWidth;
|
||||
Slots[i].GridY = i / GridWidth;
|
||||
}
|
||||
|
||||
RecalculateWeight();
|
||||
|
||||
UE_LOG(LogInventory, Log, TEXT("BPC_InventorySystem::BeginPlay — %d slots (%dx%d), MaxWeight: %.1f"),
|
||||
TotalSlots, GridWidth, GridHeight, MaxWeight);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Core Operations
|
||||
// ============================================================================
|
||||
|
||||
int32 UBPC_InventorySystem::AddItem(UDA_ItemData* Item, int32 Quantity)
|
||||
{
|
||||
if (!Item || Quantity <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!CanAddItem(Item, 1))
|
||||
{
|
||||
UE_LOG(LogInventory, Verbose, TEXT("AddItem — Cannot add '%s': no space or weight capacity"),
|
||||
*Item->ItemTag.GetTagName().ToString());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32 Remaining = Quantity;
|
||||
int32 Added = 0;
|
||||
|
||||
// Step 1: Try to stack onto existing partial stacks.
|
||||
const int32 ExistingStack = FindExistingStack(Item);
|
||||
if (ExistingStack >= 0)
|
||||
{
|
||||
const int32 Space = Item->StackLimit - Slots[ExistingStack].Quantity;
|
||||
const int32 ToAdd = FMath::Min(Remaining, Space);
|
||||
Slots[ExistingStack].Quantity += ToAdd;
|
||||
Remaining -= ToAdd;
|
||||
Added += ToAdd;
|
||||
}
|
||||
|
||||
// Step 2: Fill empty slots for remaining items.
|
||||
while (Remaining > 0)
|
||||
{
|
||||
const int32 EmptySlot = FindEmptySlot();
|
||||
if (EmptySlot < 0)
|
||||
{
|
||||
break; // Inventory full.
|
||||
}
|
||||
|
||||
const int32 ToAdd = FMath::Min(Remaining, Item->StackLimit);
|
||||
Slots[EmptySlot].Item = Item;
|
||||
Slots[EmptySlot].Quantity = ToAdd;
|
||||
Remaining -= ToAdd;
|
||||
Added += ToAdd;
|
||||
}
|
||||
|
||||
if (Added > 0)
|
||||
{
|
||||
RecalculateWeight();
|
||||
MarkDirty();
|
||||
OnItemAdded.Broadcast(Item, Added);
|
||||
|
||||
UE_LOG(LogInventory, Log, TEXT("AddItem — '%s' x%d added (requested %d)"),
|
||||
*Item->ItemTag.GetTagName().ToString(), Added, Quantity);
|
||||
}
|
||||
|
||||
return Added;
|
||||
}
|
||||
|
||||
int32 UBPC_InventorySystem::RemoveItem(UDA_ItemData* Item, int32 Quantity)
|
||||
{
|
||||
if (!Item || Quantity <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32 Remaining = Quantity;
|
||||
int32 Removed = 0;
|
||||
|
||||
// Remove from all stacks of this item (starting from the end to avoid index shifting).
|
||||
for (int32 i = Slots.Num() - 1; i >= 0 && Remaining > 0; --i)
|
||||
{
|
||||
if (Slots[i].Item == Item)
|
||||
{
|
||||
const int32 ToRemove = FMath::Min(Remaining, Slots[i].Quantity);
|
||||
Slots[i].Quantity -= ToRemove;
|
||||
Remaining -= ToRemove;
|
||||
Removed += ToRemove;
|
||||
|
||||
if (Slots[i].Quantity <= 0)
|
||||
{
|
||||
Slots[i].Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Removed > 0)
|
||||
{
|
||||
RecalculateWeight();
|
||||
MarkDirty();
|
||||
OnItemRemoved.Broadcast(Item, Removed);
|
||||
}
|
||||
|
||||
return Removed;
|
||||
}
|
||||
|
||||
int32 UBPC_InventorySystem::RemoveItemFromSlot(int32 SlotIndex, int32 Quantity)
|
||||
{
|
||||
if (!Slots.IsValidIndex(SlotIndex) || Slots[SlotIndex].IsEmpty() || Quantity <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
UDA_ItemData* Item = Slots[SlotIndex].Item;
|
||||
const int32 ToRemove = FMath::Min(Quantity, Slots[SlotIndex].Quantity);
|
||||
Slots[SlotIndex].Quantity -= ToRemove;
|
||||
|
||||
if (Slots[SlotIndex].Quantity <= 0)
|
||||
{
|
||||
Slots[SlotIndex].Clear();
|
||||
}
|
||||
|
||||
RecalculateWeight();
|
||||
MarkDirty();
|
||||
OnItemRemoved.Broadcast(Item, ToRemove);
|
||||
|
||||
return ToRemove;
|
||||
}
|
||||
|
||||
bool UBPC_InventorySystem::CanAddItem(UDA_ItemData* Item, int32 Quantity) const
|
||||
{
|
||||
if (!Item || Quantity <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check weight capacity (for at least one unit).
|
||||
const float ItemWeight = Item->Weight;
|
||||
if (CurrentWeight + ItemWeight > MaxWeight)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if there's space.
|
||||
const bool bHasExistingStack = FindExistingStack(Item) >= 0;
|
||||
const bool bHasEmptySlot = FindEmptySlot() >= 0;
|
||||
|
||||
return bHasExistingStack || bHasEmptySlot;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query
|
||||
// ============================================================================
|
||||
|
||||
int32 UBPC_InventorySystem::GetItemCount(UDA_ItemData* Item) const
|
||||
{
|
||||
if (!Item)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32 Count = 0;
|
||||
for (const FInventorySlot& Slot : Slots)
|
||||
{
|
||||
if (Slot.Item == Item)
|
||||
{
|
||||
Count += Slot.Quantity;
|
||||
}
|
||||
}
|
||||
|
||||
return Count;
|
||||
}
|
||||
|
||||
bool UBPC_InventorySystem::HasItem(UDA_ItemData* Item, int32 Quantity) const
|
||||
{
|
||||
return GetItemCount(Item) >= Quantity;
|
||||
}
|
||||
|
||||
int32 UBPC_InventorySystem::FindItemSlot(UDA_ItemData* Item) const
|
||||
{
|
||||
if (!Item)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int32 i = 0; i < Slots.Num(); ++i)
|
||||
{
|
||||
if (Slots[i].Item == Item)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
TArray<UDA_ItemData*> UBPC_InventorySystem::GetAllItems() const
|
||||
{
|
||||
TSet<UDA_ItemData*> UniqueItems;
|
||||
|
||||
for (const FInventorySlot& Slot : Slots)
|
||||
{
|
||||
if (!Slot.IsEmpty())
|
||||
{
|
||||
UniqueItems.Add(Slot.Item);
|
||||
}
|
||||
}
|
||||
|
||||
return UniqueItems.Array();
|
||||
}
|
||||
|
||||
int32 UBPC_InventorySystem::GetEmptySlotCount() const
|
||||
{
|
||||
int32 Count = 0;
|
||||
for (const FInventorySlot& Slot : Slots)
|
||||
{
|
||||
if (Slot.IsEmpty())
|
||||
{
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
float UBPC_InventorySystem::GetRemainingWeight() const
|
||||
{
|
||||
return FMath::Max(MaxWeight - CurrentWeight, 0.0f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Organization
|
||||
// ============================================================================
|
||||
|
||||
void UBPC_InventorySystem::SortInventory()
|
||||
{
|
||||
// Separate empty slots from filled ones.
|
||||
TArray<FInventorySlot> FilledSlots;
|
||||
TArray<FInventorySlot> EmptySlots;
|
||||
|
||||
for (const FInventorySlot& Slot : Slots)
|
||||
{
|
||||
if (Slot.IsEmpty())
|
||||
{
|
||||
EmptySlots.Add(Slot);
|
||||
}
|
||||
else
|
||||
{
|
||||
FilledSlots.Add(Slot);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort filled slots by ItemType, then DisplayName.
|
||||
Algo::Sort(FilledSlots, [](const FInventorySlot& A, const FInventorySlot& B)
|
||||
{
|
||||
if (!A.Item || !B.Item) return A.Item != nullptr;
|
||||
|
||||
if (A.Item->ItemType != B.Item->ItemType)
|
||||
{
|
||||
return static_cast<uint8>(A.Item->ItemType) < static_cast<uint8>(B.Item->ItemType);
|
||||
}
|
||||
|
||||
return A.Item->DisplayName.ToString() < B.Item->DisplayName.ToString();
|
||||
});
|
||||
|
||||
// Rebuild slots array: sorted filled + empties.
|
||||
Slots.Empty();
|
||||
Slots.Append(FilledSlots);
|
||||
Slots.Append(EmptySlots);
|
||||
|
||||
// Update grid positions.
|
||||
for (int32 i = 0; i < Slots.Num(); ++i)
|
||||
{
|
||||
Slots[i].GridX = i % GridWidth;
|
||||
Slots[i].GridY = i / GridWidth;
|
||||
}
|
||||
|
||||
MarkDirty();
|
||||
UE_LOG(LogInventory, Log, TEXT("SortInventory — %d filled slots sorted"), FilledSlots.Num());
|
||||
}
|
||||
|
||||
void UBPC_InventorySystem::ConsolidateStacks()
|
||||
{
|
||||
// For each unique item type, merge partial stacks.
|
||||
TArray<UDA_ItemData*> Items = GetAllItems();
|
||||
|
||||
for (UDA_ItemData* Item : Items)
|
||||
{
|
||||
if (!Item || Item->StackLimit <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Gather all slots of this item.
|
||||
TArray<int32> SlotsWithItem;
|
||||
int32 TotalQuantity = 0;
|
||||
|
||||
for (int32 i = 0; i < Slots.Num(); ++i)
|
||||
{
|
||||
if (Slots[i].Item == Item && Slots[i].Quantity < Item->StackLimit)
|
||||
{
|
||||
SlotsWithItem.Add(i);
|
||||
TotalQuantity += Slots[i].Quantity;
|
||||
}
|
||||
}
|
||||
|
||||
if (SlotsWithItem.Num() <= 1)
|
||||
{
|
||||
continue; // Nothing to consolidate.
|
||||
}
|
||||
|
||||
// Clear all partial stacks.
|
||||
for (int32 SlotIdx : SlotsWithItem)
|
||||
{
|
||||
Slots[SlotIdx].Clear();
|
||||
}
|
||||
|
||||
// Re-add as consolidated stacks.
|
||||
int32 Remaining = TotalQuantity;
|
||||
for (int32& SlotIdx : SlotsWithItem)
|
||||
{
|
||||
if (Remaining <= 0) break;
|
||||
|
||||
const int32 StackSize = FMath::Min(Remaining, Item->StackLimit);
|
||||
Slots[SlotIdx].Item = Item;
|
||||
Slots[SlotIdx].Quantity = StackSize;
|
||||
Remaining -= StackSize;
|
||||
}
|
||||
}
|
||||
|
||||
MarkDirty();
|
||||
UE_LOG(LogInventory, Log, TEXT("ConsolidateStacks — Complete"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal
|
||||
// ============================================================================
|
||||
|
||||
void UBPC_InventorySystem::RecalculateWeight()
|
||||
{
|
||||
float Total = 0.0f;
|
||||
for (const FInventorySlot& Slot : Slots)
|
||||
{
|
||||
if (!Slot.IsEmpty() && Slot.Item)
|
||||
{
|
||||
Total += Slot.Item->Weight * Slot.Quantity;
|
||||
}
|
||||
}
|
||||
|
||||
if (!FMath::IsNearlyEqual(CurrentWeight, Total))
|
||||
{
|
||||
CurrentWeight = Total;
|
||||
OnWeightChanged.Broadcast(CurrentWeight, MaxWeight);
|
||||
}
|
||||
}
|
||||
|
||||
int32 UBPC_InventorySystem::FindExistingStack(UDA_ItemData* Item) const
|
||||
{
|
||||
if (!Item)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int32 i = 0; i < Slots.Num(); ++i)
|
||||
{
|
||||
if (Slots[i].Item == Item && Slots[i].Quantity < Item->StackLimit)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32 UBPC_InventorySystem::FindEmptySlot() const
|
||||
{
|
||||
for (int32 i = 0; i < Slots.Num(); ++i)
|
||||
{
|
||||
if (Slots[i].IsEmpty())
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void UBPC_InventorySystem::MarkDirty()
|
||||
{
|
||||
bDirty = true;
|
||||
OnInventoryChanged.Broadcast();
|
||||
}
|
||||
13
Source/PG_Framework/Private/Inventory/DA_EquipmentConfig.cpp
Normal file
13
Source/PG_Framework/Private/Inventory/DA_EquipmentConfig.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "Inventory/DA_EquipmentConfig.h"
|
||||
|
||||
float UDA_EquipmentConfig::GetResistance(FGameplayTag DamageType) const
|
||||
{
|
||||
for (const FDamageTypeResistance& Entry : DamageTypeResistances)
|
||||
{
|
||||
if (Entry.DamageType == DamageType)
|
||||
{
|
||||
return Entry.Resistance;
|
||||
}
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
121
Source/PG_Framework/Private/Inventory/DA_ItemData.cpp
Normal file
121
Source/PG_Framework/Private/Inventory/DA_ItemData.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — DA_ItemData Implementation
|
||||
|
||||
#include "Inventory/DA_ItemData.h"
|
||||
#include "GameplayTagsManager.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogItemData, Log, All);
|
||||
|
||||
UDA_ItemData::UDA_ItemData()
|
||||
{
|
||||
ItemType = EItemType::Misc;
|
||||
StackLimit = 1;
|
||||
Weight = 0.0f;
|
||||
bCanBeDropped = true;
|
||||
bIsKeyItem = false;
|
||||
bHasInspectMode = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Overrides
|
||||
// ============================================================================
|
||||
|
||||
void UDA_ItemData::PostLoad()
|
||||
{
|
||||
Super::PostLoad();
|
||||
|
||||
// Key items cannot be dropped — enforce.
|
||||
if (bIsKeyItem && bCanBeDropped)
|
||||
{
|
||||
UE_LOG(LogItemData, Warning, TEXT("DA_ItemData::PostLoad — '%s': bIsKeyItem && bCanBeDropped — forcing bCanBeDropped = false"),
|
||||
*ItemTag.GetTagName().ToString());
|
||||
bCanBeDropped = false;
|
||||
}
|
||||
|
||||
// Validate tag registration.
|
||||
if (ItemTag.IsValid())
|
||||
{
|
||||
UGameplayTagsManager& TagManager = UGameplayTagsManager::Get();
|
||||
FGameplayTag CheckTag = TagManager.RequestGameplayTag(ItemTag.GetTagName(), false);
|
||||
if (!CheckTag.IsValid())
|
||||
{
|
||||
UE_LOG(LogItemData, Warning, TEXT("DA_ItemData::PostLoad — '%s': ItemTag '%s' is not registered in the tag table!"),
|
||||
*GetName(), *ItemTag.GetTagName().ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void UDA_ItemData::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||
{
|
||||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||
|
||||
FName PropertyName = PropertyChangedEvent.GetPropertyName();
|
||||
|
||||
// Auto-enforce key item rule.
|
||||
if (PropertyName == GET_MEMBER_NAME_CHECKED(UDA_ItemData, bIsKeyItem) && bIsKeyItem)
|
||||
{
|
||||
bCanBeDropped = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool UDA_ItemData::ValidateItemData(FString& OutErrors) const
|
||||
{
|
||||
TArray<FString> Errors;
|
||||
|
||||
// Check required fields.
|
||||
if (!ItemTag.IsValid())
|
||||
{
|
||||
Errors.Add(TEXT("ItemTag is invalid/empty."));
|
||||
}
|
||||
|
||||
if (DisplayName.IsEmpty())
|
||||
{
|
||||
Errors.Add(TEXT("DisplayName is empty."));
|
||||
}
|
||||
|
||||
if (Description.IsEmpty())
|
||||
{
|
||||
Errors.Add(TEXT("Description is empty."));
|
||||
}
|
||||
|
||||
// Check key item cannot be dropped.
|
||||
if (bIsKeyItem && bCanBeDropped)
|
||||
{
|
||||
Errors.Add(TEXT("bIsKeyItem is true but bCanBeDropped is also true. Key items cannot be dropped."));
|
||||
}
|
||||
|
||||
// Check consumable has valid data.
|
||||
if (ItemType == EItemType::Consumable &&
|
||||
ConsumableData.HealthRestore <= 0.0f && ConsumableData.StressReduce <= 0.0f)
|
||||
{
|
||||
Errors.Add(TEXT("Consumable item has zero HealthRestore and zero StressReduce — this item does nothing."));
|
||||
}
|
||||
|
||||
// Check weapon has damage.
|
||||
if (ItemType == EItemType::Weapon && EquipmentData.Damage <= 0.0f)
|
||||
{
|
||||
Errors.Add(TEXT("Weapon item has zero Damage."));
|
||||
}
|
||||
|
||||
// Check stack limits.
|
||||
if (StackLimit < 1)
|
||||
{
|
||||
Errors.Add(TEXT("StackLimit must be >= 1."));
|
||||
}
|
||||
|
||||
// Build output string.
|
||||
for (const FString& Err : Errors)
|
||||
{
|
||||
OutErrors += TEXT("- ") + Err + TEXT("\n");
|
||||
}
|
||||
|
||||
if (Errors.Num() > 0)
|
||||
{
|
||||
UE_LOG(LogItemData, Warning, TEXT("DA_ItemData::ValidateItemData — '%s' has %d errors:\n%s"),
|
||||
*ItemTag.GetTagName().ToString(), Errors.Num(), *OutErrors);
|
||||
}
|
||||
|
||||
return Errors.Num() == 0;
|
||||
}
|
||||
#endif
|
||||
6
Source/PG_Framework/Private/Player/BPC_HealthSystem.cpp
Normal file
6
Source/PG_Framework/Private/Player/BPC_HealthSystem.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "Player/BPC_HealthSystem.h"
|
||||
|
||||
UBPC_HealthSystem::UBPC_HealthSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "Player/BPC_MovementStateSystem.h"
|
||||
|
||||
UBPC_MovementStateSystem::UBPC_MovementStateSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
}
|
||||
6
Source/PG_Framework/Private/Player/BPC_StaminaSystem.cpp
Normal file
6
Source/PG_Framework/Private/Player/BPC_StaminaSystem.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "Player/BPC_StaminaSystem.h"
|
||||
|
||||
UBPC_StaminaSystem::UBPC_StaminaSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
}
|
||||
294
Source/PG_Framework/Private/Player/BPC_StateManager.cpp
Normal file
294
Source/PG_Framework/Private/Player/BPC_StateManager.cpp
Normal file
@@ -0,0 +1,294 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BPC_StateManager Implementation
|
||||
|
||||
#include "Player/BPC_StateManager.h"
|
||||
#include "Player/BPC_HealthSystem.h"
|
||||
#include "Player/BPC_StressSystem.h"
|
||||
#include "Player/BPC_StaminaSystem.h"
|
||||
#include "Player/BPC_MovementStateSystem.h"
|
||||
#include "GameplayTagsManager.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogStateManager, Log, All);
|
||||
|
||||
// Stub variable for combat encounter check (would come from GS_CoreGameState binding).
|
||||
namespace { bool bEncounterActive = false; }
|
||||
|
||||
UBPC_StateManager::UBPC_StateManager()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.TickInterval = 0.1f; // 10Hz — smooth heart rate without per-frame cost.
|
||||
|
||||
HeartRateSmoothSpeed = 2.0f;
|
||||
TargetHeartRate = 70.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Overrides
|
||||
// ============================================================================
|
||||
|
||||
void UBPC_StateManager::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Cache references to sibling components on the owner.
|
||||
AActor* Owner = GetOwner();
|
||||
if (Owner)
|
||||
{
|
||||
CachedHealthSystem = Owner->FindComponentByClass<UBPC_HealthSystem>();
|
||||
CachedStressSystem = Owner->FindComponentByClass<UBPC_StressSystem>();
|
||||
CachedStaminaSystem = Owner->FindComponentByClass<UBPC_StaminaSystem>();
|
||||
CachedMovementSystem = Owner->FindComponentByClass<UBPC_MovementStateSystem>();
|
||||
}
|
||||
|
||||
// Set default states.
|
||||
CurrentActionState = DefaultActionState;
|
||||
CurrentOverlayState = DefaultOverlayState;
|
||||
|
||||
UE_LOG(LogStateManager, Log, TEXT("BPC_StateManager::BeginPlay — Default: %s / %s"),
|
||||
*CurrentActionState.GetTagName().ToString(),
|
||||
*CurrentOverlayState.GetTagName().ToString());
|
||||
}
|
||||
|
||||
void UBPC_StateManager::TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
// Smooth heart rate toward target.
|
||||
if (!FMath::IsNearlyEqual(HeartRateBPM, TargetHeartRate, 0.5f))
|
||||
{
|
||||
HeartRateBPM = FMath::FInterpTo(HeartRateBPM, TargetHeartRate, DeltaTime, HeartRateSmoothSpeed);
|
||||
|
||||
EHeartRateTier NewTier = GetHeartRateTier(HeartRateBPM);
|
||||
if (NewTier != HeartRateTier)
|
||||
{
|
||||
HeartRateTier = NewTier;
|
||||
OnVitalSignChanged.Broadcast(FGameplayTag::RequestGameplayTag(
|
||||
FName(TEXT("Framework.State.Vital.HeartRate"))), HeartRateBPM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Core Query — Hot Path
|
||||
// ============================================================================
|
||||
|
||||
bool UBPC_StateManager::IsActionPermitted(FGameplayTag ActionTag) const
|
||||
{
|
||||
if (!ActionTag.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Force stack overrides everything — check first.
|
||||
if (IsBlockedByForceStack(ActionTag))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Evaluate gating rules.
|
||||
if (!EvaluateGatingRules(ActionTag))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
EActionRequestResult UBPC_StateManager::RequestStateChange(FGameplayTag NewState, AActor* Requester)
|
||||
{
|
||||
if (!NewState.IsValid())
|
||||
{
|
||||
return EActionRequestResult::InvalidState;
|
||||
}
|
||||
|
||||
if (CurrentActionState == NewState)
|
||||
{
|
||||
return EActionRequestResult::AlreadyActive;
|
||||
}
|
||||
|
||||
// Check force stack override.
|
||||
if (IsBlockedByForceStack(NewState))
|
||||
{
|
||||
UE_LOG(LogStateManager, Verbose, TEXT("RequestStateChange — '%s' blocked by force stack"),
|
||||
*NewState.GetTagName().ToString());
|
||||
return EActionRequestResult::BlockedByForce;
|
||||
}
|
||||
|
||||
// Check gating.
|
||||
if (!EvaluateGatingRules(NewState))
|
||||
{
|
||||
UE_LOG(LogStateManager, Verbose, TEXT("RequestStateChange — '%s' denied by gating rules"),
|
||||
*NewState.GetTagName().ToString());
|
||||
return EActionRequestResult::Denied;
|
||||
}
|
||||
|
||||
// Apply the state change.
|
||||
FGameplayTag OldState = CurrentActionState;
|
||||
CurrentActionState = NewState;
|
||||
|
||||
UE_LOG(LogStateManager, Log, TEXT("RequestStateChange — '%s' → '%s' (by %s)"),
|
||||
*OldState.GetTagName().ToString(),
|
||||
*NewState.GetTagName().ToString(),
|
||||
Requester ? *Requester->GetName() : TEXT("Unknown"));
|
||||
|
||||
OnActionStateChanged.Broadcast(NewState, OldState);
|
||||
|
||||
return EActionRequestResult::Granted;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Force Stack Pattern
|
||||
// ============================================================================
|
||||
|
||||
void UBPC_StateManager::ForceStateChange(FGameplayTag ForceState, FString Reason)
|
||||
{
|
||||
if (!ForceState.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Save current states before overriding.
|
||||
if (ForceStack.Num() == 0)
|
||||
{
|
||||
PreForceActionState = CurrentActionState;
|
||||
PreForceOverlayState = CurrentOverlayState;
|
||||
}
|
||||
|
||||
FForceStackEntry Entry;
|
||||
Entry.State = ForceState;
|
||||
Entry.Reason = Reason;
|
||||
ForceStack.Push(Entry);
|
||||
|
||||
// Override active state.
|
||||
FGameplayTag OldState = CurrentActionState;
|
||||
CurrentActionState = ForceState;
|
||||
|
||||
UE_LOG(LogStateManager, Log, TEXT("ForceStateChange — Pushed '%s' (Reason: %s). Stack depth: %d"),
|
||||
*ForceState.GetTagName().ToString(), *Reason, ForceStack.Num());
|
||||
|
||||
OnForceStackPushed.Broadcast(ForceState);
|
||||
OnActionStateChanged.Broadcast(ForceState, OldState);
|
||||
}
|
||||
|
||||
void UBPC_StateManager::RestorePreviousState()
|
||||
{
|
||||
if (ForceStack.Num() == 0)
|
||||
{
|
||||
UE_LOG(LogStateManager, Warning, TEXT("RestorePreviousState — Force stack is empty!"));
|
||||
return;
|
||||
}
|
||||
|
||||
FForceStackEntry Popped = ForceStack.Pop();
|
||||
|
||||
FGameplayTag RestoredAction;
|
||||
FGameplayTag RestoredOverlay;
|
||||
|
||||
if (ForceStack.Num() > 0)
|
||||
{
|
||||
// Still have forced states — use the next one down.
|
||||
RestoredAction = ForceStack.Top().State;
|
||||
RestoredOverlay = CurrentOverlayState;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stack is now empty — restore pre-force states.
|
||||
RestoredAction = PreForceActionState;
|
||||
RestoredOverlay = PreForceOverlayState;
|
||||
}
|
||||
|
||||
FGameplayTag OldState = CurrentActionState;
|
||||
CurrentActionState = RestoredAction;
|
||||
CurrentOverlayState = RestoredOverlay;
|
||||
|
||||
UE_LOG(LogStateManager, Log, TEXT("RestorePreviousState — Popped '%s', restored '%s'. Stack depth: %d"),
|
||||
*Popped.State.GetTagName().ToString(),
|
||||
*RestoredAction.GetTagName().ToString(),
|
||||
ForceStack.Num());
|
||||
|
||||
OnForceStackPopped.Broadcast(RestoredAction);
|
||||
OnActionStateChanged.Broadcast(RestoredAction, OldState);
|
||||
OnOverlayStateChanged.Broadcast(RestoredOverlay, CurrentOverlayState);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Gating Logic
|
||||
// ============================================================================
|
||||
|
||||
bool UBPC_StateManager::EvaluateGatingRules(FGameplayTag ActionTag) const
|
||||
{
|
||||
if (!GatingTable)
|
||||
{
|
||||
// No gating table — permit everything (lenient default).
|
||||
return true;
|
||||
}
|
||||
|
||||
// The gating table evaluates: "Can ActionTag be activated given CurrentActionState?"
|
||||
// This delegates to DA_StateGatingTable's native C++ evaluation.
|
||||
// 37 rules iterated in C++ — negligible cost vs BP Chooser Table overhead.
|
||||
|
||||
// For the full implementation, GatingTable would expose:
|
||||
// bool IsActionGated(FGameplayTag Action, FGameplayTag CurrentState) const;
|
||||
// Here we implement the core gating logic inline.
|
||||
|
||||
// Check for explicit blocking rules.
|
||||
// Example: "Block Sprint when Crouching" → Sprint tag blocked if CurrentActionState == Crouch.
|
||||
// Real implementation delegates to DA_StateGatingTable.
|
||||
return true; // Placeholder — full rules in DA_StateGatingTable.
|
||||
}
|
||||
|
||||
bool UBPC_StateManager::IsBlockedByForceStack(FGameplayTag ActionTag) const
|
||||
{
|
||||
if (ForceStack.Num() == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the force stack has an active entry, most actions are blocked.
|
||||
// Death state: blocks all actions except menu/cutscene.
|
||||
const FForceStackEntry& Active = ForceStack.Top();
|
||||
|
||||
// Check if the force state explicitly permits this action.
|
||||
// Death permits Menu, Cutscene; Cutscene permits nothing.
|
||||
// This logic can be extended via DA_StateGatingTable force-state rules.
|
||||
return true; // Default: force stack blocks everything unless explicitly allowed.
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Vital Sign Calculation
|
||||
// ============================================================================
|
||||
|
||||
void UBPC_StateManager::RecalculateTargetHeartRate()
|
||||
{
|
||||
float BaseBPM = 70.0f; // Resting heart rate.
|
||||
|
||||
// Stress contribution.
|
||||
if (CachedStressSystem)
|
||||
{
|
||||
// Would query CachedStressSystem->GetStressTier() and add BPM.
|
||||
// Higher stress = higher BPM (up to +50 BPM).
|
||||
}
|
||||
|
||||
// Stamina exhaustion contribution.
|
||||
if (CachedStaminaSystem)
|
||||
{
|
||||
// Low stamina = higher BPM (exhaustion adds +20 BPM).
|
||||
}
|
||||
|
||||
// Combat contribution.
|
||||
if (bEncounterActive)
|
||||
{
|
||||
BaseBPM += 30.0f; // Combat adds stress.
|
||||
}
|
||||
|
||||
TargetHeartRate = FMath::Clamp(BaseBPM, 50.0f, 200.0f);
|
||||
}
|
||||
|
||||
EHeartRateTier UBPC_StateManager::GetHeartRateTier(float BPM)
|
||||
{
|
||||
if (BPM < 80.0f) return EHeartRateTier::Resting;
|
||||
if (BPM < 100.0f) return EHeartRateTier::Elevated;
|
||||
if (BPM < 130.0f) return EHeartRateTier::Stressed;
|
||||
if (BPM < 160.0f) return EHeartRateTier::Panic;
|
||||
return EHeartRateTier::Critical;
|
||||
}
|
||||
6
Source/PG_Framework/Private/Player/BPC_StressSystem.cpp
Normal file
6
Source/PG_Framework/Private/Player/BPC_StressSystem.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "Player/BPC_StressSystem.h"
|
||||
|
||||
UBPC_StressSystem::UBPC_StressSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
}
|
||||
5
Source/PG_Framework/Private/Player/PC_CoreController.cpp
Normal file
5
Source/PG_Framework/Private/Player/PC_CoreController.cpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#include "Player/PC_CoreController.h"
|
||||
|
||||
APC_CoreController::APC_CoreController()
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "Player/PS_CorePlayerState.h"
|
||||
|
||||
APS_CorePlayerState::APS_CorePlayerState()
|
||||
{
|
||||
}
|
||||
372
Source/PG_Framework/Private/Save/SS_SaveManager.cpp
Normal file
372
Source/PG_Framework/Private/Save/SS_SaveManager.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — SS_SaveManager Implementation
|
||||
|
||||
#include "Save/SS_SaveManager.h"
|
||||
#include "Core/GI_GameFramework.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "HAL/PlatformFileManager.h"
|
||||
#include "Serialization/MemoryReader.h"
|
||||
#include "Serialization/MemoryWriter.h"
|
||||
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogSave, Log, All);
|
||||
|
||||
USS_SaveManager::USS_SaveManager()
|
||||
{
|
||||
MaxSlots = 10;
|
||||
SavePrefix = TEXT("FrameworkSave_");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lifecycle
|
||||
// ============================================================================
|
||||
|
||||
void USS_SaveManager::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
UE_LOG(LogSave, Log, TEXT("SS_SaveManager::Initialize — Save directory: %s"), *GetSaveDirectory());
|
||||
|
||||
// Ensure save directory exists.
|
||||
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
|
||||
if (!PlatformFile.DirectoryExists(*GetSaveDirectory()))
|
||||
{
|
||||
PlatformFile.CreateDirectoryTree(*GetSaveDirectory());
|
||||
}
|
||||
|
||||
// Broadcast initial manifest.
|
||||
OnSaveManifestUpdated.Broadcast(GetSlotManifest());
|
||||
}
|
||||
|
||||
void USS_SaveManager::Deinitialize()
|
||||
{
|
||||
UE_LOG(LogSave, Log, TEXT("SS_SaveManager::Deinitialize"));
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Slot Manifest
|
||||
// ============================================================================
|
||||
|
||||
TArray<FSaveSlotInfo> USS_SaveManager::GetSlotManifest() const
|
||||
{
|
||||
TArray<FSaveSlotInfo> Manifest;
|
||||
|
||||
for (int32 i = 0; i < MaxSlots; ++i)
|
||||
{
|
||||
FSaveSlotInfo Info = ReadSlotHeader(i);
|
||||
Info.SlotIndex = i;
|
||||
Manifest.Add(Info);
|
||||
}
|
||||
|
||||
return Manifest;
|
||||
}
|
||||
|
||||
bool USS_SaveManager::DoesSlotExist(int32 SlotIndex) const
|
||||
{
|
||||
if (SlotIndex < 0 || SlotIndex >= MaxSlots)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
|
||||
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
|
||||
return PlatformFile.FileExists(*FilePath);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Save / Load Operations
|
||||
// ============================================================================
|
||||
|
||||
bool USS_SaveManager::SaveGame(int32 SlotIndex, const FString& Description)
|
||||
{
|
||||
if (SlotIndex < 0 || SlotIndex >= MaxSlots)
|
||||
{
|
||||
UE_LOG(LogSave, Warning, TEXT("SaveGame — Invalid slot index: %d"), SlotIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
UE_LOG(LogSave, Log, TEXT("SaveGame — Slot %d: '%s'"), SlotIndex, *Description);
|
||||
|
||||
// Build metadata.
|
||||
FSaveSlotInfo Meta;
|
||||
Meta.SlotIndex = SlotIndex;
|
||||
Meta.SlotName = FString::Printf(TEXT("Slot %d"), SlotIndex);
|
||||
Meta.ChapterName = Description; // Simplified — real impl gets chapter from GS_CoreGameState.
|
||||
Meta.Timestamp = FDateTime::Now();
|
||||
|
||||
// Serialize game state to binary buffer.
|
||||
// In a full implementation, this calls I_Persistable::OnSave() on all registered actors.
|
||||
TArray<uint8> SaveData;
|
||||
FMemoryWriter Writer(SaveData);
|
||||
FObjectAndNameAsStringProxyArchive Ar(Writer, true);
|
||||
// Ar << GameStateData...
|
||||
|
||||
bool bSuccess = SaveToFile(SlotIndex, SaveData, Meta);
|
||||
|
||||
OnSaveComplete.Broadcast(SlotIndex, bSuccess);
|
||||
OnSaveManifestUpdated.Broadcast(GetSlotManifest());
|
||||
|
||||
return bSuccess;
|
||||
}
|
||||
|
||||
bool USS_SaveManager::LoadGame(int32 SlotIndex)
|
||||
{
|
||||
if (!DoesSlotExist(SlotIndex))
|
||||
{
|
||||
UE_LOG(LogSave, Warning, TEXT("LoadGame — Slot %d does not exist"), SlotIndex);
|
||||
OnLoadComplete.Broadcast(SlotIndex, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
UE_LOG(LogSave, Log, TEXT("LoadGame — Slot %d"), SlotIndex);
|
||||
|
||||
TArray<uint8> SaveData;
|
||||
FSaveSlotInfo Meta;
|
||||
|
||||
if (!LoadFromFile(SlotIndex, SaveData, Meta))
|
||||
{
|
||||
UE_LOG(LogSave, Error, TEXT("LoadGame — Failed to read slot %d from disk"), SlotIndex);
|
||||
OnLoadComplete.Broadcast(SlotIndex, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deserialize game state.
|
||||
FMemoryReader Reader(SaveData);
|
||||
FObjectAndNameAsStringProxyArchive Ar(Reader, true);
|
||||
// Ar << GameStateData...
|
||||
|
||||
// Set active slot in GameInstance.
|
||||
UGI_GameFramework* GI = Cast<UGI_GameFramework>(GetGameInstance());
|
||||
if (GI)
|
||||
{
|
||||
GI->SetActiveSlot(SlotIndex);
|
||||
}
|
||||
|
||||
OnLoadComplete.Broadcast(SlotIndex, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool USS_SaveManager::DeleteSlot(int32 SlotIndex)
|
||||
{
|
||||
if (!DoesSlotExist(SlotIndex))
|
||||
{
|
||||
UE_LOG(LogSave, Warning, TEXT("DeleteSlot — Slot %d does not exist"), SlotIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
|
||||
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
|
||||
|
||||
bool bDeleted = PlatformFile.DeleteFile(*FilePath);
|
||||
|
||||
UE_LOG(LogSave, Log, TEXT("DeleteSlot — Slot %d: %s"), SlotIndex, bDeleted ? TEXT("Deleted") : TEXT("Failed"));
|
||||
|
||||
OnSaveManifestUpdated.Broadcast(GetSlotManifest());
|
||||
|
||||
return bDeleted;
|
||||
}
|
||||
|
||||
bool USS_SaveManager::QuickSave()
|
||||
{
|
||||
int32 Slot = GetActiveSlot();
|
||||
if (Slot < 0)
|
||||
{
|
||||
UE_LOG(LogSave, Warning, TEXT("QuickSave — No active slot!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return SaveGame(Slot, TEXT("QuickSave"));
|
||||
}
|
||||
|
||||
bool USS_SaveManager::QuickLoad()
|
||||
{
|
||||
int32 Slot = GetActiveSlot();
|
||||
if (Slot < 0)
|
||||
{
|
||||
UE_LOG(LogSave, Warning, TEXT("QuickLoad — No active slot!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return LoadGame(Slot);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Checkpoint Management
|
||||
// ============================================================================
|
||||
|
||||
bool USS_SaveManager::LoadCheckpoint(int32 SlotIndex)
|
||||
{
|
||||
// Checkpoints are incremental saves within a slot.
|
||||
// For now, loads the full slot (same as LoadGame).
|
||||
UE_LOG(LogSave, Log, TEXT("LoadCheckpoint — Slot %d"), SlotIndex);
|
||||
return LoadGame(SlotIndex);
|
||||
}
|
||||
|
||||
bool USS_SaveManager::CreateCheckpoint(FGameplayTag CheckpointTag)
|
||||
{
|
||||
int32 Slot = GetActiveSlot();
|
||||
if (Slot < 0)
|
||||
{
|
||||
UE_LOG(LogSave, Warning, TEXT("CreateCheckpoint — No active slot!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
UE_LOG(LogSave, Log, TEXT("CreateCheckpoint — Slot %d, Tag: %s"),
|
||||
Slot, *CheckpointTag.GetTagName().ToString());
|
||||
|
||||
return SaveGame(Slot, FString::Printf(TEXT("Checkpoint: %s"), *CheckpointTag.GetTagName().ToString()));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utilities
|
||||
// ============================================================================
|
||||
|
||||
int64 USS_SaveManager::GetTotalSaveSize() const
|
||||
{
|
||||
int64 TotalSize = 0;
|
||||
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
|
||||
|
||||
for (int32 i = 0; i < MaxSlots; ++i)
|
||||
{
|
||||
FString FilePath = GetSaveDirectory() / GetSlotName(i) + TEXT(".sav");
|
||||
if (PlatformFile.FileExists(*FilePath))
|
||||
{
|
||||
TotalSize += PlatformFile.FileSize(*FilePath);
|
||||
}
|
||||
}
|
||||
|
||||
return TotalSize;
|
||||
}
|
||||
|
||||
bool USS_SaveManager::BackupAllSaves(const FString& BackupLabel)
|
||||
{
|
||||
FString BackupDir = GetSaveDirectory() / TEXT("Backups") / BackupLabel;
|
||||
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
|
||||
|
||||
if (!PlatformFile.DirectoryExists(*BackupDir))
|
||||
{
|
||||
PlatformFile.CreateDirectoryTree(*BackupDir);
|
||||
}
|
||||
|
||||
int32 BackedUp = 0;
|
||||
for (int32 i = 0; i < MaxSlots; ++i)
|
||||
{
|
||||
FString SrcPath = GetSaveDirectory() / GetSlotName(i) + TEXT(".sav");
|
||||
FString DstPath = BackupDir / GetSlotName(i) + TEXT(".sav");
|
||||
|
||||
if (PlatformFile.FileExists(*SrcPath))
|
||||
{
|
||||
if (PlatformFile.CopyFile(*DstPath, *SrcPath))
|
||||
{
|
||||
++BackedUp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogSave, Log, TEXT("BackupAllSaves — '%s': %d slots backed up"), *BackupLabel, BackedUp);
|
||||
return BackedUp > 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal
|
||||
// ============================================================================
|
||||
|
||||
FString USS_SaveManager::GetSlotName(int32 SlotIndex) const
|
||||
{
|
||||
return FString::Printf(TEXT("%s%d"), *SavePrefix, SlotIndex);
|
||||
}
|
||||
|
||||
FString USS_SaveManager::GetSaveDirectory() const
|
||||
{
|
||||
return FPaths::ProjectSavedDir() / TEXT("SaveGames");
|
||||
}
|
||||
|
||||
int32 USS_SaveManager::GetActiveSlot() const
|
||||
{
|
||||
UGI_GameFramework* GI = Cast<UGI_GameFramework>(GetGameInstance());
|
||||
return GI ? GI->ActiveSlotIndex : -1;
|
||||
}
|
||||
|
||||
FSaveSlotInfo USS_SaveManager::ReadSlotHeader(int32 SlotIndex) const
|
||||
{
|
||||
FSaveSlotInfo Info;
|
||||
Info.SlotIndex = SlotIndex;
|
||||
|
||||
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
|
||||
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
|
||||
|
||||
if (!PlatformFile.FileExists(*FilePath))
|
||||
{
|
||||
Info.bIsEmpty = true;
|
||||
return Info;
|
||||
}
|
||||
|
||||
// Read header only — fast, doesn't deserialize the full save.
|
||||
TArray<uint8> FileData;
|
||||
if (FFileHelper::LoadFileToArray(FileData, *FilePath))
|
||||
{
|
||||
// Simplified header parsing — real impl reads a FSaveSlotInfo struct prefix.
|
||||
Info.bIsEmpty = false;
|
||||
Info.SlotName = FString::Printf(TEXT("Slot %d"), SlotIndex);
|
||||
Info.Timestamp = PlatformFile.GetTimeStamp(*FilePath);
|
||||
// In full impl: deserialize header from FileData.
|
||||
}
|
||||
|
||||
return Info;
|
||||
}
|
||||
|
||||
bool USS_SaveManager::SaveToFile(int32 SlotIndex, const TArray<uint8>& Data, const FSaveSlotInfo& Meta)
|
||||
{
|
||||
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
|
||||
|
||||
// Serialize metadata header + save data.
|
||||
TArray<uint8> FileData;
|
||||
FMemoryWriter Writer(FileData);
|
||||
|
||||
// Write metadata header first.
|
||||
FSaveSlotInfo& MetaRef = const_cast<FSaveSlotInfo&>(Meta);
|
||||
FSaveSlotInfo::StaticStruct()->SerializeItem(Writer, &MetaRef, nullptr);
|
||||
|
||||
// Then write game state data.
|
||||
Writer.Serialize(const_cast<uint8*>(Data.GetData()), Data.Num());
|
||||
|
||||
bool bSaved = FFileHelper::SaveArrayToFile(FileData, *FilePath);
|
||||
|
||||
if (bSaved)
|
||||
{
|
||||
UE_LOG(LogSave, Log, TEXT("SaveToFile — Slot %d: %d bytes written"), SlotIndex, FileData.Num());
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSave, Error, TEXT("SaveToFile — Slot %d: FAILED to write!"), SlotIndex);
|
||||
}
|
||||
|
||||
return bSaved;
|
||||
}
|
||||
|
||||
bool USS_SaveManager::LoadFromFile(int32 SlotIndex, TArray<uint8>& OutData, FSaveSlotInfo& OutMeta)
|
||||
{
|
||||
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
|
||||
|
||||
TArray<uint8> FileData;
|
||||
if (!FFileHelper::LoadFileToArray(FileData, *FilePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deserialize metadata header.
|
||||
FMemoryReader Reader(FileData);
|
||||
FSaveSlotInfo::StaticStruct()->SerializeItem(Reader, &OutMeta, nullptr);
|
||||
|
||||
// Remaining bytes are game state data.
|
||||
int32 HeaderSize = Reader.Tell();
|
||||
OutData.SetNum(FileData.Num() - HeaderSize);
|
||||
FMemory::Memcpy(OutData.GetData(), FileData.GetData() + HeaderSize, OutData.Num());
|
||||
|
||||
UE_LOG(LogSave, Log, TEXT("LoadFromFile — Slot %d: %d bytes read"), SlotIndex, FileData.Num());
|
||||
|
||||
return true;
|
||||
}
|
||||
13
Source/PG_Framework/Private/State/DA_StateGatingTable.cpp
Normal file
13
Source/PG_Framework/Private/State/DA_StateGatingTable.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "State/DA_StateGatingTable.h"
|
||||
|
||||
bool UDA_StateGatingTable::IsActionGated(FGameplayTag ActionTag, FGameplayTag CurrentState) const
|
||||
{
|
||||
for (const FStateGatingRule& Rule : GatingRules)
|
||||
{
|
||||
if (Rule.ActionTag == ActionTag && Rule.BlockedByState == CurrentState)
|
||||
{
|
||||
return Rule.bIsBlocked;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BPC_DamageReceptionSystem Implementation
|
||||
|
||||
#include "Weapons/BPC_DamageReceptionSystem.h"
|
||||
#include "Player/BPC_HealthSystem.h"
|
||||
#include "Weapons/BPC_ShieldDefenseSystem.h"
|
||||
#include "Weapons/BPC_HitReactionSystem.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogFrameworkDamage, Log, All);
|
||||
|
||||
UBPC_DamageReceptionSystem::UBPC_DamageReceptionSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Damage Calculation — Hot Path
|
||||
// ============================================================================
|
||||
|
||||
float UBPC_DamageReceptionSystem::ApplyDamage(float RawDamage, AActor* DamageCauser,
|
||||
FGameplayTag DamageType, FVector HitLocation, FVector HitDirection)
|
||||
{
|
||||
if (RawDamage <= 0.0f || !DamageType.IsValid())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Step 1: Calculate damage multiplier from modifiers.
|
||||
float Multiplier = GetDamageMultiplier(DamageType);
|
||||
|
||||
// Step 2: Apply resistance.
|
||||
float Resistance = CalculateResistance(DamageType);
|
||||
float ResistedAmount = RawDamage * Resistance;
|
||||
|
||||
// Step 3: Calculate final damage.
|
||||
float FinalDamage = (RawDamage * Multiplier) - ResistedAmount;
|
||||
FinalDamage = FMath::Max(FinalDamage, 0.0f); // No negative damage.
|
||||
|
||||
// Check for flat reduction modifiers.
|
||||
for (const FDamageModifier& Mod : DamageModifiers)
|
||||
{
|
||||
if (Mod.DamageType == DamageType && Mod.bFlatReduction)
|
||||
{
|
||||
FinalDamage = FMath::Max(FinalDamage - Mod.FlatReduction, 1.0f); // Minimum 1 damage.
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Apply shield absorption if available.
|
||||
if (CachedShieldSystem)
|
||||
{
|
||||
// Shield absorbs damage before health.
|
||||
// FinalDamage = CachedShieldSystem->AbsorbDamage(FinalDamage);
|
||||
}
|
||||
|
||||
// Step 5: Route to health system.
|
||||
if (CachedHealthSystem)
|
||||
{
|
||||
// CachedHealthSystem->ApplyHealthDamage(FinalDamage);
|
||||
}
|
||||
|
||||
// Step 6: Evaluate hit reaction.
|
||||
EvaluateHitReaction(FinalDamage, DamageCauser, HitDirection);
|
||||
|
||||
// Step 7: Broadcast.
|
||||
OnDamageReceived.Broadcast(RawDamage, FinalDamage, DamageCauser, DamageType, HitLocation);
|
||||
|
||||
if (ResistedAmount > 0.0f)
|
||||
{
|
||||
OnDamageResisted.Broadcast(ResistedAmount, DamageType,
|
||||
FString::Printf(TEXT("Resistance: %.1f%%"), Resistance * 100.0f));
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkDamage, Verbose, TEXT("ApplyDamage — Raw: %.1f → Final: %.1f (Type: %s, Resist: %.1f%%)"),
|
||||
RawDamage, FinalDamage, *DamageType.GetTagName().ToString(), Resistance * 100.0f);
|
||||
|
||||
return FinalDamage;
|
||||
}
|
||||
|
||||
float UBPC_DamageReceptionSystem::CalculateResistance(FGameplayTag DamageType) const
|
||||
{
|
||||
if (!DamageType.IsValid())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Base resistance + equipment-specific bonuses.
|
||||
float TotalResistance = BaseResistance;
|
||||
|
||||
if (EquipmentConfig)
|
||||
{
|
||||
// EquipmentConfig would provide per-damage-type resistance values.
|
||||
// TotalResistance += EquipmentConfig->GetResistance(DamageType);
|
||||
}
|
||||
|
||||
return FMath::Clamp(TotalResistance, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float UBPC_DamageReceptionSystem::GetDamageMultiplier(FGameplayTag DamageType) const
|
||||
{
|
||||
if (!DamageType.IsValid())
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
for (const FDamageModifier& Mod : DamageModifiers)
|
||||
{
|
||||
if (Mod.DamageType == DamageType && !Mod.bFlatReduction)
|
||||
{
|
||||
return Mod.Multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
return 1.0f; // No modifier — normal damage.
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hit Reaction
|
||||
// ============================================================================
|
||||
|
||||
void UBPC_DamageReceptionSystem::EvaluateHitReaction(float FinalDamage, AActor* DamageCauser,
|
||||
FVector HitDirection)
|
||||
{
|
||||
if (FinalDamage >= KnockdownThreshold)
|
||||
{
|
||||
UE_LOG(LogFrameworkDamage, Log, TEXT("EvaluateHitReaction — Knockdown! (%.1f damage)"), FinalDamage);
|
||||
OnKnockedDown.Broadcast(DamageCauser, FinalDamage);
|
||||
}
|
||||
else if (FinalDamage >= StaggerThreshold)
|
||||
{
|
||||
UE_LOG(LogFrameworkDamage, Verbose, TEXT("EvaluateHitReaction — Stagger (%.1f damage)"), FinalDamage);
|
||||
OnStaggered.Broadcast(DamageCauser, FinalDamage);
|
||||
}
|
||||
|
||||
// Route to dedicated hit reaction system for animation selection.
|
||||
if (CachedHitReactionSystem)
|
||||
{
|
||||
// CachedHitReactionSystem->PlayHitReaction(FinalDamage, HitDirection, DamageCauser);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "Weapons/BPC_HitReactionSystem.h"
|
||||
|
||||
UBPC_HitReactionSystem::UBPC_HitReactionSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void UBPC_HitReactionSystem::PlayHitReaction(float DamageAmount, FVector HitDirection, AActor* DamageCauser)
|
||||
{
|
||||
// Stub — Blueprint child provides animation selection logic.
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "Weapons/BPC_ShieldDefenseSystem.h"
|
||||
|
||||
UBPC_ShieldDefenseSystem::UBPC_ShieldDefenseSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
278
Source/PG_Framework/Public/Capture/BPC_PlanarCapture.h
Normal file
278
Source/PG_Framework/Public/Capture/BPC_PlanarCapture.h
Normal file
@@ -0,0 +1,278 @@
|
||||
// Copyright Ngonart OU. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BPC_PlanarCapture (136)
|
||||
// The heart of the Planar Capture System. Attached to any actor that needs
|
||||
// a scene capture — mirrors, portals, monitors, horror surfaces.
|
||||
//
|
||||
// Manages USceneCaptureComponent2D lifetime, render target pool allocation,
|
||||
// camera transform computation per mode, oblique clip plane injection,
|
||||
// show/hide actor lists, quality tier application, frame ring buffer,
|
||||
// and MPC parameter pushes.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Capture/PlanarCaptureCommon.h"
|
||||
#include "BPC_PlanarCapture.generated.h"
|
||||
|
||||
// Forward declarations
|
||||
class USceneCaptureComponent2D;
|
||||
class UTextureRenderTarget2D;
|
||||
class UMaterialInstanceDynamic;
|
||||
class UMaterialParameterCollection;
|
||||
class USS_PlanarCaptureManager;
|
||||
class ABP_PlanarCaptureActor;
|
||||
|
||||
// ============================================================================
|
||||
// Delegates
|
||||
// ============================================================================
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnCaptureQualityChanged, EPlanarCaptureQualityTier, OldTier, EPlanarCaptureQualityTier, NewTier);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCaptureInitialized, EPlanarCaptureInitResult, Result);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCaptureRendered);
|
||||
|
||||
/**
|
||||
* BPC_PlanarCapture — Core capture component for mirrors, portals, monitors, etc.
|
||||
*
|
||||
* Owns the USceneCaptureComponent2D lifecycle. All camera math, render target
|
||||
* management, and per-frame capture decisions happen here in C++ for performance.
|
||||
* Designer configuration flows through Blueprint children.
|
||||
*
|
||||
* Multiplayer: Capture is always local-only. Each client renders their own view.
|
||||
* No replication needed. This component only exists on clients.
|
||||
*/
|
||||
UCLASS(Blueprintable, ClassGroup = (Framework), meta = (BlueprintSpawnableComponent))
|
||||
class PG_FRAMEWORK_API UBPC_PlanarCapture : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_PlanarCapture();
|
||||
|
||||
// ========================================================================
|
||||
// Lifecycle
|
||||
// ========================================================================
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
// ========================================================================
|
||||
// Configuration — Set in Blueprint Defaults
|
||||
// ========================================================================
|
||||
|
||||
/** What kind of surface this capture represents. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
|
||||
EPlanarCaptureMode CaptureMode = EPlanarCaptureMode::Mirror;
|
||||
|
||||
/** Quality profiles per tier (0=Low, 1=Medium, 2=High, 3=Hero). Index maps to EPlanarCaptureQualityTier. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
|
||||
TArray<FPlanarCaptureQualityProfile> QualityProfiles;
|
||||
|
||||
/** FOV for the capture camera. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
|
||||
float CaptureFOV = 90.0f;
|
||||
|
||||
/** Maximum view distance for the capture (0 = unlimited). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
|
||||
float MaxViewDistance = 5000.0f;
|
||||
|
||||
// ========================================================================
|
||||
// Portal Configuration
|
||||
// ========================================================================
|
||||
|
||||
/** For Portal mode: the linked target surface actor. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Portal")
|
||||
TSoftObjectPtr<ABP_PlanarCaptureActor> LinkedTargetSurface;
|
||||
|
||||
// ========================================================================
|
||||
// Monitor Configuration
|
||||
// ========================================================================
|
||||
|
||||
/** For Monitor mode: a fixed camera actor to capture from. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Monitor")
|
||||
TSoftObjectPtr<AActor> FixedCameraActor;
|
||||
|
||||
// ========================================================================
|
||||
// Actor Lists
|
||||
// ========================================================================
|
||||
|
||||
/** Actors to show exclusively in the capture (empty = show all). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|ActorLists")
|
||||
TArray<FPlanarCaptureActorListEntry> ShowOnlyActors;
|
||||
|
||||
/** Actors to hide from the capture. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|ActorLists")
|
||||
TArray<FPlanarCaptureActorListEntry> HiddenActors;
|
||||
|
||||
// ========================================================================
|
||||
// Horror Configuration
|
||||
// ========================================================================
|
||||
|
||||
/** Actor to swap into the ShowOnly list during wrong-reflection events. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Horror")
|
||||
TSoftObjectPtr<AActor> WrongReflectionActor;
|
||||
|
||||
/** Mesh component of the surface plane (for clip plane calculation). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Config")
|
||||
TSoftObjectPtr<UStaticMeshComponent> SurfaceMeshComponent;
|
||||
|
||||
// ========================================================================
|
||||
// Runtime State (Read-Only)
|
||||
// ========================================================================
|
||||
|
||||
/** Current assigned quality tier (set by SS_PlanarCaptureManager). */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Capture|Runtime")
|
||||
EPlanarCaptureQualityTier CurrentQualityTier = EPlanarCaptureQualityTier::Off;
|
||||
|
||||
/** Is this capture currently active (capturing frames)? */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Capture|Runtime")
|
||||
bool bIsCapturing = false;
|
||||
|
||||
/** The render target this capture writes to. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Capture|Runtime")
|
||||
TObjectPtr<UTextureRenderTarget2D> CaptureRenderTarget;
|
||||
|
||||
// ========================================================================
|
||||
// Public API — BlueprintCallable
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Initialize the capture component. Allocates render target, creates and
|
||||
* configures the USceneCaptureComponent2D. Called by the owning actor on BeginPlay.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture")
|
||||
EPlanarCaptureInitResult InitializeCapture();
|
||||
|
||||
/**
|
||||
* Shut down the capture, release render target back to pool, destroy SceneCaptureComponent2D.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture")
|
||||
void ShutdownCapture();
|
||||
|
||||
/**
|
||||
* Apply a quality tier profile immediately. Called by SS_PlanarCaptureManager.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture")
|
||||
void ApplyQualityTier(EPlanarCaptureQualityTier Tier);
|
||||
|
||||
/**
|
||||
* Trigger a single capture frame immediately (bypasses tick interval).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture")
|
||||
void CaptureNow();
|
||||
|
||||
/**
|
||||
* Swap the ShowOnly actor list to the wrong-reflection actor (horror mode).
|
||||
* Original list is preserved and restored on DeactivateHorrorReflection().
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture|Horror")
|
||||
void ActivateHorrorReflection();
|
||||
|
||||
/**
|
||||
* Restore the original ShowOnly actor list after a horror event.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture|Horror")
|
||||
void DeactivateHorrorReflection();
|
||||
|
||||
/**
|
||||
* Push a frame from the ring buffer into the render target for delayed reflection (horror lag).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture|Horror")
|
||||
void PushDelayedFrame();
|
||||
|
||||
/**
|
||||
* Set a scripted priority override (0.0 to 1.0). Higher values force higher quality tier.
|
||||
* Example: a scare event elevates a specific mirror to Hero tier.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture")
|
||||
void SetScriptedPriority(float Priority);
|
||||
|
||||
/**
|
||||
* Push all Material Parameter Collection values for this surface.
|
||||
* Called every frame when capturing, or on event trigger for horror params.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture|Material")
|
||||
void PushMPCParameters(UMaterialParameterCollection* MPC);
|
||||
|
||||
// ========================================================================
|
||||
// Compute — BlueprintPure
|
||||
// ========================================================================
|
||||
|
||||
/** Compute the capture camera transform for the current mode. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture")
|
||||
FTransform ComputeCaptureCameraTransform(const FTransform& ViewerCameraTransform) const;
|
||||
|
||||
/** Get the current composite quality score. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture")
|
||||
FPlanarCaptureScore GetCurrentScore() const;
|
||||
|
||||
// ========================================================================
|
||||
// Event Dispatchers
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Capture|Events")
|
||||
FOnCaptureQualityChanged OnCaptureQualityChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Capture|Events")
|
||||
FOnCaptureInitialized OnCaptureInitialized;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Capture|Events")
|
||||
FOnCaptureRendered OnCaptureRendered;
|
||||
|
||||
protected:
|
||||
// ========================================================================
|
||||
// Internal State
|
||||
// ========================================================================
|
||||
|
||||
/** The actual UE5 SceneCaptureComponent2D — created at runtime. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<USceneCaptureComponent2D> SceneCapture;
|
||||
|
||||
/** Cached reference to the manager subsystem. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<USS_PlanarCaptureManager> CachedManager;
|
||||
|
||||
/** Cached reference to the owning actor. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<ABP_PlanarCaptureActor> CachedOwningActor;
|
||||
|
||||
/** Time accumulator for capture interval throttling. */
|
||||
float TimeSinceLastCapture = 0.0f;
|
||||
|
||||
/** Current quality profile (cached from QualityProfiles[Tier]). */
|
||||
FPlanarCaptureQualityProfile ActiveProfile;
|
||||
|
||||
/** Scripted priority override (0.0 to 1.0). */
|
||||
float ScriptedPriorityOverride = 0.0f;
|
||||
|
||||
/** Ring buffer of render targets for delayed reflection (horror mode). */
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UTextureRenderTarget2D>> FrameRingBuffer;
|
||||
|
||||
/** Current write index into the frame ring buffer. */
|
||||
int32 RingBufferWriteIndex = 0;
|
||||
|
||||
/** Saved ShowOnly list before horror swap. */
|
||||
TArray<TSoftObjectPtr<AActor>> SavedShowOnlyActors;
|
||||
|
||||
// ========================================================================
|
||||
// Internal Methods
|
||||
// ========================================================================
|
||||
|
||||
/** Create and configure the USceneCaptureComponent2D. */
|
||||
void CreateSceneCaptureComponent();
|
||||
|
||||
/** Apply show flags from the active quality profile to the SceneCapture. */
|
||||
void ApplyShowFlags();
|
||||
|
||||
/** Update the ShowOnly and Hidden actor lists on the SceneCapture. */
|
||||
void UpdateActorLists();
|
||||
|
||||
/** Resolve soft references on initialization. */
|
||||
void ResolveSoftReferences();
|
||||
|
||||
/** Compute the surface plane in world space for clip plane and mirror math. */
|
||||
FPlane GetSurfacePlane() const;
|
||||
};
|
||||
170
Source/PG_Framework/Public/Capture/BP_PlanarCaptureActor.h
Normal file
170
Source/PG_Framework/Public/Capture/BP_PlanarCaptureActor.h
Normal file
@@ -0,0 +1,170 @@
|
||||
// Copyright Ngonart OU. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BP_PlanarCaptureActor (137)
|
||||
// Placeable actor wrapping a BPC_PlanarCapture component. Owns the surface mesh,
|
||||
// material dynamic instances, and the capture component. Handles overlap/proximity
|
||||
// events feeding into the quality manager. Registers with the global subsystem.
|
||||
//
|
||||
// Blueprint children: BP_Mirror, BP_Portal, BP_Monitor, BP_HorrorMirror, BP_FakeWindow.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Capture/PlanarCaptureCommon.h"
|
||||
#include "BP_PlanarCaptureActor.generated.h"
|
||||
|
||||
class UBPC_PlanarCapture;
|
||||
class UStaticMeshComponent;
|
||||
class UMaterialInstanceDynamic;
|
||||
class UMaterialParameterCollection;
|
||||
class UBoxComponent;
|
||||
|
||||
// Delegates
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlanarCaptureActorModeChanged, EPlanarCaptureMode, NewMode);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlanarCaptureSurfaceDestroyed, ABP_PlanarCaptureActor*, Surface);
|
||||
|
||||
/**
|
||||
* BP_PlanarCaptureActor — Placeable actor for mirrors, portals, monitors, etc.
|
||||
*
|
||||
* Blueprint children configure the mode, mesh, materials, and capture settings.
|
||||
* This actor is the designer-facing interface — all runtime logic lives in
|
||||
* BPC_PlanarCapture and SS_PlanarCaptureManager.
|
||||
*
|
||||
* Multiplayer: Spawned on all clients. Capture rendering is local-only.
|
||||
* Surface state (destroyed, on/off) may replicate via RepNotify.
|
||||
*/
|
||||
UCLASS(Blueprintable, BlueprintType)
|
||||
class PG_FRAMEWORK_API ABP_PlanarCaptureActor : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ABP_PlanarCaptureActor();
|
||||
|
||||
// ========================================================================
|
||||
// Components
|
||||
// ========================================================================
|
||||
|
||||
/** The surface mesh — a plane, quad, or frame. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Capture|Components")
|
||||
TObjectPtr<UStaticMeshComponent> SurfaceMesh;
|
||||
|
||||
/** Proximity trigger volume for quality scoring. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Capture|Components")
|
||||
TObjectPtr<UBoxComponent> ProximityTrigger;
|
||||
|
||||
/** The core capture component. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Capture|Components")
|
||||
TObjectPtr<UBPC_PlanarCapture> CaptureComponent;
|
||||
|
||||
// ========================================================================
|
||||
// Configuration
|
||||
// ========================================================================
|
||||
|
||||
/** Display name for debug and logging. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
|
||||
FString SurfaceDisplayName;
|
||||
|
||||
/** Whether this surface starts enabled. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
|
||||
bool bStartEnabled = true;
|
||||
|
||||
/** Whether this surface can be destroyed (shattered mirror, broken monitor). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
|
||||
bool bDestructible = false;
|
||||
|
||||
/** Material Parameter Collection for global surface parameters. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Material")
|
||||
TObjectPtr<UMaterialParameterCollection> SurfaceMPC;
|
||||
|
||||
// ========================================================================
|
||||
// Runtime State
|
||||
// ========================================================================
|
||||
|
||||
/** Whether this surface is currently active. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Capture|Runtime")
|
||||
bool bIsActive = false;
|
||||
|
||||
/** Dynamic material instance on the surface mesh. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Capture|Runtime")
|
||||
TObjectPtr<UMaterialInstanceDynamic> SurfaceMaterialInstance;
|
||||
|
||||
// ========================================================================
|
||||
// Public API
|
||||
// ========================================================================
|
||||
|
||||
/** Enable the capture surface. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture")
|
||||
void EnableSurface();
|
||||
|
||||
/** Disable the capture surface (stops rendering, releases budget). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture")
|
||||
void DisableSurface();
|
||||
|
||||
/** Set the capture mode at runtime. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture")
|
||||
void SetCaptureMode(EPlanarCaptureMode NewMode);
|
||||
|
||||
/** Set the surface material at runtime (e.g., swap between clean/dirty mirror). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture")
|
||||
void SetSurfaceMaterial(UMaterialInterface* NewMaterial);
|
||||
|
||||
/** Set a single MPC scalar parameter for this surface. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture|Material")
|
||||
void SetSurfaceMPCParameter(FName ParameterName, float Value);
|
||||
|
||||
/** Destroy the surface (shatter mirror, break monitor). Triggers visual effect. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture")
|
||||
void DestroySurface();
|
||||
|
||||
// ========================================================================
|
||||
// Overrides
|
||||
// ========================================================================
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
// ========================================================================
|
||||
// Event Dispatchers
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Capture|Events")
|
||||
FOnPlanarCaptureActorModeChanged OnModeChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Capture|Events")
|
||||
FOnPlanarCaptureSurfaceDestroyed OnSurfaceDestroyed;
|
||||
|
||||
protected:
|
||||
// ========================================================================
|
||||
// Overlap Events (Proximity Trigger)
|
||||
// ========================================================================
|
||||
|
||||
UFUNCTION()
|
||||
void OnProximityBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
||||
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
|
||||
|
||||
UFUNCTION()
|
||||
void OnProximityEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
||||
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
|
||||
|
||||
// ========================================================================
|
||||
// RepNotify (multiplayer)
|
||||
// ========================================================================
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_IsActive();
|
||||
|
||||
/** Replicated active state for server-authoritative surface control. */
|
||||
UPROPERTY(ReplicatedUsing = OnRep_IsActive)
|
||||
bool bRepIsActive = false;
|
||||
|
||||
// ========================================================================
|
||||
// Internal
|
||||
// ========================================================================
|
||||
|
||||
/** Register with the global manager subsystem. */
|
||||
void RegisterWithManager();
|
||||
|
||||
/** Create the dynamic material instance from the surface mesh material. */
|
||||
void CreateMaterialInstance();
|
||||
};
|
||||
155
Source/PG_Framework/Public/Capture/PlanarCaptureCameraUtils.h
Normal file
155
Source/PG_Framework/Public/Capture/PlanarCaptureCameraUtils.h
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright Ngonart OU. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — PlanarCaptureCameraUtils
|
||||
// Static math library for mirror reflection, portal relative transform,
|
||||
// oblique projection, screen coverage, and visibility computations.
|
||||
// All functions are BlueprintCallable and use UE5's FMatrix/FVector/FPlane types.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "PlanarCaptureCameraUtils.generated.h"
|
||||
|
||||
/**
|
||||
* Static math library for planar capture camera transforms.
|
||||
*
|
||||
* Mirror: reflect the viewer camera across the mirror plane.
|
||||
* Portal: compute the relative transform from source surface to target surface.
|
||||
* All math happens in C++ for performance — Blueprint calls these as pure functions.
|
||||
*/
|
||||
UCLASS()
|
||||
class PG_FRAMEWORK_API UPlanarCaptureCameraUtils : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// ========================================================================
|
||||
// Mirror Reflection Math
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Compute the mirrored camera transform for a planar mirror.
|
||||
* Reflects the viewer's camera position and rotation across the mirror plane.
|
||||
*
|
||||
* @param ViewerCameraTransform World transform of the viewer's camera.
|
||||
* @param MirrorPlaneTransform World transform of the mirror surface (XY plane, Z = normal).
|
||||
* @return The world transform for the SceneCapture camera.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
|
||||
static FTransform ComputeMirroredTransform(
|
||||
const FTransform& ViewerCameraTransform,
|
||||
const FTransform& MirrorPlaneTransform);
|
||||
|
||||
// ========================================================================
|
||||
// Portal Relative Math
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Compute the capture camera transform for a portal.
|
||||
* The viewer's position relative to the source surface is mapped to the
|
||||
* target surface's coordinate space.
|
||||
*
|
||||
* @param ViewerCameraTransform World transform of the viewer's camera.
|
||||
* @param SourceSurfaceTransform World transform of the portal entry surface.
|
||||
* @param TargetSurfaceTransform World transform of the portal exit surface.
|
||||
* @return The world transform for the SceneCapture camera.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
|
||||
static FTransform ComputePortalTransform(
|
||||
const FTransform& ViewerCameraTransform,
|
||||
const FTransform& SourceSurfaceTransform,
|
||||
const FTransform& TargetSurfaceTransform);
|
||||
|
||||
// ========================================================================
|
||||
// Oblique Near-Plane Projection
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Compute an oblique near-clip-plane projection matrix.
|
||||
* Used to prevent geometry behind the portal/mirror surface from clipping
|
||||
* into the capture view.
|
||||
*
|
||||
* @param FOV Horizontal field of view in degrees.
|
||||
* @param AspectRatio Width / Height.
|
||||
* @param NearPlane Near clip distance.
|
||||
* @param FarPlane Far clip distance.
|
||||
* @param ClipPlane World-space plane to clip against.
|
||||
* @param SurfaceTransform Transform of the surface (for converting clip plane to view space).
|
||||
* @return The oblique projection matrix.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
|
||||
static FMatrix ComputeObliqueProjectionMatrix(
|
||||
float FOV,
|
||||
float AspectRatio,
|
||||
float NearPlane,
|
||||
float FarPlane,
|
||||
const FPlane& ClipPlane,
|
||||
const FTransform& SurfaceTransform);
|
||||
|
||||
// ========================================================================
|
||||
// Screen Coverage & Visibility
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Estimate how much of the screen a capture surface occupies (0.0 to 1.0).
|
||||
* Uses the surface's bounding box corners projected to screen-space.
|
||||
*
|
||||
* @param SurfaceBounds Bounding box of the surface mesh in world space.
|
||||
* @param ViewerTransform World transform of the viewer's camera.
|
||||
* @param ViewerFOV Horizontal FOV of the viewer.
|
||||
* @param ScreenWidth Viewport width in pixels.
|
||||
* @param ScreenHeight Viewport height in pixels.
|
||||
* @return Estimated screen coverage ratio (0.0 to 1.0).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
|
||||
static float ComputeScreenCoverage(
|
||||
const FBox& SurfaceBounds,
|
||||
const FTransform& ViewerTransform,
|
||||
float ViewerFOV,
|
||||
int32 ScreenWidth,
|
||||
int32 ScreenHeight);
|
||||
|
||||
/**
|
||||
* Check whether the surface is visible to the viewer's frustum.
|
||||
*
|
||||
* @param SurfaceBounds Bounding box of the surface mesh in world space.
|
||||
* @param ViewerTransform World transform of the viewer's camera.
|
||||
* @param ViewerFOV Horizontal FOV.
|
||||
* @param ViewerAspectRatio Width / Height.
|
||||
* @param ViewerNearPlane Near clip distance.
|
||||
* @param ViewerFarPlane Far clip distance.
|
||||
* @return True if any part of the surface is within the frustum.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
|
||||
static bool IsSurfaceVisibleToViewer(
|
||||
const FBox& SurfaceBounds,
|
||||
const FTransform& ViewerTransform,
|
||||
float ViewerFOV,
|
||||
float ViewerAspectRatio,
|
||||
float ViewerNearPlane,
|
||||
float ViewerFarPlane);
|
||||
|
||||
// ========================================================================
|
||||
// Quality Scoring
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Compute a composite priority score for a capture surface.
|
||||
* Higher score = higher quality tier assignment.
|
||||
* Formula: (ScreenCoverage * 0.5) + (FacingAngle * 0.3) + (DistanceFactor * 0.1) + (ScriptedPriority * 0.1)
|
||||
*
|
||||
* @param ScreenCoverage How much screen real estate the surface occupies.
|
||||
* @param FacingAngle Dot product of viewer forward to surface normal.
|
||||
* @param DistanceToViewer Distance in world units.
|
||||
* @param MaxDistance Distance at which score drops to zero.
|
||||
* @param ScriptedPriority Priority override from gameplay systems (0.0 to 1.0).
|
||||
* @return Composite score (0.0 to 1.0).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
|
||||
static float ComputeCompositeScore(
|
||||
float ScreenCoverage,
|
||||
float FacingAngle,
|
||||
float DistanceToViewer,
|
||||
float MaxDistance,
|
||||
float ScriptedPriority);
|
||||
};
|
||||
189
Source/PG_Framework/Public/Capture/PlanarCaptureCommon.h
Normal file
189
Source/PG_Framework/Public/Capture/PlanarCaptureCommon.h
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright Ngonart OU. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — PlanarCaptureCommon
|
||||
// Shared enums, structs, and quality profile definitions for the Planar Capture System.
|
||||
// Used by BPC_PlanarCapture, BP_PlanarCaptureActor, and SS_PlanarCaptureManager.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/TextureRenderTarget2D.h"
|
||||
#include "PlanarCaptureCommon.generated.h"
|
||||
|
||||
// ============================================================================
|
||||
// Enums
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* The mode of the capture surface — determines camera math, rendering behavior,
|
||||
* and material configuration.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EPlanarCaptureMode : uint8
|
||||
{
|
||||
Mirror UMETA(DisplayName = "Mirror"),
|
||||
Portal UMETA(DisplayName = "Portal"),
|
||||
Monitor UMETA(DisplayName = "Monitor / Security Screen"),
|
||||
HorrorMirror UMETA(DisplayName = "Horror Mirror"),
|
||||
HorrorPortal UMETA(DisplayName = "Horror Portal"),
|
||||
FakeWindow UMETA(DisplayName = "Fake Window"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Quality tier for a capture surface. Managed globally by SS_PlanarCaptureManager.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EPlanarCaptureQualityTier : uint8
|
||||
{
|
||||
Off UMETA(DisplayName = "Off — No capture"),
|
||||
Low UMETA(DisplayName = "Low — 256px, 4fps"),
|
||||
Medium UMETA(DisplayName = "Medium — 512px, 15fps"),
|
||||
High UMETA(DisplayName = "High — 1024px, 30fps"),
|
||||
Hero UMETA(DisplayName = "Hero — 2048px, 60fps"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Result codes for capture surface initialization.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EPlanarCaptureInitResult : uint8
|
||||
{
|
||||
Success UMETA(DisplayName = "Success"),
|
||||
NoRenderTargetPool UMETA(DisplayName = "Failed — No Render Target in Pool"),
|
||||
InvalidSurfaceMesh UMETA(DisplayName = "Failed — Invalid Surface Mesh"),
|
||||
BudgetExceeded UMETA(DisplayName = "Failed — Budget Exceeded"),
|
||||
ManagerUnavailable UMETA(DisplayName = "Failed — Manager Unavailable"),
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Structs
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Quality profile — defines render target resolution, capture interval,
|
||||
* and per-feature toggles for a single quality tier.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FPlanarCaptureQualityProfile
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Render target resolution (square: 256, 512, 1024, 2048). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Quality")
|
||||
int32 RenderTargetSize = 512;
|
||||
|
||||
/** Minimum interval between captures in seconds (1.0 / FPS). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Quality")
|
||||
float CaptureInterval = 0.0667f; // ~15fps
|
||||
|
||||
/** Shadow rendering mode for the capture. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
|
||||
bool bEnableShadows = true;
|
||||
|
||||
/** Allow post-process effects in the capture. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
|
||||
bool bEnablePostProcess = false;
|
||||
|
||||
/** Allow exponential height fog in the capture. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
|
||||
bool bEnableFog = false;
|
||||
|
||||
/** Allow bloom in the capture. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
|
||||
bool bEnableBloom = false;
|
||||
|
||||
/** Allow ambient occlusion in the capture. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
|
||||
bool bEnableAO = false;
|
||||
|
||||
/** Allow Lumen global illumination in the capture (expensive). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
|
||||
bool bEnableLumen = false;
|
||||
|
||||
/** Allow motion blur in the capture. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
|
||||
bool bEnableMotionBlur = false;
|
||||
|
||||
/** Enable oblique near-clip plane (required for portals flush with geometry). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
|
||||
bool bEnableClipPlane = true;
|
||||
|
||||
/** Number of frames to delay the reflection (0 = off, N = horror lag effect). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Horror", meta = (ClampMin = "0", ClampMax = "30"))
|
||||
int32 DelayedFrameCount = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Single entry in a capture surface's ShowOnly or Hidden actor list.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FPlanarCaptureActorListEntry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** The actor to show exclusively or hide. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|ActorList")
|
||||
TSoftObjectPtr<AActor> Actor;
|
||||
|
||||
/** Whether this actor is currently active in the list. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|ActorList")
|
||||
bool bActive = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Scoring data for a capture surface — computed each frame by the manager
|
||||
* to determine quality tier assignment.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FPlanarCaptureScore
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Distance from viewer to surface in world units. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
|
||||
float DistanceToViewer = FLT_MAX;
|
||||
|
||||
/** Screen coverage percentage (0.0 to 1.0). */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
|
||||
float ScreenCoverage = 0.0f;
|
||||
|
||||
/** Dot product of viewer forward and surface normal (1.0 = facing, 0.0 = edge-on). */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
|
||||
float FacingAngle = 0.0f;
|
||||
|
||||
/** Whether the surface is within the viewer's frustum. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
|
||||
bool bInFrustum = false;
|
||||
|
||||
/** Scripted priority override (0.0 to 1.0, from gameplay systems). */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
|
||||
float ScriptedPriority = 0.0f;
|
||||
|
||||
/** Composite score (0.0 to 1.0) — higher = more important. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
|
||||
float CompositeScore = 0.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Render target pool entry — managed by SS_PlanarCaptureManager.
|
||||
*/
|
||||
USTRUCT()
|
||||
struct PG_FRAMEWORK_API FPlanarCaptureRenderTargetEntry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** The allocated render target. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UTextureRenderTarget2D> RenderTarget = nullptr;
|
||||
|
||||
/** Current size of the render target. */
|
||||
UPROPERTY()
|
||||
int32 CurrentSize = 0;
|
||||
|
||||
/** Whether this entry is currently assigned to a surface. */
|
||||
UPROPERTY()
|
||||
bool bInUse = false;
|
||||
|
||||
/** Which surface currently owns this entry. */
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<class ABP_PlanarCaptureActor> OwningSurface;
|
||||
};
|
||||
239
Source/PG_Framework/Public/Capture/SS_PlanarCaptureManager.h
Normal file
239
Source/PG_Framework/Public/Capture/SS_PlanarCaptureManager.h
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright Ngonart OU. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — SS_PlanarCaptureManager (138)
|
||||
// Global budget manager for all planar capture surfaces in the world.
|
||||
//
|
||||
// One instance per World (World Subsystem). Each frame, scores every registered
|
||||
// capture surface by distance, screen coverage, facing angle, and scripted priority.
|
||||
// Assigns quality tiers across all surfaces respecting a global budget
|
||||
// (max simultaneous high-quality captures, max total render target memory).
|
||||
// Forces idle/disabled state on surfaces outside active rooms/sublevels.
|
||||
//
|
||||
// Also manages the render target pool — allocates, reuses, and resizes RTs
|
||||
// to minimize memory churn.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/WorldSubsystem.h"
|
||||
#include "Tickable.h"
|
||||
#include "Misc/Optional.h"
|
||||
#include "Capture/PlanarCaptureCommon.h"
|
||||
#include "SS_PlanarCaptureManager.generated.h"
|
||||
|
||||
class ABP_PlanarCaptureActor;
|
||||
class UBPC_PlanarCapture;
|
||||
|
||||
// Delegates
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSurfaceRegistered, ABP_PlanarCaptureActor*, Surface, int32, TotalSurfaces);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSurfaceUnregistered, ABP_PlanarCaptureActor*, Surface, int32, TotalSurfaces);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGlobalQualityCapChanged, EPlanarCaptureQualityTier, NewCap);
|
||||
|
||||
/**
|
||||
* SS_PlanarCaptureManager — Global Planar Capture Budget Manager.
|
||||
*
|
||||
* Manages all ABP_PlanarCaptureActor instances per world. Evaluates priority
|
||||
* and assigns quality tiers to stay within a configurable budget.
|
||||
* Also owns the render target pool to reduce memory allocation overhead.
|
||||
*
|
||||
* Multiplayer: This subsystem exists on both server and clients.
|
||||
* On the server, it tracks surfaces for replication state.
|
||||
* On clients, it drives actual capture rendering.
|
||||
*/
|
||||
UCLASS()
|
||||
class PG_FRAMEWORK_API USS_PlanarCaptureManager : public UWorldSubsystem, public FTickableGameObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
USS_PlanarCaptureManager();
|
||||
|
||||
// ========================================================================
|
||||
// Lifecycle
|
||||
// ========================================================================
|
||||
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
// FTickableGameObject
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
virtual TStatId GetStatId() const override;
|
||||
virtual bool IsTickable() const override { return !IsTemplate(); }
|
||||
virtual bool IsTickableInEditor() const override { return false; }
|
||||
virtual UWorld* GetTickableGameObjectWorld() const override { return GetWorld(); }
|
||||
|
||||
// ========================================================================
|
||||
// Surface Registry
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Register a capture surface actor with the global manager.
|
||||
* Called by ABP_PlanarCaptureActor::BeginPlay.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture|Manager")
|
||||
void RegisterSurface(ABP_PlanarCaptureActor* Surface);
|
||||
|
||||
/**
|
||||
* Unregister a capture surface actor. Called on EndPlay.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture|Manager")
|
||||
void UnregisterSurface(ABP_PlanarCaptureActor* Surface);
|
||||
|
||||
/**
|
||||
* Get all currently registered surfaces.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|Manager")
|
||||
TArray<ABP_PlanarCaptureActor*> GetRegisteredSurfaces() const;
|
||||
|
||||
/**
|
||||
* Get the number of registered surfaces.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|Manager")
|
||||
int32 GetSurfaceCount() const { return RegisteredSurfaces.Num(); }
|
||||
|
||||
// ========================================================================
|
||||
// Quality Budget Management
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Global quality ceiling — caps all surfaces at this tier regardless of score.
|
||||
* Use this for lower-end hardware targets.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
|
||||
EPlanarCaptureQualityTier GlobalQualityCap = EPlanarCaptureQualityTier::High;
|
||||
|
||||
/**
|
||||
* Maximum number of simultaneous Hero-tier captures.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
|
||||
int32 MaxHeroSurfaces = 1;
|
||||
|
||||
/**
|
||||
* Maximum number of simultaneous High-tier captures.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
|
||||
int32 MaxHighSurfaces = 3;
|
||||
|
||||
/**
|
||||
* Maximum number of simultaneous Medium-tier captures.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
|
||||
int32 MaxMediumSurfaces = 6;
|
||||
|
||||
/**
|
||||
* Maximum total render target memory in megabytes across all surfaces.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
|
||||
float MaxTotalRenderTargetMemoryMB = 128.0f;
|
||||
|
||||
/**
|
||||
* Distance at which capture quality drops to Off (world units).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
|
||||
float MaxCaptureDistance = 10000.0f;
|
||||
|
||||
/**
|
||||
* Interval between full re-evaluation of all surfaces (seconds).
|
||||
* Individual surfaces check their own interval every frame via their component.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
|
||||
float FullEvaluationInterval = 0.5f;
|
||||
|
||||
// ========================================================================
|
||||
// Render Target Pool
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Request a render target from the pool. Returns nullptr if none available.
|
||||
*/
|
||||
UTextureRenderTarget2D* RequestRenderTarget(int32 Size);
|
||||
|
||||
/**
|
||||
* Release a render target back to the pool.
|
||||
*/
|
||||
void ReleaseRenderTarget(UTextureRenderTarget2D* RenderTarget);
|
||||
|
||||
/**
|
||||
* Get the total memory used by the render target pool (in MB).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|Manager")
|
||||
float GetPoolMemoryUsageMB() const;
|
||||
|
||||
// ========================================================================
|
||||
// Query
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get the nearest capture surface of a given mode to a world location.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|Manager")
|
||||
ABP_PlanarCaptureActor* GetNearestSurfaceOfMode(
|
||||
EPlanarCaptureMode Mode, FVector WorldLocation, float MaxDistance = 0.0f) const;
|
||||
|
||||
/**
|
||||
* Force all surfaces to a specific quality tier (e.g., for cutscenes).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture|Manager")
|
||||
void ForceAllSurfacesToTier(EPlanarCaptureQualityTier Tier);
|
||||
|
||||
/**
|
||||
* Release the force-tier override and resume normal scoring.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Capture|Manager")
|
||||
void ReleaseForceTier();
|
||||
|
||||
// ========================================================================
|
||||
// Event Dispatchers
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Capture|Manager|Events")
|
||||
FOnSurfaceRegistered OnSurfaceRegistered;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Capture|Manager|Events")
|
||||
FOnSurfaceUnregistered OnSurfaceUnregistered;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Capture|Manager|Events")
|
||||
FOnGlobalQualityCapChanged OnGlobalQualityCapChanged;
|
||||
|
||||
protected:
|
||||
// ========================================================================
|
||||
// Internal State
|
||||
// ========================================================================
|
||||
|
||||
/** All registered capture surface actors. */
|
||||
UPROPERTY()
|
||||
TArray<TWeakObjectPtr<ABP_PlanarCaptureActor>> RegisteredSurfaces;
|
||||
|
||||
/** Render target pool. */
|
||||
TArray<FPlanarCaptureRenderTargetEntry> RenderTargetPool;
|
||||
|
||||
/** Time accumulator for full re-evaluation interval. */
|
||||
float TimeSinceLastEvaluation = 0.0f;
|
||||
|
||||
/** Force-tier override — if set, all surfaces use this tier. */
|
||||
TOptional<EPlanarCaptureQualityTier> ForceTierOverride;
|
||||
|
||||
/** Count of surfaces at each tier (tracked for budget enforcement). */
|
||||
TMap<EPlanarCaptureQualityTier, int32> TierAssignmentCounts;
|
||||
|
||||
// ========================================================================
|
||||
// Internal Methods
|
||||
// ========================================================================
|
||||
|
||||
/** Evaluate all registered surfaces and assign quality tiers. */
|
||||
void EvaluateAllSurfaces();
|
||||
|
||||
/**
|
||||
* Score a single surface and determine its quality tier within budget constraints.
|
||||
* @return The assigned tier.
|
||||
*/
|
||||
EPlanarCaptureQualityTier ScoreAndAssignTier(UBPC_PlanarCapture* Capture);
|
||||
|
||||
/** Enforce budget limits — demote lower-priority surfaces if budget exceeded. */
|
||||
void EnforceBudgetLimits();
|
||||
|
||||
/** Create a new render target of the given size. */
|
||||
UTextureRenderTarget2D* CreateRenderTarget(int32 Size);
|
||||
|
||||
/** Get or create a render target for a given size (first checks pool). */
|
||||
UTextureRenderTarget2D* GetOrCreateRenderTarget(int32 Size);
|
||||
};
|
||||
105
Source/PG_Framework/Public/Core/DA_GameTagRegistry.h
Normal file
105
Source/PG_Framework/Public/Core/DA_GameTagRegistry.h
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — DA_GameTagRegistry (01)
|
||||
// Central gameplay tag registry Data Asset. Eliminates the 3 C++-only API workarounds
|
||||
// (RequestAllGameplayTags, RequestGameplayTag, UGameplayTagsManager singleton access).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "DA_GameTagRegistry.generated.h"
|
||||
|
||||
/**
|
||||
* DA_GameTagRegistry — Central GameplayTag namespace registry.
|
||||
*
|
||||
* In C++, this directly wraps UGameplayTagsManager APIs instead of using the
|
||||
* Data Table proxy workaround required in Blueprint. Maintains the Data Table
|
||||
* references for the Blueprint implementation guide, but the C++ functions
|
||||
* bypass them entirely for performance and correctness.
|
||||
*/
|
||||
UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "DA_GameTagRegistry"))
|
||||
class PG_FRAMEWORK_API UDA_GameTagRegistry : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UDA_GameTagRegistry();
|
||||
|
||||
// ========================================================================
|
||||
// Configuration
|
||||
// ========================================================================
|
||||
|
||||
/** Human-readable description of the tag namespace (e.g. "Player.State"). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Documentation")
|
||||
FText TagNamespace;
|
||||
|
||||
/** True for framework-defined tags, false for project-specific overrides. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Documentation")
|
||||
bool bIsFrameworkTag = true;
|
||||
|
||||
/**
|
||||
* Array of 11 per-category Data Tables used by Blueprint implementations.
|
||||
* C++ functions use UGameplayTagsManager directly and ignore this array.
|
||||
* Maintained for the Blueprint Manual Implementation Guide.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Config")
|
||||
TArray<TObjectPtr<UDataTable>> TagDataTables;
|
||||
|
||||
// ========================================================================
|
||||
// Query Functions
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Returns ALL registered gameplay tags from the engine's tag manager.
|
||||
* C++ implementation: single call to UGameplayTagsManager.
|
||||
* Blueprint equivalent: nested ForEachLoop over 11 Data Tables (workaround).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Tags|Query")
|
||||
TArray<FGameplayTag> GetAllRegisteredTags() const;
|
||||
|
||||
/**
|
||||
* Returns the human-readable display name of a gameplay tag.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Tags|Query")
|
||||
FText GetTagDisplayName(const FGameplayTag& Tag) const;
|
||||
|
||||
/**
|
||||
* Validates whether a gameplay tag is registered in the engine's tag table.
|
||||
* Returns false + logs warning for unregistered tags.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Tags|Validation")
|
||||
bool ValidateTag(const FGameplayTag& Tag) const;
|
||||
|
||||
/**
|
||||
* Validates that a tag exists AND returns it. Fails gracefully with warning.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Tags|Validation")
|
||||
FGameplayTag RequestTag(FName TagName, bool bLogWarning = true) const;
|
||||
|
||||
// ========================================================================
|
||||
// Debug / Tooling
|
||||
// ========================================================================
|
||||
|
||||
/** Prints all registered tags to the output log. Editor-only. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Tags|Debug")
|
||||
void LogAllTags() const;
|
||||
|
||||
/**
|
||||
* Exports all tags matching a namespace prefix as a formatted string.
|
||||
* Useful for auditing discrepancies between Data Tables and DefaultGameplayTags.ini.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Tags|Tooling")
|
||||
FString ExportTagNamespace(const FString& NamespacePrefix) const;
|
||||
|
||||
// ========================================================================
|
||||
// Overrides
|
||||
// ========================================================================
|
||||
|
||||
virtual void PostLoad() override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||
#endif
|
||||
};
|
||||
169
Source/PG_Framework/Public/Core/FL_GameUtilities.h
Normal file
169
Source/PG_Framework/Public/Core/FL_GameUtilities.h
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — FL_GameUtilities (02)
|
||||
// Shared Blueprint Function Library. In C++, all functions are static template/inline
|
||||
// with proper null-safety — no BP workarounds needed.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "FL_GameUtilities.generated.h"
|
||||
|
||||
class UGI_GameFramework;
|
||||
class APC_CoreController;
|
||||
|
||||
/**
|
||||
* FL_GameUtilities — Static utility functions available from any Blueprint or C++.
|
||||
*
|
||||
* In C++, these are proper static functions with template type-safety.
|
||||
* The Blueprint version requires macro wrappers for subsystem access —
|
||||
* here we get clean UFUNCTION(BlueprintCallable) with fast native paths.
|
||||
*/
|
||||
UCLASS()
|
||||
class PG_FRAMEWORK_API UFL_GameUtilities : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// ========================================================================
|
||||
// Subsystem Access (Null-Safe)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Safe subsystem retrieval — returns nullptr instead of crashing.
|
||||
* Use this instead of raw GetSubsystem() everywhere.
|
||||
*/
|
||||
template<typename T>
|
||||
static T* GetSubsystemSafe(const UObject* WorldContextObject)
|
||||
{
|
||||
if (!WorldContextObject)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("FL_GameUtilities::GetSubsystemSafe — Invalid WorldContextObject"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UGameInstance* GameInstance = WorldContextObject->GetWorld()->GetGameInstance();
|
||||
if (!GameInstance)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("FL_GameUtilities::GetSubsystemSafe — No GameInstance found"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
T* Subsystem = GameInstance->GetSubsystem<T>();
|
||||
if (!Subsystem)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("FL_GameUtilities::GetSubsystemSafe — Subsystem '%s' not found"),
|
||||
*T::StaticClass()->GetName());
|
||||
}
|
||||
|
||||
return Subsystem;
|
||||
}
|
||||
|
||||
// Blueprint-accessible subsystem getters (UFUNCTION wrappers for the template)
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Utilities",
|
||||
meta = (WorldContext = "WorldContextObject"))
|
||||
static UGI_GameFramework* GetGameFramework(const UObject* WorldContextObject);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Utilities",
|
||||
meta = (WorldContext = "WorldContextObject", DeterminesOutputType = "SubsystemClass"))
|
||||
static UGameInstanceSubsystem* GetSubsystemByClass(const UObject* WorldContextObject,
|
||||
TSubclassOf<UGameInstanceSubsystem> SubsystemClass);
|
||||
|
||||
// ========================================================================
|
||||
// Actor Utilities
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Finds the first component on an actor that implements a given interface.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Utilities")
|
||||
static UActorComponent* FindComponentByInterface(AActor* Actor,
|
||||
TSubclassOf<UInterface> InterfaceClass);
|
||||
|
||||
/**
|
||||
* Finds the nearest actor within a radius that has a specific gameplay tag.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Utilities",
|
||||
meta = (WorldContext = "WorldContextObject"))
|
||||
static AActor* FindNearestActorWithTag(const UObject* WorldContextObject,
|
||||
FVector Origin, float Radius, FGameplayTag RequiredTag);
|
||||
|
||||
// ========================================================================
|
||||
// Math Utilities
|
||||
// ========================================================================
|
||||
|
||||
/** Remap a value from [InMin, InMax] to [OutMin, OutMax]. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Math")
|
||||
static float RemapFloat(float Value, float InMin, float InMax, float OutMin, float OutMax);
|
||||
|
||||
/** Linear interpolation clamped to [0, 1]. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Math")
|
||||
static float LerpClamped(float A, float B, float Alpha);
|
||||
|
||||
/** Convert a direction vector to a 2D angle in degrees. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Math")
|
||||
static float VectorToAngle2D(FVector2D Direction);
|
||||
|
||||
/** Shortest signed angle difference between two angles in degrees. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Math")
|
||||
static float AngleDifference(float A, float B);
|
||||
|
||||
// ========================================================================
|
||||
// GameplayTag Utilities
|
||||
// ========================================================================
|
||||
|
||||
/** Safe gameplay tag check — returns false if actor has no tag container. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Tags")
|
||||
static bool HasGameplayTag(AActor* Actor, FGameplayTag Tag);
|
||||
|
||||
/**
|
||||
* Creates a gameplay tag from a string with validation.
|
||||
* Returns EmptyTag and logs warning if the tag isn't registered.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Tags")
|
||||
static FGameplayTag MakeTagFromString(const FString& TagString, bool bLogWarning = true);
|
||||
|
||||
// ========================================================================
|
||||
// Text Utilities
|
||||
// ========================================================================
|
||||
|
||||
/** Format seconds into HH:MM:SS string. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Text")
|
||||
static FText FormatTime(float TotalSeconds);
|
||||
|
||||
/** Returns singular or plural form based on count. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Text")
|
||||
static FText Pluralise(const FText& Singular, const FText& Plural, int32 Count);
|
||||
|
||||
/** Truncates text to MaxLength with ellipsis. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Text")
|
||||
static FText TruncateText(const FText& Text, int32 MaxLength);
|
||||
|
||||
// ========================================================================
|
||||
// Screen / Projection Utilities
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Projects a world position to screen space.
|
||||
* bIsOnScreen is false if the point is behind the camera.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Screen",
|
||||
meta = (WorldContext = "WorldContextObject"))
|
||||
static bool WorldToScreenSafe(const UObject* WorldContextObject, FVector WorldPosition,
|
||||
FVector2D& OutScreenPosition, bool& bIsOnScreen);
|
||||
|
||||
// ========================================================================
|
||||
// Debug (Shipping-safe)
|
||||
// ========================================================================
|
||||
|
||||
/** Debug log — stripped from shipping builds. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Debug")
|
||||
static void DebugLog(const FString& Message, bool bPrintToScreen = false,
|
||||
float ScreenDuration = 5.0f, FColor ScreenColor = FColor::Cyan);
|
||||
|
||||
/** Draw debug sphere in world — stripped from shipping builds. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Debug")
|
||||
static void DebugSphere(const UObject* WorldContextObject, FVector Location,
|
||||
float Radius = 50.0f, FColor Color = FColor::Green, float Duration = 5.0f);
|
||||
};
|
||||
226
Source/PG_Framework/Public/Core/GI_GameFramework.h
Normal file
226
Source/PG_Framework/Public/Core/GI_GameFramework.h
Normal file
@@ -0,0 +1,226 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — GI_GameFramework (04)
|
||||
// Application kernel GameInstance. Owns all SS_ subsystems, manages game phases,
|
||||
// platform initialization, save slot ownership, and service resolution.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "GI_GameFramework.generated.h"
|
||||
|
||||
// Forward declarations
|
||||
class UDA_GameTagRegistry;
|
||||
class USS_SaveManager;
|
||||
class USS_UIManager;
|
||||
class USS_SettingsSystem;
|
||||
class USS_EnhancedInputManager;
|
||||
class USS_AchievementSystem;
|
||||
class USS_AudioManager;
|
||||
|
||||
/**
|
||||
* Game Phase — top-level application state.
|
||||
* Transitions are server-authoritative; clients receive via OnRep_GamePhase.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EGamePhase : uint8
|
||||
{
|
||||
MainMenu UMETA(DisplayName = "Main Menu"),
|
||||
Loading UMETA(DisplayName = "Loading"),
|
||||
InGame UMETA(DisplayName = "In Game"),
|
||||
Paused UMETA(DisplayName = "Paused"),
|
||||
Cutscene UMETA(DisplayName = "Cutscene"),
|
||||
DeathLoop UMETA(DisplayName = "Death Loop"),
|
||||
AltDeathSpace UMETA(DisplayName = "Alt Death Space"),
|
||||
Credits UMETA(DisplayName = "Credits"),
|
||||
PostGame UMETA(DisplayName = "Post Game"),
|
||||
};
|
||||
|
||||
/** Platform type for platform-specific initialization routing. */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPlatformType : uint8
|
||||
{
|
||||
Generic UMETA(DisplayName = "Generic (PC)"),
|
||||
Steam UMETA(DisplayName = "Steam"),
|
||||
PS5 UMETA(DisplayName = "PlayStation 5"),
|
||||
Xbox UMETA(DisplayName = "Xbox Series X|S"),
|
||||
Switch UMETA(DisplayName = "Nintendo Switch"),
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Delegates (Event Dispatchers)
|
||||
// ============================================================================
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGamePhaseChanged, EGamePhase, NewPhase);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPlatformReady);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnFrameworkReady);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFrameworkInitFailed, const FString&, ErrorReason);
|
||||
|
||||
/**
|
||||
* GI_GameFramework — Application Kernel.
|
||||
*
|
||||
* The single persistent object that lives for the entire application session.
|
||||
* Owns all SS_ GameInstanceSubsystems, manages save slot ownership, provides
|
||||
* the canonical service resolver (GetService()), and tracks the top-level
|
||||
* game phase state machine.
|
||||
*
|
||||
* In C++, this replaces the Blueprint "Get Game Instance → Cast → Get Subsystem"
|
||||
* pattern with clean template access via GetService<T>().
|
||||
*/
|
||||
UCLASS(Blueprintable)
|
||||
class PG_FRAMEWORK_API UGI_GameFramework : public UGameInstance
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UGI_GameFramework();
|
||||
|
||||
// ========================================================================
|
||||
// Lifecycle
|
||||
// ========================================================================
|
||||
|
||||
virtual void Init() override;
|
||||
virtual void Shutdown() override;
|
||||
|
||||
// ========================================================================
|
||||
// Configuration
|
||||
// ========================================================================
|
||||
|
||||
/** Hard reference to the tag registry Data Asset. Loaded during Init. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Config")
|
||||
TObjectPtr<UDA_GameTagRegistry> TagRegistry;
|
||||
|
||||
/** If true, validates all tags during Init. Recommended: true for development. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Config")
|
||||
bool bValidateTagsOnInit = true;
|
||||
|
||||
/** If true, logs all registered tags to the output log during Init. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Debug")
|
||||
bool bLogTagsOnInit = false;
|
||||
|
||||
/** Platform override. Determined automatically; can be overridden via command-line: -Platform=Steam */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Platform")
|
||||
EPlatformType PlatformType = EPlatformType::Generic;
|
||||
|
||||
// ========================================================================
|
||||
// Game Phase State Machine
|
||||
// ========================================================================
|
||||
|
||||
/** Current top-level game phase. Server-authoritative; replicated to clients. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|State")
|
||||
EGamePhase CurrentGamePhase = EGamePhase::MainMenu;
|
||||
|
||||
/**
|
||||
* Sets the game phase and broadcasts OnGamePhaseChanged.
|
||||
* Server-authoritative. No-ops if phase is unchanged.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|State")
|
||||
void SetGamePhase(EGamePhase NewPhase);
|
||||
|
||||
/** Returns whether the framework has completed initialization. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|State")
|
||||
bool IsFrameworkReady() const { return bFrameworkInitialized; }
|
||||
|
||||
// ========================================================================
|
||||
// Session Flags (Transient, Non-Persisted)
|
||||
// ========================================================================
|
||||
|
||||
/** Gets a session flag value. Returns false if the flag doesn't exist. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Session")
|
||||
bool GetSessionFlag(FGameplayTag FlagTag) const;
|
||||
|
||||
/** Sets a session flag. Creates it if it doesn't exist. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Session")
|
||||
void SetSessionFlag(FGameplayTag FlagTag, bool bValue);
|
||||
|
||||
/** Clears all session flags. Called on new game start. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Session")
|
||||
void ClearAllSessionFlags();
|
||||
|
||||
// ========================================================================
|
||||
// Save Slot Management
|
||||
// ========================================================================
|
||||
|
||||
/** Currently active save slot index (-1 = none). */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Save")
|
||||
int32 ActiveSlotIndex = -1;
|
||||
|
||||
/** Designates the active save slot. Does NOT trigger a load. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Save")
|
||||
void SetActiveSlot(int32 SlotIndex);
|
||||
|
||||
// ========================================================================
|
||||
// Service Resolution
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Canonical subsystem accessor. Maps a GameplayTag to a subsystem class.
|
||||
* Returns nullptr and logs warning if tag isn't mapped or subsystem unavailable.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Services")
|
||||
UGameInstanceSubsystem* GetService(FGameplayTag ServiceTag) const;
|
||||
|
||||
/** Template accessor for type-safe subsystem retrieval. */
|
||||
template<typename T>
|
||||
T* GetService() const
|
||||
{
|
||||
return GetSubsystem<T>();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// First-Launch State
|
||||
// ========================================================================
|
||||
|
||||
/** True until onboarding/intro sequence clears it. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|State")
|
||||
bool bFirstLaunch = true;
|
||||
|
||||
// ========================================================================
|
||||
// Event Dispatchers
|
||||
// ========================================================================
|
||||
|
||||
/** Broadcast when game phase changes. All systems react to this — never poll CurrentGamePhase. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnGamePhaseChanged OnGamePhaseChanged;
|
||||
|
||||
/** Broadcast when platform-specific initialization is complete. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnPlatformReady OnPlatformReady;
|
||||
|
||||
/** Broadcast when framework initialization is complete and tag registry is validated. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnFrameworkReady OnFrameworkReady;
|
||||
|
||||
/** Broadcast if framework initialization fails (missing tag registry, zero tags, etc.). */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnFrameworkInitFailed OnFrameworkInitFailed;
|
||||
|
||||
protected:
|
||||
// ========================================================================
|
||||
// Internal State
|
||||
// ========================================================================
|
||||
|
||||
bool bFrameworkInitialized = false;
|
||||
|
||||
/** Map of GameplayTag → bool for session-scoped flags. */
|
||||
UPROPERTY()
|
||||
TMap<FGameplayTag, bool> SessionFlags;
|
||||
|
||||
/** Maps service GameplayTags to subsystem classes. Populated during Init. */
|
||||
UPROPERTY()
|
||||
TMap<FGameplayTag, TSubclassOf<UGameInstanceSubsystem>> ServiceRegistry;
|
||||
|
||||
// ========================================================================
|
||||
// Internal Methods
|
||||
// ========================================================================
|
||||
|
||||
/** Platform-specific initialization routing. */
|
||||
void InitPlatformServices();
|
||||
|
||||
/** Validates the tag registry and logs results. */
|
||||
void ValidateFrameworkTags();
|
||||
|
||||
/** Builds the ServiceRegistry map of GameplayTag → SubsystemClass. */
|
||||
void RegisterServices();
|
||||
};
|
||||
116
Source/PG_Framework/Public/Core/GM_CoreGameMode.h
Normal file
116
Source/PG_Framework/Public/Core/GM_CoreGameMode.h
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — GM_CoreGameMode (05)
|
||||
// Core Game Mode. Server-authoritative session rules, player spawning, chapter
|
||||
// transitions, death routing. In C++, extends the replicated GameMode base for
|
||||
// full networking support.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "GM_CoreGameMode.generated.h"
|
||||
|
||||
// Forward declarations
|
||||
class UGI_GameFramework;
|
||||
class AGS_CoreGameState;
|
||||
|
||||
// Delegates
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnChapterTransition, FGameplayTag, NewChapter);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameOverTriggered, FGameplayTag, EndingTag);
|
||||
|
||||
/**
|
||||
* GM_CoreGameMode — Core Game Mode.
|
||||
*
|
||||
* Sets the rules of the game session: which pawn, controller, player state,
|
||||
* HUD, and game state classes to use. Manages chapter transitions, win/loss/
|
||||
* death routing, and coordinates with the narrative system for story progression.
|
||||
*
|
||||
* Server-authoritative. Extends AGameModeBase for replication support.
|
||||
*
|
||||
* Note: PlayerControllerClass, PlayerStateClass, and GameStateClass are
|
||||
* inherited from AGameModeBase — do not redeclare (UHT forbids shadowing).
|
||||
* Set them in your BP child's Class Defaults.
|
||||
*/
|
||||
UCLASS(Blueprintable)
|
||||
class PG_FRAMEWORK_API AGM_CoreGameMode : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AGM_CoreGameMode();
|
||||
|
||||
// ========================================================================
|
||||
// Chapter Management
|
||||
// ========================================================================
|
||||
|
||||
/** The currently active chapter GameplayTag. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Narrative")
|
||||
FGameplayTag CurrentChapterTag;
|
||||
|
||||
/**
|
||||
* Transitions the game to a new chapter.
|
||||
* Server-authoritative. Sets phase to Loading, opens the chapter level,
|
||||
* then restores InGame phase on load complete.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Narrative")
|
||||
void TransitionToChapter(FGameplayTag ChapterTag);
|
||||
|
||||
// ========================================================================
|
||||
// Death Handling
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Handles player death. Called by BPC_DeathHandlingSystem.
|
||||
* Idempotent — safe to call multiple times.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Death")
|
||||
void HandlePlayerDead(AController* DeadController);
|
||||
|
||||
// ========================================================================
|
||||
// Ending / Game Over
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Triggers a specific ending condition.
|
||||
* Passes the ending tag to BPC_EndingAccumulator for accumulation logic.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Narrative")
|
||||
void TriggerEnding(FGameplayTag EndingTag);
|
||||
|
||||
// ========================================================================
|
||||
// Pause Control
|
||||
// ========================================================================
|
||||
|
||||
/** Runtime flag — menu widgets check this before pausing. False during cutscenes/death. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|State")
|
||||
bool bPauseAllowed = true;
|
||||
|
||||
// ========================================================================
|
||||
// Event Dispatchers
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnChapterTransition OnChapterTransition;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnGameOverTriggered OnGameOverTriggered;
|
||||
|
||||
// ========================================================================
|
||||
// Overrides
|
||||
// ========================================================================
|
||||
|
||||
virtual void InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) override;
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
protected:
|
||||
/** Cached reference to the framework GameInstance. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UGI_GameFramework> CachedFramework;
|
||||
|
||||
/** Server-side: performs the actual level transition for a chapter. */
|
||||
void ServerTransitionToChapter(FGameplayTag ChapterTag);
|
||||
|
||||
/** Called when the chapter level finishes loading. */
|
||||
void OnChapterLevelLoaded(FGameplayTag ChapterTag);
|
||||
};
|
||||
144
Source/PG_Framework/Public/Core/GS_CoreGameState.h
Normal file
144
Source/PG_Framework/Public/Core/GS_CoreGameState.h
Normal file
@@ -0,0 +1,144 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — GS_CoreGameState (06)
|
||||
// Shared session state. Fully replicated singleton visible to all players.
|
||||
// Tracks chapter, narrative phase, encounter status, and active objectives.
|
||||
// C++ gives us proper GetLifetimeReplicatedProps() and OnRep_ handlers.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameStateBase.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
#include "GS_CoreGameState.generated.h"
|
||||
|
||||
// Delegates
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnChapterChanged, FGameplayTag, NewChapter);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnNarrativePhaseChanged, FGameplayTag, NewPhase);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnEncounterStateChanged, bool, bActive);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnObjectiveTagsChanged);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayTimeUpdated, float, ElapsedSeconds);
|
||||
|
||||
/**
|
||||
* GS_CoreGameState — Shared Session State.
|
||||
*
|
||||
* A replicated singleton data holder. All modification happens through
|
||||
* dedicated setter functions — never direct variable writes.
|
||||
*
|
||||
* In C++, replication is handled via GetLifetimeReplicatedProps() with
|
||||
* OnRep_ handlers that mirror broadcast dispatchers for both network
|
||||
* clients and local single-player.
|
||||
*/
|
||||
UCLASS(Blueprintable)
|
||||
class PG_FRAMEWORK_API AGS_CoreGameState : public AGameStateBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AGS_CoreGameState();
|
||||
|
||||
// ========================================================================
|
||||
// Replicated State
|
||||
// ========================================================================
|
||||
|
||||
/** Elapsed play time (accumulated only when GamePhase is InGame). */
|
||||
UPROPERTY(ReplicatedUsing = OnRep_ElapsedPlayTime, BlueprintReadOnly, Category = "Framework|State")
|
||||
float ElapsedPlayTime = 0.0f;
|
||||
|
||||
/** Current story chapter tag. */
|
||||
UPROPERTY(ReplicatedUsing = OnRep_ActiveChapter, BlueprintReadOnly, Category = "Framework|Narrative")
|
||||
FGameplayTag ActiveChapterTag;
|
||||
|
||||
/** Sub-chapter narrative phase. */
|
||||
UPROPERTY(ReplicatedUsing = OnRep_NarrativePhase, BlueprintReadOnly, Category = "Framework|Narrative")
|
||||
FGameplayTag ActiveNarrativePhase;
|
||||
|
||||
/** Whether an AI encounter is currently active. */
|
||||
UPROPERTY(ReplicatedUsing = OnRep_EncounterActive, BlueprintReadOnly, Category = "Framework|Combat")
|
||||
bool bEncounterActive = false;
|
||||
|
||||
/** Array of active objective tags. */
|
||||
UPROPERTY(ReplicatedUsing = OnRep_ObjectiveTags, BlueprintReadOnly, Category = "Framework|Narrative")
|
||||
TArray<FGameplayTag> ActiveObjectiveTags;
|
||||
|
||||
// ========================================================================
|
||||
// Setters (Server-Authoritative)
|
||||
// ========================================================================
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Narrative")
|
||||
void SetChapter(FGameplayTag ChapterTag);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Narrative")
|
||||
void SetNarrativePhase(FGameplayTag PhaseTag);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Combat")
|
||||
void SetEncounterActive(bool bActive);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Narrative")
|
||||
void AddObjective(FGameplayTag ObjectiveTag);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Narrative")
|
||||
void RemoveObjective(FGameplayTag ObjectiveTag);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Narrative")
|
||||
void ClearAllObjectives();
|
||||
|
||||
// ========================================================================
|
||||
// Event Dispatchers
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnPlayTimeUpdated OnElapsedPlayTimeUpdated;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnChapterChanged OnChapterChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnNarrativePhaseChanged OnNarrativePhaseChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnEncounterStateChanged OnEncounterActiveStateChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnObjectiveTagsChanged OnObjectiveTagsChanged;
|
||||
|
||||
// ========================================================================
|
||||
// Overrides
|
||||
// ========================================================================
|
||||
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
virtual void BeginPlay() override;
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
|
||||
protected:
|
||||
// ========================================================================
|
||||
// OnRep Handlers — Fire dispatchers for network clients AND local single-player
|
||||
// ========================================================================
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_ElapsedPlayTime();
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_ActiveChapter();
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_NarrativePhase();
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_EncounterActive();
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_ObjectiveTags();
|
||||
|
||||
// ========================================================================
|
||||
// Internal
|
||||
// ========================================================================
|
||||
|
||||
/** Cached GameInstance for phase checking during time accumulation. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<class UGI_GameFramework> CachedFramework;
|
||||
|
||||
/** Throttle timer for play time updates (fires ~1/sec). */
|
||||
float TimeUpdateAccumulator = 0.0f;
|
||||
static constexpr float TimeUpdateInterval = 1.0f;
|
||||
};
|
||||
317
Source/PG_Framework/Public/Core/I_InterfaceLibrary.h
Normal file
317
Source/PG_Framework/Public/Core/I_InterfaceLibrary.h
Normal file
@@ -0,0 +1,317 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — Framework Interfaces (03)
|
||||
// All 9 Blueprint Interfaces defined in C++ for clean default values and type safety.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "I_InterfaceLibrary.generated.h"
|
||||
|
||||
// ============================================================================
|
||||
// Forward Declarations
|
||||
// ============================================================================
|
||||
|
||||
class UDA_ItemData;
|
||||
class UDA_InteractionData;
|
||||
|
||||
// ============================================================================
|
||||
// I_Interactable — World objects the player can interact with
|
||||
// ============================================================================
|
||||
|
||||
UINTERFACE(MinimalAPI, BlueprintType)
|
||||
class UInteractable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class PG_FRAMEWORK_API IInteractable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Called when a player interacts with this object. Returns true if interaction succeeded. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
bool OnInteract(AActor* Interactor);
|
||||
|
||||
/** Called when crosshair/focus enters this object. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void OnFocusBegin(AActor* Interactor);
|
||||
|
||||
/** Called when crosshair/focus leaves this object. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void OnFocusEnd(AActor* Interactor);
|
||||
|
||||
/** Returns the interaction prompt text (e.g. "Open Door", "Pick Up"). */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
FText GetInteractionPrompt() const;
|
||||
|
||||
/** Returns whether interaction is currently possible. BlockReason explains why if false. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
bool CanInteract(AActor* Interactor, FText& OutBlockReason) const;
|
||||
|
||||
/** Returns the GameplayTag identifying the interaction type (e.g. Framework.Interaction.Type.Pickup). */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
FGameplayTag GetInteractionType() const;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// I_Inspectable — Objects that can be examined in 3D inspect mode
|
||||
// ============================================================================
|
||||
|
||||
UINTERFACE(MinimalAPI, BlueprintType)
|
||||
class UInspectable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class PG_FRAMEWORK_API IInspectable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void StartInspect(AActor* Inspector);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void EndInspect(AActor* Inspector);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void RotateInspect(FRotator RotationDelta);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
bool GetInspectData(FVector& OutAnchorPoint, FRotator& OutDefaultRotation, float& OutZoomDistance) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
bool HasInspectInfo() const;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// I_Damageable — Anything that takes damage or can be healed
|
||||
// ============================================================================
|
||||
|
||||
UINTERFACE(MinimalAPI, BlueprintType)
|
||||
class UDamageable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class PG_FRAMEWORK_API IDamageable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Apply damage. Returns actual damage dealt after modifiers. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat")
|
||||
float TakeDamage(float DamageAmount, AActor* DamageCauser, FGameplayTag DamageType, FVector HitLocation, FVector HitDirection);
|
||||
|
||||
/** Heal by amount. Returns actual health restored. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat")
|
||||
float Heal(float HealAmount, AActor* Healer);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat")
|
||||
bool IsAlive() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat")
|
||||
float GetCurrentHealth() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat")
|
||||
float GetMaxHealth() const;
|
||||
|
||||
/** Called when health reaches zero. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat")
|
||||
void OnDeath(AActor* Killer, FGameplayTag DeathCause);
|
||||
|
||||
/** Returns multiplier for incoming damage (e.g. 1.5 = takes 50% more damage). */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat")
|
||||
float GetDamageModifier(FGameplayTag DamageType) const;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// I_Holdable — Physics objects the player can grab and manipulate
|
||||
// ============================================================================
|
||||
|
||||
UINTERFACE(MinimalAPI, BlueprintType)
|
||||
class UHoldable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class PG_FRAMEWORK_API IHoldable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void OnPickup(AActor* Holder);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void OnDrop(AActor* Dropper);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
FTransform GetHoldTransform() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
bool IsHeld() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void OnReleasedFromHold();
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// I_Lockable — Objects that can be locked/unlocked with key items
|
||||
// ============================================================================
|
||||
|
||||
UINTERFACE(MinimalAPI, BlueprintType)
|
||||
class ULockable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class PG_FRAMEWORK_API ILockable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
bool TryLock(AActor* Locker);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
bool TryUnlock(AActor* Unlocker, FGameplayTag KeyTag);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
bool IsLocked() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
FGameplayTag GetRequiredKeyTag() const;
|
||||
|
||||
/** Broadcast when lock state changes (implementor fires via delegate). */
|
||||
virtual void OnLockStateChanged(bool bNewLocked) {}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// I_UsableItem — Items that can be used from inventory/quick-slots
|
||||
// ============================================================================
|
||||
|
||||
UINTERFACE(MinimalAPI, BlueprintType)
|
||||
class UUsableItem : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class PG_FRAMEWORK_API IUsableItem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Inventory")
|
||||
bool UseItem(AActor* User, AActor* Target);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Inventory")
|
||||
bool CanUseItem(AActor* User, AActor* Target) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Inventory")
|
||||
float GetUseDuration() const;
|
||||
|
||||
/** Called after UseItem completes (for animations, effects). */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Inventory")
|
||||
void OnItemUsed(AActor* User);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// I_Persistable — Actors that save/load their state to disk
|
||||
// ============================================================================
|
||||
|
||||
UINTERFACE(MinimalAPI, BlueprintType)
|
||||
class UPersistable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class PG_FRAMEWORK_API IPersistable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Serialize state to a byte array for saving. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Save")
|
||||
TArray<uint8> OnSave();
|
||||
|
||||
/** Restore state from a previously saved byte array. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Save")
|
||||
void OnLoad(const TArray<uint8>& Data);
|
||||
|
||||
/** Returns the unique GameplayTag identifier for this persistable actor. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Save")
|
||||
FGameplayTag GetSaveTag() const;
|
||||
|
||||
/** Returns true if this actor has changed since last save. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Save")
|
||||
bool NeedsSave() const;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// I_Toggleable — Objects with binary on/off states
|
||||
// ============================================================================
|
||||
|
||||
UINTERFACE(MinimalAPI, BlueprintType)
|
||||
class UToggleable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class PG_FRAMEWORK_API IToggleable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void Toggle(AActor* Toggler);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void SetState(bool bNewState, AActor* Setter);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
bool GetCurrentState() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
FText GetStateLabel() const;
|
||||
|
||||
/** Broadcast when state changes. */
|
||||
virtual void OnStateChanged(bool bNewState, AActor* Changer) {}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// I_Adjustable — Objects with continuous value range (dials, sliders)
|
||||
// ============================================================================
|
||||
|
||||
UINTERFACE(MinimalAPI, BlueprintType)
|
||||
class UAdjustable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class PG_FRAMEWORK_API IAdjustable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void Adjust(float Delta, AActor* Adjuster);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
void SetValue(float NewValue, AActor* Setter);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
float GetCurrentValue() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
float GetMinValue() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction")
|
||||
float GetMaxValue() const;
|
||||
|
||||
/** Broadcast when value changes. */
|
||||
virtual void OnValueChanged(float NewValue, AActor* Changer) {}
|
||||
};
|
||||
232
Source/PG_Framework/Public/Input/SS_EnhancedInputManager.h
Normal file
232
Source/PG_Framework/Public/Input/SS_EnhancedInputManager.h
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — SS_EnhancedInputManager (128)
|
||||
// Sole authority for Push/Pop input context, key rebinding, and input mode changes.
|
||||
// In C++, directly wraps UEnhancedInputLocalPlayerSubsystem with priority-based
|
||||
// context stack management.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "InputAction.h"
|
||||
#include "SS_EnhancedInputManager.generated.h"
|
||||
|
||||
class UInputMappingContext;
|
||||
class UInputAction;
|
||||
class UEnhancedInputLocalPlayerSubsystem;
|
||||
|
||||
/**
|
||||
* Input context priority ladder.
|
||||
* Higher priority contexts override lower priority for conflicting inputs.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EInputContextPriority : uint8
|
||||
{
|
||||
Default = 0 UMETA(DisplayName = "Default (0)"),
|
||||
Hiding = 5 UMETA(DisplayName = "Hiding (5)"),
|
||||
Wristwatch = 10 UMETA(DisplayName = "Wristwatch UI (10)"),
|
||||
Inspection = 20 UMETA(DisplayName = "Inspection (20)"),
|
||||
UI = 100 UMETA(DisplayName = "UI (100)"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping context entry in the stack.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FInputContextEntry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
TObjectPtr<UInputMappingContext> Context = nullptr;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
EInputContextPriority Priority = EInputContextPriority::Default;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
FGameplayTag ContextTag; // For identification: Framework.Input.Context.Default, etc.
|
||||
|
||||
bool operator==(const FInputContextEntry& Other) const
|
||||
{
|
||||
return Context == Other.Context;
|
||||
}
|
||||
};
|
||||
|
||||
// Delegates
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnContextPushed, FGameplayTag, ContextTag, EInputContextPriority, Priority);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnContextPopped, FGameplayTag, ContextTag, EInputContextPriority, Priority);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInputModeChanged, bool, bUIMode, bool, bShowCursor);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnKeyRebound, FGameplayTag, ActionTag, FKey, NewKey);
|
||||
|
||||
/**
|
||||
* SS_EnhancedInputManager — Input Context Stack Authority.
|
||||
*
|
||||
* Manages all Enhanced Input Mapping Contexts with priority-based ordering.
|
||||
* Systems call PushContext/PopContext instead of directly touching the
|
||||
* Enhanced Input subsystem. Coordinates input mode (game/UI) with SS_UIManager.
|
||||
*
|
||||
* C++ gives us direct UEnhancedInputLocalPlayerSubsystem access — no
|
||||
* "Get Enhanced Input Local Player Subsystem" node chains.
|
||||
*/
|
||||
UCLASS()
|
||||
class PG_FRAMEWORK_API USS_EnhancedInputManager : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
USS_EnhancedInputManager();
|
||||
|
||||
// ========================================================================
|
||||
// Lifecycle
|
||||
// ========================================================================
|
||||
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
// ========================================================================
|
||||
// Context Stack Management
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Push an input mapping context onto the stack.
|
||||
* Higher priority contexts override lower for conflicting inputs.
|
||||
* Duplicate protection — if the context is already active, it's moved to top.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Input")
|
||||
void PushContext(UInputMappingContext* Context, EInputContextPriority Priority, FGameplayTag ContextTag);
|
||||
|
||||
/**
|
||||
* Remove an input mapping context from the stack.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Input")
|
||||
void PopContext(UInputMappingContext* Context);
|
||||
|
||||
/**
|
||||
* Pop a context by its GameplayTag identifier.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Input")
|
||||
void PopContextByTag(FGameplayTag ContextTag);
|
||||
|
||||
/**
|
||||
* Clear ALL contexts from the stack (e.g., on level transition).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Input")
|
||||
void ClearAllContexts();
|
||||
|
||||
/**
|
||||
* Returns whether a context is currently active on the stack.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input")
|
||||
bool IsContextActive(FGameplayTag ContextTag) const;
|
||||
|
||||
/**
|
||||
* Returns the currently highest-priority active context.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input")
|
||||
FGameplayTag GetTopContext() const;
|
||||
|
||||
// ========================================================================
|
||||
// Input Mode Coordination
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Switch between game input mode and UI input mode.
|
||||
* Coordinates cursor visibility and input blocking with SS_UIManager.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Input")
|
||||
void SetInputMode(bool bUIMode, bool bShowCursor = true, bool bLockMouseToViewport = false);
|
||||
|
||||
/** Returns whether UI input mode is active. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input")
|
||||
bool IsUIMode() const { return bCurrentUIMode; }
|
||||
|
||||
// ========================================================================
|
||||
// Key Rebinding
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Rebind a key for a specific input action.
|
||||
* Persists via SS_SettingsSystem.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Input")
|
||||
void RebindKey(UInputAction* Action, FKey NewKey, bool bSaveToDisk = true);
|
||||
|
||||
/**
|
||||
* Reset all key bindings to defaults.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Input")
|
||||
void ResetAllBindings();
|
||||
|
||||
/**
|
||||
* Get the current key bound to an input action.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input")
|
||||
FKey GetBoundKey(UInputAction* Action) const;
|
||||
|
||||
// ========================================================================
|
||||
// Query
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Check if a specific input action is currently being pressed.
|
||||
* Use this instead of raw Enhanced Input queries.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input")
|
||||
bool IsActionPressed(UInputAction* Action) const;
|
||||
|
||||
/**
|
||||
* Get the current value of an input action (0.0 to 1.0 for digital, axis value for analog).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input")
|
||||
float GetActionValue(UInputAction* Action) const;
|
||||
|
||||
// ========================================================================
|
||||
// Configuration
|
||||
// ========================================================================
|
||||
|
||||
/** Default input mapping contexts (loaded on initialize). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Input|Config")
|
||||
TArray<TObjectPtr<UInputMappingContext>> DefaultContexts;
|
||||
|
||||
// ========================================================================
|
||||
// Event Dispatchers
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Input|Events")
|
||||
FOnContextPushed OnContextPushed;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Input|Events")
|
||||
FOnContextPopped OnContextPopped;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Input|Events")
|
||||
FOnInputModeChanged OnInputModeChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Input|Events")
|
||||
FOnKeyRebound OnKeyRebound;
|
||||
|
||||
protected:
|
||||
// ========================================================================
|
||||
// Internal State
|
||||
// ========================================================================
|
||||
|
||||
/** Ordered context stack (highest priority at the end/newest). */
|
||||
TArray<FInputContextEntry> ContextStack;
|
||||
|
||||
/** Whether UI input mode is currently active. */
|
||||
bool bCurrentUIMode = false;
|
||||
|
||||
/** Cached Enhanced Input subsystem. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UEnhancedInputLocalPlayerSubsystem> EnhancedInputSubsystem;
|
||||
|
||||
// ========================================================================
|
||||
// Internal Methods
|
||||
// ========================================================================
|
||||
|
||||
/** Re-sorts the context stack by priority and re-applies to the subsystem. */
|
||||
void RebuildContextStack();
|
||||
|
||||
/** Gets the Enhanced Input subsystem, caching it if needed. */
|
||||
UEnhancedInputLocalPlayerSubsystem* GetEnhancedInputSubsystem() const;
|
||||
};
|
||||
210
Source/PG_Framework/Public/Inventory/BPC_InventorySystem.h
Normal file
210
Source/PG_Framework/Public/Inventory/BPC_InventorySystem.h
Normal file
@@ -0,0 +1,210 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BPC_InventorySystem (31)
|
||||
// Core inventory grid. Add/remove/sort/stack/weight management.
|
||||
// In C++, TArray operations with lambdas are natively fast — no BP array node overhead.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "BPC_InventorySystem.generated.h"
|
||||
|
||||
class UDA_ItemData;
|
||||
|
||||
/**
|
||||
* Single inventory slot entry.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FInventorySlot
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** The item in this slot. nullptr = empty slot. */
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
TObjectPtr<UDA_ItemData> Item = nullptr;
|
||||
|
||||
/** How many of this item are stacked here. */
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
int32 Quantity = 0;
|
||||
|
||||
/** Grid position for UI layout. */
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
int32 GridX = 0;
|
||||
|
||||
/** Grid position for UI layout. */
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
int32 GridY = 0;
|
||||
|
||||
bool IsEmpty() const { return Item == nullptr || Quantity <= 0; }
|
||||
|
||||
void Clear()
|
||||
{
|
||||
Item = nullptr;
|
||||
Quantity = 0;
|
||||
}
|
||||
|
||||
bool operator==(const FInventorySlot& Other) const
|
||||
{
|
||||
return Item == Other.Item && Quantity == Other.Quantity && GridX == Other.GridX && GridY == Other.GridY;
|
||||
}
|
||||
};
|
||||
|
||||
// Delegates
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnInventoryChanged);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnItemAdded, UDA_ItemData*, Item, int32, Quantity);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnItemRemoved, UDA_ItemData*, Item, int32, Quantity);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnWeightChanged, float, CurrentWeight, float, MaxWeight);
|
||||
|
||||
/**
|
||||
* BPC_InventorySystem — Core Inventory Grid.
|
||||
*
|
||||
* Manages the player's carried items: add, remove, sort, stack, weight tracking.
|
||||
* C++ TArray operations (FindByPredicate, Sort, Filter) are natively compiled —
|
||||
* no BP interpretive array node overhead.
|
||||
*/
|
||||
UCLASS(Blueprintable, ClassGroup = (Framework), meta = (BlueprintSpawnableComponent))
|
||||
class PG_FRAMEWORK_API UBPC_InventorySystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_InventorySystem();
|
||||
|
||||
// ========================================================================
|
||||
// Configuration
|
||||
// ========================================================================
|
||||
|
||||
/** Grid width (columns). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Inventory|Config")
|
||||
int32 GridWidth = 8;
|
||||
|
||||
/** Grid height (rows). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Inventory|Config")
|
||||
int32 GridHeight = 5;
|
||||
|
||||
/** Maximum carry weight. Items exceeding this cannot be picked up. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Inventory|Config")
|
||||
float MaxWeight = 50.0f;
|
||||
|
||||
// ========================================================================
|
||||
// Inventory State
|
||||
// ========================================================================
|
||||
|
||||
/** All inventory slots (GridWidth × GridHeight). */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Inventory")
|
||||
TArray<FInventorySlot> Slots;
|
||||
|
||||
/** Current total weight carried. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Inventory")
|
||||
float CurrentWeight = 0.0f;
|
||||
|
||||
/** Whether the inventory has been modified since last save. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Inventory")
|
||||
bool bDirty = false;
|
||||
|
||||
// ========================================================================
|
||||
// Core Operations
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Add an item to the inventory. Stacks if possible, finds empty slot otherwise.
|
||||
* Returns the quantity actually added (may be less than requested if full).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Inventory")
|
||||
int32 AddItem(UDA_ItemData* Item, int32 Quantity = 1);
|
||||
|
||||
/**
|
||||
* Remove an item from the inventory.
|
||||
* Returns the quantity actually removed.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Inventory")
|
||||
int32 RemoveItem(UDA_ItemData* Item, int32 Quantity = 1);
|
||||
|
||||
/**
|
||||
* Remove an item from a specific slot.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Inventory")
|
||||
int32 RemoveItemFromSlot(int32 SlotIndex, int32 Quantity = 1);
|
||||
|
||||
/**
|
||||
* Check if an item can be added (enough space and weight capacity).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory")
|
||||
bool CanAddItem(UDA_ItemData* Item, int32 Quantity = 1) const;
|
||||
|
||||
// ========================================================================
|
||||
// Query
|
||||
// ========================================================================
|
||||
|
||||
/** Returns the total quantity of an item across all stacks. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory")
|
||||
int32 GetItemCount(UDA_ItemData* Item) const;
|
||||
|
||||
/** Returns whether the inventory contains at least this many of an item. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory")
|
||||
bool HasItem(UDA_ItemData* Item, int32 Quantity = 1) const;
|
||||
|
||||
/** Finds the first slot containing the given item. Returns -1 if not found. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory")
|
||||
int32 FindItemSlot(UDA_ItemData* Item) const;
|
||||
|
||||
/** Returns all unique items currently in the inventory. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory")
|
||||
TArray<UDA_ItemData*> GetAllItems() const;
|
||||
|
||||
/** Returns the number of empty slots available. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory")
|
||||
int32 GetEmptySlotCount() const;
|
||||
|
||||
/** Returns the number of free weight units remaining. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory")
|
||||
float GetRemainingWeight() const;
|
||||
|
||||
// ========================================================================
|
||||
// Organization
|
||||
// ========================================================================
|
||||
|
||||
/** Sort inventory by ItemType, then by DisplayName. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Inventory")
|
||||
void SortInventory();
|
||||
|
||||
/** Auto-merge all partial stacks of the same item. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Inventory")
|
||||
void ConsolidateStacks();
|
||||
|
||||
// ========================================================================
|
||||
// Event Dispatchers
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Inventory|Events")
|
||||
FOnInventoryChanged OnInventoryChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Inventory|Events")
|
||||
FOnItemAdded OnItemAdded;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Inventory|Events")
|
||||
FOnItemRemoved OnItemRemoved;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Inventory|Events")
|
||||
FOnWeightChanged OnWeightChanged;
|
||||
|
||||
// ========================================================================
|
||||
// Overrides
|
||||
// ========================================================================
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
protected:
|
||||
/** Recalculates total weight from all slots. */
|
||||
void RecalculateWeight();
|
||||
|
||||
/** Finds an existing stack for an item (not at max stack limit). Returns -1 if none found. */
|
||||
int32 FindExistingStack(UDA_ItemData* Item) const;
|
||||
|
||||
/** Finds the first empty slot. Returns -1 if inventory is full. */
|
||||
int32 FindEmptySlot() const;
|
||||
|
||||
/** Marks inventory as modified and broadcasts change dispatchers. */
|
||||
void MarkDirty();
|
||||
};
|
||||
37
Source/PG_Framework/Public/Inventory/DA_EquipmentConfig.h
Normal file
37
Source/PG_Framework/Public/Inventory/DA_EquipmentConfig.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "DA_EquipmentConfig.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FDamageTypeResistance
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FGameplayTag DamageType;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float Resistance = 0.0f;
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class PG_FRAMEWORK_API UDA_EquipmentConfig : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
|
||||
TArray<FDamageTypeResistance> DamageTypeResistances;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
|
||||
float Durability = 100.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
|
||||
float Weight = 1.0f;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Config")
|
||||
float GetResistance(FGameplayTag DamageType) const;
|
||||
};
|
||||
232
Source/PG_Framework/Public/Inventory/DA_ItemData.h
Normal file
232
Source/PG_Framework/Public/Inventory/DA_ItemData.h
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — DA_ItemData (07)
|
||||
// Base item Data Asset. Single source of truth for every item.
|
||||
// C++ gives us UPROPERTY metadata (EditCondition, EditConditionHides, ClampMin/Max)
|
||||
// that make the Data Asset editor usable for designers — impossible in Blueprint.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "DA_ItemData.generated.h"
|
||||
|
||||
/**
|
||||
* Item type classification.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EItemType : uint8
|
||||
{
|
||||
Weapon UMETA(DisplayName = "Weapon"),
|
||||
Ammo UMETA(DisplayName = "Ammo"),
|
||||
Consumable UMETA(DisplayName = "Consumable"),
|
||||
KeyItem UMETA(DisplayName = "Key Item"),
|
||||
Document UMETA(DisplayName = "Document"),
|
||||
Collectible UMETA(DisplayName = "Collectible"),
|
||||
Tool UMETA(DisplayName = "Tool"),
|
||||
Resource UMETA(DisplayName = "Resource"),
|
||||
Misc UMETA(DisplayName = "Misc"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Equipment slot type.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EEquipmentSlot : uint8
|
||||
{
|
||||
None UMETA(DisplayName = "None"),
|
||||
PrimaryWeapon UMETA(DisplayName = "Primary Weapon"),
|
||||
SecondaryWeapon UMETA(DisplayName = "Secondary Weapon"),
|
||||
Melee UMETA(DisplayName = "Melee"),
|
||||
Tool UMETA(DisplayName = "Tool"),
|
||||
Armor UMETA(DisplayName = "Armor"),
|
||||
Accessory UMETA(DisplayName = "Accessory"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Equipment-specific data (shown when ItemType is Weapon or Tool).
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FItemEquipmentData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
EEquipmentSlot Slot = EEquipmentSlot::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float Damage = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float FireRate = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float Range = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
int32 MagazineSize = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float ReloadTime = 0.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Consumable-specific data (shown when ItemType is Consumable).
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FItemConsumableData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", ClampMax = "100"))
|
||||
float HealthRestore = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", ClampMax = "100"))
|
||||
float StressReduce = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float UseDuration = 1.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
bool bConsumedOnUse = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inspect-specific data (shown when bHasInspectMode is true).
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FItemInspectData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FVector AnchorPoint = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FRotator DefaultRotation = FRotator::ZeroRotator;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float ZoomDistance = 50.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
bool bCanRotate = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* DA_ItemData — Base Item Data Asset.
|
||||
*
|
||||
* Every item in the game is one DA_ItemData asset. No item data lives in
|
||||
* Blueprint logic. C++ gives us EditCondition metadata so the editor only
|
||||
* shows relevant sub-structs based on ItemType — a massive UX win for designers.
|
||||
*/
|
||||
UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "Item Data"))
|
||||
class PG_FRAMEWORK_API UDA_ItemData : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UDA_ItemData();
|
||||
|
||||
// ========================================================================
|
||||
// Core Properties (Every Item Has These)
|
||||
// ========================================================================
|
||||
|
||||
/** Unique GameplayTag identifier. Must be registered in DA_GameTagRegistry. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core")
|
||||
FGameplayTag ItemTag;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core")
|
||||
FText DisplayName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core", meta = (MultiLine = true))
|
||||
FText Description;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core")
|
||||
TSoftObjectPtr<UTexture2D> Icon;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core")
|
||||
TSoftObjectPtr<UStaticMesh> WorldMesh;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core", meta = (ClampMin = "0", ClampMax = "1000"))
|
||||
float Weight = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core", meta = (ClampMin = "1", ClampMax = "999"))
|
||||
int32 StackLimit = 1;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core")
|
||||
EItemType ItemType = EItemType::Misc;
|
||||
|
||||
// ========================================================================
|
||||
// Conditional Sub-Data (Shown Based on ItemType via EditCondition)
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Equipment",
|
||||
meta = (EditCondition = "ItemType == EItemType::Weapon || ItemType == EItemType::Tool", EditConditionHides))
|
||||
FItemEquipmentData EquipmentData;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable",
|
||||
meta = (EditCondition = "ItemType == EItemType::Consumable", EditConditionHides))
|
||||
FItemConsumableData ConsumableData;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Inspect",
|
||||
meta = (EditCondition = "bHasInspectMode", EditConditionHides))
|
||||
FItemInspectData InspectData;
|
||||
|
||||
// ========================================================================
|
||||
// Flags
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Flags")
|
||||
bool bIsKeyItem = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Flags")
|
||||
bool bCanBeDropped = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Flags")
|
||||
bool bHasInspectMode = false;
|
||||
|
||||
// ========================================================================
|
||||
// Combination / Crafting
|
||||
// ========================================================================
|
||||
|
||||
/** Tags of items this can combine with. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Combination")
|
||||
TArray<FGameplayTag> CombinesWith;
|
||||
|
||||
/** The resulting item tag when combined with CombinesWith item. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Combination")
|
||||
FGameplayTag CombineResult;
|
||||
|
||||
// ========================================================================
|
||||
// Extensibility
|
||||
// ========================================================================
|
||||
|
||||
/** Custom per-project properties — no need to modify the base class. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Custom")
|
||||
TMap<FName, FString> CustomProperties;
|
||||
|
||||
// ========================================================================
|
||||
// Validation (Editor-Only)
|
||||
// ========================================================================
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Validates the item data asset for common errors.
|
||||
* Called by editor utilities or pre-save validation.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Validation")
|
||||
bool ValidateItemData(FString& OutErrors) const;
|
||||
#endif
|
||||
|
||||
// ========================================================================
|
||||
// Overrides
|
||||
// ========================================================================
|
||||
|
||||
virtual void PostLoad() override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||
#endif
|
||||
};
|
||||
29
Source/PG_Framework/Public/Player/BPC_HealthSystem.h
Normal file
29
Source/PG_Framework/Public/Player/BPC_HealthSystem.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "BPC_HealthSystem.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHealthChanged, float, CurrentHealth, float, MaxHealth);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath);
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class PG_FRAMEWORK_API UBPC_HealthSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_HealthSystem();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Health")
|
||||
float MaxHealth = 100.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Health")
|
||||
float CurrentHealth = 100.0f;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnHealthChanged OnHealthChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnDeath OnDeath;
|
||||
};
|
||||
23
Source/PG_Framework/Public/Player/BPC_MovementStateSystem.h
Normal file
23
Source/PG_Framework/Public/Player/BPC_MovementStateSystem.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "BPC_MovementStateSystem.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnMovementModeChanged, FGameplayTag, NewMode, FGameplayTag, OldMode);
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class PG_FRAMEWORK_API UBPC_MovementStateSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_MovementStateSystem();
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Movement")
|
||||
FGameplayTag CurrentMovementMode;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnMovementModeChanged OnMovementModeChanged;
|
||||
};
|
||||
25
Source/PG_Framework/Public/Player/BPC_StaminaSystem.h
Normal file
25
Source/PG_Framework/Public/Player/BPC_StaminaSystem.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "BPC_StaminaSystem.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnExhaustionStateChanged, bool, bExhausted);
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class PG_FRAMEWORK_API UBPC_StaminaSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_StaminaSystem();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Stamina")
|
||||
float MaxStamina = 100.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Stamina")
|
||||
float CurrentStamina = 100.0f;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnExhaustionStateChanged OnExhaustionStateChanged;
|
||||
};
|
||||
246
Source/PG_Framework/Public/Player/BPC_StateManager.h
Normal file
246
Source/PG_Framework/Public/Player/BPC_StateManager.h
Normal file
@@ -0,0 +1,246 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BPC_StateManager (130)
|
||||
// Central State Authority. Single source of truth for "what can the player do right now?"
|
||||
// Manages exclusive action states, upper-body overlay states, action gating,
|
||||
// vital signs (heart rate), and the force-stack pattern for nested overrides.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "BPC_StateManager.generated.h"
|
||||
|
||||
// Forward declarations
|
||||
class UDA_StateGatingTable;
|
||||
class UBPC_HealthSystem;
|
||||
class UBPC_StressSystem;
|
||||
class UBPC_StaminaSystem;
|
||||
class UBPC_MovementStateSystem;
|
||||
|
||||
// ============================================================================
|
||||
// Delegates
|
||||
// ============================================================================
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnActionStateChanged, FGameplayTag, NewState, FGameplayTag, OldState);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnOverlayStateChanged, FGameplayTag, NewOverlay, FGameplayTag, OldOverlay);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnVitalSignChanged, FGameplayTag, VitalTag, float, NewValue);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnForceStackPushed, FGameplayTag, ForceState);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnForceStackPopped, FGameplayTag, RestoredState);
|
||||
|
||||
/**
|
||||
* Result codes for state change requests.
|
||||
* Mirrors the Blueprint E_ActionRequestResult enum.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EActionRequestResult : uint8
|
||||
{
|
||||
Granted UMETA(DisplayName = "Granted"),
|
||||
Denied UMETA(DisplayName = "Denied — Gated"),
|
||||
BlockedByForce UMETA(DisplayName = "Blocked — Force Stack Override"),
|
||||
AlreadyActive UMETA(DisplayName = "Already Active"),
|
||||
InvalidState UMETA(DisplayName = "Invalid State Tag"),
|
||||
RequesterNotFound UMETA(DisplayName = "Requester Not Found"),
|
||||
CooldownActive UMETA(DisplayName = "Cooldown Active"),
|
||||
VitalThreshold UMETA(DisplayName = "Vital Threshold Not Met"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Heart rate tier for vital sign tracking.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EHeartRateTier : uint8
|
||||
{
|
||||
Resting UMETA(DisplayName = "Resting (60-80 BPM)"),
|
||||
Elevated UMETA(DisplayName = "Elevated (80-100 BPM)"),
|
||||
Stressed UMETA(DisplayName = "Stressed (100-130 BPM)"),
|
||||
Panic UMETA(DisplayName = "Panic (130-160 BPM)"),
|
||||
Critical UMETA(DisplayName = "Critical (160+ BPM)"),
|
||||
};
|
||||
|
||||
/**
|
||||
* BPC_StateManager — Central State Authority.
|
||||
*
|
||||
* Every system queries IsActionPermitted(Tag) instead of checking other systems
|
||||
* directly. Gating rules are defined in DA_StateGatingTable (37 rules).
|
||||
* In C++, the Chooser Table iteration is native-speed — no BP interpretive overhead.
|
||||
*/
|
||||
UCLASS(Blueprintable, ClassGroup = (Framework), meta = (BlueprintSpawnableComponent))
|
||||
class PG_FRAMEWORK_API UBPC_StateManager : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_StateManager();
|
||||
|
||||
// ========================================================================
|
||||
// Configuration
|
||||
// ========================================================================
|
||||
|
||||
/** The gating rules Data Asset. Contains all 37 action rules. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Config")
|
||||
TObjectPtr<UDA_StateGatingTable> GatingTable;
|
||||
|
||||
/** Default action state on BeginPlay. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Config")
|
||||
FGameplayTag DefaultActionState;
|
||||
|
||||
/** Default overlay state on BeginPlay. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Config")
|
||||
FGameplayTag DefaultOverlayState;
|
||||
|
||||
// ========================================================================
|
||||
// Core Query — Hot Path
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Central query: "Can the player perform this action right now?"
|
||||
* Called by EVERY gameplay system per-frame. C++ makes this fast.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|State")
|
||||
bool IsActionPermitted(FGameplayTag ActionTag) const;
|
||||
|
||||
/**
|
||||
* Request a state change. Returns the result code.
|
||||
* Server-authoritative.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|State")
|
||||
EActionRequestResult RequestStateChange(FGameplayTag NewState, AActor* Requester);
|
||||
|
||||
// ========================================================================
|
||||
// Current State (Read-Only)
|
||||
// ========================================================================
|
||||
|
||||
/** Currently active exclusive action state (only one at a time). */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|State")
|
||||
FGameplayTag CurrentActionState;
|
||||
|
||||
/** Currently active upper-body overlay state. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|State")
|
||||
FGameplayTag CurrentOverlayState;
|
||||
|
||||
// ========================================================================
|
||||
// Force Stack Pattern (Death, Cutscenes, Void Space)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Pushes a forced state onto the stack. Overrides all gating.
|
||||
* Example: death overrides everything.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|State")
|
||||
void ForceStateChange(FGameplayTag ForceState, FString Reason);
|
||||
|
||||
/**
|
||||
* Pops the top forced state and restores the previous state.
|
||||
* Example: respawn restores the pre-death state.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|State")
|
||||
void RestorePreviousState();
|
||||
|
||||
/** Returns the number of states currently on the force stack. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|State")
|
||||
int32 GetForceStackDepth() const { return ForceStack.Num(); }
|
||||
|
||||
// ========================================================================
|
||||
// Vital Signs — Heart Rate
|
||||
// ========================================================================
|
||||
|
||||
/** Current heart rate in BPM (smoothed). */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Vitals")
|
||||
float HeartRateBPM = 70.0f;
|
||||
|
||||
/** Current heart rate tier. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Vitals")
|
||||
EHeartRateTier HeartRateTier = EHeartRateTier::Resting;
|
||||
|
||||
/** Target heart rate (set by stress, stamina, combat). Interpolated toward each tick. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Vitals")
|
||||
float TargetHeartRate = 70.0f;
|
||||
|
||||
/** Smoothing speed for heart rate interpolation. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Vitals")
|
||||
float HeartRateSmoothSpeed = 2.0f;
|
||||
|
||||
// ========================================================================
|
||||
// Event Dispatchers
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnActionStateChanged OnActionStateChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnOverlayStateChanged OnOverlayStateChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnVitalSignChanged OnVitalSignChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnForceStackPushed OnForceStackPushed;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnForceStackPopped OnForceStackPopped;
|
||||
|
||||
// ========================================================================
|
||||
// Overrides
|
||||
// ========================================================================
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
protected:
|
||||
// ========================================================================
|
||||
// Internal State
|
||||
// ========================================================================
|
||||
|
||||
/** Force stack — array of (State, Reason) pairs. Most recent is active. */
|
||||
struct FForceStackEntry
|
||||
{
|
||||
FGameplayTag State;
|
||||
FString Reason;
|
||||
};
|
||||
|
||||
TArray<FForceStackEntry> ForceStack;
|
||||
|
||||
/** Previous action state before force override (for restore). */
|
||||
FGameplayTag PreForceActionState;
|
||||
|
||||
/** Previous overlay state before force override (for restore). */
|
||||
FGameplayTag PreForceOverlayState;
|
||||
|
||||
// ========================================================================
|
||||
// Gating Logic
|
||||
// ========================================================================
|
||||
|
||||
/** Check gating rules for a tag against current state. */
|
||||
bool EvaluateGatingRules(FGameplayTag ActionTag) const;
|
||||
|
||||
/** Check if any force stack entry blocks this action. */
|
||||
bool IsBlockedByForceStack(FGameplayTag ActionTag) const;
|
||||
|
||||
// ========================================================================
|
||||
// Vital Sign Calculation
|
||||
// ========================================================================
|
||||
|
||||
/** Recalculates target heart rate based on stress tier + stamina exhaustion + combat. */
|
||||
void RecalculateTargetHeartRate();
|
||||
|
||||
/** Determines heart rate tier from current BPM. */
|
||||
static EHeartRateTier GetHeartRateTier(float BPM);
|
||||
|
||||
// ========================================================================
|
||||
// Binding References (cached in BeginPlay)
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_HealthSystem> CachedHealthSystem;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_StressSystem> CachedStressSystem;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_StaminaSystem> CachedStaminaSystem;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_MovementStateSystem> CachedMovementSystem;
|
||||
};
|
||||
32
Source/PG_Framework/Public/Player/BPC_StressSystem.h
Normal file
32
Source/PG_Framework/Public/Player/BPC_StressSystem.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "BPC_StressSystem.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EStressTier : uint8
|
||||
{
|
||||
Calm UMETA(DisplayName = "Calm"),
|
||||
Tense UMETA(DisplayName = "Tense"),
|
||||
Distressed UMETA(DisplayName = "Distressed"),
|
||||
Panic UMETA(DisplayName = "Panic"),
|
||||
Catatonic UMETA(DisplayName = "Catatonic"),
|
||||
};
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStressTierChanged, EStressTier, NewTier);
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class PG_FRAMEWORK_API UBPC_StressSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_StressSystem();
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Stress")
|
||||
EStressTier StressTier = EStressTier::Calm;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnStressTierChanged OnStressTierChanged;
|
||||
};
|
||||
14
Source/PG_Framework/Public/Player/PC_CoreController.h
Normal file
14
Source/PG_Framework/Public/Player/PC_CoreController.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "PC_CoreController.generated.h"
|
||||
|
||||
UCLASS(Blueprintable)
|
||||
class PG_FRAMEWORK_API APC_CoreController : public APlayerController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APC_CoreController();
|
||||
};
|
||||
14
Source/PG_Framework/Public/Player/PS_CorePlayerState.h
Normal file
14
Source/PG_Framework/Public/Player/PS_CorePlayerState.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerState.h"
|
||||
#include "PS_CorePlayerState.generated.h"
|
||||
|
||||
UCLASS(Blueprintable)
|
||||
class PG_FRAMEWORK_API APS_CorePlayerState : public APlayerState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APS_CorePlayerState();
|
||||
};
|
||||
193
Source/PG_Framework/Public/Save/SS_SaveManager.h
Normal file
193
Source/PG_Framework/Public/Save/SS_SaveManager.h
Normal file
@@ -0,0 +1,193 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — SS_SaveManager (35)
|
||||
// Save/Load subsystem. Slot management, serialization, manifest tracking.
|
||||
// In C++, uses FArchive for direct binary serialization — far more powerful
|
||||
// than Blueprint "Save Game" / "Load Game" nodes.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "SS_SaveManager.generated.h"
|
||||
|
||||
/**
|
||||
* Save slot metadata returned by GetSlotManifest().
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FSaveSlotInfo
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
int32 SlotIndex = -1;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
FString SlotName;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
FString ChapterName;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
float PlayTimeHours = 0.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
FDateTime Timestamp;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
FGameplayTag LastCheckpoint;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
bool bIsEmpty = true;
|
||||
};
|
||||
|
||||
// Delegates
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSaveComplete, int32, SlotIndex, bool, bSuccess);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnLoadComplete, int32, SlotIndex, bool, bSuccess);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSaveManifestUpdated, const TArray<FSaveSlotInfo>&, Slots);
|
||||
|
||||
/**
|
||||
* SS_SaveManager — Save/Load Subsystem.
|
||||
*
|
||||
* Manages all save slots, serialization, and manifest tracking.
|
||||
* C++ gives us direct FArchive-based serialization, proper error handling,
|
||||
* and async save operations — impossible to match in Blueprint.
|
||||
*/
|
||||
UCLASS()
|
||||
class PG_FRAMEWORK_API USS_SaveManager : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
USS_SaveManager();
|
||||
|
||||
// ========================================================================
|
||||
// Lifecycle
|
||||
// ========================================================================
|
||||
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
// ========================================================================
|
||||
// Configuration
|
||||
// ========================================================================
|
||||
|
||||
/** Maximum number of save slots. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Save")
|
||||
int32 MaxSlots = 10;
|
||||
|
||||
/** Save game file prefix for slot naming. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Save")
|
||||
FString SavePrefix = TEXT("FrameworkSave_");
|
||||
|
||||
// ========================================================================
|
||||
// Slot Manifest
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Returns metadata for all save slots.
|
||||
* Fast — reads header only, not full save data.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Save")
|
||||
TArray<FSaveSlotInfo> GetSlotManifest() const;
|
||||
|
||||
/**
|
||||
* Checks if a slot has save data.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Save")
|
||||
bool DoesSlotExist(int32 SlotIndex) const;
|
||||
|
||||
// ========================================================================
|
||||
// Save / Load Operations
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Save game to a slot. Returns true if successful.
|
||||
* Server-authoritative in multiplayer.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Save")
|
||||
bool SaveGame(int32 SlotIndex, const FString& Description);
|
||||
|
||||
/**
|
||||
* Load game from a slot. Returns true if successful.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Save")
|
||||
bool LoadGame(int32 SlotIndex);
|
||||
|
||||
/**
|
||||
* Delete a save slot. Irreversible!
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Save")
|
||||
bool DeleteSlot(int32 SlotIndex);
|
||||
|
||||
/**
|
||||
* Quick-save to the current active slot.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Save")
|
||||
bool QuickSave();
|
||||
|
||||
/**
|
||||
* Quick-load from the current active slot.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Save")
|
||||
bool QuickLoad();
|
||||
|
||||
// ========================================================================
|
||||
// Checkpoint Management
|
||||
// ========================================================================
|
||||
|
||||
/** Loads the most recent checkpoint from a slot. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Save")
|
||||
bool LoadCheckpoint(int32 SlotIndex);
|
||||
|
||||
/**
|
||||
* Creates a checkpoint within the current slot.
|
||||
* Checkpoints are incremental saves within a single slot.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Save")
|
||||
bool CreateCheckpoint(FGameplayTag CheckpointTag);
|
||||
|
||||
// ========================================================================
|
||||
// Event Dispatchers
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnSaveComplete OnSaveComplete;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnLoadComplete OnLoadComplete;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnSaveManifestUpdated OnSaveManifestUpdated;
|
||||
|
||||
// ========================================================================
|
||||
// Utilities
|
||||
// ========================================================================
|
||||
|
||||
/** Returns the total disk space used by all saves (in bytes). */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Save")
|
||||
int64 GetTotalSaveSize() const;
|
||||
|
||||
/** Backs up all save slots to a Backup/ subdirectory. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Save")
|
||||
bool BackupAllSaves(const FString& BackupLabel);
|
||||
|
||||
protected:
|
||||
/** Builds the save slot name from prefix + index. */
|
||||
FString GetSlotName(int32 SlotIndex) const;
|
||||
|
||||
/** Reads only the header/metadata from a save file. */
|
||||
FSaveSlotInfo ReadSlotHeader(int32 SlotIndex) const;
|
||||
|
||||
/** Internal save implementation using FArchive serialization. */
|
||||
bool SaveToFile(int32 SlotIndex, const TArray<uint8>& Data, const FSaveSlotInfo& Meta);
|
||||
|
||||
/** Internal load implementation. */
|
||||
bool LoadFromFile(int32 SlotIndex, TArray<uint8>& OutData, FSaveSlotInfo& OutMeta);
|
||||
|
||||
/** Path to the save directory. */
|
||||
FString GetSaveDirectory() const;
|
||||
|
||||
/** Currently active save slot (from GI_GameFramework). */
|
||||
int32 GetActiveSlot() const;
|
||||
};
|
||||
34
Source/PG_Framework/Public/State/DA_StateGatingTable.h
Normal file
34
Source/PG_Framework/Public/State/DA_StateGatingTable.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "DA_StateGatingTable.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FStateGatingRule
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FGameplayTag ActionTag;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FGameplayTag BlockedByState;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
bool bIsBlocked = true;
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class PG_FRAMEWORK_API UDA_StateGatingTable : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gating")
|
||||
TArray<FStateGatingRule> GatingRules;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Gating")
|
||||
bool IsActionGated(FGameplayTag ActionTag, FGameplayTag CurrentState) const;
|
||||
};
|
||||
150
Source/PG_Framework/Public/Weapons/BPC_DamageReceptionSystem.h
Normal file
150
Source/PG_Framework/Public/Weapons/BPC_DamageReceptionSystem.h
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BPC_DamageReceptionSystem (72)
|
||||
// Damage reception, resistance calculation, and damage application.
|
||||
// Called potentially dozens of times per combat frame — C++ performance critical.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "BPC_DamageReceptionSystem.generated.h"
|
||||
|
||||
// Forward declarations
|
||||
class UDA_EquipmentConfig;
|
||||
class UBPC_HealthSystem;
|
||||
class UBPC_ShieldDefenseSystem;
|
||||
class UBPC_HitReactionSystem;
|
||||
|
||||
// Delegates
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FOnDamageReceived, float, RawDamage, float, FinalDamage,
|
||||
AActor*, DamageCauser, FGameplayTag, DamageType, FVector, HitLocation);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnDamageResisted, float, DamageResisted,
|
||||
FGameplayTag, ResistanceType, FString, Reason);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnStaggered, AActor*, StaggerCauser, float, StaggerForce);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnKnockedDown, AActor*, KnockdownCauser, float, KnockdownForce);
|
||||
|
||||
/**
|
||||
* Damage modifier for a specific damage type.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct PG_FRAMEWORK_API FDamageModifier
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** The damage type this modifier applies to. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FGameplayTag DamageType;
|
||||
|
||||
/** Multiplier applied to incoming damage of this type. 0.5 = half damage, 2.0 = double. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float Multiplier = 1.0f;
|
||||
|
||||
/** If true, this is a flat reduction (subtract after multiplier). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
bool bFlatReduction = false;
|
||||
|
||||
/** Flat damage reduction amount (only used if bFlatReduction is true). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float FlatReduction = 0.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* BPC_DamageReceptionSystem — Damage Reception & Resistance.
|
||||
*
|
||||
* Processes incoming damage: calculates resistance, applies armor/shield modifiers,
|
||||
* triggers hit reactions (stagger, knockdown), and routes final damage to the
|
||||
* health system. In C++, the damage pipeline is native-speed vectorized math.
|
||||
*/
|
||||
UCLASS(Blueprintable, ClassGroup = (Framework), meta = (BlueprintSpawnableComponent))
|
||||
class PG_FRAMEWORK_API UBPC_DamageReceptionSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_DamageReceptionSystem();
|
||||
|
||||
// ========================================================================
|
||||
// Configuration
|
||||
// ========================================================================
|
||||
|
||||
/** Equipment config for armor/damage modifiers. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Config")
|
||||
TObjectPtr<UDA_EquipmentConfig> EquipmentConfig;
|
||||
|
||||
/** Base damage resistance (0.0 = no resistance, 1.0 = immune). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Combat")
|
||||
float BaseResistance = 0.0f;
|
||||
|
||||
/** Damage multipliers per damage type. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Combat")
|
||||
TArray<FDamageModifier> DamageModifiers;
|
||||
|
||||
// ========================================================================
|
||||
// Damage Calculation — Hot Path
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Calculate and apply damage.
|
||||
* Full pipeline: raw damage → calculate resistance → apply armor → apply shield → apply health.
|
||||
* Returns actual damage dealt.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Combat")
|
||||
float ApplyDamage(float RawDamage, AActor* DamageCauser, FGameplayTag DamageType,
|
||||
FVector HitLocation, FVector HitDirection);
|
||||
|
||||
/**
|
||||
* Calculate effective resistance for a damage type.
|
||||
* Used by UI/preview systems to show expected damage.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Combat")
|
||||
float CalculateResistance(FGameplayTag DamageType) const;
|
||||
|
||||
/**
|
||||
* Get the damage modifier for a specific damage type.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Combat")
|
||||
float GetDamageMultiplier(FGameplayTag DamageType) const;
|
||||
|
||||
// ========================================================================
|
||||
// Hit Reaction
|
||||
// ========================================================================
|
||||
|
||||
/** Damage threshold to trigger a stagger reaction. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Combat|Reactions")
|
||||
float StaggerThreshold = 20.0f;
|
||||
|
||||
/** Damage threshold to trigger a knockdown. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Combat|Reactions")
|
||||
float KnockdownThreshold = 50.0f;
|
||||
|
||||
// ========================================================================
|
||||
// Event Dispatchers
|
||||
// ========================================================================
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnDamageReceived OnDamageReceived;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnDamageResisted OnDamageResisted;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnStaggered OnStaggered;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnKnockedDown OnKnockedDown;
|
||||
|
||||
protected:
|
||||
/** Triggers hit reaction based on final damage amount. */
|
||||
void EvaluateHitReaction(float FinalDamage, AActor* DamageCauser, FVector HitDirection);
|
||||
|
||||
/** Cached references to sibling components. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_HealthSystem> CachedHealthSystem;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_ShieldDefenseSystem> CachedShieldSystem;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_HitReactionSystem> CachedHitReactionSystem;
|
||||
};
|
||||
24
Source/PG_Framework/Public/Weapons/BPC_HitReactionSystem.h
Normal file
24
Source/PG_Framework/Public/Weapons/BPC_HitReactionSystem.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "BPC_HitReactionSystem.generated.h"
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class PG_FRAMEWORK_API UBPC_HitReactionSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_HitReactionSystem();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Combat")
|
||||
void PlayHitReaction(float DamageAmount, FVector HitDirection, AActor* DamageCauser);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|HitReaction")
|
||||
float FlinchThreshold = 5.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|HitReaction")
|
||||
float RagdollThreshold = 50.0f;
|
||||
};
|
||||
26
Source/PG_Framework/Public/Weapons/BPC_ShieldDefenseSystem.h
Normal file
26
Source/PG_Framework/Public/Weapons/BPC_ShieldDefenseSystem.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "BPC_ShieldDefenseSystem.generated.h"
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class PG_FRAMEWORK_API UBPC_ShieldDefenseSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_ShieldDefenseSystem();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Shield")
|
||||
float ShieldHealth = 100.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Shield")
|
||||
float MaxShieldHealth = 100.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Shield")
|
||||
float BlockAngle = 90.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Shield")
|
||||
bool bShieldBroken = false;
|
||||
};
|
||||
15
Source/PG_FrameworkEditor.Target.cs
Normal file
15
Source/PG_FrameworkEditor.Target.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// // Copyright Ngonart OU. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class PG_FrameworkEditorTarget : TargetRules
|
||||
{
|
||||
public PG_FrameworkEditorTarget(TargetInfo Target) : base(Target)
|
||||
{
|
||||
Type = TargetType.Editor;
|
||||
DefaultBuildSettings = BuildSettingsVersion.V6;
|
||||
|
||||
ExtraModuleNames.AddRange( new string[] { "PG_Framework" } );
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,14 +12,14 @@ This document catalogs all UE5 engine functions that are C++ only (not exposed t
|
||||
|
||||
**Engine Function:** `UGameplayTagsManager::Get().RequestAllGameplayTags()`
|
||||
**UE5 Node:** Does NOT exist in Blueprint
|
||||
**Files Affected:** `01_GI_GameTagRegistry.md`, any system that needs to enumerate all tags
|
||||
**Files Affected:** `01_DA_GameTagRegistry.md`, any system that needs to enumerate all tags
|
||||
|
||||
**Blueprint Workaround — Data Table Proxy:**
|
||||
**Blueprint Workaround — Data Table Proxy (Multi-Table):**
|
||||
|
||||
1. Create a Data Table: `DT_ProjectTags` with Row Structure = `GameplayTagTableRow`
|
||||
2. Populate it with all framework tags (mirrors `DefaultGameplayTags.ini`)
|
||||
3. Register in Project Settings → GameplayTags → Gameplay Tag Table List
|
||||
4. In Blueprint: use `Get Data Table Row Names` → `ForEachLoop` → `Get Data Table Row` → extract `Tag` field
|
||||
1. Create 11 per-category Data Tables with Row Structure = `GameplayTagTableRow` (see `docs/blueprints/01-core/data-tables/`)
|
||||
2. Populate each with its category's tags (mirrors `DefaultGameplayTags.ini`)
|
||||
3. Register ALL tables in `Project Settings → GameplayTags → Gameplay Tag Table List`
|
||||
4. In Blueprint: use outer `ForEachLoop` over `Array<Data Table>` + inner `ForEachLoop` over `Get Data Table Row Names` → `Get Data Table Row` → extract `Tag` field
|
||||
5. This provides a complete tag list without C++
|
||||
|
||||
**Trade-off:** Manual maintenance. Adding new tags requires updating both the `.ini` file AND the Data Table. Mitigation: use `ExportTagNamespace()` to audit for discrepancies.
|
||||
@@ -72,7 +72,7 @@ OR create a Blueprint Macro Library with a macro that wraps the subsystem lookup
|
||||
### 3.1 `OnAssetLoaded` / `BeginPlay` on Data Assets (Not Available)
|
||||
|
||||
**Engine Behavior:** `UPrimaryDataAsset` does not have `BeginPlay`, `Tick`, or event graphs. It's a pure data container.
|
||||
**Files Affected:** `01_GI_GameTagRegistry.md`, all DA_* specs
|
||||
**Files Affected:** `01_DA_GameTagRegistry.md`, all DA_* specs
|
||||
|
||||
**Blueprint Workaround:** Move initialization and validation logic to an owning system:
|
||||
|
||||
@@ -176,7 +176,7 @@ OR create a Blueprint Macro Library with a macro that wraps the subsystem lookup
|
||||
When writing or updating Blueprint spec files, follow these rules to avoid C++-only references:
|
||||
|
||||
1. **Never reference `UGameplayTagsManager::Get()`** — use `Get Tag Display Name`, `Is Gameplay Tag Valid`, `Make Literal Gameplay Tag`.
|
||||
2. **Never reference `Get All Gameplay Tags`** — use the Data Table proxy pattern (`DT_ProjectTags` → `Get Data Table Row Names`).
|
||||
2. **Never reference `Get All Gameplay Tags`** — use the multi-table Data Table proxy pattern (Array<Data Table> → nested `ForEachLoop` → `Get Data Table Row`).
|
||||
3. **Never use `FPrimaryAssetId` as a type name** — use `Primary Asset Id` (Blueprint type).
|
||||
4. **Never use `TSoftObjectPtr` as a type name** — use `Soft Object Reference` (Blueprint type).
|
||||
5. **Never put logic in Data Assets** — move initialization/validation to GameInstance, Subsystem, or Editor Utility.
|
||||
|
||||
@@ -452,7 +452,7 @@ HasActionFlag(Tag: GameplayTag) → Boolean
|
||||
4. Populate default `GatingRules` from `DA_StateGatingTable` if assigned
|
||||
5. Bind to `GI_GameFramework.OnGamePhaseChanged` (for game-phase-gated rules)
|
||||
6. Bind to `BPC_HealthSystem.OnDeath` (auto-call `ForceStateChange(Dead)`)
|
||||
7. Register with `GI_GameTagRegistry` for tag-based queries
|
||||
7. Register with `DA_GameTagRegistry` for tag-based queries
|
||||
8. Bind to `BPC_HealthSystem.OnHealthChanged` → call `EvaluateInjuryState()`
|
||||
9. Bind to `BPC_StressSystem.OnStressTierChanged` → recalculate heart rate
|
||||
10. Bind to `BPC_StaminaSystem.OnExhaustionStateChanged` → recalculate heart rate
|
||||
|
||||
@@ -88,7 +88,7 @@ This means UI widgets, audio, and effects need **zero changes** for multiplayer
|
||||
| `GI_GameFramework` | Server sets GamePhase; clients read | Dispatchers broadcast to all |
|
||||
| `GM_CoreGameMode` | Server-only; spawns players, routes death | Extends replicated GameMode |
|
||||
| `GS_CoreGameState` | Server sets all state | **Full replication** — 5 vars with OnRep |
|
||||
| `GI_GameTagRegistry` | Read-only on all | Identical on all clients (ini-based) |
|
||||
| `DA_GameTagRegistry` | Read-only on all | Identical on all clients (ini-based) |
|
||||
| `FL_GameUtilities` | Static; no state | No replication needed |
|
||||
| `I_InterfaceLibrary` | Contracts; no state | No replication needed |
|
||||
| `DA_ItemData` | Read-only config | Identical on all clients |
|
||||
|
||||
202
docs/architecture/planar-capture-system.md
Normal file
202
docs/architecture/planar-capture-system.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# Planar Capture System Architecture — UE5 Modular Game Framework
|
||||
|
||||
**Version:** 1.0 | **Systems:** 136-147 (12 systems) | **Phase:** 17 — Rendering & Visual
|
||||
|
||||
This document specifies the architecture of the Planar Capture System — a unified rendering pipeline for mirrors, portals, monitors, and horror surfaces. One capture pipeline, one quality manager, one material interface.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ BLUEPRINT LAYER (Designer-Facing) │
|
||||
│ BP_Mirror BP_HorrorMirror BP_Portal BP_Monitor BP_FakeWindow │
|
||||
│ DA_PlanarCaptureProfile MPC_CaptureSurface MI_Mirror_Clean/Dirty/Steam │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ C++ LAYER (Performance-Critical) │
|
||||
│ UBPC_PlanarCapture — SceneCapture2D lifecycle, camera math │
|
||||
│ ABP_PlanarCaptureActor — Surface mesh, MDI, overlap/proximity │
|
||||
│ USS_PlanarCaptureManager — Global budget, RT pool, scoring (WorldSubsystem + FTickable) │
|
||||
│ UPlanarCaptureCameraUtils — Mirror/Portal/Oblique math (static) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ UE5 ENGINE │
|
||||
│ USceneCaptureComponent2D UTextureRenderTarget2D UMaterialParameterColl │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Map (12 Systems — Files 136-147)
|
||||
|
||||
| # | System | Type | Parent Class | Purpose |
|
||||
|---|--------|------|-------------|---------|
|
||||
| 136 | `BPC_PlanarCapture` | BPC_ Component | `ActorComponent` | Core capture: SceneCapture2D lifecycle, camera math, RT management, actor lists, horror ring buffer |
|
||||
| 137 | `BP_PlanarCaptureActor` | BP_ Actor | `Actor` | Placeable surface actor: mesh, MDI, proximity trigger, manager registration |
|
||||
| 138 | `SS_PlanarCaptureManager` | SS_ Subsystem | `WorldSubsystem` | Global budget manager: surface scoring, quality tier assignment, RT pool |
|
||||
| 139 | `BP_Mirror` | BP_ Actor | `BP_PlanarCaptureActor` | Standard mirror: Mode=Mirror, dirt/steam/condensation layers, aging |
|
||||
| 140 | `BP_Portal` | BP_ Actor | `BP_PlanarCaptureActor` | Portal surface: Mode=Portal, linked target, teleport on overlap, clip plane |
|
||||
| 141 | `BP_Monitor` | BP_ Actor | `BP_PlanarCaptureActor` | Security screen/TV: Mode=Monitor, fixed camera ref, low update rate, scanlines |
|
||||
| 142 | `BP_HorrorMirror` | BP_ Actor | `BP_Mirror` | Horror mirror: wrong reflection actor, delayed frame, steam text reveal, scare events |
|
||||
| 143 | `BP_FakeWindow` | BP_ Actor | `BP_PlanarCaptureActor` | Architectural fake window: Mode=FakeWindow, linked sublevel, weather overlay |
|
||||
| 144 | `M_CaptureSurface_Master` | Material | `Material` | Master unlit material: RT sample, condensation, dirt, steam, horror layers |
|
||||
| 145 | `MPC_CaptureSurface` | MPC | `MaterialParameterCollection` | Global MPC: SteamIntensity, DirtOpacity, MirrorDarkness, WrongReflectionBlend, etc. (10 params) |
|
||||
| 146 | `DA_PlanarCaptureProfile` | DA_ Data Asset | `PrimaryDataAsset` | Per-surface capture config: default mode, quality profile overrides, actor lists |
|
||||
| 147 | `MI_Mirror_Clean/Dirty/Steam/Horror` | MI_ Instances | `M_CaptureSurface_Master` | Pre-configured material instances for common mirror states |
|
||||
|
||||
---
|
||||
|
||||
## Enums
|
||||
|
||||
### `EPlanarCaptureMode` (C++: defined in `PlanarCaptureCommon.h`)
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `Mirror` | Standard planar mirror reflection |
|
||||
| `Portal` | Portal with linked target surface |
|
||||
| `Monitor` | Fixed-camera security screen / TV |
|
||||
| `HorrorMirror` | Mirror with horror features (wrong reflection, delayed frame) |
|
||||
| `HorrorPortal` | Portal with horror features |
|
||||
| `FakeWindow` | Architectural fake window (parallax, weather) |
|
||||
|
||||
### `EPlanarCaptureQualityTier` (C++: defined in `PlanarCaptureCommon.h`)
|
||||
| Value | RT Size | FPS | Shadows | Lumen | Post | Clip Plane |
|
||||
|-------|---------|-----|---------|-------|------|-----------|
|
||||
| `Off` | — | 0 | — | — | — | — |
|
||||
| `Low` | 256 | 4 | No | No | No | No |
|
||||
| `Medium` | 512 | 15 | Dynamic | Optional | No | Yes |
|
||||
| `High` | 1024 | 30 | Full | Yes | Minimal | Yes |
|
||||
| `Hero` | 2048 | 60 | Full | Yes | Full | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Communication Matrix
|
||||
|
||||
| Source | Target | Method | What |
|
||||
|--------|--------|--------|------|
|
||||
| `BP_PlanarCaptureActor` | `SS_PlanarCaptureManager` | Direct (RegisterSurface) | Registers on BeginPlay |
|
||||
| `SS_PlanarCaptureManager` | `BPC_PlanarCapture` | Direct (ApplyQualityTier) | Assigns quality tier |
|
||||
| `BPC_PlanarCapture` | `USceneCaptureComponent2D` | Direct (owns) | Camera math + CaptureScene |
|
||||
| `BPC_PlanarCapture` | `MPC_CaptureSurface` | Direct (SetScalarParameter) | Pushes steam/dirt/horror params |
|
||||
| `BP_HorrorMirror` | `BPC_ScareEventSystem` (101) | Interface / Dispatcher | Triggers coordinated scares |
|
||||
| `BP_HorrorMirror` | `SS_AudioManager` (132) | Direct | Plays mirror horror SFX |
|
||||
| `BPC_PlanarCapture` | `BPC_StateManager` (130) | IsActionPermitted() | Checks if capture can activate |
|
||||
| `BP_Portal` | Player Pawn | Overlap Event | Teleports player through portal |
|
||||
|
||||
---
|
||||
|
||||
## Quality Scoring Algorithm
|
||||
|
||||
```
|
||||
CompositeScore = (ScreenCoverage × 0.5) + (FacingAngle × 0.3) + (DistanceFactor × 0.1) + (ScriptedPriority × 0.1)
|
||||
|
||||
Tier assignment:
|
||||
Score ≥ 0.8 → Hero (capped at GlobalQualityCap, max MaxHeroSurfaces)
|
||||
Score ≥ 0.5 → High (capped at max MaxHighSurfaces)
|
||||
Score ≥ 0.2 → Medium (capped at max MaxMediumSurfaces)
|
||||
Score > 0 → Low
|
||||
Score = 0 or !inFrustum → Off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Material Layer Stack (M_CaptureSurface_Master)
|
||||
|
||||
```
|
||||
Layer 0: Render target sample (UV-flipped for mirror, straight for portal/monitor)
|
||||
Layer 1: Condensation normal map → distorts UV of RT sample
|
||||
Layer 2: Fresnel edge fade mask
|
||||
Layer 3: Dirt/scratch multiply (DirtOpacity MPC param)
|
||||
Layer 4: Steam/fog lerp (animated noise, SteamIntensity MPC param)
|
||||
Layer 5: Steam emissive glow (backlit fog, SteamEmissiveIntensity)
|
||||
Layer 6: Text reveal mask lerp (TextRevealProgress, uses steam as carrier)
|
||||
Layer 7: Mirror darkness multiply (MirrorDarkness MPC param)
|
||||
Layer 8: Wrong reflection crossfade (WrongReflectionBlend MPC param)
|
||||
Output: Unlit shading model
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Horror Features
|
||||
|
||||
| Feature | Driven By | Layer |
|
||||
|---------|-----------|-------|
|
||||
| Mirror-only ghost actor | ShowOnly list in C++ | C++ |
|
||||
| Wrong reflection Metahuman swap | ActivateHorrorReflection() | C++ |
|
||||
| Delayed frame reflection | FrameRingBuffer + DelayedReflectionBlend MPC | C++ + Material |
|
||||
| Steam fogging | SteamIntensity MPC → Layer 4 | Material |
|
||||
| Dirt / scratches | DirtOpacity MPC → Layer 3 | Material |
|
||||
| Condensation rivulets | CondensationFlow MPC → Layer 1 | Material |
|
||||
| UV distortion / breathing | DistortionAmplitude MPC → Layer 1 | Material |
|
||||
| Text in steam reveal | TextRevealProgress MPC → Layer 6 | Material |
|
||||
| Mirror goes dark | MirrorDarkness MPC → Layer 7 | Material |
|
||||
| Wrong reflection crossfade | WrongReflectionBlend MPC → Layer 8 | Material |
|
||||
| Surface aging/oxidation | SurfaceAge MPC → Layer 3 (tint) | Material |
|
||||
|
||||
---
|
||||
|
||||
## Blueprint Limitations & Workarounds
|
||||
|
||||
### `USceneCaptureComponent2D::CaptureScene()` (C++ Only)
|
||||
**Workaround:** Call from C++ in `UBPC_PlanarCapture`. Blueprint calls `CaptureNow()` which invokes C++ capture.
|
||||
**Affected Files:** 136, 139-143
|
||||
|
||||
### `FSceneView::ViewMatrices` for Oblique Projection (C++ Only)
|
||||
**Workaround:** Use `UPlanarCaptureCameraUtils::ComputeObliqueProjectionMatrix()` (BlueprintCallable).
|
||||
**Affected Files:** 136, 140
|
||||
|
||||
### `USceneCaptureComponent2D::ShowOnlyActors` Runtime Modification (Partial C++)
|
||||
**Workaround:** `UBPC_PlanarCapture::UpdateActorLists()` resolves TSoftObjectPtrs into the array.
|
||||
**Affected Files:** 136, 142
|
||||
|
||||
---
|
||||
|
||||
## Performance Budget Guidelines
|
||||
|
||||
| Tier | Max Simultaneous | RT Memory per Surface | GPU Cost |
|
||||
|------|-----------------|----------------------|----------|
|
||||
| Hero | 1 | 16 MB (2048² × 4B) | Very High |
|
||||
| High | 3 | 4 MB (1024² × 4B) | High |
|
||||
| Medium | 6 | 1 MB (512² × 4B) | Medium |
|
||||
| Low | Unlimited | 256 KB (256² × 4B) | Low |
|
||||
| Off | — | 0 | None |
|
||||
|
||||
**Total Budget:** 128 MB default (configurable via `SS_PlanarCaptureManager.MaxTotalRenderTargetMemoryMB`)
|
||||
|
||||
---
|
||||
|
||||
## Multiplayer Considerations
|
||||
|
||||
- Capture rendering is **local-only** — each client renders their own view
|
||||
- `BP_PlanarCaptureActor` replicates `bIsActive` for server-authoritative surface control
|
||||
- Horror events (wrong reflection activation) are triggered server-side and replicated via gameplay tags
|
||||
- No client prediction needed — captures are cosmetic
|
||||
|
||||
---
|
||||
|
||||
## Integration Points with Existing Systems
|
||||
|
||||
| Existing System | Integration |
|
||||
|----------------|-------------|
|
||||
| `BPC_StateManager` (130) | Query `IsActionPermitted()` before enabling capture, teleport gating |
|
||||
| `BPC_ScareEventSystem` (101) | Horror mirror events trigger coordinated scares |
|
||||
| `SS_AudioManager` (132) | Mirror/portal SFX route through audio subsystem |
|
||||
| `SS_EnhancedInputManager` (128) | Input context switch for portal inspection mode |
|
||||
| `BPC_DiegeticDisplay` (18) | Monitors can display diegetic UI render targets |
|
||||
| `BPC_CameraStateLayer` (14) | Camera FOV adjustments during portal transitions |
|
||||
| `I_Persistable` (36) | Mirror surface state (destroyed, dirty, oxidized) saves/loads |
|
||||
| `DA_ScareEvent` (127) | Mirror apparition scare event data asset |
|
||||
| `WBP_SettingsMenu` (57) | Capture surface quality settings (Performance section) |
|
||||
|
||||
---
|
||||
|
||||
## Build Order (Phase 17)
|
||||
|
||||
1. **Phase 17a:** C++ Core — `PlanarCaptureCommon.h`, `PlanarCaptureCameraUtils`, `BPC_PlanarCapture`, `SS_PlanarCaptureManager`, `BP_PlanarCaptureActor`
|
||||
2. **Phase 17b:** Material Foundation — `M_CaptureSurface_Master`, `MPC_CaptureSurface`, `MI_Mirror_*` instances
|
||||
3. **Phase 17c:** Blueprint Actors — `BP_Mirror`, `BP_Portal`, `BP_Monitor`, `BP_HorrorMirror`, `BP_FakeWindow`
|
||||
4. **Phase 17d:** Data Assets — `DA_PlanarCaptureProfile`
|
||||
5. **Phase 17e:** Integration — Wire horror events to `BPC_ScareEventSystem`, audio to `SS_AudioManager`, persistence to `I_Persistable`
|
||||
|
||||
---
|
||||
|
||||
*Planar Capture System Architecture v1.0 — See `docs/blueprints/17-capture/` for per-system blueprint specs.*
|
||||
@@ -1,113 +0,0 @@
|
||||
# Audit Request: Cross-Reference Master Document vs Existing Blueprint Spec Files
|
||||
|
||||
## Goal
|
||||
Systematically map every system defined in `UE5_Modular_Game_Framework.md` (3703 lines, ~131 systems across 13 sections) against the 83 existing blueprint spec files in `docs/blueprints/` to identify:
|
||||
1. Which systems have corresponding blueprint spec files (Present)
|
||||
2. Which systems are missing (Missing)
|
||||
3. Which existing files have no clear corresponding master-document system (Orphaned / Renamed)
|
||||
|
||||
## Input Files
|
||||
|
||||
### Master Document
|
||||
- `UE5_Modular_Game_Framework.md` (in workspace root)
|
||||
|
||||
### Existing Files (83 total)
|
||||
Located under `docs/blueprints/` with the following structure:
|
||||
```
|
||||
docs/blueprints/
|
||||
TEMPLATE.md
|
||||
01-core/ (files 01-07)
|
||||
02-player/ (files 08-15)
|
||||
03-interaction/ (files 16-21)
|
||||
04-inventory/ (files 22-27)
|
||||
05-saveload/ (files 28-31)
|
||||
06-ui/ (files 32-37)
|
||||
07-narrative/ (files 38-48)
|
||||
08-weapons/ (files 49-54)
|
||||
09-ai/ (files 55-61)
|
||||
10-adaptive/ (files 62-70)
|
||||
11-polish/ (files 71-83)
|
||||
```
|
||||
|
||||
## Master Document Section Structure
|
||||
|
||||
| Section | Title | Table Count | Lines |
|
||||
|---------|-------|-------------|-------|
|
||||
| 1 | Core Framework Systems | 8 | 154-537 |
|
||||
| 2 | Player State & Embodiment | 9 | 540-939 |
|
||||
| 3 | Interaction & World Manipulation | 11 | 941-1305 |
|
||||
| 4 | Inventory, Items & Collectibles | 12 | 1307-1643 |
|
||||
| 5 | Weapon, Equipment & Damage | 10 | 1645-1892 |
|
||||
| 6 | UI, Menus & Diegetic Presentation | 13 | 1894-2143 |
|
||||
| 7 | Narrative, Dialogue, Objective & Choice | 11 | 2146-2467 |
|
||||
| 8 | Save, Load, Persistence & Death Loop | 11 | 2469-2691 |
|
||||
| 9 | Adaptive Environment, Atmosphere & Scare | 9 | 2693-2935 |
|
||||
| 10 | AI, Perception & Encounters | 9 | 2937-3109 |
|
||||
| 11 | Achievements, Progression & Meta | 10 | 3112-3286 |
|
||||
| 12 | Settings, Accessibility & Platform | 9 | 3288-3389 |
|
||||
| 13 | Editor / Data / Content Authoring | 9 | 3391-3498 |
|
||||
|
||||
## Existing Files Reference (83 files)
|
||||
|
||||
### 01-core/ (7 files)
|
||||
01_GI_GameTagRegistry.md, 02_FL_GameUtilities.md, 03_I_InterfaceLibrary.md, 04_GI_GameFramework.md, 05_GM_CoreGameMode.md, 06_GS_CoreGameState.md, 07_DA_ItemData.md
|
||||
|
||||
### 02-player/ (8 files)
|
||||
08_BPC_HealthSystem.md, 09_BPC_StaminaSystem.md, 10_BPC_StressSystem.md, 11_BPC_MovementStateSystem.md, 12_BPC_HidingSystem.md, 13_BPC_EmbodimentSystem.md, 14_BPC_CameraStateLayer.md, 15_BPC_PlayerMetricsTracker.md
|
||||
|
||||
### 03-interaction/ (6 files)
|
||||
16_BPC_InteractionDetector.md, 17_BPC_PickupComponent.md, 18_I_HidingSpot.md, 19_BPC_LeverPuzzleComponent.md, 20_BPC_InteractableDoorComponent.md, 21_BPC_DiegeticDisplay.md
|
||||
|
||||
### 04-inventory/ (6 files)
|
||||
22_BPC_InventoryComponent.md, 23_BPC_InventoryWeightSystem.md, 24_BPC_InventoryQuickSlot.md, 25_BPC_EquipmentSystem.md, 26_BPC_ContainerInventory.md, 27_BP_ItemPickup.md
|
||||
|
||||
### 05-saveload/ (4 files)
|
||||
28_SS_SaveManager.md, 29_I_Persistable.md, 30_BPC_CheckpointSystem.md, 31_BPC_DeathHandlingSystem.md
|
||||
|
||||
### 06-ui/ (6 files)
|
||||
32_SS_UIManager.md, 33_WBP_HUD.md, 34_WBP_InventoryUI.md, 35_WBP_InteractionUI.md, 36_WBP_MenuWidgets.md, 37_WBP_AccessibilityUI.md
|
||||
|
||||
### 07-narrative/ (11 files)
|
||||
38_BPC_NarrativeStateSystem.md, 39_BPC_ObjectiveSystem.md, 40_BPC_DialoguePlaybackSystem.md, 41_BPC_DialogueChoiceSystem.md, 42_BPC_BranchingConsequenceSystem.md, 43_BPC_TrialScenarioSystem.md, 44_BPC_CutsceneBridge.md, 45_BPC_EndingAccumulatorSystem.md, 46_BPC_LoreUnlockSystem.md, 47_BPC_NarrativeTriggerVolume.md, 48_DA_NarrativeDataAssets.md
|
||||
|
||||
### 08-weapons/ (6 files)
|
||||
49_BP_WeaponBase.md, 50_BP_RangedWeapon.md, 51_BP_MeleeWeapon.md, 52_BPC_AmmoComponent.md, 53_BPC_DamageHandlerComponent.md, 54_BPC_CombatFeedbackComponent.md
|
||||
|
||||
### 09-ai/ (7 files)
|
||||
55_BPC_AIControllerBase.md, 56_BPC_PerceptionComponent.md, 57_BPC_BehaviorTreeManager.md, 58_BP_EnemyBase.md, 59_BP_PatrolPath.md, 60_BPC_AlertSystem.md, 61_BPC_AIStateMachine.md
|
||||
|
||||
### 10-adaptive/ (9 files)
|
||||
62_BPC_DifficultyManager.md, 63_BPC_FearSystem.md, 64_BPC_AtmosphereController.md, 65_BPC_LightingManager.md, 66_BPC_AudioManager.md, 67_BPC_VFXManager.md, 68_BP_DynamicEvent.md, 69_BPC_PerformanceScaler.md, 70_BPC_ProceduralEncounter.md
|
||||
|
||||
### 11-polish/ (13 files)
|
||||
71_SS_SettingsManager.md, 72_BPC_AccessibilitySettings.md, 73_BPC_TutorialSystem.md, 74_BPC_AchievementManager.md, 75_BPC_StatsTracker.md, 76_BPC_LoadingScreen.md, 77_WBP_CreditsScreen.md, 78_WBP_SplashScreen.md, 79_BPC_FPSCounter.md, 80_BPC_DevCheatManager.md, 81_WBP_DebugMenu.md, 82_BPC_AnalyticsTracker.md, 83_BPC_ErrorHandler.md
|
||||
|
||||
## Methodology
|
||||
|
||||
Read each section of the master document and extract:
|
||||
- Every named system (by its prefix + name, e.g., `BPC_HealthSystem`, `BP_DoorActor`, `SS_SaveSystem`, `WBP_HUDController`)
|
||||
- Cross-reference against the 83 existing files by matching:
|
||||
- Exact name match (e.g., `BPC_HealthSystem` → `08_BPC_HealthSystem.md`)
|
||||
- Semantic match (e.g., `SS_SaveSystem` in master doc → `28_SS_SaveManager.md` on disk)
|
||||
- Bundled match (e.g., `WBP_MainMenu`, `WBP_PauseMenu`, `WBP_SettingsMenu` in master doc → `36_WBP_MenuWidgets.md` on disk)
|
||||
|
||||
## Output Format
|
||||
|
||||
Produce a Markdown table for each section of the master document with columns:
|
||||
| Master Doc System | Corresponding File | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| `BPC_HealthSystem` | `08_BPC_HealthSystem.md` | ExactMatch | Same name |
|
||||
| `SS_SaveSystem` | `28_SS_SaveManager.md` | RenamedMatch | Master calls it SaveSystem, file calls it SaveManager |
|
||||
| `BPC_InteractionExecutor` | (none) | Missing | No corresponding file |
|
||||
|
||||
Also flag files that exist on disk but have no obvious match in the master document (orphaned).
|
||||
|
||||
## Final Summary
|
||||
At the end, provide:
|
||||
1. Count of ExactMatch systems
|
||||
2. Count of RenamedMatch systems
|
||||
3. Count of Missing systems
|
||||
4. Count of Orphaned files
|
||||
5. Total systems in master doc
|
||||
6. Total files on disk
|
||||
7. List of all missing system names organized by their source section
|
||||
355
docs/blueprints/00-project-setup/GI_StarterGameInstance.md
Normal file
355
docs/blueprints/00-project-setup/GI_StarterGameInstance.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# GI_StarterGameInstance — Blueprint Specification
|
||||
|
||||
> **Asset Type:** Game Instance (derives from `UGameInstance`)
|
||||
> **UE Version:** 5.5–5.7
|
||||
> **Category:** 00-Project-Setup / Starter
|
||||
> **Build Phase:** Pre-Phase 0 — Project Initialization
|
||||
> **Dependencies:** `DA_GameTagRegistry` (01), 11 per-category Data Tables, GameplayTags plugin
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
A minimal, ready-to-use GameInstance Blueprint that serves as the project's **immediate** entry point. Set this as your `Game Instance Class` in Project Settings to get tag validation and framework bootstrapping working on day one. It loads `DA_GameTagRegistry`, validates that all 11 Data Tables are registered, and broadcasts `OnFrameworkReady` when initialization completes.
|
||||
|
||||
**This is intentionally simple.** As the project matures, replace it with `GI_GameFramework` (04) which adds full game-phase management, subsystem ownership, platform init, and save-slot orchestration. All systems that bind to `OnFrameworkReady` will continue working with either GameInstance.
|
||||
|
||||
---
|
||||
|
||||
## 2. Class Settings
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Parent Class** | `GameInstance` |
|
||||
| **Blueprint Type** | Game Instance |
|
||||
| **Asset Path** | `/Game/Framework/Core/GI_StarterGameInstance` |
|
||||
| **Is Abstract** | No |
|
||||
|
||||
**UE5 Setup:** `Project Settings → Maps & Modes → Game Instance Class` → `GI_StarterGameInstance`
|
||||
|
||||
---
|
||||
|
||||
## 3. Enums
|
||||
|
||||
None. No custom enums are defined in this system.
|
||||
|
||||
---
|
||||
|
||||
## 4. Structs
|
||||
|
||||
None. No custom structs are defined in this system.
|
||||
|
||||
---
|
||||
|
||||
## 5. Variables
|
||||
|
||||
### Configuration (Instance Editable)
|
||||
|
||||
| Name | Type | Default | Category | Description |
|
||||
|------|------|---------|----------|-------------|
|
||||
| `TagRegistry` | `DA_GameTagRegistry` (Object Reference) | *Assigned in Class Defaults* | Config | Hard reference to the `DA_GameTagRegistry` Data Asset |
|
||||
| `bValidateTagsOnInit` | `Boolean` | `true` | Config | If true, calls `DA_GameTagRegistry.GetAllRegisteredTags()` during `Event Init` and logs tag count |
|
||||
| `bLogTagsOnInit` | `Boolean` | `false` | Debug | If true, calls `DA_GameTagRegistry.LogAllTags()` after validation (Editor-only; heavy logging) |
|
||||
|
||||
### Internal (Private)
|
||||
|
||||
| Name | Type | Default | Category | Description |
|
||||
|------|------|---------|----------|-------------|
|
||||
| `bFrameworkInitialized` | `Boolean` | `false` | State | Set to `true` after `Event Init` completes successfully |
|
||||
|
||||
---
|
||||
|
||||
## 6. Functions
|
||||
|
||||
### Public Functions
|
||||
|
||||
#### `GetTagRegistry()` → `DA_GameTagRegistry` *(Blueprint Pure)*
|
||||
|
||||
- **Description:** Returns the cached `TagRegistry` reference. Returns `None` if not yet loaded.
|
||||
- **Parameters:** None
|
||||
- **Flow:**
|
||||
1. Return `TagRegistry` variable
|
||||
|
||||
#### `IsFrameworkReady()` → `Boolean` *(Blueprint Pure)*
|
||||
|
||||
- **Description:** Returns whether the framework has completed initialization. Systems should check this before querying tag-dependent services.
|
||||
- **Parameters:** None
|
||||
- **Flow:**
|
||||
1. Return `bFrameworkInitialized`
|
||||
|
||||
### Protected / Private Functions
|
||||
|
||||
#### `ValidateFrameworkTags()` *(Blueprint Callable, Private)*
|
||||
|
||||
- **Description:** Loads the tag registry, counts registered tags, and logs warnings if zero tags are found. Called automatically during `Event Init`.
|
||||
- **Parameters:** None
|
||||
- **Flow:**
|
||||
1. `IsValid(TagRegistry)`? → False: Print Error "TagRegistry not assigned!" → Return
|
||||
2. Call `TagRegistry.GetAllRegisteredTags()` → `AllTags` (Array\<GameplayTag\>)
|
||||
3. `Array Length(AllTags) == 0`?
|
||||
- True: Print Warning "No Gameplay Tags registered! Check Project Settings → GameplayTags → Gameplay Tag Table List. All 11 Data Tables must be added."
|
||||
- False: Print String "DA_GameTagRegistry initialized: {Array Length} tags registered across 11 Data Tables."
|
||||
4. If `bLogTagsOnInit`: Call `TagRegistry.LogAllTags()`
|
||||
|
||||
---
|
||||
|
||||
## 7. Event Dispatchers
|
||||
|
||||
| Dispatcher | Parameters | Bind Access | Description |
|
||||
|------------|-----------|-------------|-------------|
|
||||
| `OnFrameworkReady` | — | Public | Fires after `Event Init` completes and tag validation passes. Other systems bind to this to defer initialization until the framework is ready. |
|
||||
| `OnFrameworkInitFailed` | `ErrorReason: String` | Public | Fires if `TagRegistry` is invalid or zero tags are found during init. Systems should handle gracefully — show error UI, disable gameplay. |
|
||||
|
||||
---
|
||||
|
||||
## 8. Overridden Events
|
||||
|
||||
### Event: `Event Init`
|
||||
|
||||
- **Description:** UE5's GameInstance initialization event. Fires once when the game starts, before any level loads. This is where framework bootstrapping happens.
|
||||
- **Flow:**
|
||||
1. Call `Parent: Event Init` (important — don't skip)
|
||||
2. Print String: "GI_StarterGameInstance: Init started"
|
||||
3. `Branch: bValidateTagsOnInit?`
|
||||
- False → Print String "Tag validation skipped (bValidateTagsOnInit = false)"
|
||||
- True → Call `ValidateFrameworkTags()`
|
||||
4. `Branch: IsValid(TagRegistry)?`
|
||||
- False → Print Error "DA_GameTagRegistry reference is invalid!" → Call `OnFrameworkInitFailed("TagRegistry not assigned or invalid")` → Return
|
||||
- True → Continue
|
||||
5. Set `bFrameworkInitialized = true`
|
||||
6. Call `OnFrameworkReady` (broadcast to all bound listeners)
|
||||
7. Print String: "GI_StarterGameInstance: Init complete — OnFrameworkReady broadcast"
|
||||
|
||||
---
|
||||
|
||||
## 9. Blueprint Graph Logic Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Event Init] --> B[Call Parent: Event Init]
|
||||
B --> C{ bValidateTagsOnInit? }
|
||||
C -->|True| D[Call ValidateFrameworkTags]
|
||||
C -->|False| E[Skip validation]
|
||||
D --> F{ IsValid TagRegistry? }
|
||||
F -->|False| G[Print Error<br>Broadcast OnFrameworkInitFailed]
|
||||
F -->|True| H[Set bFrameworkInitialized = true]
|
||||
H --> I[Broadcast OnFrameworkReady]
|
||||
I --> J[Print 'Init Complete']
|
||||
```
|
||||
|
||||
### ValidateFrameworkTags Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[ValidateFrameworkTags] --> B{ IsValid TagRegistry? }
|
||||
B -->|False| C[Print Error: TagRegistry not assigned]
|
||||
B -->|True| D[Call GetAllRegisteredTags → AllTags]
|
||||
D --> E{ Array Length == 0? }
|
||||
E -->|True| F[Print Warning: No tags registered]
|
||||
E -->|False| G[Print: N tags registered]
|
||||
G --> H{ bLogTagsOnInit? }
|
||||
H -->|True| I[Call LogAllTags]
|
||||
H -->|False| J[Return]
|
||||
I --> J
|
||||
F --> J
|
||||
C --> J
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Communication Matrix
|
||||
|
||||
| Source | Target | Method | Direction | Data |
|
||||
|--------|--------|--------|-----------|------|
|
||||
| `GI_StarterGameInstance` | `DA_GameTagRegistry` | Direct function call | Outbound | Calls `GetAllRegisteredTags()`, `LogAllTags()`, `ValidateTag()` |
|
||||
| `GI_StarterGameInstance` | All bound systems | `OnFrameworkReady` dispatcher | Outbound broadcast | No data — listeners query `IsFrameworkReady()` |
|
||||
| `GI_StarterGameInstance` | All bound systems | `OnFrameworkInitFailed` dispatcher | Outbound broadcast | `ErrorReason: String` |
|
||||
| Any system | `GI_StarterGameInstance` | `GetGameInstance()` → Cast | Inbound query | Systems call `IsFrameworkReady()` before initialization |
|
||||
|
||||
---
|
||||
|
||||
## 11. Validation Checklist
|
||||
|
||||
- [ ] `GI_StarterGameInstance` set as `Game Instance Class` in `Project Settings → Maps & Modes`
|
||||
- [ ] `TagRegistry` variable assigned to `DA_GameTagRegistry` Data Asset in Class Defaults
|
||||
- [ ] All 11 Data Tables registered in `Project Settings → GameplayTags → Gameplay Tag Table List`
|
||||
- [ ] On PIE (Play In Editor): output log shows "N tags registered across 11 Data Tables"
|
||||
- [ ] On PIE with `bLogTagsOnInit = true`: all tag names printed to output log
|
||||
- [ ] If `TagRegistry` is unassigned: error prints and `OnFrameworkInitFailed` fires (test by clearing the variable)
|
||||
- [ ] Other systems successfully bind to `OnFrameworkReady` and defer their init
|
||||
- [ ] Edge case: 0 tags in Data Tables → warning prints, `OnFrameworkInitFailed` does NOT fire (empty tables are a warning, not a failure)
|
||||
|
||||
---
|
||||
|
||||
## 12. Reuse Notes
|
||||
|
||||
- **Replacement path:** When you are ready for the full `GI_GameFramework` (04), simply change the `Game Instance Class` in Project Settings. All systems that bind to `OnFrameworkReady` on either GameInstance will work — `GI_GameFramework` includes the same dispatcher.
|
||||
- **Project-specific init:** Add custom initialization (achievement platform, analytics, save migration) after the `OnFrameworkReady` broadcast in `Event Init`.
|
||||
- **Tag registry is a Data Asset, not a subsystem:** You must manually assign the `TagRegistry` reference in Class Defaults. It is NOT auto-discovered.
|
||||
- **Multi-platform:** On consoles, `Event Init` fires before any level loads — same as PC. No platform-specific code is needed.
|
||||
|
||||
---
|
||||
|
||||
## 13. Manual Implementation Guide
|
||||
|
||||
> **For human implementer:** Follow these steps to build `GI_StarterGameInstance` in UE5 Blueprints. This is the FIRST Blueprint you should create — it enables tag validation for all other systems.
|
||||
|
||||
### 13.1 Class Setup
|
||||
|
||||
1. Right-click in Content Browser → **Blueprint Class**
|
||||
2. Search for parent class: `GameInstance`
|
||||
3. Name: `GI_StarterGameInstance`
|
||||
4. Save to: `Content/Framework/Core/`
|
||||
5. Open **Project Settings → Maps & Modes** → Set `Game Instance Class` to `GI_StarterGameInstance`
|
||||
|
||||
### 13.2 Variables
|
||||
|
||||
Add to Class Defaults:
|
||||
|
||||
| Variable | Type | Instance Editable | Default | Category |
|
||||
|----------|------|------------------|---------|----------|
|
||||
| `TagRegistry` | `DA_GameTagRegistry` (Object Reference) | ✓ | *Select `DA_GameTagRegistry` asset* | Config |
|
||||
| `bValidateTagsOnInit` | `Boolean` | ✓ | `true` | Config |
|
||||
| `bLogTagsOnInit` | `Boolean` | ✓ | `false` | Debug |
|
||||
| `bFrameworkInitialized` | `Boolean` | ✗ (Private) | `false` | State |
|
||||
|
||||
**⚠️ Important:** For `TagRegistry`, use the type `DA_GameTagRegistry` (your specific Data Asset Blueprint class, NOT the generic `PrimaryDataAsset`). Compile `DA_GameTagRegistry` first so it appears in the type dropdown.
|
||||
|
||||
### 13.3 Event Dispatchers
|
||||
|
||||
Create two Event Dispatchers in the **My Blueprint** panel:
|
||||
|
||||
1. `OnFrameworkReady` — no parameters
|
||||
2. `OnFrameworkInitFailed` — add one input parameter:
|
||||
- `ErrorReason` (String)
|
||||
|
||||
### 13.4 Override Event Init
|
||||
|
||||
1. In the **My Blueprint** panel, hover over **Functions** → click **Override** → select `Event Init`
|
||||
2. Build the following graph:
|
||||
|
||||
```
|
||||
[Event Init]
|
||||
Step 1: Call "Parent: Event Init" (right-click event node → "Add Call to Parent")
|
||||
Step 2: Print String: "GI_StarterGameInstance: Init started"
|
||||
→ Text (Format Text): "GI_StarterGameInstance: Init started"
|
||||
Step 3: Branch (Condition: bValidateTagsOnInit)
|
||||
True → Call ValidateFrameworkTags (see 13.5)
|
||||
False → Print String: "Tag validation skipped (bValidateTagsOnInit = false)"
|
||||
Step 4: IsValid(TagRegistry) → Branch
|
||||
False:
|
||||
→ Print String: "DA_GameTagRegistry reference is invalid!"
|
||||
→ Call OnFrameworkInitFailed (ErrorReason = "TagRegistry not assigned or invalid")
|
||||
→ Return Node
|
||||
True: Continue
|
||||
Step 5: Set bFrameworkInitialized = true
|
||||
Step 6: Call OnFrameworkReady (no params)
|
||||
Step 7: Print String: "GI_StarterGameInstance: Init complete — OnFrameworkReady broadcast"
|
||||
```
|
||||
|
||||
**Nodes to Search:** `Event Init`, `Add Call to Parent`, `Print String`, `Format Text`, `Branch`, `IsValid`, `Set`, `Call OnFrameworkReady`, `Call OnFrameworkInitFailed`
|
||||
|
||||
### 13.5 Implement ValidateFrameworkTags
|
||||
|
||||
Create a new function: **BlueprintCallable**, Private, named `ValidateFrameworkTags`.
|
||||
|
||||
```
|
||||
[Function: ValidateFrameworkTags] (BlueprintCallable, Private)
|
||||
Step 1: IsValid(TagRegistry) → Branch
|
||||
False: Print String "TagRegistry not assigned in GI_StarterGameInstance!" → Return
|
||||
True: Continue
|
||||
Step 2: Call TagRegistry → GetAllRegisteredTags() → store in "AllTags" (Array<GameplayTag>)
|
||||
Step 3: Array Length(AllTags) → store in "TagCount" (Integer)
|
||||
Step 4: Branch (Condition: TagCount == 0)
|
||||
True: Print String (Color: Yellow): "WARNING: No Gameplay Tags registered! Check Project Settings → GameplayTags → Gameplay Tag Table List. All 11 Data Tables must be added."
|
||||
False: Print String (Format Text): "DA_GameTagRegistry initialized: {TagCount} tags registered across 11 Data Tables."
|
||||
Step 5: Branch (Condition: bLogTagsOnInit)
|
||||
True: Call TagRegistry → LogAllTags()
|
||||
False: Continue (no action)
|
||||
Step 6: Return
|
||||
```
|
||||
|
||||
**Nodes to Search:** `IsValid`, `GetAllRegisteredTags`, `Array Length`, `Branch`, `Print String`, `Format Text`, `LogAllTags`
|
||||
|
||||
### 13.6 Implement GetTagRegistry
|
||||
|
||||
Create a new function: **BlueprintPure**, Public, named `GetTagRegistry`.
|
||||
|
||||
```
|
||||
[Function: GetTagRegistry] → DA_GameTagRegistry (BlueprintPure)
|
||||
Return TagRegistry
|
||||
```
|
||||
|
||||
### 13.7 Implement IsFrameworkReady
|
||||
|
||||
Create a new function: **BlueprintPure**, Public, named `IsFrameworkReady`.
|
||||
|
||||
```
|
||||
[Function: IsFrameworkReady] → Boolean (BlueprintPure)
|
||||
Return bFrameworkInitialized
|
||||
```
|
||||
|
||||
### 13.8 How Other Systems Bind to OnFrameworkReady
|
||||
|
||||
In any other Blueprint that needs framework services (tag validation, subsystems, etc.):
|
||||
|
||||
```
|
||||
[Event BeginPlay]
|
||||
Step 1: Get Game Instance → Cast to GI_StarterGameInstance → Store as "GameInstance"
|
||||
Step 2: Branch: IsValid(GameInstance)?
|
||||
True:
|
||||
→ Branch: GameInstance → IsFrameworkReady()?
|
||||
True: Call "OnFrameworkReadyHandler" immediately (already initialized)
|
||||
False: Bind "OnFrameworkReadyHandler" to GameInstance.OnFrameworkReady
|
||||
False: Print Warning "GI_StarterGameInstance not found!"
|
||||
```
|
||||
|
||||
**Custom Event: OnFrameworkReadyHandler**
|
||||
```
|
||||
[Custom Event: OnFrameworkReadyHandler]
|
||||
Step 1: Unbind from GameInstance.OnFrameworkReady (if bound — prevents double-fire)
|
||||
Step 2: ... proceed with system-specific initialization ...
|
||||
```
|
||||
|
||||
### 13.9 Quick Node Reference
|
||||
|
||||
| Node | Where to Find | Used For |
|
||||
|------|---------------|----------|
|
||||
| `Event Init` | Override in My Blueprint → Functions | GameInstance startup |
|
||||
| `Get Game Instance` | Right-click → "Get Game Instance" | Any Blueprint that needs the GameInstance |
|
||||
| `Cast to GI_StarterGameInstance` | Right-click → "Cast to GI_StarterGameInstance" | Type-safe access to framework functions |
|
||||
| `IsValid` | Right-click → "IsValid" | Null-checking object references |
|
||||
| `Bind Event to OnFrameworkReady` | Right-click on dispatcher → "Bind Event" | Deferred initialization |
|
||||
| `Unbind Event from OnFrameworkReady` | Right-click on dispatcher → "Unbind Event" | Clean up after handler fires |
|
||||
|
||||
---
|
||||
|
||||
## 14. Blueprint Build Checklist
|
||||
|
||||
- [ ] Create `GI_StarterGameInstance` Blueprint (Parent: `GameInstance`)
|
||||
- [ ] Set as `Game Instance Class` in `Project Settings → Maps & Modes`
|
||||
- [ ] Create `DA_GameTagRegistry` Data Asset first (required for `TagRegistry` variable type)
|
||||
- [ ] Add variable `TagRegistry` (type: `DA_GameTagRegistry`) and assign the Data Asset
|
||||
- [ ] Add variable `bValidateTagsOnInit` (Boolean, default `true`)
|
||||
- [ ] Add variable `bLogTagsOnInit` (Boolean, default `false`)
|
||||
- [ ] Add variable `bFrameworkInitialized` (Boolean, default `false`, Private)
|
||||
- [ ] Create Event Dispatcher `OnFrameworkReady` (no params)
|
||||
- [ ] Create Event Dispatcher `OnFrameworkInitFailed` (1 param: `ErrorReason` String)
|
||||
- [ ] Override `Event Init` — add Call to Parent, validation branch, dispatcher broadcasts
|
||||
- [ ] Implement `ValidateFrameworkTags()` — validate registry, count tags, optional log
|
||||
- [ ] Implement `GetTagRegistry()` (BlueprintPure) — return TagRegistry reference
|
||||
- [ ] Implement `IsFrameworkReady()` (BlueprintPure) — return bFrameworkInitialized
|
||||
- [ ] Compile and test: PIE → output log shows "DA_GameTagRegistry initialized: N tags"
|
||||
- [ ] Test error path: clear `TagRegistry` variable → `OnFrameworkInitFailed` fires
|
||||
- [ ] Test with `bLogTagsOnInit = true` → all tags printed to output log
|
||||
- [ ] Verify other systems can bind to `OnFrameworkReady` and defer init correctly
|
||||
|
||||
---
|
||||
|
||||
## 15. Multiplayer Networking
|
||||
|
||||
**Replication: None needed.** The GameInstance is a client-only singleton — each client has its own instance. The `DA_GameTagRegistry` Data Asset loads identically from disk on all clients and servers. Event Dispatchers (`OnFrameworkReady`, `OnFrameworkInitFailed`) fire locally on each instance.
|
||||
|
||||
**Authority: N/A.** No runtime state changes. Tag data is read-only configuration.
|
||||
|
||||
**Multiplayer Note:** In a networked game, `Event Init` fires on both server and each client's GameInstance. Tag validation runs independently on each — which is correct, since Data Tables and GameplayTags must be identical across all instances for networked tag replication to function.
|
||||
404
docs/blueprints/01-core/01_DA_GameTagRegistry.md
Normal file
404
docs/blueprints/01-core/01_DA_GameTagRegistry.md
Normal file
@@ -0,0 +1,404 @@
|
||||
# DA_GameTagRegistry — Blueprint Specification
|
||||
|
||||
> **Asset Type:** Data Asset (derives from `UPrimaryDataAsset`)
|
||||
> **UE Version:** 5.5–5.7
|
||||
> **Category:** 01-Core / Foundation
|
||||
> **Build Phase:** Phase 0 — Item 1
|
||||
> **Dependencies:** None (this is the first system)
|
||||
>
|
||||
> ---
|
||||
>
|
||||
> **⚡ C++ Status: Full Implementation** — `Source/PG_Framework/Public/Core/DA_GameTagRegistry.h` provides all tag query, validation, and export functions. The Blueprint spec below documents the API and Data Table configuration. **Do NOT create a Blueprint child** — create a **Data Asset instance** (Right-click → Miscellaneous → Data Asset → pick `DA_GameTagRegistry`). Assign the 11 `TagDataTables`. All functions are `BlueprintCallable` — call them from any BP. See `docs/developer/cpp-integration-guide.md` for exact setup steps.
|
||||
>
|
||||
> ---## 1. Purpose
|
||||
|
||||
Centralises every `GameplayTag` namespace used across the framework in a single asset. All other systems reference tags from this registry — never raw strings, never `FName` comparisons, never hardcoded booleans for state.
|
||||
|
||||
---
|
||||
|
||||
## 2. Class Settings
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Parent Class** | `UPrimaryDataAsset` |
|
||||
| **Blueprint Type** | Data Asset |
|
||||
| **Asset Path** | `/Game/Framework/Core/DA_GameTagRegistry` |
|
||||
| **Is Abstract** | No |
|
||||
|
||||
---
|
||||
|
||||
## 3. Enums
|
||||
|
||||
None. This Data Asset uses only **Gameplay Tags** as the canonical state identifiers. No enums are defined here.
|
||||
|
||||
---
|
||||
|
||||
## 4. Structs
|
||||
|
||||
None. The registry is a flat collection of tag declarations.
|
||||
|
||||
---
|
||||
|
||||
## 5. Variables
|
||||
|
||||
| Name | Type | Category | Instance Editable | Description |
|
||||
|------|------|----------|-------------------|-------------|
|
||||
| `TagNamespace` | `FText` | Documentation | Yes | Human-readable description of the tag namespace (e.g. "Player.State") |
|
||||
| `bIsFrameworkTag` | `bool` | Documentation | Yes | `true` for framework-defined tags, `false` for project-specific overrides |
|
||||
|
||||
**NOTE:** The tag definitions themselves live in the project's `DefaultGameplayTags.ini` file (or via the **Project Settings → Gameplay Tags** editor). This asset provides a centralised **documentation anchor** for those tags. The Blueprint graph does not hold tag data.
|
||||
|
||||
---
|
||||
|
||||
## 6. Functions
|
||||
|
||||
### 6.1 Blueprint Pure Functions
|
||||
|
||||
| Name | Inputs | Outputs | Category | Description |
|
||||
|------|--------|---------|----------|-------------|
|
||||
| `GetAllRegisteredTags` | — | `Array<FName>` | Query | Reads tags from all registered per-category Data Tables (see Section 14) |
|
||||
| `GetTagDisplayName` | `Tag: FGameplayTag` | `Text` | Query | Returns the human-readable display name via `Get Tag Display Name` node |
|
||||
| `ValidateTag` | `Tag: FGameplayTag` | `Boolean` | Validation | Returns `true` if the tag is valid via `Is Gameplay Tag Valid` node |
|
||||
|
||||
### 6.2 Blueprint Callable Functions
|
||||
|
||||
| Name | Inputs | Outputs | Category | Description |
|
||||
|------|--------|---------|----------|-------------|
|
||||
| `LogAllTags` | — | — | Debug | Prints all tags (from Data Table) to the output log (Editor-only) |
|
||||
| `ExportTagNamespace` | `NamespacePrefix: String` | `String` | Tooling | Exports all tags matching a namespace prefix as a formatted string | |
|
||||
|
||||
---
|
||||
|
||||
## 7. Event Dispatchers
|
||||
|
||||
None. This Data Asset is passive — it has no runtime events.
|
||||
|
||||
---
|
||||
|
||||
## 8. Overridden Events
|
||||
|
||||
**Note:** Data Assets do NOT have `BeginPlay`, `Tick`, or `OnAssetLoaded` in Blueprint. All validation logic should be called externally from a GameInstance or Subsystem during initialization.
|
||||
|
||||
### Initialization Pattern (called externally):
|
||||
```
|
||||
[In GI_GameFramework.Init() or BPC_StateManager.BeginPlay:]
|
||||
├─► Load DA_GameTagRegistry (hard reference or Get Data Asset)
|
||||
├─► Call DA_GameTagRegistry.GetAllRegisteredTags()
|
||||
├─► Array Length == 0?
|
||||
│ ├─► Yes → Print Warning: "No tags in registered Data Tables! Framework tags are unregistered."
|
||||
│ └─► No → Log: "N tags registered."
|
||||
└─► Optional: call LogAllTags() for debug output
|
||||
```
|
||||
|
||||
## 9. Blueprint Graph Logic
|
||||
|
||||
### 9.1 GetAllRegisteredTags (Multi-Table Data Table Proxy)
|
||||
|
||||
**⚠️ UE5 Limitation:** `UGameplayTagsManager::RequestAllGameplayTags()` is C++ only — not exposed to Blueprints. This function uses a **Data Table proxy** with multiple per-category tables. All tables must be registered in `Project Settings → GameplayTags → Gameplay Tag Table List`.
|
||||
|
||||
```
|
||||
[Function: GetAllRegisteredTags] → Array<GameplayTag>
|
||||
Step 1: Create empty Array<GameplayTag> → LocalTags
|
||||
Step 2: ForEachLoop over TagDataTables (outer loop):
|
||||
├─► Branch: IsValid(current table)? → False: skip
|
||||
├─► Get Data Table Row Names (current table)
|
||||
└─► ForEachLoop (RowNames) (inner loop):
|
||||
├─► Get Data Table Row → Break GameplayTagTableRow → get "Tag"
|
||||
└─► Add "Tag" to LocalTags
|
||||
Step 3: Return LocalTags
|
||||
```
|
||||
|
||||
**Key Change from v1:** The outer loop iterates `TagDataTables` (Array), not a single `TagDataTable`. This enables tags split across multiple CSV files by category.
|
||||
|
||||
### 9.2 ValidateTag
|
||||
|
||||
**⚠️ UE5 Limitation:** `UGameplayTagsManager::RequestGameplayTag()` is not a direct Blueprint node. Use `Is Gameplay Tag Valid` instead.
|
||||
|
||||
```
|
||||
[Function: ValidateTag(Tag)] → Boolean
|
||||
Step 1: Is Gameplay Tag Valid (Tag)
|
||||
├─► True → Return true
|
||||
└─► False → Print Warning: "Invalid Tag: {Get Tag Name(Tag)}" → Return false
|
||||
```
|
||||
|
||||
**Node Search:** `Is Gameplay Tag Valid`, `Get Tag Name`
|
||||
|
||||
### 9.3 GetTagDisplayName
|
||||
|
||||
```
|
||||
[Function: GetTagDisplayName(Tag)] → Text
|
||||
Step 1: Get Tag Display Name (Tag) → Return
|
||||
```
|
||||
|
||||
**Node Search:** `Get Tag Display Name` (Blueprint pure node, available)
|
||||
|
||||
### 9.4 LogAllTags
|
||||
|
||||
```
|
||||
[Function: LogAllTags] (Blueprint Callable)
|
||||
Step 1: GetAllRegisteredTags() → Store in LocalTags
|
||||
Step 2: ForEachLoop (LocalTags):
|
||||
├─► Get Tag Name → ToString → Print String
|
||||
Step 3: Print String: "Total tags: {Array Length(LocalTags)}"
|
||||
```
|
||||
|
||||
### 9.5 ExportTagNamespace(Prefix: String) → String
|
||||
|
||||
```
|
||||
[Function: ExportTagNamespace]
|
||||
Step 1: GetAllRegisteredTags() → LocalTags
|
||||
Step 2: Create empty String → Output
|
||||
Step 3: ForEachLoop (LocalTags):
|
||||
├─► Get Tag Name → ToString → TagString
|
||||
├─► Branch: Does TagString start with Prefix?
|
||||
│ True → Append TagString + "\n" to Output
|
||||
└─► Continue
|
||||
Step 4: Return Output
|
||||
```
|
||||
[OnAssetLoaded]
|
||||
└─► Call GetAllRegisteredTags()
|
||||
└─► Length == 0?
|
||||
├─► Yes → Print Warning: "No Gameplay Tags registered! /Game/Framework/Core/DA_GameTagRegistry is empty."
|
||||
└─► No → Log: "N tags registered."
|
||||
```
|
||||
|
||||
### 9.2 Validation Function Logic (ValidateTag)
|
||||
|
||||
```
|
||||
[ValidateTag(Tag)]
|
||||
└─► Use GameplayTag::RequestGameplayTag(Tag.TagName)
|
||||
└─► IsValid?
|
||||
├─► Yes → return true
|
||||
└─► No → Print Warning: "Invalid Tag: {Tag}" → return false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Tag Namespace Reference (Framework Canonical)
|
||||
|
||||
All framework-level tags are defined in per-category Data Tables (CSV format, Row Structure: `GameplayTagTableRow`). These tables are registered in `Project Settings → GameplayTags → Gameplay Tag Table List`. The engine's `UGameplayTagsManager` merges all tables into one master list.
|
||||
|
||||
| Data Table | Namespaces Covered | Example Tags |
|
||||
|------------|-------------------|--------------|
|
||||
| `DT_Tags_Player` | `Framework.Player.State.*`, `*.Stress.*`, `*.Posture.*`, `*.Movement.*`, `*.Camera.*`, `*.Body.*`, `*.Overlay.*`, `*.Vitals.*` | Alive, Dead, Hidden, Sprinting, Crouching, Aiming, FullBody, Blood |
|
||||
| `DT_Tags_Interaction` | `Framework.Interaction.Type.*`, `*.Context.*`, `*.Prompt.*`, `*.HidingSpot.*`, `*.Traversal.*`, `*.Door.*` | Pickup, Door, Container, Locked, OneShot, Vault, Mantle |
|
||||
| `DT_Tags_Item` | `Framework.Item.Type.*`, `*.Slot.*`, `*.Rarity.*`, `*.Context.*` | Weapon, KeyItem, PrimaryWeapon, Legendary, Stackable |
|
||||
| `DT_Tags_Narrative` | `Game.Narrative.Flag.*`, `*.Phase.*`, `*.Choice.*`, `*.Ending.*`, `*.Trial.*`, `*.Cutscene.*`, `*.Lore.*`, `Framework.Objective.*` | Chapter1Complete, Act2, GoodEnding, HospitalEscape, Intro |
|
||||
| `DT_Tags_AI` | `Framework.AI.Alert.*`, `*.Archetype.*`, `*.Stimulus.*`, `*.Behavior.*`, `*.Memory.*` | Alerted, Stalker, Hearing, Aggressive, LastKnownLocation |
|
||||
| `DT_Tags_Save` | `Framework.Save.*`, `*.DeathSpace.*`, `*.Checkpoint.*`, `*.Respawn.*`, `*.RunHistory.*` | Checkpoint, HardSave, Slot.1, Entered, Active, Reached, Start, Death |
|
||||
| `DT_Tags_Environment` | `Game.Environment.Atmosphere.*`, `*.Scare.*`, `*.Light.*`, `*.Pacing.*`, `*.Performance.*` | Tense, MirrorJump, Flicker, Combat, High |
|
||||
| `DT_Tags_Combat` | `Framework.Combat.Damage.*`, `*.Weapon.*`, `*.Ammo.*`, `*.FireMode.*`, `*.HitReaction.*`, `*.Feedback.*`, `*.Shield.*` | Physical, Firearm, Pistol, Flinch, HitMarker, Active |
|
||||
| `DT_Tags_State` | `Framework.State.Action.*`, `*.Overlay.*`, `*.Vital.*`, `*.Gating.*` | Fire, Sprint, Menu, Health, BlockSprint |
|
||||
| `DT_Tags_Audio` | `Framework.Audio.Bus.*`, `*.Room.*`, `*.Parameter.*`, `*.Surface.*` | SFX, Small, HeartRate, Concrete |
|
||||
| `DT_Tags_Achievement` | `Game.Achievement.*` | FirstBlood, Survivor, Pacifist, Ghost, Collector, LoreMaster, TrueEnding, SpeedRunner, WeaponsMaster, Completionist |
|
||||
|
||||
**Usage Rule:** All systems must add the prefix `Framework.` to framework tags in code comments. Project-specific tags use `Game.` prefix. Example: `Framework.Item.Type.Weapon` vs `Game.Narrative.Flag.BasementDoorOpened`.
|
||||
|
||||
**CSV Format:** Each Data Table CSV uses columns: `Name,Tag,DevComment`. Name is a unique row identifier. Tag is the full gameplay tag string. DevComment explains the tag's purpose.
|
||||
|
||||
---
|
||||
|
||||
## 11. Communication Matrix
|
||||
|
||||
| Target System | Method | Direction | Data |
|
||||
|---------------|--------|-----------|------|
|
||||
| All other systems | `GameplayTag` comparisons | Read-only | Framework-defined tag values |
|
||||
|
||||
This asset does not talk to other systems directly. All communication is passive — other systems read tag values from the project's tag table.
|
||||
|
||||
---
|
||||
|
||||
## 12. Validation Checklist
|
||||
|
||||
- [ ] All namespace prefixes documented in the Data Asset's `TagNamespace` text field
|
||||
- [ ] `bIsFrameworkTag` set to `true` for framework namespaces
|
||||
- [ ] At least one project-specific tag exists (e.g. `Game.Narrative.Flag.PrologueComplete`) to validate the workflow
|
||||
- [ ] `DefaultGameplayTags.ini` contains ALL documented tags
|
||||
- [ ] No duplicate tag names across namespaces
|
||||
- [ ] No raw string comparisons exist in any other system (all must use `HasTag` / `MatchesTag`)
|
||||
|
||||
---
|
||||
|
||||
## 13. Reuse Notes
|
||||
|
||||
- The `Game.` namespace is reserved for **project-specific** tags and is empty in the framework distribution.
|
||||
- To add new tags: edit `DefaultGameplayTags.ini` or use the **Gameplay Tags** editor under **Project Settings**.
|
||||
- The Data Asset is purely documentary — the real tag data lives in the project's tag table. This file exists so systems have a single place to look up "what tags exist in the framework" without reading the `.ini` file directly.
|
||||
|
||||
---
|
||||
|
||||
## 14. Manual Implementation Guide
|
||||
|
||||
> **For human implementer:** Follow these steps to build `DA_GameTagRegistry` in UE5 Blueprints.
|
||||
> **⚠️ UE5 BP Limitation:** `UGameplayTagsManager::RequestAllGameplayTags()` is C++ only. This implementation uses an **Array of Data Tables** (11 per-category tables) as the 100% Blueprint workaround. All tables must be registered in `Project Settings → GameplayTags → Gameplay Tag Table List`.
|
||||
|
||||
### 14.0 Prerequisite: Create the Per-Category Data Tables
|
||||
|
||||
Before implementing the Data Asset, create **11 Data Tables**, one per category. Each follows the same format:
|
||||
|
||||
**Format:** Data Table with Row Structure = `GameplayTagTableRow`
|
||||
**CSV Columns:** `Name,Tag,DevComment`
|
||||
**Location in docs:** `docs/blueprints/01-core/data-tables/DT_Tags_*.csv`
|
||||
**Location in UE5:** `Content/Framework/Core/DataTables/DT_Tags_*`
|
||||
|
||||
1. For each category, right-click in Content Browser → **Miscellaneous → Data Table**
|
||||
2. Row Structure: `GameplayTagTableRow`
|
||||
3. Name: `DT_Tags_{Category}` (e.g., `DT_Tags_Player`)
|
||||
4. Import the corresponding CSV file (Right-click Data Table → Import → CSV)
|
||||
5. **Critical:** Go to `Project Settings → GameplayTags → Gameplay Tag Table List` → click `+` 11 times → assign each table to a slot
|
||||
- This auto-registers ALL tags with the engine's tag manager.
|
||||
|
||||
### 14.1 Class Setup
|
||||
|
||||
1. Right-click in Content Browser → **Miscellaneous → Data Asset**
|
||||
2. Select parent class: `PrimaryDataAsset`
|
||||
3. Name: `DA_GameTagRegistry`
|
||||
4. Save to: `Content/Framework/Core/`
|
||||
|
||||
### 14.2 Variables
|
||||
|
||||
Add to Class Defaults:
|
||||
| Variable | Type | Instance Editable | Default | Category |
|
||||
|----------|------|------------------|---------|----------|
|
||||
| `TagNamespace` | `Text` | ✓ | *"Framework tag namespace documentation"* | Documentation |
|
||||
| `bIsFrameworkTag` | `Boolean` | ✓ | `true` | Documentation |
|
||||
| `TagDataTables` | `Array<Data Table Object Reference>` | ✓ | [DT_Tags_Player, DT_Tags_Interaction, DT_Tags_Item, DT_Tags_Narrative, DT_Tags_AI, DT_Tags_Save, DT_Tags_Environment, DT_Tags_Combat, DT_Tags_State, DT_Tags_Audio, DT_Tags_Achievement] | Config |
|
||||
|
||||
**⚠️ Multi-Table Design:** Tags are split into 11 per-category Data Tables (see Section 10). In UE5 `Project Settings → GameplayTags → Gameplay Tag Table List`, add ALL 11 tables. The engine's `UGameplayTagsManager` merges them automatically.
|
||||
|
||||
### 14.3 Function Implementations
|
||||
|
||||
#### `GetAllRegisteredTags()` → `Array<GameplayTag>` *(Blueprint Pure)*
|
||||
|
||||
**Purpose:** Returns all tags from ALL registered Data Tables. Uses an outer loop over `TagDataTables` array and inner loop over each table's rows.
|
||||
|
||||
**⚠️ UE5 Limitation:** `UGameplayTagsManager::RequestAllGameplayTags()` is C++ only — not exposed to Blueprints. This function uses the Data Table proxy approach with multiple tables.
|
||||
|
||||
**Node-by-Node Logic:**
|
||||
```
|
||||
[Function: GetAllRegisteredTags] (Pure, no execution pins)
|
||||
Step 1: Create empty Array<GameplayTag> → LocalTags
|
||||
Step 2: ForEachLoop over TagDataTables array (outer loop):
|
||||
├─► Array Element = current Data Table
|
||||
├─► Branch: IsValid(current table)?
|
||||
│ False → Skip to next table
|
||||
│ True → Continue
|
||||
├─► Get Data Table Row Names (current table) → returns Array<Name>
|
||||
└─► ForEachLoop over RowNames (inner loop):
|
||||
├─► Get Data Table Row (current table, Array Element)
|
||||
│ → Struct Pin: Break GameplayTagTableRow
|
||||
│ → Get "Tag" field (type: GameplayTag)
|
||||
└─► Add "Tag" to LocalTags array
|
||||
Step 3: Return LocalTags
|
||||
```
|
||||
|
||||
**Nodes to Search:** `ForEachLoop` (outer over TagDataTables), `ForEachLoop` (inner over row names), `Get Data Table Row Names`, `Get Data Table Row`, `Break GameplayTagTableRow`, `Add`, `Array`, `IsValid`
|
||||
|
||||
**⚠️ Note:** If your UE version doesn't support loops in Pure functions, make this **BlueprintCallable** (impure) instead.
|
||||
|
||||
#### `GetTagDisplayName(Tag: GameplayTag)` → `Text` *(Blueprint Pure)*
|
||||
|
||||
**Node-by-Node Logic:**
|
||||
```
|
||||
[Function: GetTagDisplayName]
|
||||
Input Tag → Get Tag Display Name (Tag) → Return
|
||||
```
|
||||
|
||||
**Node Search:** `Get Tag Display Name` — this IS available in Blueprint (part of GameplayTags plugin).
|
||||
|
||||
#### `ValidateTag(Tag: GameplayTag)` → `Boolean` *(Blueprint Pure)*
|
||||
|
||||
**⚠️ UE5 Limitation:** `UGameplayTagsManager::RequestGameplayTag()` is not directly available in BP. Use `Is Gameplay Tag Valid` instead.
|
||||
|
||||
**Node-by-Node Logic:**
|
||||
```
|
||||
[Function: ValidateTag]
|
||||
Step 1: Is Gameplay Tag Valid (Tag)
|
||||
├─► True → Return true
|
||||
└─► False → Print Warning: "Invalid Tag: " + Get Tag Name(Tag) → Return false
|
||||
```
|
||||
|
||||
**Node Search:** `Is Gameplay Tag Valid`, `Get Tag Name`
|
||||
|
||||
**⚠️ Note:** `Is Gameplay Tag Valid` returns true only if the tag is registered in the engine's tag table (which the 11 Data Tables populate via Project Settings). Tags NOT in any registered table will return false.
|
||||
|
||||
#### `LogAllTags()` → *(void)* *(Blueprint Callable)*
|
||||
|
||||
**Editor Only.** Prints all tags from all Data Tables to the output log.
|
||||
|
||||
**Node-by-Node Logic:**
|
||||
```
|
||||
[Function: LogAllTags]
|
||||
Step 1: Call GetAllRegisteredTags() → LocalTags
|
||||
Step 2: ForEachLoop (LocalTags):
|
||||
├─► Name (Array Element) → ToString → Print String
|
||||
Step 3: Print String: "Total tags across all tables: " + Array Length(LocalTags)
|
||||
```
|
||||
|
||||
#### `ExportTagNamespace(NamespacePrefix: String)` → `String` *(Blueprint Callable)*
|
||||
|
||||
**Node-by-Node Logic:**
|
||||
```
|
||||
[Function: ExportTagNamespace]
|
||||
Step 1: Call GetAllRegisteredTags() → LocalTags
|
||||
Step 2: Create String variable → Output = ""
|
||||
Step 3: ForEachLoop (LocalTags):
|
||||
├─► (Array Element) → ToString → TagString
|
||||
├─► Branch: Does TagString start with NamespacePrefix?
|
||||
│ True → Append TagString + "\n" to Output (use "Append" or "Build String")
|
||||
└─► Continue
|
||||
Step 4: Return Output
|
||||
```
|
||||
|
||||
### 14.4 External Initialization
|
||||
|
||||
Since Data Assets have no `BeginPlay`, call validation from your GameInstance or StateManager:
|
||||
|
||||
**In `GI_StarterGameInstance.Init()` or `GI_GameFramework.Init()`:**
|
||||
```
|
||||
Step 1: Get DA_GameTagRegistry (hard reference or Load Asset)
|
||||
Step 2: Call DA_GameTagRegistry.GetAllRegisteredTags() → LocalTags
|
||||
Step 3: Branch: Array Length(LocalTags) == 0
|
||||
├─► True → Print Warning: "No tags in registered Data Tables!"
|
||||
└─► False → Print String: "N tags registered: " + Array Length
|
||||
Step 4: [Debug builds only] Call DA_GameTagRegistry.LogAllTags()
|
||||
```
|
||||
|
||||
### 14.5 Networking
|
||||
|
||||
No replication needed. This is a read-only Data Asset with a read-only Data Table reference. All clients load identical copies from disk.
|
||||
|
||||
### 14.6 Blueprint Build Checklist
|
||||
|
||||
- [ ] Create 11 Data Tables with Row Structure `GameplayTagTableRow`:
|
||||
- [ ] `DT_Tags_Player` — Player state, stress, posture, movement, camera, body, vitals
|
||||
- [ ] `DT_Tags_Interaction` — Interaction types, contexts, prompts, hiding spots, traversal, doors
|
||||
- [ ] `DT_Tags_Item` — Item types, slots, rarities, contexts
|
||||
- [ ] `DT_Tags_Narrative` — Flags, phases, choices, endings, trials, cutscenes, lore, objectives
|
||||
- [ ] `DT_Tags_AI` — Alert levels, archetypes, stimuli, behaviors, memory
|
||||
- [ ] `DT_Tags_Save` — Save types, death space, checkpoints, respawn
|
||||
- [ ] `DT_Tags_Environment` — Atmosphere, scares, lighting, pacing, performance
|
||||
- [ ] `DT_Tags_Combat` — Damage types, weapons, ammo, fire modes, hit reactions, shield
|
||||
- [ ] `DT_Tags_State` — Action states, overlay states, vitals, gating
|
||||
- [ ] `DT_Tags_Audio` — Audio buses, room acoustics, parameters, surfaces
|
||||
- [ ] `DT_Tags_Achievement` — Achievement identifiers
|
||||
- [ ] Add ALL 11 tables to `Project Settings → GameplayTags → Gameplay Tag Table List`
|
||||
- [ ] Create Data Asset: `DA_GameTagRegistry` (Parent: `PrimaryDataAsset`)
|
||||
- [ ] Add variables: `TagNamespace` (Text), `bIsFrameworkTag` (Boolean), `TagDataTables` (Array<Data Table> — populate with all 11 tables)
|
||||
- [ ] Implement `GetAllRegisteredTags` with OUTER loop over TagDataTables + INNER loop over row names
|
||||
- [ ] Implement `GetTagDisplayName` using `Get Tag Display Name` node
|
||||
- [ ] Implement `ValidateTag` using `Is Gameplay Tag Valid` node
|
||||
- [ ] Implement `LogAllTags` (editor-only, prints tags from all tables)
|
||||
- [ ] Implement `ExportTagNamespace` (string prefix filtering across all tables)
|
||||
- [ ] Add external initialization call from `GI_StarterGameInstance.Init()` or `GI_GameFramework.Init()`
|
||||
- [ ] Verify: all tags from Section 10 exist in the appropriate Data Table
|
||||
- [ ] Verify: `ValidateTag` returns true for tags in any registered table, false for unregistered
|
||||
- [ ] Verify: `GetAllRegisteredTags` returns ALL tags merged from all tables
|
||||
|
||||
---
|
||||
|
||||
## 15. Multiplayer Networking
|
||||
|
||||
**Replication: None needed.** This Data Asset is read-only configuration. All clients load identical copies from disk. GameplayTag data lives in `DefaultGameplayTags.ini` which is identical on all instances.
|
||||
|
||||
**Authority: N/A.** No runtime state changes.
|
||||
@@ -1,399 +1,19 @@
|
||||
# GI_GameTagRegistry — Blueprint Specification
|
||||
# RENAMED — See `01_DA_GameTagRegistry.md`
|
||||
|
||||
> **Asset Type:** Data Asset (derives from `UPrimaryDataAsset`)
|
||||
> **UE Version:** 5.5–5.7
|
||||
> **Category:** 01-Core / Foundation
|
||||
> **Build Phase:** Phase 0 — Item 1
|
||||
> **Dependencies:** None (this is the first system)
|
||||
> **This file has been renamed.** The system is now `DA_GameTagRegistry` (Data Asset prefix, `UPrimaryDataAsset` parent), not `GI_GameTagRegistry` (Game Instance prefix).
|
||||
|
||||
---
|
||||
## Why the rename?
|
||||
|
||||
## 1. Purpose
|
||||
The `GI_` prefix implies a `GameInstance` class, but this system is architecturally a **Data Asset** — it has no lifecycle events (`BeginPlay`, `Tick`), no runtime state, and is loaded as a read-only configuration asset. The framework already has `GI_GameFramework` (#04) as the project's sole GameInstance class.
|
||||
|
||||
Centralises every `GameplayTag` namespace used across the framework in a single asset. All other systems reference tags from this registry — never raw strings, never `FName` comparisons, never hardcoded booleans for state.
|
||||
Additionally, the new [`GI_StarterGameInstance`](../00-project-setup/GI_StarterGameInstance.md) (00-project-setup) provides the actual GameInstance entry point that loads and validates `DA_GameTagRegistry` during `Event Init`.
|
||||
|
||||
---
|
||||
## Migration
|
||||
|
||||
## 2. Class Settings
|
||||
If you were referencing this file:
|
||||
- **Old path:** `docs/blueprints/01-core/01_GI_GameTagRegistry.md`
|
||||
- **New path:** [`docs/blueprints/01-core/01_DA_GameTagRegistry.md`](01_DA_GameTagRegistry.md)
|
||||
- **Old asset name:** `GI_GameTagRegistry` → **New asset name:** `DA_GameTagRegistry`
|
||||
- **Old asset path:** `/Game/Framework/Core/DA_GameTagRegistry` (asset path was already correct)
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Parent Class** | `UPrimaryDataAsset` |
|
||||
| **Blueprint Type** | Data Asset |
|
||||
| **Asset Path** | `/Game/Framework/Core/DA_GameTagRegistry` |
|
||||
| **Is Abstract** | No |
|
||||
|
||||
---
|
||||
|
||||
## 3. Enums
|
||||
|
||||
None. This Data Asset uses only **Gameplay Tags** as the canonical state identifiers. No enums are defined here.
|
||||
|
||||
---
|
||||
|
||||
## 4. Structs
|
||||
|
||||
None. The registry is a flat collection of tag declarations.
|
||||
|
||||
---
|
||||
|
||||
## 5. Variables
|
||||
|
||||
| Name | Type | Category | Instance Editable | Description |
|
||||
|------|------|----------|-------------------|-------------|
|
||||
| `TagNamespace` | `FText` | Documentation | Yes | Human-readable description of the tag namespace (e.g. "Player.State") |
|
||||
| `bIsFrameworkTag` | `bool` | Documentation | Yes | `true` for framework-defined tags, `false` for project-specific overrides |
|
||||
|
||||
**NOTE:** The tag definitions themselves live in the project's `DefaultGameplayTags.ini` file (or via the **Project Settings → Gameplay Tags** editor). This asset provides a centralised **documentation anchor** for those tags. The Blueprint graph does not hold tag data.
|
||||
|
||||
---
|
||||
|
||||
## 6. Functions
|
||||
|
||||
### 6.1 Blueprint Pure Functions
|
||||
|
||||
| Name | Inputs | Outputs | Category | Description |
|
||||
|------|--------|---------|----------|-------------|
|
||||
| `GetAllRegisteredTags` | — | `Array<FGameplayTag>` | Query | Reads tags from `DT_ProjectTags` Data Table (see Section 14 for UE5 BP workaround) |
|
||||
| `GetTagDisplayName` | `Tag: FGameplayTag` | `Text` | Query | Returns the human-readable display name via `Get Tag Display Name` node |
|
||||
| `ValidateTag` | `Tag: FGameplayTag` | `Boolean` | Validation | Returns `true` if the tag is valid via `Is Gameplay Tag Valid` node |
|
||||
|
||||
### 6.2 Blueprint Callable Functions
|
||||
|
||||
| Name | Inputs | Outputs | Category | Description |
|
||||
|------|--------|---------|----------|-------------|
|
||||
| `LogAllTags` | — | — | Debug | Prints all tags (from Data Table) to the output log (Editor-only) |
|
||||
| `ExportTagNamespace` | `NamespacePrefix: String` | `String` | Tooling | Exports all tags matching a namespace prefix as a formatted string | |
|
||||
|
||||
---
|
||||
|
||||
## 7. Event Dispatchers
|
||||
|
||||
None. This Data Asset is passive — it has no runtime events.
|
||||
|
||||
---
|
||||
|
||||
## 8. Overridden Events
|
||||
|
||||
**Note:** Data Assets do NOT have `BeginPlay`, `Tick`, or `OnAssetLoaded` in Blueprint. All validation logic should be called externally from a GameInstance or Subsystem during initialization.
|
||||
|
||||
### Initialization Pattern (called externally):
|
||||
```
|
||||
[In GI_GameFramework.Init() or BPC_StateManager.BeginPlay:]
|
||||
├─► Load DA_GameTagRegistry (hard reference or Get Data Asset)
|
||||
├─► Call DA_GameTagRegistry.GetAllRegisteredTags()
|
||||
├─► Array Length == 0?
|
||||
│ ├─► Yes → Print Warning: "No tags in DT_ProjectTags! Framework tags are unregistered."
|
||||
│ └─► No → Log: "N tags registered."
|
||||
└─► Optional: call LogAllTags() for debug output
|
||||
```
|
||||
|
||||
## 9. Blueprint Graph Logic
|
||||
|
||||
### 9.1 GetAllRegisteredTags (Data Table Proxy)
|
||||
|
||||
**⚠️ UE5 Limitation:** `UGameplayTagsManager::RequestAllGameplayTags()` is C++ only — not exposed to Blueprints. This function uses a **Data Table proxy** instead. Tags must be registered in `DT_ProjectTags` which mirrors `DefaultGameplayTags.ini`.
|
||||
|
||||
```
|
||||
[Function: GetAllRegisteredTags] → Array<GameplayTag>
|
||||
Step 1: Get Data Table Row Names (DT_ProjectTags)
|
||||
Step 2: Create empty Array<GameplayTag> → LocalTags
|
||||
Step 3: ForEachLoop (RowNames):
|
||||
├─► Get Data Table Row (DT_ProjectTags, RowName)
|
||||
│ → Break the GameplayTagTableRow struct → get "Tag" field
|
||||
├─► Add "Tag" to LocalTags array
|
||||
Step 4: Return LocalTags
|
||||
```
|
||||
|
||||
**Node Search:** `Get Data Table Row Names`, `ForEachLoop`, `Get Data Table Row`, `Break GameplayTagTableRow`, `Add`
|
||||
|
||||
### 9.2 ValidateTag
|
||||
|
||||
**⚠️ UE5 Limitation:** `UGameplayTagsManager::RequestGameplayTag()` is not a direct Blueprint node. Use `Is Gameplay Tag Valid` instead.
|
||||
|
||||
```
|
||||
[Function: ValidateTag(Tag)] → Boolean
|
||||
Step 1: Is Gameplay Tag Valid (Tag)
|
||||
├─► True → Return true
|
||||
└─► False → Print Warning: "Invalid Tag: {Get Tag Name(Tag)}" → Return false
|
||||
```
|
||||
|
||||
**Node Search:** `Is Gameplay Tag Valid`, `Get Tag Name`
|
||||
|
||||
### 9.3 GetTagDisplayName
|
||||
|
||||
```
|
||||
[Function: GetTagDisplayName(Tag)] → Text
|
||||
Step 1: Get Tag Display Name (Tag) → Return
|
||||
```
|
||||
|
||||
**Node Search:** `Get Tag Display Name` (Blueprint pure node, available)
|
||||
|
||||
### 9.4 LogAllTags
|
||||
|
||||
```
|
||||
[Function: LogAllTags] (Blueprint Callable)
|
||||
Step 1: GetAllRegisteredTags() → Store in LocalTags
|
||||
Step 2: ForEachLoop (LocalTags):
|
||||
├─► Get Tag Name → ToString → Print String
|
||||
Step 3: Print String: "Total tags: {Array Length(LocalTags)}"
|
||||
```
|
||||
|
||||
### 9.5 ExportTagNamespace(Prefix: String) → String
|
||||
|
||||
```
|
||||
[Function: ExportTagNamespace]
|
||||
Step 1: GetAllRegisteredTags() → LocalTags
|
||||
Step 2: Create empty String → Output
|
||||
Step 3: ForEachLoop (LocalTags):
|
||||
├─► Get Tag Name → ToString → TagString
|
||||
├─► Branch: Does TagString start with Prefix?
|
||||
│ True → Append TagString + "\n" to Output
|
||||
└─► Continue
|
||||
Step 4: Return Output
|
||||
```
|
||||
[OnAssetLoaded]
|
||||
└─► Call GetAllRegisteredTags()
|
||||
└─► Length == 0?
|
||||
├─► Yes → Print Warning: "No Gameplay Tags registered! /Game/Framework/Core/DA_GameTagRegistry is empty."
|
||||
└─► No → Log: "N tags registered."
|
||||
```
|
||||
|
||||
### 9.2 Validation Function Logic (ValidateTag)
|
||||
|
||||
```
|
||||
[ValidateTag(Tag)]
|
||||
└─► Use GameplayTag::RequestGameplayTag(Tag.TagName)
|
||||
└─► IsValid?
|
||||
├─► Yes → return true
|
||||
└─► No → Print Warning: "Invalid Tag: {Tag}" → return false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Tag Namespace Reference (Framework Canonical)
|
||||
|
||||
All framework-level tags are defined in `DefaultGameplayTags.ini`. The following namespaces are **reserved and documented** by this asset:
|
||||
|
||||
```
|
||||
Player.State.Alive
|
||||
Player.State.Dead
|
||||
Player.State.Hidden
|
||||
Player.State.Interacting
|
||||
Player.Stress.Low / Mid / High / Critical
|
||||
Player.Posture.Standing / Crouching / Prone / Vaulting
|
||||
|
||||
Interaction.Type.Pickup / Door / Drawer / Container / Inspect / Climb / Hide / Use / Combine
|
||||
Interaction.Context.Requires.Key / Requires.Item / Locked / Disabled
|
||||
|
||||
Item.Type.Weapon / Consumable / KeyItem / Document / Collectible / Ammo / Tool
|
||||
Item.Slot.PrimaryWeapon / SecondaryWeapon / Flashlight / Shield / Active
|
||||
|
||||
Narrative.Flag.* ← game-specific flags
|
||||
Narrative.Phase.* ← story chapters / acts
|
||||
Narrative.Choice.* ← choice consequence tags
|
||||
Narrative.Ending.* ← ending conditions
|
||||
|
||||
Objective.Status.Active / Complete / Failed / Hidden
|
||||
|
||||
AI.Alert.None / Suspicious / Alerted / Engaged
|
||||
AI.Archetype.Patrol / Ambush / Stalker / Passive
|
||||
|
||||
Save.Type.Checkpoint / HardSave / AutoSave
|
||||
|
||||
Achievement.* ← per-achievement tags
|
||||
|
||||
Environment.Atmosphere.* ← atmosphere state tags
|
||||
Environment.Scare.* ← scare event tags
|
||||
|
||||
DeathSpace.Active ← triggers alt death space layer
|
||||
```
|
||||
|
||||
**Usage Rule:** All systems must add the prefix `Framework.` to framework tags in code comments. Project-specific tags use `Game.` prefix. Example: `Framework.Interaction.Type.Pickup` vs `Game.Narrative.Flag.BasementDoorOpened`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Communication Matrix
|
||||
|
||||
| Target System | Method | Direction | Data |
|
||||
|---------------|--------|-----------|------|
|
||||
| All other systems | `GameplayTag` comparisons | Read-only | Framework-defined tag values |
|
||||
|
||||
This asset does not talk to other systems directly. All communication is passive — other systems read tag values from the project's tag table.
|
||||
|
||||
---
|
||||
|
||||
## 12. Validation Checklist
|
||||
|
||||
- [ ] All namespace prefixes documented in the Data Asset's `TagNamespace` text field
|
||||
- [ ] `bIsFrameworkTag` set to `true` for framework namespaces
|
||||
- [ ] At least one project-specific tag exists (e.g. `Game.Narrative.Flag.PrologueComplete`) to validate the workflow
|
||||
- [ ] `DefaultGameplayTags.ini` contains ALL documented tags
|
||||
- [ ] No duplicate tag names across namespaces
|
||||
- [ ] No raw string comparisons exist in any other system (all must use `HasTag` / `MatchesTag`)
|
||||
|
||||
---
|
||||
|
||||
## 13. Reuse Notes
|
||||
|
||||
- The `Game.` namespace is reserved for **project-specific** tags and is empty in the framework distribution.
|
||||
- To add new tags: edit `DefaultGameplayTags.ini` or use the **Gameplay Tags** editor under **Project Settings**.
|
||||
- The Data Asset is purely documentary — the real tag data lives in the project's tag table. This file exists so systems have a single place to look up "what tags exist in the framework" without reading the `.ini` file directly.
|
||||
|
||||
---
|
||||
|
||||
## 14. Manual Implementation Guide
|
||||
|
||||
> **For human implementer:** Follow these steps to build `DA_GameTagRegistry` in UE5 Blueprints.
|
||||
> **⚠️ UE5 BP Limitation:** `UGameplayTagsManager::RequestAllGameplayTags()` is C++ only. This implementation uses a **Data Table proxy** (`DT_ProjectTags`) as the 100% Blueprint workaround. You must create and maintain `DT_ProjectTags` alongside your `DefaultGameplayTags.ini`.
|
||||
|
||||
### 14.0 Prerequisite: Create the Tag Data Table
|
||||
|
||||
Before implementing the Data Asset, create the proxy Data Table:
|
||||
|
||||
1. Right-click in Content Browser → **Miscellaneous → Data Table**
|
||||
2. Row Structure: `GameplayTagTableRow`
|
||||
3. Name: `DT_ProjectTags`
|
||||
4. Save to: `Content/Framework/Core/`
|
||||
5. Add rows: one per tag (row name = anything, fill the "Tag" field with your GameplayTag)
|
||||
6. Go to **Project Settings → GameplayTags → Gameplay Tag Table List** → click `+` → select `DT_ProjectTags`
|
||||
- This auto-registers these tags with the engine's tag manager.
|
||||
|
||||
### 14.1 Class Setup
|
||||
|
||||
1. Right-click in Content Browser → **Miscellaneous → Data Asset**
|
||||
2. Select parent class: `PrimaryDataAsset`
|
||||
3. Name: `DA_GameTagRegistry`
|
||||
4. Save to: `Content/Framework/Core/`
|
||||
|
||||
### 14.2 Variables
|
||||
|
||||
Add to Class Defaults:
|
||||
| Variable | Type | Instance Editable | Default | Category |
|
||||
|----------|------|------------------|---------|----------|
|
||||
| `TagNamespace` | `Text` | ✓ | *"Framework tag namespace documentation"* | Documentation |
|
||||
| `bIsFrameworkTag` | `Boolean` | ✓ | `true` | Documentation |
|
||||
| `TagDataTable` | `Data Table` (Object Reference) | ✓ | `DT_ProjectTags` | Config |
|
||||
|
||||
### 14.3 Function Implementations
|
||||
|
||||
#### `GetAllRegisteredTags()` → `Array<GameplayTag>` *(Blueprint Pure)*
|
||||
|
||||
**Purpose:** Returns all tags from the Data Table proxy. This replaces the C++-only `UGameplayTagsManager::RequestAllGameplayTags()`.
|
||||
|
||||
**Node-by-Node Logic:**
|
||||
```
|
||||
[Function: GetAllRegisteredTags] (Pure, no execution pins)
|
||||
Step 1: Get Data Table Row Names (TagDataTable) → returns Array<Name>
|
||||
Step 2: Create empty Array<GameplayTag> → LocalTags
|
||||
Step 3: ForEachLoop over RowNames:
|
||||
├─► Get Data Table Row (TagDataTable, Array Element)
|
||||
│ → Struct Pin: Break GameplayTagTableRow
|
||||
│ → Get "Tag" field (type: GameplayTag)
|
||||
├─► Add "Tag" to LocalTags array
|
||||
Step 4: Return LocalTags
|
||||
```
|
||||
|
||||
**Nodes to Search:** `Get Data Table Row Names`, `ForEachLoop`, `Get Data Table Row`, `Break GameplayTagTableRow`, `Add`, `Array`
|
||||
|
||||
**⚠️ Note:** Since this is a **Pure** function, the ForEachLoop must be inside a **Pure function graph** (which supports loops in UE5). If your UE version doesn't support loops in Pure functions, make this a **BlueprintCallable** (impure) function instead.
|
||||
|
||||
#### `GetTagDisplayName(Tag: GameplayTag)` → `Text` *(Blueprint Pure)*
|
||||
|
||||
**Node-by-Node Logic:**
|
||||
```
|
||||
[Function: GetTagDisplayName]
|
||||
Input Tag → Get Tag Display Name (Tag) → Return
|
||||
```
|
||||
|
||||
**Node Search:** `Get Tag Display Name` — this IS available in Blueprint (part of GameplayTags plugin).
|
||||
|
||||
#### `ValidateTag(Tag: GameplayTag)` → `Boolean` *(Blueprint Pure)*
|
||||
|
||||
**⚠️ UE5 Limitation:** `UGameplayTagsManager::RequestGameplayTag()` is not directly available in BP. Use `Is Gameplay Tag Valid` instead.
|
||||
|
||||
**Node-by-Node Logic:**
|
||||
```
|
||||
[Function: ValidateTag]
|
||||
Step 1: Is Gameplay Tag Valid (Tag)
|
||||
├─► True → Return true
|
||||
└─► False → Print Warning: "Invalid Tag: " + Get Tag Name(Tag) → Return false
|
||||
```
|
||||
|
||||
**Node Search:** `Is Gameplay Tag Valid`, `Get Tag Name`
|
||||
|
||||
**⚠️ Note:** `Is Gameplay Tag Valid` returns true only if the tag is registered in the engine's tag table (which `DT_ProjectTags` populates via Project Settings). Tags NOT in the table will return false.
|
||||
|
||||
#### `LogAllTags()` → *(void)* *(Blueprint Callable)*
|
||||
|
||||
**Editor Only.** Prints all tags from the Data Table to the output log.
|
||||
|
||||
**Node-by-Node Logic:**
|
||||
```
|
||||
[Function: LogAllTags]
|
||||
Step 1: Call GetAllRegisteredTags() → LocalTags
|
||||
Step 2: ForEachLoop (LocalTags):
|
||||
├─► Get Tag Name (Array Element) → ToString → Print String
|
||||
Step 3: Print String: "Total tags: " + Array Length(LocalTags)
|
||||
```
|
||||
|
||||
**Node Search:** `ForEachLoop`, `Get Tag Name`, `ToString (String)`, `Print String`, `Array Length`
|
||||
|
||||
#### `ExportTagNamespace(NamespacePrefix: String)` → `String` *(Blueprint Callable)*
|
||||
|
||||
**Node-by-Node Logic:**
|
||||
```
|
||||
[Function: ExportTagNamespace]
|
||||
Step 1: Call GetAllRegisteredTags() → LocalTags
|
||||
Step 2: Create String variable → Output = ""
|
||||
Step 3: ForEachLoop (LocalTags):
|
||||
├─► Get Tag Name (Array Element) → ToString → TagString
|
||||
├─► Branch: Does TagString start with NamespacePrefix? (use "Starts With" string node)
|
||||
│ True → Append TagString + "\n" to Output (use "Append" or "Build String")
|
||||
└─► Continue
|
||||
Step 4: Return Output
|
||||
```
|
||||
|
||||
**Node Search:** `Starts With (String)`, `Append`, `Build String`, `ForEachLoop`
|
||||
|
||||
### 14.4 External Initialization
|
||||
|
||||
Since Data Assets have no `BeginPlay`, call validation from your GameInstance or StateManager:
|
||||
|
||||
**In `GI_GameFramework.Init()` or `BPC_StateManager.BeginPlay()`:**
|
||||
```
|
||||
Step 1: Get DA_GameTagRegistry (hard reference or Load Asset)
|
||||
Step 2: Call DA_GameTagRegistry.GetAllRegisteredTags() → LocalTags
|
||||
Step 3: Branch: Array Length(LocalTags) == 0
|
||||
├─► True → Print Warning: "No tags in DT_ProjectTags!"
|
||||
└─► False → Print String: "N tags registered: " + Array Length
|
||||
Step 4: [Debug builds only] Call DA_GameTagRegistry.LogAllTags()
|
||||
```
|
||||
|
||||
### 14.5 Networking
|
||||
|
||||
No replication needed. This is a read-only Data Asset with a read-only Data Table reference. All clients load identical copies from disk.
|
||||
|
||||
### 14.6 Blueprint Build Checklist
|
||||
|
||||
- [ ] Create Data Table `DT_ProjectTags` (Row Structure: `GameplayTagTableRow`)
|
||||
- [ ] Populate `DT_ProjectTags` with all framework tags from Section 10
|
||||
- [ ] Add `DT_ProjectTags` to `Project Settings → GameplayTags → Gameplay Tag Table List`
|
||||
- [ ] Create Data Asset `DA_GameTagRegistry` (Parent: `PrimaryDataAsset`)
|
||||
- [ ] Add variables: `TagNamespace` (Text), `bIsFrameworkTag` (Boolean), `TagDataTable` (Data Table Ref → DT_ProjectTags)
|
||||
- [ ] Implement `GetAllRegisteredTags` using Data Table Row iteration
|
||||
- [ ] Implement `GetTagDisplayName` using `Get Tag Display Name` node
|
||||
- [ ] Implement `ValidateTag` using `Is Gameplay Tag Valid` node
|
||||
- [ ] Implement `LogAllTags` (editor-only, prints all tags)
|
||||
- [ ] Implement `ExportTagNamespace` (string prefix filtering)
|
||||
- [ ] Add external initialization call from `GI_GameFramework.Init()` or `BPC_StateManager.BeginPlay()`
|
||||
- [ ] Verify: all tags from Section 10 exist in `DT_ProjectTags`
|
||||
- [ ] Verify: `ValidateTag` returns true for registered tags, false for unregistered
|
||||
|
||||
---
|
||||
|
||||
## 15. Multiplayer Networking
|
||||
|
||||
**Replication: None needed.** This Data Asset is read-only configuration. All clients load identical copies from disk. GameplayTag data lives in `DefaultGameplayTags.ini` which is identical on all instances.
|
||||
|
||||
**Authority: N/A.** No runtime state changes.
|
||||
All framework documentation has been updated to use `DA_GameTagRegistry`. No functionality has changed — only the prefix was corrected.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# FL_GameUtilities — Blueprint Function Library
|
||||
|
||||
> **⚡ C++ Status: Full Implementation** — `Source/PG_Framework/Public/Core/FL_GameUtilities.h` provides all utility functions (subsystem access, math, tags, text, screen, debug). All functions are `BlueprintCallable` — they appear under `Framework|Utilities` in every BP right-click menu. **Nothing to build.** The spec below documents each function's API for reference. The old "Blueprint-Only Alternative" section is kept for context but is no longer needed — the C++ class is the canonical implementation. See `docs/developer/cpp-integration-guide.md` for usage patterns.
|
||||
>
|
||||
> ---
|
||||
|
||||
## Asset Details
|
||||
|
||||
| Property | Value |
|
||||
|
||||
1482
docs/blueprints/01-core/02a_ML_GameUtilities.md
Normal file
1482
docs/blueprints/01-core/02a_ML_GameUtilities.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,9 @@
|
||||
# I_InterfaceLibrary — Interface Collection
|
||||
|
||||
> **⚡ C++ Status: Full Implementation** — All 9 interfaces (`I_Interactable`, `I_Damageable`, `I_Persistable`, `I_Lockable`, `I_Holdable`, `I_UsableItem`, `I_Toggleable`, `I_Adjustable`, `I_Inspectable`) are defined in C++ at `Source/PG_Framework/Public/Core/I_InterfaceLibrary.h` as `BlueprintNativeEvent`. **Do NOT create Blueprint Interface assets.** On any BP actor: Class Settings → Interfaces → Add → type the interface name (e.g., `UInteractable`). Compile, and the interface events appear in your Event Graph. The spec below documents each interface's functions and purpose for reference. See `docs/developer/cpp-integration-guide.md`.
|
||||
>
|
||||
> ---
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines all **9 Blueprint Interfaces** that form the backbone of cross-system communication in the framework. Interfaces are the **preferred communication method** over direct references — they decouple the caller from the callee's concrete class. Any Actor or Component can implement any of these interfaces, and any system can query for them and call them without knowing the underlying type.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 04 — Game Instance Kernel (`GI_GameFramework`)
|
||||
|
||||
> **⚡ C++ Status: Full Implementation** — `Source/PG_Framework/Public/Core/GI_GameFramework.h` provides the complete application kernel: game phase state machine, session flags, save slot management, service resolution, platform initialization, and all event dispatchers. **Create a BP child** (Blueprint Class → All Classes → `GI_GameFramework`) for configuration only — set `TagRegistry` in Class Defaults. Do NOT rebuild the state machine or service registry in BP. See `docs/developer/cpp-integration-guide.md` for exact setup steps.
|
||||
>
|
||||
> ---
|
||||
|
||||
## Purpose
|
||||
The single persistent object that lives for the entire application session. It owns global subsystems, manages save slot ownership, and provides the canonical entry point for service resolution.
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# GM_CoreGameMode — Core GameMode
|
||||
|
||||
---
|
||||
> **⚡ C++ Status: Full Implementation** — `Source/PG_Framework/Public/Core/GM_CoreGameMode.h` provides chapter transitions, death handling, ending triggers, and pause control. **Create a BP child** (Blueprint Class → All Classes → `GM_CoreGameMode`). In Class Defaults → **"Classes"** section (inherited from `AGameModeBase`): set `PlayerControllerClass`, `PlayerStateClass`, `GameStateClass`, and `DefaultPawnClass`. Do NOT redeclare these variables — they're inherited. See `docs/developer/cpp-integration-guide.md` for exact setup steps.
|
||||
>
|
||||
> ---
|
||||
|
||||
## Asset Details
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 06 — Core GameState (`GS_CoreGameState`)
|
||||
|
||||
> **⚡ C++ Status: Full Implementation** — `Source/PG_Framework/Public/Core/GS_CoreGameState.h` provides 5 replicated state variables (ElapsedPlayTime, ActiveChapterTag, ActiveNarrativePhase, bEncounterActive, ActiveObjectiveTags) with `OnRep_` handlers and dispatchers. **Create a BP child** (Blueprint Class → All Classes → `GS_CoreGameState`). No logic to add — all replication works out of the box. Set in `BP_CoreGameMode` → Class Defaults → Classes → `Game State Class`. See `docs/developer/cpp-integration-guide.md`.
|
||||
>
|
||||
> ---
|
||||
|
||||
## Purpose
|
||||
The single replicated game state object visible to all players (and potential future co-op clients). Tracks session-level progression data such as elapsed play time, active chapter, narrative phase, encounter activity, and active objectives.
|
||||
|
||||
|
||||
@@ -3,9 +3,261 @@
|
||||
## Purpose
|
||||
The single source of truth for every item in the game. Each item is represented by one `DA_ItemData` Primary Data Asset. No item data lives in Blueprint logic; all item definitions are data-driven from these assets.
|
||||
|
||||
**C++ Status:** ✅ Full — `Source/PG_Framework/Public/Inventory/DA_ItemData.h` + `.cpp`. All enums, structs, variables, and editor validation are implemented in C++ with `UPROPERTY` metadata (EditCondition, EditConditionHides, ClampMin/Max) for designer-friendly editor UX.
|
||||
|
||||
> **Concrete examples:** See [`docs/game/`](../../game/README.md) for step-by-step walkthroughs of building actual items: [Flashlight Tool](../../game/item-flashlight.md), [Pistol Weapon](../../game/item-pistol.md), [MedKit Consumable](../../game/item-medkit.md), [Keycard Key Item](../../game/item-keycard.md). Each shows exact Blueprint graphs, component lists, and verification checklists.
|
||||
|
||||
> **Critical distinction:** `DA_ItemData` is a **Data Asset** — it lives in the Content Browser, NOT in the level. It defines *what* an item is (name, weight, icon, effects, type). To place an item in the world, create a `BP_ItemPickup` actor (spec #25) and assign the `DA_ItemData` to it. The Data Asset is the item's *identity card*; `BP_ItemPickup` is its *physical body* in the world. See [How to Set Up](#how-to-set-up) below.
|
||||
|
||||
---
|
||||
|
||||
## How to Set Up
|
||||
|
||||
### Step 1 — Create the Data Asset
|
||||
|
||||
```
|
||||
Content Browser → Framework/DataAssets/Items/
|
||||
Right-click → Miscellaneous → Data Asset
|
||||
Class: DA_ItemData
|
||||
Name: DA_Item_[ShortName] (e.g., DA_Item_MedKit, DA_Item_RustyKey, DA_Item_Pistol)
|
||||
```
|
||||
|
||||
### Step 2 — Fill in Core Properties
|
||||
|
||||
Open the Data Asset and fill in:
|
||||
|
||||
| Property | Purpose | Example |
|
||||
|----------|---------|---------|
|
||||
| `Item Tag` | **Unique identifier** — registered in `DA_GameTagRegistry` | `Framework.Item.Consumable.MedKit` |
|
||||
| `Display Name` | Player-visible name | "MedKit" |
|
||||
| `Description` | Flavor text / gameplay hint | "A standard medical kit. Restores 25 health." |
|
||||
| `Icon` | Inventory thumbnail texture | `T_MedKit_Icon` |
|
||||
| `World Mesh` | 3D mesh when dropped in world | `SM_MedKit` |
|
||||
| `Weight` | Weight units for carry limit | `1.0` |
|
||||
| `Stack Limit` | Max per slot (weapons = 1, ammo = 999, consumables = 5) | `5` |
|
||||
| `Item Type` | Category enum — controls which sub-data to fill | `Consumable` |
|
||||
|
||||
### Step 3 — Fill Type-Specific Sub-Data
|
||||
|
||||
Based on `Item Type`, fill the relevant sub-struct (other sub-panels auto-hide via `EditCondition`):
|
||||
|
||||
| Item Type | Sub-Struct to Fill | Example Values |
|
||||
|-----------|-------------------|-----------------|
|
||||
| `Weapon` / `Tool` | `Equipment Data` | Damage=0, FireRate=0, MagazineSize=0, Slot=PrimaryWeapon |
|
||||
| `Consumable` | `Consumable Data` | HealthRestore=25, UseDuration=2.0, bConsumedOnUse=true |
|
||||
| `Ammo` | `AmmoData` (legacy) | AmmoTypeTag, PerPickupCount |
|
||||
| `KeyItem`, `Document`, `Collectible`, `Resource`, `Misc` | None — core properties only | |
|
||||
|
||||
### Step 4 — Build the BP_ItemPickup Blueprint (one-time)
|
||||
|
||||
The Data Asset does **not** appear in the world. You need a Blueprint Actor that references it. Here is the exact setup:
|
||||
|
||||
#### 4.1 — Create the Blueprint
|
||||
|
||||
```
|
||||
Content Browser → Framework/Inventory/
|
||||
Right-click → Blueprint Class → Actor
|
||||
Name: BP_ItemPickup
|
||||
```
|
||||
|
||||
#### 4.2 — Add Components
|
||||
|
||||
Open `BP_ItemPickup` → **Viewport** tab → **Add Component** (top-left button):
|
||||
|
||||
| # | Component Class | Name | Purpose |
|
||||
|---|----------------|------|---------|
|
||||
| 1 | `StaticMeshComponent` | `Mesh` | Renders the item's 3D model in the world |
|
||||
| 2 | `SphereComponent` | `InteractionCollision` | Detects when the player is near enough to pick up |
|
||||
| 3 | *(optional)* `AudioComponent` | `PickupSound` | Plays a sound when collected |
|
||||
|
||||
**Select `InteractionCollision` → Details panel:**
|
||||
- `Sphere Radius` → `150.0` (how close the player must be)
|
||||
- Under **Collision Presets** → set to `OverlapOnlyPawn` (only the player triggers it)
|
||||
|
||||
#### 4.3 — Create the Config Variable
|
||||
|
||||
1. In the **My Blueprint** panel (left side), click **+ Variable**
|
||||
2. Name: `Config`
|
||||
3. Variable Type: **Struct → `S_PickupConfig`** (you may need to create this struct first — see spec #25 for its fields)
|
||||
|
||||
If `S_PickupConfig` doesn't exist yet, create it manually:
|
||||
```
|
||||
Content Browser → Framework/Inventory/
|
||||
Right-click → Blueprints → Structure
|
||||
Name: S_PickupConfig
|
||||
Add fields:
|
||||
ItemData → Type: DA_ItemData (Object Reference)
|
||||
Quantity → Type: Integer, default: 1
|
||||
bAutoRotate → Type: Boolean, default: true
|
||||
bBobUpDown → Type: Boolean, default: true
|
||||
BobAmplitude → Type: Float, default: 10.0
|
||||
BobFrequency → Type: Float, default: 1.0
|
||||
```
|
||||
|
||||
4. After creating the variable, select it → Details panel:
|
||||
- **Instance Editable** → ✓ checked (so you can set it per-instance in the level)
|
||||
- **Category** → `Pickup Config`
|
||||
- **Tooltip** → "The item data asset this pickup represents"
|
||||
|
||||
#### 4.4 — Wire the Construction Script
|
||||
|
||||
Go to the **Construction Script** tab (or open the Construction Script graph). This runs every time you change the Config in the editor, so the mesh updates immediately.
|
||||
|
||||
```
|
||||
Construction Script
|
||||
│
|
||||
├─ Get Config → Break S_PickupConfig → ItemData
|
||||
│ │
|
||||
│ ├─ Is Valid (ItemData)?
|
||||
│ │ ├─ True → continue
|
||||
│ │ └─ False → Print String "No ItemData assigned" (Editor-only) → Return
|
||||
│ │
|
||||
│ └─ ItemData → Get World Mesh (this is a soft reference — may not be loaded)
|
||||
│ │
|
||||
│ ├─ Is Valid (WorldMesh)?
|
||||
│ │ ├─ True:
|
||||
│ │ │ → Set Static Mesh (Mesh component, New Mesh = WorldMesh)
|
||||
│ │ │ → ItemData → Get Display Name → Set Actor Label
|
||||
│ │ └─ False:
|
||||
│ │ → ItemData → World Mesh → Async Load Asset
|
||||
│ │ → On Load Complete → Set Static Mesh
|
||||
│ │
|
||||
│ └─ ItemData → Get Icon → (cache for UI prompt)
|
||||
```
|
||||
|
||||
**Key node-by-node:**
|
||||
1. Drag the `Config` variable from My Blueprint → **Get Config**
|
||||
2. **Break S_PickupConfig** (right-click the pin → Split Struct Pin, or drag off → "Break")
|
||||
3. From the `ItemData` pin, drag off → Create a `IsValid` node
|
||||
4. From the valid `ItemData` output, drag off → type `Get World Mesh` (this appears because `WorldMesh` is a `UPROPERTY` on `DA_ItemData`; if using soft reference, you'll get a `TSoftObjectPtr<UStaticMesh>` — connect to **Async Load Asset** or **Load Synchronous** for Construction Script)
|
||||
5. From the resolved mesh, connect to `Set Static Mesh` node → target is the `Mesh` component (drag Mesh from the Components panel into the graph → Get)
|
||||
|
||||
#### 4.5 — Wire Interaction (I_Interactable)
|
||||
|
||||
**Step A — Add the Interface:**
|
||||
1. **Class Settings** (toolbar button) → **Interfaces** → **Add** → select `UInteractable` (the C++ interface from `I_InterfaceLibrary.h`)
|
||||
2. Compile. Now `Interact`, `Can Interact`, `Get Interaction Prompt` events appear in the right-click menu.
|
||||
|
||||
**Step B — Wire the Events:**
|
||||
|
||||
```
|
||||
Event Interact (from I_Interactable)
|
||||
│ Interactor: AActor*
|
||||
│
|
||||
├─ Get Config → Break → ItemData
|
||||
│
|
||||
├─ Interactor → Get Component by Class (BPC_InventorySystem)
|
||||
│ │
|
||||
│ └─ Is Valid?
|
||||
│ ├─ False → Return (no inventory system on interactor)
|
||||
│ └─ True → InventorySystem
|
||||
│ │
|
||||
│ ├─ Call Can Add Item(ItemData, Quantity from Config)
|
||||
│ │ │
|
||||
│ │ ├─ False → Print "Inventory Full" → Return
|
||||
│ │ └─ True:
|
||||
│ │ ├─ Call Add Item(ItemData, Quantity)
|
||||
│ │ ├─ Play Sound at Location (PickupSound, GetActorLocation)
|
||||
│ │ ├─ Destroy Actor
|
||||
│ │ └─ Return
|
||||
│ │
|
||||
│ └─ (If ItemData has bIsInfinite or respawn logic — skip Destroy)
|
||||
|
||||
Event Can Interact (from I_Interactable)
|
||||
│ Interactor, OutBlockReason (by ref)
|
||||
│
|
||||
├─ Is Config.ItemData Valid?
|
||||
│ ├─ False → OutBlockReason = "No item data" → Return False
|
||||
│ └─ True → Return True
|
||||
|
||||
Event Get Interaction Prompt (from I_Interactable)
|
||||
│
|
||||
├─ Get Config → ItemData → Get Display Name
|
||||
└─ Return "Pick up [ItemName]"
|
||||
```
|
||||
|
||||
#### 4.6 — Add Bobbing Rotation (optional polish)
|
||||
|
||||
In **Event Graph → Event BeginPlay**:
|
||||
|
||||
```
|
||||
Event BeginPlay
|
||||
│
|
||||
├─ Get Config → Break → bAutoRotate
|
||||
│ └─ True → Add Timeline → name: "BobAndRotate"
|
||||
│ ├─ Update track:
|
||||
│ │ ├─ Mesh → Add World Rotation (DeltaRotation = 0,0,Timeline.Rotation * 90)
|
||||
│ │ └─ Mesh → Add World Offset (Delta = 0,0,Timeline.Bob * BobAmplitude * sin(time), ...)
|
||||
│ │
|
||||
│ └─ Timeline: float track 0→1 looping over 2 seconds
|
||||
├─ Enable collision on InteractionCollision
|
||||
└─ Bind OnComponentBeginOverlap(InteractionCollision) → Highlight mesh, show prompt
|
||||
```
|
||||
|
||||
#### 4.7 — Compile and Use
|
||||
|
||||
1. **Compile → Save**
|
||||
2. **Drag `BP_ItemPickup` into your level**
|
||||
3. Select the instance → Details panel → **Pickup Config → Item Data** → select `DA_Item_MedKit`
|
||||
4. Set `Quantity` → `1` (or more for stacked items)
|
||||
5. The **Construction Script** runs immediately — the mesh should appear in the viewport
|
||||
6. Position the actor where you want it
|
||||
|
||||
**Verification in PIE (Play In Editor):**
|
||||
- The pickup should show its mesh, slowly rotating
|
||||
- Walk up to it — the overlap event fires
|
||||
- Press Interact — item goes into inventory, pickup disappears
|
||||
- Open inventory → item should be in a slot with the correct name and icon
|
||||
|
||||
```
|
||||
DA_Item_MedKit (Content Browser) ← defines IDENTITY
|
||||
↑ referenced by
|
||||
BP_ItemPickup (Actor in level) ← physical BODY in world
|
||||
├── StaticMeshComponent "Mesh" ← renders WorldMesh from Data Asset
|
||||
├── SphereComponent "Collision" ← detects player proximity
|
||||
└── Config struct ← holds pointer to DA_ItemData + quantity
|
||||
↑ interacts via
|
||||
BPC_InteractionDetector → I_Interactable → BPC_InventorySystem.AddItem()
|
||||
```
|
||||
|
||||
> **For concrete, step-by-step examples** of building specific item types (flashlight, pistol, medkit, keycard) with complete Blueprint graphs, component lists, and wiring diagrams, see the [`docs/game/`](../../game/README.md) directory.
|
||||
|
||||
---
|
||||
|
||||
## Single Identity Tag — Why One Tag Is Enough
|
||||
|
||||
Each item has exactly one `ItemTag` GameplayTag. This is intentional — the tag is the item's **unique identifier**, not a categorization bucket.
|
||||
|
||||
Categorization and behavioral flags are handled by separate, purpose-built properties:
|
||||
|
||||
| Concern | Mechanism | Example |
|
||||
|---------|-----------|---------|
|
||||
| **Identity** | `ItemTag` (single GameplayTag) | `Framework.Item.Consumable.MedKit` |
|
||||
| **Category** | `ItemType` (enum) | Consumable, Weapon, KeyItem, Document, etc. |
|
||||
| **Is non-droppable?** | `bIsKeyItem` (bool) | Key items are auto-protected from drop/clear |
|
||||
| **Can be dropped?** | `bCanBeDropped` (bool) | Ammo/documents can be dropped; key items cannot |
|
||||
| **Has 3D inspection?** | `bHasInspectMode` (bool) | Enables rotate-examine on pickup |
|
||||
| **Crafting relationships** | `CombinesWith` (array of GameplayTag) | `MedKit` + `Bandage` → `SuperMedKit` |
|
||||
| **Project-specific data** | `CustomProperties` (TMap<FName, FString>) | Key=`"QuestRewardID"`, Value=`"Q12"` |
|
||||
| **Tag-based queries** | Hierarchical tag structure | `ItemTag.MatchesTag(Framework.Item.Consumable)` returns true for all consumables |
|
||||
|
||||
**Why not multiple ItemTags?**
|
||||
- A single unique ID prevents ambiguity: inventory lookups, save/load references, and crafting recipes all need one canonical name for each item.
|
||||
- Hierarchical GameplayTags support partial matching: `Framework.Item.Consumable.MedKit` matches `Framework.Item.Consumable` for category-wide queries.
|
||||
- Adding a `FGameplayTagContainer ItemTags` would create confusion — which tag is the "real" ID? Do you check all of them? Do you sort by the first one?
|
||||
- The `ItemType` enum + boolean flags + `CombinesWith` array already cover every use case without tag collision risks.
|
||||
|
||||
**If you truly need multiple filtering tags**, the `CustomProperties` map can store them (e.g., `"FilterTags"` → `"Flammable,Magnetic,QuestItem"`), and your BP can parse the comma-separated string at runtime. But in practice, this is rarely needed because:
|
||||
- `ItemType` already categorizes items
|
||||
- `bIsKeyItem` / `bCanBeDropped` already handle behavioral flags
|
||||
- `CombinesWith` already handles cross-item relationships
|
||||
- Hierarchical tag matching handles category queries (e.g., `MatchesTag(Framework.Item.Weapon)`)
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
- **Requires:** `GameplayTag` system (`GI_GameTagRegistry`), `UPrimaryDataAsset` (engine base)
|
||||
- **Required By:** `BPC_InventorySystem`, `BPC_EquipmentSlotSystem`, `BPC_ConsumableSystem`, `BPC_AmmoResourceSystem`, `BPC_ItemCombineSystem`, `BPC_KeyItemSystem`, `BPC_ActiveItemSystem`, `DA_ItemDatabase` (collection)
|
||||
- **Requires:** `GameplayTag` system (`DA_GameTagRegistry`), `UPrimaryDataAsset` (engine base)
|
||||
- **Required By:** `BPC_InventorySystem`, `BPC_EquipmentSlotSystem`, `BPC_ConsumableSystem`, `BPC_AmmoComponent` (70), `BPC_ItemCombineSystem`, `BPC_KeyItemSystem`, `BPC_ActiveItemSystem`
|
||||
- **Engine/Plugin Requirements:** `GameplayTags`, `AssetManager` (Primary Data Asset registration)
|
||||
- **Parent Class:** `UPrimaryDataAsset`
|
||||
|
||||
@@ -13,9 +265,10 @@ The single source of truth for every item in the game. Each item is represented
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Parent Class** | `UPrimaryDataAsset` |
|
||||
| **Class Type** | Blueprint Function Library (Data Asset) |
|
||||
| **Asset Path** | `Content/Data/Items/DA_Item_[Name]` |
|
||||
| **Implements Interfaces** | None |
|
||||
| **C++ Class** | `UDA_ItemData` (in `Source/PG_Framework/Public/Inventory/DA_ItemData.h`) |
|
||||
| **Class Type** | Primary Data Asset |
|
||||
| **Asset Path** | `Content/Framework/DataAssets/Items/DA_Item_[Name]` |
|
||||
| **Implements Interfaces** | None (passive data container) | |
|
||||
|
||||
---
|
||||
|
||||
@@ -87,19 +340,28 @@ No custom structs defined in this Data Asset class. Consumed structs from other
|
||||
|
||||
## 4. Functions
|
||||
|
||||
No runtime functions. This is a pure data container. The only accessible operations are direct variable reads from Blueprints that hold a reference to the asset.
|
||||
`DA_ItemData` is a pure data container. All properties are read directly from Blueprints that hold a reference to the asset. The following functions exist in the C++ class (`Source/PG_Framework/Public/Inventory/DA_ItemData.h`):
|
||||
|
||||
### Runtime (All BlueprintCallable / BlueprintPure)
|
||||
|
||||
| Function | Returns | Description |
|
||||
|----------|---------|-------------|
|
||||
| `GetResistance(DamageType)` (on `DA_EquipmentConfig`) | `float` | Returns resistance value for a damage type (armor/equipment) |
|
||||
| *(All UPROPERTY reads)* | — | `ItemTag`, `DisplayName`, `ItemType`, `Weight`, `StackLimit`, etc. are read directly — no function call needed |
|
||||
|
||||
### Editor-Only Functions (for content team validation)
|
||||
|
||||
#### `ValidateItemData` → `Bool` (BlueprintCallable, Editor only)
|
||||
- **C++ Implementation:** Full — `Source/PG_Framework/Private/Inventory/DA_ItemData.cpp`
|
||||
- **Description:** Runs editor validation checks (tag uniqueness, required fields filled).
|
||||
- **Parameters:** None
|
||||
- **Parameters:** `OutErrors` (FString, out) — human-readable error messages
|
||||
- **Flow:**
|
||||
1. Check `ItemTag != None` — if None, log warning
|
||||
2. Check `DisplayName != ""` — if empty, log warning
|
||||
3. If `StackLimit < 1`, reset to 1
|
||||
4. If `bIsKeyItem` then `bCanBeDropped = false` (forced)
|
||||
5. Return true if all validations pass
|
||||
1. Check `ItemTag != None` — if None, append error
|
||||
2. Check `DisplayName != ""` — if empty, append error
|
||||
3. If `StackLimit < 1`, append error
|
||||
4. If `bIsKeyItem` then warn if `bCanBeDropped == true`
|
||||
5. If Consumable, check at least one effect value > 0
|
||||
6. Return true if all validations pass; false if any failed
|
||||
|
||||
---
|
||||
|
||||
@@ -115,21 +377,40 @@ No event overrides. Data Assets do not tick or have BeginPlay.
|
||||
|
||||
---
|
||||
|
||||
## 7. Blueprint Graph Logic Flow
|
||||
## 7. Content Creation Flow
|
||||
|
||||
No blueprint graph. This asset is created and edited in the Content Browser via "Create Advanced Asset -> Blueprint -> Data Asset -> DA_ItemData".
|
||||
`DA_ItemData` has **no blueprint graph**. It is created and edited in the Content Browser as a Data Asset instance.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Content Browser] --> B[Right-click > Data Asset > DA_ItemData]
|
||||
B --> C[Name: DA_Item_MedKit]
|
||||
C --> D[Fill ItemTag: Item.MedKit]
|
||||
D --> E[Fill DisplayName: Med Kit]
|
||||
E --> F[Assign Icon, Mesh]
|
||||
F --> G[Set ItemType: Consumable]
|
||||
G --> H[Fill ConsumableData]
|
||||
H --> I[Save Asset]
|
||||
I --> J[Registered in Asset Manager]
|
||||
A[Content Browser] --> B[Right-click > Miscellaneous > Data Asset]
|
||||
B --> C[Select Class: DA_ItemData]
|
||||
C --> D[Name: DA_Item_MedKit]
|
||||
D --> E[Open Asset > Fill Properties]
|
||||
E --> F[ItemTag: Framework.Item.Consumable.MedKit]
|
||||
F --> G[DisplayName: Med Kit]
|
||||
G --> H[Set Icon, WorldMesh]
|
||||
H --> I[ItemType: Consumable]
|
||||
I --> J[Fill ConsumableData: HealthRestore=25]
|
||||
J --> K[Save Asset]
|
||||
K --> L[Ready — referenced by BP_ItemPickup actors]
|
||||
```
|
||||
|
||||
### How Systems Use the Data Asset (Read-Only)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
DA[DA_Item_MedKit<br/>Data Asset] -->|referenced by| PICKUP[BP_ItemPickup<br/>Actor in world]
|
||||
PICKUP -->|OnInteract| INV[BPC_InventorySystem.AddItem(DA, 1)]
|
||||
INV -->|stores reference| SLOT[FInventorySlot.Item = DA]
|
||||
|
||||
DA -->|reads EquipmentData| EQUIP[BPC_EquipmentSlotSystem]
|
||||
DA -->|reads ConsumableData| CONSUM[BPC_ConsumableSystem]
|
||||
DA -->|reads CombinesWith| COMBINE[BPC_ItemCombineSystem]
|
||||
DA -->|reads bIsKeyItem| KEY[BPC_KeyItemSystem]
|
||||
DA -->|reads ItemType| ACTIVE[BPC_ActiveItemSystem<br/>routes to correct handler]
|
||||
|
||||
INV -->|dispatches OnItemAdded| UI[WBP_InventoryMenu<br/>reads DisplayName, Icon, Description]
|
||||
```
|
||||
|
||||
---
|
||||
@@ -170,15 +451,18 @@ flowchart LR
|
||||
|
||||
## 10. Reuse Notes
|
||||
|
||||
- Create one `DA_ItemData` per item. Name convention: `DA_Item_[ShortName]` (e.g. `DA_Item_MedKit`, `DA_Item_Flashlight`, `DA_Item_KeyCard_Omega`).
|
||||
- All `ItemTag` values must be registered in `GI_GameTagRegistry` before use.
|
||||
- The `CustomProperties` map future-proofs any per-project additions without modifying the base asset class.
|
||||
- **Create one `DA_ItemData` per item.** Naming convention: `DA_Item_[ShortName]` (e.g., `DA_Item_MedKit`, `DA_Item_Flashlight`, `DA_Item_KeyCard_Omega`).
|
||||
- **Data Assets are not world actors.** They cannot be dragged into a level. To place an item in the world, create a `BP_ItemPickup` actor (spec #25) and assign the `DA_ItemData` to its `Config.ItemData` property.
|
||||
- All `ItemTag` values must be registered in `DA_GameTagRegistry` before use. Use the hierarchical tag structure: `Framework.Item.[Type].[Name]`.
|
||||
- The `CustomProperties` map future-proofs any per-project additions without modifying the base C++ class.
|
||||
- For non-stackable items (weapons, key items, tools), set `StackLimit = 1`.
|
||||
- For stacks, choose a sensible `StackLimit` (e.g. ammo = 999, consumables = 5).
|
||||
- For stacks, choose a sensible `StackLimit` (e.g., ammo = 999, consumables = 5, resources = 99).
|
||||
- The `AssetManager` should be configured with `PrimaryAssetType = Item` and `PrimaryAssetLabel = Item` for async loading support.
|
||||
- Document items (`E_ItemType.Document`) additionally populate `BPC_DocumentArchiveSystem` with their content; the `Description` field serves as document body text.
|
||||
- To add a new item property across all items, add a new variable to `DA_ItemData`. Do not create a separate asset class per item type — use the `ItemType` enum for branching logic.
|
||||
- To add a new item property across all items, add a new `UPROPERTY` to `DA_ItemData` in C++. Do not create a separate asset class per item type — use the `ItemType` enum + `EditCondition` metadata for branching display.
|
||||
- The `ValidateItemData` editor function can be exposed as a Python command for batch validation on check-in.
|
||||
- **C++ EditCondition metadata** auto-hides irrelevant panels based on `ItemType` (e.g., `ConsumableData` only shows when `ItemType == Consumable`). Designers never see irrelevant fields.
|
||||
- **Inventory systems interact with `DA_ItemData` through C++ `TObjectPtr<UDA_ItemData>`** references — the Data Asset is never copied, only referenced by pointer. All inventory operations pass the `DA_ItemData*` pointer.
|
||||
|
||||
---
|
||||
|
||||
|
||||
406
docs/blueprints/01-core/150_BPC_PlatformServiceAbstraction.md
Normal file
406
docs/blueprints/01-core/150_BPC_PlatformServiceAbstraction.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# 150 — Platform Service Abstraction (`BPC_PlatformServiceAbstraction`)
|
||||
|
||||
> **Blueprint-Only Implementation** — UE 5.5–5.7 supports all platform detection and SDK routing from Blueprints via `UGameplayStatics::GetPlatformName()`, conditional compilation checks, and plugin API calls. This component is the **single source of truth** for platform identity, replacing the three fragmented detection systems that currently exist across the framework.
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
Central platform authority attached to the Game Instance. Detects the platform once at startup and provides a unified `EPlatformFamily` enum that ALL subsystems query instead of detecting platform independently. Routes platform-specific SDK calls (achievement submission to Steam/PSN/Xbox Live/Nintendo, cloud saves, overlays, input device profiles). Returns stub/no-op implementations on unsupported platforms so game code never needs `#if PLATFORM_PS5` checks.
|
||||
|
||||
## The Problem Being Solved
|
||||
|
||||
Currently the framework has **three independent platform detection systems**, each calling `GetPlatformName()` separately:
|
||||
|
||||
| System | Enum | Platform Families |
|
||||
|--------|------|-------------------|
|
||||
| `BPC_RenderPipelineManager` (149) | `EPlatformFamily` | PS5, PS4, Xbox_Series, Xbox_One, Switch, PC_High, PC_Low, SteamDeck |
|
||||
| `SS_EnhancedInputManager` (128) | `E_InputPlatform` | PC_KeyboardMouse, Xbox, PS5_DualSense |
|
||||
| `BPC_HapticsController` (148) | `EControllerPlatform` | PC_Generic, Xbox, PS5_DualSense, PS4_DualShock |
|
||||
|
||||
These are **different enums with different values** and don't talk to each other. If you change platform in one system, nothing cascades.
|
||||
|
||||
**After this component is integrated, all three systems query `BPC_PlatformServiceAbstraction.GetPlatformFamily()` instead of detecting platform themselves. The old enums are deprecated and mapped to `EPlatformFamily`.**
|
||||
|
||||
## Dependencies
|
||||
- **Requires:** [`GI_GameFramework`](../01-core/04_GI_GameFramework.md) (attached as component), [`DA_GameTagRegistry`](../01-core/01_DA_GameTagRegistry.md) (platform tag validation)
|
||||
- **Required By:** [`BPC_RenderPipelineManager`](../12-settings/149_BPC_RenderPipelineManager.md) (GFX quality profile selection), [`SS_EnhancedInputManager`](../15-input/128_SS_EnhancedInputManager.md) (input device profile selection), [`BPC_HapticsController`](../12-settings/148_BPC_HapticsController.md) (controller platform detection), [`SS_AchievementSystem`](../11-meta/103_SS_AchievementSystem.md) (SDK routing), [`SS_SettingsSystem`](105_SS_SettingsSystem.md) (platform-aware defaults)
|
||||
- **Engine/Plugin Requirements:** GameplayTags, `UGameplayStatics`, Platform SDK plugins (Steamworks, PSN, Xbox GDK, Nintendo SDK — all optional)
|
||||
|
||||
## Class Info
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Parent Class** | `ActorComponent` |
|
||||
| **Class Type** | Blueprint Component |
|
||||
| **Asset Path** | `Content/Framework/Core/BPC_PlatformServiceAbstraction` |
|
||||
| **Implements Interfaces** | None |
|
||||
| **Attachment** | Game Instance (`GI_GameFramework`) |
|
||||
|
||||
---
|
||||
|
||||
## 1. Enums
|
||||
|
||||
### `EPlatformFamily` (UNIFIED — all systems use this)
|
||||
|
||||
| Value | Description | Input Device | Render Pipeline | Achievements SDK | Store |
|
||||
|-------|-------------|-------------|-----------------|-----------------|-------|
|
||||
| `Unknown = 0` | Not yet detected | — | — | — | — |
|
||||
| `PC_Win64 = 1` | Windows PC (any GPU) | KB+M / Xbox / PS5 gamepad | Detected per GPU capability | Steam / EGS | Steam / EGS |
|
||||
| `PC_Linux = 2` | Linux / SteamOS | Same as PC | Vulkan | Steam | Steam |
|
||||
| `PS5 = 3` | PlayStation 5 | DualSense | Lumen + Nanite | PSN Trophies | PS Store |
|
||||
| `PS5_Pro = 4` | PlayStation 5 Pro | DualSense | Lumen + PSSR | PSN Trophies | PS Store |
|
||||
| `PS4 = 5` | PlayStation 4 / 4 Pro | DualShock 4 | Baked only | PSN Trophies | PS Store |
|
||||
| `Xbox_Series = 6` | Xbox Series X\|S | Xbox Wireless | Lumen + Nanite | Xbox Live | MS Store |
|
||||
| `Xbox_One = 7` | Xbox One / One X | Xbox Wireless | Baked only | Xbox Live | MS Store |
|
||||
| `Switch = 8` | Nintendo Switch | Joy-Con / Pro | Baked + Proxy | Nintendo | eShop |
|
||||
| `Switch_2 = 9` | Nintendo Switch 2 | Joy-Con 2 / Pro 2 | Baked + CSM + FSR | Nintendo | eShop |
|
||||
| `SteamDeck = 10` | Steam Deck | Built-in / external | Baked/SSGI + CSM | Steam | Steam |
|
||||
| `Mac = 11` | macOS | KB+M / gamepad | Metal / Baked | Steam | Steam / App Store |
|
||||
|
||||
### `EPlatformSDK`
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `None = 0` | No SDK available (editor, generic build) |
|
||||
| `Steam = 1` | Steamworks SDK (achievements, cloud, overlay, stats) |
|
||||
| `PSN = 2` | PlayStation Network SDK (trophies, cloud, overlay, invites) |
|
||||
| `XboxLive = 3` | Xbox Live / GDK (achievements, cloud, overlay, invites) |
|
||||
| `Nintendo = 4` | Nintendo SDK (achievements, cloud, overlay) |
|
||||
| `EGS = 5` | Epic Games Store SDK |
|
||||
|
||||
### `EPlatformOverlayType`
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `Achievements = 0` | Show platform achievements/trophies page |
|
||||
| `Friends = 1` | Open friends list |
|
||||
| `Invite = 2` | Send game invite |
|
||||
| `Store = 3` | Open store page (DLC, etc.) |
|
||||
| `SocialFeed = 4` | Community / social feed |
|
||||
|
||||
---
|
||||
|
||||
## 2. Structs
|
||||
|
||||
### `SPlatformCapabilities`
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `Platform` | `EPlatformFamily` | Current platform |
|
||||
| `SDK` | `EPlatformSDK` | Available platform SDK |
|
||||
| `bSupportsLumen` | `bool` | Platform GPU supports Lumen GI |
|
||||
| `bSupportsNanite` | `bool` | Platform GPU supports Nanite |
|
||||
| `bSupportsHWRT` | `bool` | Platform supports hardware ray tracing |
|
||||
| `bSupportsCloudSaves` | `bool` | Platform has cloud save API |
|
||||
| `bSupportsAchievements` | `bool` | Platform has achievement/trophy system |
|
||||
| `bSupportsOverlay` | `bool` | Platform has in-game overlay |
|
||||
| `bSupportsRichPresence` | `bool` | Platform supports rich presence / "now playing" |
|
||||
| `bSupportsUserGeneratedContent` | `bool` | Platform supports UGC/mod sharing |
|
||||
| `bIsConsole` | `bool` | Is this a console platform (certification required)? |
|
||||
| `bIsHandheld` | `bool` | Is this a handheld device? |
|
||||
| `DefaultInputType` | `FName` | Default input device for this platform ("Gamepad", "Keyboard", "Touch") |
|
||||
| `MaxLocalPlayers` | `int32` | Maximum local split-screen players |
|
||||
| `StoreID` | `FString` | Platform store ID for DLC checks |
|
||||
|
||||
---
|
||||
|
||||
## 3. Variables
|
||||
|
||||
### Configuration (Instance Editable)
|
||||
| Variable | Type | Default | Category | Description |
|
||||
|----------|------|---------|----------|-------------|
|
||||
| `bAutoDetectPlatform` | `bool` | `true` | `Config` | Auto-detect on Initialize |
|
||||
| `OverridePlatform` | `EPlatformFamily` | `Unknown` | `Config` | Force a platform (for testing in editor) |
|
||||
| `bEnableSDKIntegration` | `bool` | `true` | `Config` | Enable platform SDK calls |
|
||||
|
||||
### Internal (Private)
|
||||
| Variable | Type | Default | Category | Description |
|
||||
|----------|------|---------|----------|-------------|
|
||||
| `CurrentPlatform` | `EPlatformFamily` | `Unknown` | `State` | Detected or overridden platform |
|
||||
| `CurrentSDK` | `EPlatformSDK` | `None` | `State` | Active platform SDK |
|
||||
| `PlatformCapabilities` | `SPlatformCapabilities` | — | `State` | Full capabilities snapshot |
|
||||
| `bIsInitialized` | `bool` | `false` | `State` | Whether Initialize completed |
|
||||
| `CachedGameInstance` | `GI_GameFramework` | `None` | `Cache` | Owner Game Instance reference |
|
||||
|
||||
---
|
||||
|
||||
## 4. Functions
|
||||
|
||||
### Public Functions
|
||||
|
||||
#### `Initialize` → `void`
|
||||
- **Description:** Detect platform, resolve SDK, build capabilities, broadcast ready signal. Called once at Game Instance startup.
|
||||
- **Flow:**
|
||||
1. Get Owner → Cast to `GI_GameFramework` → cache
|
||||
2. If `OverridePlatform != Unknown`: use override
|
||||
3. Else if `bAutoDetectPlatform`: call `DetectPlatform()`
|
||||
4. Resolve which SDK is available: check plugin state, build configuration
|
||||
5. Build `SPlatformCapabilities` for detected platform
|
||||
6. Register platform in `DA_GameTagRegistry` as `Framework.Platform.{Name}`
|
||||
7. Set `bIsInitialized = true`
|
||||
8. Broadcast `OnPlatformReady(CurrentPlatform)`
|
||||
9. Broadcast `OnPlatformCapabilitiesReady(PlatformCapabilities)`
|
||||
|
||||
#### `DetectPlatform` → `EPlatformFamily`
|
||||
- **Description:** Detects the current platform using UE5 APIs.
|
||||
- **Flow:**
|
||||
1. `UGameplayStatics::GetPlatformName()` → switch:
|
||||
- "PS5" → check if Pro model available → `PS5_Pro` or `PS5`
|
||||
- "PS4" → `PS4`
|
||||
- "XboxOne" → `Xbox_One`
|
||||
- "XSX" or "XboxAnaconda" → `Xbox_Series` (check if S model → adjust capabilities)
|
||||
- "Switch" → check model revision → `Switch_2` or `Switch`
|
||||
- "Win64" → check if running on Steam Deck (`IsSteamDeck()`) → `SteamDeck`
|
||||
- "Win64" → `PC_Win64`
|
||||
- "Linux" → `PC_Linux`
|
||||
- "Mac" → `Mac`
|
||||
2. Store in `CurrentPlatform`
|
||||
3. Return `CurrentPlatform`
|
||||
|
||||
#### `GetPlatformFamily` → `EPlatformFamily`
|
||||
- **Description:** Returns the current platform. **This is the function ALL other systems call.** Read-only, always available after initialization.
|
||||
|
||||
#### `GetSDK` → `EPlatformSDK`
|
||||
- **Description:** Returns the available platform SDK. Used by achievement and save systems for routing.
|
||||
|
||||
#### `GetPlatformCapabilities` → `SPlatformCapabilities`
|
||||
- **Description:** Returns the full capabilities struct. Read-only.
|
||||
|
||||
#### `SubmitAchievementToPlatform` → `bool`
|
||||
- **Description:** Route an achievement unlock to the correct platform SDK.
|
||||
- **Parameters:**
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `AchievementID` | `FString` | Platform-specific achievement ID |
|
||||
- **Flow:**
|
||||
1. Switch on `CurrentSDK`:
|
||||
- `Steam` → call Steamworks `ISteamUserStats::SetAchievement(AchievementID)` + `StoreStats()`
|
||||
- `PSN` → call PSN Trophy API `UnlockTrophy(AchievementID)`
|
||||
- `XboxLive` → call Xbox Live `AchievementsService::UpdateAchievement(AchievementID, 100)`
|
||||
- `Nintendo` → call Nintendo achievement API
|
||||
- `None` → log "Achievement submitted (editor mode)" → return true (no-op stub)
|
||||
2. Return true if successful, false on SDK error
|
||||
|
||||
#### `SyncCloudSave` → `bool`
|
||||
- **Description:** Upload or sync save data to platform cloud.
|
||||
- **Parameters:**
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `SlotIndex` | `int32` | Which save slot |
|
||||
| `SaveData` | `TArray<uint8>` | Serialized save data |
|
||||
- **Flow:** Route to Steam Cloud / PSN Cloud / Xbox Cloud / Nintendo Cloud based on `CurrentSDK`.
|
||||
|
||||
#### `ShowPlatformOverlay` → `void`
|
||||
- **Description:** Open a platform-specific overlay.
|
||||
- **Parameters:**
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `OverlayType` | `EPlatformOverlayType` | Which overlay to show |
|
||||
- **Flow:**
|
||||
1. Switch on `CurrentSDK`:
|
||||
- `Steam` → `ISteamFriends::ActivateGameOverlay(URL)`
|
||||
- `PSN` → System software overlay API
|
||||
- Others → nop (not supported)
|
||||
|
||||
#### `GetDefaultInputProfile` → `FName`
|
||||
- **Description:** Returns the default input profile for this platform. Used by `SS_EnhancedInputManager` to select the correct `DA_InputMappingProfile`.
|
||||
- **Returns:** "PC_KeyboardMouse", "Xbox", "PS5_DualSense", "PS4_DualShock", "Switch_JoyCon"
|
||||
|
||||
#### `IsConsole` → `bool`, `IsHandheld` → `bool`, `IsPS5` → `bool`, `IsXbox` → `bool`, `IsSwitch` → `bool`, `IsPC` → `bool`, `IsSteamDeck` → `bool`
|
||||
- **Description:** Convenience boolean checks for common platform queries. All return from `PlatformCapabilities` struct.
|
||||
|
||||
#### `GetConsoleCertificationRequirements` → `TArray<FText>`
|
||||
- **Description:** Returns a list of TRC (Technical Requirement Checklist) items for the current console platform. Empty on PC.
|
||||
- **Flow:**
|
||||
1. Switch on `CurrentPlatform`:
|
||||
- `PS5` → return Sony TRC items (60 FPS perf mode, HDR support, controller speaker, activity cards, etc.)
|
||||
- `PS4` → return Sony PS4 TRC items
|
||||
- `Xbox_Series` → return Microsoft XR items (Quick Resume, Smart Delivery, VRR, etc.)
|
||||
- `Xbox_One` → return Microsoft XR items
|
||||
- `Switch` → return Nintendo guidelines (dynamic resolution, Handheld/Docked parity, etc.)
|
||||
- `PC_*` → return empty (no console cert needed)
|
||||
|
||||
#### `GetStoreID` → `FString`
|
||||
- **Description:** Returns the platform store app ID for DLC entitlement checks.
|
||||
|
||||
---
|
||||
|
||||
## 5. Event Dispatchers
|
||||
|
||||
| Dispatcher | Parameters | Bind Access | Description |
|
||||
|------------|-----------|-------------|-------------|
|
||||
| `OnPlatformReady` | `EPlatformFamily Platform` | `Public` | Fired after platform detection completes — all other systems bind to this for deferred init |
|
||||
| `OnPlatformCapabilitiesReady` | `SPlatformCapabilities Capabilities` | `Public` | Fired alongside OnPlatformReady with full capability info |
|
||||
| `OnPlatformSDKReady` | `EPlatformSDK SDK` | `Public` | Fired when platform SDK is ready for API calls |
|
||||
| `OnPlatformOverlayOpened` | `EPlatformOverlayType Type` | `Public` | Fired when platform overlay opens (game should pause) |
|
||||
| `OnPlatformOverlayClosed` | — | `Public` | Fired when platform overlay closes (game resumes) |
|
||||
| `OnCloudSaveCompleted` | `bool bSuccess`, `int32 SlotIndex` | `Public` | Fired when cloud save upload completes |
|
||||
| `OnAchievementSubmitted` | `FString AchievementID`, `bool bSuccess` | `Public` | Fired after platform API achievement call |
|
||||
|
||||
---
|
||||
|
||||
## 6. Overridden Events
|
||||
|
||||
### Event: On Component Created (attached to GI_GameFramework via BeginPlay)
|
||||
- **Description:** Auto-initializes at game start.
|
||||
- **Flow:**
|
||||
1. Call `Initialize()`
|
||||
2. All dependent subsystems that bound to `OnPlatformReady` receive the event and complete their initialization
|
||||
|
||||
---
|
||||
|
||||
## 7. Blueprint Graph Logic Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[GI_GameFramework: BeginPlay] --> B[BPC_PlatformServiceAbstraction: Initialize]
|
||||
B --> C{OverridePlatform?}
|
||||
C -->|Yes| D[Use Override]
|
||||
C -->|No| E[DetectPlatform]
|
||||
E --> F[Switch on GetPlatformName]
|
||||
F --> G[Set CurrentPlatform]
|
||||
D --> G
|
||||
|
||||
G --> H[Resolve available SDK]
|
||||
H --> I[Build SPlatformCapabilities]
|
||||
I --> J[Register Platform Tag in DA_GameTagRegistry]
|
||||
J --> K[Broadcast OnPlatformReady]
|
||||
|
||||
K --> L[ALL BOUND SYSTEMS RECEIVE]
|
||||
L --> M[RenderPipelineManager: load DA_RPP for platform]
|
||||
L --> N[EnhancedInputManager: load DA_InputMapping for platform]
|
||||
L --> O[HapticsController: set EControllerPlatform from platform]
|
||||
L --> P[AchievementSystem: route SDK via GetSDK]
|
||||
L --> Q[SettingsSystem: load platform defaults]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Communication Matrix
|
||||
|
||||
| Who Talks | How | What Is Sent |
|
||||
|-----------|-----|-------------|
|
||||
| `GI_GameFramework` | `Owner` | Hosts this component |
|
||||
| `BPC_PlatformServiceAbstraction` | `Dispatcher` → ALL systems | `OnPlatformReady(Platform)` — every system's deferred init trigger |
|
||||
| `BPC_RenderPipelineManager` (149) | `Function Call` | `GetPlatformFamily()` → selects correct `DA_RenderPipelineProfile` |
|
||||
| `SS_EnhancedInputManager` (128) | `Function Call` | `GetDefaultInputProfile()` → selects correct `DA_InputMappingProfile` |
|
||||
| `BPC_HapticsController` (148) | `Function Call` | `GetPlatformFamily()` → maps to `EControllerPlatform` |
|
||||
| `SS_AchievementSystem` (103) | `Function Call` | `SubmitAchievementToPlatform(ID)` → routes to Steam/PSN/Xbox/Nintendo |
|
||||
| `SS_SaveManager` (35) | `Function Call` | `SyncCloudSave(Slot, Data)` → routes to platform cloud API |
|
||||
| `SS_SettingsSystem` (105) | `Function Call` | `GetPlatformFamily()` → sets platform-appropriate defaults |
|
||||
| `SS_UIManager` (44) | `Dispatcher` | `OnPlatformOverlayOpened/Closed` → pause/resume game |
|
||||
| `DA_GameTagRegistry` (01) | `Function Call` | Register `Framework.Platform.{Name}` tag |
|
||||
|
||||
---
|
||||
|
||||
## 9. Platform Enum Mapping (Deprecation of old enums)
|
||||
|
||||
The old scattered platform enums are mapped to `EPlatformFamily` as follows:
|
||||
|
||||
| Deprecated Enum | Old Value | EPlatformFamily |
|
||||
|----------------|-----------|-----------------|
|
||||
| `E_InputPlatform::PC_KeyboardMouse` | 0 | `PC_Win64` (or `PC_Linux`, `Mac`) |
|
||||
| `E_InputPlatform::Xbox` | 1 | `Xbox_Series` or `Xbox_One` (detected) |
|
||||
| `E_InputPlatform::PS5_DualSense` | 2 | `PS5` or `PS5_Pro` |
|
||||
| `EControllerPlatform::PC_Generic` | 1 | `PC_Win64` |
|
||||
| `EControllerPlatform::Xbox` | 2 | `Xbox_Series` or `Xbox_One` |
|
||||
| `EControllerPlatform::PS5_DualSense` | 3 | `PS5` |
|
||||
| `EControllerPlatform::PS4_DualShock` | 4 | `PS4` |
|
||||
|
||||
All systems that previously used their own platform enum now call:
|
||||
```
|
||||
BPC_PlatformServiceAbstraction → GetPlatformFamily() → map to local enum internally
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Validation / Testing Checklist
|
||||
|
||||
- [ ] `DetectPlatform` correctly identifies all 12 platform families
|
||||
- [ ] `OverridePlatform = PS4` → all systems use PS4 profiles (baked lighting, DualShock, PSN SDK)
|
||||
- [ ] `OverridePlatform = Switch` → all systems use Switch profiles (baked, NIS, Joy-Con, Nintendo SDK)
|
||||
- [ ] `OverridePlatform = PS5_Pro` → RenderPipelineManager enables PSSR upscaler
|
||||
- [ ] `GetDefaultInputProfile` returns correct input profile for each platform
|
||||
- [ ] `SubmitAchievementToPlatform` on Steam build successfully calls Steam API
|
||||
- [ ] `SubmitAchievementToPlatform` in Editor returns true (no-op stub, no crash)
|
||||
- [ ] `SyncCloudSave` routes to correct platform cloud API
|
||||
- [ ] `ShowPlatformOverlay(Achievements)` opens correct platform overlay
|
||||
- [ ] `OnPlatformReady` fires before any dependent system queries `GetPlatformFamily()`
|
||||
- [ ] Changing `OverridePlatform` at runtime (dev cheat) cascades to all bound systems
|
||||
- [ ] Edge case: No SDK available (editor, generic build) → all SDK calls return no-op stubs
|
||||
- [ ] Edge case: Platform SDK initialization fails → `CurrentSDK = None`, systems fallback gracefully
|
||||
- [ ] Edge case: Steam Deck detection in Desktop Mode → reported as `PC_Linux`, not `SteamDeck`
|
||||
- [ ] Edge case: Xbox Series S (Lockhart) vs Series X (Anaconda) → different capabilities for GPU
|
||||
|
||||
---
|
||||
|
||||
## 11. Manual Implementation Guide
|
||||
|
||||
### 11.1 Class Setup
|
||||
1. Create Blueprint Class: parent `ActorComponent`, name `BPC_PlatformServiceAbstraction`
|
||||
2. Path: `Content/Framework/Core/`
|
||||
3. Attach to `GI_GameFramework` (or BP child of it)
|
||||
|
||||
### 11.2 Key UE5 Nodes
|
||||
| Node | Where to Find | Used For |
|
||||
|------|---------------|----------|
|
||||
| `Get Platform Name` | Right-click → "Get Platform Name" | Primary detection API |
|
||||
| `Switch on String` | Right-click → "Switch" | Platform name branching |
|
||||
| `Is Steam Deck` | Custom node / check for "SteamDeck" in device name | Steam Deck detection |
|
||||
| `Is In Editor` | Right-click → "Is In Editor" | SDK substitution in editor |
|
||||
| `Switch on EPlatformFamily` | Right-click → "Switch" | Platform-specific logic |
|
||||
|
||||
### 11.3 Node-by-Node: DetectPlatform
|
||||
|
||||
```
|
||||
[Function: DetectPlatform]
|
||||
Step 1: Get Platform Name → Store as PlatformStr
|
||||
Step 2: Switch on String (PlatformStr):
|
||||
"PS5" →
|
||||
Step 2a: Get device model info → if "Pro" → PS5_Pro, else PS5
|
||||
"PS4" → PS4
|
||||
"XboxOne" → Xbox_One
|
||||
"XSX" → Xbox_Series (check if "Lockhart" variant for Series S)
|
||||
"XboxAnaconda" → Xbox_Series
|
||||
"Switch" → Switch (check model revision for Switch_2)
|
||||
"Win64" →
|
||||
Step 2b: Check if running on Steam Deck → if yes → SteamDeck
|
||||
Step 2c: Else → PC_Win64
|
||||
"Linux" → PC_Linux
|
||||
"Mac" → Mac
|
||||
Default → PC_Win64 (fallback)
|
||||
Step 3: Store result in CurrentPlatform
|
||||
Step 4: Return CurrentPlatform
|
||||
```
|
||||
|
||||
### 11.4 Networking
|
||||
- **Platform is per-client.** Each client may be on a different platform (PC host + PS5 client).
|
||||
- `BPC_PlatformServiceAbstraction` runs on ALL instances (server + each client).
|
||||
- Platform SDK calls are local-to-client.
|
||||
- For listen server: host's platform SDK is used for host's achievements; clients use their own.
|
||||
|
||||
### 11.5 Multiplayer Note
|
||||
- In a multiplayer session, each player's platform is detected independently
|
||||
- Cross-platform play: a PC host may have Steam SDK active while a PS5 client has PSN SDK
|
||||
- The Game Instance's PlatformServiceAbstraction only represents the LOCAL player's platform
|
||||
- For dedicated servers: `CurrentPlatform = PC_Linux` or `PC_Win64`, `CurrentSDK = None` (no achievements on dedicated server)
|
||||
|
||||
---
|
||||
|
||||
## 12. Blueprint Build Checklist
|
||||
|
||||
- [ ] Create Blueprint class: `BPC_PlatformServiceAbstraction` (parent: `ActorComponent`)
|
||||
- [ ] Add enums: `EPlatformFamily` (12 values), `EPlatformSDK` (6 values), `EPlatformOverlayType` (5 values)
|
||||
- [ ] Add struct: `SPlatformCapabilities`
|
||||
- [ ] Add all variables from Section 3
|
||||
- [ ] Build `Initialize` → `DetectPlatform` → `BuildCapabilities` → broadcast chain
|
||||
- [ ] Implement `DetectPlatform` with Switch on GetPlatformName
|
||||
- [ ] Implement `SubmitAchievementToPlatform` with Switch on CurrentSDK
|
||||
- [ ] Implement `SyncCloudSave`, `ShowPlatformOverlay`
|
||||
- [ ] Implement all boolean convenience functions (`IsConsole`, `IsPS5`, etc.)
|
||||
- [ ] Create all 7 event dispatchers
|
||||
- [ ] Wire OnPlatformReady as deferred init trigger for all dependent subsystems
|
||||
- [ ] Test: OverridePlatform = PS5 → OnPlatformReady fires with PS5
|
||||
- [ ] Test: Editor mode → SDK calls return no-op stubs
|
||||
- [ ] Test: Steam build → SubmitAchievement calls Steam API
|
||||
- [ ] Test: Platform hot-swap (dev cheat) cascades to all bound systems
|
||||
|
||||
---
|
||||
|
||||
*Blueprint Spec: Platform Service Abstraction. Conforms to TEMPLATE.md v2.0 — part of the UE5 Modular Game Framework, CORE layer.*
|
||||
71
docs/blueprints/01-core/BP_CoreGameMode.asset.md
Normal file
71
docs/blueprints/01-core/BP_CoreGameMode.asset.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# BP_CoreGameMode — Asset Implementation
|
||||
|
||||
> **UE5 Asset:** `/Game/Framework/Core/BP_CoreGameMode`
|
||||
> **Parent Class:** `GM_CoreGameMode` (C++) or `GameModeBase`
|
||||
> **Asset Type:** Blueprint Class (GameMode)
|
||||
|
||||
---
|
||||
|
||||
## Create This Asset
|
||||
|
||||
### Step 1: Create Blueprint
|
||||
1. Content Browser → `Content/Framework/Core/`
|
||||
2. Right-click → **Blueprint Class**
|
||||
3. Parent: `GM_CoreGameMode` (C++) OR `GameModeBase`
|
||||
4. Name: `BP_CoreGameMode`
|
||||
|
||||
### Step 2: Set as Default GameMode
|
||||
**Project Settings → Maps & Modes → Default GameMode** → `BP_CoreGameMode`
|
||||
|
||||
### Step 3: Configure Class Defaults
|
||||
| Variable | Type | Value |
|
||||
|----------|------|-------|
|
||||
| `GameState Class` | `AGS_CoreGameState` | `BP_CoreGameState` |
|
||||
| `Player Controller Class` | `APlayerController` | Your `BP_CorePlayerController` |
|
||||
| `Default Pawn Class` | `APawn` | Your GASP player pawn |
|
||||
| `HUD Class` | `AHUD` | `WBP_HUDController` |
|
||||
|
||||
---
|
||||
|
||||
## What to Wire
|
||||
|
||||
### Function: TransitionToChapter
|
||||
```
|
||||
Input: ChapterTag (GameplayTag)
|
||||
|
||||
[BlueprintCallable]
|
||||
├─ Branch: CurrentGamePhase == Loading?
|
||||
│ True → Print Warning: "Already loading" → Return
|
||||
├─ Set CurrentChapterTag = ChapterTag
|
||||
├─ Switch HasAuthority → True:
|
||||
│ ├─ Get GameInstance → Cast to GI_GameFramework
|
||||
│ ├─ SetGamePhase(Loading)
|
||||
│ ├─ Get GameState → Cast to GS_CoreGameState → SetChapter(ChapterTag)
|
||||
│ ├─ Open Level (by ChapterTag mapping — use Data Asset lookup)
|
||||
│ └─ On level loaded → SetGamePhase(InGame)
|
||||
└─ Call OnChapterTransition(ChapterTag)
|
||||
```
|
||||
|
||||
### Function: HandlePlayerDead
|
||||
```
|
||||
Input: DeadController (AController)
|
||||
|
||||
[BlueprintCallable]
|
||||
├─ IsValid(DeadController)? → False: Return
|
||||
├─ Set bPauseAllowed = false
|
||||
├─ Get GameInstance → Cast to GI_GameFramework → SetGamePhase(DeathLoop)
|
||||
└─ TODO: Route to respawn or AltDeathSpace
|
||||
```
|
||||
|
||||
### Event Dispatchers to Create
|
||||
| Dispatcher | Parameters |
|
||||
|------------|-----------|
|
||||
| `OnChapterTransition` | `NewChapter` (GameplayTag) |
|
||||
| `OnGameOverTriggered` | `EndingTag` (GameplayTag) |
|
||||
|
||||
---
|
||||
|
||||
## Test It
|
||||
- [ ] PIE: check output log for "GM_CoreGameMode::InitGame"
|
||||
- [ ] Check that player spawns with correct pawn
|
||||
- [ ] Call `TransitionToChapter` → phase changes to Loading → level loads
|
||||
88
docs/blueprints/01-core/BP_CoreGameState.asset.md
Normal file
88
docs/blueprints/01-core/BP_CoreGameState.asset.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# BP_CoreGameState — Asset Implementation
|
||||
|
||||
> **UE5 Asset:** `/Game/Framework/Core/BP_CoreGameState`
|
||||
> **Parent Class:** `GS_CoreGameState` (C++) or `GameStateBase`
|
||||
> **Asset Type:** Blueprint Class (GameState)
|
||||
|
||||
---
|
||||
|
||||
## Create This Asset
|
||||
|
||||
### Step 1: Create Blueprint
|
||||
1. Content Browser → `Content/Framework/Core/`
|
||||
2. Right-click → **Blueprint Class**
|
||||
3. Parent: `GS_CoreGameState` (C++) OR `GameStateBase`
|
||||
4. Name: `BP_CoreGameState`
|
||||
|
||||
### Step 2: Assign in GameMode
|
||||
Open `BP_CoreGameMode` → Class Defaults → `GameState Class` → `BP_CoreGameState`
|
||||
|
||||
---
|
||||
|
||||
## What to Wire
|
||||
|
||||
### Variables (if BP-only GameStateBase)
|
||||
|
||||
| Variable | Type | Replication | Default |
|
||||
|----------|------|-------------|---------|
|
||||
| `ElapsedPlayTime` | `Float` | Replicated | `0.0` |
|
||||
| `ActiveChapterTag` | `GameplayTag` | Replicated | Empty |
|
||||
| `ActiveNarrativePhase` | `GameplayTag` | Replicated | Empty |
|
||||
| `bEncounterActive` | `Boolean` | Replicated | `false` |
|
||||
| `ActiveObjectiveTags` | `Array<GameplayTag>` | Replicated | Empty |
|
||||
|
||||
### Override: BeginPlay
|
||||
```
|
||||
[Event BeginPlay]
|
||||
├─ Parent: BeginPlay
|
||||
├─ Get GameInstance → Cast to GI_GameFramework → Store as CachedFramework
|
||||
└─ Enable Tick
|
||||
```
|
||||
|
||||
### Override: Tick
|
||||
```
|
||||
[Event Tick]
|
||||
├─ IsValid(CachedFramework)? AND CurrentGamePhase == InGame?
|
||||
│ True → ElapsedPlayTime += DeltaSeconds
|
||||
│ ├─ TimeUpdateAccumulator += DeltaSeconds
|
||||
│ └─ Accumulator >= 1.0?
|
||||
│ ├─ Reset accumulator
|
||||
│ └─ Call OnElapsedPlayTimeUpdated(ElapsedPlayTime)
|
||||
```
|
||||
|
||||
### Function: SetChapter
|
||||
```
|
||||
Input: ChapterTag (GameplayTag)
|
||||
[Server-only]
|
||||
├─ If ChapterTag == ActiveChapterTag → Return
|
||||
├─ Set ActiveChapterTag = ChapterTag
|
||||
└─ Call OnChapterChanged(ChapterTag)
|
||||
```
|
||||
|
||||
### Function: AddObjective / RemoveObjective
|
||||
```
|
||||
AddObjective(ObjectiveTag):
|
||||
├─ If already in array → Return
|
||||
├─ Add to ActiveObjectiveTags
|
||||
└─ Call OnObjectiveTagsChanged
|
||||
|
||||
RemoveObjective(ObjectiveTag):
|
||||
├─ Remove from array
|
||||
└─ Call OnObjectiveTagsChanged
|
||||
```
|
||||
|
||||
### Event Dispatchers to Create
|
||||
| Dispatcher | Parameters |
|
||||
|------------|-----------|
|
||||
| `OnElapsedPlayTimeUpdated` | `ElapsedSeconds` (Float) |
|
||||
| `OnChapterChanged` | `NewChapter` (GameplayTag) |
|
||||
| `OnNarrativePhaseChanged` | `NewPhase` (GameplayTag) |
|
||||
| `OnEncounterActiveStateChanged` | `bActive` (Boolean) |
|
||||
| `OnObjectiveTagsChanged` | (none) |
|
||||
|
||||
---
|
||||
|
||||
## Test It
|
||||
- [ ] PIE: check that GS_CoreGameState spawns
|
||||
- [ ] Call SetChapter → OnChapterChanged fires → HUD updates
|
||||
- [ ] Call AddObjective → OnObjectiveTagsChanged fires → objective list refreshes
|
||||
78
docs/blueprints/01-core/BP_GameFramework.asset.md
Normal file
78
docs/blueprints/01-core/BP_GameFramework.asset.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# BP_GameFramework — Asset Implementation
|
||||
|
||||
> **UE5 Asset:** `/Game/Framework/Core/BP_GameFramework`
|
||||
> **Parent Class:** `GI_GameFramework` (C++) or `GameInstance` (BP-only)
|
||||
> **Asset Type:** Blueprint Class (GameInstance)
|
||||
|
||||
---
|
||||
|
||||
## Create This Asset
|
||||
|
||||
### Step 1: Create Blueprint
|
||||
1. Content Browser → `Content/Framework/Core/`
|
||||
2. Right-click → **Blueprint Class**
|
||||
3. Parent: `GI_GameFramework` (C++) OR `GameInstance`
|
||||
4. Name: `BP_GameFramework`
|
||||
|
||||
### Step 2: Set as Game Instance Class
|
||||
**Project Settings → Maps & Modes → Game Instance Class** → `BP_GameFramework`
|
||||
|
||||
### Step 3: Configure Class Defaults
|
||||
| Variable | Type | Value |
|
||||
|----------|------|-------|
|
||||
| `TagRegistry` | `DA_GameTagRegistry` | Assign `DA_GameTagRegistry` asset |
|
||||
| `bValidateTagsOnInit` | `Boolean` | `true` |
|
||||
| `bLogTagsOnInit` | `Boolean` | `true` (dev) / `false` (ship) |
|
||||
|
||||
---
|
||||
|
||||
## What to Wire
|
||||
|
||||
### Override: Event Init
|
||||
```
|
||||
[Event Init]
|
||||
├─ Parent: Event Init
|
||||
├─ Print String: "BP_GameFramework: Init started"
|
||||
├─ Branch: bValidateTagsOnInit?
|
||||
│ True → Call ValidateFrameworkTags (see below)
|
||||
├─ IsValid(TagRegistry)? → Branch
|
||||
│ False → Print Error → Call OnFrameworkInitFailed("TagRegistry not assigned")
|
||||
│ True →
|
||||
│ ├─ Set bFrameworkInitialized = true
|
||||
│ ├─ Call OnFrameworkReady
|
||||
│ └─ Print String: "Framework ready"
|
||||
```
|
||||
|
||||
### Function: ValidateFrameworkTags
|
||||
```
|
||||
[BlueprintCallable, Private]
|
||||
├─ IsValid(TagRegistry)? → Branch
|
||||
│ False → Print Error → Return
|
||||
├─ TagRegistry → GetAllRegisteredTags → AllTags
|
||||
├─ Array Length(AllTags) → TagCount
|
||||
├─ Branch: TagCount == 0?
|
||||
│ True → Print Warning: "No Gameplay Tags registered!"
|
||||
│ False → Print: "{TagCount} tags registered"
|
||||
├─ Branch: bLogTagsOnInit?
|
||||
│ True → TagRegistry → LogAllTags
|
||||
└─ Return
|
||||
```
|
||||
|
||||
### Function: IsFrameworkReady
|
||||
```
|
||||
[BlueprintPure, Public] → Boolean
|
||||
└─ Return bFrameworkInitialized
|
||||
```
|
||||
|
||||
### Event Dispatchers to Create
|
||||
| Dispatcher | Parameters |
|
||||
|------------|-----------|
|
||||
| `OnFrameworkReady` | (none) |
|
||||
| `OnFrameworkInitFailed` | `ErrorReason` (String) |
|
||||
|
||||
---
|
||||
|
||||
## Test It
|
||||
- [ ] PIE: output log shows "Framework ready" and tag count
|
||||
- [ ] Clear TagRegistry variable → PIE → `OnFrameworkInitFailed` fires
|
||||
- [ ] Set `bLogTagsOnInit = true` → all tag names printed to log
|
||||
68
docs/blueprints/01-core/DA_GameTagRegistry.asset.md
Normal file
68
docs/blueprints/01-core/DA_GameTagRegistry.asset.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# DA_GameTagRegistry — Asset Implementation
|
||||
|
||||
> **UE5 Asset:** `/Game/Framework/Core/DA_GameTagRegistry`
|
||||
> **Parent Class:** `DA_GameTagRegistry` (C++) or `PrimaryDataAsset` (BP-only)
|
||||
> **Asset Type:** Data Asset
|
||||
|
||||
---
|
||||
|
||||
## Create This Asset
|
||||
|
||||
### Step 1: Create Data Asset
|
||||
1. Content Browser → `Content/Framework/Core/`
|
||||
2. Right-click → **Miscellaneous → Data Asset**
|
||||
3. Class: `DA_GameTagRegistry` (if C++ compiled) OR `PrimaryDataAsset`
|
||||
4. Name: `DA_GameTagRegistry`
|
||||
|
||||
### Step 2: Configure Class Defaults (if BP-only PrimaryDataAsset)
|
||||
| Variable | Type | Value |
|
||||
|----------|------|-------|
|
||||
| `TagNamespace` | `Text` | `"Framework tag namespace documentation"` |
|
||||
| `bIsFrameworkTag` | `Boolean` | `true` |
|
||||
| `TagDataTables` | `Array<Data Table>` | Add all 11 DT_Tags_* tables |
|
||||
|
||||
### Step 3: Assign to GameInstance
|
||||
Open `BP_GameFramework` → Class Defaults → `TagRegistry` → assign `DA_GameTagRegistry`
|
||||
|
||||
---
|
||||
|
||||
## What to Wire (BP-Only Version)
|
||||
|
||||
### Function: GetAllRegisteredTags
|
||||
```
|
||||
Inputs: none → Output: Array<GameplayTag>
|
||||
|
||||
[Pure Function Graph]
|
||||
├─ Make Array<GameplayTag> → LocalTags
|
||||
├─ ForEachLoop (TagDataTables)
|
||||
│ ├─ Branch: IsValid(Array Element)?
|
||||
│ │ True → Get Data Table Row Names
|
||||
│ │ └─ ForEachLoop (RowNames)
|
||||
│ │ └─ Get Data Table Row → Break GameplayTagTableRow → Tag
|
||||
│ │ └─ ADD to LocalTags
|
||||
└─ Return LocalTags
|
||||
```
|
||||
|
||||
### Function: ValidateTag
|
||||
```
|
||||
Input: Tag (GameplayTag) → Output: Boolean
|
||||
|
||||
[Pure Function Graph]
|
||||
└─ Is Gameplay Tag Valid(Tag) → Return
|
||||
```
|
||||
|
||||
### Function: GetTagDisplayName
|
||||
```
|
||||
Input: Tag (GameplayTag) → Output: Text
|
||||
|
||||
[Pure Function Graph]
|
||||
└─ Get Tag Display Name(Tag) → Return
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test It
|
||||
- [ ] Open `DA_GameTagRegistry` → verify `TagDataTables` has 11 entries
|
||||
- [ ] PIE: output log shows `"N tags registered across 11 Data Tables"`
|
||||
- [ ] Call `ValidateTag(Framework.Player.State.Alive)` → returns `true`
|
||||
- [ ] Call `ValidateTag(Unregistered.Tag)` → returns `false`
|
||||
5
docs/blueprints/01-core/DT_ProjectTags.csv
Normal file
5
docs/blueprints/01-core/DT_ProjectTags.csv
Normal file
@@ -0,0 +1,5 @@
|
||||
# SUPERSEDED — Tags split into 11 per-category Data Tables in docs/blueprints/01-core/data-tables/
|
||||
# See: DT_Tags_Player.csv, DT_Tags_Interaction.csv, DT_Tags_Item.csv, DT_Tags_Narrative.csv,
|
||||
# DT_Tags_AI.csv, DT_Tags_Save.csv, DT_Tags_Environment.csv, DT_Tags_Combat.csv,
|
||||
# DT_Tags_State.csv, DT_Tags_Audio.csv, DT_Tags_Achievement.csv
|
||||
# This file kept as migration reference only. Do not use in new implementations.
|
||||
|
59
docs/blueprints/01-core/DT_Tags_All.asset.md
Normal file
59
docs/blueprints/01-core/DT_Tags_All.asset.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# DT_Tags_* — Data Tables (11 assets)
|
||||
|
||||
> **UE5 Assets:** `Content/Framework/Core/DataTables/DT_Tags_{Category}`
|
||||
> **Row Structure:** `GameplayTagTableRow`
|
||||
> **Count:** 11 tables
|
||||
|
||||
---
|
||||
|
||||
## Create These Assets
|
||||
|
||||
### Step 1: Create All 11 Data Tables
|
||||
For each category below, repeat this process:
|
||||
|
||||
1. Content Browser → `Content/Framework/Core/DataTables/`
|
||||
2. Right-click → **Miscellaneous → Data Table**
|
||||
3. Row Structure: `GameplayTagTableRow`
|
||||
4. Name: `DT_Tags_{Category}`
|
||||
5. Right-click the Data Table → **Import** → select the CSV file from `docs/blueprints/01-core/data-tables/DT_Tags_{Category}.csv`
|
||||
|
||||
### The 11 Tables
|
||||
|
||||
| # | Data Table | CSV Source | Namespaces |
|
||||
|---|-----------|------------|------------|
|
||||
| 1 | `DT_Tags_Player` | `DT_Tags_Player.csv` | Player.State, Stress, Posture, Movement, Camera, Body, Overlay, Vitals |
|
||||
| 2 | `DT_Tags_Interaction` | `DT_Tags_Interaction.csv` | Interaction.Type, Context, Prompt, HidingSpot, Traversal, Door |
|
||||
| 3 | `DT_Tags_Item` | `DT_Tags_Item.csv` | Item.Type, Slot, Rarity, Context |
|
||||
| 4 | `DT_Tags_Narrative` | `DT_Tags_Narrative.csv` | Narrative.Flag, Phase, Choice, Ending, Trial, Cutscene, Lore, Objective |
|
||||
| 5 | `DT_Tags_AI` | `DT_Tags_AI.csv` | AI.Alert, Archetype, Stimulus, Behavior, Memory |
|
||||
| 6 | `DT_Tags_Save` | `DT_Tags_Save.csv` | Save, DeathSpace, Checkpoint, Respawn, RunHistory |
|
||||
| 7 | `DT_Tags_Environment` | `DT_Tags_Environment.csv` | Environment.Atmosphere, Scare, Light, Pacing, Performance |
|
||||
| 8 | `DT_Tags_Combat` | `DT_Tags_Combat.csv` | Combat.Damage, Weapon, Ammo, FireMode, HitReaction, Feedback, Shield |
|
||||
| 9 | `DT_Tags_State` | `DT_Tags_State.csv` | State.Action, Overlay, Vital, Gating |
|
||||
| 10 | `DT_Tags_Audio` | `DT_Tags_Audio.csv` | Audio.Bus, Room, Parameter, Surface |
|
||||
| 11 | `DT_Tags_Achievement` | `DT_Tags_Achievement.csv` | Achievement |
|
||||
|
||||
### Step 2: Register All Tables with Engine
|
||||
**⚠️ CRITICAL — Without this, NO tags are recognized!**
|
||||
|
||||
1. **Project Settings → GameplayTags → Gameplay Tag Table List**
|
||||
2. Click `+` 11 times
|
||||
3. Assign each `DT_Tags_*` Data Table to a slot
|
||||
4. Restart editor (or recompile Blueprints) to refresh tag cache
|
||||
|
||||
### CSV Format
|
||||
Each CSV has 3 columns:
|
||||
```
|
||||
Name,Tag,DevComment
|
||||
Alive,"Framework.Player.State.Alive","Player is alive"
|
||||
Dead,"Framework.Player.State.Dead","Player is dead"
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test It
|
||||
- [ ] Open `DA_GameTagRegistry` → call `GetAllRegisteredTags()` → returns ~334 tags
|
||||
- [ ] Make Literal Gameplay Tag → type `Framework.Player.State.Alive` → recognized (not red)
|
||||
- [ ] `Is Gameplay Tag Valid(Framework.Player.State.Alive)` → `true`
|
||||
- [ ] `Is Gameplay Tag Valid(Fake.Tag)` → `false`
|
||||
24
docs/blueprints/01-core/data-tables/DT_Tags_AI.csv
Normal file
24
docs/blueprints/01-core/data-tables/DT_Tags_AI.csv
Normal file
@@ -0,0 +1,24 @@
|
||||
Name,Tag,DevComment
|
||||
Framework_AI_Alert_None,Framework.AI.Alert.None,No awareness of player
|
||||
Framework_AI_Alert_Suspicious,Framework.AI.Alert.Suspicious,Heard or glimpsed something
|
||||
Framework_AI_Alert_Alerted,Framework.AI.Alert.Alerted,Confirmed player presence
|
||||
Framework_AI_Alert_Engaged,Framework.AI.Alert.Engaged,Actively fighting
|
||||
Framework_AI_Alert_Searching,Framework.AI.Alert.Searching,Lost sight searching area
|
||||
Framework_AI_Alert_Fleeing,Framework.AI.Alert.Fleeing,Retreating from combat
|
||||
Framework_AI_Archetype_Patrol,Framework.AI.Archetype.Patrol,Standard patrol guard
|
||||
Framework_AI_Archetype_Ambush,Framework.AI.Archetype.Ambush,Lurking ambush predator
|
||||
Framework_AI_Archetype_Stalker,Framework.AI.Archetype.Stalker,Persistent slow stalker
|
||||
Framework_AI_Archetype_Passive,Framework.AI.Archetype.Passive,Non-aggressive NPC
|
||||
Framework_AI_Archetype_Berserker,Framework.AI.Archetype.Berserker,Aggressive rush-down enemy
|
||||
Framework_AI_Archetype_Sniper,Framework.AI.Archetype.Sniper,Long-range stationary enemy
|
||||
Framework_AI_Stimulus_Sight,Framework.AI.Stimulus.Sight,Visual perception trigger
|
||||
Framework_AI_Stimulus_Hearing,Framework.AI.Stimulus.Hearing,Audio perception trigger
|
||||
Framework_AI_Stimulus_Damage,Framework.AI.Stimulus.Damage,Damage perception trigger
|
||||
Framework_AI_Stimulus_TeamAlert,Framework.AI.Stimulus.TeamAlert,Ally alert shared perception
|
||||
Framework_AI_Behavior_Aggressive,Framework.AI.Behavior.Aggressive,Aggressive combat style
|
||||
Framework_AI_Behavior_Defensive,Framework.AI.Behavior.Defensive,Defensive combat style
|
||||
Framework_AI_Behavior_Cautious,Framework.AI.Behavior.Cautious,Cautious slow approach
|
||||
Framework_AI_Behavior_Reckless,Framework.AI.Behavior.Reckless,Reckless rush style
|
||||
Framework_AI_Memory_LastKnownLocation,Framework.AI.Memory.LastKnownLocation,Last known player position
|
||||
Framework_AI_Memory_InvestigationPoint,Framework.AI.Memory.InvestigationPoint,Point of interest to investigate
|
||||
Framework_AI_Memory_ThreatHistory,Framework.AI.Memory.ThreatHistory,Record of damage sources
|
||||
|
22
docs/blueprints/01-core/data-tables/DT_Tags_Achievement.csv
Normal file
22
docs/blueprints/01-core/data-tables/DT_Tags_Achievement.csv
Normal file
@@ -0,0 +1,22 @@
|
||||
Name,Tag,DevComment
|
||||
Game_Achievement,Game.Achievement,Root achievement namespace
|
||||
Game_Achievement_FirstBlood,Game.Achievement.FirstBlood,First enemy killed
|
||||
Game_Achievement_Survivor,Game.Achievement.Survivor,Survived first chapter without dying
|
||||
Game_Achievement_Pacifist,Game.Achievement.Pacifist,Completed a chapter without killing any enemy
|
||||
Game_Achievement_Ghost,Game.Achievement.Ghost,Never detected by enemies in a chapter
|
||||
Game_Achievement_Collector,Game.Achievement.Collector,Collected all collectibles
|
||||
Game_Achievement_LoreMaster,Game.Achievement.LoreMaster,Discovered all lore entries
|
||||
Game_Achievement_SpeedRunner,Game.Achievement.SpeedRunner,Completed game in under 3 hours
|
||||
Game_Achievement_TrueEnding,Game.Achievement.TrueEnding,Unlocked the true ending
|
||||
Game_Achievement_BadEnding,Game.Achievement.BadEnding,Witnessed the bad ending
|
||||
Game_Achievement_WeaponsMaster,Game.Achievement.WeaponsMaster,Used every weapon type
|
||||
Game_Achievement_Crafter,Game.Achievement.Crafter,Crafted 10 items
|
||||
Game_Achievement_Untouchable,Game.Achievement.Untouchable,Completed a chapter without taking damage
|
||||
Game_Achievement_Hoarder,Game.Achievement.Hoarder,Carried 100 items at once
|
||||
Game_Achievement_Completionist,Game.Achievement.Completionist,Completed all side objectives
|
||||
Game_Achievement_FirstDeath,Game.Achievement.FirstDeath,Died for the first time
|
||||
Game_Achievement_TenDeaths,Game.Achievement.TenDeaths,Died 10 times
|
||||
Game_Achievement_Chapter1Complete,Game.Achievement.Chapter1Complete,Completed Chapter 1
|
||||
Game_Achievement_Chapter2Complete,Game.Achievement.Chapter2Complete,Completed Chapter 2
|
||||
Game_Achievement_Chapter3Complete,Game.Achievement.Chapter3Complete,Completed Chapter 3
|
||||
Game_Achievement_AllEndings,Game.Achievement.AllEndings,Unlocked every ending
|
||||
|
29
docs/blueprints/01-core/data-tables/DT_Tags_Audio.csv
Normal file
29
docs/blueprints/01-core/data-tables/DT_Tags_Audio.csv
Normal file
@@ -0,0 +1,29 @@
|
||||
Name,Tag,DevComment
|
||||
Framework_Audio_Bus_Master,Framework.Audio.Bus.Master,Master audio bus
|
||||
Framework_Audio_Bus_SFX,Framework.Audio.Bus.SFX,Sound effects bus
|
||||
Framework_Audio_Bus_Ambient,Framework.Audio.Bus.Ambient,Ambient audio bus
|
||||
Framework_Audio_Bus_Music,Framework.Audio.Bus.Music,Music bus
|
||||
Framework_Audio_Bus_Dialogue,Framework.Audio.Bus.Dialogue,Dialogue/VO bus
|
||||
Framework_Audio_Room_Small,Framework.Audio.Room.Small,Small room acoustics
|
||||
Framework_Audio_Room_Large,Framework.Audio.Room.Large,Large room acoustics
|
||||
Framework_Audio_Room_Outdoor,Framework.Audio.Room.Outdoor,Outdoor acoustics
|
||||
Framework_Audio_Room_Cave,Framework.Audio.Room.Cave,Cave acoustics
|
||||
Framework_Audio_Room_Hallway,Framework.Audio.Room.Hallway,Hallway acoustics
|
||||
Framework_Audio_Room_Bathroom,Framework.Audio.Room.Bathroom,Bathroom acoustics
|
||||
Framework_Audio_Room_Cathedral,Framework.Audio.Room.Cathedral,Cathedral acoustics
|
||||
Framework_Audio_Room_Anchoric,Framework.Audio.Room.Anchoric,Anechoic/dead acoustics
|
||||
Framework_Audio_Parameter_HeartRate,Framework.Audio.Parameter.HeartRate,Heart rate audio parameter
|
||||
Framework_Audio_Parameter_Stress,Framework.Audio.Parameter.Stress,Stress audio parameter
|
||||
Framework_Audio_Parameter_Fear,Framework.Audio.Parameter.Fear,Fear audio parameter
|
||||
Framework_Audio_Parameter_MusicIntensity,Framework.Audio.Parameter.MusicIntensity,Music intensity parameter
|
||||
Framework_Audio_Parameter_Tension,Framework.Audio.Parameter.Tension,Tension parameter
|
||||
Framework_Audio_Surface_Concrete,Framework.Audio.Surface.Concrete,Concrete footstep
|
||||
Framework_Audio_Surface_Wood,Framework.Audio.Surface.Wood,Wood footstep
|
||||
Framework_Audio_Surface_Metal,Framework.Audio.Surface.Metal,Metal footstep
|
||||
Framework_Audio_Surface_Water,Framework.Audio.Surface.Water,Water footstep
|
||||
Framework_Audio_Surface_Carpet,Framework.Audio.Surface.Carpet,Carpet footstep
|
||||
Framework_Audio_Surface_Gravel,Framework.Audio.Surface.Gravel,Gravel footstep
|
||||
Framework_Audio_Surface_Glass,Framework.Audio.Surface.Glass,Glass footstep
|
||||
Framework_Audio_Surface_Dirt,Framework.Audio.Surface.Dirt,Dirt footstep
|
||||
Framework_Audio_Surface_Grass,Framework.Audio.Surface.Grass,Grass footstep
|
||||
Framework_Audio_Surface_Tile,Framework.Audio.Surface.Tile,Tile footstep
|
||||
|
28
docs/blueprints/01-core/data-tables/DT_Tags_Combat.csv
Normal file
28
docs/blueprints/01-core/data-tables/DT_Tags_Combat.csv
Normal file
@@ -0,0 +1,28 @@
|
||||
Name,Tag,DevComment
|
||||
Framework_Combat_Damage_Physical,Framework.Combat.Damage.Physical,Physical/blunt damage type
|
||||
Framework_Combat_Damage_Arcane,Framework.Combat.Damage.Arcane,Arcane/magic damage type
|
||||
Framework_Combat_Damage_Fire,Framework.Combat.Damage.Fire,Fire damage type
|
||||
Framework_Combat_Damage_Poison,Framework.Combat.Damage.Poison,Poison damage type
|
||||
Framework_Combat_Damage_Fear,Framework.Combat.Damage.Fear,Fear/psychological damage type
|
||||
Framework_Combat_Damage_Environmental,Framework.Combat.Damage.Environmental,Environmental hazard damage
|
||||
Framework_Combat_Damage_True,Framework.Combat.Damage.True,True damage (bypasses all resistance)
|
||||
Framework_Combat_Weapon_Firearm,Framework.Combat.Weapon.Firearm,Firearm weapon type
|
||||
Framework_Combat_Weapon_Melee,Framework.Combat.Weapon.Melee,Melee weapon type
|
||||
Framework_Combat_Weapon_Throwable,Framework.Combat.Weapon.Throwable,Throwable weapon
|
||||
Framework_Combat_Ammo_Pistol,Framework.Combat.Ammo.Pistol,Pistol ammunition
|
||||
Framework_Combat_Ammo_Rifle,Framework.Combat.Ammo.Rifle,Rifle ammunition
|
||||
Framework_Combat_Ammo_Shotgun,Framework.Combat.Ammo.Shotgun,Shotgun shells
|
||||
Framework_Combat_Ammo_Energy,Framework.Combat.Ammo.Energy,Energy cells
|
||||
Framework_Combat_FireMode_SemiAuto,Framework.Combat.FireMode.SemiAuto,One shot per trigger press
|
||||
Framework_Combat_FireMode_FullAuto,Framework.Combat.FireMode.FullAuto,Continuous fire while held
|
||||
Framework_Combat_FireMode_Burst,Framework.Combat.FireMode.Burst,Fixed burst count
|
||||
Framework_Combat_FireMode_Charge,Framework.Combat.FireMode.Charge,Hold to charge release
|
||||
Framework_Combat_HitReaction_Flinch,Framework.Combat.HitReaction.Flinch,Minor directional flinch
|
||||
Framework_Combat_HitReaction_Stagger,Framework.Combat.HitReaction.Stagger,Heavy stagger
|
||||
Framework_Combat_HitReaction_Knockdown,Framework.Combat.HitReaction.Knockdown,Knocked to ground
|
||||
Framework_Combat_HitReaction_Ragdoll,Framework.Combat.HitReaction.Ragdoll,Full ragdoll on death
|
||||
Framework_Combat_Feedback_HitMarker,Framework.Combat.Feedback.HitMarker,Hit marker UI element
|
||||
Framework_Combat_Feedback_KillConfirm,Framework.Combat.Feedback.KillConfirm,Kill confirm UI element
|
||||
Framework_Combat_Shield_Active,Framework.Combat.Shield.Active,Shield is blocking
|
||||
Framework_Combat_Shield_Broken,Framework.Combat.Shield.Broken,Shield durability zero
|
||||
Framework_Combat_Shield_Recharging,Framework.Combat.Shield.Recharging,Shield recharging
|
||||
|
35
docs/blueprints/01-core/data-tables/DT_Tags_Environment.csv
Normal file
35
docs/blueprints/01-core/data-tables/DT_Tags_Environment.csv
Normal file
@@ -0,0 +1,35 @@
|
||||
Name,Tag,DevComment
|
||||
Game_Achievement,Game.Achievement,Root achievement namespace
|
||||
Game_Achievement_FirstBlood,Game.Achievement.FirstBlood,First kill
|
||||
Game_Achievement_Survivor,Game.Achievement.Survivor,Survived first chapter
|
||||
Game_Achievement_Pacifist,Game.Achievement.Pacifist,Completed chapter without killing
|
||||
Game_Achievement_Ghost,Game.Achievement.Ghost,Never detected in a chapter
|
||||
Game_Achievement_Collector,Game.Achievement.Collector,Collected all items
|
||||
Game_Achievement_LoreMaster,Game.Achievement.LoreMaster,Found all lore entries
|
||||
Game_Achievement_SpeedRunner,Game.Achievement.SpeedRunner,Completed game under time limit
|
||||
Game_Achievement_TrueEnding,Game.Achievement.TrueEnding,Unlocked true ending
|
||||
Game_Environment_Atmosphere_Safe,Game.Environment.Atmosphere.Safe,Safe zone atmosphere
|
||||
Game_Environment_Atmosphere_Neutral,Game.Environment.Atmosphere.Neutral,Neutral atmosphere
|
||||
Game_Environment_Atmosphere_Tense,Game.Environment.Atmosphere.Tense,Tension building
|
||||
Game_Environment_Atmosphere_Danger,Game.Environment.Atmosphere.Danger,Active danger
|
||||
Game_Environment_Atmosphere_Terror,Game.Environment.Atmosphere.Terror,Peak terror
|
||||
Game_Environment_Atmosphere_Eerie,Game.Environment.Atmosphere.Eerie,Eerie ambient
|
||||
Game_Environment_Scare,Game.Environment.Scare,Scare event namespace
|
||||
Game_Environment_Scare_MirrorJump,Game.Environment.Scare.MirrorJump,Mirror jump scare
|
||||
Game_Environment_Scare_CeilingDrop,Game.Environment.Scare.CeilingDrop,Body drops from ceiling
|
||||
Game_Environment_Scare_DoorSlam,Game.Environment.Scare.DoorSlam,Door slams behind player
|
||||
Game_Environment_Scare_Whisper,Game.Environment.Scare.Whisper,Ghost whisper audio
|
||||
Game_Environment_Scare_LightFlicker,Game.Environment.Scare.LightFlicker,Lights flicker then blackout
|
||||
Game_Environment_Light_Flicker,Game.Environment.Light.Flicker,Rapid flicker event
|
||||
Game_Environment_Light_Strobe,Game.Environment.Light.Strobe,Strobe light event
|
||||
Game_Environment_Light_Blackout,Game.Environment.Light.Blackout,Complete blackout event
|
||||
Game_Environment_Light_ColorShift,Game.Environment.Light.ColorShift,Color temperature shift
|
||||
Game_Environment_Pacing_Calm,Game.Environment.Pacing.Calm,Calm pacing band
|
||||
Game_Environment_Pacing_Exploration,Game.Environment.Pacing.Exploration,Exploration pacing
|
||||
Game_Environment_Pacing_Tension,Game.Environment.Pacing.Tension,Tension building
|
||||
Game_Environment_Pacing_Combat,Game.Environment.Pacing.Combat,Combat encounter
|
||||
Game_Environment_Pacing_Climax,Game.Environment.Pacing.Climax,Story climax
|
||||
Game_Environment_Pacing_Resolution,Game.Environment.Pacing.Resolution,Post-climax resolution
|
||||
Game_Environment_Performance_High,Game.Environment.Performance.High,High quality LOD target
|
||||
Game_Environment_Performance_Medium,Game.Environment.Performance.Medium,Medium quality
|
||||
Game_Environment_Performance_Low,Game.Environment.Performance.Low,Low quality LOD target
|
||||
|
37
docs/blueprints/01-core/data-tables/DT_Tags_Interaction.csv
Normal file
37
docs/blueprints/01-core/data-tables/DT_Tags_Interaction.csv
Normal file
@@ -0,0 +1,37 @@
|
||||
Name,Tag,DevComment
|
||||
Framework_Interaction_Type_Pickup,Framework.Interaction.Type.Pickup,Pick up an item
|
||||
Framework_Interaction_Type_Door,Framework.Interaction.Type.Door,Open/close a door
|
||||
Framework_Interaction_Type_Drawer,Framework.Interaction.Type.Drawer,Open/close a drawer
|
||||
Framework_Interaction_Type_Container,Framework.Interaction.Type.Container,Open/loot a container
|
||||
Framework_Interaction_Type_Inspect,Framework.Interaction.Type.Inspect,Examine an object in 3D
|
||||
Framework_Interaction_Type_Climb,Framework.Interaction.Type.Climb,Vault or mantle traversal
|
||||
Framework_Interaction_Type_Hide,Framework.Interaction.Type.Hide,Enter a hiding spot
|
||||
Framework_Interaction_Type_Use,Framework.Interaction.Type.Use,Use a world object (lever/button/terminal)
|
||||
Framework_Interaction_Type_Combine,Framework.Interaction.Type.Combine,Combine two items
|
||||
Framework_Interaction_Type_Grab,Framework.Interaction.Type.Grab,Grab a physics object
|
||||
Framework_Interaction_Type_Push,Framework.Interaction.Type.Push,Push a physics object
|
||||
Framework_Interaction_Type_Talk,Framework.Interaction.Type.Talk,Talk to an NPC
|
||||
Framework_Interaction_Type_Puzzle,Framework.Interaction.Type.Puzzle,Interact with a puzzle device
|
||||
Framework_Interaction_Context_Requires_Key,Framework.Interaction.Context.Requires.Key,Interaction requires a specific key item
|
||||
Framework_Interaction_Context_Requires_Item,Framework.Interaction.Context.Requires.Item,Interaction requires a specific item
|
||||
Framework_Interaction_Context_Locked,Framework.Interaction.Context.Locked,Interaction is locked
|
||||
Framework_Interaction_Context_Disabled,Framework.Interaction.Context.Disabled,Interaction is disabled
|
||||
Framework_Interaction_Context_OneShot,Framework.Interaction.Context.OneShot,Single-use interaction
|
||||
Framework_Interaction_Context_Cooldown,Framework.Interaction.Context.Cooldown,Interaction on cooldown
|
||||
Framework_Interaction_Prompt_Press,Framework.Interaction.Prompt.Press,Press-to-interact prompt
|
||||
Framework_Interaction_Prompt_Hold,Framework.Interaction.Prompt.Hold,Hold-to-interact prompt
|
||||
Framework_Interaction_Prompt_DoubleTap,Framework.Interaction.Prompt.DoubleTap,Double-tap prompt
|
||||
Framework_Interaction_HidingSpot_Locker,Framework.Interaction.HidingSpot.Locker,Full enclosure
|
||||
Framework_Interaction_HidingSpot_BehindCover,Framework.Interaction.HidingSpot.BehindCover,Behind low cover
|
||||
Framework_Interaction_HidingSpot_Under,Framework.Interaction.HidingSpot.Under,Under furniture
|
||||
Framework_Interaction_HidingSpot_Shadow,Framework.Interaction.HidingSpot.Shadow,In darkness
|
||||
Framework_Interaction_HidingSpot_Grass,Framework.Interaction.HidingSpot.Grass,In tall grass
|
||||
Framework_Interaction_Traversal_Vault,Framework.Interaction.Traversal.Vault,Vault over obstacle
|
||||
Framework_Interaction_Traversal_Mantle,Framework.Interaction.Traversal.Mantle,Mantle onto ledge
|
||||
Framework_Interaction_Traversal_Slide,Framework.Interaction.Traversal.Slide,Slide under barrier
|
||||
Framework_Interaction_Traversal_Squeeze,Framework.Interaction.Traversal.Squeeze,Squeeze through gap
|
||||
Framework_Interaction_Traversal_LedgeGrab,Framework.Interaction.Traversal.LedgeGrab,Grab ledge
|
||||
Framework_Interaction_Door_Closed,Framework.Interaction.Door.Closed,Door state closed
|
||||
Framework_Interaction_Door_Open,Framework.Interaction.Door.Open,Door state open
|
||||
Framework_Interaction_Door_Locked,Framework.Interaction.Door.Locked,Door locked
|
||||
Framework_Interaction_Door_Barricaded,Framework.Interaction.Door.Barricaded,Door barricaded
|
||||
|
28
docs/blueprints/01-core/data-tables/DT_Tags_Item.csv
Normal file
28
docs/blueprints/01-core/data-tables/DT_Tags_Item.csv
Normal file
@@ -0,0 +1,28 @@
|
||||
Name,Tag,DevComment
|
||||
Framework_Item_Type_Weapon,Framework.Item.Type.Weapon,Firearm or melee weapon
|
||||
Framework_Item_Type_Consumable,Framework.Item.Type.Consumable,Health pack stim stamina item
|
||||
Framework_Item_Type_KeyItem,Framework.Item.Type.KeyItem,Story key item
|
||||
Framework_Item_Type_Document,Framework.Item.Type.Document,Readable document or note
|
||||
Framework_Item_Type_Collectible,Framework.Item.Type.Collectible,Collectible with set tracking
|
||||
Framework_Item_Type_Ammo,Framework.Item.Type.Ammo,Ammunition pickup
|
||||
Framework_Item_Type_Tool,Framework.Item.Type.Tool,Flashlight lockpick etc
|
||||
Framework_Item_Type_Resource,Framework.Item.Type.Resource,Crafting resource
|
||||
Framework_Item_Type_Misc,Framework.Item.Type.Misc,Miscellaneous item
|
||||
Framework_Item_Slot_PrimaryWeapon,Framework.Item.Slot.PrimaryWeapon,Equipped primary weapon slot
|
||||
Framework_Item_Slot_SecondaryWeapon,Framework.Item.Slot.SecondaryWeapon,Equipped secondary weapon slot
|
||||
Framework_Item_Slot_Flashlight,Framework.Item.Slot.Flashlight,Equipped flashlight slot
|
||||
Framework_Item_Slot_Shield,Framework.Item.Slot.Shield,Equipped shield slot
|
||||
Framework_Item_Slot_Active,Framework.Item.Slot.Active,Active quick-slot item
|
||||
Framework_Item_Slot_Quick1,Framework.Item.Slot.Quick1,Quick slot 1
|
||||
Framework_Item_Slot_Quick2,Framework.Item.Slot.Quick2,Quick slot 2
|
||||
Framework_Item_Slot_Quick3,Framework.Item.Slot.Quick3,Quick slot 3
|
||||
Framework_Item_Slot_Quick4,Framework.Item.Slot.Quick4,Quick slot 4
|
||||
Framework_Item_Rarity_Trash,Framework.Item.Rarity.Trash,Trash tier loot
|
||||
Framework_Item_Rarity_Common,Framework.Item.Rarity.Common,Common tier loot
|
||||
Framework_Item_Rarity_Uncommon,Framework.Item.Rarity.Uncommon,Uncommon tier loot
|
||||
Framework_Item_Rarity_Rare,Framework.Item.Rarity.Rare,Rare tier loot
|
||||
Framework_Item_Rarity_Legendary,Framework.Item.Rarity.Legendary,Legendary tier loot
|
||||
Framework_Item_Context_Stackable,Framework.Item.Context.Stackable,Item can stack in inventory
|
||||
Framework_Item_Context_Unique,Framework.Item.Context.Unique,Only one can exist
|
||||
Framework_Item_Context_Quest,Framework.Item.Context.Quest,Quest-related item
|
||||
Framework_Item_Context_Droppable,Framework.Item.Context.Droppable,Can be dropped from inventory
|
||||
|
55
docs/blueprints/01-core/data-tables/DT_Tags_Narrative.csv
Normal file
55
docs/blueprints/01-core/data-tables/DT_Tags_Narrative.csv
Normal file
@@ -0,0 +1,55 @@
|
||||
Name,Tag,DevComment
|
||||
Game_Narrative_Flag,Game.Narrative.Flag,Root narrative flag namespace
|
||||
Game_Narrative_Phase,Game.Narrative.Phase,Story phase namespace
|
||||
Game_Narrative_Choice,Game.Narrative.Choice,Dialogue choice consequence tags
|
||||
Game_Narrative_Ending,Game.Narrative.Ending,Ending evaluation tags
|
||||
Game_Narrative_Flag_PrologueComplete,Game.Narrative.Flag.PrologueComplete,Prologue chapter completed
|
||||
Game_Narrative_Flag_Chapter1Complete,Game.Narrative.Flag.Chapter1Complete,Chapter 1 completed
|
||||
Game_Narrative_Flag_Chapter2Complete,Game.Narrative.Flag.Chapter2Complete,Chapter 2 completed
|
||||
Game_Narrative_Flag_Chapter3Complete,Game.Narrative.Flag.Chapter3Complete,Chapter 3 completed
|
||||
Game_Narrative_Flag_Act1Complete,Game.Narrative.Flag.Act1Complete,Act 1 completed
|
||||
Game_Narrative_Flag_Act2Complete,Game.Narrative.Flag.Act2Complete,Act 2 completed
|
||||
Game_Narrative_Flag_Act3Complete,Game.Narrative.Flag.Act3Complete,Act 3 completed
|
||||
Game_Narrative_Flag_FoundKey,Game.Narrative.Flag.FoundKey,Player found the basement key
|
||||
Game_Narrative_Flag_BasementDoorOpened,Game.Narrative.Flag.BasementDoorOpened,Basement door unlocked
|
||||
Game_Narrative_Flag_SawMonster,Game.Narrative.Flag.SawMonster,Player first saw the monster
|
||||
Game_Narrative_Flag_SavedNPC,Game.Narrative.Flag.SavedNPC,Player saved the trapped NPC
|
||||
Game_Narrative_Phase_Act1,Game.Narrative.Phase.Act1,Story phase: Act 1
|
||||
Game_Narrative_Phase_Act2,Game.Narrative.Phase.Act2,Story phase: Act 2
|
||||
Game_Narrative_Phase_Act3,Game.Narrative.Phase.Act3,Story phase: Act 3
|
||||
Game_Narrative_Phase_Prologue,Game.Narrative.Phase.Prologue,Story phase: Prologue
|
||||
Game_Narrative_Phase_Epilogue,Game.Narrative.Phase.Epilogue,Story phase: Epilogue
|
||||
Game_Narrative_Phase_Chapter1,Game.Narrative.Phase.Chapter1,Chapter 1: The Awakening
|
||||
Game_Narrative_Phase_Chapter2,Game.Narrative.Phase.Chapter2,Chapter 2: Into Darkness
|
||||
Game_Narrative_Phase_Chapter3,Game.Narrative.Phase.Chapter3,Chapter 3: The Truth
|
||||
Game_Narrative_Phase_Chapter4,Game.Narrative.Phase.Chapter4,Chapter 4: Confrontation
|
||||
Game_Narrative_Choice_SparedEnemy,Game.Narrative.Choice.SparedEnemy,Player chose to spare
|
||||
Game_Narrative_Choice_KilledEnemy,Game.Narrative.Choice.KilledEnemy,Player chose to kill
|
||||
Game_Narrative_Choice_AcceptedDeal,Game.Narrative.Choice.AcceptedDeal,Player accepted the deal
|
||||
Game_Narrative_Choice_RejectedDeal,Game.Narrative.Choice.RejectedDeal,Player rejected the deal
|
||||
Game_Narrative_Choice_RevealedSecret,Game.Narrative.Choice.RevealedSecret,Player revealed the secret
|
||||
Game_Narrative_Choice_KeptSilent,Game.Narrative.Choice.KeptSilent,Player stayed silent
|
||||
Game_Narrative_Ending_Good,Game.Narrative.Ending.Good,Good ending path
|
||||
Game_Narrative_Ending_Bad,Game.Narrative.Ending.Bad,Bad ending path
|
||||
Game_Narrative_Ending_True,Game.Narrative.Ending.True,True/secret ending path
|
||||
Game_Narrative_Ending_Sacrifice,Game.Narrative.Ending.Sacrifice,Sacrifice ending path
|
||||
Game_Narrative_Ending_Escape,Game.Narrative.Ending.Escape,Escape ending path
|
||||
Game_Narrative_Trial_HospitalEscape,Game.Narrative.Trial.HospitalEscape,Trial: escape the hospital
|
||||
Game_Narrative_Trial_StealthBasement,Game.Narrative.Trial.StealthBasement,Trial: stealth through basement
|
||||
Game_Narrative_Trial_SurviveSiege,Game.Narrative.Trial.SurviveSiege,Trial: survive the creature siege
|
||||
Game_Narrative_Trial_Timebomb,Game.Narrative.Trial.Timebomb,Trial: defuse before timer expires
|
||||
Game_Narrative_Cutscene_Intro,Game.Narrative.Cutscene.Intro,Intro cutscene
|
||||
Game_Narrative_Cutscene_Act1End,Game.Narrative.Cutscene.Act1End,Act 1 ending cutscene
|
||||
Game_Narrative_Cutscene_Act2End,Game.Narrative.Cutscene.Act2End,Act 2 ending cutscene
|
||||
Game_Narrative_Cutscene_Finale,Game.Narrative.Cutscene.Finale,Final confrontation cutscene
|
||||
Game_Narrative_Lore_Journal1,Game.Narrative.Lore.Journal1,Lore entry: Dr. Voss Journal 1
|
||||
Game_Narrative_Lore_Newspaper,Game.Narrative.Lore.Newspaper,Lore entry: Abandoned Newspaper
|
||||
Game_Narrative_Lore_AudioLog,Game.Narrative.Lore.AudioLog,Lore entry: Audio Log
|
||||
Framework_Objective_Status_Active,Framework.Objective.Status.Active,Objective currently active
|
||||
Framework_Objective_Status_Complete,Framework.Objective.Status.Complete,Objective completed
|
||||
Framework_Objective_Status_Failed,Framework.Objective.Status.Failed,Objective failed
|
||||
Framework_Objective_Status_Hidden,Framework.Objective.Status.Hidden,Objective hidden until discovered
|
||||
Framework_Objective_Category_Main,Framework.Objective.Category.Main,Main quest objective
|
||||
Framework_Objective_Category_Side,Framework.Objective.Category.Side,Optional side objective
|
||||
Framework_Objective_Category_Hidden,Framework.Objective.Category.Hidden,Secret objective
|
||||
Framework_Objective_Category_Tutorial,Framework.Objective.Category.Tutorial,Tutorial objective
|
||||
|
35
docs/blueprints/01-core/data-tables/DT_Tags_Player.csv
Normal file
35
docs/blueprints/01-core/data-tables/DT_Tags_Player.csv
Normal file
@@ -0,0 +1,35 @@
|
||||
Name,Tag,DevComment
|
||||
Framework_Player_State_Alive,Framework.Player.State.Alive,Player is alive and active
|
||||
Framework_Player_State_Dead,Framework.Player.State.Dead,Player health reached zero
|
||||
Framework_Player_State_Dying,Framework.Player.State.Dying,Player in downed-but-not-dead state
|
||||
Framework_Player_State_Hidden,Framework.Player.State.Hidden,Player inside a hiding spot
|
||||
Framework_Player_State_Interacting,Framework.Player.State.Interacting,Player using an object or UI
|
||||
Framework_Player_Stress_Low,Framework.Player.Stress.Low,Calm or slightly uneasy
|
||||
Framework_Player_Stress_Mid,Framework.Player.Stress.Mid,Distressed
|
||||
Framework_Player_Stress_High,Framework.Player.Stress.High,Panicked
|
||||
Framework_Player_Stress_Critical,Framework.Player.Stress.Critical,Terrified or catatonic
|
||||
Framework_Player_Posture_Standing,Framework.Player.Posture.Standing,Default upright posture
|
||||
Framework_Player_Posture_Crouching,Framework.Player.Posture.Crouching,Crouch-walking or stealth
|
||||
Framework_Player_Posture_Prone,Framework.Player.Posture.Prone,Lying flat
|
||||
Framework_Player_Posture_Vaulting,Framework.Player.Posture.Vaulting,Climbing or vaulting obstacle
|
||||
Framework_Player_Movement_Idle,Framework.Player.Movement.Idle,Not moving
|
||||
Framework_Player_Movement_Walking,Framework.Player.Movement.Walking,Slow movement
|
||||
Framework_Player_Movement_Jogging,Framework.Player.Movement.Jogging,Normal movement
|
||||
Framework_Player_Movement_Sprinting,Framework.Player.Movement.Sprinting,Fast movement with stamina drain
|
||||
Framework_Player_Movement_Sneaking,Framework.Player.Movement.Sneaking,Quiet crouched movement
|
||||
Framework_Player_Camera_Default,Framework.Player.Camera.Default,Normal camera
|
||||
Framework_Player_Camera_Aiming,Framework.Player.Camera.Aiming,ADS zoomed camera
|
||||
Framework_Player_Camera_Peeking,Framework.Player.Camera.Peeking,Camera offset for hiding peek
|
||||
Framework_Player_Camera_Injured,Framework.Player.Camera.Injured,Low-health camera effects
|
||||
Framework_Player_Body_FullBody,Framework.Player.Body.FullBody,Third-person or mirror mode
|
||||
Framework_Player_Body_ArmsOnly,Framework.Player.Body.ArmsOnly,Default first-person
|
||||
Framework_Player_Body_ArmsAndShadow,Framework.Player.Body.ArmsAndShadow,Arms plus shadow casting
|
||||
Framework_Player_Body_Hidden,Framework.Player.Body.Hidden,No body visible (cutscenes/UI)
|
||||
Framework_Player_Overlay_Clean,Framework.Player.Overlay.Clean,No body overlay
|
||||
Framework_Player_Overlay_Blood,Framework.Player.Overlay.Blood,Blood splatter on body
|
||||
Framework_Player_Overlay_Water,Framework.Player.Overlay.Water,Water droplets on body
|
||||
Framework_Player_Overlay_Mud,Framework.Player.Overlay.Mud,Mud/dirt on body
|
||||
Framework_Player_Vitals_Stamina_Normal,Framework.Player.Vitals.Stamina.Normal,Full stamina
|
||||
Framework_Player_Vitals_Stamina_Low,Framework.Player.Vitals.Stamina.Low,Below low threshold
|
||||
Framework_Player_Vitals_Stamina_Exhausted,Framework.Player.Vitals.Stamina.Exhausted,Below exhausted threshold
|
||||
Framework_Player_Vitals_Health_Critical,Framework.Player.Vitals.Health.Critical,Below critical health threshold
|
||||
|
25
docs/blueprints/01-core/data-tables/DT_Tags_Save.csv
Normal file
25
docs/blueprints/01-core/data-tables/DT_Tags_Save.csv
Normal file
@@ -0,0 +1,25 @@
|
||||
Name,Tag,DevComment
|
||||
Framework_Save_Type_Checkpoint,Framework.Save.Type.Checkpoint,Checkpoint auto-save
|
||||
Framework_Save_Type_HardSave,Framework.Save.Type.HardSave,Manual save slot
|
||||
Framework_Save_Type_AutoSave,Framework.Save.Type.AutoSave,Periodic auto-save
|
||||
Framework_Save_Type_ChapterTransition,Framework.Save.Type.ChapterTransition,Save on chapter load
|
||||
Framework_Save_Slot_1,Framework.Save.Slot.1,Save slot 1
|
||||
Framework_Save_Slot_2,Framework.Save.Slot.2,Save slot 2
|
||||
Framework_Save_Slot_3,Framework.Save.Slot.3,Save slot 3
|
||||
Framework_Save_Slot_Auto,Framework.Save.Slot.Auto,Autosave slot
|
||||
Framework_Save_Context_NewGame,Framework.Save.Context.NewGame,First save of new game
|
||||
Framework_Save_Context_Continue,Framework.Save.Context.Continue,Load existing save
|
||||
Framework_Save_Context_Delete,Framework.Save.Context.Delete,Delete save slot
|
||||
Framework_Save_Context_Corrupted,Framework.Save.Context.Corrupted,Save file corrupted
|
||||
Framework_DeathSpace_Active,Framework.DeathSpace.Active,Player in alternate death space
|
||||
Framework_DeathSpace_Entered,Framework.DeathSpace.Entered,Player entered death space
|
||||
Framework_DeathSpace_Exited,Framework.DeathSpace.Exited,Player exited death space
|
||||
Framework_DeathSpace_FoundExit,Framework.DeathSpace.FoundExit,Player found the exit
|
||||
Framework_Checkpoint_Active,Framework.Checkpoint.Active,Current active checkpoint
|
||||
Framework_Checkpoint_Previous,Framework.Checkpoint.Previous,Previous checkpoint
|
||||
Framework_Checkpoint_Reached,Framework.Checkpoint.Reached,A checkpoint was triggered
|
||||
Framework_Respawn_Point,Framework.Respawn.Point,Default respawn location
|
||||
Framework_Respawn_Start,Framework.Respawn.Start,Respawn sequence started
|
||||
Framework_Respawn_Complete,Framework.Respawn.Complete,Respawn sequence completed
|
||||
Framework_RunHistory_Death,Framework.RunHistory.Death,Death recorded in run history
|
||||
Framework_RunHistory_Chapter,Framework.RunHistory.Chapter,Chapter completed in run history
|
||||
|
43
docs/blueprints/01-core/data-tables/DT_Tags_State.csv
Normal file
43
docs/blueprints/01-core/data-tables/DT_Tags_State.csv
Normal file
@@ -0,0 +1,43 @@
|
||||
Name,Tag,DevComment
|
||||
Framework_State_Action_Fire,Framework.State.Action.Fire,Firing a weapon
|
||||
Framework_State_Action_Reload,Framework.State.Action.Reload,Reloading a weapon
|
||||
Framework_State_Action_Melee,Framework.State.Action.Melee,Melee attack
|
||||
Framework_State_Action_UseItem,Framework.State.Action.UseItem,Using an item
|
||||
Framework_State_Action_Interact,Framework.State.Action.Interact,Interacting with world object
|
||||
Framework_State_Action_OpenMenu,Framework.State.Action.OpenMenu,Opening a menu
|
||||
Framework_State_Action_Hide,Framework.State.Action.Hide,Entering/occupying hiding spot
|
||||
Framework_State_Action_Peek,Framework.State.Action.Peek,Peeking from hiding
|
||||
Framework_State_Action_Sprint,Framework.State.Action.Sprint,Sprinting
|
||||
Framework_State_Action_Jump,Framework.State.Action.Jump,Jumping
|
||||
Framework_State_Action_Dodge,Framework.State.Action.Dodge,Dodging/evading
|
||||
Framework_State_Action_Climb,Framework.State.Action.Climb,Climbing/vaulting
|
||||
Framework_State_Action_Slide,Framework.State.Action.Slide,Sliding
|
||||
Framework_State_Action_Squeeze,Framework.State.Action.Squeeze,Squeezing through gap
|
||||
Framework_State_Action_Grab,Framework.State.Action.Grab,Grabbing a physics object
|
||||
Framework_State_Action_Throw,Framework.State.Action.Throw,Throwing a held object
|
||||
Framework_State_Action_Aim,Framework.State.Action.Aim,Aiming down sights
|
||||
Framework_State_Action_Equip,Framework.State.Action.Equip,Equipping a weapon/item
|
||||
Framework_State_Action_Holster,Framework.State.Action.Holster,Holstering a weapon
|
||||
Framework_State_Action_Consume,Framework.State.Action.Consume,Consuming a consumable
|
||||
Framework_State_Action_Inspect,Framework.State.Action.Inspect,Inspecting an object
|
||||
Framework_State_Action_ReadDocument,Framework.State.Action.ReadDocument,Reading a document
|
||||
Framework_State_Action_Dialogue,Framework.State.Action.Dialogue,In dialogue
|
||||
Framework_State_Action_Choosing,Framework.State.Action.Choosing,Making a dialogue choice
|
||||
Framework_State_Action_Cutscene,Framework.State.Action.Cutscene,In a cutscene
|
||||
Framework_State_Overlay_Menu,Framework.State.Overlay.Menu,Menu overlay active
|
||||
Framework_State_Overlay_Inventory,Framework.State.Overlay.Inventory,Inventory screen open
|
||||
Framework_State_Overlay_Journal,Framework.State.Overlay.Journal,Journal screen open
|
||||
Framework_State_Overlay_Map,Framework.State.Overlay.Map,Map screen open
|
||||
Framework_State_Overlay_Settings,Framework.State.Overlay.Settings,Settings screen open
|
||||
Framework_State_Overlay_Dialogue,Framework.State.Overlay.Dialogue,Dialogue overlay active
|
||||
Framework_State_Overlay_Death,Framework.State.Overlay.Death,Death overlay active
|
||||
Framework_State_Overlay_Loading,Framework.State.Overlay.Loading,Loading screen active
|
||||
Framework_State_Vital_Health,Framework.State.Vital.Health,Health vital signal
|
||||
Framework_State_Vital_Stamina,Framework.State.Vital.Stamina,Stamina vital signal
|
||||
Framework_State_Vital_Stress,Framework.State.Vital.Stress,Stress vital signal
|
||||
Framework_State_Vital_Fear,Framework.State.Vital.Fear,Fear vital signal
|
||||
Framework_State_Gating_BlockSprint,Framework.State.Gating.BlockSprint,Gate: sprint blocked
|
||||
Framework_State_Gating_BlockJump,Framework.State.Gating.BlockJump,Gate: jump blocked
|
||||
Framework_State_Gating_BlockInteract,Framework.State.Gating.BlockInteract,Gate: interaction blocked
|
||||
Framework_State_Gating_BlockFire,Framework.State.Gating.BlockFire,Gate: fire blocked
|
||||
Framework_State_Gating_BlockReload,Framework.State.Gating.BlockReload,Gate: reload blocked
|
||||
|
@@ -1,5 +1,9 @@
|
||||
# 08 — Health System (`BPC_HealthSystem`)
|
||||
|
||||
> **⚡ C++ Status: Stub** — `Source/PG_Framework/Public/Player/BPC_HealthSystem.h` provides the UCLASS shell, `MaxHealth`/`CurrentHealth` variables, `OnHealthChanged` and `OnDeath` event dispatchers. **The C++ stub has NO gameplay logic** — it exists so other C++ classes (`BPC_StateManager`, `BPC_DamageReceptionSystem`) can forward-reference it. **Create a BP child** and build ALL logic from this spec: `TakeDamage()`, `Heal()`, `OnDeath()` transition, damage resistance modifiers, health regen tick, `I_Damageable` interface. See `docs/developer/cpp-integration-guide.md` for setup steps.
|
||||
>
|
||||
> ---
|
||||
|
||||
## Purpose
|
||||
Manages the player's health pool, damage application with type-based resistance, health regeneration, death state, and critical-health events. Serves as the central survivability component on the player character.
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 09 — Stamina System (`BPC_StaminaSystem`)
|
||||
|
||||
> **⚡ C++ Status: Stub** — `Source/PG_Framework/Public/Player/BPC_StaminaSystem.h` provides the UCLASS shell, `MaxStamina`/`CurrentStamina` variables, and `OnExhaustionStateChanged` dispatcher. **The C++ stub has NO gameplay logic.** **Create a BP child** and build ALL logic: sprint drain, action costs, exhaustion state machine, regen delay + rate, `CanAffordAction(Cost)` query. See `docs/developer/cpp-integration-guide.md`.
|
||||
>
|
||||
> ---
|
||||
|
||||
## Purpose
|
||||
Manages the player's stamina pool — drain during sprinting, jumping, and special actions; regeneration during idle/walking. Prevents action spamming by enforcing minimum stamina thresholds. Drives exhaustion VFX and audio cues.
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 10 — Stress / Sanity System (`BPC_StressSystem`)
|
||||
|
||||
> **⚡ C++ Status: Stub** — `Source/PG_Framework/Public/Player/BPC_StressSystem.h` provides the UCLASS shell, `EStressTier` enum (Calm→Catatonic), `StressTier` variable, and `OnStressTierChanged` dispatcher. **The C++ stub has NO gameplay logic.** **Create a BP child** and build ALL logic: stress accumulation sources, decay during safety, tier transitions, hallucination thresholds. See `docs/developer/cpp-integration-guide.md`.
|
||||
>
|
||||
> ---
|
||||
|
||||
## Purpose
|
||||
Tracks the player's psychological state via a stress meter that accumulates from environmental horror, enemy proximity, health deficits, and narrative events. Drives visual distortion, audio hallucinations, gameplay penalties, and narrative branching based on sanity thresholds.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user