Skip to content

05_Combat_Skill_Animation

github-actions[bot] edited this page Oct 19, 2026 · 3 revisions

05. Combat, Skill & Animation

Sonheim의 전투는 스킬 사용 가능 여부와 비용을 서버에서 검사하고, Montage Notify가 정한 시점에 효과·타격 판정을 실행합니다. 스킬 설정은 FSkillData, 보유·시전 상태는 USonheimSkillComponent, 실제 실행은 UBaseSkill이 담당하며, 타격 결과는 FAttackData → FCustomDamageEvent → TakeDamage 경로로 처리합니다.

전투 시연과 핵심 구현

Montage의 SkillFire·근접 공격 구간·행동 상태 Notify

Montage에서 발동·타격 구간·행동 가능 상태 전환 시점을 지정합니다. 타격은 Notify로 활성화한 판정 구간에서 서버가 수행합니다.

스킬 사용부터 타격 결과까지 — 플레이 영상

05_skill_cast_to_damage.mp4

공격 입력 이후 Montage 재생, 타격 판정과 피격 반응으로 이어지는 전투 흐름을 보여줍니다.

구분 담당 코드 구현 내용
정의 데이터 FSkillData / FAttackData 스킬 클래스, Phase별 비용, Montage, 타격 형태·데미지
허용 여부·요약 상태 USonheimSkillComponent 기본 보유·Grant 검증, SkillSpecs FastArray
스킬 실행 UBaseSkill CanCast → Activate → Fire → Complete/Cancel, 쿨타임
애니메이션 시점 AnimNotify / NotifyState 발사·타격 구간·행동 가능 상태 전환
근접 판정 UMeleeAttack Socket 기반 Sweep, 보간, 중복 타격 방지
데미지 AAreaObject::CalcDamage / TakeDamage 피아식별, 속성·약점·방어, HP/스태미나, 효과
네트워크 FastArray + UBaseSkill 서브오브젝트 스킬 요약 상태와 실행 객체의 복제 상태 구분

목차


스킬 실행 흐름

flowchart TB
    INPUT["플레이어 입력 / AI 요청"] --> TRY["USonheimSkillComponent<br/>TryCastSkillById"]
    TRY --> SERVER["서버 허용 여부 · CanCast 검사"]
    DEF["FSkillData<br/>비용 · Montage · AttackData"] --> SERVER
    SERVER --> ACT["UBaseSkill::Activate<br/>OnActivate 비용 적용"]
    ACT --> SPEC["SkillSpecs 상태 갱신"]
    ACT --> MONTAGE["MultiCast_CastSkill<br/>Montage 재생"]
    MONTAGE --> NOTIFY["AnimNotify / NotifyState"]
    NOTIFY --> FIRE["서버 UBaseSkill::Fire<br/>OnFire 비용 적용"]
    NOTIFY --> WINDOW["UMeleeAttack<br/>판정 구간 시작·종료"]
    WINDOW --> TICK["서버 Tick · Socket Sweep"]
    TICK --> DAMAGE["CalcDamage → FCustomDamageEvent"]
    DAMAGE --> TARGET["TakeDamage<br/>HP · Stamina · 피격 효과"]
    MONTAGE --> FINISH["Complete / Cancel<br/>OnComplete 비용 · CoolTime"]
    FINISH --> SPEC
Loading

TryCastSkillById는 클라이언트에서 Server_TryCastSkill을 요청하고, 서버는 IsSkillAllowed와 CanCastSkill을 검사합니다. 승인되면 OnServerSkillActivated로 상태를 갱신하고 MultiCast_CastSkill로 Montage를 재생합니다.

AAreaObject::Tick은 서버에서 현재 UBaseSkill::Tick을 호출합니다. 따라서 근접 공격은 Montage Notify가 판정 구간을 열고, 서버 Tick이 해당 구간의 충돌을 검사하는 구조입니다.


스킬 정의·상태·실행 객체

DataTable — FSkillData

FSkillData : FTableRowBase는 여러 스킬이 공통으로 갖는 정의 필드입니다.

// SonheimGameType.h — 주요 필드 발췌
struct FSkillData : public FTableRowBase
{
    int SkillID = 0;
    TSubclassOf<UBaseSkill> SkillClass;
    TArray<FSkillStaminaCost> StaminaCosts;
    TArray<FSkillItemCost> ItemCosts;
    float CastRange = 0.f;
    float CoolTime = 0.f;
    UAnimMontage* Montage = nullptr;
    TArray<FAttackData> AttackData;
    int NextSkillID = 0;
};

