Add weather audio cues

This commit is contained in:
2026-05-16 01:58:05 -07:00
parent dddac97658
commit 08a1df6ebe
7 changed files with 324 additions and 4 deletions
+1 -1
View File
@@ -436,7 +436,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
- [x] Connect weather to body temperature.
- [x] Connect shelter to weather protection. Survival now calculates the best overlapping shelter protection volume, replicates current weather protection, reduces ambient exposure and cold damage by shelter coverage, trends care-history shelter quality toward active protection, and shows shelter protection on the dev HUD.
- [x] Add first-pass sky and lighting. Added `AAgrarianSkyLightingController` with movable sun, skylight, and fog components driven by replicated time, local sunrise/sunset, weather state, and provider cloud cover; updated the Ground Zero setup script to place the controller and remove legacy static lighting actors.
- [ ] Add audio cues for weather.
- [x] Add audio cues for weather. Added `AAgrarianWeatherAudioController` with ambient, rain, wind, and storm audio components, assignable loop sound slots, weather/wind/night-driven volume fades, and Ground Zero map setup placement so placeholder or final loops can be assigned without changing gameplay code.
## 0.1.D Single Biome MVP Map
Binary file not shown.
+10
View File
@@ -164,6 +164,16 @@ the represented local day/night cycle and current weather without hard-coded
static light settings. The Ground Zero map setup script places this controller
and removes the earlier static demo sun/skylight/fog actors.
First-pass weather audio uses `AAgrarianWeatherAudioController`. The controller
owns ambient, rain, wind, and storm audio components with assignable loop sound
slots. It reads replicated weather state, provider wind speed, provider cloud
data, and local night/day state, then fades component volumes so rain, wind, and
storm cues follow the same authoritative weather mapping used by temperature and
lighting. The current MVP can ship without final sound assets because the
controller is silent until loops are assigned; placeholder or final audio can be
added by setting the exposed sound properties on the placed controller or a
Blueprint child.
The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses
Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the
current temperature, daily low/high, precipitation, wind, humidity, cloud cover,
+8 -1
View File
@@ -74,6 +74,13 @@ DEMO_ACTORS = [
"fixed_z": 12000.0,
"rotation": unreal.Rotator(-42.0, -35.0, 0.0),
},
{
"label": "AGR_DemoWeatherAudioController",
"class": unreal.AgrarianWeatherAudioController,
"location_xy": unreal.Vector(-18000.0, -7000.0, 0.0),
"fixed_z": 11800.0,
"rotation": unreal.Rotator(0.0, 0.0, 0.0),
},
{
"label": "AGR_DemoNoticeActor",
"class": unreal.AgrarianDemoNoticeActor,
@@ -322,7 +329,7 @@ def spawn_foliage_actor(height_values):
reserved_points = [
spec["location_xy"]
for spec in DEMO_ACTORS
if spec["label"] not in {"AGR_DemoSkyLightingController", "AGR_DemoNoticeActor"}
if spec["label"] not in {"AGR_DemoSkyLightingController", "AGR_DemoWeatherAudioController", "AGR_DemoNoticeActor"}
]
foliage_actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
@@ -0,0 +1,62 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
AUDIO_H = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherAudioController.h"
AUDIO_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherAudioController.cpp"
MAP_SETUP = ROOT / "Scripts" / "setup_ground_zero_demo_map.py"
TDD = ROOT / "Docs" / "TechnicalDesignDocument.md"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
EXPECTED = {
AUDIO_H: [
"class AAgrarianWeatherAudioController : public AActor",
"TObjectPtr<UAudioComponent> AmbientAudio;",
"TObjectPtr<UAudioComponent> RainAudio;",
"TObjectPtr<UAudioComponent> WindAudio;",
"TObjectPtr<UAudioComponent> StormAudio;",
"TObjectPtr<USoundBase> RainLoopSound;",
"void RefreshWeatherAudio(float DeltaSeconds);",
],
AUDIO_CPP: [
"#include \"AgrarianGameState.h\"",
"#include \"Components/AudioComponent.h\"",
"AmbientAudio = CreateDefaultSubobject<UAudioComponent>",
"RainAudio = CreateDefaultSubobject<UAudioComponent>",
"WindAudio = CreateDefaultSubobject<UAudioComponent>",
"StormAudio = CreateDefaultSubobject<UAudioComponent>",
"GameState->Weather",
"GameState->ActiveWeatherInputs.WindSpeedKmh",
"GameState->IsNight()",
"ApplyComponentVolume(RainAudio, CurrentRainVolume);",
"AudioComponent->SetVolumeMultiplier",
],
MAP_SETUP: [
"AGR_DemoWeatherAudioController",
"unreal.AgrarianWeatherAudioController",
],
TDD: [
"`AAgrarianWeatherAudioController`",
"ambient, rain, wind, and storm audio components",
],
ROADMAP: [
"[x] Add audio cues for weather.",
],
}
def main() -> None:
missing = []
for path, snippets in EXPECTED.items():
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
missing.append(f"{path.relative_to(ROOT)}: {snippet}")
if missing:
raise RuntimeError("Weather audio controller verification failed: " + "; ".join(missing))
print("Agrarian weather audio controller verification complete.")
if __name__ == "__main__":
main()
@@ -0,0 +1,149 @@
// Copyright Pacificao. All Rights Reserved.
#include "AgrarianWeatherAudioController.h"
#include "AgrarianGameState.h"
#include "Components/AudioComponent.h"
#include "Components/SceneComponent.h"
#include "Engine/World.h"
AAgrarianWeatherAudioController::AAgrarianWeatherAudioController()
{
PrimaryActorTick.bCanEverTick = true;
bReplicates = false;
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
RootComponent = SceneRoot;
AmbientAudio = CreateDefaultSubobject<UAudioComponent>(TEXT("AmbientAudio"));
AmbientAudio->SetupAttachment(SceneRoot);
AmbientAudio->bAutoActivate = false;
RainAudio = CreateDefaultSubobject<UAudioComponent>(TEXT("RainAudio"));
RainAudio->SetupAttachment(SceneRoot);
RainAudio->bAutoActivate = false;
WindAudio = CreateDefaultSubobject<UAudioComponent>(TEXT("WindAudio"));
WindAudio->SetupAttachment(SceneRoot);
WindAudio->bAutoActivate = false;
StormAudio = CreateDefaultSubobject<UAudioComponent>(TEXT("StormAudio"));
StormAudio->SetupAttachment(SceneRoot);
StormAudio->bAutoActivate = false;
}
void AAgrarianWeatherAudioController::BeginPlay()
{
Super::BeginPlay();
AssignConfiguredSounds();
RefreshWeatherAudio(0.0f);
}
void AAgrarianWeatherAudioController::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
RefreshWeatherAudio(DeltaSeconds);
}
void AAgrarianWeatherAudioController::RefreshWeatherAudio(float DeltaSeconds)
{
const UWorld* World = GetWorld();
const AAgrarianGameState* GameState = World ? World->GetGameState<AAgrarianGameState>() : nullptr;
if (!GameState)
{
return;
}
CurrentWeather = GameState->Weather;
const float WindAlpha = GetProviderWindAlpha(GameState->ActiveWeatherInputs.WindSpeedKmh, GameState->ActiveWeatherInputs.bHasProviderData);
float TargetRainVolume = 0.0f;
float TargetWindVolume = WindAlpha * MaxWindVolume;
float TargetStormVolume = 0.0f;
switch (GameState->Weather)
{
case EAgrarianWeatherType::Rain:
TargetRainVolume = MaxRainVolume;
TargetWindVolume = FMath::Max(TargetWindVolume, MaxWindVolume * 0.25f);
break;
case EAgrarianWeatherType::ColdWind:
TargetWindVolume = FMath::Max(TargetWindVolume, MaxWindVolume * 0.7f);
break;
case EAgrarianWeatherType::Storm:
TargetRainVolume = MaxRainVolume;
TargetWindVolume = MaxWindVolume;
TargetStormVolume = MaxStormVolume;
break;
default:
break;
}
const float TargetAmbientVolume = GameState->IsNight() ? AmbientNightVolume : AmbientDayVolume;
const float InterpSpeed = FMath::Max(0.1f, VolumeInterpSpeed);
CurrentAmbientVolume = FMath::FInterpTo(CurrentAmbientVolume, TargetAmbientVolume, DeltaSeconds, InterpSpeed);
CurrentRainVolume = FMath::FInterpTo(CurrentRainVolume, TargetRainVolume, DeltaSeconds, InterpSpeed);
CurrentWindVolume = FMath::FInterpTo(CurrentWindVolume, TargetWindVolume, DeltaSeconds, InterpSpeed);
CurrentStormVolume = FMath::FInterpTo(CurrentStormVolume, TargetStormVolume, DeltaSeconds, InterpSpeed);
if (DeltaSeconds <= 0.0f)
{
CurrentAmbientVolume = TargetAmbientVolume;
CurrentRainVolume = TargetRainVolume;
CurrentWindVolume = TargetWindVolume;
CurrentStormVolume = TargetStormVolume;
}
ApplyComponentVolume(AmbientAudio, CurrentAmbientVolume);
ApplyComponentVolume(RainAudio, CurrentRainVolume);
ApplyComponentVolume(WindAudio, CurrentWindVolume);
ApplyComponentVolume(StormAudio, CurrentStormVolume);
}
void AAgrarianWeatherAudioController::AssignConfiguredSounds()
{
if (AmbientAudio && ClearAmbientSound)
{
AmbientAudio->SetSound(ClearAmbientSound);
}
if (RainAudio && RainLoopSound)
{
RainAudio->SetSound(RainLoopSound);
}
if (WindAudio && WindLoopSound)
{
WindAudio->SetSound(WindLoopSound);
}
if (StormAudio && StormLoopSound)
{
StormAudio->SetSound(StormLoopSound);
}
}
void AAgrarianWeatherAudioController::ApplyComponentVolume(UAudioComponent* AudioComponent, float Volume) const
{
if (!AudioComponent)
{
return;
}
const float SafeVolume = FMath::Clamp(Volume, 0.0f, 1.0f);
AudioComponent->SetVolumeMultiplier(SafeVolume);
if (AudioComponent->Sound && SafeVolume > 0.01f && !AudioComponent->IsPlaying())
{
AudioComponent->Play();
}
else if (SafeVolume <= 0.01f && AudioComponent->IsPlaying())
{
AudioComponent->Stop();
}
}
float AAgrarianWeatherAudioController::GetProviderWindAlpha(float WindSpeedKmh, bool bHasProviderData) const
{
if (!bHasProviderData)
{
return 0.0f;
}
return FMath::Clamp(WindSpeedKmh / 55.0f, 0.0f, 1.0f);
}
@@ -0,0 +1,92 @@
// Copyright Pacificao. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "AgrarianTypes.h"
#include "AgrarianWeatherAudioController.generated.h"
class UAudioComponent;
class USceneComponent;
class USoundBase;
UCLASS(Blueprintable)
class AAgrarianWeatherAudioController : public AActor
{
GENERATED_BODY()
public:
AAgrarianWeatherAudioController();
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio")
TObjectPtr<USceneComponent> SceneRoot;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio")
TObjectPtr<UAudioComponent> AmbientAudio;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio")
TObjectPtr<UAudioComponent> RainAudio;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio")
TObjectPtr<UAudioComponent> WindAudio;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio")
TObjectPtr<UAudioComponent> StormAudio;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio")
TObjectPtr<USoundBase> ClearAmbientSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio")
TObjectPtr<USoundBase> RainLoopSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio")
TObjectPtr<USoundBase> WindLoopSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio")
TObjectPtr<USoundBase> StormLoopSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.1"))
float VolumeInterpSpeed = 1.5f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float AmbientDayVolume = 0.35f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float AmbientNightVolume = 0.22f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float MaxRainVolume = 0.8f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float MaxWindVolume = 0.7f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float MaxStormVolume = 0.9f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio")
EAgrarianWeatherType CurrentWeather = EAgrarianWeatherType::Clear;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio")
float CurrentAmbientVolume = 0.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio")
float CurrentRainVolume = 0.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio")
float CurrentWindVolume = 0.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio")
float CurrentStormVolume = 0.0f;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Weather Audio")
void RefreshWeatherAudio(float DeltaSeconds);
protected:
void AssignConfiguredSounds();
void ApplyComponentVolume(UAudioComponent* AudioComponent, float Volume) const;
float GetProviderWindAlpha(float WindSpeedKmh, bool bHasProviderData) const;
};