본문 바로가기

Unreal Engine 4/C++

<Unreal C++> 73 - Action RPG (Inverse Kinemetics(IK))

 

 

필요한 개념


이걸 이해해야 멱살잡는 동작(Grappling), 파쿠르 동작(벽에 매달리고), 사다리 올라가는것, 지면을 발에 정확히 붙인다던가 이런것을 할 수 있음

 

우리는 발로 IK를 어떻게 하는지 처리(계단, 경사진 면에 발을 정확히 어떻게 붙이는지)



<버그 수정>

1) Null인지 체크, UnarmedMode일시에는 Equip 안함

* CActionComponent에 추가

// SetMode() 수정

if (!!Datas[(int32)InType])
{
	// 새로운 타입 무기 장착
	ACEquipment* equipment = Datas[(int32)InType]->GetEquipment();
	CheckNull(equipment);

	equipment->Equip();
}




unarmed는 Equip이 필요가 없다.

* Equip 부분 제거

void UCActionComponent::SetUnarmedMode()
{
	// 이미 무기가 장착되어 있다면 해제하고
	if (!!Datas[(int32)Type])
	{
		ACEquipment* equipment = Datas[(int32)Type]->GetEquipment();
		if (!!equipment)
			equipment->Unequip();
	}

	ChangeType(EActionType::Unarmed);
}



2) IceBall이 생성 안되는 경우가 생김

결과를 보면 가까이서 계속 아이스볼을 던지다 보면 에디터가 터지는 경우가 있다.
객체가 생성이 안되서 터진것(SpawnActor() 함수 때문에)
손에 생성되는 위치에 이미 액터가 존재해서 충돌되어서 생성되지 않아서 터지는 것이다.
-> 그래서 무조건 생성이 보장되는 SpawnActorDeferred() 함수를 사용한다.

SpawnActor() 함수는 반드시 스폰한다고 보장하지 않음(생성이 안될 수도 있음)
SpawnActorDeferred()에서 추가적으로 ESpawnActorCollisionHandlingMethod::AlwaysSpawn 인자를 줘서 무조건 스폰을 할 수 있음

* DoAction_Throw의 Begin_DoAction() 수정

void ACDoAction_Throw::Begin_DoAction()
{
	FVector location = OwnerCharacter->GetMesh()->GetSocketLocation("Hand_Throw_Projectile");

	// 플레이어의 전방 or 적의 전방
	FRotator rotator = OwnerCharacter->GetController()->GetControlRotation();

	FTransform transform = Datas[0].EffectTransform;
	transform.AddToTranslation(location);
	transform.SetRotation(FQuat(rotator));

	FActorSpawnParameters params;
	params.Owner = OwnerCharacter; // 스폰한 액터
	// FActorSpawnParameters안에 ESpawnActorCollisionHandlingMethod가 있음
	// ESpawnActorCollisionHandlingMethod : 현재 Spawn하려는 위치에 다른액터가 있을때 어떻게 처리할지를 결정하는 열거형
	// 위치에 스폰되면서 얘가 자동으로 충돌을 한다. 그래서 그 자리에 있으면
	// Undefined : 스폰시키지 않음
	// AlwaysSpawn : 있든 없든 충돌 무시하고, 무조건 스폰 시켜라
	// AdjustIfPossibleButAlwaysSpawn : 위치를 조정한다음 스폰시켜라
	// AdjustIfPossibleButDontSpawnIfColliding : 충돌되었다면 조정해서 스폰시켜라
	// DontSpawnIfColliding : 아예 스폰시키지 않겠다.
	// params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

	// CThrow의 GetWorld()로 사용해도 무방하나 캐릭터가 배치되어 있는 월드에 스폰
	// SpawnActor() 함수는 반드시 스폰한다고 보장하지 않음
	// ACThrow* throwObject = OwnerCharacter->GetWorld()->SpawnActor<ACThrow>(Datas[0].ThrowClass, transform, params);

	// 그래서 반드시 스폰되는 함수를 사용
	ACThrow* throwObject = OwnerCharacter->GetWorld()->SpawnActorDeferred<ACThrow>(Datas[0].ThrowClass, transform, OwnerCharacter, NULL, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

	throwObject->OnThrowBeginOverlap.AddDynamic(this, &ACDoAction_Throw::OnThrowBeginOverlap);
	// 등장 확정 처리
	UGameplayStatics::FinishSpawningActor(throwObject, transform);
}




<Inverse Kinemetics>
우리가 현재 애니메이션 그리는 방식은 FK(Forward Kinemetics) 이다.
FK(Forward Kinemetics, 정운동학) : 루트에서부터 애니메이션을 계산해서 출력하는 방식

캐릭터->본그리기->선택 및 부모 하면 보임

 


* 오른손을 뻗어서 공격하는 애니메이션
루트에서부터 오른손이 펼쳐지는 것까지 계산함(부모에서 부터 자식까지 쭉 계산함)
문제가 발생? 만약에 벽이 있다면 오른손 주먹이 벽을 뚫고 지나가게 됨(루트에서 부터 계산해서 나가기 때문에)

 

 


주먹이 딱 벽에 맞으면 깔끔하다
-> 그래서 그런걸 할 때 사용하는 것이 IK(Inverse Kinemetics)이다.
루트에서 부터 어깨까지는 FK로 계산하고, 주먹에서 부터 어깨까지는 IK, 즉 반대로 계산함
밑(루트)에서 부터 위(어깨)까지 계산하고(부모에서 자식) ->  FK(정운동학)으로 계산,

주먹이 벽에 붙었다고 가정하의 주먹에서 부터 어깨까지 역으로 계산함(자식에서 부모로) -> IK(역운동학)으로 계산

IK(Inverse Kinemetics, 역운동학) : 지정된 본으로부터 반대로 계산을 해서 올라가는 방식

언리얼에서는 2 Bone IK라고 부른다
Why? 루트에서 부터 어깨까지(1본) + 주먹에서 부터 어깨까지(1본)
2 Bone IK : 정운동학과 역운동학으로 동시에 계산하는 방식

Hand IK, Foot IK가 있는데 우리는 Foot IK만 다룬다.
원리는 똑같음

우리는 발의 위치뿐만 아니라, 회전까지도 적용할 것이다.
언덕에서 일어서 있다면 발도 회전되어 있어야 한다.

* Foot IK 테스트를 위해 계단과 경사를 World에 배치


플레이어가 얼만큼의 높이를 올라갈 수 있는지, 얼마만큼 경사를 올라갈 수 있는지는 

모두 Character의 CharacterMovement의 CharacterMovement: Walking의 MaxStepHeight 값이다.

* CharacterMovement에 있는 관련 변수들
MaxStepHeight : 캐릭터가 밟고 올라갈 수 있는 최대 높이(한번에)
Walkable Floor Angle : 경사(기본 값이 45도 인데, 45도에 가깝게 등판(걸어 올라갈 수 있다는 의미)
Ground Friction : 경사에 서있을 때, 올라갈 수록 뒤로 밀리는 저항

<발이 붙는지 테스트 확인 할려면 ZoonIn, ZoonOut을 세팅해야함>

 

* 프로젝트 세팅 -> 입력 -> 축 매핑 -> Zoom 추가
마우스 휠 축


Zoom은 SpringArm에 대한 간격을 사용했음

* Zoom 입력 추가
CPlayer에 추가

 

PlayerInputComponent->BindAxis("Zoom", this, &ACPlayer::OnZoom);



void ACPlayer::OnZoom(float InAxis)
{
	// DeltaTime을 곱해줘서 일정 속도 유지
	SpringArm->TargetArmLength += (1000.0f * InAxis * GetWorld()->GetDeltaSeconds());

	// 0은 완전 중심부로 와버려서, 50으로 함
	SpringArm->TargetArmLength = FMath::Clamp(SpringArm->TargetArmLength, 50.0f, 500.0f);
}



* 결과를 보면 마우스 휠로 ZoomIn, ZoomOut이 가능하다.

 

 


* 계단을 올라가보면 캐릭터가 한발이 계단위에 올라가 있고, 다른 한발은 공중에 떠있다.

 


-> 이것을 IK를 통해 한발은 계단위로 가면, 다른 한발은 그 바닥에 붙을 수 있게 처리한다.

* 경사에 올라가보면 캐릭터가 두 발이 바닥에 떠 있다.


-> 이것을 IK를 통해 발목을 회전시켜서 바닥에 붙을 수 있도록 처리한다.

 

IK Component가 붙은 애들은 작동시킬 것이고, 없는 애들은 작동을 안 시킬것


* ActorComponent를 상속 받는 CFeetComponent 생성
우선 왼발 오른발 위치 조정할 것이고, 허리 부분 높이 조정하고, 발이 붙은 곳에 대한 회전각도를 구해서 발을 회전시킴


* Tip) 캡슐 컴포넌트 같은거 HiddenInGame을 꺼주면 게임 내에서 충돌체가 보임


* CFeetComponent에 추가

// 움직일 값
USTRUCT(BlueprintType)
struct FFeetData
{
	GENERATED_BODY()

	// BlueprintReadOnly : 읽기 전용으로
	// EditAnywhere : 애니메이션에서 디버깅(프리뷰)이 가능하도록
	// float형으로 사용해도 되지만, BP에서 귀찮아져서 vector형으로 사용
	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FVector LeftDistance; // 왼발에 대한 왼발이 떨어져 있을때, 떨어진 곳과의 간격(X축만 사용)

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FVector RightDistance; // 오른발과 떨어진 곳과의 간격(X축만 사용)

};


Tip) 왜 x축만 사용하나?
Foot_L까지 부모에서 부터 누적되어 온 회전 공간의 높이가 X축이 되므로 X축 값만 사용

