- C++ 코드카타
- Ch3 팀프로젝트 기획
- Ch3 개인 프로젝트
장애물로, 회전 통나무와 회전 도끼 시스템을 추가하려고 한다.
먼저 회전 통나무는 다음과 같이 동작한다.

모든 회전 통나무는 Tick으로 회전값을 갱신해줄 수 있지만, RotatingMovementComponent를 활용해서 코드를 좀 더 간결하게 유지했다.
void ARotationLog::BeginPlay()
{
Super::BeginPlay();
if (bIsRollRotation) // Roll 회전 전용이라면?
{
RotatingMovement->RotationRate = FRotator(0.f, 0.f, RotationSpeed);
}
else // Yaw 회전 전용이라면?
{
RotatingMovement->RotationRate = FRotator(0.f, RotationSpeed, 0.f);
}
}
그리고 회전 도끼는 도끼가 1개/3개/5개의 장애물로 분리해서 상황에 맞게 배치하고 싶었으나, 해결하지 못해서 하드 코딩의 경향이 좀 있지만 다음과 같이 구성했다.
// ARotationAxe.h
USTRUCT(BlueprintType)
struct FRotationAxeData
{
GENERATED_BODY()
// 0~4 중 사용할 피벗 슬롯 인덱스
UPROPERTY(EditDefaultsOnly, Category = "Axe", meta = (ClampMin = "0", ClampMax = "4", UIMin = "0", UIMax = "4"))
int32 PivotIndex{0};
// 최대 스윙 각도
UPROPERTY(EditAnywhere, Category = "Swing Settings")
float MaxSwingAngle{75.0f};
// 스윙 속도
UPROPERTY(EditAnywhere, Category = "Swing Settings")
float SwingSpeed{2.0f};
// 스윙 시작 타이밍
UPROPERTY(EditAnywhere, Category = "Axe")
float TimeOffset{0.0f};
};
private:
static constexpr int32 MaxPivotCount = 5;
UPROPERTY(EditDefaultsOnly, Category = "Axe")
TArray<FRotationAxeData> RotationAxes;
// 생성자에서 미리 만든 피벗 슬롯
UPROPERTY(VisibleDefaultsOnly, Category = "Axe")
TArray<TObjectPtr<USceneComponent>> PivotSlots;
// ARotationAxe.cpp
ARotationAxes::ARotationAxes()
{
/* ... */
PivotSlots.Reserve(MaxPivotCount);
for (int32 PivotIndex = 0; PivotIndex < MaxPivotCount; ++PivotIndex)
{
// 피벗을 미리 생성
const FName PivotName(*FString::Printf(TEXT("Pivot%d"), PivotIndex + 1));
USceneComponent* NewPivot = CreateDefaultSubobject<USceneComponent>(PivotName);
if (RootComponent)
{
NewPivot->SetupAttachment(RootComponent);
}
else
{
RootComponent = NewPivot;
}
PivotSlots.Add(NewPivot);
}
}
void ARotationAxes::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
const float CurrentTime = GetWorld()->GetTimeSeconds();
// 배열에 등록된 모든 도끼를 순회하며 회전
for (const FRotationAxeData& AxeData : RotationAxes)
{
if (!PivotSlots.IsValidIndex(AxeData.PivotIndex))
{
continue;
}
USceneComponent* PivotComponent = PivotSlots[AxeData.PivotIndex];
if (IsValid(PivotComponent))
{
// 도끼 개별 오프셋을 더해서 사인 그래프 계산
const float CurrentAngle = FMath::Sin((CurrentTime + AxeData.TimeOffset) * AxeData.SwingSpeed) * AxeData.MaxSwingAngle;
const FRotator NewRotation = FRotator(CurrentAngle, 0.0f, 0.0f);
PivotComponent->SetRelativeRotation(NewRotation, true);
}
}
}
이렇게 한 후에 블루프린트 내부에서 메시 및 충돌체를 추가하고 피벗과 도끼 데이터를 추가했다.

결과는 다음과 같이 처리된다.

기존의 AltF4와 조금은 달리, 특정 Trigger Volume을 밟으면 AI가 끝까지 쫓아가서 플레이어를 공격하는 메커니즘을 만드려고 했다.
다만, 단순한 공격 애니메이션을 수행하면 플레이어에게 영향을 줄 수 없기 때문에 애니메이션 도중에 특정 위치와 회전을 기반으로 캐릭터가 회전하는 Motion Warping 기능을 사용하기로 했다.
적 메시와 애니메이션, 기본 이동 로직은 다 구현한 후에 Hit Montage를 좀 수정하려고 한다.
(참고로, Motion Warping 기능을 사용하려면 플러그인에서 "Motion Warping"을 추가해야 한다.)

위 이미지처럼 위치/회전 보정을 하고 싶은 부분에 Motion Warping Notify를 추가하고

루트 모션 수식어로 애니메이션의 스케일을 조정하는 "Scale"과 위치/회전을 보정하는"Skew Warp"가 있다. 여기선 후자를 사용하고 워프할 타겟의 이름, Rotation Type을 Facing으로 맞추고 다 기본 값으로 세팅했다.
이후 공격 함수에서 다음과 같이 코드를 추가했다.
// 공격 함수 내부
if (MotionWarpingComponent)
{
// 공격할 타겟의 씬 컴포넌트를 찾고
USceneComponent* TargetRoot = TargetActor ? TargetActor->GetRootComponent() : nullptr;
if (TargetRoot)
{
// 씬 컴포넌트를 기반으로 모션 워핑을 수행한다.
MotionWarpingComponent->AddOrUpdateWarpTargetFromComponent(
TEXT("AttackTarget"),
TargetRoot,
NAME_None,
true
);
}
}
다음과 같이 플레이어의 위치에 따라서 적의 위치 및 회전이 보정되는 모습을 볼 수 있다.
(블러드 이펙트와 UI는 간단하게 배치했다.)
