본문 바로가기

게임 개발/언리얼 개인 프로젝트-FlightFight

[언리얼 개인 프로젝트 - FlightFight/C++] #1. Pawn 설계 - (3) 총알 발사

반응형

FlightFight-포스터
<FlightFight 포스터>

비행기의 조작법 중, 마우스 좌클릭에 해당하는
총알 발사를 해보자.

언제, 어디서, 무엇을, 어떻게 작동시킬 것인지 생각해 보자.

<1>. 총알 구성 (무엇을)

너무나 당연하지만, 우선 총알을 발사하려면 총알이 먼저 있어야 한다. 이 총알을 어떻게 표현할지에 대해 생각해 보다, 생각보다 복잡할 거 같지 않아 다른 분이 만들어 놓은 에셋을 쓰기보다는 '액터 블루프린트'를 사용해 직접 총알을 만들어 보기로 했다.

 

총알-구성
<총알 구성>

 

루트 컴포넌트는 Cube 모양을 길게 늘여서 만들고 원하는 색상을 나타내는 Material을 만들어서 입혀주었다.

그리고 그 총알을 충분히 감쌀 만큼의 크기를 가진 Cube 모양의 Box Collision을 입혀주고, 총알의 움직임을 담당해 주는 ProjectileMovement(발사체 이동 컴포넌트)를 추가해 준다.

ProjectileMovement의 속성 값들을 자신이 원하는 대로 조정해 주면 된다.

 

총알-이벤트-그래프
<총알 이벤트 그래프>

 

총알 블루프린트의 이벤트 그래프이다. 요약하자면 '가해자가 아닌 물체와 접촉했다면, 접촉한 그 위치에서 폭발 효과를 생성하고 총알은 사라진다.'이다. 이 게임에서는 자신이 발사한 총알이 다른 비행기나 지형과 충돌했을 때 피격 효과가 나타나게 하기 위함이다.

<2>. 조작 (언제)

이 게임에서는 비행기가 총을 발사하는 기능을 해야 하고, 앞에서 비행기의 움직임에 대해 키 설정을 해준 것처럼 총알 발사에 대한 키 설정도 해준다.

조작-키-설정
<조작 키 설정>

 

앞에서 다룬 비행기의 움직임은 일정 범위 내의 연속적인 수치를 입력하는 Axis Mapping(축 매핑)이었지만, 총알 발사는 0 또 1의 수치만 입력하는 Action Mapping(액션 매핑)으로 설정해 준다.

 

//FFPawn.cpp

void AFFPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
     
    PlayerInputComponent->BindAxis(TEXT("MoveForward"), this, &AFFPawn::MoveForward);
    PlayerInputComponent->BindAxis(TEXT("Turn"), this, &AFFPawn::Turn);
    PlayerInputComponent->BindAxis(TEXT("LookUp"), this, &AFFPawn::LookUp);
    PlayerInputComponent->BindAxis(TEXT("Rolling"), this, &AFFPawn::Rolling);
    PlayerInputComponent->BindAction(TEXT("Fire"), EInputEvent::IE_Pressed, this, &AFFPawn::Fire);
    PlayerInputComponent->BindAction(TEXT("Fire"), EInputEvent::IE_Released, this, &AFFPawn::StopShooting);
}

 

따라서 Fire 키, 즉 왼쪽 마우스 버튼을 눌렀을 때는 Fire 함수를, 왼쪽 마우스 버튼에서 손을 뗐을 때는 StopShooting 함수를 실행할 수 있도록 C++로 작성을 하면 다음과 같다.  

 

 

<3>. 총알 발사 위치 (어디서)

총알이 비행기의 어떤 위치에서 발사되게 할 것인지도 정해줘야 한다. 이를 위해서는 Bone 구조가 나와있는 스켈레톤 메시에서 소켓을 추가해줘야 한다. (소켓 추가에 대한 자세한 방법은 다음 챕터에 나와있다)

 

소켓-위치
<소켓 위치>

 

비록 빈 공간이지만, 여태 비행기가 총알을 발사하는 모습을 봤을 때, 저 위치에 적당할 것 같아서 저 위치에서 총알이 나갈 수 있게 정했다.

 

<4>. 총알 발사 방법 (어떻게)

