claude-code-arcane 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +131 -41
- package/agents/game/ue-blueprint-specialist.md +125 -0
- package/agents/game/ue-gas-specialist.md +133 -0
- package/agents/game/ue-umg-specialist.md +131 -0
- package/dist/cli.js +642 -478
- package/docs/SKILLS-CATALOG.md +42 -3
- package/docs/USER-GUIDE.md +28 -54
- package/hooks/detect-unreal.sh +90 -0
- package/package.json +2 -1
- package/profiles/agile.yaml +1 -1
- package/profiles/ai.yaml +1 -1
- package/profiles/android-native.yaml +1 -1
- package/profiles/audio.yaml +1 -1
- package/profiles/backend-dotnet.yaml +1 -1
- package/profiles/backend-go.yaml +1 -1
- package/profiles/backend-nestjs.yaml +1 -1
- package/profiles/backend-nextjs.yaml +1 -1
- package/profiles/backend-ts.yaml +1 -1
- package/profiles/business.yaml +1 -1
- package/profiles/clevel.yaml +1 -1
- package/profiles/clickup.yaml +1 -1
- package/profiles/core.yaml +1 -1
- package/profiles/database.yaml +1 -1
- package/profiles/design.yaml +1 -1
- package/profiles/docs.yaml +1 -1
- package/profiles/ecommerce.yaml +1 -1
- package/profiles/finance.yaml +1 -1
- package/profiles/flutter.yaml +1 -1
- package/profiles/freelance.yaml +1 -1
- package/profiles/frontend.yaml +1 -1
- package/profiles/infra.yaml +1 -1
- package/profiles/integrations.yaml +1 -1
- package/profiles/ios-native.yaml +1 -1
- package/profiles/jira.yaml +1 -1
- package/profiles/job-hunt.yaml +1 -1
- package/profiles/marketing.yaml +1 -1
- package/profiles/mobile.yaml +1 -1
- package/profiles/regulatory.yaml +1 -1
- package/profiles/second-brain.yaml +1 -1
- package/profiles/security.yaml +1 -1
- package/profiles/self-improving.yaml +1 -1
- package/profiles/statusline.yaml +1 -1
- package/profiles/testing.yaml +1 -1
- package/profiles/unity-design.yaml +1 -1
- package/profiles/unity-dev.yaml +1 -1
- package/profiles/unreal-dev.yaml +97 -0
- package/profiles/visual-novel.yaml +1 -1
- package/rules/gamedev/unreal-code.md +125 -0
- package/skills/arcane-list/SKILL.md +12 -7
- package/skills/install-mcp/SKILL.md +135 -46
- package/skills/install-mcp/references/unreal-operations.md +55 -0
- package/skills/ue-actor-component-architecture/SKILL.md +509 -0
- package/skills/ue-actor-component-architecture/references/actor-lifecycle.md +301 -0
- package/skills/ue-actor-component-architecture/references/component-types.md +592 -0
- package/skills/ue-ai-navigation/SKILL.md +526 -0
- package/skills/ue-ai-navigation/references/behavior-tree-patterns.md +392 -0
- package/skills/ue-ai-navigation/references/behavior-tree-workflow.md +107 -0
- package/skills/ue-ai-navigation/references/custom-bttask.md +104 -0
- package/skills/ue-ai-navigation/references/eqs-reference.md +423 -0
- package/skills/ue-animation-system/SKILL.md +521 -0
- package/skills/ue-animation-system/references/anim-notify-reference.md +486 -0
- package/skills/ue-animation-system/references/locomotion-setup.md +550 -0
- package/skills/ue-async-threading/SKILL.md +433 -0
- package/skills/ue-async-threading/references/thread-safety-guide.md +213 -0
- package/skills/ue-async-threading/references/threading-patterns.md +345 -0
- package/skills/ue-audio-system/SKILL.md +527 -0
- package/skills/ue-audio-system/references/audio-setup-patterns.md +700 -0
- package/skills/ue-blueprints/SKILL.md +134 -0
- package/skills/ue-blueprints/references/communication.md +82 -0
- package/skills/ue-character-movement/SKILL.md +475 -0
- package/skills/ue-character-movement/references/cmc-extension-patterns.md +335 -0
- package/skills/ue-character-movement/references/movement-pipeline.md +189 -0
- package/skills/ue-cpp-foundations/SKILL.md +533 -0
- package/skills/ue-cpp-foundations/references/components-and-gc.md +107 -0
- package/skills/ue-cpp-foundations/references/container-patterns.md +544 -0
- package/skills/ue-cpp-foundations/references/delegate-patterns.md +497 -0
- package/skills/ue-cpp-foundations/references/gameplay-classes-cpp.md +136 -0
- package/skills/ue-cpp-foundations/references/property-specifiers.md +306 -0
- package/skills/ue-data-assets-tables/SKILL.md +527 -0
- package/skills/ue-data-assets-tables/references/asset-loading-patterns.md +306 -0
- package/skills/ue-data-assets-tables/references/data-driven-design.md +378 -0
- package/skills/ue-editor-tools/SKILL.md +523 -0
- package/skills/ue-editor-tools/references/detail-customization-patterns.md +395 -0
- package/skills/ue-editor-tools/references/editor-module-setup.md +480 -0
- package/skills/ue-game-features/SKILL.md +477 -0
- package/skills/ue-game-features/references/experience-system.md +222 -0
- package/skills/ue-game-features/references/game-feature-patterns.md +308 -0
- package/skills/ue-gameplay-abilities/SKILL.md +518 -0
- package/skills/ue-gameplay-abilities/references/ability-task-reference.md +391 -0
- package/skills/ue-gameplay-abilities/references/gameplay-effect-reference.md +397 -0
- package/skills/ue-gameplay-abilities/references/gas-setup-patterns.md +369 -0
- package/skills/ue-gameplay-framework/SKILL.md +466 -0
- package/skills/ue-gameplay-framework/references/framework-class-map.md +243 -0
- package/skills/ue-input-system/SKILL.md +394 -0
- package/skills/ue-input-system/references/cpp-setup.md +131 -0
- package/skills/ue-input-system/references/enhanced-input-workflow.md +117 -0
- package/skills/ue-input-system/references/input-action-reference.md +731 -0
- package/skills/ue-mass-entity/SKILL.md +476 -0
- package/skills/ue-mass-entity/references/mass-entity-patterns.md +319 -0
- package/skills/ue-mass-entity/references/mass-fragment-reference.md +119 -0
- package/skills/ue-materials-rendering/SKILL.md +526 -0
- package/skills/ue-materials-rendering/references/material-parameter-reference.md +216 -0
- package/skills/ue-materials-rendering/references/post-process-settings.md +383 -0
- package/skills/ue-module-build-system/SKILL.md +487 -0
- package/skills/ue-module-build-system/references/build-cs-reference.md +418 -0
- package/skills/ue-module-build-system/references/common-build-errors.md +381 -0
- package/skills/ue-naming-conventions/SKILL.md +142 -0
- package/skills/ue-naming-conventions/references/asset-name-modifiers.md +233 -0
- package/skills/ue-naming-conventions/references/content-structure.md +83 -0
- package/skills/ue-networking-replication/SKILL.md +452 -0
- package/skills/ue-networking-replication/references/replication-patterns.md +526 -0
- package/skills/ue-networking-replication/references/rpc-decision-guide.md +393 -0
- package/skills/ue-niagara-effects/SKILL.md +531 -0
- package/skills/ue-niagara-effects/references/niagara-data-interfaces.md +491 -0
- package/skills/ue-niagara-effects/references/niagara-parameter-types.md +153 -0
- package/skills/ue-niagara-effects/references/niagara-workflow.md +115 -0
- package/skills/ue-packaging/SKILL.md +116 -0
- package/skills/ue-physics-collision/SKILL.md +423 -0
- package/skills/ue-physics-collision/references/collision-channel-setup.md +218 -0
- package/skills/ue-physics-collision/references/trace-patterns.md +499 -0
- package/skills/ue-procedural-generation/SKILL.md +505 -0
- package/skills/ue-procedural-generation/references/pcg-node-reference.md +381 -0
- package/skills/ue-procedural-generation/references/procedural-mesh-patterns.md +940 -0
- package/skills/ue-project-context/SKILL.md +305 -0
- package/skills/ue-sequencer-cinematics/SKILL.md +506 -0
- package/skills/ue-sequencer-cinematics/references/sequencer-patterns.md +629 -0
- package/skills/ue-serialization-savegames/SKILL.md +524 -0
- package/skills/ue-serialization-savegames/references/save-system-architecture.md +705 -0
- package/skills/ue-state-trees/SKILL.md +489 -0
- package/skills/ue-state-trees/references/state-tree-mass-integration.md +184 -0
- package/skills/ue-state-trees/references/state-tree-patterns.md +321 -0
- package/skills/ue-testing-debugging/SKILL.md +520 -0
- package/skills/ue-testing-debugging/references/automation-test-patterns.md +387 -0
- package/skills/ue-testing-debugging/references/profiling-commands.md +357 -0
- package/skills/ue-ui-umg-slate/SKILL.md +431 -0
- package/skills/ue-ui-umg-slate/references/common-ui-setup.md +484 -0
- package/skills/ue-ui-umg-slate/references/widget-types.md +487 -0
- package/skills/ue-world-level-streaming/SKILL.md +518 -0
- package/skills/ue-world-level-streaming/references/streaming-patterns.md +414 -0
- package/templates/gamedev/unreal-project-context.md +107 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Asset Loading Patterns
|
|
2
|
+
|
|
3
|
+
Reference guide for async loading, `FStreamableManager`, load grouping, and handle lifecycle in Unreal Engine 5.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## FStreamableManager Access
|
|
8
|
+
|
|
9
|
+
`FStreamableManager` is a member of `UAssetManager`. Do not create your own instances in game code — share the one owned by the Asset Manager.
|
|
10
|
+
|
|
11
|
+
```cpp
|
|
12
|
+
// Preferred access (UE5):
|
|
13
|
+
FStreamableManager& SM = UAssetManager::GetStreamableManager();
|
|
14
|
+
// Equivalent to: UAssetManager::Get().StreamableManager
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Module dependency required in `Build.cs`:
|
|
18
|
+
```csharp
|
|
19
|
+
PublicDependencyModuleNames.AddRange(new string[] { "Engine", "AssetManager" });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Pattern 1: Single Asset, Callback on Completion
|
|
25
|
+
|
|
26
|
+
```cpp
|
|
27
|
+
// Header
|
|
28
|
+
TSharedPtr<FStreamableHandle> LoadHandle;
|
|
29
|
+
|
|
30
|
+
UPROPERTY(EditDefaultsOnly)
|
|
31
|
+
TSoftObjectPtr<UStaticMesh> WeaponMeshSoft;
|
|
32
|
+
|
|
33
|
+
// Load
|
|
34
|
+
void AMyCharacter::BeginLoadWeaponMesh()
|
|
35
|
+
{
|
|
36
|
+
FStreamableManager& SM = UAssetManager::GetStreamableManager();
|
|
37
|
+
|
|
38
|
+
LoadHandle = SM.RequestAsyncLoad(
|
|
39
|
+
WeaponMeshSoft.ToSoftObjectPath(),
|
|
40
|
+
FStreamableDelegate::CreateUObject(this, &AMyCharacter::OnWeaponMeshReady));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void AMyCharacter::OnWeaponMeshReady()
|
|
44
|
+
{
|
|
45
|
+
// At callback time, IsValid() returns true if successfully loaded.
|
|
46
|
+
if (WeaponMeshSoft.IsValid())
|
|
47
|
+
{
|
|
48
|
+
WeaponMeshComp->SetStaticMesh(WeaponMeshSoft.Get());
|
|
49
|
+
}
|
|
50
|
+
// Handle can now be released; assets stay in memory while referenced by component.
|
|
51
|
+
LoadHandle.Reset();
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Pattern 2: Multiple Assets, Single Callback
|
|
58
|
+
|
|
59
|
+
Load a batch and receive one callback when all are ready.
|
|
60
|
+
|
|
61
|
+
```cpp
|
|
62
|
+
void UMyLoadSystem::LoadUIAssets(const TArray<TSoftObjectPtr<UTexture2D>>& IconRefs)
|
|
63
|
+
{
|
|
64
|
+
TArray<FSoftObjectPath> Paths;
|
|
65
|
+
Paths.Reserve(IconRefs.Num());
|
|
66
|
+
for (const TSoftObjectPtr<UTexture2D>& Ref : IconRefs)
|
|
67
|
+
{
|
|
68
|
+
if (!Ref.IsNull())
|
|
69
|
+
{
|
|
70
|
+
Paths.Add(Ref.ToSoftObjectPath());
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Paths.IsEmpty()) { return; }
|
|
75
|
+
|
|
76
|
+
FStreamableManager& SM = UAssetManager::GetStreamableManager();
|
|
77
|
+
UILoadHandle = SM.RequestAsyncLoad(
|
|
78
|
+
Paths,
|
|
79
|
+
FStreamableDelegate::CreateUObject(this, &UMyLoadSystem::OnUIAssetsLoaded));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
void UMyLoadSystem::OnUIAssetsLoaded()
|
|
83
|
+
{
|
|
84
|
+
// Retrieve loaded assets from handle.
|
|
85
|
+
TArray<UTexture2D*> LoadedIcons;
|
|
86
|
+
UILoadHandle->GetLoadedAssets(LoadedIcons);
|
|
87
|
+
|
|
88
|
+
for (UTexture2D* Icon : LoadedIcons)
|
|
89
|
+
{
|
|
90
|
+
if (Icon) { /* bind to widget */ }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Pattern 3: High Priority / Synchronous Fallback
|
|
98
|
+
|
|
99
|
+
For assets needed immediately (e.g., during level transition):
|
|
100
|
+
|
|
101
|
+
```cpp
|
|
102
|
+
// Option A: RequestSyncLoad — blocks until done.
|
|
103
|
+
// Use only for small assets or during loading screens.
|
|
104
|
+
TSharedPtr<FStreamableHandle> Handle = SM.RequestSyncLoad(
|
|
105
|
+
AssetPath,
|
|
106
|
+
false, // bManageActiveHandle
|
|
107
|
+
TEXT("LevelTransition sync load"));
|
|
108
|
+
|
|
109
|
+
UObject* Asset = Handle->GetLoadedAsset();
|
|
110
|
+
|
|
111
|
+
// Option B: WaitUntilComplete on an already-started async load.
|
|
112
|
+
if (LoadHandle.IsValid() && LoadHandle->IsLoadingInProgress())
|
|
113
|
+
{
|
|
114
|
+
LoadHandle->WaitUntilComplete(5.f); // Timeout seconds; 0 = wait forever.
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Pattern 4: Lambda Callback with Captured State
|
|
121
|
+
|
|
122
|
+
```cpp
|
|
123
|
+
FPrimaryAssetId AssetId(TEXT("WeaponDefinition"), TEXT("DA_Rifle"));
|
|
124
|
+
|
|
125
|
+
TSharedPtr<FStreamableHandle> Handle = UAssetManager::Get().LoadPrimaryAsset(
|
|
126
|
+
AssetId,
|
|
127
|
+
TArray<FName>{ TEXT("Game") },
|
|
128
|
+
FStreamableDelegate::CreateLambda([this, AssetId]()
|
|
129
|
+
{
|
|
130
|
+
UWeaponDefinition* Def =
|
|
131
|
+
UAssetManager::Get().GetPrimaryAssetObject<UWeaponDefinition>(AssetId);
|
|
132
|
+
if (Def && IsValid(this))
|
|
133
|
+
{
|
|
134
|
+
EquipWeapon(Def);
|
|
135
|
+
}
|
|
136
|
+
}));
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Important: Lambdas capturing `this` are not safe if the object is destroyed before the callback fires. Use `CreateUObject` (which checks object validity) or guard with `IsValid(this)`.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Pattern 5: Progress Tracking
|
|
144
|
+
|
|
145
|
+
```cpp
|
|
146
|
+
void UMyLoadingScreen::StartLoadWithProgress(const TArray<FSoftObjectPath>& Assets)
|
|
147
|
+
{
|
|
148
|
+
FStreamableManager& SM = UAssetManager::GetStreamableManager();
|
|
149
|
+
|
|
150
|
+
LoadHandle = SM.RequestAsyncLoad(
|
|
151
|
+
Assets,
|
|
152
|
+
FStreamableDelegate::CreateUObject(this, &UMyLoadingScreen::OnLoadComplete));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Poll from Tick or a timer.
|
|
156
|
+
void UMyLoadingScreen::Tick(float DeltaTime)
|
|
157
|
+
{
|
|
158
|
+
if (LoadHandle.IsValid() && LoadHandle->IsLoadingInProgress())
|
|
159
|
+
{
|
|
160
|
+
float Progress = LoadHandle->GetLoadProgress(); // 0.0 to 1.0
|
|
161
|
+
ProgressBar->SetPercent(Progress);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
void UMyLoadingScreen::OnLoadComplete()
|
|
166
|
+
{
|
|
167
|
+
ProgressBar->SetPercent(1.f);
|
|
168
|
+
LoadHandle.Reset();
|
|
169
|
+
HideLoadingScreen();
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Pattern 6: Load Grouping with Asset Manager Bundles
|
|
176
|
+
|
|
177
|
+
Bundle state transitions are the recommended way to manage groups of related assets across gameplay states.
|
|
178
|
+
|
|
179
|
+
```cpp
|
|
180
|
+
UAssetManager& AM = UAssetManager::Get();
|
|
181
|
+
|
|
182
|
+
// Get all weapon IDs.
|
|
183
|
+
TArray<FPrimaryAssetId> WeaponIds;
|
|
184
|
+
AM.GetPrimaryAssetIdList(FPrimaryAssetType(TEXT("WeaponDefinition")), WeaponIds);
|
|
185
|
+
|
|
186
|
+
// On entering the main menu: load UI bundle only.
|
|
187
|
+
AM.LoadPrimaryAssets(WeaponIds, { TEXT("UI") });
|
|
188
|
+
|
|
189
|
+
// On entering gameplay: add Game bundle, keep UI or remove it.
|
|
190
|
+
AM.ChangeBundleStateForPrimaryAssets(
|
|
191
|
+
WeaponIds,
|
|
192
|
+
{ TEXT("Game") }, // AddBundles
|
|
193
|
+
{ TEXT("UI") }, // RemoveBundles
|
|
194
|
+
false, // bRemoveAllBundles
|
|
195
|
+
FStreamableDelegate::CreateLambda([]() { /* gameplay ready */ }));
|
|
196
|
+
|
|
197
|
+
// On leaving gameplay: unload Game bundle, keep UI.
|
|
198
|
+
AM.ChangeBundleStateForMatchingPrimaryAssets(
|
|
199
|
+
{ TEXT("UI") }, // NewBundles
|
|
200
|
+
{ TEXT("Game") }); // OldBundles (removes these)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Handle Lifecycle Rules
|
|
206
|
+
|
|
207
|
+
| State | Meaning |
|
|
208
|
+
|---|---|
|
|
209
|
+
| `IsLoadingInProgress()` | Async load still running |
|
|
210
|
+
| `HasLoadCompleted()` | All assets loaded (callback may not have fired yet if deferred) |
|
|
211
|
+
| `WasCanceled()` | `CancelHandle()` was called; completion callback not invoked |
|
|
212
|
+
| `HasError()` | One or more assets failed to load |
|
|
213
|
+
|
|
214
|
+
**Ownership rules:**
|
|
215
|
+
- `TSharedPtr<FStreamableHandle>` keeps assets pinned while alive.
|
|
216
|
+
- Letting the `TSharedPtr` go out of scope calls `ReleaseHandle()` implicitly.
|
|
217
|
+
- `ReleaseHandle()` is deferred if called before the completion callback fires; callback still runs.
|
|
218
|
+
- `CancelHandle()` cancels immediately — completion callback is NOT called; cancel callback is called if bound.
|
|
219
|
+
|
|
220
|
+
```cpp
|
|
221
|
+
// Cancellation with a cancel callback.
|
|
222
|
+
Handle->BindCancelDelegate(FStreamableDelegate::CreateLambda([]()
|
|
223
|
+
{
|
|
224
|
+
UE_LOG(LogTemp, Warning, TEXT("Load was canceled."));
|
|
225
|
+
}));
|
|
226
|
+
Handle->CancelHandle();
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Combined / Merged Handles
|
|
232
|
+
|
|
233
|
+
When multiple independent loads should be tracked as one unit:
|
|
234
|
+
|
|
235
|
+
```cpp
|
|
236
|
+
TSharedPtr<FStreamableHandle> HandleA = SM.RequestAsyncLoad(PathA, {});
|
|
237
|
+
TSharedPtr<FStreamableHandle> HandleB = SM.RequestAsyncLoad(PathB, {});
|
|
238
|
+
|
|
239
|
+
// Combine into a single tracking handle. CreateCombinedHandle is on FStreamableManager.
|
|
240
|
+
TSharedPtr<FStreamableHandle> Merged = SM.CreateCombinedHandle({ HandleA, HandleB });
|
|
241
|
+
|
|
242
|
+
Merged->BindCompleteDelegate(FStreamableDelegate::CreateLambda([]()
|
|
243
|
+
{
|
|
244
|
+
// Both A and B are loaded.
|
|
245
|
+
}));
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Priority Constants
|
|
251
|
+
|
|
252
|
+
From `Engine/StreamableManager.h` (namespace `UE::StreamableManager::Private`):
|
|
253
|
+
|
|
254
|
+
```cpp
|
|
255
|
+
constexpr TAsyncLoadPriority DefaultAsyncLoadPriority = 0;
|
|
256
|
+
constexpr TAsyncLoadPriority AsyncLoadHighPriority = 100;
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Pass priority to `RequestAsyncLoad`:
|
|
260
|
+
```cpp
|
|
261
|
+
SM.RequestAsyncLoad(
|
|
262
|
+
Paths, Delegate,
|
|
263
|
+
UE::StreamableManager::Private::AsyncLoadHighPriority);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Anti-Patterns
|
|
269
|
+
|
|
270
|
+
### Releasing Handle Before Using Loaded Assets
|
|
271
|
+
|
|
272
|
+
```cpp
|
|
273
|
+
// BAD: assets may be GC'd immediately after Reset().
|
|
274
|
+
SM.RequestAsyncLoad(Path, [this]() {
|
|
275
|
+
LoadHandle.Reset(); // released inside callback
|
|
276
|
+
UStaticMesh* Mesh = MeshSoft.Get(); // nullptr — already GC'd
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// GOOD: only reset after assigning to a hard reference.
|
|
280
|
+
SM.RequestAsyncLoad(Path, [this]() {
|
|
281
|
+
MeshComp->SetStaticMesh(MeshSoft.Get()); // component holds the ref
|
|
282
|
+
LoadHandle.Reset(); // safe to release now
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Calling LoadSynchronous in Tick
|
|
287
|
+
|
|
288
|
+
```cpp
|
|
289
|
+
// BAD: stalls every frame if asset not cached.
|
|
290
|
+
void AMyActor::Tick(float DeltaTime)
|
|
291
|
+
{
|
|
292
|
+
UStaticMesh* Mesh = MeshSoft.LoadSynchronous(); // blocks rendering
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// GOOD: start async load once, use result only after callback.
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Ignoring Load Errors
|
|
299
|
+
|
|
300
|
+
```cpp
|
|
301
|
+
// Always check after callback:
|
|
302
|
+
if (Handle->HasError())
|
|
303
|
+
{
|
|
304
|
+
UE_LOG(LogTemp, Error, TEXT("One or more assets failed to load."));
|
|
305
|
+
}
|
|
306
|
+
```
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# Data-Driven Design in Unreal Engine
|
|
2
|
+
|
|
3
|
+
Patterns for structuring game data using `UDataAsset`, `UPrimaryDataAsset`, and `UDataTable` so designers control gameplay values without C++ changes.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Core Principle
|
|
8
|
+
|
|
9
|
+
Data-driven design separates **what** (values, configurations, relationships) from **how** (C++ logic). C++ defines the schema; designers populate instances in the editor or spreadsheets.
|
|
10
|
+
|
|
11
|
+
| Layer | Owner | Tool |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Data schema | Programmer | C++ `USTRUCT` / `UCLASS` |
|
|
14
|
+
| Data values | Designer | Data Asset editor, CSV/JSON |
|
|
15
|
+
| Data loading | Programmer | Asset Manager, soft refs |
|
|
16
|
+
| Data consumption | Programmer | `FindRow`, `GetPrimaryAssetObject` |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Pattern A: Item / Ability Definitions (UPrimaryDataAsset)
|
|
21
|
+
|
|
22
|
+
Best when each item has unique properties, may need Blueprint extension, and should integrate with the Asset Manager for selective loading.
|
|
23
|
+
|
|
24
|
+
### Schema
|
|
25
|
+
|
|
26
|
+
```cpp
|
|
27
|
+
// ItemDefinition.h
|
|
28
|
+
#pragma once
|
|
29
|
+
#include "Engine/DataAsset.h"
|
|
30
|
+
#include "GameplayTagContainer.h"
|
|
31
|
+
#include "ItemDefinition.generated.h"
|
|
32
|
+
|
|
33
|
+
UENUM(BlueprintType)
|
|
34
|
+
enum class EItemRarity : uint8
|
|
35
|
+
{
|
|
36
|
+
Common,
|
|
37
|
+
Uncommon,
|
|
38
|
+
Rare,
|
|
39
|
+
Legendary
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
UCLASS(BlueprintType)
|
|
43
|
+
class MYGAME_API UItemDefinition : public UPrimaryDataAsset
|
|
44
|
+
{
|
|
45
|
+
GENERATED_BODY()
|
|
46
|
+
public:
|
|
47
|
+
// Identity
|
|
48
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Item")
|
|
49
|
+
FText DisplayName;
|
|
50
|
+
|
|
51
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Item")
|
|
52
|
+
FText Description;
|
|
53
|
+
|
|
54
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Item")
|
|
55
|
+
EItemRarity Rarity = EItemRarity::Common;
|
|
56
|
+
|
|
57
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Item",
|
|
58
|
+
AssetRegistrySearchable)
|
|
59
|
+
FGameplayTagContainer ItemTags;
|
|
60
|
+
|
|
61
|
+
// Stats
|
|
62
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Stats")
|
|
63
|
+
float BaseDamage = 0.f;
|
|
64
|
+
|
|
65
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Stats")
|
|
66
|
+
int32 MaxStack = 1;
|
|
67
|
+
|
|
68
|
+
// UI bundle: loaded in inventory screen.
|
|
69
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Art",
|
|
70
|
+
meta = (AssetBundles = "UI"))
|
|
71
|
+
TSoftObjectPtr<UTexture2D> Icon;
|
|
72
|
+
|
|
73
|
+
// Game bundle: loaded in the world.
|
|
74
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Art",
|
|
75
|
+
meta = (AssetBundles = "Game"))
|
|
76
|
+
TSoftObjectPtr<UStaticMesh> WorldMesh;
|
|
77
|
+
|
|
78
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Spawning",
|
|
79
|
+
meta = (AssetBundles = "Game"))
|
|
80
|
+
TSoftClassPtr<AActor> DroppedActorClass;
|
|
81
|
+
};
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### DefaultGame.ini Registration
|
|
85
|
+
|
|
86
|
+
```ini
|
|
87
|
+
[/Script/Engine.AssetManagerSettings]
|
|
88
|
+
+PrimaryAssetTypesToScan=(
|
|
89
|
+
PrimaryAssetType="ItemDefinition",
|
|
90
|
+
AssetBaseClass=/Script/MyGame.ItemDefinition,
|
|
91
|
+
bHasBlueprintClasses=False,
|
|
92
|
+
bIsEditorOnly=False,
|
|
93
|
+
Directories=((Path="/Game/Data/Items")),
|
|
94
|
+
Rules=(Priority=1,ChunkId=-1,bApplyRecursively=True,CookRule=AlwaysCook))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Discovery and Loading
|
|
98
|
+
|
|
99
|
+
```cpp
|
|
100
|
+
// Get all item IDs (no assets loaded yet).
|
|
101
|
+
TArray<FPrimaryAssetId> AllItemIds;
|
|
102
|
+
UAssetManager::Get().GetPrimaryAssetIdList(
|
|
103
|
+
FPrimaryAssetType(TEXT("ItemDefinition")), AllItemIds);
|
|
104
|
+
|
|
105
|
+
// Filter by tag using Asset Registry (no load).
|
|
106
|
+
IAssetRegistry& AR = IAssetRegistry::GetChecked();
|
|
107
|
+
TArray<FAssetData> MeleeAssets;
|
|
108
|
+
AR.GetAssetsByClass(
|
|
109
|
+
FTopLevelAssetPath(TEXT("/Script/MyGame"), TEXT("ItemDefinition")),
|
|
110
|
+
MeleeAssets, true);
|
|
111
|
+
|
|
112
|
+
// Load UI bundle for inventory.
|
|
113
|
+
UAssetManager::Get().LoadPrimaryAssetsWithType(
|
|
114
|
+
FPrimaryAssetType(TEXT("ItemDefinition")),
|
|
115
|
+
{ TEXT("UI") });
|
|
116
|
+
|
|
117
|
+
// After callback: retrieve a specific item.
|
|
118
|
+
FPrimaryAssetId SwordId(TEXT("ItemDefinition"), TEXT("DA_Sword"));
|
|
119
|
+
UItemDefinition* Sword =
|
|
120
|
+
UAssetManager::Get().GetPrimaryAssetObject<UItemDefinition>(SwordId);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Pattern B: Stat Tables (UDataTable)
|
|
126
|
+
|
|
127
|
+
Best for large, flat, designer-authored datasets where all rows share the same shape: XP curves, damage falloff, NPC dialogue, loot drop weights.
|
|
128
|
+
|
|
129
|
+
### Row Struct
|
|
130
|
+
|
|
131
|
+
```cpp
|
|
132
|
+
// XPTableRow.h
|
|
133
|
+
#pragma once
|
|
134
|
+
#include "Engine/DataTable.h"
|
|
135
|
+
#include "XPTableRow.generated.h"
|
|
136
|
+
|
|
137
|
+
USTRUCT(BlueprintType)
|
|
138
|
+
struct FXPTableRow : public FTableRowBase
|
|
139
|
+
{
|
|
140
|
+
GENERATED_BODY()
|
|
141
|
+
|
|
142
|
+
/** Player level. Also used as the row name in the DataTable. */
|
|
143
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
|
144
|
+
int32 Level = 1;
|
|
145
|
+
|
|
146
|
+
/** XP required to reach this level from the previous one. */
|
|
147
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
|
148
|
+
int32 XPRequired = 0;
|
|
149
|
+
|
|
150
|
+
/** Stat multiplier applied at this level. */
|
|
151
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
|
152
|
+
float StatMultiplier = 1.f;
|
|
153
|
+
|
|
154
|
+
// Validate row after import.
|
|
155
|
+
virtual void OnPostDataImport(const UDataTable* InDataTable,
|
|
156
|
+
const FName InRowName,
|
|
157
|
+
TArray<FString>& OutCollectedImportProblems) override
|
|
158
|
+
{
|
|
159
|
+
if (XPRequired < 0)
|
|
160
|
+
{
|
|
161
|
+
OutCollectedImportProblems.Add(
|
|
162
|
+
FString::Printf(TEXT("Row '%s': XPRequired cannot be negative."),
|
|
163
|
+
*InRowName.ToString()));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### CSV Format
|
|
170
|
+
|
|
171
|
+
```csv
|
|
172
|
+
---,Level,XPRequired,StatMultiplier
|
|
173
|
+
Level_1,1,0,1.0
|
|
174
|
+
Level_2,2,100,1.05
|
|
175
|
+
Level_3,3,250,1.10
|
|
176
|
+
Level_4,4,500,1.15
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The first column (`---` or `Name`) is the row name. Import via: DataTable asset > Import > select CSV.
|
|
180
|
+
|
|
181
|
+
### Runtime Lookup
|
|
182
|
+
|
|
183
|
+
```cpp
|
|
184
|
+
// Stored as a hard reference because it is small and always needed.
|
|
185
|
+
UPROPERTY(EditDefaultsOnly, Category = "Data")
|
|
186
|
+
TObjectPtr<UDataTable> XPTable;
|
|
187
|
+
|
|
188
|
+
FXPTableRow* ULevelingComponent::GetRowForLevel(int32 Level) const
|
|
189
|
+
{
|
|
190
|
+
if (!XPTable) { return nullptr; }
|
|
191
|
+
FName RowName = FName(*FString::Printf(TEXT("Level_%d"), Level));
|
|
192
|
+
return XPTable->FindRow<FXPTableRow>(RowName, TEXT("GetRowForLevel"));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
int32 ULevelingComponent::GetXPToNextLevel(int32 CurrentLevel) const
|
|
196
|
+
{
|
|
197
|
+
const FXPTableRow* Row = GetRowForLevel(CurrentLevel + 1);
|
|
198
|
+
return Row ? Row->XPRequired : 0;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Pattern C: Config Objects (UDataAsset — Not Primary)
|
|
205
|
+
|
|
206
|
+
Use plain `UDataAsset` (not Primary) for global configuration that is always loaded with the referencing class, is not addressable by ID, and does not need Asset Manager integration.
|
|
207
|
+
|
|
208
|
+
```cpp
|
|
209
|
+
// GameBalanceConfig.h
|
|
210
|
+
#pragma once
|
|
211
|
+
#include "Engine/DataAsset.h"
|
|
212
|
+
#include "GameBalanceConfig.generated.h"
|
|
213
|
+
|
|
214
|
+
UCLASS(BlueprintType)
|
|
215
|
+
class MYGAME_API UGameBalanceConfig : public UDataAsset
|
|
216
|
+
{
|
|
217
|
+
GENERATED_BODY()
|
|
218
|
+
public:
|
|
219
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Economy")
|
|
220
|
+
int32 StartingGold = 100;
|
|
221
|
+
|
|
222
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Economy")
|
|
223
|
+
float SellPriceMultiplier = 0.5f;
|
|
224
|
+
|
|
225
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Combat")
|
|
226
|
+
float GlobalDamageScale = 1.f;
|
|
227
|
+
|
|
228
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Combat")
|
|
229
|
+
float CritMultiplier = 2.f;
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Reference from GameMode or GameInstance with a hard `TObjectPtr`:
|
|
234
|
+
|
|
235
|
+
```cpp
|
|
236
|
+
UPROPERTY(EditDefaultsOnly, Category = "Config")
|
|
237
|
+
TObjectPtr<UGameBalanceConfig> BalanceConfig;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Pattern D: Hybrid — DataAsset with DataTable Reference
|
|
243
|
+
|
|
244
|
+
An item definition holds a soft reference to a DataTable for per-tier stat scaling, while the DataAsset handles identity and art.
|
|
245
|
+
|
|
246
|
+
```cpp
|
|
247
|
+
UCLASS(BlueprintType)
|
|
248
|
+
class MYGAME_API UWeaponDefinition : public UPrimaryDataAsset
|
|
249
|
+
{
|
|
250
|
+
GENERATED_BODY()
|
|
251
|
+
public:
|
|
252
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon")
|
|
253
|
+
FText WeaponName;
|
|
254
|
+
|
|
255
|
+
// Reference to a scaling table — loaded on demand.
|
|
256
|
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Stats",
|
|
257
|
+
meta = (AssetBundles = "Game"))
|
|
258
|
+
TSoftObjectPtr<UDataTable> ScalingTable;
|
|
259
|
+
|
|
260
|
+
float GetDamageAtTier(int32 Tier) const
|
|
261
|
+
{
|
|
262
|
+
// ScalingTable must already be loaded (via bundle).
|
|
263
|
+
UDataTable* Table = ScalingTable.Get();
|
|
264
|
+
if (!Table) { return 0.f; }
|
|
265
|
+
|
|
266
|
+
FName RowName = FName(*FString::Printf(TEXT("Tier_%d"), Tier));
|
|
267
|
+
const FWeaponScalingRow* Row =
|
|
268
|
+
Table->FindRow<FWeaponScalingRow>(RowName, TEXT("GetDamageAtTier"));
|
|
269
|
+
return Row ? Row->Damage : 0.f;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Pattern E: Subsystem as Data Gateway
|
|
277
|
+
|
|
278
|
+
A `UGameInstanceSubsystem` caches loaded data assets after initial scan, providing a central access point that avoids repeated Asset Manager calls throughout the codebase.
|
|
279
|
+
|
|
280
|
+
```cpp
|
|
281
|
+
// ItemSubsystem.h
|
|
282
|
+
#pragma once
|
|
283
|
+
#include "Subsystems/GameInstanceSubsystem.h"
|
|
284
|
+
#include "ItemSubsystem.generated.h"
|
|
285
|
+
|
|
286
|
+
UCLASS()
|
|
287
|
+
class MYGAME_API UItemSubsystem : public UGameInstanceSubsystem
|
|
288
|
+
{
|
|
289
|
+
GENERATED_BODY()
|
|
290
|
+
public:
|
|
291
|
+
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
|
292
|
+
|
|
293
|
+
UItemDefinition* GetItemById(const FPrimaryAssetId& Id) const;
|
|
294
|
+
TArray<UItemDefinition*> GetItemsByTag(FGameplayTag Tag) const;
|
|
295
|
+
|
|
296
|
+
private:
|
|
297
|
+
void OnItemsLoaded();
|
|
298
|
+
|
|
299
|
+
TMap<FPrimaryAssetId, TObjectPtr<UItemDefinition>> ItemCache;
|
|
300
|
+
TSharedPtr<FStreamableHandle> LoadHandle;
|
|
301
|
+
};
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```cpp
|
|
305
|
+
// ItemSubsystem.cpp
|
|
306
|
+
void UItemSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
|
307
|
+
{
|
|
308
|
+
Super::Initialize(Collection);
|
|
309
|
+
|
|
310
|
+
UAssetManager& AM = UAssetManager::Get();
|
|
311
|
+
TArray<FPrimaryAssetId> ItemIds;
|
|
312
|
+
AM.GetPrimaryAssetIdList(FPrimaryAssetType(TEXT("ItemDefinition")), ItemIds);
|
|
313
|
+
|
|
314
|
+
LoadHandle = AM.LoadPrimaryAssets(
|
|
315
|
+
ItemIds,
|
|
316
|
+
{ TEXT("UI") },
|
|
317
|
+
FStreamableDelegate::CreateUObject(this, &UItemSubsystem::OnItemsLoaded));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
void UItemSubsystem::OnItemsLoaded()
|
|
321
|
+
{
|
|
322
|
+
UAssetManager& AM = UAssetManager::Get();
|
|
323
|
+
TArray<UObject*> LoadedObjects;
|
|
324
|
+
AM.GetPrimaryAssetObjectList(FPrimaryAssetType(TEXT("ItemDefinition")), LoadedObjects);
|
|
325
|
+
|
|
326
|
+
for (UObject* Obj : LoadedObjects)
|
|
327
|
+
{
|
|
328
|
+
if (UItemDefinition* Item = Cast<UItemDefinition>(Obj))
|
|
329
|
+
{
|
|
330
|
+
ItemCache.Add(Item->GetPrimaryAssetId(), Item);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
UItemDefinition* UItemSubsystem::GetItemById(const FPrimaryAssetId& Id) const
|
|
336
|
+
{
|
|
337
|
+
const TObjectPtr<UItemDefinition>* Found = ItemCache.Find(Id);
|
|
338
|
+
return Found ? Found->Get() : nullptr;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
TArray<UItemDefinition*> UItemSubsystem::GetItemsByTag(FGameplayTag Tag) const
|
|
342
|
+
{
|
|
343
|
+
TArray<UItemDefinition*> Results;
|
|
344
|
+
for (const auto& Pair : ItemCache)
|
|
345
|
+
{
|
|
346
|
+
if (Pair.Value && Pair.Value->ItemTags.HasTag(Tag))
|
|
347
|
+
{
|
|
348
|
+
Results.Add(Pair.Value);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return Results;
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## When to Use Each Approach
|
|
358
|
+
|
|
359
|
+
| Scenario | Recommended Approach |
|
|
360
|
+
|---|---|
|
|
361
|
+
| Per-item configs, ~10–500 items, designers use editor | `UPrimaryDataAsset` + Asset Manager |
|
|
362
|
+
| Large flat tables, designers use Excel/Sheets | `UDataTable` + CSV import |
|
|
363
|
+
| Global game settings, always in memory | `UDataAsset` (plain), hard reference |
|
|
364
|
+
| Server-only data (drop rates, economy) | `UDataTable` with `bStripFromClientBuilds=true` |
|
|
365
|
+
| Items with stat scaling per tier | Hybrid: PrimaryDataAsset + soft ref to DataTable |
|
|
366
|
+
| Runtime-generated content | `UDataTable::AddRow`, or dynamic `FPrimaryAssetId` via `AddDynamicAsset` |
|
|
367
|
+
| Querying without loading | `IAssetRegistry::GetAssetsByClass` + `FAssetData` |
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Designer Workflow Checklist
|
|
372
|
+
|
|
373
|
+
1. **C++ schema merged**: Row struct or DataAsset class is compiled and visible in the editor.
|
|
374
|
+
2. **Asset registered**: `DefaultGame.ini` has the correct `PrimaryAssetTypesToScan` entry (for PrimaryDataAssets).
|
|
375
|
+
3. **Editor asset created**: Data Asset instances or DataTable created in Content Browser under the scanned path.
|
|
376
|
+
4. **Data populated**: Fields filled in the editor or CSV imported.
|
|
377
|
+
5. **Cook verified**: Launch a development cook and confirm assets appear in the cooked output. Check for "Asset not in cook" warnings in the log.
|
|
378
|
+
6. **Bundle states tested**: Verify UI bundle loads in menus and Game bundle loads during gameplay without performance spikes.
|