-
언리얼 프로젝트_Parkour 구현Unreal 2025. 6. 2. 07:07
설계 과정
1. 어떤 파쿠르를 할건지 화살표로 구분
Player
1. 화살표는 플레이어가 가지고 있음
UPROPERTY(VisibleAnywhere) class USceneComponent* ArrowGroup; UPROPERTY(VisibleAnywhere) class UArrowComponent* Arrows[(int32)EParkourArrowType::Max];
2. enum이 전역(UENUM(BlueprintType)) 으로 정의돼 있으면 어디서든 사용 가능 하도록 하였음
UENUM(BlueprintType) enum class EParkourArrowType : uint8 { Center = 0,Head,Foot,Left,Right,Land,Max, }; UENUM(BlueprintType) enum class EParkourType : uint8 { Climb = 0,Fall,Slide,Short,Normal,Wall,Max };
3. 화살표 색상과 위치 지정
- 파쿠르 방향 정보를 EParkourArrowType Enum으로 관리하고,반복문에서 StaticEnum()->GetNameStringByIndex()를 이용해 각 방향별 ArrowComponent를 생성했습니다.
- SetRelativeLocation()은 생성한 화살표 컴포넌트들을 부모인 ArrowGroup 기준으로 위치 배치하기 위해 사용
for (int32 i = 0; i < (int32)EParkourArrowType::Max; i++) { FString name = StaticEnum<EParkourArrowType>()->GetNameStringByIndex(i); CHelpers::CreateComponent<UArrowComponent>(this,&Arrows[i],FName(name),ArrowGroup); switch (EParkourArrowType(i)) { case EParkourArrowType::Center: Arrows[i]->ArrowColor = FColor::Red; break; case EParkourArrowType::Head: Arrows[i]->ArrowColor = FColor::Green; Arrows[i]->SetRelativeLocation(FVector(0,0,100)); break; .... }
4. 플레이어 인풋 매핑
PlayerInputComponent->BindAction("SubAction",EInputEvent::IE_Pressed,this,&ACPlayer::OnSubAction); PlayerInputComponent->BindAction("SubAction",EInputEvent::IE_Released,this,&ACPlayer::OffSubAction);
void ACPlayer::OnSubAction() { if (bEquipped == false) { Parkour->DoParkour(); return; } }
- 무기를 장착하지 않았을 경우만 파쿠르 가능
5. 파쿠르 실행 전 타입 구분
void UCParkourComponent::DoParkour(bool bLanded) { //파쿠르 수행중인지 체크 CheckFalse(Type == EParkourType::Max); if (bLanded && Check_FallMode()) { DoParkour_Fall(); return; } ... CheckTrace_Center(); if (!!HitObstacle) { //머리 쪽에 닿는게 있는지 체크 CheckTrace_Head(); CheckTrace_Foot(); CheckTrace_Side(); } CheckFalse(Check_Obstacle()); //기어갈 수 있는지 체크 if (Check_ClimbMode()) { DoParkour_Climb(); return; } ... }
5. 파쿠르 실행 조건 확인
- 플레이어에게 여러 화살표두고 선을 쏜 뒤 파쿠르를 진행할 오브젝트와 닿는지 확인
- 파쿠르 가능한지 판단하기 위해 바운딩 박스를 구함
Origin 박스의 중심 좌표 박스의 중간점 (중심점) BoxExtent 박스 중심에서의 반지름 (XYZ 절반) 가로/세로/높이의 절반 GetBounds() 컴포넌트의 바운드 정보 RootComponent 기준 void UCParkourComponent::CheckTrace_Center() { EParkourArrowType type = EParkourArrowType::Center; LineTrace(type); const FHitResult& hitResult = HitResults[(int32)type]; CheckFalse(hitResult.bBlockingHit); //파쿠르 블럭 반응에 막힌애만 파쿠르 실행 UStaticMeshComponent* mesh = CHelpers::GetComponent<UStaticMeshComponent>(hitResult.GetActor()); CheckNull(mesh); HitObstacle = hitResult.GetActor(); FVector minBound,maxBound; mesh->GetLocalBounds(minBound,maxBound); float x = FMath::Abs(minBound.X - maxBound.X); float y = FMath::Abs(minBound.Y - maxBound.Y); float z = FMath::Abs(minBound.Z - maxBound.Z); HitObstacleExtent = FVector(x,y,z); HitDistance = hitResult.Distance; //충돌 거리 }
6. ImpactNormal + MakeRotFromX
//플레이어가 들어가는 방향의 회전값 //impactNormal의 안쪽방향 ToFrontYaw = UKismetMathLibrary::MakeRotFromX(-hitResult.ImpactNormal).Yaw;
- 캐릭터가 벽을 정면으로 바라보도록 회전값 계산
- 충돌한 벽의 수직 벡터(ImpactNormal) 방향을 기준으로 반대 방향을 바라보는 회전 값(Yaw)을 구함
7. ImpactPoint
- 월드 좌표계 기준 라인 트레이스(또는 충돌)가 발생한 정확한 "접촉 지점"
- 플레이어가 벽에 닿은 그 "점"
8. 데이터 테이블을 통해 데이터 불러오기
bool UCParkourComponent::Check_ClimbMode() { //머리가 닿지 않으면 false 리턴 CheckFalseResult(HitResults[(int32)EParkourArrowType::Head].bBlockingHit,false); //데이터 불러오기 DataMap[EParkourType::Climb]; const TArray<FParkrourData>* datas = DataMap.Find(EParkourType::Climb); //(*datas)[0]data가 객체가 되어서 배열에 넘어오고 걔의 0번째 //(*datas)[0].MinDistance //히트된 거리의 최소거리보다 작으면 하면 안됨 CheckFalseResult((*datas)[0].MinDistance < HitDistance,false); CheckFalseResult((*datas)[0].MaxDistance > HitDistance,false); // //주언진 오차값 이하면 같은값으로 간주 // //HitObstacleExtent.Z이 10차이보다 크면 false CheckFalseResult(FMath::IsNearlyEqual((*datas)[0].Extent,HitObstacleExtent.Z,10),false); return true; }
9. 올라가는 파쿠르 구현
- 플레이어가 벽을 마주보고 오르기 위해 플레이어의 회전값을 벽이 바라보는 방향의 반대 방향의 회전값으로 세팅
- ImpactPoint를 통해 캐릭터를 해당 지점으로 이동 시키도록 함
- 미리 받아놓은 데이터 맵에서 파쿠르 동작과 맞는 열거형 값에 따라 몽타주를 재생
- 중력을 없애기 위해 MOVE_Flying 모드로 설정
10. 파쿠르 애니메이션 노티파이
void UCParkourComponent::DoParkour_Climb() { Type = EParkourType::Climb; //센터 가운데를 쐈을때 닿은 지점으로 캐릭터를 움직여준다 OwnerCharacter->SetActorLocation(HitResults[(int32)EParkourArrowType::Center].ImpactPoint); OwnerCharacter->SetActorRotation(FRotator(0,ToFrontYaw,0)); (*DataMap.Find(EParkourType::Climb))[0].PlayMontage(OwnerCharacter); OwnerCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Flying); }
- 애니메이션에서 노티파이 실행 지점 도달 시 UCAnimNotify_EndParkour::Notify() 호출
- 파쿠르 상태에 맞게 해당 파쿠르 종료 함수 호출
void UCAnimNotify_EndParkour::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) { //상속받으면 무조건 super 콜 해줘야 함 Super::Notify(MeshComp, Animation, EventReference); CheckNull(MeshComp); CheckNull(MeshComp->GetOwner()); UCParkourComponent* Parkour = CHelpers::GetComponent<UCParkourComponent>(MeshComp->GetOwner()); CheckNull(Parkour); Parkour->End_DoParkour(); }
- 파쿠르가 끝났을 경우 애님 노티파이를 통해 원래 상태 MOVE_Walking로 전환
void UCParkourComponent::End_DoParkour_Climb() { OwnerCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking); }
ParkourComponent
1. 파쿠르를 전담하는 컴포넌트로 관리
2. Enum 값으로 파쿠르의 화살표 타입 구분 , 파쿠르 종류 구분
3. 파쿠르 컴포넌트와 생성한 화살표를 플레이어의 자식으로 부착
4. 구조체 구성
- 각 파쿠르 타입마다 실행 거리 범위, 모양 조건(Extent), 속도, 애니메이션을 명시
USTRUCT(BlueprintType) struct FParkrourData : public FTableRowBase { GENERATED_BODY() UPROPERTY(EditAnywhere) EParkourType Type; UPROPERTY(EditAnywhere) float MinDistance; UPROPERTY(EditAnywhere) float MaxDistance; UPROPERTY(EditAnywhere) float Extent; UPROPERTY(EditAnywhere) float PlayRate; UPROPERTY(EditAnywhere) bool bFixedCamera; UPROPERTY(EditAnywhere) UAnimMontage* Montage; ... };
void UCParkourComponent::BeginPlay() { OwnerCharacter = Cast<ACharacter>(GetOwner()); CheckNull(OwnerCharacter); USceneComponent* arrows = CHelpers::GetComponent<USceneComponent>(OwnerCharacter,"Arrows"); //찾아오려는 이름 TArray<USceneComponent*> components; arrows->GetChildrenComponents(false,components); for (int32 i = 0; i < (int32)EParkourArrowType::Max; i++) { Arrows[i] = Cast<UArrowComponent>(components[i]); } }
- 플레이어의 부착된 화살표들을 UCParkourComponent의 components변수에 저장하여 사용
- 데이터 테이블을 통해 데이터 불러오기
- DataTable에서 파쿠르 관련 데이터를 로딩하고, EParkourType별로 분류해서 DataMap에 저장
- TMap<EParkourType, TArray<FParkrourData>> DataMap에 저장해서 이후 쉽게 사용 가능
Super::BeginPlay(); TArray<FParkrourData*> rows; //전체 행 가져오기 DataTable->GetAllRows<FParkrourData>("",rows); for (int32 i = 0; i < (int32)EParkourType::Max; i++) { TArray<FParkrourData> temp; for (FParkrourData* data : rows) { if (data -> Type == (EParkourType)i) { temp.Add(*data); //포인터 기호로 data객체 자체를 줌 } } DataMap.Add((EParkourType)i,temp); }
문제 및 해결
문제 : 파쿠르 실행 할 거리와 플레이어 사이가 멀 경우에도 파쿠르 실행
해결 :
- HitDistance는 LineTrace로부터 얻은 실제 충돌까지의 거리
- FParkrourData에서 설정된 MinDistance, MaxDistance 범위 내에 있어야 파쿠르 가능
CheckFalseResult((*datas)[0].MinDistance < distance,false); CheckFalseResult((*datas)[0].MaxDistance > distance,false);
문제 : 대각선 방향에서도 파쿠르가 실행되는 문제
해결 :
- lookAt: 플레이어가 벽을 바라보는 방향
- impactAt: 벽의 표면 방향 (수직벡터 = 노멀)
- 두 방향이 유사해야 정면으로 간주
- 일정 각도(AvailableFrontAngle) 이상 벗어나면 파쿠르 금지
float lookAt = UKismetMathLibrary::FindLookAtRotation(start,end).Yaw; FVector impactNormal = HitResults[(int32)EParkourArrowType::Center].ImpactNormal; float impactAt = UKismetMathLibrary::MakeRotFromX(impactNormal).Yaw; //부딪힌 곳의 플레이어를 바라보는 방향 , 수직벡터의 값 float yaw = FMath::Abs(FMath::Abs(lookAt)-FMath::Abs(impactAt)); CheckFalseResult(yaw <= AvailableFrontAngle,false);
문제 : 캐릭터가 벽의 모서리에 딱 부딪힌 경우
해결 :
- 중앙/좌/우 Trace가 모두 벽에 닿는지 확인
- 중앙, 왼쪽, 오른쪽 LineTrace가 각각 다른 벽면에 부딪힘
- 각 벽은 서로 다른 방향을 바라보고 있음 → ImpactNormal이 서로 다름 -> 이걸 곡면 또는 모서리로 판단
- 벽이 일관된 면을 가져야 하므로, 세 방향 모두 히트되지 않으면 수행 제한
bool UCParkourComponent::Check_Obstacle() { ... b &= HitResults[(int32)EParkourArrowType::Center].bBlockingHit; b &= HitResults[(int32)EParkourArrowType::Left].bBlockingHit; b &= HitResults[(int32)EParkourArrowType::Right].bBlockingHit; CheckFalseResult(b,false); //b가 false면 넘어감 FVector center = HitResults[(int32)EParkourArrowType::Center].ImpactNormal; FVector left = HitResults[(int32)EParkourArrowType::Left].ImpactNormal; FVector right = HitResults[(int32)EParkourArrowType::Right].ImpactNormal; //Equals는 오차값 내이면 같은값이라고 간주함 //같은 경우 true로 패스함 CheckFalseResult(center.Equals(left),false); CheckFalseResult(center.Equals(right),false); return true; }
5. 하나의 파쿠르 타입에 여러 파쿠르 동작
void UCParkourComponent::DoParkour(bool bLanded) { ... FParkrourData data; if (Check_ObstacleMode(EParkourType::Normal,data)) { //노멀에 대해서만 체크 DoParkour_Obstacle(EParkourType::Normal,data); return; } if (Check_ObstacleMode(EParkourType::Short,data)) { DoParkour_Obstacle(EParkourType::Short,data); return; } if (Check_ObstacleMode(EParkourType::Wall,data)) { DoParkour_Obstacle(EParkourType::Wall,data); return; } }
5 - 1. 장애물 파쿠르 조건 확인 및 해당 데이터 추출
bool UCParkourComponent::Check_ObstacleMode(EParkourType InType, FParkrourData& OutData) { //머리가 닿으면 안됨 CheckTrueResult(HitResults[(int32)EParkourArrowType::Head].bBlockingHit,false); const TArray<FParkrourData>* datas = DataMap.Find(InType); ... return false; }
- DataMap의 구조
- Key: EParkourType (예: Climb, Fall, Normal, Short 등 파쿠르 동작 유형)
- Value: TArray<FParkrourData> (각 유형에 해당하는 파쿠르 데이터 리스트)
- TMap::Find(Key)는 해당 키가 존재하면 그 키에 대응하는 Value의 포인터를 반환
- datas는 해당 파쿠르 타입(EParkourType)에 연결된 FParkrourData 배열(TArray)의 주소를 담음
- 이를 통해 복사 없이 직접 배열 요소를 순회 및 비교 조건에 사용할 수 있음
for (int32 i = 0; i < (*datas).Num(); i++) { bool b = true; b &= (*datas)[i].MinDistance < HitDistance; b &= (*datas)[i].MaxDistance > HitDistance; b &= FMath::IsNearlyEqual((*datas)[i].Extent,HitObstacleExtent.Y,10); //현재 for문 돌리는 데이터의 번호를 넣어줌 OutData = (*datas)[i]; //b가 true면 끝 CheckTrueResult(b,true); } //위에서 하나라도 만족하면 true
- HitDistance가 조건 범위에 있는지 확인
- 장애물의 두께(Extent.Y)가 데이터 기준과 비슷한지 확인
- FMath::IsNearlyEqual은 float 오차를 감안해 비교
- 조건이 만족되면 해당 데이터를 OutData로 전달하고 true 반환
6. 올라가는 파쿠르 실행 후 낙하 시
- 착지 시 추락 거리가 700 이상일 때만 구르기 애니메이션 실행
- DataTable에서 정의된 낙하 동작(Fall 타입)의 거리 기준을 기반으로 판단
- DataMap은 TMap<EParkourType, TArray<FParkrourData>> 구조로 설계되어 있으며, 이 중 Fall 타입 데이터를 조회
- 구조체 내부 데이터를 안전하게 사용하기 위해 (*datas)[0]은 포인터를 역참조하여 구조체 객체를 직접 사용하기 위한 문법 사용
bool UCParkourComponent::Check_FallMode() { float distance = HitResults[(int32)EParkourArrowType::Land].Distance; const TArray<FParkrourData>* datas = DataMap.Find(EParkourType::Fall); CheckFalseResult((*datas)[0].MinDistance < distance, false); CheckFalseResult((*datas)[0].MaxDistance > distance, false); return true; }
7. 파쿠르 타입별 설정을 기준으로 애니메이션과 카메라 고정을 동시에 수행
void FParkrourData::PlayMontage(class ACharacter* InCharacter) { if (bFixedCamera) { UCMovementComponent* movement = CHelpers::GetComponent<UCMovementComponent>(InCharacter); if (!!movement) { movement->EnableFixedCamera(); } } InCharacter->PlayAnimMontage(Montage,PlayRate,SectionName); }
8. 파쿠르 종료 시 카메라 고정 해제
void UCParkourComponent::End_DoParkour() { ... UCMovementComponent* movement = CHelpers::GetComponent<UCMovementComponent>(OwnerCharacter); if (!!movement) { if (movement->GetFixedCamera() == true) { movement->DisableFixedCamera(); } } }
'Unreal' 카테고리의 다른 글
[UE5] 워프(Warp) 시스템 (0) 2025.06.14 [UE5] 타게팅 시스템 (0) 2025.06.13 [UE5] 적 피격 시스템 (0) 2025.06.13 [UE5] 무기 관리 (0) 2025.06.12 [UE5] 무기 장착 (0) 2025.06.09