An Event Driven Audio Ambience System in Unreal Engine and Wwise

February 3, 2026
February 3, 2026 Sam

An Event Driven Audio Ambience System in Unreal Engine and Wwise

When the weather in a level changes, the ambiences should change with it. Every ambience therefore needs to know the current weather, and I didn’t want each of them polling for it.

Jump to the full source files ↓

This post walks through the small system I built for that in Unreal Engine with Wwise. It’s four classes and none of them tick. A world subsystem holds the weather, and a component on each ambient actor listens to it and sets RTPCs on that actor’s Wwise game object, using either the project defaults or per-instance overrides.

Goal

  • Weather is set in one place. Gameplay, a sequencer track or a debug command calls one function and all the audio follows.
  • Nothing polls. Weather changes rarely, so it shouldn’t cost anything per frame.
  • A sound designer can drop the component on an actor and have it work, and can change which RTPC it drives by unticking a checkbox.
  • An ambience that starts mid-storm sounds like a storm from its first sample, with no audible fade from the dry version.
  • Wind and rain are independent parameters.

How the pieces fit

Data flows in one direction: something sets the weather, the manager tells the emitters, and each emitter tells Wwise.

  1. Gameplay code or Blueprint calls SetWeather, SetRainAmount or SetWindAmount on the WeatherManager.
  2. The WeatherManager, a world subsystem, stores rain and wind as 0-1 values and broadcasts OnRainAmountChanged or OnWindAmountChanged when a value changes.
  3. Each AmbienceAudioEmitter, one per ambient actor, receives the new value. It takes its RTPCs from the WeatherAudioConfig asset set in Project Settings, or from its own overrides.
  4. The emitter sets those RTPCs on the actor’s Ak component, where the Wwise ambience event is playing.

The emitter is the only class that knows about both the weather and Wwise.

WeatherManager

The weather lives in a UWorldSubsystem. That gives me one instance per world, created and destroyed with it, and reachable from anywhere with GetWorld()->GetSubsystem<UWeatherManager>(). I don’t have to place a manager actor in each level, and the values reset when the map changes.

It holds two floats, rain and wind, both normalised to 0-1. Each has a setter and a multicast delegate.

void UWeatherManager::SetRainAmount(float Amount)
{
    Amount = FMath::Clamp(Amount, 0.0f, 1.0f);
    if (Amount == RainAmount)
    {
        return;
    }

    RainAmount = Amount;
    OnRainAmountChanged.Broadcast(RainAmount);
}

The clamp means the manager owns the 0-1 contract. Listeners don’t have to guard against a stray 1.7, and the RTPC range in Wwise can be set to match. The early return means that setting the same value twice broadcasts nothing, so a caller can push the weather every frame from a blend and listeners only hear about actual changes.

I used plain C++ multicast delegates rather than dynamic ones because they’re cheaper to broadcast and all the listeners are native code. The setters and getters are still exposed to Blueprint, so designers can drive the weather from there, though they can’t bind to the change events.

The manager has no Wwise include and doesn’t know who is listening, so the same delegates could drive particles or a wetness material later.

Project-wide default RTPCs

Nearly every emitter drives the same two RTPCs. Assigning them by hand on every actor is tedious and easy to get wrong, so the defaults live in two small classes.

UWeatherAudioConfig is a data asset with two fields, a rain RTPC and a wind RTPC. UWeatherSettings is a UDeveloperSettings class holding a soft pointer to one of those assets.

UCLASS(Config = Game, DefaultConfig, meta = (DisplayName = "Weather"))
class UWeatherSettings : public UDeveloperSettings
{
    GENERATED_BODY()

public:
    UPROPERTY(Config, EditAnywhere, Category = "Audio")
    TSoftObjectPtr<UWeatherAudioConfig> DefaultWeatherAudioConfig;
};

Deriving from UDeveloperSettings adds a Weather page to Project Settings without any editor code. The value is saved to the game config file, so it’s versioned with the project and shared by the team.