한 행의 SkillClass가 실행 로직을 정하고, Montage와 AttackData가 공격 시점·범위·데미지를 연결합니다. NextSkillID는 연계 스킬을 지정합니다. 행 구조와 에셋 분류 기준은 03. Data & Content Architecture에서 설명합니다.

SkillSpecs — 보유·시전 요약 상태

USonheimSkillComponent는 다음 FSonheimSkillSpecItem을 FastArray로 관리합니다.

struct FSonheimSkillSpecItem : public FFastArraySerializerItem
{
    int32 SkillId = 0;
    int32 Level = 1;
    bool bIsCasting = false;
    float CooldownEndTime = 0.0f;
};

OnServerSkillActivated, OnServerSkillCooldownStart, OnServerSkillCancelled가 서버에서 엔트리를 갱신하고 MarkItemDirty를 호출합니다.

UBaseSkill — 실행과 Phase

UBaseSkill은 CanCast·Activate·Fire·Complete·Cancel을 제공하고, UMeleeAttack 등의 파생 클래스가 타격 방식을 구현합니다.

Ready
  ↓ Activate
Casting
  ↓ Fire
PostCasting
  ↓ Complete
CoolTime
  ↓ 쿨타임 종료
Ready

Cancel → CoolTime

UBaseSkill의 Phase는 실행 상태이며, SkillSpecs는 UI·네트워크에 필요한 시전 여부와 쿨타임 종료 시각을 요약합니다.


기본 스킬과 장비 Grant

스킬 인스턴스의 생성·재사용

USonheimSkillComponent::EnsureSkillInstance는 SkillId에 해당하는 객체가 있으면 재사용하고, 없다면 DataTable의 SkillClass로 UBaseSkill을 생성해 SkillInstances에 보관합니다.

// SonheimSkillComponent.cpp — 생성부 발췌
const FString NameStr = FString::Printf(TEXT("BaseSkill_%d"), SkillId);
UBaseSkill* NewSkill =
    NewObject<UBaseSkill>(OwnerArea, Data->SkillClass, *NameStr);
NewSkill->InitSkill(Data);
SkillInstances.Add(SkillId, NewSkill);

생성 시점은 다음과 같습니다.

  • 기본 보유 스킬: AAreaObject::BeginPlay에서 m_OwnSkillIDSet을 순회하며 EnsureSkillInstance 호출
  • 장비 등으로 부여한 스킬: GrantSkills / ReplaceGrant 시점에 생성·캐시
  • 추가 조회: GetSkillByID / TryCastSkillById에서도 필요하면 EnsureSkillInstance 호출

즉, 모든 스킬을 사용 직전에만 생성하는 완전한 지연 생성 방식은 아닙니다. 보유 스킬 집합·Grant를 기준으로 준비하면서 누락된 객체를 필요 시 생성하는 구조입니다.

GrantId와 RefCount

장비를 변경할 때 다른 경로로 부여받은 스킬까지 해제하지 않도록 Grant를 구분합니다.

TMap<FGuid, TArray<int32>> GrantsById;
TMap<int32, int32> GrantRefCounts;
무기 A ─────┐
            ├── Skill 101 · RefCount 2
다른 Grant ┘

무기 A 해제 → RefCount 1 · 사용 허용 유지
마지막 Grant 해제 → RefCount 0 · 사용 허용 목록에서 제외

USonheimSkillComponent::IsSkillAllowed는 기본 OwnedSkillIds 또는 GrantRefCounts로 사용 가능 여부를 결정합니다. ReplaceGrant는 특정 Grant의 스킬 목록만 바꿉니다.

Grant의 RefCount가 0이 되면 사용 권한을 회수합니다. 캐시된 SkillInstances UObject는 즉시 파괴하지 않으므로 인스턴스 수명과 스킬 사용 권한을 따로 관리합니다.


스킬 사용 조건과 Phase별 비용

서버 실행 조건

UBaseSkill::CanCast는 현재 Phase가 Ready인지, OnActivate 단계의 스태미나·아이템을 지불할 수 있는지, 대상이 사거리 안인지 확인합니다. AAreaObject::CanCastSkill은 다른 스킬이 실행 중인지도 검사합니다.

Server_TryCastSkill
    ↓ IsSkillAllowed
CanCastSkill
    ↓ Current Skill / Ready / Cost / Range
UBaseSkill::Activate
    ↓ ApplyCosts(OnActivate)
