프로그래밍 / C++ / 언리얼

Programming/C | C++ | Unreal

[언리얼 엔진] Actor 회전시키기 (사용자 입력, AI 회전구현)

아트성 2022. 2. 28. 21:10

언리얼 엔진에는 플레이어가 키보드나 마우스를 입력함에따라 액터를 회전시키게할수있는 여러 API들을 가지고있다.

이뿐만아니라 플레이어가 조종하는것 외에 액터가 특정대상을 바라볼수있게하는 AI기능을 갖추게 할 수도있다.

상속관계는 이렇다.

부모클래스인 BasePawn 아래에 자식클래스인 Tank, Tower를 만든다. (컴포넌트 안에 메쉬삽입하는 과정은 생략한다.)

RotateTurret 멤버함수는 탱크(아군)와 타워(적군) 모두 필요한 기능이므로 부모클래스에 정의를 한다.

매개변수타입은 FVector타입이며 (0.f, 0,f 0,f)의 좌표형식으로 받는다.

 

탱크는 플레이어가 마우스로 움직임에 따라 화면이 회전하도록 설정하고, 타워는 일정범위안에 탱크가 감지되면 탱크를 향하도록 회전하도록 하는 로직을 구성한다.

 

 

BasePawn.h

Protected:
	void RotateTurret(FVector LookAtTarget);

Private:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
		class UCapsuleComponent* CapsuleComp;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
		UStaticMeshComponent* BaseMesh;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
		UStaticMeshComponent* TurretMesh;

부모클래스가있는 헤더파일에 RotateTurret이라는 함수와, 벡터타입의 매개변수를 선언한다.

이 멤버함수는 추후에 자식클래스에도 쓰이게 된다.

 

 

BasePawn.cpp

#include "BasePawn.h"
#include "Components/CapsuleComponent.h"
#include "Components/StaticMeshComponent.h"
ABasePawn::ABasePawn()
{
	// Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	CapsuleComp = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule Collider"));
	RootComponent = CapsuleComp;

	BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Base Mesh"));
	BaseMesh->SetupAttachment(CapsuleComp);

	TurretMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Turret Mesh"));
	TurretMesh->SetupAttachment(BaseMesh);
}

// 포탑이 대상을 따라 움직이게 함
void ABasePawn::RotateTurret(FVector LookAtTarget) 
{
	FVector ToTarget = LookAtTarget - TurretMesh->GetComponentLocation();
	FRotator LookAtRotation = FRotator(0.f, ToTarget.Rotation().Yaw, 0.f);
	TurretMesh->SetWorldRotation(LookAtRotation);
}

 

 

- 주요함수 / 클래스

CreateDefaultSubobject<C>(TEXT("Name"))
생성자 내부에서 인스턴스(컴포넌트)를 생성한다. 

클래스 타입으로는 UCapsuleComponentUStaticMeshComponentUSkeletalMeshComponent등의 타입이 들어간다.

RootComponent에 상위클래스를 대입하고, 다른컴포넌트들을 추가하면 Components탭에 아래와 같이 표시된다. (CreateDefaultSubobject 함수는 반드시 생성자 내부에서 선언해 주어야한다.)




SetWorldRotation(FRotator x)
FRotator타입의 매개변수를 받으면 회전상태를 업데이트시킴.

GetComponentLocation()
컴포넌트의 현재위치를 Vector타입의 좌표로 반환

주로 선언하는 코드는 헤더파일에 작성하고, 구체적으로 구현하는 코드는 cpp파일에 작성한다.

컴포넌트 구성요소를 보면 TurretMesh와 BaseMesh로 분리되어있는데, 탱크 상부만 회전 시키기위해서 TurretMesh상의 축을기준으로 SetWorldRotation함수를 호출한다.

 

여기서 매개변수를 그대로 타겟좌표에 대입을하면 문제가 생기는데, 아래 그림을 확인하면 이해하기 쉽다.

예를들어 좌표값이 각각 Turret : (2,1,0) LookAtTarget : (1,4,0) 일때 두 좌표간의 뺄셈으로 만들어진 To Target값은 (-1,3,0)가 된다. 여기서 타겟좌표는 데카르트 좌표값이 아닌 Turret 기준으로 바뀐 벡터값을 나타낸것이다.

만약 매개변수인 LookAtTarget값을 그대로 Totarget에 대입을시키면 방향이 완전 틀어진값으로 게임상에 구현이 될것이다. 항상 물체를 바라보는쪽은 벡터의 뺄셈을 이용해야한다는것을 명심해야한다. 

 

그 다음에 타겟좌표는 Rotation()함수를 이용해 좌표값을 degree값으로 변환시키면 값에 대응되는 θ만큼 TurretMesh를 회전시킬수 있다. 이때 회전은 Yaw축을 기준으로 움직이는데, 아래 그림과 같이 Yaw는 Z축, Roll은 X축, Pitch는 Y축을 기준으로 움직인다.

 

 

이 게임에서는 Yaw값만 움직이는것을 목표로 하기때문에, Rotation().yaw를 통해 yaw값만 추출하도록 한다. LookAtTarget변수는 추후에 자식클래스의 tick함수에서 호출받을때 사용된다.

 

 

Tank.h

#include "BasePawn.h"
APlayerController* TankPlayerController; // private에 선언

플레이어가 키보드나 마우스를 사용해 액터를 움직이게 하기위해서 헤더파일에 선언한다.

 

Tank.cpp

#include "Tank.h"
// Called every frame
void ATank::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (TankPlayerController) // 표적 그리기 
	{
		FHitResult HitResult;
		TankPlayerController->GetHitResultUnderCursor(ECollisionChannel::ECC_Visibility,
			false, 
			HitResult);

		RotateTurret(HitResult.ImpactPoint);
		FVector HitPoint = HitResult.ImpactPoint;
		UE_LOG(LogTemp, Warning, TEXT("Hit Point : %s"), *HitPoint.ToString());
	}
}

 

