초보 코린이의 성장 일지

UE4 Parkour Final, Feet IK 본문

언리얼

UE4 Parkour Final, Feet IK

코오린이 2023. 7. 20. 18:28
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Engine/DataTable.h"
#include "CParkourComponent.generated.h"

UCLASS()
class U2212_06_API UCParkourComponent : public UActorComponent
{
	GENERATED_BODY()

private:
	void CheckTrace_Land(); // 떨어질때 밑에 추적하기 용도

private:
	bool Check_FallMode();
	void DoParkour_Fall();

public:
	bool bFalling;

};
#include "Parkour/CParkourComponent.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/ArrowComponent.h"
#include "Components/CMovementComponent.h"

void UCParkourComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    CheckTrace_Land();
}

void UCParkourComponent::CheckTrace_Land()
{
    // 추락중인지 체크
    CheckFalse(OwnerCharacter->GetCharacterMovement()->IsFalling());

    CheckTrue(bFalling);
    bFalling = true;

    UArrowComponent* arrow = Arrows[(int32)EParkourArrowType::Land];
    FLinearColor color = FLinearColor(arrow->ArrowColor);

    FTransform transform = arrow->GetComponentToWorld();
    FVector start = transform.GetLocation();

    // 회전값과 높이까지 추적
    const TArray<FParkourData>* datas = DataMap.Find(EParkourType::Fall);
    FVector end = start + transform.GetRotation().GetForwardVector() * (*datas)[0].Extent;

    TArray<AActor*> ignores;
    ignores.Add(OwnerCharacter);

    UKismetSystemLibrary::LineTraceSingle(GetWorld(), start, end, ETraceTypeQuery::TraceTypeQuery1, false, ignores, DebugType, HitResults[(int32)EParkourArrowType::Land], true, color, FLinearColor::White);
}

void UCParkourComponent::DoParkour(bool bLanded)
{
    // 현재 수행중인 동작이 있다면 못하도록, Max가 수행중이 아닐때를 나타낸다  .
    CheckFalse(Type == EParkourType::Max);

    if (bLanded && Check_FallMode())
    {
        DoParkour_Fall();

        return;
    }
}

void UCParkourComponent::End_DoParkour()
{
    // 현재 수행 중인 타입들 체크 후 실행, 끝이나면 다시 타입 돌려주기
    switch (Type)
    {
    case EParkourType::Fall:
        End_DoParkour_Fall();
        break;
    }

}

bool UCParkourComponent::Check_FallMode()
{
    CheckFalseResult(bFalling, false);
    bFalling = false;

    // 거리 판단
    float distance = HitResults[(int32)EParkourArrowType::Land].Distance;

    const TArray<FParkourData>* datas = DataMap.Find(EParkourType::Fall);
    CheckFalseResult((*datas)[0].MinDistance < distance, false);
    CheckFalseResult((*datas)[0].MaxDistance > distance, false);

    return true;

}

void UCParkourComponent::DoParkour_Fall()
{
    Type = EParkourType::Fall;

    (*DataMap.Find(EParkourType::Fall))[0].PlayMontage(OwnerCharacter);
}

1. Climb를 하고 나서 아래로 떨어질때 구르는 동작을 하기 위해 아래를 판단하는 화살표를 쏴서 확인해준다.

2. 체크가 다 끝나면 DoParkour에서 실행하고 End_DoParkour에서 종료되도록 설정해준다.

 

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Components/CStateComponent.h"
#include "Characters/ICharacter.h"
#include "Parkour/CParkourComponent.h"	
#include "CPlayer.generated.h"

UCLASS()
class U2212_06_API ACPlayer 
	: public ACharacter
	, public IICharacter
{
	GENERATED_BODY()

public:
	void Landed(const FHitResult& Hit) override;
};
#include "Characters/CPlayer.h"
#include "Global.h"
#include "CAnimInstance.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/InputComponent.h"
#include "Components/CWeaponComponent.h"
#include "Components/CMontagesComponent.h"
#include "Components/CMovementComponent.h"
#include "Components/ArrowComponent.h"

