미리보기


구현내용
해당 전투시스템은 기본적으로 세키로의 전투시스템을 오마주합니다
그래서 상대가 처형 가능 상태가 된다면
기본공격을 사용하면 처형 모션이 나가고 적을 처형시킵니다
기본적으로 처형 이후에는 적이 LifePoint가 한개 사라지며
LifePoint가 0이되면 적은 사망합니다
사용 클래스
| 사용한 클래스 | 사용 목적 |
| InteractionComponent(액터컴포넌트) | 사용자가 인터렉션을 사용하기위한 액터컴포넌트 처형 이라는 존재의 유무를 가지고있는 컴포넌트 |
| EnemyBaseCharacter(캐릭터) | 처형을 당하는 적 캐릭터 |
| MainCharacter(캐릭터) | 처형을 실행하는 플레이어 |
구현C++
처형 시스템을 구현하기에 앞서, 처형이 가능한 대상의 조건을 먼저 명확히 정의하는 과정이 필요했습니다
제가 정의한 처형 조건은 이와 같습니다
- 전투 상태 조건 ( 적이 Stragger (탈진) 혹은 GuardBreak (가드브레이킹) 혹은 Idle (전투상태가아닌 일반 비전투모드) 인경우
- 적의 현재 체력이 최대 체력의 22% 이하일 경우
- 적과 처형자 사이의 거리가 일정거리가 충족된 경우
- 적과 처형자의 각도 조건이 일정각도가 충족된 경우
UFUNCTION()
EExecutionDirection CanBeExecutedBy(ACharacter* executor, float maxDistance = 200.f, float maxAngle = 45.f) const;
EExecutionDirection AEnemyBaseCharacter::CanBeExecutedBy(ACharacter* executor, float maxDistance, float maxAngle) const
{
if (!executor)
return EExecutionDirection::None;
if(mainCharacter->combatMode == false)
return EExecutionDirection::None;
//처형조건
// 체력이 22퍼 이하일경우 , 거리가 충족될경우 , 각도가 충족될경우 , Enemy의 조건이 충족될경우 (Straggle, GuardBreak, Idle)
//체력 체킹
if ((currentHp / maxHp) > 0.22f)
return EExecutionDirection::None;
// 2. 거리 체킹
float distance = FVector::Dist(executor->GetActorLocation(), GetActorLocation());
if (distance > maxDistance)
return EExecutionDirection::None;
//3. enemy의 조건 충족
if (!(currentState == EEnemyBaseState::Stagger || currentState == EEnemyBaseState::GuardBreak || currentState == EEnemyBaseState::Idle))
return EExecutionDirection::None;
//4. 각도체킹
FVector toExecutor = executor->GetActorLocation() - GetActorLocation();
toExecutor.Z = 0.f;
if (toExecutor.IsNearlyZero())
return EExecutionDirection::None;
toExecutor.Normalize();
FVector forward = GetActorForwardVector();
forward.Z = 0.f;
forward.Normalize();
float angleDeg = FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(FVector::DotProduct(forward, toExecutor), -1.f, 1.f)));
FVector cross = FVector::CrossProduct(forward, toExecutor);
if (angleDeg <= maxAngle)
return EExecutionDirection::Front;
if (angleDeg >= 180.f - maxAngle)
return EExecutionDirection::Back;
return EExecutionDirection::None;
}
적의 BaseState 는 이와 같습니다
UENUM(BlueprintType)
enum class EEnemyBaseState : uint8
{
Idle UMETA(DisplayName = "Idle"),
Patrol UMETA(DisplayName = "Patrol"),
Combat UMETA(DisplayName = "Combat"),
WeaponDraw UMETA(DisplayName = "WeaponDraw"),
Guard UMETA(DisplayName = "Guard"),
Stagger UMETA(DisplayName = "Stagger"),
GuardBreak UMETA(DisplayName = "GuardBreak"),
Execution UMETA(DisplayName = "Execution"),
Dead UMETA(DisplayName = "Dead")
};
플레이어가 처형을 시도했을 때
주변 적들중 처형 가능한 적을 탐색하고 가장 가까운 적을 대상으로 처형을 시작하는 함수를 정의했습니다
void UUInteractionComponent::TryExecuteEnemy()
{
if (!mainChar)
return;
if (mainChar->bIsExecutionAnimationing)
return;
UWorld* world = mainChar->GetWorld();
if (!world)
return;
TArray<AActor*> actors;
UGameplayStatics::GetAllActorsOfClass(world, AEnemyBaseCharacter::StaticClass(), actors);
TArray<AEnemyBaseCharacter*> nearbyEnemies;
for (AActor* actor : actors)
{
if (AEnemyBaseCharacter* enemy = Cast<AEnemyBaseCharacter>(actor))
{
EExecutionDirection execDir = enemy->CanBeExecutedBy(mainChar);
if (execDir != EExecutionDirection::None)
nearbyEnemies.Add(enemy);
}
}
if (nearbyEnemies.Num() == 0)
return;
nearbyEnemies.Sort([this](const AEnemyBaseCharacter& a, const AEnemyBaseCharacter& b)
{
float distA = FVector::Dist(mainChar->GetActorLocation(), a.GetActorLocation());
float distB = FVector::Dist(mainChar->GetActorLocation(), b.GetActorLocation());
return distA < distB;
});
currentExecutingEnemy = nearbyEnemies[0];
if (!currentExecutingEnemy)
return;
EExecutionDirection execDir = currentExecutingEnemy->CanBeExecutedBy(mainChar);
bool bFrontExecution = execDir == EExecutionDirection::Front;
int32 rowIdx = bFrontExecution ? 0 : 1;
FCharacterAnimDataTable* executeRow = mainChar->executeRows[rowIdx];
if (!executeRow)
return;
FVector targetLoc;
FRotator targetRot;
if (bFrontExecution)
{
targetLoc = currentExecutingEnemy->GetActorLocation() + currentExecutingEnemy->GetActorForwardVector() * 50.f;
targetRot = (currentExecutingEnemy->GetActorLocation() - targetLoc).Rotation();
}
else
{
targetLoc = currentExecutingEnemy->GetActorLocation() - currentExecutingEnemy->GetActorForwardVector() * 30.f;
targetRot = (currentExecutingEnemy->GetActorLocation() - targetLoc).Rotation();
}
targetLoc.Z += mainChar->GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
if (motionWarpComp)
motionWarpComp->AddOrUpdateWarpTargetFromLocationAndRotation(FName("ExecutionTarget"), targetLoc, targetRot);
if (mainChar->bIsExecuting)
return;
//sound
mainChar->soundSubsystem->PlaySound("Execution", this, mainChar->GetActorLocation());
mainChar->ExecutionStartWidgetNoti();
currentExecutingEnemy->ExecuteEnemy(mainChar, bFrontExecution);
mainChar->PlayExecution(bFrontExecution);
}
플레이어가 처형을 시도하면 정해진 위치에 자연스럽게 접근하는 방법은 모션워핑 방식을 채용하였습니다
사용법은 이와 같습니다
2025.04.12 - [Unreal 프로젝트 다이어리/첫번째 프로젝트] - Unreal - 모션워핑(Motion Warping)
Unreal - 모션워핑(Motion Warping)
현재 공격하거나 스킬을 사용하면 애니메이션이 나오는데애니메이션 도중 방향을 꺾을수가없다그래서 적이 지나갔는데도 애니메이션은 계속 나오니조작감도 별로고 너무 딱딱한느낌이 든다.
lucodev.tistory.com
처형자인 플레이어 그리고 처형대상인 적에서 처형 / 처형당함 을 담당하는 함수를 정의하였습니다
우선 적의 처형을 담당하는 함수입니다
처형의 기본 방향은 두가지
앞 처형과 뒤 처형이 존재합니다
그래서 뒷방향을 인덱스 0 앞 방향을 인덱스 1 로 가정하고
해당 인덱스에 맞는 애니메이션을 애니메이션 담당 데이터테이블에서 호출하여 불러와 사용하도록 하였습니다
void AEnemyBaseCharacter::ExecuteEnemy(ACharacter* executor, bool bFrontExecution)
{
if (!executor)
return;
EExecutionDirection execDir = CanBeExecutedBy(executor);
if (execDir == EExecutionDirection::None)
return;
currentExecutor = executor;
UCapsuleComponent* capsule = GetCapsuleComponent();
capsule->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore);
//life point 차감
lifePoint--;
if (lifePoint <= 0)
EnemyDie();
TArray<FEnemyAnimationDataTable*>* executionRows = animTypeRows.Find(EEnemyAnimType::Execution);
if (!executionRows || executionRows->Num() == 0)
return;
// 앞/뒤 row 선택 (기본 0: 뒤, 1: 앞)
int32 rowIdx = 0;
if (bFrontExecution)
rowIdx = 1; // 앞은 두 번째
FEnemyAnimationDataTable* selectedAnim = (*executionRows)[rowIdx];
if (!selectedAnim || !selectedAnim->animMontage)
return;
UAnimInstance* animIst = GetMesh()->GetAnimInstance();
if (!animIst)
return;
FOnMontageEnded endExecuteDel;
endExecuteDel.BindUObject(this, &AEnemyBaseCharacter::OnExecutionMontageEnded);
animIst->Montage_Play(selectedAnim->animMontage, selectedAnim->playRate);
animIst->Montage_SetEndDelegate(endExecuteDel, selectedAnim->animMontage);
}
플레이어 또한 동일합니다
void AMainCharacter::PlayExecution(bool bFrontExecution)
{
if (bIsExecuting && bIsExecutionAnimationing)
return;
if (executeRows.Num() == 0)
return;
int32 rowIdx = bFrontExecution ? 0 : 1;
if (!executeRows.IsValidIndex(rowIdx))
return;
bIsExecuting = true;
bIsExecutionAnimationing = true;
FCharacterAnimDataTable* row = executeRows[rowIdx];
UAnimInstance* animIst = GetMesh()->GetAnimInstance();
if (!animIst)
return;
FOnMontageEnded executeEndDel;
executeEndDel.BindUObject(this, &AMainCharacter::OnExecutionMontageEnded);
float cameraPush = bFrontExecution ? 50.f : 70.f;
PushCameraAction(cameraPush, 0.2f);
animIst->Montage_Play(row->usingAnimation);
animIst->Montage_SetEndDelegate(executeEndDel, row->usingAnimation);
}
여기서부턴 연출에 대한 고민을 많이 했습니다
어떻게 하면 밋밋한 모션에 액션감을 더해줄수있을까 라는 주제로 말입니다
그래서 선택한 방식은 이와 같습니다
- 처형을 할때 플레이어의 카메라는 앞으로 튀어나간다 (끝나면 원상복구)
- 적을 처형할때 카메라쉐이크 기능을 사용한다
카메라쉐이크를 담당하는 함수는 PlayerController에서 정의하였습니다
단순히 UPROPERTY()로 지정된 한개의 카메라 쉐이크만 사용되는것이 아닌
FName으로 판별되며 여러가지로 원하는 스케일로 사용할수있도록 범용성 넓게 설계하였습니다
UPROPERTY(EditAnywhere, Category="Camera Shake")
TMap<FName, TSubclassOf<UCameraShakeBase>> cameraShakes;
UFUNCTION()
void PlayCameraShakeCallName(FName shakeName, float scale = 1.f);
void AMainCharacterController::PlayCameraShakeCallName(FName shakeName, float scale)
{
if (!cameraShakes.Contains(shakeName))
return;
TSubclassOf<UCameraShakeBase> ShakeClass = cameraShakes[shakeName];
if (ShakeClass && PlayerCameraManager)
PlayerCameraManager->StartCameraShake(ShakeClass, scale);
}
Camera Shake Base는 이와같이 생성이 가능합니다