SkillSpecs 갱신 + Montage 재생

스킬은 서버에서 사용 조건을 다시 검사하고 Activate가 성공할 때 시전 상태로 전환합니다.

OnActivate·OnFire·OnComplete

FSkillStaminaCost와 FSkillItemCost는 비용을 소모할 Phase를 지정합니다.

Phase 발생 시점 사용 사례
OnActivate Activate 스킬 시작 시 스태미나·아이템 비용
OnFire Fire 발사 시점 탄약·소모 아이템
OnComplete Complete 동작 완료 시점의 후속 비용

CheckCosts는 비용을 실제로 차감하지 않고 충족 여부를 검사합니다. ApplyCosts는 서버 권한 경로에서 비용을 실제로 적용합니다.

ApplyCosts는 같은 Phase에서 여러 아이템 중 일부를 차감한 뒤 실패하면 그때까지 차감한 아이템을 AddItem으로 돌려줍니다. 단, 앞서 소모한 스태미나까지 함께 되돌리지는 않으므로 전체 비용에 대한 원자적 트랜잭션은 아닙니다.

OnFire 비용 적용에 실패하면 Fire는 Cancel을 호출합니다. 실패·취소 후에는 스킬 실행 상태와 쿨타임을 처리합니다.


Montage와 AnimNotify

타격 프레임·취소 가능 구간을 C++ 고정 타이머에만 의존하지 않고 Animation 타임라인에 배치한 Notify로 제어합니다.

Notify 실제 역할
USkillFireNotify 현재 스킬의 Fire 시점 전달. 서버면 직접 호출, 클라이언트면 Server_NotifySkillFire 요청
UMeleeAttackNotifyState 서버에서 근접 판정 구간 시작·종료 및 공격 데이터 인덱스 지정
USetPlayerStateNotify NORMAL / ACTION / CANACTION 등의 플레이어 행동 상태 전환
UAddConditionNotify Invincible 등 Condition 적용
Montage
 ├─ 행동 시작 / ACTION
 ├─ SkillFireNotify → Fire
 ├─ MeleeAttackNotifyState Begin → 타격 구간
 │     └─ Server Tick → ProcessHitDetection
 ├─ MeleeAttackNotifyState End → 판정·LOD 복원
 ├─ CANACTION → 연계 입력 / 회피 가능 상태
 └─ NORMAL → 일반 조작 복원

위 순서는 타임라인에서 구성할 수 있는 역할의 예시이며, 모든 Montage에 동일한 Notify 배치가 강제되는 것은 아닙니다.

특히 USkillFireNotify는 Authority에서 Fire를 직접 실행하고, 비권한 측에서는 현재 스킬 ID를 서버에 전달합니다. 근접 판정 NotifyState의 SetCasterMesh와 ResetCollisionData는 서버에서만 처리합니다.

행동·취소 가능 구간

USetPlayerStateNotify는 ASonheimPlayer::SetPlayerState를 호출해 공격 중 이동·다음 행동 제한을 조정합니다. CANACTION과 NORMAL 전환 시에는 ClearCurrentSkill도 호출합니다.

이처럼 Animation은 언제 행동 제한을 풀지를 결정하고, 캐릭터 상태와 FActionRestrictions가 실제로 어떤 입력이 가능한지를 관리합니다. 플레이어 상태 규칙은 04. Player & Character Systems에서 다룹니다.


근접 공격의 판정과 프레임 보간

NotifyState가 서버 판정 구간을 연다

UMeleeAttackNotifyState::NotifyBegin이 AttackDataIndex를 이용해 UMeleeAttack::SetCasterMesh를 호출합니다. 이후 AAreaObject::Tick에서 호출되는 UMeleeAttack::Tick이 해당 인덱스의 ProcessHitDetection을 실행합니다. NotifyEnd는 판정 데이터를 정리합니다.

flowchart TB
    BEGIN["NotifyBegin · AttackDataIndex"] --> SETUP["SetCasterMesh<br/>Source Mesh · Socket · LOD 설정"]
    SETUP --> LOOP["서버 Tick<br/>ProcessHitDetection"]
    LOOP --> TRACE["Line / Sphere / Capsule / Box"]
    TRACE --> FILTER["HitActors · ProcessedActors<br/>중복 제거"]
    FILTER --> DMG["CalcDamage"]
    ENDING["NotifyEnd / Cancel / Complete"] --> RESET["ResetCollisionData<br/>LOD 복원 · 판정 종료"]
    LOOP -. "판정 창 유지" .-> LOOP