void ACPlayer::Landed(const FHitResult& Hit)
{
	Super::Landed(Hit);

	// 파쿠르 콜해주고 떨어지는 중인지 true
	Parkour->DoParkour(true);
}

1. Player로가서 파쿠르 떨어지고 있는 중인지 ovrride 함수를 사용 해서 확인해준다.

 

1. 사용할 몽타주에서 끝내는 노티파이를 설정해준다.

2. 이제 캐릭터가 공중에서 떨어질때 구르는 동작이 나올 것이다.


#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Engine/DataTable.h"
#include "CParkourComponent.generated.h"

UCLASS()
class U2212_06_API UCParkourComponent : public UActorComponent
{
	GENERATED_BODY()

private:
	bool Check_SlideMode();
	void DoParkour_Slide();
	void End_DoParkour_Slide();

private:
	AActor* BackupObstacle;

};
#include "Parkour/CParkourComponent.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/ArrowComponent.h"
#include "Components/CMovementComponent.h"

void UCParkourComponent::DoParkour(bool bLanded)
{
    // 현재 수행중인 동작이 있다면 못하도록, Max가 수행중이 아닐때를 나타낸다  .
    CheckFalse(Type == EParkourType::Max);

    if (Check_SlideMode())
    {
        DoParkour_Slide();

        return;
    }
}

void UCParkourComponent::End_DoParkour()
{
    // 현재 수행 중인 타입들 체크 후 실행, 끝이나면 다시 타입 돌려주기
    switch (Type)
    {
    case EParkourType::Slide:
        End_DoParkour_Slide();
        break;
    }
}

bool UCParkourComponent::Check_SlideMode()
{
    CheckTrueResult(HitResults[(int32)EParkourArrowType::Floor].bBlockingHit, false);

    // 거리안에 충돌된 객체가 들어와 있는지 판단 
    const TArray<FParkourData>* datas = DataMap.Find(EParkourType::Slide);
    CheckFalseResult((*datas)[0].MinDistance < HitDistance, false);
    CheckFalseResult((*datas)[0].MaxDistance > HitDistance, false);

    UArrowComponent* arrow = Arrows[(int32)EParkourArrowType::Floor];
    FLinearColor color = FLinearColor(arrow->ArrowColor);

    FTransform transform = arrow->GetComponentToWorld();

    // 화살표 위치, 캐릭터 위치를 구해준다.
    FVector arrowLocation = transform.GetLocation();
    FVector ownerLocation = OwnerCharacter->GetActorLocation();

    // Extent는 이 곳에서 Box에 부피로 사용
    float extent = (*datas)[0].Extent;
    float z = arrowLocation.Z + extent;

    // 박스 방향
    FVector forward = OwnerCharacter->GetActorForwardVector();
    forward = FVector(forward.X, forward.Y, 0);

    // 해당 방향과 부피를 사용해서 Box를 그려준다.
    FVector start = FVector(arrowLocation.X, arrowLocation.Y, z);
    FVector end = start + forward * TraceDistance;

    TArray<AActor*> ignores;
    FHitResult hitResult;

    // FVector에 X값 길이가 0인 이유는 위에서 이미 X에 길이 관련해서 생성해놨기 때문에
    UKismetSystemLibrary::BoxTraceSingle(GetWorld(), start, end, FVector(0, extent, extent), OwnerCharacter->GetActorRotation(), ETraceTypeQuery::TraceTypeQuery1, false, ignores, DebugType, hitResult, true);
    CheckTrueResult(hitResult.bBlockingHit, false);

    return true;

}