(모델의 Foot_L축을 보면 발의 높이가 X축이다.)

-> 발의 높낮이만 조정하면 되서(즉, X축만 필요)

 

* Foot_l의 회전 축(부모로 부터 누적되어 회전되어서 축이 다름)

 

 

private:
	// BP에서 수정가능하도록
	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
		FName LeftSocket = "Foot_L";

	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
		FName RightSocket = "Foot_R";



// 뛰는 것은 실제 바닥에서 좀 떨어져 있어야 한다.
// 근데 이 값이 너무 크면 지면을 추적을 해서 발이 위에 떨어져 있어야 하는데 붙게됨
// 너무 크면 점프할때도 달라붙는 느낌이 남(-> 적당히 조정하면 된다.)
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
	float TraceDistance = 55.0f; // 추적할 간격(거리)



// TEnumAsByte : 자료형의 크기를 알 수 있도록(바이트 크기로 명시함)
// 그냥 enum은 크기를 알 수 없기 때문에
// For One Frame : 매프레임 마다 그림
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
	TEnumAsByte<EDrawDebugTrace::Type> DrawDebugType = EDrawDebugTrace::None; // #include "Kismet/KismetSystemLibrary.h"에 있음



private:
	// 발의 본은 굳이 소켓하지 않고 본으로 하면 된다.
	// 발의 본과 지면의 붙은 곳의 거리를 구하는 내부함수
	// Trace : 해당 소켓을 추적하여 소켓과 충돌 지점의 거리를 구해서 리턴해주는 함수
	void Trace(FName InSocket, float& OutDistance);



private:
	class ACharacter* OwnerCharacter;
	FFeetData Data;




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


	// 간격을 구해줌
	float leftDistance; // 왼발과 지면 붙은 곳의 간격
	Trace(LeftSocket, leftDistance);

	float rightDistance; // 오른발과 지면 붙은 곳의 간격
	Trace(RightSocket, rightDistance);
}

 

* Trace() 함수 설명

 

 

