SimulatePhysics, 사운드 어테뉴에이션

김지윤·2025년 2월 2일

UE5_MultiplayerShooter

목록 보기
7/25
post-thumbnail

저번 게시물까지 총알 발사를 구현했고, 이번엔 탄피 배출을 구현했다.
강의에선 성급하게 데미지를 주고받는 등의 로직을 구현하지 않고, 차근차근 게임의 만듦새를 보강하며 언리얼의 기능을 하나씩 활용할 수 있게 돕고 있다.
회사 선배의 추천으로 듣고 있는 강의인데 정말 마음에 든다.

// Weapon.h
UPROPERTY(EditAnywhere)
TSubclassOf<class ACasing> CasingClass;

//Weapon.cpp
void AWeapon::Fire(const FVector& HitTarget)
{
	if (FireAnimation)
	{
		WeaponMesh->PlayAnimation(FireAnimation, false);
	}
	if (CasingClass)
	{
		const USkeletalMeshSocket* AmmoEjectSocket = WeaponMesh->GetSocketByName(FName("AmmoEject"));
		
		if (AmmoEjectSocket)
		{
			FTransform SocketTransform = AmmoEjectSocket->GetSocketTransform(WeaponMesh);
			
			// CasingClass를 기반으로 한 액터 스폰
			UWorld* World = GetWorld();
			if (World)
			{
				World->SpawnActor<ACasing>(
					CasingClass,
					SocketTransform.GetLocation(),
					SocketTransform.GetRotation().Rotator()
				);
			}
		}
	}
}

Weapon 클래스의 Fire 함수에서 Casing 클래스를 기반으로 액터 스폰만 해주고 있다.
메쉬의 "AmmoEject"라는 소켓의 위치에서 스폰하고 있다.
CasingClass는 (강의에서도 그렇지만) TSoftClassPtr보다 TSubclassOf가 적절하다고 생각했다.
슈팅 게임이라서 사격이 멈추는 순간이 없을 거기 때문에 탄피는 계속 쏟아져나올 거고, 그때마다 클래스를 로드하면서 사용하는 쪽이 오히려 손해다.

//Casing.cpp
ACasing::ACasing()
{
	PrimaryActorTick.bCanEverTick = false;

	CasingMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("CasingMesh"));
	SetRootComponent(CasingMesh);
	
	CasingMesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	CasingMesh->SetSimulatePhysics(true);
	CasingMesh->SetEnableGravity(true);
	CasingMesh->SetNotifyRigidBodyCollision(true);
	ShellEjectionImpulse = 5.f;
}

생성자에서 카메라와 캐릭터 사이에 탄피가 있으면 스프링암이 앞으로 땡겨지므로 Ignore 처리해주고
물리 시뮬레이트와 중력을 활성화
Mesh가 HitEvent를 발생할 수 있도록 설정해줬다.
Impulse(힘의 크기라고 이해하면 편하다)는 5정도 줬다.

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

	CasingMesh->OnComponentHit.AddDynamic(this, &ThisClass::OnHit);

	CasingMesh->AddImpulse(GetActorForwardVector() * ShellEjectionImpulse);
}

생성을 마치면 HitEvent에 OnHit 함수를 바인딩하며, ForwardVector 방향으로 AddImpulse해준다.
여기서 ForwadVector는 앞서 말한 WeaponMesh의 AmmoEject 소켓의 ForwardVector를 말한다.

AmmoEject 소켓의 ForwardVector가 총의 오른쪽을 바라볼 수 있도록 적절히 조정해줬다.

void ACasing::OnHit(UPrimitiveComponent* HitComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	FVector NormalImpulse,
	const FHitResult& Hit)
{
	// 더이상 HitEvent를 발생시키지 않도록 설정, 해당 함수가 연속 호출되는 걸 방지
	CasingMesh->SetNotifyRigidBodyCollision(false);
	
	if (ShellSound)
	{
		UGameplayStatics::PlaySoundAtLocation(this, ShellSound, GetActorLocation());
	}

	// 5초 후 Destroy하도록 람다식 타이머에 바인드
	FTimerHandle TimerHandle;
	GetWorld()->GetTimerManager().SetTimer(TimerHandle, [this]()
	{
		Destroy();
	}, 5.0f, false);
}

OnHit 이벤트의 경우, 더이상 HitEvent가 발생하지 않도록 false로 설정해줬으며 (이걸 하지 않으면 부딪힐 때마다 해당 함수가 호출됨)
ShellSound를 재생하고, 5초 후 Destory되도록 타이머에 바인드해줬다.

이건 탄피 사운드의 어테뉴에이션 중 일부다.
저번엔 전혀 모르겠다고 게시물을 작성했는데, 이 정도는 읽어보니 알아들어서 적는다.
AudioListenerComponent(나에게 재생될 소리를 듣는다. PlayerController가 갖고 있다.)로부터 거리 200 내에 있으면 최대, 멀어질수록 점점 줄어들다가 1500에서 0이 된다.
그 줄어드는 정도를 어테뉴에이션 함수로 정할 수 있고, 그 모양을 셰이프로 정할 수 있다.
틀릴 수도 있는데 이런 의미인 것 같다.

어쨌든 잘 작동한다. 탄피는 노란색이고 초원은 연두색이라 잘 안 보일 것 같아서 그림자 위에 떨어지도록 촬영했다. 움짤이라 들려줄 순 없지만 소리도 잘 재생된다.

profile
공부한 거 시간 날 때 작성하는 곳

0개의 댓글