필요한 개념
Warp(워프)
커서 출력, 클릭한 지점 플레이어 몽타주 실행하면서 이동
Attachment는 화면에 보여줄 걸 담당
* BP_CAttachment_Warp를 생성
커서를 보여 줄 수 있도록 Decal을 추가
Tip) 부모함수 추가 단축키 설정
에디터 개인 설정 -> 디테일 검색에 부모라고 치면 -> 부모 함수로의 호출 추가가 뜬다.
거기서 단축키 세팅하면 된다.
Ctrl + Alt + Num0로 세팅
* BP 이벤트 그래프에서 Decal을 BeginPlay할때 꺼주고, OnEquip()할때 켜주고, OnUnequip()할때 꺼줌
단, 부모함수들 호출해서 세팅하자
* Wizard_Warp_Montage 세팅
WizardSpell 애니메이션 클릭해서 끝시간 조절 가능(길면 짜를 수 있음)
- FullBody로 세팅
- BeginAction과 EndAction만 세팅함
Tip) 노티파이 우클릭해서 노티파이 프레임 설정으로 프레임 위치 조절 가능
* DA_Warp를 생성해서 세팅
단, PawnControl(정면 바라보게)는 끄면 됨(어느 방향이든 뛰어도 상관없으니까)
DoActionData에 CanMove를 해제해서 애니메이션 상황에서 움직이지 못하도록한다.
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);
}
// 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);
}
}
}
결과
'Unreal Engine 4 > C++' 카테고리의 다른 글
<Unreal C++> 61 - Action RPG (Dead) (0) | 2022.05.30 |
---|---|
<Unreal C++> 58 - Action RPG (Targeting) (0) | 2022.05.16 |
<Unreal C++> 55 - Action RPG (Fist Mode) (0) | 2022.05.16 |
<Unreal C++> 54 - Action RPG (TwoHand Mode) (0) | 2022.05.16 |
<Unreal C++> 53 - Action RPG(Juice(타격감)) (0) | 2022.05.16 |