void UCFeetComponent::Trace(FName InSocket, float& OutDistance)
{
	OutDistance = 0.0f;

	// GetSocketLocation() : 소켓이 가진 상대위치가 아닌 월드 위치를 리턴해줌
	FVector location = OwnerCharacter->GetMesh()->GetSocketLocation(InSocket);

	// GetActorLocation()은 해당 액터의 중심점의 위치를 리턴한다.
	// 중심점(허리)으로 부터 다리까지 다리를 지나가도록 선을 그을 것이다.
	// X(앞뒤), Y(좌우)는 발의 좌우 앞뒤, Z는 허리서부터 내려오는 높이(즉 허리 높이)
	// 즉, start는 허리높이에 발의 x,y 위치를 갖는 것이 된다.)
	FVector start = FVector(location.X, location.Y , OwnerCharacter->GetActorLocation().Z);

	// GetScaledCapsuleHalfHeight() : 캡슐의 스케일을 적용한 상태의 1/2 크기를 리턴
	// 캐릭터의 크기 절반 만큼인 캡슐의 절반을 빼주면 발바닥의 위치가 되고, TraceDistance(추적할 간격)만큼 더 빼주면 발밑으로 내려간다.
	float traceZ = start.Z - OwnerCharacter->GetCapsuleComponent()->GetScaledCapsuleHalfHeight() - TraceDistance;

	// 결국 end는 발바닥(소켓)의 좌우, 앞뒤의 위치를 가지고 높이는 발밑 TraceDistance 만큼 내려간다.
	FVector end = FVector(location.X, location.Y, traceZ);

	// LineTraceSingle에서 아무것도 안붙이는 이 함수는
	// LineTraceByChannel과 같다.
	// 4번 인자는 Channel, UEngineTypes::ConvertToTraceType()을 사용하면 CollisionChannel로 추적하게 된다.
	// ECC_Visibillity : 화면에 등장한거 전부다 추적해라(지면은 보이는 것이니까, 지면마다 채널이 다르게 지정되어있을 수 있어서)
	// 5번 인자 : 복합 콜리젼 여부(True로 할시 느림, 삼각형 단위 체크) -> But 더 정밀한 경과 나옴, IK에서는 정밀하게 계산해야 해서 필요
	// -> 그래서 IK는 느리다.(정밀하게 추적해야 하니까)

	// 플레이어 캐릭터 추적 무시
	TArray<AActor*> ignoreActors;
	ignoreActors.Add(OwnerCharacter);

	// 추적하는 곳의 색상은 녹색, 충돌되는 색상은 빨간색으로 설정
	FHitResult hitResult;
	UKismetSystemLibrary::LineTraceSingle(GetWorld(), start, end, UEngineTypes::ConvertToTraceType(ECC_Visibility), true, ignoreActors, DrawDebugType, hitResult, true, FLinearColor::Green, FLinearColor::Red);
}



LineTrace해서 충돌되는 부분에 IK 해당본을 움직이는 작업

* CPlayer에서 FeetComponent 추가 및 생성

UPROPERTY(VisibleDefaultsOnly)
	class UCFeetComponent* Feet;



CHelpers::CreateActorComponent<UCFeetComponent>(this, &Feet, "Feet");






* BP_CPlayer에서 FeetComponent에 DrawDebugMode에 ForOneFrame으로 값 변경

-> Tick()으로 돌기 때문에 ForOneFrame으로 할시에 계속 보이게 된다.


* 결과를 보면 발의 계단에 올라가면 한쪽 발은 계단에 있고 다른 한쪽 발 밑에는 바닥에 충돌 되는 것을 볼 수 가 있음
-> 그 충돌 위치에 발의 높이를 조정해줄 것이다.

* CFeetComponent에 추가

UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
	float InterpSpeed = 17.0f; // 보간 속도



public:
	// 복사 방지
	FORCEINLINE const FFeetData& GetData() { return Data; }



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


	// 간격을 구해줌
	float leftDistance; // 왼발과 지면 붙은 곳의 간격
	Trace(LeftSocket, leftDistance);

	float rightDistance; // 오른발과 지면 붙은 곳의 간격
	Trace(RightSocket, rightDistance);

	// FInterpTo는 이동 보간이고, RInterpTo는 회전보간이다.
	// 발이 현재 있는 위치에서 발이 닫는 위치까지 흘러가는 시간에 따라 보간
	// 1 Current : 현재, 2 Target : 지점, 3 DeltaTime : 부드럽게 해주기 위한 흘러가는 시간, 4 : 속도
	// Data.LeftDistance.X은 0이니까, 0부터 간격까지 이동 보간 해서 다시 넣어준다.
	Data.LeftDistance.X = UKismetMathLibrary::FInterpTo(Data.LeftDistance.X, leftDistance, DeltaTime, InterpSpeed);
}




* 애니메이션에 이 값을 가져다 써야 한다.

* CAnimInstace에 추가

UPROPERTY(BlueprintReadOnly, EditAnywhere)
	FFeetData FeetData; // IK에 관한 정보(발과 타겟과의 간격 정보가 담겨 있음)




// NativeUpdateAnimation()

UCFeetComponent* feet = CHelpers::GetComponent<UCFeetComponent>(character);
if (!!feet)
	FeetData = feet->GetData();



* 이제 넘어온 이 값을 가지고 ABP_CPlayer에서 IK관한 처리를 함

 

* ABP_CPlayer AnimGraph



* 포즈 공간
로컬 공간(Local 공간) : 기본적인 애니메이션이 계산되는 공간
컴포넌트 공간(Component 공간) : 스켈레톤 메시를 다뤄야 할 경우 사용되는 공간

Local 공간 : Root본에서 부터 계산되는 FK 공간(흔히 애니메이션에서 써왔던 공간)
Component 공간 : SkeletalMeshComponent의 상대적 공간(부모로부터 계산되는 공간이 아닌, 우리가 임의대로 본을 움직일 수 있도록 사용하는 포즈공간)
-> FK가 아니라 우리가 어느본을 임의로 움직여야되는 포즈공간이면 Component Pose를 사용한다.

애니메이션은 Local 공간이 되고, IK는 컴포넌트 공간에서 수행해야 함
애니메이션 기본 공간은 Local이라고 생각하면 되고, 컴포넌트 공간(소켓을 제어하기 위한 공간)은 소켓이 포함되어 있는 SkeletalMeshComponent의 공간이다.
IK는 소켓에 붙어있는 애니메이션을 움직이기 때문에, 컴포넌트 공간으로 바꿔준다고 생각하면 됨

Foot부터 허벅지까지 역으로 계산하는 것이 IK이다.
우리는 Root에서 부터 calf(왼쪽 허벅지)까지는 정 방향(정 운동학)으로 계산하고(FK), 왼발(Foot)부터 calf(왼쪽 허벅지)까지 역 방향(역 운동학)으로 계산(IK)하라는 명령을 줄 것이다.

우리는 이제까지 FK를 사용함(전부 Root에서 부터 밑으로 내려오면서 누적되면서 내려오는 FK를 기본으로 쓰고 있었음)

Foot은 우리가 원하는 만큼 움직여 줘야 하기때문에 Foot부터 calf까지는 올라가는 연산을 함(IK)
물론 Foot밑에 있는 ball(발가락)은 정방향 계산을 함(FK)

2본 IK : Effector 본(IK 적용될)과 Joint 본(FK로 계산될) 계산 결과를 합산하여 본을 조정하기 위한 기능

(본을 계산하는데 정방향 연산을 하고, 밑에서 올라오는 역방향 연산을 해서 총 2번해서)

* 2본 IK에서 본을 설정해준다.
IKBone : 적용할 기준 본(우리는 Foot_L(왼발))

 

 


