본문 바로가기

Unreal Engine 4/C++

<Unreal C++> 56 - Action RPG (Warp Mode)

 

필요한 개념


Warp(워프)

 

커서 출력, 클릭한 지점 플레이어 몽타주 실행하면서 이동

Attachment는 화면에 보여줄 걸 담당

 

* BP_CAttachment_Warp를 생성

 

커서를 보여 줄 수 있도록 Decal을 추가

 

*&nbsp;BP_CAttachment_Warp



Tip) 부모함수 추가 단축키 설정
에디터 개인 설정 -> 디테일 검색에 부모라고 치면 -> 부모 함수로의 호출 추가가 뜬다.

 

 

거기서 단축키 세팅하면 된다.
Ctrl + Alt + Num0로 세팅

 

 

* BP 이벤트 그래프에서 Decal을 BeginPlay할때 꺼주고, OnEquip()할때 켜주고, OnUnequip()할때 꺼줌
단, 부모함수들 호출해서 세팅하자

* BP_CAttachment_Warp EventGraph

 


* Wizard_Warp_Montage 세팅

 

WizardSpell 애니메이션 클릭해서 끝시간 조절 가능(길면 짜를 수 있음)

 

 


- FullBody로 세팅
- BeginAction과 EndAction만 세팅함

 

 



Tip) 노티파이 우클릭해서 노티파이 프레임 설정으로 프레임 위치 조절 가능



* DA_Warp를 생성해서 세팅
단, PawnControl(정면 바라보게)는 끄면 됨(어느 방향이든 뛰어도 상관없으니까)
DoActionData에 CanMove를 해제해서 애니메이션 상황에서 움직이지 못하도록한다.

 

* DA_Warp



Magician(매지션)은 마법대로 액션을 계속 만들어감, Melee는 세분화 할필요 없어서 안함

* (CDoAction을 상속 받는) CDoAction_Warp class 생성
Decal의 위치를 Action의 Tick에서 움직일 예정임(즉, CDoAction_Warp에서 Attachment에 있는 Decal을 소유함)
-> 우리가 만든 클래스는 소유하는 것은 별로 좋지 않지만, 언리얼에서 만든 클래스는 바뀔 가능성이 없어서 소유해도 됨


private:
	class UDecalComponent* Decal;





- 장비가 되었다면 커서를 옮겨줄 것이다.
Equipment가 Attachment가 되었는지를 CDoAction_Warp 클래스에서 알 방법이 없다.
-> 그래서 CEquipment에서 장착되었는지 변수를 설정해 놓는다.

// CEquipment.h
// 주소로 리턴해서, bEquipped 값이 바뀌면 실시간적으로 사용하도록 처리해줌, 또한 const여서 외부에서 수정도 불가
// 즉, 상대방 객체에서 이 변수에 대한 주소값을 소유해도 상관없음
FORCEINLINE const bool* GetEquipped() { return &bEquipped; }
// 장착이 완료되었는지
bool bEquipped;

 

// CEquipment.cpp
// 장착이 완료되었다면
void ACEquipment::End_Equip_Implementation()
{
	bEquipped = true;

	State->SetIdleMode();
}

 

// CDoAction.h
FORCEINLINE void SetEquipped(const bool* InEquipped) { bEquipped = InEquipped; }

protected:
	const bool* bEquipped;




// 장착 해제했다면
void ACEquipment::Unequip_Implementation()
{
	bEquipped = false;

	if (OnUnequipmentDelegate.IsBound())
		OnUnequipmentDelegate.Broadcast();

	// 따로 호출할 필요가 없음
	OwnerCharacter->bUseControllerRotationYaw = false;
	OwnerCharacter->GetCharacterMovement()->bOrientRotationToMovement = true;
}



* State의 EquipMode는 사용하면 안됨, 장비가 장착 중이라는 것만 알려줄 뿐 장비가 완료되었는지는 판단이 안됨