void UCParkourComponent::DoParkour_Slide()
{
    Type = EParkourType::Slide;

    OwnerCharacter->SetActorRotation(FRotator(0, ToFrontYaw, 0));
    (*DataMap.Find(EParkourType::Slide))[0].PlayMontage(OwnerCharacter);

    // Box로 hit된 곳을 지나갈때 충돌체 Collision을 꺼줘여한다. 그래야 통과가 가능
    BackupObstacle = HitObstacle;
    BackupObstacle->SetActorEnableCollision(false);

}

void UCParkourComponent::End_DoParkour_Slide()
{
	// 끝나면 켜주고 다시 초기화
    BackupObstacle->SetActorEnableCollision(true);
    BackupObstacle = NULL;

}

1. Slide를 수행할 수 있도록 Box Trace를 쏴서 확인해준다.

2. Extent 변수를 Box에서는 길이가 아닌 부피로 사용하고, Box Trace 그릴때 X축을 사용하지 않는 이유는 위에서 이미 X값의 길이를 구해놨기 때문에 따로 사용하지 않는다.

3. Box가 그려지고 그 공간을 지나갈때 충돌체를 잠시 꺼주고 동작이 끝내면 End_DoParkour에서 End_DoParkour_Slide를 콜 해줌으로써 다시 돌려준다.

1. 동일하게 사용할 파쿠르에 노티파이에 End를 설정해줌으써 동작을 끝내도록 설정해준다.

2. 이제 슬라이드 동작도 잘 수행되는걸 확인할 수 있다.


#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Engine/DataTable.h"
#include "CParkourComponent.generated.h"

USTRUCT(BlueprintType)
// FTableRowBase로 상속을 받아야 불러지는 곳에서 노출이된다.
struct FParkourData : public FTableRowBase
{
	GENERATED_BODY()

private:
	bool Check_ObstacleMode(EParkourType InType, FParkourData& OutData);
	void DoParkour_Obstacle(EParkourType InType, FParkourData& InData);
	void End_DoParkour_Obstacle();

private:
	// 기본 충돌에 관한 정보들
	AActor* HitObstacle;
	FVector HitObstacleExtent;

	float HitDistance;
	float ToFrontYaw;
    
};
#include "Parkour/CParkourComponent.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/ArrowComponent.h"
#include "Components/CMovementComponent.h"

void UCParkourComponent::DoParkour(bool bLanded)
{
    // 현재 수행중인 동작이 있다면 못하도록, Max가 수행중이 아닐때를 나타낸다  .
    CheckFalse(Type == EParkourType::Max);

    FParkourData data;
    // 3가지 동작에 대해 정해진 타입 동작이 나오도록 설정
    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;
    }
}

void UCParkourComponent::End_DoParkour()
{
    // 현재 수행 중인 타입들 체크 후 실행, 끝이나면 다시 타입 돌려주기
    switch (Type)
    {
    case EParkourType::Short:

    case EParkourType::Normal:

    case EParkourType::Wall:
        End_DoParkour_Obstacle();
        break;
    }
}

bool UCParkourComponent::Check_ObstacleMode(EParkourType InType, FParkourData& OutData)
{
    CheckTrueResult(HitResults[(int32)EParkourArrowType::Ceil].bBlockingHit, false);

    const TArray<FParkourData>* datas = DataMap.Find(InType);

    for (int32 i = 0; i < (*datas).Num(); i++)
    {
        bool b = true;
        b &= (*datas)[i].MinDistance < HitDistance;
        b &= (*datas)[i].MaxDistance > HitDistance;
        // 거리, 부피 체크하고 return
        b &= FMath::IsNearlyEqual((*datas)[i].Extent, HitObstacleExtent.Y, 10);

        OutData = (*datas)[i]; // 체크된 결과를 매개변수 OutData로 return 해준다.
        CheckTrueResult(b, true);
    }

    return false;

}