* 스켈레톤 메시를 보면 ik_foot_l이 따로 있는 경우가 있음
-> 움직이도록 디자이너가 잡아놈(애니메이션 될때 움직임)
움직이는 애니메이션을 플레이 해보면 ik_foot_l은 정확히 맞춰져 있지 않고, 대략적인 위치로 지정되어 있다.
* 이럴 때는 ik붙인거를 쓰면 안됨
-> Foot_L 처럼 정확히 붙어서 움직이는 본이어야만 IK를 쓸 수 있음

* 그러면 왜 디자이너는 ik_foot_l를 디자인 했나?
-> 디자이너가 프로그래머한테 IK지점을 별도록 움직이겠다 해서 별도로 만들어 놓음

(근데 기준이 하나가 있고, 사본 정도로 마련한 정도로 이해하면 됨)
우리 상황에서는 IK위치가 정확하지 않아서, IK디자인 된 본을 사용하면 안됨
만약 IK가 별도로 정확히 움직이는 모델이라면 IK를 쓰는 게 좋음

(원본을 유지한 상태에서 IK를 들어가서 좀 더 정확히 들어갈 수 있음)
에셋 스토어에 IK가 정확히 들어가는게 있는데 그런 것들은 IK로 잡아서 해주는 것이 결과가 좀 더 깔끔함


우리는 정확히 적용이 안되어있기 때문에 Foot_L로 지정

* 2본 IK의 속성들을 살펴보자

 

IKBone : 적용할 기준 본(우리는 Foot_L(왼발))
Maintain Effector RelRot : 로컬 회전을 유지할 건지

(ex) Foot_L이 회전하면 자식들도 회전할 건지 -> 우리는 같이 회전할 거라 true로 켜줌)

 

Effector 

 

Tip) 용어 설명
Effector : IK가 적용될 값


Effector Location Space : IK는 컴포넌트 공간이라 컴포넌트 공간이 켜져있음

(블루프린트에서는 컴포넌트 공간이지만, C에서는 본공간으로 사용해줄 것임)
C++코드에서 AnimBP로 주는 이펙터의 값은 본이 얼마만큼 움직여질지의 상대값으므로 BoneSpace로 지정해준다.

Take Rotation From Effector Space : IK본이 회전을 하게 할거냐

(우리는 나중에 발을 회전해서 땅바닥에 정확히 붙일 거여서 true로 설정)

Effector Target : IK를 적용할 실제 본(우리는 Foot_L)
만약 디자이너가 ik_foot_l을 만들어 놓았다면 IKBone에는 ik_foot_l이고 Effector Target에는 Foot_L(실제 본)이다.
-> 그래서 좀 더 깔끔히 나올 수 있음

Effector Location : IK를 적용해서 움직일 위치

2본 IK 노드에 노출된 세팅 가능 한 값이다.(실제로 움직일 위치(C에서 구한 충돌된 위치를 줄것임))

 

Joint Target

 

Tip) 용어 설명

Joint : IK를 어디까지 연산해나갈 것인지

(적용을 어느본까지 시킬건지, 우리는 Foot_L(발) 부터 Calf_L(무릎)까지로 계산하겠다.)

 

Joint Target Location : 발 부터 무릎까지 연산을 할거기 때문에 BoneSpace로 설정 함

(코드 상으로 계산된다고 가정하니까)

 

Joint Target : Calf_L(무릎)으로 설정
Calf_L(무릎)가 정방향으로 부터 계산된 기준점이 됨
무릎의 위치를 조정하고 싶다면 Joint Target Location에 값을 넣으면 된다.(우리는 따로 조정할 건 아니어서 꺼버림)

 

Alpha


Alpha값 : IK의 적용은 완전히 시키느냐, 혹은 반만 적용할건지(우리는 완전 적용시킬 것이어서, 1.0로)

 

* CFeetComponent에 추가

모델보면 지면에서 발이 떠 있다. -> 그래서 OffsetDistance로 발과 땅사이에 간격을 조절함

UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
	float OffsetDistance = 5.0f; // 발과 땅사이에 보정간격

 

 

* Trace() 수식 설명

 



수식
Distance = Offset + (ImpactPoint - TraceEnd).Size() - TraceDistance;

// CFeetComponent::Trace(FName InSocket, float& OutDistance)

// OutDistance -> 발이 움직일 높이 간격 구함
// .Size()로 구한 이유는 간격이어서 길이로 구함
// hitResult.TraceEnd, TraceDistance는 원래 발의 위치로 옮겨줌
// ImpactPoint 발이 이동할 위치로 잡아줌
// OffsetDistance : 발의 보정값을 줌
float length = (hitResult.ImpactPoint - hitResult.TraceEnd).Size();
OutDistance = OffsetDistance + length - TraceDistance; // TraceDistance 만큼 내려감





* 왼발만 적용시에 결과를 보면 거의 차이를 못느낌
-> 아직 오른발의 차이가 없으며 최종적으로 허리(Pelvis)의 높이까지 적용되어야 정확한 높이로 발이 움직임

<오른발에도 적용>

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


	// 간격을 구해줌
	float leftDistance; // 왼발과 지면 붙은 곳의 간격
	Trace(LeftSocket, leftDistance);

	float rightDistance; // 오른발과 지면 붙은 곳의 간격
	Trace(RightSocket, rightDistance);

	// FInterpTo는 이동 보간이고, RInterpTo는 회전보간이다.
	// 발이 현재 있는 위치에서 발이 닫는 위치까지 흘러가는 시간에 따라 보간
	// 1 Current : 현재, 2 Target : 지점, 3 DeltaTime : 부드럽게 해주기 위한 흘러가는 시간, 4 : 속도
	// Data.LeftDistance.X은 0이니까, 0부터 간격까지 이동 보간 해서 다시 넣어준다.
	Data.LeftDistance.X = UKismetMathLibrary::FInterpTo(Data.LeftDistance.X, leftDistance, DeltaTime, InterpSpeed);
	// right는 left에 대칭되서 -값이다.
	Data.RightDistance.X = UKismetMathLibrary::FInterpTo(Data.RightDistance.X, -rightDistance, DeltaTime, InterpSpeed);
}



* Foot_l, Foot_r의 위치값이 정확히 대칭되는 것을 볼 수 있음

 

Tip) 캐릭터의 좌우는 항상 대칭이므로 값을 계산할 때도 대칭으로 계산함(디자인 할때 대칭으로 디자이너들이 함)
ex) 왼발이 X가 -40이라면 오른발 X는 +40이다. -> 대칭됨
그래서 -rightDistance로 대칭되도록 해줌
여기서는 right를 -로 뒤집었는데, 실제로는 왼발이 -이다.

 

 

* ABP_CPlayer AnimGraph



ABP_CPlayer에서는
IKBone, Effector Target을 Foot_R로 바꿔주고 Joint Target을 calf_r(오른 무릎)으로 바꿔준다. 


