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] 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.
- [ ] 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 mix settings.
- [ ] 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`.
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
`AAgrarianGameCharacter`. The character owns a spatialized
`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 "AgrarianInventoryComponent.h"
#include "AIController.h"
#include "Components/AudioComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h"
#include "GameFramework/CharacterMovementComponent.h"
@@ -86,6 +87,11 @@ AAgrarianWildlifeBase::AAgrarianWildlifeBase()
WildlifeTailProxy->SetRelativeRotation(FRotator(0.0f, 90.0f, 90.0f));
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"));
}
@@ -168,6 +174,10 @@ void AAgrarianWildlifeBase::SetWildlifeState(EAgrarianWildlifeState NewState)
}
WildlifeState = NewState;
if (HasAuthority())
{
MulticastPlayWildlifeStateSound(WildlifeState);
}
BroadcastStateChanged();
}
@@ -204,6 +214,34 @@ void AAgrarianWildlifeBase::OnRep_WildlifeState()
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)
{
if (!bEnablePerformanceLimits || WildlifeState == EAgrarianWildlifeState::Dead)
@@ -524,6 +562,7 @@ bool AAgrarianWildlifeBase::Harvest(AAgrarianGameCharacter* Interactor)
}
bHarvested = true;
MulticastPlayWildlifeHarvestSound();
return true;
}
@@ -536,3 +575,19 @@ void AAgrarianWildlifeBase::BroadcastStateChanged()
{
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"
class AAgrarianGameCharacter;
class UAudioComponent;
class USoundBase;
class UStaticMeshComponent;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianWildlifeStateChangedSignature, EAgrarianWildlifeState, NewState);
@@ -46,6 +48,9 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Wildlife|Visuals")
TObjectPtr<UStaticMeshComponent> WildlifeTailProxy;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Wildlife|Audio")
TObjectPtr<UAudioComponent> WildlifeAudioComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife")
FName WildlifeId = TEXT("wildlife");
@@ -109,6 +114,18 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Wildlife|Harvest")
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")
bool IsAlive() const;
@@ -131,6 +148,12 @@ protected:
UFUNCTION()
void OnRep_WildlifeState();
UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayWildlifeStateSound(EAgrarianWildlifeState NewState);
UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayWildlifeHarvestSound();
bool ShouldRunServerThink(float DeltaSeconds);
void ServerThink(float DeltaSeconds);
void ChooseWanderTarget();
@@ -146,6 +169,7 @@ protected:
bool Harvest(AAgrarianGameCharacter* Interactor);
void BroadcastHealthChanged();
void BroadcastStateChanged();
USoundBase* GetSoundForWildlifeState(EAgrarianWildlifeState State) const;
UPROPERTY()
FVector SpawnLocation = FVector::ZeroVector;