void UCParkourComponent::DoParkour_Obstacle(EParkourType InType, FParkourData& InData)
{
    Type = InType;

    // 위에 Check_ObstacleMode에서 return 받은 매개변수 OutData를 InData로 받아와서 체크가 되었다면 동작 수행하도록 설정
    OwnerCharacter->SetActorRotation(FRotator(0, ToFrontYaw, 0));
    InData.PlayMontage(OwnerCharacter);

    BackupObstacle = HitObstacle;
    BackupObstacle->SetActorEnableCollision(false);

}

void UCParkourComponent::End_DoParkour_Obstacle()
{
    BackupObstacle->SetActorEnableCollision(true);
    BackupObstacle = NULL;

}

1. 3가지 동작에 대해 Check_ObstacleMode에서 조건에 알맞다면 return 해준다.

2. 매개변수 OutData를 동작을 할수 있게 해주는 DoParkour_Obstacle에서 매개변수 InData로 return 받은 매개변수를 받아와서 동작을 수행할 수 있도록 해준다.

3. 사용할 몽타주들에 End 노티파이를 설정해줬다면 이제 알맞은 동작이 나오게 될 것이다.


void UCParkourComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    // 값 초기화
    HitObstacle = NULL;
    HitObstacleExtent = FVector::ZeroVector;
    HitDistance = 0;
    ToFrontYaw = 0;

    CheckTrace_Center();

    // HitObstacle이 있다는건 hit가 되었다는 뜻
    if (!!HitObstacle)
    {
        CheckTrace_Ceil();
        CheckTrace_Floor();
        CheckTrace_LeftRight();
    }
}

1. Tick에서 매번 검사를 하게 해줬던 수정 전 코드.

void UCParkourComponent::DoParkour(bool bLanded)
{
    // 현재 수행중인 동작이 있다면 못하도록, Max가 수행중이 아닐때를 나타낸다  .
    CheckFalse(Type == EParkourType::Max);

    if (bLanded && Check_FallMode())
    {
        DoParkour_Fall();

        return;
    }

    // 값 초기화
    HitObstacle = NULL;
    HitObstacleExtent = FVector::ZeroVector;
    HitDistance = 0;
    ToFrontYaw = 0;

    CheckTrace_Center();

    // HitObstacle이 있다는건 hit가 되었다는 뜻
    if (!!HitObstacle)
    {
        CheckTrace_Ceil();
        CheckTrace_Floor();
        CheckTrace_LeftRight();
    }
}

1. Tick에서 매번 검사해주고 있는건 비효율적이므로, 파쿠르 동작을 시작할때 미리 검사를 해서 통과한다면 동작이 나오도록 하는게 더 효율적인 코드가 된다. 그래서 변경해준다.


#include "Parkour/CParkourComponent.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/ArrowComponent.h"
#include "Components/CMovementComponent.h"

void FParkourData::PlayMontage(class ACharacter* InCharacter)
{

    if (bFixedCamera)
    {
        UCMovementComponent* movement = CHelpers::GetComponent<UCMovementComponent>(InCharacter);

        if (!!movement)
            movement->EnableFixedCamera();
    }

    InCharacter->PlayAnimMontage(Montage, PlayRatio, SectionName);
}

void UCParkourComponent::End_DoParkour()
{
    Type = EParkourType::Max;

    UCMovementComponent* movement = CHelpers::GetComponent<UCMovementComponent>(OwnerCharacter);

    if (!!movement)
        movement->DisableFixedCamera();

}

1. 모든 동작 수행중에는 카메라가 못 움직이도록 고정 시켜준다.


파쿠르가 끝이나고 이제 IK를 시작해 볼 것이다.

1. Actor Component를 상속받아 클래스를 하나 생성해준다.

1. 한가지 설명하고 넘어가야하는게 있다.

2. 위에 Left, Right에 Distance 변수 X 축을 높이로 사용하게 되는 이유는 Bone_foot_l, Bone_foot_r을 보면은 정답이 나온다. 그림을 보면 x축이 위를 향하도록 되어있다. 즉 x축이 수직으로 앞을 보는게 아닌 높이처럼 사용되고 있다는 의미를 나타낸다. 그래서 x축을 높이로 사용할 것이다.