* 결과를 보면 계단을 오르면 오른발, 왼발 다 잘 꺽이는 것을 볼 수 있음, 그런데 아직 어색함?
-> 허리까지, 즉 몸체까지 같이 내려와야 땅에 발이 정확히 붙을 수 있음(발이 좀 떠 있음 -> 즉, 허리가 내려와야 함)
허리까지 높이를 조정한다면 완전한 IK높이 조정이 됨

<허리 높이 조정>

Tip)
Root와 Pelvis(허리)는 월드 공간 상의 회전 방향을 사용해야 한다.
Root : 언리얼 내부적으로 캐릭터의 위치를 움직이기 위한 가상 본
본의 시작이 Pelvis이다.
본의 시작 공간은 항상 월드 공간이라고 생각하면 된다.
Pelvis는 Root 바로 밑이다.
Root와 Pelvis로 나머지 애들이 계산됨
-> 그래서 Pelvis는 월드 공간 좌표인 Z값으로 높이를 조정

USTRUCT(BlueprintType)
struct FFeetData
{
	GENERATED_BODY()

	// BlueprintReadOnly : 읽기 전용으로
	// EditAnywhere : 애니메이션에서 디버깅(프리뷰)이 가능하도록
	// float형으로 사용해도 되지만, BP에서 귀찮아져서 vector형으로 사용
	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FVector LeftDistance; // 왼발에 대한 왼발이 떨어져 있을때, 떨어진 곳과의 간격(X축만 사용)

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FVector RightDistance; // 오른발과 떨어진 곳과의 간격(X축만 사용)

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FVector PelvisDistance; // 허리(Z축만 사용)
};



* FVector로 한 이유는 AnimBlueprint에 2본IK 노드에 Vector로 받고 있어서


허리(Pelvis) 높이 조정과 발의 회전 처리
두 발 중에 낮은 값을 이용해서 한쪽은 더 낮춰주고, 한쪽은 더 올려주고 해서 높이를 자연스럽게 만듬
두 발 중 낮은 값을 기준으로 하여 계산, 낮은 발 높이로 허리를 이동시킴, 본 트랜스폼의 이동으로 대입

허리 높이 조정은 간단하다. 오른발 혹은 왼발 중 가장 높이가 낮은(밑으로 내려간다면) 값을 기준으로 더 작은 애를 선택해서 그 값을 이용하면 됨
계단에 한 발을 올리면 다른 발은 떨어지게 되고 상체는 숙이게 된다.

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

	float leftDistance;
	Trace(LeftSocket, leftDistance);

	float rightDistance;
	Trace(RightSocket, rightDistance);

	// 왼발과 오른발 중 높이가 가장 낮은 값을 선택
	float offset = FMath::Min(leftDistance, rightDistance);
	Data.PelvisDistance.Z = UKismetMathLibrary::FInterpTo(Data.PelvisDistance.Z, offset, DeltaTime, InterpSpeed);


	// offset이 -라면 LeftDistance가 내려가고, +라면 RightDistance가 내려감
	// -> 각 발에 offset을 빼준다.
	// Data.LeftDistance.X은 0이니까, 0부터 간격까지 이동 보간 해서 다시 넣어준다.
	Data.LeftDistance.X = UKismetMathLibrary::FInterpTo(Data.LeftDistance.X, (leftDistance - offset), DeltaTime, InterpSpeed);
	// right는 left에 대칭되서 -값이다.
	Data.RightDistance.X = UKismetMathLibrary::FInterpTo(Data.RightDistance.X, -(rightDistance - offset), DeltaTime, InterpSpeed);
}



* ABP_CPlayer 수정
Pelvis는 어디에 붙이기 위한 IK값이 아니라 본 자체의 높이만 수정하므로 "본 트랜스폼(변경)"을 사용함
우리는 거리를 조정할 것임

* ABP_CPlayer AnimGraph


Pelvis(허리)는 루트 바로 밑이어서 축이 뒤집혀져 있지 않음(월드 공간상의 좌표 SRT)

* 본 트랜스폼(변경) 속성

Bone to Modify : 바꿀 본

Translation(위치)

 

TranslationMode : Ignore(바꾸지 않음), Replace Existing(바로 변경시킴), Add to Existing(보간되면서 서서히 변경시킴) 
Translation Space : WorldSpace(본을 움직일 값을 구한 것이 World 공간의 값을 구한 것이므로 World 공간으로 지정해줌)

Rotation

 

RotationMode : 적용할 것이 아니어서 Ignore로 처리

Scale

 

ScaleMode : 적용할 것이 아니어서 Ignore로 처리

 

Alpha

 

Alpha : 얼만큼 이동할지(0.5면 반만큼 이동) -> 기본값 1로 사용


* 결과를 보면 계단에 올라갔을때 자연스럽게 적용이 됨
약간 겹쳐서 계단 안쪽에 들어가는 것은 어쩔 수 없음
-> 발 중간 쪽에 가상 본을 두고, 처리하면 없어짐

만약 발이 움직여 조정되는 속도가 느리다면 InterpSpeed 변수를 조정해주면 된다.

<발 회전>
경사에 올라가보면 발이 정확히 붙지 않는다.
-> 발의 본에 회전 적용이 필요


// 애니메이션을 사용할 구조체
USTRUCT(BlueprintType)
struct FFeetData
{
	GENERATED_BODY()

	// BlueprintReadOnly : 읽기 전용으로
	// EditAnywhere : 애니메이션에서 디버깅(프리뷰)이 가능하도록
	// float형으로 사용해도 되지만, BP에서 귀찮아져서 vector형으로 사용
	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FVector LeftDistance; // 왼발에 대한 왼발이 떨어져 있을때, 떨어진 곳과의 간격(X축만 사용)

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FVector RightDistance; // 오른발과 떨어진 곳과의 간격(X축만 사용)

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FVector PelvisDistance; // 허리(Z축만 사용)

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FRotator LeftRotation; // 왼발에 대한 회전

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FRotator RightRotation; // 오른발에 대한 회전
};



Atan을 이용해 발의 Roll, Pitch 회전값을 계산
본 트랜스폼의 회전으로 대입