* CActionData.cpp에 DoActionData 세팅할때 세팅해줌

if (!!Equipment)
{
	DoAction->SetEquipped(Equipment->GetEquipped());
}




<12강 Warp - 2>

* 액션매핑에 Warp키 추가

* CActionComponent에 Warp 추가

UENUM(BlueprintType)
enum class EActionType: uint8
{
	Unarmed, Fist, OneHand, TwoHand, Warp, Max,
};




* CPlayer에서 입력매핑 추가

// CPlayer.cpp
PlayerInputComponent->BindAction("Warp", EInputEvent::IE_Pressed, this, &ACPlayer::OnWarp);

 

void ACPlayer::OnWarp()
{
	CheckFalse(State->IsIdleMode());

	Action->SetWarpMode();
}





* BP_CPlayer에 ActionComponent에 DA_Warp 세팅



Tip)
4.25에서는 컴포넌트나 액터 수정하니까 아무의미 없이 터질 수 있음

<커서를 마우스 위치에 따라 보이도록 만들기>

* CDoAction_Warp에 마우스와 WorldStatic이 충돌하는 위치를 구해서 Tick에 업데이트 시키고 충돌되었다면 Decal을 보여줌

-> 우리는 월드의 지면을 선택해서 이동할 것 (지면은 WorldStatic)

 

 

void ACDoAction_Warp::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	// 장착된 상태에서만 진행
	CheckFalse(*bEquipped);

	FVector location;
	FRotator rotator;

	if (GetCursorLocationAndRotation(location, rotator))
	{
		Decal->SetVisibility(true);
		// 커서가 리턴해 주는 위치에 세팅
		Decal->SetWorldLocation(location);
		Decal->SetWorldRotation(rotator);

		return;
	}

	// 충돌하는 위치가 없다면 보이지 않도록 처리
	Decal->SetVisibility(false);
}

* 프로젝트 세팅-> 콜리전 -> NoCollision -> Preset

// BP와 유사해서 비교해서 보면 좋다
// 커서의 위치값과 회전값을 받아옴
bool ACDoAction_Warp::GetCursorLocationAndRotation(FVector& OutLocation, FRotator& OutRotator)
{
	APlayerController* controller = UGameplayStatics::GetPlayerController(GetWorld(), 0);


	TArray<TEnumAsByte<EObjectTypeQuery>> objects;
	// 프로젝트 세팅 -> 콜리전 -> 아무거나 preset 들어가 보면
	// 오브젝트 유형에 밑으로 쭉 1, 2, 3, 4, 5이다.
	// 프로젝트 세팅 콜리전에 정의되어 있는 오브젝트 유형의 순서가 우리가
	// 사용할 번호가 된다.
	// 7번 부터는 프로젝트 세팅 -> 콜리전 -> ObejctChannels에 등록해서 사용하면 된다.
	// WorldStatic만 가져다 사용할 거라서, 1번이다.(맵의 바닥이 Worldstatic이어서)
	// 즉 그 안에서만 커서를 출력할려고
	objects.Add(EObjectTypeQuery::ObjectTypeQuery1);


	// BP에서는 GetHitResultUnderCursor()를 사용했지만,
	// cpp에서는 타입만 찾아낼 거라 얘 사용
	// 인자로 TEnumAsByte가 있다.
	// TEnumAsByte : enum class는 자료형의 크기가 지정되어있지만
	// 순수한 enum을 사용할 경우 자료형의 크기가 결정되지 않았으므로
	// 이 템플릿을 이용해 enum의 크기를 정의해 줌
	// 정해주지 않으면 BP에서는 enum의 자료형은 알지만, 크기를 모름
	// -> 즉 TEnumAsByte를 넣어서 명시해줘서 바이트 형태로 사용할 것임을 알림
	// uproperty도 마찬가지
	// 1 : Query 유형 2 : 복잡하게 검사할건지(삼각형 단위 처리(느림)), 3 : 결과
	// 하나라도 충돌 된 것이 있다면 true가 나옴
	FHitResult hitResult;
	if (controller->GetHitResultUnderCursorForObjects(objects, false, hitResult))
	{
		OutLocation = hitResult.Location;
		// ImpactNormal은 충돌된 면의 수직 백터를 리턴함
		// 해당 수직 벡터로 회전 하게 되면 항상 정상적으로 잘 나타남
		// 이전의 FireBall의 터지는 면을 이것을 통해 회전 처리하게 되면 좀 더 자연스러움
		OutRotator = hitResult.ImpactNormal.Rotation();

		return true;
	}
	return false;
}