3. Pelvis는 실제 Root를 나타내며, World로 봐야한다 그래서 윗 방향이 z축이 맞기 때문에 그대로 사용하면 된다.


#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Kismet/KismetSystemLibrary.h"
#include "CFeetComponent.generated.h"

USTRUCT(BlueprintType)
struct FFeetData
{
	GENERATED_BODY()

public:
	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Feet")
		FVector LeftDistance; // X, 왼발하고 땅하고에 간격 즉 높이

	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Feet")
		FVector RightDistance; // X, 왼발하고 땅하고에 간격 즉 높이

	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Feet")
		FVector PelvisDistance; // Z, 허리 부분

	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Feet")
		FRotator LeftRotation;

	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Feet")
		FRotator RightRotation;
};

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class U2212_06_API UCFeetComponent : public UActorComponent
{
	GENERATED_BODY()

private:
	UPROPERTY(EditAnywhere, Category = "Trace")
		TEnumAsByte<EDrawDebugTrace::Type> DrawDebug;

	UPROPERTY(EditAnywhere, Category = "Trace")
		float InterpSpeed = 50; // 보간되는 속도

	UPROPERTY(EditAnywhere, Category = "Trace")
		float TraceDistance = 50; // 땅까지에 Trace 간격

	UPROPERTY(EditAnywhere, Category = "Trace")
		float OffsetDistance = 5; // 살짝 떠있는거에 대한 보정값

	UPROPERTY(EditAnywhere, Category = "Socket")
		FName LeftSocket = "Foot_L"; // left 소켓

	UPROPERTY(EditAnywhere, Category = "Socket")
		FName RightSocket = "Foot_R"; // right 소켓

public:
	FORCEINLINE FFeetData GetData() { return Data; }

public:	
	UCFeetComponent();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

private:
	// Trace를 추적해서 return 받기 용도
	void Trace(FName InName, float& OutDistance, FRotator& OutRotation);

private:
	class ACharacter* OwnerCharacter;

	FFeetData Data;

};
#include "Components/CFeetComponent.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/CapsuleComponent.h"

#define LOG_UCFeetComponent

UCFeetComponent::UCFeetComponent()
{
	PrimaryComponentTick.bCanEverTick = true;

}

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

	OwnerCharacter = Cast<ACharacter>(GetOwner());
}

void UCFeetComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	float leftDistance, rightDistance;
	FRotator leftRotation, rightRotation;

	// Trace를 이용하여 간격과 회전값.
	Trace(LeftSocket, leftDistance, leftRotation);
	Trace(RightSocket, rightDistance, rightRotation);

	// 둘중에 낮은 값에 허리값을 구해준다.
	float offset = FMath::Min(leftDistance, rightDistance);
	// 허리가 낮은 수치쪽으로 내려가야 하므로 보간처리
	Data.PelvisDistance.Z = UKismetMathLibrary::FInterpTo(Data.PelvisDistance.Z, offset, DeltaTime, InterpSpeed);

	// left, right offset를 빼서 보간처리해준다, -를 해주는 이유는 캐릭터가 좌우 대칭을 이루고 있기 때문에 한쪽은 반대로 해줘야한다.
	Data.LeftDistance.X = UKismetMathLibrary::FInterpTo(Data.LeftDistance.X, (leftDistance - offset), DeltaTime, InterpSpeed);
	Data.RightDistance.X = UKismetMathLibrary::FInterpTo(Data.RightDistance.X, -(rightDistance - offset), DeltaTime, InterpSpeed);

	// 회전도 동일하게 보간처리 해준다.
	Data.LeftRotation = UKismetMathLibrary::RInterpTo(Data.LeftRotation, leftRotation, DeltaTime, InterpSpeed);
	Data.RightRotation = UKismetMathLibrary::RInterpTo(Data.RightRotation, rightRotation, DeltaTime, InterpSpeed);