void UCFeetComponent::Trace(FName InSocket, float& OutDistance, FRotator& OutRotation)
{
	...

	// hit된 면에 수직 벡터
	FVector normal = hitResult.ImpactNormal;
	// normal를 이용해서 회전을 2개 찾아옴
	// Roll 전방축 회전(옆구르기 회전), Pitch 우측 벡터 기준 회전(위아래 회전), Yaw(좌,우 회전)은 필요없음, 왼쪽이나 오른쪽으로 발을 회전하지 않기 떄문
	// 발을 위아래로 회전하거나, 옆으로 회전하는 경우가 있다.

	// 아크 탄젠트로 회전값 구함
	// 아크 탄젠트를 쓰면 해당 방향으로 회전하는 방향을 구할 수 있음
	// 원래는 Y(우측)에서 Z(위)로 방향으로 옆구르기(Roll)처럼 되지만
	// 역 탄젠트여서 Z(위)에서 Y(우측)으로 가는 것이 됨(roll이 됨)
	float roll = UKismetMathLibrary::DegAtan2(normal.Y, normal.Z);
	// 삼각함수의 항등식 정리에서 보면 한쪽이 뒤집어진다, 그래서 -로 뒤집음
	// -> -하는 이유는 왼손 좌표계에서 회전은 시계 방향으로 이루어져야 하므로 pitch는 회전 각도를 뒤집음
	float pitch = -UKismetMathLibrary::DegAtan2(normal.X, normal.Z);

	OutRotation = FRotator(pitch, 0.0f, roll);
}



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


	// 간격을 구해줌
	float leftDistance; // 왼발과 지면 붙은 곳의 간격
	FRotator leftRotation;
	Trace(LeftSocket, leftDistance, leftRotation);

	float rightDistance; // 오른발과 지면 붙은 곳의 간격
	FRotator rightRotation;
	Trace(RightSocket, rightDistance, rightRotation);

	// 왼발과 오른발 중 높이가 가장 낮은 값을 선택
	float offset = FMath::Min(leftDistance, rightDistance);
	Data.PelvisDistance.Z = UKismetMathLibrary::FInterpTo(Data.PelvisDistance.Z, offset, DeltaTime, InterpSpeed);


	// offset이 -라면 LeftDistance가 내려가고, +라면 RightDistance가 내려감
	// -> 각 발에 offset을 빼준다.

	// FInterpTo는 이동 보간이고, RInterpTo는 회전보간이다.
	// 발이 현재 있는 위치에서 발이 닫는 위치까지 흘러가는 시간에 따라 보간
	// 1 Current : 현재, 2 Target : 지점, 3 DeltaTime : 부드럽게 해주기 위한 흘러가는 시간, 4 : 속도
	// Data.LeftDistance.X은 0이니까, 0부터 간격까지 이동 보간 해서 다시 넣어준다.
	Data.LeftDistance.X = UKismetMathLibrary::FInterpTo(Data.LeftDistance.X, (leftDistance - offset), DeltaTime, InterpSpeed);
	// right는 left에 대칭되서 -값이다.
	Data.RightDistance.X = UKismetMathLibrary::FInterpTo(Data.RightDistance.X, -(rightDistance - offset), DeltaTime, InterpSpeed);

	// RInterpTo : 회전값을 보간해줌
	Data.LeftRotation = UKismetMathLibrary::RInterpTo(Data.LeftRotation, leftRotation , DeltaTime, InterpSpeed);
	Data.RightRotation = UKismetMathLibrary::RInterpTo(Data.RightRotation, rightRotation, DeltaTime, InterpSpeed);
}



* ABP_CPlayer 수정

2BoneIK처럼 위치를 움직이는 것이 아닌, 
본을 회전 시킬 것이어서 "본 트랜스폼(변경)"으로 처리한다.

 

* ABP_CPlayer AnimGraph



BoneToModify(적용할 본) : Foot_L

Rotation만 씀
Rotation Mode : Add to Existing(보간되면서 서서히 변경시킴) 
Rotation Space : WorldSpace(본을 회전할때도 WorldSpace로 넣어주는 것이 편함(월드 공간에서 적용한다고 받아들이자))

* "본 트랜스폼(변경)" 하나 더 만들어서 BoneToModify(적용할 본) : Foot_R로 설정, 나머지 적용은 이전꺼와 동일

 

* 결과를 보면 발이 경사에 잘 회전되는 것을 볼 수 있음

 

 

 

 

 

 

 

 

 

 

 

CActionComponent.cpp 수정된 부분


더보기
...

void UCActionComponent::SetUnarmedMode()
{
	// 이미 무기가 장착되어 있다면 해제하고
	if (!!Datas[(int32)Type])
	{
		ACEquipment* equipment = Datas[(int32)Type]->GetEquipment();
		if (!!equipment)
			equipment->Unequip();
	}

	ChangeType(EActionType::Unarmed);

}

void UCActionComponent::SetMode(EActionType InType)
{
	// 수행할꺼 수행하고 ChangeType을 통해 바꿔주고 델리게이션 콜 됨

	// 이전 타입과 현재 타입이 같다면 칼을 넣어주는 동작
	if (Type == InType) // 같은 무기 해제
	{
		SetUnarmedMode();

		return;
	}

	// 다른 무기 장착 부분
	else if (IsUnarmedMode() == false)
	{
		ACEquipment* equipment = Datas[(int32)Type]->GetEquipment();
		CheckNull(equipment);

		// 기존 무기를 해제시킴
		equipment->Unequip();
	}

	if (!!Datas[(int32)InType])
	{
		// 새로운 타입 무기 장착
		ACEquipment* equipment = Datas[(int32)InType]->GetEquipment();
		CheckNull(equipment);

		equipment->Equip();
	}

	// 상태 바꿔줌
	ChangeType(InType);
}

 

 

 

 

 

 

 

CDoAction_Throw.cpp 수정된 부분


더보기
...