- 주요함수 / 클래스

FHitResult
hit된 오브젝트의 정보를 담는다.

FHitResult.ImpactPoint
hit된 오브젝트의 좌표를 반환한다.

APlayerController
플레이어가 폰에대해 제어가 가능한 클라이언트 시스템이 들어있는 클래스. 이 시스템(멤버함수)들은 인스턴스를 생성해서 사용할 수있다.

GetHitResultUnderCursor(type, bool, FHitresult)
PlayerController의 멤버함수마우스 커서 위치를 직선형태로 쏴서 그 결과를 가져온다.

헤더파일에서 선언된 APlayerController 의 인스턴스(TankPlayerController)가 존재하면 실시간으로 포탑이 움직일 수 있도록 설정한다. 마우스 커서가 가리키는곳을 추적해서 HitResult값을 벡터값으로 반환시킨다. 벡터값은 RotateTurret의 매개변수로 들어가게되고, RotateTurret내에 SetWorldRotation의 이벤트가 발생해 TurretMesh가 회전할 수 있게된다.

또한 변환된 좌표값은 UE_LOG 매크로를 통해 확인할 수있다.

 

컴파일을 완료하고 게임을 실행 시켜보면 마우스커서에 따라 물체가 회전하는것을 확인할 수 있고, 가리키는 좌표들이 OutputLog탭을 통해 실시간을 확인되는것을 알수있다.

 

Tower.h

#include "BasePawn.h"
private:
	class ATank* Tank;

	UPROPERTY(EditDefaultsOnly, Category = "Combat")
	float FireRange = 300.f;

마우스를 이용해 탱크를 움직였다면, AI가 플레이어를 향하도록 구현가능하다.

FireRange를 통해 플롯타입으로 범위를 설정해주고, UPROPERTY매크로를 활용해 에디터상에도 쉽게 설정할수 있게 나타낸다. 탱크의 헤더파일은 cpp파일에 선언할 예정이므로 Class ATank* Tank를 미리 선언한다.

 

Tower.cpp

#include "Tower.h"
#include "Tank.h"
#include "Kismet/GameplayStatics.h"
void ATower::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// 범위 안이면 탱크쪽을 향하게한다.
	if (InFireRange())
	{
		RotateTurret(Tank->GetActorLocation());
	}
}

void ATower::BeginPlay()
{
	Super::BeginPlay();
	// 탱크까지의 거리를 찾기
	Tank = Cast<ATank>(UGameplayStatics::GetPlayerPawn(this, 0)); 
}


// 아군이 포탑 일정범위에 접근하면 true를 반납, 범위밖이면 false를 반납
bool ATower::InFireRange()
{
	if (Tank)
	{
		float Distance = FVector::Dist(GetActorLocation(), Tank->GetActorLocation()); 
		if (Distance <= FireRange)
		{
			return true;
		}
	}
	return false; 
}

 

- 주요함수 / 클래스

UGameplayStatics
게임 플레이에 필요한 요소들을 여러가지 담고있는 잡동사니같은 클래스
 ("Kismet/GameplayStatics.h" 선언필수)

GetPlayerPawn(this, 0)
지정된 플레이어의 폰을 가져온다. (0이면 첫번재플레이어, 1이면 두번째 플레이어.... )

GetActorLocation()
RootComponet에 등록된 액터의 현재 위치 반환.

Dist(point1, point2)
폰과 폰 사이의 거리를 벡터형태로 반환.

탱크가 포탑주위를 맴돌면서 범위안으로 들어오게되면 Tower가 탱크를 향하도록 한다. 

 

먼저 로직을 짜기전에 탱크의 인스턴스를 Tower.cpp로 불러야되는데, Tank = UGameplayStatics::GetPlayerPawn(this, 0); 선언해서 불러오려니 컴파일 오류가 났다. 알고보니 pawn클래스의 자식클래스인 ATank클래스로 인스턴스를 초기화 할수없었던 것이였다. 엔진내에는 이를 방지하고자 Cast라는 함수로 쉽게 타입변환 할수있는 방법이 있다.


Tank = Cast(UGameplayStatics::GetPlayerPawn(this, 0));

위와같이 캐스팅 선언해주어야지 ATank인스턴스에 할당과 동시에 포인터로 사용이 가능해진다.

 

이후에 bool값을 반환한는 InFireRange()함수를 선언한다. 함수 내부에는 Dist함수가 타워와 탱크사이의 거리를 반환해 fireRange값보다 작으면 true를 반환한다. 범위 밖에있으면 flase를 반환한다. tick에서 bool값에 따라 RotateTurret함수의 이벤트가 작동하게 된다.

 

컴파일을 진행하면 아래와 같이 탱크가 움직임에 따라 타워가 회전하게 된다.

 

 

 

특정 이벤트를 등록하고 컴파일은 진행되었는데, 액터의 움직임이 구현되지 않거나 런타임 오류가 나온다면, 엔진 에디터상에서 클래스를 최신화가 안되어있을수있다. 이럴때는 클래스를 재정의 해줄 필요가 있다.

 

ClassSettings - ClassOptions - ParentClass로 들어가서 각각 업데이트한 클래스(Tank, Tower)로 바꿔주면 해결된다.

 

위 예제와 비슷하게 AI가 bool값에따라 조건문 내부에서 이벤트를 발생시켜서, 플레이어를 감지해서 조준하고 발사 이벤트까지 구현해서 게임내에서 실제 전투모션을 구현할수도 있다. 이런 로직들은 3인칭 FPS 슈팅게임에도 유용하게 쓰일 수있다.

반응형