Loading

FHitBoxData는 판정 방식, 메시 컴포넌트 태그, 시작·끝 Socket, 크기와 보간 여부를 정의합니다. 지정된 MeshComponentTag로 무기 Mesh의 Socket도 선택할 수 있습니다.

EHitDetectionType 실제 호출
Line LineTraceMultiByChannel
Sphere SweepMultiByChannel / Sphere
Capsule SweepMultiByChannel / Capsule
Box SweepMultiByChannel / Box

빠른 공격의 프레임 간격 보완

빠른 무기 Swing은 프레임 사이에 Socket이 크게 움직입니다. 현재 프레임에서만 Sweep하면 이전 위치와 현재 위치 사이를 건너뛰는 판정이 생길 수 있습니다.

이전 프레임 Socket ● ─ ─ ─ X(대상) ─ ─ ─ ● 현재 프레임

bUseInterpolation = true
이전 Socket ● ─ ● ─ ● ─ ● ─ 현재 Socket
               중간 위치별 추가 Sweep

UMeleeAttack::ProcessHitDetection은 bUseInterpolation이 켜져 있고 이전 Socket 위치가 유효하면 시작·끝 위치와 회전을 InterpolationSteps만큼 보간해 단계별 충돌 검사를 수행합니다. 첫 프레임처럼 이전 위치가 없는 경우에는 현재 위치에서 검사합니다.

// MeleeAttack.cpp — 주요 로직 발췌
const int32 Steps = FMath::Max(2,
    AttackData->HitBoxData.InterpolationSteps);
for (int32 i = 1; i <= Steps; ++i)
{
    const float Alpha = float(i) / Steps;
    const FVector Start = FMath::Lerp(PreviousStart, CurrentStart, Alpha);
    const FVector End = FMath::Lerp(PreviousEnd, CurrentEnd, Alpha);
    // 보간 위치·회전에서 PerformCollisionCheck(...)
}

실제 필드명과 판정 흐름에 맞춘 설명용 축약 코드입니다. 실행 가능한 원본 함수는 아래 소스 링크에서 확인할 수 있습니다.

다중 Sweep의 중복 타격 방지

  • ProcessedActors: 현재 ProcessHitDetection 호출에서 같은 Actor를 중복 처리하지 않음
  • AttackCollision::HitActors: 하나의 Notify 판정 구간 전체에서 이미 타격한 Actor를 제외

따라서 보간 단계 수가 늘어나더라도 동일 판정 창에서 같은 대상에 중복 데미지가 들어가지 않도록 처리합니다. 판정 창이 새로 열리면 별도 기록을 사용합니다.

Socket 정확도와 LOD

서버는 화면에 보이지 않는 캐릭터라도 타격 Socket 위치가 필요합니다.

// AreaObject.cpp
GetMesh()->VisibilityBasedAnimTickOption =
    EVisibilityBasedAnimTickOption::AlwaysTickPoseAndRefreshBones;
GetMesh()->bEnableUpdateRateOptimizations = false;

또한 근접 판정 창에서 Source SkeletalMesh에 SetForcedLOD(1)을 적용해 가장 높은 LOD(LOD0)의 Socket 위치를 사용하고, ResetCollisionData에서 이전 LOD 설정으로 복원합니다.

이는 서버의 타격 판정 정확도를 높이는 대신 Animation·Bone 갱신과 추가 Sweep 비용을 부담하는 선택입니다.

근접 공격 Socket 보간과 Hit Shape 디버그 화면

무기 Socket의 이동 경로를 따라 수행하는 근접 공격 충돌 판정을 디버그 화면에서 확인할 수 있습니다.


데미지 계산과 피격 처리

공격의 타격 정보는 FAttackData에 정의합니다.

// SonheimGameType.h — 주요 필드 발췌
struct FAttackData
{
    float HealthDamageAmountMin = 0.f;
    float HealthDamageAmountMax = 0.f;
    float StaminaDamageAmount = 0.f;
    EElementalAttribute AttackElementalAttribute;
    FHitBoxData HitBoxData;
    bool bEnableHitStop = false;
    float HitStopDuration = 0.1f;
    float KnockBackForce = 0.f;
    // VFX / SFX 필드 등
};

타격에 성공하면 AAreaObject::CalcDamage가 공격 가능 대상인지 먼저 검사하고, 공격자의 데미지 보정 및 동일 속성 보너스를 적용한 뒤 FCustomDamageEvent에 FAttackData·HitInfo·계산한 Damage를 담아 대상의 TakeDamage에 전달합니다.

