TIL_056: 리타겟팅, 총알 발사

김펭귄·2025년 10월 31일

Today What I Learned (TIL)

목록 보기
56/94

오늘 학습 키워드

  • 리타겟팅

  • 총알 발사 로직

1. 리타켓팅

  • 프로젝트에서 캐릭터에 사용할 스켈레톤 메시의 스켈레톤과, 애니메이션의 스켈레톤이 다름

  • 애니메이션이 더 적합하게 움직이도록 스켈레톤 리타겟팅에 대해 알아보며 적용해보았음

IK_Rig

  • 언리얼 에디터에서 Auto Create Retarget Chains 버튼을 누르면 자동으로 Retarget Chain을 생성해줌

  • 그리고 옆에 Auto Create IK버튼을 누르면 Full Body IK도 자동으로 생성

  • 다른 스켈레톤도 마찬가지로 해주기

  • 하단 우측을 보면 Chain들이 있는데 이 Chain구조를 두 스켈레톤이 동일하도록 만들어주면 됨

  • Chain Name을 동일하게 가지도록 해주고, 각 뼈의 계층 구조를 확인하며 Chain에 각 뼈들을 넣어준다

  • 해당 Chain에 속하는 뼈들은 Start Bone부터 End Bone까지가 하나의 Chain으로 포함된다

  • UEFN(애니메이션 스켈레톤)의 경우 Trooper(스켈레탈메시의 스켈레톤)와 다르게 팔-metacarpal-손가락 형식의 Chain을 가졌고, Trooper는 팔-손가락 형식의 Chain 구조를 가졌었음

  • 따라서, UEFN의 metacarpal 체인을 제거하고 해당 뼈 구조를 팔 체인의 EndBone으로 설정해주어 팔 체인에 속하게 해줌

ReTargeter

  • 그리고 IK_ReTargeter를 이용해 방금 만든 두 개의 IK_Rig파일을 서로 연동해줌

  • 우측 하단에서 볼 수 있듯이 Source와 Target의 체인이 서로 연결된 것을 확인할 수 있음

  • 우측 상단의 Detail 패널에서는 위치 조정, 크기 조정 등 여러 옵션을 선택 가능

  • Asset Browser에서 애니메이션들을 확인해보며 잘 되었는지 확인 가능

  • Export버튼을 이용하여 리타겟팅이 적용된 애니메이션을 저장할 수 있다

문제 상황

  • 대부분의 애니메이션은 잘 적용이 되었지만, 위의 영상에서도 나온 Land 애니메이션에 문제가 발생

  • 리타겟터에서는 잘 작동하였지만, 막상 Export하여 저장 후 애니메이션 시퀀스를 보면 스켈레톤이 망가진 것을 확인하였음

  • 스켈레탈 체인이 서로 다른지 확인해보고, ReTargeter를 다시 만들어보는 등 여러 방법을 시도해보았지만, 해결하지 못 하였고 해당 애니메이션만 리타겟팅 하지 않은 기본 Land 애니메이션으로 대체하였음

2. 낙하-착지 애니메이션 트러블슈팅

  • 새로 만든 애니메이션 시퀀스들로 애니메이션 블루프린트를 만들어 캐릭터에 적용하였음

  • 테스트 결과, 캐릭터가 지상에서 걷다가 떨어지면, 바닥에 착지해도 착지 애니메이션이 발생하지 않고 계속 Falling 애니메이션이 나타나는 것을 발견

  • 따라서 먼저 Falling State에서 Land State로 전환되는 조건인 Is Falling값이 잘 바뀌는지 확인하였음

  • Event Graph에서 Tick마다 Is Falling값을 확인해 본 결과, 착륙 시 false로 잘 바뀌었음. 즉, 착륙했다는 상태변환은 잘 되었음

  • Fall Loop에서 Land로 가는 Transition도 문제가 없으므로, 각 State에 문제가 있지 않을까 생각하여 확인해본 결과 To Land라는 State Alias 설정에 문제가 있었음

  • 해당 Alias에 Fall Loop State가 체크되어 있지 않아, Transition 전환이 되지 않았던 것

  • 설정해주어 문제를 해결하였음

3. 총알 생성

헤더

  • 날아가는 물체이므로 ProjectileMovementComponent를 이용하여 구현

  • 적과 충돌 시 데미지 가할 것이므로 OnHit 이용

// Bullet.h

UCLASS()
class INTOTHEPARADOX_API ABullet : public AActor
{
	// ... //

public:
	ABullet();
	virtual void FireInDirection(FVector Direction);

protected:
	virtual void BeginPlay() override;

	USceneComponent* SceneComponent;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "SkeletalMesh")
	UStaticMeshComponent* StaticMeshComponent;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Collision")
	UCapsuleComponent* CapsuleComponent;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Movement")
	UProjectileMovementComponent* ProjectileMovementComponent;

	UFUNCTION()
	void OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor,
		UPrimitiveComponent* OtherComp, FVector NormalImpulse,
		const FHitResult& Hit); 
};

cpp