I split the settings from the asset so that the settings class only points at an asset and the asset holds the Wwise references. Swapping the whole set means picking a different asset, and because the pointer is soft, the settings object doesn’t pull the Wwise assets into memory on its own.

On the emitter, each weather type gets three properties:

Property What it does
bRespondsToRain Whether this emitter listens to rain at all. On by default.
bUseDefaultRainRtpc Ticked: use the RTPC from Project Settings. On by default.
RainRtpc The override, used when the box above is unticked.

Wind has the same three, with bRespondsToWind off by default. EditCondition and EditConditionHides tidy up the Details panel: the override slot only appears once you untick the default, and both are hidden when the emitter doesn’t respond to that weather type.

The RTPCs are resolved once, in BeginPlay. The default config is only loaded if at least one enabled weather type uses it. If an RTPC comes back null, the log says which one is missing and whether to fix it in the default config or on the component.

The emitter and its BeginPlay order

UAmbienceAudioEmitter is an actor component. Add it to an actor, assign a Wwise event, and that actor’s ambience follows the weather. Its constructor turns ticking off.

UAmbienceAudioEmitter::UAmbienceAudioEmitter()
{
    // Purely event-driven: no per-frame work.
    PrimaryComponentTick.bCanEverTick = false;
}

BeginPlay then does four things, in this order:

  1. It finds an Ak component on the owner. If there isn’t one, it creates one and attaches it to the root, so designers don’t need to remember a second component.
  2. It resolves the RTPCs, default or override, as described above.
  3. It subscribes to the weather. It binds to the manager’s delegates, then immediately calls each handler with the current value.
  4. It plays the ambience, if bPlayOnStart is ticked.

Step 3 has to come before step 4. A delegate only tells you about the next change, so a listener that only binds would sit at the RTPC’s default value until the weather next moved. Calling the handler with the current value at subscribe time avoids that.

if (bRespondsToRain && ActiveRainRtpc)
{
    Weather->OnRainAmountChanged.AddUObject(this, &UAmbienceAudioEmitter::HandleRainAmount);
    HandleRainAmount(Weather->GetRainAmount());
}

Doing this before the event is posted matters most when the RTPC has interpolation set in Wwise. If the event starts first, you hear the sound begin dry and slide towards the correct value. If the RTPC is already set, the sound starts at the correct value.

The subscription happens whether or not the ambience is playing, so an emitter that’s triggered later, from a volume or a Blueprint call, already has current values on its game object.

The handlers themselves are one line each. They set the RTPC on the emitter’s own Ak component, so the value is scoped to that game object rather than set globally.

void UAmbienceAudioEmitter::HandleRainAmount(float NewAmount)
{
    if (AkComponent && ActiveRainRtpc)
    {
        AkComponent->SetRTPCValue(ActiveRainRtpc, NewAmount, 0, FString());
    }
}

The interpolation time passed from code is 0. Smoothing is left to Wwise, where the sound designer can tune it per RTPC without a recompile.

Starting and stopping

The only playback state the emitter keeps is the Wwise playing ID of its current instance, where zero means not playing.

PlayAmbience is safe to call repeatedly. If an instance is already playing it does nothing, so overlapping triggers never stack a second loop on top of the first. Otherwise it posts the event and asks for one callback, EndOfEvent.

Two ways to stop

The component can stop the ambience in two ways, chosen with the bUseDedicatedStopEvent checkbox.

Mode How it stops Where the fade lives
Stop by playing ID (default) StopPlayingID on this exact instance On the component: FadeOutTimeMs and FadeOutCurve
Dedicated stop event Posts a second Wwise event In Wwise, on the stop action

The Details panel only shows the fields for the mode in use. If the dedicated mode is ticked but no stop event is assigned, the emitter logs a warning and falls back to stopping by playing ID, so a missing asset can’t leave a loop running.