void ACDoAction_Throw::Begin_DoAction()
{
	FVector location = OwnerCharacter->GetMesh()->GetSocketLocation("Hand_Throw_Projectile");

	// 플레이어의 전방 or 적의 전방
	FRotator rotator = OwnerCharacter->GetController()->GetControlRotation();

	FTransform transform = Datas[0].EffectTransform;
	transform.AddToTranslation(location);
	transform.SetRotation(FQuat(rotator));

	FActorSpawnParameters params;
	params.Owner = OwnerCharacter; // 스폰한 액터
	// FActorSpawnParameters안에 ESpawnActorCollisionHandlingMethod가 있음
	// ESpawnActorCollisionHandlingMethod : 현재 Spawn하려는 위치에 다른액터가 있을때 어떻게 처리할지를 결정하는 열거형
	// 위치에 스폰되면서 얘가 자동으로 충돌을 한다. 그래서 그 자리에 있으면
	// Undefined : 스폰시키지 않음
	// AlwaysSpawn : 있든 없든 충돌 무시하고, 무조건 스폰 시켜라
	// AdjustIfPossibleButAlwaysSpawn : 위치를 조정한다음 스폰시켜라
	// AdjustIfPossibleButDontSpawnIfColliding : 충돌되었다면 조정해서 스폰시켜라
	// DontSpawnIfColliding : 아예 스폰시키지 않겠다.
	// params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

	// CThrow의 GetWorld()로 사용해도 무방하나 캐릭터가 배치되어 있는 월드에 스폰
	// SpawnActor() 함수는 반드시 스폰한다고 보장하지 않음
	// ACThrow* throwObject = OwnerCharacter->GetWorld()->SpawnActor<ACThrow>(Datas[0].ThrowClass, transform, params);

	// 그래서 반드시 스폰되는 함수를 사용
	ACThrow* throwObject = OwnerCharacter->GetWorld()->SpawnActorDeferred<ACThrow>(Datas[0].ThrowClass, transform, OwnerCharacter, NULL, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

	throwObject->OnThrowBeginOverlap.AddDynamic(this, &ACDoAction_Throw::OnThrowBeginOverlap);
	// 등장 확정 처리
	UGameplayStatics::FinishSpawningActor(throwObject, transform);
}

 

 

CPlayer.h 추가된 내용


더보기
...

UCLASS()
class UONLINE_04_ACTION_API ACPlayer : public ACharacter, public IICharacter, public IGenericTeamAgentInterface
{
	GENERATED_BODY()
    
    ...
    
    
private:
	UPROPERTY(VisibleDefaultsOnly)
		class UCFeetComponent* Feet;
    
private:
	void OnZoom(float InAxis);
};

 

 

 

 

 

 

 

CPlayer.cpp 추가된 내용


더보기
...

ACPlayer::ACPlayer()
{
	...
	CHelpers::CreateActorComponent<UCFeetComponent>(this, &Feet, "Feet");
}

void ACPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	...
	PlayerInputComponent->BindAxis("Zoom", this, &ACPlayer::OnZoom);
}

void ACPlayer::OnZoom(float InAxis)
{
	// DeltaTime을 곱해줘서 일정 속도 유지
	SpringArm->TargetArmLength += (1000.0f * InAxis * GetWorld()->GetDeltaSeconds());
	
	// 0은 완전 중심부로 와버려서, 50으로 함
	SpringArm->TargetArmLength = FMath::Clamp(SpringArm->TargetArmLength, 50.0f, 500.0f);
}

 

 

CFeetComponent.h


더보기
#pragma once

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

// 움직일 값
USTRUCT(BlueprintType)
struct FFeetData
{
	GENERATED_BODY()

	// BlueprintReadOnly : 읽기 전용으로
	// EditAnywhere : 애니메이션에서 디버깅(프리뷰)이 가능하도록
	// float형으로 사용해도 되지만, BP에서 귀찮아져서 vector형으로 사용
	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FVector LeftDistance; // 왼발에 대한 왼발이 떨어져 있을때, 떨어진 곳과의 간격(X축만 사용)

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FVector RightDistance; // 오른발과 떨어진 곳과의 간격(X축만 사용)

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FVector PelvisDistance; // 허리(Z축만 사용)

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FRotator LeftRotation; // 왼발에 대한 회전

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FRotator RightRotation; // 오른발에 대한 회전
};


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

protected:
	// BP에서 수정가능하도록
	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
		FName LeftSocket = "Foot_L";

	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
		FName RightSocket = "Foot_R";

	// 뛰는 것은 실제 바닥에서 좀 떨어져 있어야 한다.
	// 근데 이 값이 너무 크면 지면을 추적을 해서 발이 위에 떨어져 있어야 하는데 붙게됨
	// 너무 크면 점프할때도 달라붙는 느낌이 남(-> 적당히 조정하면 된다.)
	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
		float TraceDistance = 55.0f; // 추적할 간격(거리)

	// TEnumAsByte : 자료형의 크기를 알 수 있도록(바이트 크기로 명시함)
	// 그냥 enum은 크기를 알 수 없기 때문에
	// For One Frame : 매프레임 마다 그림
	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
		TEnumAsByte<EDrawDebugTrace::Type> DrawDebugType = EDrawDebugTrace::None;
	
	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
		float InterpSpeed = 17.0f; // 보간 속도

	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "InverseKinemetics")
		float OffsetDistance = 5.0f; // 발과 땅사이에 보정간격

public:
	// 복사 방지
	FORCEINLINE const FFeetData& GetData() { return Data; }


public:
	UCFeetComponent();

protected:
	virtual void BeginPlay() override;

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

private:
	// 발의 본은 굳이 소켓하지 않고 본으로 하면 된다.
	// 발의 본과 지면의 붙은 곳의 거리를 구하는 내부함수
	// Trace : 해당 소켓을 추적하여 소켓과 충돌 지점의 거리를 구해서 리턴해주는 함수
	void Trace(FName InSocket, float& OutDistance, FRotator& OutRotation);

private:
	class ACharacter* OwnerCharacter;
	FFeetData Data;

};

 

 

 

 

 

 

 

CFeetComponent.cpp


더보기
#include "CFeetComponent.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "Components/CapsuleComponent.h"

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; // 왼발과 지면 붙은 곳의 간격
	FRotator leftRotation;
	Trace(LeftSocket, leftDistance, leftRotation);
	
	float rightDistance; // 오른발과 지면 붙은 곳의 간격
	FRotator rightRotation;
	Trace(RightSocket, rightDistance, rightRotation);

	// 왼발과 오른발 중 높이가 가장 낮은 값을 선택
	float offset = FMath::Min(leftDistance, rightDistance);
	Data.PelvisDistance.Z = UKismetMathLibrary::FInterpTo(Data.PelvisDistance.Z, offset, DeltaTime, InterpSpeed);
	
	
	// offset이 -라면 LeftDistance가 내려가고, +라면 RightDistance가 내려감
	// -> 각 발에 offset을 빼준다.

	// FInterpTo는 이동 보간이고, RInterpTo는 회전보간이다.
	// 발이 현재 있는 위치에서 발이 닫는 위치까지 흘러가는 시간에 따라 보간
	// 1 Current : 현재, 2 Target : 지점, 3 DeltaTime : 부드럽게 해주기 위한 흘러가는 시간, 4 : 속도
	// Data.LeftDistance.X은 0이니까, 0부터 간격까지 이동 보간 해서 다시 넣어준다.
	Data.LeftDistance.X = UKismetMathLibrary::FInterpTo(Data.LeftDistance.X, (leftDistance - offset), DeltaTime, InterpSpeed);
	// right는 left에 대칭되서 -값이다.
	Data.RightDistance.X = UKismetMathLibrary::FInterpTo(Data.RightDistance.X, -(rightDistance - offset), DeltaTime, InterpSpeed);

	// RInterpTo : 회전값을 보간해줌
	Data.LeftRotation = UKismetMathLibrary::RInterpTo(Data.LeftRotation, leftRotation , DeltaTime, InterpSpeed);
	Data.RightRotation = UKismetMathLibrary::RInterpTo(Data.RightRotation, rightRotation, DeltaTime, InterpSpeed);
}

