Quick answer: A dedicated server has no audio device, so UAudioComponent::Play() is a no-op and OnAudioFinished never fires. Decouple gameplay from sound events: cache the sound’s Duration and use a FTimerHandle for the schedule, keep clients playing audible sound via NetMulticast, and use a fake listener attached to the GameState if you need attenuation-dependent cue events.
Your spell cast plays a 3-second chant sound, and at the end an OnAudioFinished bound event triggers the spell effect. In single-player and listen-server it works perfectly. Ship it on dedicated server and the spell never resolves — the chant plays on the client, but the damage never hits. This is the classic dedicated-server audio pitfall: you tied gameplay logic to an event that only fires when audio actually plays, and on a dedicated server it never does.
Why AudioComponent Is Silent on Dedicated Servers
When Unreal starts a dedicated server, it initializes with -nosound behavior by default. The audio device is null, all USoundBase::Play calls short-circuit, and UAudioComponent never posts a sound to the mixer. That means:
OnAudioFinisheddelegate never broadcasts.IsPlaying()always returns false.- Sound cue branches based on attenuation (like “only play if listener is close”) never evaluate.
Any gameplay logic bound to audio completion silently breaks. The client plays the sound and shows the animation; the server finishes the movement animation but never executes the damage, because the “finished” signal never arrived.
Step 1: Decouple Gameplay Logic from Audio
Do not use audio events to drive gameplay. Audio should be a presentation layer. Instead, read the sound’s duration once and schedule gameplay with a timer:
void AMySpell::StartCast() {
if (!HasAuthority()) return;
float Duration = ChantSound->GetDuration();
GetWorld()->GetTimerManager().SetTimer(
ChantTimerHandle,
this,
&AMySpell::FinishCast,
Duration,
false);
Multicast_PlayChant(); // plays audibly on clients only
}
void AMySpell::FinishCast() {
// Deterministic; runs on server regardless of audio state
ApplyDamage();
}
Because GetDuration is a property of the USoundBase asset, not of audio playback, it works on a dedicated server. The timer fires after exactly N seconds whether or not a sound ever played.
Step 2: Multicast the Audible Play
Use a NetMulticast RPC so every client plays the sound locally. Do not try to replicate the audio component itself; it will not work.
UFUNCTION(NetMulticast, Unreliable)
void Multicast_PlayChant();
void AMySpell::Multicast_PlayChant_Implementation() {
if (GetNetMode() != NM_DedicatedServer) {
UGameplayStatics::PlaySoundAtLocation(
this, ChantSound, GetActorLocation());
}
}
The GetNetMode() != NM_DedicatedServer guard is belt-and-suspenders — the engine will no-op the call on a dedicated server anyway, but being explicit makes the intent clear to the next reader.
Step 3: Handle Interruption
If the caster is stunned mid-chant, you cancel the gameplay timer on the server and tell clients to stop the sound. Both sides must cancel independently because the server has no handle to the client’s UAudioComponent.
void AMySpell::InterruptCast() {
if (HasAuthority()) {
GetWorld()->GetTimerManager().ClearTimer(ChantTimerHandle);
Multicast_StopChant();
}
}
UFUNCTION(NetMulticast, Unreliable)
void Multicast_StopChant();
void AMySpell::Multicast_StopChant_Implementation() {
if (ChantAudioComp && ChantAudioComp->IsPlaying()) {
ChantAudioComp->FadeOut(0.2f, 0.0f);
}
}
Step 4: Fake Listener for Attenuation-Dependent Cues
If your design requires the server to evaluate sound attenuation (e.g., AI that reacts to whether a sound would be audible to the player), spawn a fake listener component and register it with the audio device. This works because the audio system’s distance math is pure — it does not require the audio device to play.
// In GameState
void AMyGameState::InitFakeListener() {
if (GetNetMode() != NM_DedicatedServer) return;
FakeListener = NewObject<USceneComponent>(this);
FakeListener->RegisterComponent();
// Move each tick to the most relevant player pawn
}
void AMyGameState::Tick(float DeltaTime) {
if (FakeListener) {
APawn* Target = FindPrimaryListenerPawn();
if (Target) FakeListener->SetWorldLocation(Target->GetActorLocation());
}
}
This is an advanced pattern; most games should prefer Step 1’s decoupling. Use the fake listener only if you have audio-driven AI logic that genuinely depends on attenuation.
Step 5: Config Check
In DefaultEngine.ini, audit these settings for server build configurations:
[/Script/Engine.Engine]
bUseFixedFrameRate=false
[Audio]
UnfocusedVolumeMultiplier=1.0
[/Script/Engine.AudioSettings]
DedicatedServerAudioDeviceModuleName="" ; intentionally empty
Do not try to force a real audio device on a server — it is a waste of CPU and will produce no sound anyway on headless hardware. The fix is always to route gameplay through timers.
“Audio is a client concern. If your server cares about whether a sound finished, you built the wrong dependency.”
Verifying the Fix
Run a dedicated server build (use the -server command-line flag) with -log. Cast the spell. The client logs should show the sound playing; the server logs should show the gameplay effect firing exactly at the end of the chant duration. If the effect never fires, check that the timer is being set on HasAuthority() and that the function pointer is valid.
Related Issues
If sounds play on server but not on late-joining clients, see Replicated Sound Cue Late Join. For attenuation issues on listen servers, read Listen Server Wrong Listener Position.
GetDuration + FTimerHandle on server + NetMulticast on clients = no more silent dedicated-server gameplay.