총알도 만들었고, 뭘 눌러야 발사되는지, 어디서 발사할 건지까지 정했으니 어떻게 총알을 발사해야 할지를 정해야 한다.

 

우선 모든 총알을 발사하는 게임이 그렇듯, 사용자의 화면 정가운데에 있는 조준선이 필요하다. 조준선 png 파일을 다운로드하여 'UserWidget Blueprint'를 생성한다.

 

조준선-블루프린트
<조준선 블루프린트>

 

사용자의 화면 전체를 범위로 잡을 것이기 때문에 'Fill Screen'으로 변경해 준 후에, 팔레트에서 캔버스 패널을 추가하고 다운로드한 조준선 이미지파일을 입혀준다.

화면의 전체 크기를 나타내는 초록색 사각형이 보일 텐데(위 사진의 사각형은 아님), 조준선 이미지의 포지션을 잘 조정해서 큰 사각형 정가운데에 올 수 있게끔 하자.

 

//FFPawn.cpp


AFFPawn::AFFPawn()
{
	static ConstructorHelpers::FClassFinder<UUserWidget>AimWidget(TEXT("/Game/Book/UI/UI_Aim.UI_Aim_C"));
	if (AimWidget.Succeeded())
	{
    	CrosshairWidgetClass = AimWidget.Class;
	}
}

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

	if (IsValid(CrosshairWidgetClass))
	{
    	CrosshairWidget = Cast<UUserWidget>(CreateWidget(GetWorld(), CrosshairWidgetClass));
   	 	if (IsValid(CrosshairWidget))
    	{
        	CrosshairWidget->AddToViewport();
    	}
	}
}

 

조준선 UI 구현이 완료됐으면, C++를 통해 화면에 띄워주면 총알 발사를 조준할 수 있는 조준선이 사용자의 화면 정 가운데에 나타나게 된다!

 

그럼 이제 어디서 총알이 발사될지는 정했는데 '어느 방향으로 총알이 발사돼야 할까?'에 대해서는 정해주지 않았다.

 

<4> - 1. 직선 방향 

원래는 단순히 생각했을 때 비행기가 바라보는 방향으로, 소켓에서부터 직선으로 총알을 발사하면 되겠지란 생각이었다.

하지만 그렇게 설계를 해보니 선 방향으로 총알이 나가지 않게 되는 현상이 발생했다.

 

이유는 너무 단순했다. 조준선은 카메라를 기준으로 하고, 직선 방향은 비행기를 기준으로 한다. 카메라는 비행기 뒤에 위치해 있어서 두 오브젝트의 위치가 같지 않기 때문에 총구가 향하는 지점과 카메라가 바라보는 지점이 차이가 날 수밖에 없다. 둘 중 올바른 방향인 조준선 방향으로 총알이 발사되게끔 수정해야 한다.

 

<4> - 2. Line Tracing 기법

그래서 대부분의 TPS(Third-Person Shooter) 게임에서 사용하고 있는 방식인 Line Tracing 기법이란 것을 발견해 적용시켜 보기로 했다.

Line-Tracing-기법
<Line Tracing 기법>

 

 

이는 카메라가 바라보고 있는 방향으로 직선을 길게 발사하여 충돌을 감지하고 충돌 객체의 정보를 반환하는 기법이다.

 

  1. 직선을 발사한다.
  2. 충돌 지점(Impact Point)이 발생한다.
  3. Impact Point의 Transform을 반환시킨다.
  4. 총알이 발사되는 소켓의 Transform과 Impact Point의 Transform, 두 개의 점을 이어주면, 총알이 발사될 방향을 구할 수 있다.

직선을 발사했는데 아무것도 충돌하지 않을 때는, 직선의 길이가 유효하기 때문에 직선의 끝 점과 소켓을 이어주면 됨.

 

추가적인 설정으로는, 마우스 좌클릭을 꾹 누르고 있으면 0.2초마다 총알이 발사되고, 총알이 충돌하지 않을 때 계속 소멸하지 않으면 게임에 과부하가 걸리기 때문에 총알의 수명은 3초로 설정한다.

 

위에 설명한 내용들을 C++로 구현하면 다음과 같다.

 

//AFFPawn.cpp

