Add MVP wildlife audio hooks

This commit is contained in:
2026-05-19 12:33:01 -07:00
parent 6cad0682ff
commit f6e126aabb
5 changed files with 145 additions and 1 deletions
+1 -1
View File
@@ -846,7 +846,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
- [x] Persist active grass, forest, and structure fires across save/load without corrupting world state. Extended campfire persistence coverage for ignition flags, ignition risk scores, active grass/forest/structure fire intensities, spread radius, and suppression pressure so save/load recovery preserves active and partially suppressed fire state. - [x] Persist active grass, forest, and structure fires across save/load without corrupting world state. Extended campfire persistence coverage for ignition flags, ignition risk scores, active grass/forest/structure fire intensities, spread radius, and suppression pressure so save/load recovery preserves active and partially suppressed fire state.
- [x] Add QA coverage for safe campfires, unsafe campfires, vegetation spread, shelter ignition, suppression, and save/load recovery. Added a fire-risk QA coverage document and verifier requiring safe/unsafe campfire, vegetation spread, shelter ignition, suppression, and save/load recovery scenarios plus the supporting fire-risk verification scripts. - [x] Add QA coverage for safe campfires, unsafe campfires, vegetation spread, shelter ignition, suppression, and save/load recovery. Added a fire-risk QA coverage document and verifier requiring safe/unsafe campfire, vegetation spread, shelter ignition, suppression, and save/load recovery scenarios plus the supporting fire-risk verification scripts.
- [x] Add weather sounds. Formalized the existing placed weather audio controller as the MVP weather-sound path, documenting rain, wind, storm, clear ambient, and biome loop slots plus verification that weather playback follows replicated weather state, provider wind speed, and day/night state while remaining silent until assets are assigned. - [x] Add weather sounds. Formalized the existing placed weather audio controller as the MVP weather-sound path, documenting rain, wind, storm, clear ambient, and biome loop slots plus verification that weather playback follows replicated weather state, provider wind speed, and day/night state while remaining silent until assets are assigned.
- [ ] Add wildlife sounds. - [x] Add wildlife sounds. Added spatialized wildlife audio hooks with assignable idle, flee/chase, death, and harvest sound slots plus server-triggered multicast playback from authoritative wildlife state changes and harvest events.
- [ ] Add UI sounds. - [ ] Add UI sounds.
- [ ] Add mix settings. - [ ] Add mix settings.
- [ ] Add volume sliders. - [ ] Add volume sliders.
+5
View File
@@ -318,6 +318,11 @@ controller or a Blueprint child.
Weather sound requirements are tracked in `Docs/Audio/WeatherSounds.md`. Weather sound requirements are tracked in `Docs/Audio/WeatherSounds.md`.
Wildlife sounds are optional, spatialized hooks on `AAgrarianWildlifeBase`.
Wildlife expose idle, flee/chase, death, and harvest sound slots. The server
multicasts state-change and harvest cues so clients hear wildlife reactions from
the authoritative AI state while the dedicated server remains silent.
Player movement audio starts with native footstep placeholders on Player movement audio starts with native footstep placeholders on
`AAgrarianGameCharacter`. The character owns a spatialized `AAgrarianGameCharacter`. The character owns a spatialized
`FootstepAudioComponent` plus assignable walk, sprint, crouch, and prone sound `FootstepAudioComponent` plus assignable walk, sprint, crouch, and prone sound
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Verify MVP wildlife sound hooks are authoritative and spatialized."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
WILDLIFE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianWildlifeBase.h"
WILDLIFE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianWildlifeBase.cpp"
TDD = ROOT / "Docs" / "TechnicalDesignDocument.md"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
REQUIRED = {
WILDLIFE_H: [
"TObjectPtr<UAudioComponent> WildlifeAudioComponent;",
"TObjectPtr<USoundBase> IdleWildlifeSound;",
"TObjectPtr<USoundBase> FleeWildlifeSound;",
"TObjectPtr<USoundBase> DeathWildlifeSound;",
"TObjectPtr<USoundBase> HarvestWildlifeSound;",
"UFUNCTION(NetMulticast, Unreliable)",
"void MulticastPlayWildlifeStateSound(EAgrarianWildlifeState NewState);",
"void MulticastPlayWildlifeHarvestSound();",
"USoundBase* GetSoundForWildlifeState(EAgrarianWildlifeState State) const;",
],
WILDLIFE_CPP: [
"#include \"Components/AudioComponent.h\"",
"WildlifeAudioComponent = CreateDefaultSubobject<UAudioComponent>",
"WildlifeAudioComponent->bAllowSpatialization = true",
"MulticastPlayWildlifeStateSound(WildlifeState);",
"AAgrarianWildlifeBase::MulticastPlayWildlifeStateSound_Implementation",
"AAgrarianWildlifeBase::MulticastPlayWildlifeHarvestSound_Implementation",
"MulticastPlayWildlifeHarvestSound();",
"EAgrarianWildlifeState::Fleeing",
"EAgrarianWildlifeState::Dead",
],
TDD: [
"Wildlife sounds are optional, spatialized hooks",
"idle, flee/chase, death, and harvest",
"server\nmulticasts state-change and harvest cues",
],
ROADMAP: [
"[x] Add wildlife sounds.",
],
}
def main() -> None:
missing = []
for path, snippets in REQUIRED.items():
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}")
if missing:
raise SystemExit("FAILED: " + "; ".join(missing))
print("OK: wildlife sound hooks are authoritative and spatialized.")
if __name__ == "__main__":
main()
@@ -4,6 +4,7 @@
#include "AgrarianGameCharacter.h" #include "AgrarianGameCharacter.h"
#include "AgrarianInventoryComponent.h" #include "AgrarianInventoryComponent.h"
#include "AIController.h" #include "AIController.h"
#include "Components/AudioComponent.h"
#include "Components/StaticMeshComponent.h" #include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h" #include "Engine/StaticMesh.h"
#include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/CharacterMovementComponent.h"
@@ -86,6 +87,11 @@ AAgrarianWildlifeBase::AAgrarianWildlifeBase()
WildlifeTailProxy->SetRelativeRotation(FRotator(0.0f, 90.0f, 90.0f)); WildlifeTailProxy->SetRelativeRotation(FRotator(0.0f, 90.0f, 90.0f));
WildlifeTailProxy->SetRelativeScale3D(FVector(0.12f, 0.12f, 0.24f)); WildlifeTailProxy->SetRelativeScale3D(FVector(0.12f, 0.12f, 0.24f));
WildlifeAudioComponent = CreateDefaultSubobject<UAudioComponent>(TEXT("WildlifeAudioComponent"));
WildlifeAudioComponent->SetupAttachment(RootComponent);
WildlifeAudioComponent->bAutoActivate = false;
WildlifeAudioComponent->bAllowSpatialization = true;
DisplayName = FText::FromString(TEXT("Wildlife")); DisplayName = FText::FromString(TEXT("Wildlife"));
} }
@@ -168,6 +174,10 @@ void AAgrarianWildlifeBase::SetWildlifeState(EAgrarianWildlifeState NewState)
} }
WildlifeState = NewState; WildlifeState = NewState;
if (HasAuthority())
{
MulticastPlayWildlifeStateSound(WildlifeState);
}
BroadcastStateChanged(); BroadcastStateChanged();
} }
@@ -204,6 +214,34 @@ void AAgrarianWildlifeBase::OnRep_WildlifeState()
BroadcastStateChanged(); BroadcastStateChanged();
} }
void AAgrarianWildlifeBase::MulticastPlayWildlifeStateSound_Implementation(EAgrarianWildlifeState NewState)
{
if (GetNetMode() == NM_DedicatedServer || !WildlifeAudioComponent)
{
return;
}
USoundBase* StateSound = GetSoundForWildlifeState(NewState);
if (!StateSound)
{
return;
}
WildlifeAudioComponent->SetSound(StateSound);
WildlifeAudioComponent->Play();
}
void AAgrarianWildlifeBase::MulticastPlayWildlifeHarvestSound_Implementation()
{
if (GetNetMode() == NM_DedicatedServer || !WildlifeAudioComponent || !HarvestWildlifeSound)
{
return;
}
WildlifeAudioComponent->SetSound(HarvestWildlifeSound);
WildlifeAudioComponent->Play();
}
bool AAgrarianWildlifeBase::ShouldRunServerThink(float DeltaSeconds) bool AAgrarianWildlifeBase::ShouldRunServerThink(float DeltaSeconds)
{ {
if (!bEnablePerformanceLimits || WildlifeState == EAgrarianWildlifeState::Dead) if (!bEnablePerformanceLimits || WildlifeState == EAgrarianWildlifeState::Dead)
@@ -524,6 +562,7 @@ bool AAgrarianWildlifeBase::Harvest(AAgrarianGameCharacter* Interactor)
} }
bHarvested = true; bHarvested = true;
MulticastPlayWildlifeHarvestSound();
return true; return true;
} }
@@ -536,3 +575,19 @@ void AAgrarianWildlifeBase::BroadcastStateChanged()
{ {
OnWildlifeStateChanged.Broadcast(WildlifeState); OnWildlifeStateChanged.Broadcast(WildlifeState);
} }
USoundBase* AAgrarianWildlifeBase::GetSoundForWildlifeState(EAgrarianWildlifeState State) const
{
switch (State)
{
case EAgrarianWildlifeState::Fleeing:
case EAgrarianWildlifeState::Chasing:
return FleeWildlifeSound;
case EAgrarianWildlifeState::Dead:
return DeathWildlifeSound;
case EAgrarianWildlifeState::Idle:
case EAgrarianWildlifeState::Wandering:
default:
return IdleWildlifeSound;
}
}
@@ -9,6 +9,8 @@
#include "AgrarianWildlifeBase.generated.h" #include "AgrarianWildlifeBase.generated.h"
class AAgrarianGameCharacter; class AAgrarianGameCharacter;
class UAudioComponent;
class USoundBase;
class UStaticMeshComponent; class UStaticMeshComponent;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianWildlifeStateChangedSignature, EAgrarianWildlifeState, NewState); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianWildlifeStateChangedSignature, EAgrarianWildlifeState, NewState);
@@ -46,6 +48,9 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Wildlife|Visuals") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Wildlife|Visuals")
TObjectPtr<UStaticMeshComponent> WildlifeTailProxy; TObjectPtr<UStaticMeshComponent> WildlifeTailProxy;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Wildlife|Audio")
TObjectPtr<UAudioComponent> WildlifeAudioComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife")
FName WildlifeId = TEXT("wildlife"); FName WildlifeId = TEXT("wildlife");
@@ -109,6 +114,18 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Wildlife|Harvest") UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Wildlife|Harvest")
bool bHarvested = false; bool bHarvested = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Audio")
TObjectPtr<USoundBase> IdleWildlifeSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Audio")
TObjectPtr<USoundBase> FleeWildlifeSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Audio")
TObjectPtr<USoundBase> DeathWildlifeSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Audio")
TObjectPtr<USoundBase> HarvestWildlifeSound;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Wildlife") UFUNCTION(BlueprintCallable, Category = "Agrarian|Wildlife")
bool IsAlive() const; bool IsAlive() const;
@@ -131,6 +148,12 @@ protected:
UFUNCTION() UFUNCTION()
void OnRep_WildlifeState(); void OnRep_WildlifeState();
UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayWildlifeStateSound(EAgrarianWildlifeState NewState);
UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayWildlifeHarvestSound();
bool ShouldRunServerThink(float DeltaSeconds); bool ShouldRunServerThink(float DeltaSeconds);
void ServerThink(float DeltaSeconds); void ServerThink(float DeltaSeconds);
void ChooseWanderTarget(); void ChooseWanderTarget();
@@ -146,6 +169,7 @@ protected:
bool Harvest(AAgrarianGameCharacter* Interactor); bool Harvest(AAgrarianGameCharacter* Interactor);
void BroadcastHealthChanged(); void BroadcastHealthChanged();
void BroadcastStateChanged(); void BroadcastStateChanged();
USoundBase* GetSoundForWildlifeState(EAgrarianWildlifeState State) const;
UPROPERTY() UPROPERTY()
FVector SpawnLocation = FVector::ZeroVector; FVector SpawnLocation = FVector::ZeroVector;