* Attachment, Equipment라고만 나옴, 어느무기에 대한 것인지 안나와서 수정

// CActionData 

private:
	// 이름 출력해 줄 애
	FString GetLableName(class ACharacter* InOwnerCharacter, FString InName);

FString UCActionData::GetLableName(class ACharacter* InOwnerCharacter, FString InName)
{
	FString str;
	str.Append(InOwnerCharacter->GetActorLabel());
	str.Append("_");
	str.Append(InName);
	str.Append("_");
	// DataAsset은 DA로 world에 나타나는데 DA_를 없앰
	str.Append(GetName().Replace(L"DA_", L""));

	return str;

	ex) BP_CPlayer_Attachment_Warp 이런식으로 나옴
}



이런식으로 사용가능(이런식으로 하면 ActionData의 이름이 출력되니까 구분지어서 누구의 Attachment, Equipmemt, DoAction인지 알 수 있다.)

ex) Attachment->SetActorLabel(GetLableName(InOwnerCharacter, "Attachment"));

 

 

 

// CDoAction.cpp

void ACDoAction_Warp::DoAction()
{
	// 장비가 장착되어있는 상태일때만 실행
	CheckFalse(*bEquipped);

	FRotator rotator;
	CheckFalse(GetCursorLocationAndRotation(Location, rotator));

	// Idle 모드 일때만 실행
	CheckFalse(State->IsIdleMode());
	State->SetActionMode();

	// 이미 Tick에서 해주고 있어서, 딱히 의미는 없다.
	Decal->SetWorldLocation(Location);
	Decal->SetWorldRotation(rotator);

	OwnerCharacter->PlayAnimMontage(Datas[0].AnimMontage, Datas[0].PlayRatio, Datas[0].StartSection);

	Datas[0].bCanMove ? Status->SetMove() : Status->SetStop();
}



void ACDoAction_Warp::Begin_DoAction()
{
	// 이펙트 지정한 거 출력함
	FTransform transform = Datas[0].EffectTransform;
	// 파티클 위치 조정
	// transform.AddToTranslation(OwnerCharacter->GetActorLocation());
	// UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), Datas[0].Effect, transform);
	// Location이 아닌 Attach를 이용해 메시에 붙여서 플레이 되도록 처리해주면
	// 파티클이 메시에 붙어있는 상태이므로 같이 이동하게 된다.
	// 1 : 파티클 시스템 2 : 붙일 Mesh 3 : 소켓이름, 값 안넣으면("") 하면 root에 붙음 4 : 오프셋 위치 5 : 오프셋 회전값 6 : 오프셋 크기
	UGameplayStatics::SpawnEmitterAttached(Datas[0].Effect, OwnerCharacter->GetMesh(), "", transform.GetLocation(), FRotator(transform.GetRotation()), transform.GetScale3D());
}



void ACDoAction_Warp::End_DoAction()
{
	// 우리가 저장했던 옮기려던 위치로 캐릭터가 이동
	OwnerCharacter->SetActorLocation(Location);
	Location = FVector::ZeroVector;

	State->SetIdleMode();
	Status->SetMove();
}