#ifdef LOG_UCFeetComponent
	CLog::Print(Data.LeftDistance, 11);
	CLog::Print(Data.RightDistance, 12);
	CLog::Print(Data.PelvisDistance, 13);
	CLog::Print(Data.LeftRotation, 14);
	CLog::Print(Data.RightRotation, 15);
#endif
}

void UCFeetComponent::Trace(FName InName, float& OutDistance, FRotator& OutRotation)
{
	// Socket에 위치는 World에 위치를 나타낸다.
	FVector socket = OwnerCharacter->GetMesh()->GetSocketLocation(InName);

	float z = OwnerCharacter->GetActorLocation().Z; // Owner에 높이를 구해준다.

	FVector start = FVector(socket.X, socket.Y, z); // Start에 X, Y만 넣어준다.

	// Capsule에 절반 간격과 - Trace 간격을 빼준다.
	z = start.Z - OwnerCharacter->GetCapsuleComponent()->GetScaledCapsuleHalfHeight() - TraceDistance;
	FVector end = FVector(socket.X, socket.Y, z);

	TArray<AActor*> ignores;
	ignores.Add(OwnerCharacter);

	FHitResult hitResult;
	// 플레이어 발에서 아래 방향으로 향하는 Trace, 복합충돌 true로 켜준다.
	UKismetSystemLibrary::LineTraceSingle(GetWorld(), start, end, ETraceTypeQuery::TraceTypeQuery1, true, ignores, DrawDebug, hitResult, true, FLinearColor::Green, FLinearColor::Red);

	// 간격, 회전 초기화
	OutDistance = 0;
	OutRotation = FRotator::ZeroRotator;

	// 블록이 안되어있다면 연산할 필요 x
	CheckFalse(hitResult.bBlockingHit);

	// Hit된 지점 - Trace를 빼준 이유는 간격을 구하기 위해서
	float length = (hitResult.ImpactPoint - hitResult.TraceEnd).Size();
	// 보정치를 더해주고 전체길이에서 TraceDistance 만큼 빼준다. 간격만 남기기 위해
	OutDistance = length + OffsetDistance - TraceDistance;

	float roll = UKismetMathLibrary::DegAtan2(hitResult.Normal.Y, hitResult.Normal.Z);
	// pitch를 -로 쓰는 이유는 발이 들려있을때 pitch를 반대로 써야 발이 내려갈 수 있다.
	float pitch = -UKismetMathLibrary::DegAtan2(hitResult.Normal.X, hitResult.Normal.Z);

	OutRotation = FRotator(pitch, 0, roll); // 발에 z축 회전은 필요없으므로 0,

}

1. 위에 주석에 보면 정리가 어느정도 되어있지만 제일 중요한것만 설명해 보겠다.

2. 발에있는 x축은 전방이 아닌 위를 바라보고 있기 때문에 z축을 사용하는 것처럼 x축을 높이로 사용한다.

3. 플레이어 발에서 아래로 바라보는 Trace를 쏴서 어느 발이 더 낮은 값이 적용되었는지 판별해준다.

4. 낮은 값을 가지고있는 발이 판별이 되어있다면, 그걸 기준으로 허리값을 얼마나 내려줄지 판단을 해준다.

5. 판단이 완료되면 허리를 낮은 수치쪽으로 일정 수치만큼 내려서 발이 땅에 정확히 닿을 수 있도록 설정해준다.

6. 그리고 모든 거리계산과 회전 계산에는 보간처리를 해줌으로써 자연스럽게 만들어준다.


1. Player로가서 만들어준 Feet 컴포넌트를 추가해주고, Debug를 켜서 확인해 보면서 진행할 것이다.

1. IK가 잘먹는지 확인하기 위한 지형도 만들어 준다.


#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Engine/DataTable.h"
#include "CParkourComponent.generated.h"

UCLASS()
class U2212_06_API UCParkourComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	FORCEINLINE bool IsExecuting() { return Type != EParkourType::Max; }