ABullet::ABullet() :
    BulletSpeed(3000.f), 
    BulletDamage(20)
{
	// ... //
    
    CapsuleComponent = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
    CapsuleComponent->SetCollisionProfileName(TEXT("BlockAll"));
    CapsuleComponent->SetupAttachment(SceneComponent);
    CapsuleComponent->OnComponentHit.AddDynamic(this, &ABullet::OnHit);


    ProjectileMovementComponent = 
    	CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("PMovement"));
    ProjectileMovementComponent->SetUpdatedComponent(SceneComponent);
    ProjectileMovementComponent->InitialSpeed = BulletSpeed;
    ProjectileMovementComponent->MaxSpeed = BulletSpeed;
    ProjectileMovementComponent->bRotationFollowsVelocity = true;
    ProjectileMovementComponent->ProjectileGravityScale = 0.f;

    InitialLifeSpan = 3.0f;
}
  • Collision을 위해 Capsule Component 사용하였음.
    모든 물체와 Hit 이벤트를 발생하므로 BlockAll을 사용

  • Projectile Movement Component

    • SetUpdatedComponent() : 어느 컴포넌트를 이동시킬지 설정하는 함수

    • bRotationFollowsVelocity : 발사체가 이동하는 방향(속도 벡터 방향)에 따라 발사체의 회전 방향을 자동으로 맞추는 기능

    • ProjectileGravityScale : 발사체가 중력의 영향을 받는 정도(0 ~ 1)

  • Actor클래스의 InitialLifeSpan을 통해 3초 뒤 소멸하도록 설정

void ABullet::FireInDirection(FVector Direction)
{
    Direction.Normalize();
    ProjectileMovementComponent->Velocity = Direction * BulletSpeed;
}


void ABullet::OnHit(UPrimitiveComponent* HitComponent,
					AActor* OtherActor, 
                    UPrimitiveComponent* OtherComp, 
                    FVector NormalImpulse, 
                    const FHitResult& Hit)
{
    if (AITPAICharacter* AI = Cast<AITPAICharacter>(OtherActor))
    {
        UGameplayStatics::ApplyDamage(AI,
        							  BulletDamage,
                                      nullptr, 
                                      this,
                                      UDamageType::StaticClass());
    }
    Destroy();
}
  • FireInDirection() : 캐릭터에서 총알 spawn하고 호출하는 함수.
    속도값을 초기화해주어 총알이 원하는 방향으로 날아가도록 함

  • OnHit() : 부딪힌 상대가 적일때만 데미지 발생시킴

참고자료

4. 총알 발사

  • 공격 로직을 구현하기 위해, 마우스 좌클릭 시 총알이 발사되도록 구현하였음

  • Input 처리는 이전의 Enhanced Input System을 이용한 다른 Input Action과 동일하게 생성함

  • 다만, 마우스 좌클릭 유지 시 Auto처럼 총알이 자동으로 연발되게 구현

Binding

// MyCharacter.cpp
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	UEnhancedInputComponent* EnhancedInput = 
    	Cast<UEnhancedInputComponent>(PlayerInputComponent);
        
	if (EnhancedInput) {
		if (AMyPlayerController* PlayerController = 
        		Cast<AMyPlayerController>(GetController()))	{
			if (PlayerController->FireAction) {
				EnhancedInput->BindAction(PlayerController->FireAction,
                						  ETriggerEvent::Started, this,
                                          &AMyCharacter::StartFire);
                                          
				EnhancedInput->BindAction(PlayerController->FireAction,
                						  ETriggerEvent::Completed,
                                          this, &AMyCharacter::StopFire);
			}
		}
	}
}
  • 좌클릭하면, 자동발사하는 StartFire()를 호출할 것이므로, Started로 연결

발사 로직

  1. 좌클릭시, FireRate(발사주기)마다 총알이 자동으로 나가야 함

  2. 좌클릭을 떼면, 총알이 나가지 않아야 함

  3. 하지만 좌클릭 해제했다가 바로 좌클릭했을 때, 아직 발사주기가 되지 않았다면 발사주기가 될때까지 기다렸다가 총알이 나가야 함

void AMyCharacter::StartFire(const FInputActionValue& Value)
{
	bShouldFire = true;
	if (!(GetWorldTimerManager().IsTimerActive(FireHandle)))
	{
		Fire();
		GetWorld()->GetTimerManager().SetTimer(
			FireHandle,
			this,
			&AMyCharacter::Fire,
			ITPPlayerState->GetFireRate(),
			true
		);
	}
}

void AMyCharacter::StopFire(const FInputActionValue& Value)
{
	bShouldFire = false;
}

void AMyCharacter::Fire()
{
	if (!bShouldFire)
	{
		GetWorldTimerManager().ClearTimer(FireHandle);
		return;
	}

	if (!BulletClass)
		return;

	// 컨트롤러 회전 가져오기
	FRotator SpawnRotation = GetControlRotation();
	// 카메라 forward vector (바라보는 방향)
	FVector CameraForwardVector = SpawnRotation.Vector();

	// 총구 위치 설정 (캐릭터 위치 + 전방 오프셋)
	FVector SpawnLocation = 
    	GetActorLocation() + SpawnRotation.RotateVector(BulletOffset);

	ABullet* Bullet = GetWorld()->SpawnActor<ABullet>(
		BulletClass,
		SpawnLocation,
		SpawnRotation
	);

	// 발사체에 속도 설정 (포워드 벡터 방향)
	if (Bullet)
	{
		Bullet->FireInDirection(CameraForwardVector);
	}
}
  • 좌클릭 시 주기적으로 발사 로직이 실행되며, 좌클릭 해제하여 bShouldFire가 거짓이라면 Fire함수에서 타이머를 초기화하여 멈춤

  • 1번 조건은 반복실행되는 FireHandle타이머로 해결

  • 2번 조건은 bShouldFire 변수를 통해, 총알을 발사할 것인지 아닌지를 정함

  • 3번 조건은 이미 돌아가고 있는 타이머가 있으면 자동으로 Fire함수가 실행되므로 해결

5. 결과 영상

profile
반갑습니다

0개의 댓글