Clearing the ID early

StopAmbience sets the playing ID back to zero straight away, before the sound has finished fading.

// Cleared here, not in the callback, so the ambience can be retriggered during the fade-out.
PlayingID = 0;

IsPlaying() therefore means “is meant to be playing”, which is what gameplay code usually wants to know. If the player walks out of a trigger volume and straight back in, the ambience restarts immediately while the old instance is still fading out underneath it.

The race this creates

Clearing the ID early also sets up a race. The old instance finishes its fade a moment later and fires EndOfEvent. If the callback just cleared the playing ID, it would wipe the ID of the new instance. The emitter would then think it was silent while a loop was still playing, and the next PlayAmbience would stack a second one on top.

So the callback checks which instance has ended.

// Ignore the end of an older instance that was stopped and then replaced.
const UAkEventCallbackInfo* EventInfo = Cast<UAkEventCallbackInfo>(CallbackInfo);
if (!EventInfo || EventInfo->PlayingID == PlayingID)
{
    PlayingID = 0;
}

The callback is still needed for one-shot or finite ambiences. When those end on their own, the ID resets and the emitter can be played again.

EndPlay unsubscribes from the weather and stops the ambience, so a destroyed or streamed-out actor doesn’t leave a delegate bound or a loop playing.

Limitations

I kept the system small, and it has some limitations.

  • Blueprint can set the weather but can’t listen to it, because the change delegates are native-only. Adding a pair of dynamic delegates alongside them would let Blueprint-driven VFX respond too.
  • The default config loads synchronously. LoadSynchronous in BeginPlay is acceptable for one tiny data asset, but it is a blocking load, and preloading the asset once when the world starts would avoid it.
  • Rain and wind are hard-coded. Adding snow means a new float, delegate, RTPC field and three emitter properties. Beyond three weather types I’d switch to a map from gameplay tag to value and RTPC.
  • Retriggering during a fade works best with stop-by-ID. With a dedicated stop event, what gets stopped depends on the scope of the stop action in Wwise, so I’d check that mode before relying on fast retriggers.
  • The manager stores values and doesn’t blend them. Whatever drives it has to ramp rain from 0 to 1 over time.

None of these would change the basic design: a subsystem that owns the weather, and emitters that are told about changes and set the current values before they play anything.

Source files

The complete source for every file covered in this post.

Show WeatherManager.h
#pragma once

#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "WeatherManager.generated.h"

DECLARE_MULTICAST_DELEGATE_OneParam(FOnRainAmountChanged, float /*NewAmount*/);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnWindAmountChanged, float /*NewAmount*/);

/** Holds the world's current rain and wind amounts (0-1) and broadcasts when they change. */
UCLASS()
class THIRDPERSONDEMO_API UWeatherManager : public UWorldSubsystem
{
    GENERATED_BODY()

public:
    /** Clamped to 0-1. Broadcasts OnRainAmountChanged if the value changed. */
    UFUNCTION(BlueprintCallable, Category = "Weather")
    void SetRainAmount(float Amount);

    /** Clamped to 0-1. Broadcasts OnWindAmountChanged if the value changed. */
    UFUNCTION(BlueprintCallable, Category = "Weather")
    void SetWindAmount(float Amount);

    UFUNCTION(BlueprintCallable, Category = "Weather")
    void SetWeather(float InWindAmount, float InRainAmount);

    UFUNCTION(BlueprintPure, Category = "Weather")
    float GetRainAmount() const { return RainAmount; }

    UFUNCTION(BlueprintPure, Category = "Weather")
    float GetWindAmount() const { return WindAmount; }

    UFUNCTION(BlueprintPure, Category = "Weather")
    bool IsRaining() const { return RainAmount > 0.0f; }

    FOnRainAmountChanged OnRainAmountChanged;
    FOnWindAmountChanged OnWindAmountChanged;

private:
    float RainAmount = 0.0f;
    float WindAmount = 0.0f;
};
Show WeatherManager.cpp
#include "WeatherManager.h"