private:
	// 현재 수행중인 파쿠르 타입
	EParkourType Type = EParkourType::Max;

};

1. ParkourComponent에서 Animinstance에서 체크하기 위한 용도인 Type을 넘겨서 받아온다.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Kismet/KismetSystemLibrary.h"
#include "CFeetComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class U2212_06_API UCFeetComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	FORCEINLINE FFeetData GetData() { return Data; }

private:
	FFeetData Data;

};

1. CFeetComponent에서 Data를 넘겨주는 함수를 만들어준다.

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "Components/CFeetComponent.h"
#include "Components/CWeaponComponent.h"
#include "CAnimInstance.generated.h"

UCLASS()
class U2212_06_API UCAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
	
protected:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "InverseKinemetics")
		bool bFeet;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "InverseKinemetics")
		FFeetData FeetData;

};
#include "Characters/CAnimInstance.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "Weapons/CSubAction.h"
#include "Parkour/CParkourComponent.h"
#include "Components/CFeetComponent.h"

void UCAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
	Super::NativeUpdateAnimation(DeltaSeconds);
	CheckNull(OwnerCharacter);

	UCParkourComponent* parkour = CHelpers::GetComponent<UCParkourComponent>(OwnerCharacter);
	UCFeetComponent* feet = CHelpers::GetComponent<UCFeetComponent>(OwnerCharacter);

	bFeet = false;

	CheckNull(Weapon);
	CheckFalse(Weapon->IsUnarmedMode()); // 확실하게 무기장착 안했을때만 할 수 있도록 체크

	if (!!parkour && !!feet) // 파쿠르 중에는 또 동작하면 안되므로 체크
	{
		bFeet = parkour->IsExecuting() == false; // false와 같으면 수행한다.
		FeetData = feet->GetData(); // feet 데이터를 넣어준다.
	}

	else if (!!feet) // 아닐경우, 파쿠르는 수행 중인 상황
	{
		bFeet = true;
		FeetData = feet->GetData();
	}
}

1. 파쿠르와 Feet가 체크되어야 동작 수행할 수 있으므로, 함수로 각 변수들을 넘겨와준다.

2. 넘겨온 변수와 호환해주고, 조건이 맞는다면 동작을 나가도록 설정해준다.


#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "Components/CFeetComponent.h"
#include "Components/CWeaponComponent.h"
#include "CAnimInstance.generated.h"

UCLASS()
class U2212_06_API UCAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
	
protected:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Animation")
		bool bFalling;

};
#include "Characters/CAnimInstance.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Weapons/CSubAction.h"
#include "Parkour/CParkourComponent.h"
#include "Components/CFeetComponent.h"

void UCAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
	Super::NativeUpdateAnimation(DeltaSeconds);
	CheckNull(OwnerCharacter);

	bFalling = OwnerCharacter->GetCharacterMovement()->IsFalling();

}

1. Falling로 떨어지는 중인지를 체크해준다.


1. 만들어준 Falling 변수를 연결해준다. Locomotion Pose 바로 앞에 둔 이유는 몽타주가 나오는 구간인 곳에서 해줘야 자연스럽게 나올 수 있기 때문이다.

2. 이제 Climb 후에 떨어지면 가만히 서서 떨어지는게 아닌 떨어지는 동작이 바닥에 닫기 전까지 나오게 될 것이다.


IK는 아직 완성된게 아닌 70프로 정로만 설정되어 있다. 다음에 마무리해 보도록 할 것이다.

https://www.youtube.com/watch?v=Sa0vhf42a7Q 

 

'언리얼' 카테고리의 다른 글

UE4 Behavior Tree  (0) 2023.07.24
UE4 Feet IK, Behavior, HealBar  (0) 2023.07.21
UE4 Parkour Climb  (0) 2023.07.19
UE4 Parkour  (0) 2023.07.18
UE4 Bow Arrow  (0) 2023.07.13
Comments