void UCFeetComponent::Trace(FName InSocket, float& OutDistance, FRotator& OutRotation)
{
	OutDistance = 0.0f;

	// GetSocketLocation() : 소켓이 가진 상대위치가 아닌 월드 위치를 리턴해줌
	FVector location = OwnerCharacter->GetMesh()->GetSocketLocation(InSocket);

	// GetActorLocation()은 해당 액터의 중심점의 위치를 리턴한다.
	// 중심점(허리)으로 부터 다리까지 다리를 지나가도록 선을 그을 것이다.
	// X(앞뒤), Y(좌우)는 발의 좌우 앞뒤, Z는 허리서부터 내려오는 높이(즉 허리 높이)
	// 즉, start는 허리높이에 발의 x,y 위치를 갖는 것이 된다.)
	FVector start = FVector(location.X, location.Y , OwnerCharacter->GetActorLocation().Z);

	// GetScaledCapsuleHalfHeight() : 캡슐의 스케일을 적용한 상태의 1/2 크기를 리턴
	// 캐릭터의 크기 절반 만큼인 캡슐의 절반을 빼주면 발바닥의 위치가 되고, TraceDistance(추적할 간격)만큼 더 빼주면 발밑으로 내려간다.
	float traceZ = start.Z - OwnerCharacter->GetCapsuleComponent()->GetScaledCapsuleHalfHeight() - TraceDistance;
	
	// 결국 end는 발바닥(소켓)의 좌우, 앞뒤의 위치를 가지고 높이는 발밑 TraceDistance 만큼 내려간다.
	FVector end = FVector(location.X, location.Y, traceZ);

	// LineTraceSingle에서 아무것도 안붙이는 이 함수는
	// LineTraceByChannel과 같다.
	// 4번 인자는 Channel, UEngineTypes::ConvertToTraceType()을 사용하면 CollisionChannel로 추적하게 된다.
	// ECC_Visibillity : 화면에 등장한거 전부다 추적해라(지면은 보이는 것이니까, 지면마다 채널이 다르게 지정되어있을 수 있어서)
	// 5번 인자 Complex : 복합 콜리젼 여부(True로 할시 느림, 삼각형 단위 체크) -> But 더 정밀한 경과 나옴, IK에서는 정밀하게 계산해야 해서 필요
	// -> 그래서 IK는 느리다.(정밀하게 추적해야 하니까)
	// Complex : 삼각형 단위로 추적 만약 false로 만들면 정밀하지 않게 구하므로 발의 높이가 정확히 나오지 않을 가능성이 있음


	// 플레이어 캐릭터 추적 무시
	TArray<AActor*> ignoreActors;
	ignoreActors.Add(OwnerCharacter);

	// 추적하는 곳의 색상은 녹색, 충돌되는 색상은 빨간색으로 설정
	FHitResult hitResult;
	UKismetSystemLibrary::LineTraceSingle(GetWorld(), start, end, UEngineTypes::ConvertToTraceType(ECC_Visibility), true, ignoreActors, DrawDebugType, hitResult, true, FLinearColor::Green, FLinearColor::Red);

	// IsValidBlockingHit : 추적된 물체가 하나라도 있을 경우 true를 리턴
	// 즉 충돌된게 없다면 빠져나옴
	CheckFalse(hitResult.IsValidBlockingHit());

	// OutDistance -> 발이 움직일 높이 간격 구함
	// .Size()로 구한 이유는 간격이어서 길이로 구함
	// hitResult.TraceEnd, TraceDistance는 원래 발의 위치로 옮겨줌
	// ImpactPoint 발이 이동할 위치로 잡아줌
	// OffsetDistance : 발의 보정값을 줌
	float length = (hitResult.ImpactPoint - hitResult.TraceEnd).Size();
	OutDistance = OffsetDistance + length - TraceDistance;

	// hit된 면에 수직 벡터
	FVector normal = hitResult.ImpactNormal;
	// normal를 이용해서 회전을 2개 찾아옴
	// Roll 전방축 회전(옆구르기 회전), Pitch 우측 벡터 기준 회전(위아래 회전), Yaw(좌,우 회전)은 필요없음, 왼쪽이나 오른쪽으로 발을 회전하지 않기 떄문
	// 발을 위아래로 회전하거나, 옆으로 회전하는 경우가 있다.

	// 아크 탄젠트로 회전값 구함
	// 아크 탄젠트를 쓰면 해당 방향으로 회전하는 방향을 구할 수 있음
	// 원래는 Y(우측)에서 Z(위)로 방향으로 옆구르기(Roll)처럼 되지만
	// 역 탄젠트여서 Z(위)에서 Y(우측)으로 가는 것이 됨(roll이 됨)
	float roll = UKismetMathLibrary::DegAtan2(normal.Y, normal.Z);
	// 삼각함수의 항등식 정리에서 보면 한쪽이 뒤집어진다, 그래서 -로 뒤집음
	// -> -하는 이유는 왼손 좌표계에서 회전은 시계 방향으로 이루어져야 하므로 pitch는 회전 각도를 뒤집음
	float pitch = -UKismetMathLibrary::DegAtan2(normal.X, normal.Z);

	OutRotation = FRotator(pitch, 0.0f, roll);
}

 

 

CAnimInstance.h 추가된 내용


더보기
...

UCLASS()
class UONLINE_04_ACTION_API UCAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
	

protected:
	...
	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FFeetData FeetData; // IK에 관한 정보(발과 타겟과의 간격 정보가 담겨 있음)
};

 

 

 

 

 

 

 

CAnimInstance.cpp 수정된 내용


더보기
...

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

	ACharacter* character = Cast<ACharacter>(TryGetPawnOwner());
	CheckNull(character);

	Speed = character->GetVelocity().Size2D();
	Direction = CalculateDirection(character->GetVelocity(), character->GetControlRotation());

	UCFeetComponent* feet = CHelpers::GetComponent<UCFeetComponent>(character);
	if (!!feet)
		FeetData = feet->GetData();
}

 

 

 

 

 

결과