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,700 @@
|
|
|
1
|
+
# Audio Setup Patterns
|
|
2
|
+
|
|
3
|
+
Common audio architecture patterns for Unreal Engine projects. All patterns use real UE API
|
|
4
|
+
as found in `AudioComponent.h`, `GameplayStatics.h`, `SoundSubmix.h`, `SoundConcurrency.h`,
|
|
5
|
+
and `SoundAttenuation.h`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Pattern 1: Music System with Crossfading
|
|
10
|
+
|
|
11
|
+
A persistent music manager that crossfades between tracks using two `UAudioComponent` instances
|
|
12
|
+
— one active, one incoming — with `FadeIn`/`FadeOut` on both.
|
|
13
|
+
|
|
14
|
+
### MusicManager.h
|
|
15
|
+
|
|
16
|
+
```cpp
|
|
17
|
+
#pragma once
|
|
18
|
+
#include "CoreMinimal.h"
|
|
19
|
+
#include "GameFramework/Actor.h"
|
|
20
|
+
#include "Components/AudioComponent.h"
|
|
21
|
+
#include "MusicManager.generated.h"
|
|
22
|
+
|
|
23
|
+
UCLASS(BlueprintType)
|
|
24
|
+
class MYGAME_API AMusicManager : public AActor
|
|
25
|
+
{
|
|
26
|
+
GENERATED_BODY()
|
|
27
|
+
|
|
28
|
+
public:
|
|
29
|
+
AMusicManager();
|
|
30
|
+
|
|
31
|
+
UFUNCTION(BlueprintCallable, Category = "Audio|Music")
|
|
32
|
+
void PlayMusic(USoundBase* NewTrack, float CrossfadeDuration = 1.0f);
|
|
33
|
+
|
|
34
|
+
UFUNCTION(BlueprintCallable, Category = "Audio|Music")
|
|
35
|
+
void StopMusic(float FadeOutDuration = 1.0f);
|
|
36
|
+
|
|
37
|
+
UFUNCTION(BlueprintCallable, Category = "Audio|Music")
|
|
38
|
+
void SetMusicVolume(float Volume);
|
|
39
|
+
|
|
40
|
+
protected:
|
|
41
|
+
virtual void BeginPlay() override;
|
|
42
|
+
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
|
43
|
+
|
|
44
|
+
UFUNCTION()
|
|
45
|
+
void OnTrackFinished();
|
|
46
|
+
|
|
47
|
+
private:
|
|
48
|
+
// Slot A: currently playing track
|
|
49
|
+
UPROPERTY()
|
|
50
|
+
TObjectPtr<UAudioComponent> TrackA;
|
|
51
|
+
|
|
52
|
+
// Slot B: incoming track during crossfade
|
|
53
|
+
UPROPERTY()
|
|
54
|
+
TObjectPtr<UAudioComponent> TrackB;
|
|
55
|
+
|
|
56
|
+
// Which slot is active
|
|
57
|
+
bool bSlotAIsActive = true;
|
|
58
|
+
|
|
59
|
+
// Volume scalar applied to both tracks (separate from crossfade)
|
|
60
|
+
float MasterMusicVolume = 1.0f;
|
|
61
|
+
|
|
62
|
+
// Submix all music routes through — set in editor
|
|
63
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio|Music")
|
|
64
|
+
TObjectPtr<USoundSubmix> MusicSubmix;
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### MusicManager.cpp
|
|
69
|
+
|
|
70
|
+
```cpp
|
|
71
|
+
#include "MusicManager.h"
|
|
72
|
+
#include "Kismet/GameplayStatics.h"
|
|
73
|
+
#include "Sound/SoundSubmix.h"
|
|
74
|
+
|
|
75
|
+
AMusicManager::AMusicManager()
|
|
76
|
+
{
|
|
77
|
+
PrimaryActorTick.bCanEverTick = false;
|
|
78
|
+
|
|
79
|
+
// Two permanent audio components — no sound assigned at construction
|
|
80
|
+
TrackA = CreateDefaultSubobject<UAudioComponent>(TEXT("TrackA"));
|
|
81
|
+
TrackA->SetupAttachment(RootComponent);
|
|
82
|
+
TrackA->bAutoActivate = false;
|
|
83
|
+
TrackA->bIsUISound = true; // non-spatialized music
|
|
84
|
+
|
|
85
|
+
TrackB = CreateDefaultSubobject<UAudioComponent>(TEXT("TrackB"));
|
|
86
|
+
TrackB->SetupAttachment(RootComponent);
|
|
87
|
+
TrackB->bAutoActivate = false;
|
|
88
|
+
TrackB->bIsUISound = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
void AMusicManager::BeginPlay()
|
|
92
|
+
{
|
|
93
|
+
Super::BeginPlay();
|
|
94
|
+
TrackA->OnAudioFinished.AddDynamic(this, &AMusicManager::OnTrackFinished);
|
|
95
|
+
TrackB->OnAudioFinished.AddDynamic(this, &AMusicManager::OnTrackFinished);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
void AMusicManager::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|
99
|
+
{
|
|
100
|
+
TrackA->OnAudioFinished.RemoveAll(this);
|
|
101
|
+
TrackB->OnAudioFinished.RemoveAll(this);
|
|
102
|
+
Super::EndPlay(EndPlayReason);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
void AMusicManager::PlayMusic(USoundBase* NewTrack, float CrossfadeDuration)
|
|
106
|
+
{
|
|
107
|
+
if (!NewTrack) { return; }
|
|
108
|
+
if (GetNetMode() == NM_DedicatedServer) { return; }
|
|
109
|
+
|
|
110
|
+
UAudioComponent* ActiveSlot = bSlotAIsActive ? TrackA : TrackB;
|
|
111
|
+
UAudioComponent* IncomingSlot = bSlotAIsActive ? TrackB : TrackA;
|
|
112
|
+
|
|
113
|
+
// Fade out the currently playing slot
|
|
114
|
+
if (ActiveSlot->GetPlayState() == EAudioComponentPlayState::Playing ||
|
|
115
|
+
ActiveSlot->GetPlayState() == EAudioComponentPlayState::FadingIn)
|
|
116
|
+
{
|
|
117
|
+
ActiveSlot->FadeOut(CrossfadeDuration, 0.0f, EAudioFaderCurve::Linear);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Configure and fade in the incoming slot
|
|
121
|
+
IncomingSlot->SetSound(NewTrack);
|
|
122
|
+
IncomingSlot->FadeIn(CrossfadeDuration, MasterMusicVolume, 0.0f, EAudioFaderCurve::Linear);
|
|
123
|
+
|
|
124
|
+
bSlotAIsActive = !bSlotAIsActive;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
void AMusicManager::StopMusic(float FadeOutDuration)
|
|
128
|
+
{
|
|
129
|
+
TrackA->FadeOut(FadeOutDuration, 0.0f, EAudioFaderCurve::Linear);
|
|
130
|
+
TrackB->FadeOut(FadeOutDuration, 0.0f, EAudioFaderCurve::Linear);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
void AMusicManager::SetMusicVolume(float Volume)
|
|
134
|
+
{
|
|
135
|
+
MasterMusicVolume = FMath::Clamp(Volume, 0.0f, 1.0f);
|
|
136
|
+
// Alternatively, control via submix:
|
|
137
|
+
if (MusicSubmix)
|
|
138
|
+
{
|
|
139
|
+
MusicSubmix->SetSubmixOutputVolume(this, MasterMusicVolume);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
void AMusicManager::OnTrackFinished()
|
|
144
|
+
{
|
|
145
|
+
// Looping tracks should have a looping SoundCue or SoundWave —
|
|
146
|
+
// this callback fires only for one-shot music tracks.
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Pattern 2: Ambient Soundscape System
|
|
153
|
+
|
|
154
|
+
An actor that manages a set of looping ambient sounds (wind, birds, insects, water) with
|
|
155
|
+
distance-based crossfading driven by the player's position relative to defined zones.
|
|
156
|
+
|
|
157
|
+
### AmbientSoundscape.h
|
|
158
|
+
|
|
159
|
+
```cpp
|
|
160
|
+
#pragma once
|
|
161
|
+
#include "CoreMinimal.h"
|
|
162
|
+
#include "GameFramework/Actor.h"
|
|
163
|
+
#include "Components/AudioComponent.h"
|
|
164
|
+
#include "AmbientSoundscape.generated.h"
|
|
165
|
+
|
|
166
|
+
USTRUCT(BlueprintType)
|
|
167
|
+
struct FAmbientLayer
|
|
168
|
+
{
|
|
169
|
+
GENERATED_BODY()
|
|
170
|
+
|
|
171
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
|
172
|
+
TObjectPtr<USoundBase> Sound;
|
|
173
|
+
|
|
174
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(ClampMin="0.0", ClampMax="1.0"))
|
|
175
|
+
float MaxVolume = 1.0f;
|
|
176
|
+
|
|
177
|
+
// Component assigned at runtime
|
|
178
|
+
UPROPERTY(Transient)
|
|
179
|
+
TObjectPtr<UAudioComponent> Component;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
UCLASS(BlueprintType)
|
|
183
|
+
class MYGAME_API AAmbientSoundscape : public AActor
|
|
184
|
+
{
|
|
185
|
+
GENERATED_BODY()
|
|
186
|
+
|
|
187
|
+
public:
|
|
188
|
+
AAmbientSoundscape();
|
|
189
|
+
|
|
190
|
+
UFUNCTION(BlueprintCallable, Category = "Audio|Ambient")
|
|
191
|
+
void SetLayerVolume(int32 LayerIndex, float NormalizedVolume, float BlendTime = 0.5f);
|
|
192
|
+
|
|
193
|
+
UFUNCTION(BlueprintCallable, Category = "Audio|Ambient")
|
|
194
|
+
void FadeOutAll(float FadeDuration = 1.0f);
|
|
195
|
+
|
|
196
|
+
UFUNCTION(BlueprintCallable, Category = "Audio|Ambient")
|
|
197
|
+
void FadeInAll(float FadeDuration = 1.0f);
|
|
198
|
+
|
|
199
|
+
protected:
|
|
200
|
+
virtual void BeginPlay() override;
|
|
201
|
+
|
|
202
|
+
UPROPERTY(EditAnywhere, Category = "Audio|Ambient")
|
|
203
|
+
TArray<FAmbientLayer> Layers;
|
|
204
|
+
|
|
205
|
+
// Attenuation for all ambient layers (sphere, no occlusion for ambient)
|
|
206
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio|Ambient")
|
|
207
|
+
TObjectPtr<USoundAttenuation> AmbientAttenuation;
|
|
208
|
+
};
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### AmbientSoundscape.cpp
|
|
212
|
+
|
|
213
|
+
```cpp
|
|
214
|
+
#include "AmbientSoundscape.h"
|
|
215
|
+
#include "Kismet/GameplayStatics.h"
|
|
216
|
+
|
|
217
|
+
AAmbientSoundscape::AAmbientSoundscape()
|
|
218
|
+
{
|
|
219
|
+
PrimaryActorTick.bCanEverTick = false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
void AAmbientSoundscape::BeginPlay()
|
|
223
|
+
{
|
|
224
|
+
Super::BeginPlay();
|
|
225
|
+
|
|
226
|
+
if (GetNetMode() == NM_DedicatedServer) { return; }
|
|
227
|
+
|
|
228
|
+
for (FAmbientLayer& Layer : Layers)
|
|
229
|
+
{
|
|
230
|
+
if (!Layer.Sound) { continue; }
|
|
231
|
+
|
|
232
|
+
// SpawnSoundAttached creates a component attached to this actor
|
|
233
|
+
UAudioComponent* Comp = UGameplayStatics::SpawnSoundAttached(
|
|
234
|
+
Layer.Sound,
|
|
235
|
+
GetRootComponent(),
|
|
236
|
+
NAME_None,
|
|
237
|
+
FVector::ZeroVector,
|
|
238
|
+
FRotator::ZeroRotator,
|
|
239
|
+
EAttachLocation::SnapToTarget,
|
|
240
|
+
/*bStopWhenAttachedToDestroyed=*/true,
|
|
241
|
+
0.0f, // start at volume 0
|
|
242
|
+
1.0f, // pitch
|
|
243
|
+
0.0f, // start time
|
|
244
|
+
AmbientAttenuation,
|
|
245
|
+
nullptr, // concurrency (ambient loops don't need concurrency)
|
|
246
|
+
/*bAutoDestroy=*/false
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (Comp)
|
|
250
|
+
{
|
|
251
|
+
Layer.Component = Comp;
|
|
252
|
+
// Fade in over 2 seconds to MaxVolume
|
|
253
|
+
Comp->FadeIn(2.0f, Layer.MaxVolume, 0.0f, EAudioFaderCurve::Linear);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
void AAmbientSoundscape::SetLayerVolume(int32 LayerIndex, float NormalizedVolume, float BlendTime)
|
|
259
|
+
{
|
|
260
|
+
if (!Layers.IsValidIndex(LayerIndex)) { return; }
|
|
261
|
+
FAmbientLayer& Layer = Layers[LayerIndex];
|
|
262
|
+
if (!Layer.Component) { return; }
|
|
263
|
+
|
|
264
|
+
const float TargetVolume = Layer.MaxVolume * FMath::Clamp(NormalizedVolume, 0.0f, 1.0f);
|
|
265
|
+
if (BlendTime <= 0.0f)
|
|
266
|
+
{
|
|
267
|
+
Layer.Component->SetVolumeMultiplier(TargetVolume);
|
|
268
|
+
}
|
|
269
|
+
else
|
|
270
|
+
{
|
|
271
|
+
Layer.Component->AdjustVolume(BlendTime, TargetVolume);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
void AAmbientSoundscape::FadeOutAll(float FadeDuration)
|
|
276
|
+
{
|
|
277
|
+
for (FAmbientLayer& Layer : Layers)
|
|
278
|
+
{
|
|
279
|
+
if (Layer.Component)
|
|
280
|
+
{
|
|
281
|
+
Layer.Component->FadeOut(FadeDuration, 0.0f, EAudioFaderCurve::Linear);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
void AAmbientSoundscape::FadeInAll(float FadeDuration)
|
|
287
|
+
{
|
|
288
|
+
for (FAmbientLayer& Layer : Layers)
|
|
289
|
+
{
|
|
290
|
+
if (Layer.Component)
|
|
291
|
+
{
|
|
292
|
+
Layer.Component->FadeIn(FadeDuration, Layer.MaxVolume, 0.0f, EAudioFaderCurve::Linear);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Pattern 3: Gameplay Audio Component (Weapon SFX)
|
|
301
|
+
|
|
302
|
+
A `UActorComponent` managing a weapon's audio: fire, reload, empty-click, and mechanical
|
|
303
|
+
state sounds. Demonstrates proper server guarding and concurrency usage.
|
|
304
|
+
|
|
305
|
+
### WeaponAudioComponent.h
|
|
306
|
+
|
|
307
|
+
```cpp
|
|
308
|
+
#pragma once
|
|
309
|
+
#include "CoreMinimal.h"
|
|
310
|
+
#include "Components/ActorComponent.h"
|
|
311
|
+
#include "Sound/SoundConcurrency.h"
|
|
312
|
+
#include "WeaponAudioComponent.generated.h"
|
|
313
|
+
|
|
314
|
+
UCLASS(ClassGroup=Audio, meta=(BlueprintSpawnableComponent))
|
|
315
|
+
class MYGAME_API UWeaponAudioComponent : public UActorComponent
|
|
316
|
+
{
|
|
317
|
+
GENERATED_BODY()
|
|
318
|
+
|
|
319
|
+
public:
|
|
320
|
+
UWeaponAudioComponent();
|
|
321
|
+
|
|
322
|
+
UFUNCTION(BlueprintCallable, Category = "Audio|Weapon")
|
|
323
|
+
void PlayFireSound();
|
|
324
|
+
|
|
325
|
+
UFUNCTION(BlueprintCallable, Category = "Audio|Weapon")
|
|
326
|
+
void PlayReloadSound();
|
|
327
|
+
|
|
328
|
+
UFUNCTION(BlueprintCallable, Category = "Audio|Weapon")
|
|
329
|
+
void PlayEmptyClickSound();
|
|
330
|
+
|
|
331
|
+
UFUNCTION(BlueprintCallable, Category = "Audio|Weapon")
|
|
332
|
+
void StartMechanicalLoop();
|
|
333
|
+
|
|
334
|
+
UFUNCTION(BlueprintCallable, Category = "Audio|Weapon")
|
|
335
|
+
void StopMechanicalLoop(float FadeTime = 0.15f);
|
|
336
|
+
|
|
337
|
+
protected:
|
|
338
|
+
virtual void BeginPlay() override;
|
|
339
|
+
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
|
340
|
+
|
|
341
|
+
// One-shot sounds — use SpawnSoundAtLocation each play
|
|
342
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio")
|
|
343
|
+
TObjectPtr<USoundBase> FireSound;
|
|
344
|
+
|
|
345
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio")
|
|
346
|
+
TObjectPtr<USoundBase> ReloadSound;
|
|
347
|
+
|
|
348
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio")
|
|
349
|
+
TObjectPtr<USoundBase> EmptyClickSound;
|
|
350
|
+
|
|
351
|
+
// Looping sound — permanent component
|
|
352
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio")
|
|
353
|
+
TObjectPtr<USoundBase> MechanicalLoopSound;
|
|
354
|
+
|
|
355
|
+
// Attenuation for world-space weapon sounds
|
|
356
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio")
|
|
357
|
+
TObjectPtr<USoundAttenuation> WeaponAttenuation;
|
|
358
|
+
|
|
359
|
+
// Concurrency: max 4 fire sounds simultaneously, stop farthest if over
|
|
360
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio")
|
|
361
|
+
TObjectPtr<USoundConcurrency> FireConcurrency;
|
|
362
|
+
|
|
363
|
+
private:
|
|
364
|
+
UPROPERTY()
|
|
365
|
+
TObjectPtr<UAudioComponent> MechanicalLoopComp;
|
|
366
|
+
|
|
367
|
+
bool bIsOnServer() const;
|
|
368
|
+
FVector GetOwnerLocation() const;
|
|
369
|
+
};
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### WeaponAudioComponent.cpp
|
|
373
|
+
|
|
374
|
+
```cpp
|
|
375
|
+
#include "WeaponAudioComponent.h"
|
|
376
|
+
#include "GameFramework/Actor.h"
|
|
377
|
+
#include "Kismet/GameplayStatics.h"
|
|
378
|
+
#include "Components/AudioComponent.h"
|
|
379
|
+
|
|
380
|
+
UWeaponAudioComponent::UWeaponAudioComponent()
|
|
381
|
+
{
|
|
382
|
+
PrimaryComponentTick.bCanEverTick = false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
void UWeaponAudioComponent::BeginPlay()
|
|
386
|
+
{
|
|
387
|
+
Super::BeginPlay();
|
|
388
|
+
|
|
389
|
+
if (bIsOnServer() || !MechanicalLoopSound) { return; }
|
|
390
|
+
|
|
391
|
+
// Create the loop component but don't play yet
|
|
392
|
+
MechanicalLoopComp = UGameplayStatics::SpawnSoundAttached(
|
|
393
|
+
MechanicalLoopSound,
|
|
394
|
+
GetOwner()->GetRootComponent(),
|
|
395
|
+
NAME_None,
|
|
396
|
+
FVector::ZeroVector,
|
|
397
|
+
FRotator::ZeroRotator,
|
|
398
|
+
EAttachLocation::SnapToTarget,
|
|
399
|
+
/*bStopWhenAttachedToDestroyed=*/true,
|
|
400
|
+
1.0f, 1.0f, 0.0f,
|
|
401
|
+
WeaponAttenuation,
|
|
402
|
+
nullptr,
|
|
403
|
+
/*bAutoDestroy=*/false
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
if (MechanicalLoopComp)
|
|
407
|
+
{
|
|
408
|
+
MechanicalLoopComp->Stop(); // ensure not auto-playing
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
void UWeaponAudioComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|
413
|
+
{
|
|
414
|
+
if (MechanicalLoopComp)
|
|
415
|
+
{
|
|
416
|
+
MechanicalLoopComp->Stop();
|
|
417
|
+
}
|
|
418
|
+
Super::EndPlay(EndPlayReason);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
void UWeaponAudioComponent::PlayFireSound()
|
|
422
|
+
{
|
|
423
|
+
if (bIsOnServer() || !FireSound) { return; }
|
|
424
|
+
|
|
425
|
+
// Fire-and-forget: attenuation + concurrency prevent stacking
|
|
426
|
+
UGameplayStatics::PlaySoundAtLocation(
|
|
427
|
+
this,
|
|
428
|
+
FireSound,
|
|
429
|
+
GetOwnerLocation(),
|
|
430
|
+
FRotator::ZeroRotator,
|
|
431
|
+
1.0f, 1.0f, 0.0f,
|
|
432
|
+
WeaponAttenuation,
|
|
433
|
+
FireConcurrency, // USoundConcurrency: MaxCount=4, StopFarthestThenOldest
|
|
434
|
+
GetOwner()
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
void UWeaponAudioComponent::PlayReloadSound()
|
|
439
|
+
{
|
|
440
|
+
if (bIsOnServer() || !ReloadSound) { return; }
|
|
441
|
+
UGameplayStatics::PlaySoundAtLocation(
|
|
442
|
+
this, ReloadSound, GetOwnerLocation(),
|
|
443
|
+
FRotator::ZeroRotator, 1.0f, 1.0f, 0.0f,
|
|
444
|
+
WeaponAttenuation
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
void UWeaponAudioComponent::PlayEmptyClickSound()
|
|
449
|
+
{
|
|
450
|
+
if (bIsOnServer() || !EmptyClickSound) { return; }
|
|
451
|
+
UGameplayStatics::PlaySoundAtLocation(
|
|
452
|
+
this, EmptyClickSound, GetOwnerLocation(),
|
|
453
|
+
FRotator::ZeroRotator, 1.0f, 1.0f, 0.0f,
|
|
454
|
+
WeaponAttenuation
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
void UWeaponAudioComponent::StartMechanicalLoop()
|
|
459
|
+
{
|
|
460
|
+
if (bIsOnServer() || !MechanicalLoopComp) { return; }
|
|
461
|
+
MechanicalLoopComp->FadeIn(0.1f, 1.0f, 0.0f, EAudioFaderCurve::Linear);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
void UWeaponAudioComponent::StopMechanicalLoop(float FadeTime)
|
|
465
|
+
{
|
|
466
|
+
if (!MechanicalLoopComp) { return; }
|
|
467
|
+
MechanicalLoopComp->FadeOut(FadeTime, 0.0f, EAudioFaderCurve::Linear);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
bool UWeaponAudioComponent::bIsOnServer() const
|
|
471
|
+
{
|
|
472
|
+
const AActor* Owner = GetOwner();
|
|
473
|
+
return Owner && Owner->GetNetMode() == NM_DedicatedServer;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
FVector UWeaponAudioComponent::GetOwnerLocation() const
|
|
477
|
+
{
|
|
478
|
+
const AActor* Owner = GetOwner();
|
|
479
|
+
return Owner ? Owner->GetActorLocation() : FVector::ZeroVector;
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## Pattern 4: MetaSound Parameter-Driven Engine Audio
|
|
486
|
+
|
|
487
|
+
An engine sound that uses a `UMetaSoundSource` with a `RPM` float input and `Engaged` bool input,
|
|
488
|
+
driven from a vehicle's tick.
|
|
489
|
+
|
|
490
|
+
```cpp
|
|
491
|
+
// In vehicle header:
|
|
492
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio")
|
|
493
|
+
TObjectPtr<UMetaSoundSource> EngineSoundAsset; // UMetaSoundSource derives USoundBase
|
|
494
|
+
|
|
495
|
+
UPROPERTY()
|
|
496
|
+
TObjectPtr<UAudioComponent> EngineAudioComp;
|
|
497
|
+
|
|
498
|
+
// In BeginPlay:
|
|
499
|
+
void AMyVehicle::BeginPlay()
|
|
500
|
+
{
|
|
501
|
+
Super::BeginPlay();
|
|
502
|
+
if (GetNetMode() == NM_DedicatedServer) { return; }
|
|
503
|
+
|
|
504
|
+
EngineAudioComp = UGameplayStatics::SpawnSoundAttached(
|
|
505
|
+
EngineSoundAsset,
|
|
506
|
+
GetMesh(),
|
|
507
|
+
TEXT("AudioSocket"),
|
|
508
|
+
FVector::ZeroVector,
|
|
509
|
+
FRotator::ZeroRotator,
|
|
510
|
+
EAttachLocation::SnapToTarget,
|
|
511
|
+
true, 1.0f, 1.0f, 0.0f,
|
|
512
|
+
EngineAttenuation,
|
|
513
|
+
nullptr,
|
|
514
|
+
/*bAutoDestroy=*/false
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// In Tick — update MetaSound parameters:
|
|
519
|
+
void AMyVehicle::Tick(float DeltaTime)
|
|
520
|
+
{
|
|
521
|
+
Super::Tick(DeltaTime);
|
|
522
|
+
if (!EngineAudioComp) { return; }
|
|
523
|
+
|
|
524
|
+
const float CurrentRPM = GetCurrentRPM(); // your vehicle RPM value
|
|
525
|
+
const bool bIsEngaged = IsTransmissionEngaged();
|
|
526
|
+
|
|
527
|
+
// ISoundParameterControllerInterface methods on UAudioComponent:
|
|
528
|
+
EngineAudioComp->SetFloatParameter(FName("RPM"), CurrentRPM);
|
|
529
|
+
EngineAudioComp->SetBoolParameter(FName("Engaged"), bIsEngaged);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// On gear shift event:
|
|
533
|
+
void AMyVehicle::OnGearShift(int32 NewGear)
|
|
534
|
+
{
|
|
535
|
+
if (EngineAudioComp)
|
|
536
|
+
{
|
|
537
|
+
EngineAudioComp->SetIntParameter(FName("GearIndex"), NewGear);
|
|
538
|
+
EngineAudioComp->SetTriggerParameter(FName("OnGearShift"));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## Pattern 5: Submix-Based Volume Control (Settings Menu)
|
|
546
|
+
|
|
547
|
+
Typical volume slider implementation using `USoundSubmix::SetSubmixOutputVolume`.
|
|
548
|
+
|
|
549
|
+
```cpp
|
|
550
|
+
// In game settings subsystem or GameUserSettings subclass:
|
|
551
|
+
|
|
552
|
+
UPROPERTY(config)
|
|
553
|
+
float MasterVolume = 1.0f;
|
|
554
|
+
|
|
555
|
+
UPROPERTY(config)
|
|
556
|
+
float MusicVolume = 1.0f;
|
|
557
|
+
|
|
558
|
+
UPROPERTY(config)
|
|
559
|
+
float SFXVolume = 1.0f;
|
|
560
|
+
|
|
561
|
+
// References to submix assets loaded via soft references or direct UPROPERTY:
|
|
562
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio")
|
|
563
|
+
TObjectPtr<USoundSubmix> MasterSubmix;
|
|
564
|
+
|
|
565
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio")
|
|
566
|
+
TObjectPtr<USoundSubmix> MusicSubmix;
|
|
567
|
+
|
|
568
|
+
UPROPERTY(EditDefaultsOnly, Category = "Audio")
|
|
569
|
+
TObjectPtr<USoundSubmix> SFXSubmix;
|
|
570
|
+
|
|
571
|
+
void UMyGameUserSettings::ApplyAudioSettings()
|
|
572
|
+
{
|
|
573
|
+
if (UWorld* World = GetWorld())
|
|
574
|
+
{
|
|
575
|
+
if (MasterSubmix)
|
|
576
|
+
{
|
|
577
|
+
MasterSubmix->SetSubmixOutputVolume(World, MasterVolume);
|
|
578
|
+
}
|
|
579
|
+
if (MusicSubmix)
|
|
580
|
+
{
|
|
581
|
+
MusicSubmix->SetSubmixOutputVolume(World, MusicVolume);
|
|
582
|
+
}
|
|
583
|
+
if (SFXSubmix)
|
|
584
|
+
{
|
|
585
|
+
SFXSubmix->SetSubmixOutputVolume(World, SFXVolume);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
void UMyGameUserSettings::SetMasterVolume(float NewVolume)
|
|
591
|
+
{
|
|
592
|
+
MasterVolume = FMath::Clamp(NewVolume, 0.0f, 1.0f);
|
|
593
|
+
ApplyAudioSettings();
|
|
594
|
+
SaveConfig();
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## Pattern 6: Beat-Reactive Visual (Spectrum Analysis)
|
|
601
|
+
|
|
602
|
+
Drive a material parameter or visual effect from submix spectral analysis.
|
|
603
|
+
|
|
604
|
+
```cpp
|
|
605
|
+
// In an actor that reacts to music beats:
|
|
606
|
+
|
|
607
|
+
void ABeaconLight::BeginPlay()
|
|
608
|
+
{
|
|
609
|
+
Super::BeginPlay();
|
|
610
|
+
if (GetNetMode() == NM_DedicatedServer || !MusicSubmix) { return; }
|
|
611
|
+
|
|
612
|
+
// Start FFT on the music submix
|
|
613
|
+
MusicSubmix->StartSpectralAnalysis(
|
|
614
|
+
this,
|
|
615
|
+
EFFTSize::Medium,
|
|
616
|
+
EFFTPeakInterpolationMethod::Linear,
|
|
617
|
+
EFFTWindowType::Hann,
|
|
618
|
+
0.0f,
|
|
619
|
+
EAudioSpectrumType::MagnitudeSpectrum
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// Low (bass) and mid bands
|
|
623
|
+
TArray<FSoundSubmixSpectralAnalysisBandSettings> Bands;
|
|
624
|
+
FSoundSubmixSpectralAnalysisBandSettings Bass;
|
|
625
|
+
Bass.BandFrequency = 80.f;
|
|
626
|
+
Bass.AttackTimeMsec = 5.f;
|
|
627
|
+
Bass.ReleaseTimeMsec = 80.f;
|
|
628
|
+
Bands.Add(Bass);
|
|
629
|
+
|
|
630
|
+
FSoundSubmixSpectralAnalysisBandSettings Mid;
|
|
631
|
+
Mid.BandFrequency = 800.f;
|
|
632
|
+
Mid.AttackTimeMsec = 10.f;
|
|
633
|
+
Mid.ReleaseTimeMsec = 120.f;
|
|
634
|
+
Bands.Add(Mid);
|
|
635
|
+
|
|
636
|
+
FOnSubmixSpectralAnalysisBP SpectralDelegate;
|
|
637
|
+
SpectralDelegate.BindDynamic(this, &ABeaconLight::OnSpectralData);
|
|
638
|
+
|
|
639
|
+
MusicSubmix->AddSpectralAnalysisDelegate(
|
|
640
|
+
this, Bands, SpectralDelegate,
|
|
641
|
+
30.f, // UpdateRate Hz
|
|
642
|
+
-40.f, // DecibelNoiseFloor
|
|
643
|
+
true, // bDoNormalize (output 0..1)
|
|
644
|
+
false // bDoAutoRange
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
UFUNCTION()
|
|
649
|
+
void ABeaconLight::OnSpectralData(const TArray<float>& Magnitudes)
|
|
650
|
+
{
|
|
651
|
+
// Magnitudes[0] = bass band (0..1 normalized), Magnitudes[1] = mid band
|
|
652
|
+
const float BassMag = Magnitudes.IsValidIndex(0) ? Magnitudes[0] : 0.0f;
|
|
653
|
+
const float MidMag = Magnitudes.IsValidIndex(1) ? Magnitudes[1] : 0.0f;
|
|
654
|
+
|
|
655
|
+
// Drive a dynamic material instance parameter
|
|
656
|
+
if (LightMaterial)
|
|
657
|
+
{
|
|
658
|
+
LightMaterial->SetScalarParameterValue(TEXT("BassIntensity"), BassMag);
|
|
659
|
+
LightMaterial->SetScalarParameterValue(TEXT("MidIntensity"), MidMag);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Scale a point light radius
|
|
663
|
+
if (PointLight)
|
|
664
|
+
{
|
|
665
|
+
const float BaseRadius = 200.0f;
|
|
666
|
+
PointLight->SetAttenuationRadius(BaseRadius + BassMag * 800.0f);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
void ABeaconLight::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|
671
|
+
{
|
|
672
|
+
if (MusicSubmix)
|
|
673
|
+
{
|
|
674
|
+
MusicSubmix->StopSpectralAnalysis(this);
|
|
675
|
+
}
|
|
676
|
+
Super::EndPlay(EndPlayReason);
|
|
677
|
+
}
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## Asset Setup Checklist
|
|
683
|
+
|
|
684
|
+
For every 3D sound in the project:
|
|
685
|
+
|
|
686
|
+
- [ ] `AttenuationSettings` assigned — sphere falloff, `FalloffDistance` tuned to gameplay scale
|
|
687
|
+
- [ ] `SoundClassObject` set — SFX, Music, Voice, or Ambient
|
|
688
|
+
- [ ] `SoundSubmixObject` set — routes audio to the correct mixer bus
|
|
689
|
+
- [ ] `ConcurrencySet` populated — at least one `USoundConcurrency` for high-frequency sounds
|
|
690
|
+
- [ ] `Priority` value set — 1.0 default; increase for critical gameplay sounds
|
|
691
|
+
- [ ] `bAttenuate` and `bSpatialize` both enabled in the attenuation asset
|
|
692
|
+
- [ ] `bEnableOcclusion` considered — enable for interior/exterior transitions
|
|
693
|
+
- [ ] Streaming behavior — set `LoadingBehavior` on long SoundWave assets to `LoadOnDemand`
|
|
694
|
+
|
|
695
|
+
For MetaSound sources:
|
|
696
|
+
|
|
697
|
+
- [ ] All gameplay-driven inputs declared with clear names matching C++ `FName` strings
|
|
698
|
+
- [ ] Default parameter values set in the MetaSound graph
|
|
699
|
+
- [ ] `USoundConcurrency` assigned on the `UMetaSoundSource` asset
|
|
700
|
+
- [ ] Attenuation assigned if used in world space
|