Class GameDev* SheepAdult

[Unreal Engine 4] OnComponentHit Sound C++ (Physics Collision Sound) 본문

Unreal Engine

[Unreal Engine 4] OnComponentHit Sound C++ (Physics Collision Sound)

SheepAdult 2022. 12. 5. 03:31

(2022-12-27 수정) 

 물건이 떨어져서 바닥에 닿았을 때 소리가 나는 경우를 구현하고자 할 때, OnComponentHit을 사용해서 구현해야 한다는 생각이 자연스럽게 들 것이다. 하지만, OnComponentHit은 우리가 생각했던 것보다 더 많은 빈도수로 호출된다. 만약 어떠한 처리도 하지 않고 물건을 떨어뜨리면 바닥에 한 번만 닿아도 수십 번의 소리가 재생되어 원치 않던 소리가 나므로 적절한 처리를 해주어야 한다. 본문에서 사용할 처리 방법은 OnComponentHit의 매개변수인 NormalImpulse에 임계점을 주어 강한 충돌만 다룰 것이다.

 

 여러 오브젝트에 이식할 수 있도록 컴포넌트로 만들었다. 아래는 PhysicsCollisionSoundComponent.cpp 이다.

// PhysicsCollisionSoundComponent.cpp 
void UPhysicsCollisionSoundComponent::BeginPlay()
{
	Super::BeginPlay();
	auto* PrimitiveComponent = Cast<UPrimitiveComponent>(GetOwner()->GetRootComponent());
	if (PrimitiveComponent)
	{
		PrimitiveComponent->SetNotifyRigidBodyCollision(true);
		PrimitiveComponent->OnComponentHit.AddDynamic(this, &UPhysicsCollisionSoundComponent::OnHit);
	}
}

SetNotifyRigidBodyCollision는 충돌 시 이벤트를 전달하는 역할로, 블루프린트에서 Simulation Generates Hit Events이다. 해당 옵션을 켜야 바인딩된 함수를 호출할 수 있다.

SetNotifyRigidBodyCollision

아래는 OnComponentHit에 바인딩된 함수이다. 기본적인 뼈대는 아래와 같다.

void UPhysicsCollisionSoundComponent::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
	...
	if (NormalImpulse.Size() > ImpulseThreshold * 10000)
	{
		UGameplayStatics::PlaySoundAtLocation(GetWorld(), ThrowableSound, Hit.Location);
	}
    ...
}

 ImpulseThreshold는 float으로 충돌 크기에 제한을 주기 위한 변수이다. 값이 크면 클 수록 소리 재생의 빈도 수는 적어진다. 여러 요인에 따라 다르겠지만 본인은 해당 값이 10000을 곱한 것을 포함하여 50000 ~ 100000 정도가 되어야 자연스러운 느낌을 주었다. 

 충돌 크기가 Threshold 값보다 커야지만 소리가 나므로 한 번 바닥에 충돌하고 그다음 작은 충돌들은 무시되는 원리이다. 만약 닿는 바닥이나 물체 자체의 Physics Material에 따라 소리를 다르게 하고 싶다면 매개 변수를 적절히 가져와 코드를 짜면 될 것이다.

 

::2022-12-27 수정::

좀 더 관리하기 편한 방향으로 수정했다. Owner Actor의 Physical Material에 따라 소리를 다르게 했으며 충돌 세기에 비례하여 Volume도 다르게 설정하여 재생하도록 했다. 해당 cpp파일에 FObjectFinder로 Physical Material들을 배열로 저장한 후 하나하나 Owner의 Physical Material과 비교하여 맞는 Index를 Cue의 switch에 대입하여 이에 대응하는 소리를 재생하게 했다. Owner Actor가 될 cpp파일에 Audio 컴포넌트와  PhysicsCollisionSound 컴포넌트만 추가하면 작동한다.

 

* 해당 cpp파일에서 owner에 audio컴포넌트를 추가까지하여 해당 컴포넌트만 추가하면 되도록 하려했으나 알 수 없는 이유로 소리가 재생되지 않았다. Audio 컴포너트와 PhysicsCollisionSound 컴포넌트도 Owner Actor에 잘 추가 되었고 cue도 잘 대입 되었지만 소리만 나지 않아 후추에 좀 더 손 볼 예정이다.

// PhysicsCollisionSoundComponent.cpp
void UPhysicsCollisionSoundComponent::BeginPlay()
{
	Super::BeginPlay();
	SetAudioComponent();
	SetPrimitiveComponent();
	SetPhysicsMateialIndex();
}

 // 부모 오브젝트에서 AudioComponent를 불러와 맞는 Cue를 대입합니다.
void UPhysicsCollisionSoundComponent::SetAudioComponent()
{
	AudioComponent = GetOwner()->FindComponentByClass<UAudioComponent>();
	auto* SoundManager = Cast<ASoundManager>(UGameplayStatics::GetActorOfClass(GetOwner()->GetWorld(), ASoundManager::StaticClass()));
	if (SoundManager && AudioComponent)
	{
		AudioComponent->SetSound(SoundManager->PhysicsCollisionSound);
	}
}

void UPhysicsCollisionSoundComponent::SetPrimitiveComponent()
{
	PrimitiveComponent = Cast<UPrimitiveComponent>(GetOwner()->GetComponentByClass(UStaticMeshComponent::StaticClass()));
	if (PrimitiveComponent)
	{
		PrimitiveComponent->SetNotifyRigidBodyCollision(true);
		PrimitiveComponent->OnComponentHit.AddDynamic(this, &UPhysicsCollisionSoundComponent::OnHit);
	}
}

// 저장된 Physical Material배열과 현재 Onwer오브젝트의 Physical Material을 비교하여 맞는
// Index의 Sound를 Parameter을 통해 수정합니다.
void UPhysicsCollisionSoundComponent::SetPhysicsMateialIndex()
{
	for (int i = 0; i < PhysicalMaterials.Num(); i++)
	{
		if (PrimitiveComponent->GetMaterial(0)->GetPhysicalMaterial() == PhysicalMaterials[i])
		{
			AudioComponent->SetIntParameter(FName("PhysicsCollisionParam"), i);
			break;
		}
	}
}

void UPhysicsCollisionSoundComponent::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
	...
	// 만약 특정 소리가 일정 임계값 이상이면 재생합니다.
    // ImpulseThreshold 는 10, VolumeSensitivity 는 50 이 기본값입니다.
	if (NormalImpulse.Size() > ImpulseThreshold * 10000)
	{
		float Volume = UKismetMathLibrary::FMin(NormalImpulse.Size() / (VolumeSensitivity * 10000), 1.f);
		// 충동 세기에 비례하여 Volume 조절
		AudioComponent->VolumeMultiplier = Volume;
		AudioComponent->Play();
	}
}

 

실행 결과:

https://www.youtube.com/watch?v=Ohqj-1psTSk 

 

 :: 2023-01-12 추가

위의 영상과는 달리 소리의 음량이 충돌 세기에 비례하여 조절되며 physics material에 따라 소리를 다르게 했다. 소리는 임의로 넣었다.

https://youtu.be/grl3gKR_vlk