void UWeatherManager::SetRainAmount(float Amount)
{
    Amount = FMath::Clamp(Amount, 0.0f, 1.0f);
    if (Amount == RainAmount)
    {
        return;
    }

    RainAmount = Amount;
    OnRainAmountChanged.Broadcast(RainAmount);
}

void UWeatherManager::SetWindAmount(float Amount)
{
    Amount = FMath::Clamp(Amount, 0.0f, 1.0f);
    if (Amount == WindAmount)
    {
        return;
    }

    WindAmount = Amount;
    OnWindAmountChanged.Broadcast(WindAmount);
}

void UWeatherManager::SetWeather(float InWindAmount, float InRainAmount)
{
    SetWindAmount(InWindAmount);
    SetRainAmount(InRainAmount);
}
Show WeatherSettings.h
#pragma once

#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
#include "WeatherSettings.generated.h"

class UWeatherAudioConfig;

UCLASS(Config = Game, DefaultConfig, meta = (DisplayName = "Weather"))
class THIRDPERSONDEMO_API UWeatherSettings : public UDeveloperSettings
{
    GENERATED_BODY()

public:
    UPROPERTY(Config, EditAnywhere, Category = "Audio")
    TSoftObjectPtr<UWeatherAudioConfig> DefaultWeatherAudioConfig;
};
Show WeatherSettings.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "WeatherSettings.h"
Show WeatherAudioConfig.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "WeatherAudioConfig.generated.h"

class UAkRtpc;

UCLASS()
class THIRDPERSONDEMO_API UWeatherAudioConfig : public UDataAsset
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, Category = "Weather")
    TObjectPtr<UAkRtpc> RainRtpc;

    UPROPERTY(EditAnywhere, Category = "Weather")
    TObjectPtr<UAkRtpc> WindRtpc;
};
Show WeatherAudioConfig.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "WeatherAudioConfig.h"
Show AmbienceAudioEmitter.h
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "AkGameplayTypes.h"
#include "AmbienceAudioEmitter.generated.h"

class UAkRtpc;
class UAkComponent;
class UAkAudioEvent;
class UAkCallbackInfo;

// Plays a Wwise ambience event on the owning actor and drives its weather RTPCs from the WeatherManager.
UCLASS(ClassGroup = (Audio), meta = (BlueprintSpawnableComponent))
class THIRDPERSONDEMO_API UAmbienceAudioEmitter : public UActorComponent
{
    GENERATED_BODY()

public:
    UAmbienceAudioEmitter();

    // Starts the ambience. Returns true if it is playing afterwards, including if it already was.
    UFUNCTION(BlueprintCallable, Category = "Audio")
    bool PlayAmbience();

    // Stops the ambience, using either the dedicated stop event or the fade settings.
    UFUNCTION(BlueprintCallable, Category = "Audio")
    void StopAmbience();

    // False as soon as StopAmbience is called, even while the sound is still fading out.
    UFUNCTION(BlueprintPure, Category = "Audio")
    bool IsPlaying() const { return PlayingID != 0; }

protected:
    virtual void BeginPlay() override;
    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

    // Start the ambience automatically when play begins.
    UPROPERTY(EditAnywhere, Category = "Audio")
    bool bPlayOnStart = false;

    // Wwise event posted on this actor's Ak component to start the ambience.
    UPROPERTY(EditAnywhere, Category = "Audio")
    TObjectPtr<UAkAudioEvent> AmbienceEvent;

    // Ticked: stop by posting a Wwise stop event. Unticked: stop this instance directly, with the fade below.
    UPROPERTY(EditAnywhere, Category = "Audio|Stop")
    bool bUseDedicatedStopEvent = false;