결과를 보면 버그가 있음) 벽에다가 커서를 움직여서 이동하면 위에서 떨어지면서 굴곡진 바닥에 박혀버림
-> BlockingVolume(블록킹 볼륨)으로 못가도록 막음
BlockingVolume : 액터가 해당 구역을 넘어서지 않도록 해주는 볼륨
ex) 벽을 선택해도 Blocing Volume으로 그 해당 구역 선까지 밀려나옴

BlockingVolume(블록킹 볼륨) 추가해서 해당 벽 구역을 감싼다.
* 벽구역 총 4군대를 각 블록킹 볼륨으로 감싸면 됨



벽가까이 가면 블록킹 볼륨으로 인해 미끄러지면서 캐릭터가 내려오는데 그것은 슬라이딩 백터이다.

* 면접 문제 
슬라이딩 백터, 반사 백터 무조건 나옴(공부하기)
즉, 수식만 알고 있으면 됨

 

 

 

CPlayer.h 수정된 내용


더보기
...

UCLASS()
class UONLINE_04_ACTION_API ACPlayer : public ACharacter, public IICharacter
{
	GENERATED_BODY()
    ...
    
private:
	..
	void OnWarp();
};

 

 

 

 

 

 

 

CPlayer.cpp 수정된 내용


더보기
...

void ACPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	...
	PlayerInputComponent->BindAction("Warp", EInputEvent::IE_Pressed, this, &ACPlayer::OnWarp);
}


void ACPlayer::OnWarp()
{
	CheckFalse(State->IsIdleMode());

	Action->SetWarpMode();
}

 

 

CActionComponent.h 추가된 내용


더보기
...

UENUM(BlueprintType)
enum class EActionType: uint8
{
	Unarmed, Fist, OneHand, TwoHand, Warp, Max,
};

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class UONLINE_04_ACTION_API UCActionComponent : public UActorComponent
{
	GENERATED_BODY()
	...
    
public:
	...
	UFUNCTION(BlueprintPure)
		FORCEINLINE bool IsWarpMode() { return Type == EActionType::Warp; }

public:
	...
	void SetWarpMode();
};

 

 

 

 

 

 

 

CActionComponent.cpp 추가된 내용


더보기
...

void UCActionComponent::SetWarpMode()
{
	SetMode(EActionType::Warp);
}

 

 

CDoAction_Warp.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "Actions/CDoAction.h"
#include "CDoAction_Warp.generated.h"

UCLASS()
class UONLINE_04_ACTION_API ACDoAction_Warp : public ACDoAction
{
	GENERATED_BODY()

protected:
	virtual void BeginPlay() override;

public:
	virtual void DoAction() override;
	virtual void Begin_DoAction() override;
	virtual void End_DoAction() override;


	virtual void Tick(float DeltaTime) override;

private:
	// 커서의 위치값과 회전값을 받아옴
	bool GetCursorLocationAndRotation(FVector& OutLocation, FRotator& OutRotator);
	
private:
	class UDecalComponent* Decal;
	// 이동할 위치 기록
	FVector Location;
};

 

 

 

 

 

 

 

CDoAction_Warp.cpp


더보기
#include "CDoAction_Warp.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "Actions/CAttachment.h"
#include "Components/DecalComponent.h"
#include "Components/CStateComponent.h"
#include "Components/CStatusComponent.h"

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

	// 캐릭터가 가지고 있는 Owner들을 다 리턴받음(플레이시에 월드에 플레이어가 가진 애들)
	for (AActor* actor : OwnerCharacter->Children)
	{
		// IsA : 상속 받는지(맞으면 true가 나옴)
		// 여기서는 즉, actor가 ACAttachment로 다운 캐스팅 될 수 있는애인지
		// 레이블에 Wrap라는 단어가 포함되어 있는지
		// 즉, 두가지가 만족하면 CAttachment_Warp이다.
		if (actor->IsA<ACAttachment>() && actor->GetActorLabel().Contains("Warp"))
		{
			// 액터에서 데칼컴포넌트를 찾는다.
			Decal = CHelpers::GetComponent<UDecalComponent>(actor);

			break;
		}
	}
}