flowchart TB
    HIT["UMeleeAttack / Projectile / Boss Strike"] --> CALC["AAreaObject::CalcDamage<br/>IFF 검사 · 기본 피해 · 동일 속성 보너스"]
    CALC --> EVENT["FCustomDamageEvent<br/>FAttackData + HitInfo + Damage"]
    EVENT --> TARGET["AAreaObject::TakeDamage<br/>서버 권한 · 피아식별 · 생존/상태 검사"]
    TARGET --> REACTION["Custom Damage 확인<br/>Hit Stop · 넉백"]
    REACTION --> MITIGATE["방어 보정 → 약점 ×1.5<br/>속성 상성 배율"]
    MITIGATE --> HP["Health / Stamina 갱신<br/>사망 판정"]
    HP --> FEEDBACK["MulticastDamageEffect<br/>피격 숫자 · VFX/SFX"]
Loading

CalcDamage의 동일 속성 보너스는 공격자의 방어 속성 목록에 공격 속성이 있으면 1.2배를 적용하는 현재 코드 기준입니다. 대상 TakeDamage는 별도로 방어 보정·약점(1.5배)·대상의 방어 속성에 대한 상성 배율을 적용합니다.

Hit Stop과 넉백은 TakeDamage 내부에서 FCustomDamageEvent 정보를 확인한 뒤 처리하며, 이후 실제 HP·Stamina 갱신과 데미지 표시·효과를 전달합니다. 단순 float 데미지뿐 아니라 공격의 맥락을 Target에 보존하는 구조입니다.

보스의 ABossMonster::LandStrike도 FAttackData와 기존 CalcDamage를 사용합니다. 보스가 추가하는 책임은 어떤 패턴을 언제 어디에 실행할지이며, 실제 대상 피해 처리 경로는 공유합니다. 10. Boss Encounter Runtime 참고.


스킬 상태의 네트워크 동기화

스킬 상태는 SkillSpecs FastArray와 UBaseSkill 서브오브젝트의 두 경로로 복제합니다.

경로 복제 필드·대상
USonheimSkillComponent::SkillSpecs SkillId, Level, bIsCasting, CooldownEndTime — FastArray
AAreaObject::ReplicateSubobjects → UBaseSkill m_CurrentPhase, m_CurrentCoolTime, m_TargetPos, m_NextSkillID — 실행 객체 프로퍼티
AAreaObject::MultiCast_CastSkill 스킬 사용 Montage 재생 — 순간 연출

EnsureSkillInstance는 BaseSkill_<ID>의 안정적인 UObject 이름을 사용하고, AAreaObject::GetSubobjectsWithStableNamesForNetworking에서 스킬 객체를 등록합니다. 실제 복제는 AAreaObject::ReplicateSubobjects에서 Channel->ReplicateSubobject를 호출합니다.

서버는 시전 권한을 검증하고, FastArray·서브오브젝트 복제는 지속 상태를 전달하며, Multicast는 Montage 연출을 시작합니다. 복제 범위는 12. Multiplayer Synchronization에서 다룹니다.


구현 선택과 비용

선택 구현상 이점 관리 비용·주의점
정의 / 실행 객체 / 복제 상태 분리 DataTable로 서로 다른 스킬을 구성하고 서버 실행·화면 상태 구분 SkillSpecs·UBaseSkill 사이 일부 상태 중복
기본 스킬 선생성 + 필요 시 인스턴스 확보 보유 스킬과 Grant 스킬을 재사용 Grant 권한 변화와 캐시된 객체의 수명 구분
GrantId + RefCount 여러 출처가 같은 스킬을 제공해도 개별 회수 Grant ID·카운트 일관성
Phase별 비용 시작·발사·완료 시점을 분리해 소모 스태미나 포함 전체 원자적 롤백은 아님
Montage Notify 공격 프레임과 입력·Cancel Window를 애니메이션에서 조절 서버 Anim/Bone 갱신과 Notify 타이밍 검증
프레임 간 Socket 보간 빠른 Swing의 타격 누락 위험 감소 추가 Sweep·중복 제거·디버깅 비용
동일한 Damage Context 플레이어·몬스터·보스의 피해 처리 공유 AttackData의 범위·Socket·Index 설정 검증 필요

Montage Notify는 공격 시점을 지정하며, 서버의 스킬·피해 처리 경로가 비용과 타격 결과를 확정합니다.

관련 코드 및 문서

Clone this wiki locally