    // Fade-out duration when stopping.
    UPROPERTY(EditAnywhere, Category = "Audio|Stop", meta = (EditCondition = "!bUseDedicatedStopEvent", EditConditionHides, ClampMin = "0", Units = "ms"))
    int32 FadeOutTimeMs = 0;

    // Shape of the fade-out.
    UPROPERTY(EditAnywhere, Category = "Audio|Stop", meta = (EditCondition = "!bUseDedicatedStopEvent", EditConditionHides))
    EAkCurveInterpolation FadeOutCurve = EAkCurveInterpolation::Linear;

    // Wwise event posted to stop the ambience. Its fade is defined in Wwise.
    UPROPERTY(EditAnywhere, Category = "Audio|Stop", meta = (EditCondition = "bUseDedicatedStopEvent", EditConditionHides))
    TObjectPtr<UAkAudioEvent> StopAmbienceEvent;

    // Drive the rain RTPC from the WeatherManager's rain amount.
    UPROPERTY(EditAnywhere, Category = "Weather|Rain")
    bool bRespondsToRain = true;

    // Ticked: use the RTPC from the default config in Project Settings > Weather.
    UPROPERTY(EditAnywhere, Category = "Weather|Rain", meta = (EditCondition = "bRespondsToRain", EditConditionHides))
    bool bUseDefaultRainRtpc = true;

    // RTPC to use instead of the default one.
    UPROPERTY(EditAnywhere, Category = "Weather|Rain", meta = (EditCondition = "bRespondsToRain && !bUseDefaultRainRtpc", EditConditionHides))
    TObjectPtr<UAkRtpc> RainRtpc;

    // Drive the wind RTPC from the WeatherManager's wind amount.
    UPROPERTY(EditAnywhere, Category = "Weather|Wind")
    bool bRespondsToWind = false;

    // Ticked: use the RTPC from the default config in Project Settings > Weather.
    UPROPERTY(EditAnywhere, Category = "Weather|Wind", meta = (EditCondition = "bRespondsToWind", EditConditionHides))
    bool bUseDefaultWindRtpc = true;

    // RTPC to use instead of the default one.
    UPROPERTY(EditAnywhere, Category = "Weather|Wind", meta = (EditCondition = "bRespondsToWind && !bUseDefaultWindRtpc", EditConditionHides))
    TObjectPtr<UAkRtpc> WindRtpc;

private:
    UAkRtpc* ResolveRtpc(bool bUseDefault, UAkRtpc* DefaultRtpc, UAkRtpc* OverrideRtpc, const TCHAR* Label) const;
    void ResolveRtpcs();
    void SubscribeToWeather();
    void UnsubscribeFromWeather();
    void HandleRainAmount(float NewAmount);
    void HandleWindAmount(float NewAmount);

    UFUNCTION()
    void HandleAmbienceCallback(EAkCallbackType CallbackType, UAkCallbackInfo* CallbackInfo);

    // 0 == AK_INVALID_PLAYING_ID, i.e. not playing.
    int32 PlayingID = 0;

    // Found on the owner in BeginPlay, or spawned if the owner has none.
    UPROPERTY(Transient)
    TObjectPtr<UAkComponent> AkComponent;

    // Resolved once in BeginPlay.
    UPROPERTY(Transient)
    TObjectPtr<UAkRtpc> ActiveRainRtpc;

    // Resolved once in BeginPlay.
    UPROPERTY(Transient)
    TObjectPtr<UAkRtpc> ActiveWindRtpc;
};
Show AmbienceAudioEmitter.cpp
#include "AmbienceAudioEmitter.h"
#include "WeatherManager.h"
#include "WeatherSettings.h"
#include "WeatherAudioConfig.h"
#include "AkComponent.h"
#include "AkAudioEvent.h"
#include "AkAudioDevice.h"
#include "AkRtpc.h"
#include "Engine/World.h"

DEFINE_LOG_CATEGORY_STATIC(LogAmbienceAudio, Log, All);