void ACDoAction_Warp::DoAction()
{
	// 장비가 장착되어있는 상태일때만 실행
	CheckFalse(*bEquipped);

	FRotator rotator;
	CheckFalse(GetCursorLocationAndRotation(Location, rotator));

	// Idle 모드 일때만 실행
	CheckFalse(State->IsIdleMode());
	State->SetActionMode();

	// 이미 Tick에서 해주고 있어서, 딱히 의미는 없다.
	Decal->SetWorldLocation(Location);
	Decal->SetWorldRotation(rotator);

	OwnerCharacter->PlayAnimMontage(Datas[0].AnimMontage, Datas[0].PlayRatio, Datas[0].StartSection);

	Datas[0].bCanMove ? Status->SetMove() : Status->SetStop();
}

void ACDoAction_Warp::Begin_DoAction()
{
	// 이펙트 지정한 거 출력함
	FTransform transform = Datas[0].EffectTransform;
	// 파티클 위치 조정
	// transform.AddToTranslation(OwnerCharacter->GetActorLocation());
	// UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), Datas[0].Effect, transform);
	// Location이 아닌 Attach를 이용해 메시에 붙여서 플레이 되도록 처리해주면
	// 파티클이 메시에 붙어있는 상태이므로 같이 이동하게 된다.
	// 1 : 파티클 시스템 2 : 붙일 Mesh 3 : 소켓이름, 값 안넣으면("") 하면 root에 붙음 4 : 오프셋 위치 5 : 오프셋 회전값 6 : 오프셋 크기
	UGameplayStatics::SpawnEmitterAttached(Datas[0].Effect, OwnerCharacter->GetMesh(), "", transform.GetLocation(), FRotator(transform.GetRotation()), transform.GetScale3D());
}

void ACDoAction_Warp::End_DoAction()
{
	// 우리가 저장했던 옮기려던 위치로 캐릭터가 이동
	OwnerCharacter->SetActorLocation(Location);
	Location = FVector::ZeroVector;

	State->SetIdleMode();
	Status->SetMove();
}

void ACDoAction_Warp::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	// 장착된 상태에서만 진행
	CheckFalse(*bEquipped);

	FVector location;
	FRotator rotator;

	if (GetCursorLocationAndRotation(location, rotator))
	{
		Decal->SetVisibility(true);
		// 커서가 리턴해 주는 위치에 세팅
		Decal->SetWorldLocation(location);
		Decal->SetWorldRotation(rotator);

		return;
	}

	// 충돌하는 위치가 없다면 보이지 않도록 처리
	Decal->SetVisibility(false);


}

bool ACDoAction_Warp::GetCursorLocationAndRotation(FVector& OutLocation, FRotator& OutRotator)
{
	APlayerController* controller = UGameplayStatics::GetPlayerController(GetWorld(), 0);


	TArray<TEnumAsByte<EObjectTypeQuery>> objects;
	// 프로젝트 세팅 -> 콜리전 -> 아무거나 preset 들어가 보면
	// 오브젝트 유형에 밑으로 쭉 1, 2, 3, 4, 5이다.
	// 프로젝트 세팅 콜리전에 정의되어 있는 오브젝트 유형의 순서가 우리가
	// 사용할 번호가 된다.
	// 7번 부터는 프로젝트 세팅 -> 콜리전 -> ObejctChannels에 등록해서 사용하면 된다.
	// WorldStatic만 가져다 사용할 거라서, 1번이다.(맵의 바닥이 Worldstatic이어서)
	// 즉 그 안에서만 커서를 출력할려고
	objects.Add(EObjectTypeQuery::ObjectTypeQuery1);


	// BP에서는 GetHitResultUnderCursor()를 사용했지만,
	// cpp에서는 타입만 찾아낼 거라 얘 사용
	// 인자로 TEnumAsByte가 있다.
	// TEnumAsByte : enum class는 자료형의 크기가 지정되어있지만
	// 순수한 enum을 사용할 경우 자료형의 크기가 결정되지 않았으므로
	// 이 템플릿을 이용해 enum의 크기를 정의해 줌
	// 정해주지 않으면 BP에서는 enum의 자료형은 알지만, 크기를 모름
	// -> 즉 TEnumAsByte를 넣어서 명시해줘서 바이트 형태로 사용할 것임을 알림
	// uproperty도 마찬가지
	// 1 : Query 유형 2 : 복잡하게 검사할건지(삼각형 단위 처리(느림)), 3 : 결과
	// 하나라도 충돌 된 것이 있다면 true가 나옴
	FHitResult hitResult;
	if (controller->GetHitResultUnderCursorForObjects(objects, false, hitResult))
	{
		OutLocation = hitResult.Location;
		// ImpactNormal은 충돌된 면의 수직 백터를 리턴함
		// 해당 수직 벡터로 회전 하게 되면 항상 정상적으로 잘 나타남
		// 이전의 FireBall의 터지는 면을 이것을 통해 회전 처리하게 되면 좀 더 자연스러움
		OutRotator = hitResult.ImpactNormal.Rotation();

		return true;
	}
	return false;
}

 

 

