ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 언리얼 프로젝트_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
Designed by Tistory.