UAmbienceAudioEmitter::UAmbienceAudioEmitter()
{
    // Purely event-driven: no per-frame work.
    PrimaryComponentTick.bCanEverTick = false;
}

void UAmbienceAudioEmitter::BeginPlay()
{
    Super::BeginPlay();

    AActor* Owner = GetOwner();
    AkComponent = Owner->FindComponentByClass<UAkComponent>();

    if (!AkComponent)
    {
        AkComponent = NewObject<UAkComponent>(Owner);

        if (USceneComponent* Root = Owner->GetRootComponent())
        {
            AkComponent->SetupAttachment(Root);
        }
        else
        {
            Owner->SetRootComponent(AkComponent);
        }

        AkComponent->RegisterComponent();

        UE_LOG(LogAmbienceAudio, Log, TEXT("%s: no AkComponent found, AmbienceAudioEmitter spawned one."), *GetNameSafe(Owner));
    }

    ResolveRtpcs();

    // Subscribe on start, not on play, so the RTPCs are already at the right value when the event is posted.
    // This matters most for RTPCs that have interpolation set in Wwise.
    if (bRespondsToRain || bRespondsToWind)
    {
        SubscribeToWeather();
    }

    if (bPlayOnStart)
    {
        PlayAmbience();
    }
}

void UAmbienceAudioEmitter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    UnsubscribeFromWeather();
    StopAmbience();

    Super::EndPlay(EndPlayReason);
}

bool UAmbienceAudioEmitter::PlayAmbience()
{
    if (IsPlaying())
    {
        // Already playing, don't stack another instance.
        return true;
    }

    if (!AmbienceEvent)
    {
        UE_LOG(LogAmbienceAudio, Warning, TEXT("%s: no Ambience Event assigned."), *GetNameSafe(GetOwner()));
        return false;
    }

    if (!AkComponent)
    {
        UE_LOG(LogAmbienceAudio, Error, TEXT("%s: PlayAmbience called before BeginPlay, no AkComponent yet."), *GetNameSafe(GetOwner()));
        return false;
    }

    FOnAkPostEventCallback Callback;
    Callback.BindDynamic(this, &UAmbienceAudioEmitter::HandleAmbienceCallback);

    const int32 CallbackMask = 1 << static_cast<int32>(EAkCallbackType::EndOfEvent);
    PlayingID = AkComponent->PostAkEvent(AmbienceEvent, CallbackMask, Callback);

    return IsPlaying();
}

void UAmbienceAudioEmitter::StopAmbience()
{
    if (!IsPlaying())
    {
        return;
    }

    if (bUseDedicatedStopEvent && StopAmbienceEvent)
    {
        if (AkComponent)
        {
            AkComponent->PostAkEvent(StopAmbienceEvent, 0, FOnAkPostEventCallback());
        }
    }
    else
    {
        if (bUseDedicatedStopEvent)
        {
            UE_LOG(LogAmbienceAudio, Warning, TEXT("%s: dedicated stop event enabled but none assigned, stopping by playing ID instead."), *GetNameSafe(GetOwner()));
        }

        if (FAkAudioDevice* AudioDevice = FAkAudioDevice::Get())
        {
            AudioDevice->StopPlayingID(PlayingID, FadeOutTimeMs, static_cast<AkCurveInterpolation>(FadeOutCurve));
        }
        else
        {
            UE_LOG(LogAmbienceAudio, Error, TEXT("%s: no Wwise audio device, could not stop ambience."), *GetNameSafe(GetOwner()));
        }
    }

    // Cleared here, not in the callback, so the ambience can be retriggered during the fade-out.
    PlayingID = 0;
}