CEquipment.h 수정된 내용


더보기
...

UCLASS()
class UONLINE_04_ACTION_API ACEquipment : public AActor
{
	GENERATED_BODY()
	
public:
	// CActionData로 부터 값을 세팅함
	FORCEINLINE void SetData(FEquipmentData InData) { Data = InData; }
	FORCEINLINE void SetColor(FLinearColor InColor) { Color = InColor; }
	FORCEINLINE const bool* GetEquipped() { return &bEquipped; }

private:
	// 장착이 완료되었는지
	bool bEquipped;
};

 

 

 

 

 

 

 

CEquipment.cpp 수정된 내용


더보기
...

void ACEquipment::End_Equip_Implementation()
{
	bEquipped = true;

	State->SetIdleMode();
}

void ACEquipment::Unequip_Implementation()
{
	bEquipped = false;

	if (OnUnequipmentDelegate.IsBound())
		OnUnequipmentDelegate.Broadcast();

	// 따로 호출할 필요가 없음
	OwnerCharacter->bUseControllerRotationYaw = false;
	OwnerCharacter->GetCharacterMovement()->bOrientRotationToMovement = true;

}

 

 

 

CActionData.cpp 수정된 내용


더보기
...

void UCActionData::BeginPlay(class ACharacter* InOwnerCharacter)
{
	...

	if(!!DoActionClass)
	{
		DoAction = InOwnerCharacter->GetWorld()->SpawnActorDeferred<ACDoAction>(DoActionClass, transform, InOwnerCharacter);
		DoAction->AttachToComponent(InOwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true));
		DoAction->SetActorLabel(GetLableName(InOwnerCharacter, "DoAction"));
		DoAction->SetDatas(DoActionDatas);
		UGameplayStatics::FinishSpawningActor(DoAction, transform);

		if (!!Equipment)
		{
			// 포인터 넘겨줘서 계속 상태를 체크할 수 있도록, 최초 한번만 넘겨서 처리
			DoAction->SetEquipped(Equipment->GetEquipped());
		}

		if (!!Attachment)
		{
			// 충돌 함수 연결해줌
			Attachment->OnAttachmentBeginOverlap.AddDynamic(DoAction, &ACDoAction::OnAttachmentBeginOverlap);
			Attachment->OnAttachmentEndOverlap.AddDynamic(DoAction, &ACDoAction::OnAttachmentEndOverlap);

			Attachment->OnAttachmentCollision.AddDynamic(DoAction, &ACDoAction::OnAttahmentCollision);
			Attachment->OffAttachmentCollision.AddDynamic(DoAction, &ACDoAction::OffAttahmentCollision);
		}
	}
}

 

 

 

 

결과