웨이브 오실레이터 카메라 셰이크를 베이스로 값을 설정해두고 할당하였습니다
또한 카메라를 앞으로 튀어나가게하고싶었는데 단순히 카메라를 앞으로 이동시키면 해결되지않았습니다
그래서 채택한 방식은 스프링 - 댐퍼 방식을 사용하였습니다
스프링 댐퍼방식이란 간단하게
용수철 + 쇼크 업소버 를 흉내낸 개념입니다
Tick Rate를 사용하지않고 타이머로 간단하게 설계하였습니다
void AMainCharacter::PushCameraAction(float pushOffset, float duration)
{
if (!mainCamera)
return;
FVector cameraStartLoc = mainCamera->GetRelativeLocation();
FVector cameraTargetLoc = cameraStartLoc + FVector(pushOffset, 0.f, 0.f);
float deltaTime = 0.016f;
float stiffness = 600.f;
float damping = 15.f;
int32 steps = FMath::CeilToInt(duration / deltaTime);
int32* currentStep = new int32(0);
mainCamera->GetWorld()->GetTimerManager().SetTimer(th_ExecutionCameraPushHandle,
[this, cameraStartLoc, cameraTargetLoc, deltaTime, stiffness, damping, steps, currentStep]()
{
if (*currentStep >= steps)
{
mainCamera->SetRelativeLocation(cameraTargetLoc);
mainCamera->GetWorld()->GetTimerManager().ClearTimer(th_ExecutionCameraPushHandle);
delete currentStep;
CameraVelocity = FVector::ZeroVector;
return;
}
FVector currentLoc = mainCamera->GetRelativeLocation();
FVector displacement = cameraTargetLoc - currentLoc;
CameraVelocity += displacement * stiffness * deltaTime;
CameraVelocity *= FMath::Clamp(1.f - damping * deltaTime, 0.f, 1.f);
FVector newLoc = currentLoc + CameraVelocity * deltaTime;
mainCamera->SetRelativeLocation(newLoc);
(*currentStep)++;
}, deltaTime, true);
}
처형 몽타지의 노티파이 섹션은 이와같습니다

왼쪽에 모션워핑 , 그리고 카메라 튀어나가는 연출 돌아오는 돌아오는기능 을 넣어주었습니다
코드를 짜면서 최대한 범용성 넓게 재사용성이 높게 객체지향적이게 수백가지가 될수있는 경우를 생각하며 설계하였습니다
결과
플레이어가 적을 처형 가능한 조건이 충족되면 적을 포커싱하는 위젯이 빨갛게 표시됩니다

앞 처형

뒤 처형


영상
'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글
| Unreal - 가드 (Guard) / 체간 (Posture) 시스템 (4) | 2026.01.12 |
|---|---|
| Unreal - 그래플링 훅 (0) | 2026.01.08 |
| Unreal - 회피(Dodge) (0) | 2026.01.03 |
| Unreal - 스킬창 & 연동 (0) | 2026.01.01 |
| Unreal - 버프창 (0) | 2025.12.27 |