void UAmbienceAudioEmitter::HandleAmbienceCallback(EAkCallbackType CallbackType, UAkCallbackInfo* CallbackInfo)
{
    if (CallbackType != EAkCallbackType::EndOfEvent)
    {
        return;
    }

    // Ignore the end of an older instance that was stopped and then replaced.
    const UAkEventCallbackInfo* EventInfo = Cast<UAkEventCallbackInfo>(CallbackInfo);
    if (!EventInfo || EventInfo->PlayingID == PlayingID)
    {
        PlayingID = 0;
    }
}

void UAmbienceAudioEmitter::ResolveRtpcs()
{
    const UWeatherAudioConfig* DefaultWeatherAudioConfig = nullptr;
    const bool bNeedsDefault = (bRespondsToRain && bUseDefaultRainRtpc) || (bRespondsToWind && bUseDefaultWindRtpc);

    if (bNeedsDefault)
    {
        if (const UWeatherSettings* Settings = GetDefault<UWeatherSettings>())
        {
            DefaultWeatherAudioConfig = Settings->DefaultWeatherAudioConfig.LoadSynchronous();
        }
    }

    if (bRespondsToRain)
    {
        ActiveRainRtpc = ResolveRtpc(bUseDefaultRainRtpc, DefaultWeatherAudioConfig ? DefaultWeatherAudioConfig->RainRtpc.Get() : nullptr, RainRtpc, TEXT("rain"));
    }

    if (bRespondsToWind)
    {
        ActiveWindRtpc = ResolveRtpc(bUseDefaultWindRtpc, DefaultWeatherAudioConfig ? DefaultWeatherAudioConfig->WindRtpc.Get() : nullptr, WindRtpc, TEXT("wind"));
    }
}

UAkRtpc* UAmbienceAudioEmitter::ResolveRtpc(bool bUseDefault, UAkRtpc* DefaultRtpc, UAkRtpc* OverrideRtpc, const TCHAR* Label) const
{
    UAkRtpc* Result = bUseDefault ? DefaultRtpc : OverrideRtpc;
    if (!Result)
    {
        UE_LOG(LogAmbienceAudio, Error, TEXT("%s: no %s RTPC assigned (%s)."),
            *GetNameSafe(GetOwner()), Label,
            bUseDefault ? TEXT("default config") : TEXT("on the component"));
    }
    return Result;
}

void UAmbienceAudioEmitter::SubscribeToWeather()
{
    UWeatherManager* Weather = GetWorld()->GetSubsystem<UWeatherManager>();
    if (!Weather)
    {
        UE_LOG(LogAmbienceAudio, Error, TEXT("%s: no WeatherManager found."), *GetNameSafe(GetOwner()));
        return;
    }

    if (bRespondsToRain && ActiveRainRtpc)
    {
        Weather->OnRainAmountChanged.AddUObject(this, &UAmbienceAudioEmitter::HandleRainAmount);
        HandleRainAmount(Weather->GetRainAmount());
    }

    if (bRespondsToWind && ActiveWindRtpc)
    {
        Weather->OnWindAmountChanged.AddUObject(this, &UAmbienceAudioEmitter::HandleWindAmount);
        HandleWindAmount(Weather->GetWindAmount());
    }
}

void UAmbienceAudioEmitter::UnsubscribeFromWeather()
{
    if (UWorld* World = GetWorld())
    {
        if (UWeatherManager* Weather = World->GetSubsystem<UWeatherManager>())
        {
            Weather->OnRainAmountChanged.RemoveAll(this);
            Weather->OnWindAmountChanged.RemoveAll(this);
        }
    }
}

void UAmbienceAudioEmitter::HandleRainAmount(float NewAmount)
{
    if (AkComponent && ActiveRainRtpc)
    {
        AkComponent->SetRTPCValue(ActiveRainRtpc, NewAmount, 0, FString());
    }
}

void UAmbienceAudioEmitter::HandleWindAmount(float NewAmount)
{
    if (AkComponent && ActiveWindRtpc)
    {
        AkComponent->SetRTPCValue(ActiveWindRtpc, NewAmount, 0, FString());
    }
}
, , , ,