void AMyPawn::Fire()
{
    // 타이머를 설정하여 일정 시간 간격으로 ShootBullet 함수를 호출하도록 한다.
    GetWorld()->GetTimerManager().SetTimer(
        ShootingTimerHandle,        // 타이머 핸들: 타이머를 추적하고 조작하는 데 사용된다.
        this,                       // 타이머가 호출할 함수가 속한 객체 (현재 객체: 'this')
        &AFFPawn::ShootBullet,      // 타이머가 호출할 함수: ShootBullet
        0.2f,                       // 함수 호출 간격: 0.2초
        true                        // 반복 여부: true (반복 호출)
    );
}


void AFFPawn::ShootBullet()
{
    FHitResult OutHit_L, OutHit_R;
    ECollisionChannel TraceChannel = ECC_Visibility; 
    FCollisionQueryParams CollisionParams;  
    CollisionParams.AddIgnoredActor(this);  // 자신은 콜리전 반응이 일어나지 않게끔.

    bool bResult_L = GetWorld()->LineTraceSingleByChannel(
        OutHit_L,
        CameraLocation,
        CameraLocation + GetActorForwardVector() * 35000.0f,
        TraceChannel,
        CollisionParams
    );

    bool bResult_R = GetWorld()->LineTraceSingleByChannel(
        OutHit_R,
        CameraLocation,
        CameraLocation + GetActorForwardVector() * 35000.0f,
        TraceChannel,
        CollisionParams
    );

    if (bResult_L && bResult_R)
    {
        FVector ImpactPoint_L = OutHit_L.ImpactPoint;
        FVector ImpactPoint_R = OutHit_R.ImpactPoint;

        FRotator ShootRot_R = UKismetMathLibrary::FindLookAtRotation(ShootSocketLocation_R, ImpactPoint_R);
        FRotator ShootRot_L = UKismetMathLibrary::FindLookAtRotation(ShootSocketLocation_L, ImpactPoint_L);

        SpawnBullet(ShootSocketLocation_L, ShootRot_L);
        SpawnBullet(ShootSocketLocation_R, ShootRot_R);
    }
    else
    {
        FVector TraceEnd_L = OutHit_L.TraceEnd;
        FVector TraceEnd_R = OutHit_R.TraceEnd;

        FRotator ShootRot_R = UKismetMathLibrary::FindLookAtRotation(ShootSocketLocation_R, TraceEnd_R);
        FRotator ShootRot_L = UKismetMathLibrary::FindLookAtRotation(ShootSocketLocation_L, TraceEnd_L);

        SpawnBullet(ShootSocketLocation_L, ShootRot_L);
        SpawnBullet(ShootSocketLocation_R, ShootRot_R);
    }
}

void AFFPawn::SpawnBullet_Implementation(const FVector& Location, const FRotator& Rotation)
{
    AActor* SpawnedBullet = GetWorld()->SpawnActor<AActor>(BulletActorClass, Location, Rotation);
    if (SpawnedBullet)
    {
        SpawnedBullet->SetInstigator(this);

        FTimerHandle BulletTimerHandle;
        GetWorld()->GetTimerManager().SetTimer(BulletTimerHandle, [SpawnedBullet]()  //시간이 지나면 스폰된 총알을 소멸시킴
            {
                if (IsValid(SpawnedBullet))
                {
                    SpawnedBullet->Destroy();
                }
            }, 3.0f, false);
    }  
}
 
void AFFPawn::StopShooting()
{
    if (ShootingTimerHandle.IsValid())
    { 
        GetWorld()->GetTimerManager().ClearTimer(ShootingTimerHandle);  //타이머를 정지
        ShootingTimerHandle.Invalidate();                               //핸들을 무효화하여 타이머가 더 이상 실행되지 않도록
    }
}

 

 

※ 다음 챕터

 

[언리얼 개인 프로젝트-FlightFight/C++] #1. Pawn 설계 - (4) 추가 이펙트(Niagara System)

생동감을 위해 비행기에 이펙트를 추가한다.. 추가 이펙트 (Thruster Effect, Trail Effect)가속할 때 엔진에서 불이 나오는 Thruster Effect, 비행기가 지나갈 때 공기의 흐름을 보여주는 Trail Effect, 이렇게

lbto.tistory.com

반응형