DirectX11/Eternal Return 모작

[DirectX 11 Eternal Return 모작] 11. 스킬 시스템

Vfly 2025. 9. 3. 01:27

전체 코드 : https://github.com/HyangRim/DirectX11-Engine-Client

 

GitHub - HyangRim/DirectX11-Engine-Client

Contribute to HyangRim/DirectX11-Engine-Client development by creating an account on GitHub.

github.com


스킬 시스템과 쿨타임 관리

1. 스킬 메타데이터 시스템 설계

1.1 스킬 타입 정의

먼저 스킬의 기본적인 타입들을 정의

// SkillConfig.h
enum class SkillTargetType
{
    None,           // 타겟 불필요 (비앙카 R)
    Single,         // 단일 타겟 (니키 R)
};

enum class SkillCastType
{
    Instant,        // 즉시 발동
    Channeling,     // 채널링 (니키 Q처럼)
};

struct SkillMetaData
{
    SkillTargetType targetType = SkillTargetType::None;
    SkillCastType castType = SkillCastType::Instant;
    float range = 0.0f;
    bool canCastWhileMoving = false;    // 스킬 중 이동할 수 있는지
};

 

1.2 캐릭터별 스킬 설정 시스템

각 캐릭터별로 스킬 메타데이터를 체계적으로 관리하는 SkillConfig 구현

(사실 이 부분도 아이템과 마찬가지로 외부 파일에 미리 정의해 둔 다음 불러오는 방식이 더 효율 적일듯?)

// SkillConfig.cpp
void SkillConfig::InitializeConfigs()
{
    // Nicky 스킬 설정
    s_skillConfigs[L"Nicky"] = {
        // Q 스킬 - 차징, 타겟 불필요
        {SkillTargetType::None, SkillCastType::Channeling, 15.0f, false},
        // W 스킬 - 즉시, 타겟 불필요  
        {SkillTargetType::None, SkillCastType::Instant, 0.0f, false},
        // E 스킬 - 즉시, 타겟 불필요
        {SkillTargetType::None, SkillCastType::Instant, 10.0f, false},
        // R 스킬 - 즉시, 단일 타겟 필요
        {SkillTargetType::Single, SkillCastType::Instant, 20.0f, false}
    };

    // Bianca 스킬 설정
    s_skillConfigs[L"Bianca"] = {
        // Q 스킬 - 즉시, 지역 타겟
        {SkillTargetType::None, SkillCastType::Instant, 10.0f, false},
        // W 스킬 - 즉시, 타겟 불필요
        {SkillTargetType::None, SkillCastType::Instant, 0.0f, false},
        // E 스킬 - 채널링, 타겟 불필요  
        {SkillTargetType::None, SkillCastType::Channeling, 8.0f, false},
        // R 스킬 - 즉시, 이동하면서 시전 가능
        {SkillTargetType::None, SkillCastType::Instant, 15.0f, true}
    };
}

 

이 시스템의 장점은 데이터 기반 설계로 각 스킬의 특성을 명확히 정의하고, 런타임에서 쉽게 조회할 수 있다는 것

 

2. 스킬 인터페이스와 쿨타임 시스템

2.1 스킬 기본 인터페이스

모든 스킬이 공통으로 구현해야 하는 ISkill 인터페이스를 정의

// ISkill.h
class ISkill
{
public:
    virtual ~ISkill() = default;

    // 스킬 실행 관련
    virtual bool CanExecuteSkill() const = 0;
    virtual void ExecuteSkill() = 0;

    // 쿨다운 관련
    virtual float GetCurrentCooldown() const = 0;
    virtual float GetMaxCooldown() const = 0;
    virtual bool IsOnCooldown() const = 0;
    virtual void StartCooldown() = 0;
    virtual void UpdateCooldown(float deltaTime) = 0;

    // 스킬 정보
    virtual int GetSkillIndex() const = 0;
    virtual const wstring& GetSkillName() const = 0;
    virtual void PlaySkill() = 0;

    // 스킬 레벨 정보
    virtual void SkillLevelUp() = 0;
    virtual int GetCurSkillLevel() = 0;

protected:
    int m_curSkillLevel = 0;
    int m_maxSkillLevel = 5;
};

 

2.2 쿨타임 관리 시스템

각 스킬은 독립적인 쿨타임을 가지며, 매 프레임마다 업데이트 된다.

// 스킬 구현 예시 (NickyQSkill)
void UpdateCooldown(float deltaTime) override
{
    if (m_currentCooldown > 0.0f)
    {
        m_currentCooldown -= deltaTime;
        m_currentCooldown = max(0.0f, m_currentCooldown);
    }
}

bool IsOnCooldown() const override
{
    return m_currentCooldown > 0.0f;
}

 


스킬 사용 후 남은 쿨타임이 GamuHUDPanelUI에 표시됨.

 

 

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

니키 스킬 시연 영상

 

 

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

비앙카 스킬 시연 영상

 

 

3. 채널링 vs 즉시발동 스킬 처리

3.1 채널링 스킬 구현 (니키 Q 스킬, 비앙카 E 스킬)

채널링 스킬은 사용자 입력을 지속적으로 감지하고, 중도 취소가 가능

// PlayerStateMachine.cpp에서 채널링 스킬 처리
void PlayerStateMachine::HandleSkillInput()
{
    // Q 스킬 (채널링)
    if (INPUT->GetButton(KEY_TYPE::Q))
    {
        const auto& skillMeta = SkillConfig::GetSkillMetaData(m_characterIndex, 0);
        
        if (skillMeta.castType == SkillCastType::Channeling)
        {
            // 채널링 시작 또는 지속
            if (!IsInState(PlayerStateType::Skill_1))
            {
                RequestStateChange(PlayerStateType::Skill_1);
                if (m_animationStateMachine)
                    m_animationStateMachine->RequestStateChange(AnimationStateType::Skill_1);
            }
        }
    }
    
    // Q 키 해제 시 채널링 종료
    if (INPUT->GetButtonUp(KEY_TYPE::Q) && IsInState(PlayerStateType::Skill_1))
    {
        CheckQSkillCompletion();
    }
}

 

3.2 즉시발동 스킬 구현

즉시발동 스킬은 한 번의 키 입력으로 바로 실행

// W, E, R 스킬 (즉시발동)
if (INPUT->GetButtonDown(KEY_TYPE::W))
{
    const auto& skillMeta = SkillConfig::GetSkillMetaData(m_characterIndex, 1);
    
    if (skillMeta.castType == SkillCastType::Instant)
    {
        RequestStateChange(PlayerStateType::Skill_2);
        if (m_animationStateMachine)
            m_animationStateMachine->RequestStateChange(AnimationStateType::Skill_2);
        
        OnSkillUsed(1, nullptr);  // 즉시 스킬 실행
    }
}

 

3.3 동적 타겟 추적 시스템 ( 니키 R 스킬 )

리그 오브 레전드의 바이의 R 스킬과 유사

void NickyRSkill::UpdateTargetPosition() {
    Vec3 currentTargetPos = m_target->GetTransform()->GetPosition();
    float distanceMoved = Vec3::Distance(currentTargetPos, m_lastTargetPos);
    
    if (distanceMoved < 0.1f) return; // 불필요한 계산 방지
    
    // 부드러운 전환: 기존 목표점과 새 목표점을 보간
    Vec3 newTargetPos = playerPos + direction * moveDistance;
    m_targetPos = Utils::Lerp(m_targetPos, newTargetPos, 0.3f);
}

 

 

 

4. 스킬 관리 시스템

4.1 State Machine 기반 스킬 관리 시스템

PlayerStateMachine 과 AnimationStateMachine을 분리하여 로직과 비주얼을 독립적으로 관리

class PlayerStateMachine : public Component {
    // 스킬 상태 관리
    void RequestStateChange(PlayerStateType newState);
    bool CanChangeState(PlayerStateType newState);
    
    // 각 스킬별 완료 체크 Delegate
    Delegate::Delegate<bool&> OnQSkillCompleted;
    Delegate::Delegate<bool&> OnWSkillCompleted;
    Delegate::Delegate<bool&> OnESkillCompleted;
    Delegate::Delegate<bool&> OnRSkillCompleted;
};

 

특징

  • 스킬 상태 전환의 명확한 규칙 정의
  • 애니메이션과 게임 로직의 완전한 분리
  • 각 상태별 CanTransitionTo() 메서드로 안전한 상태 전환 보장

 

4.2 Delegate패턴 기반 이벤트 시스템

스킬 완료 감지를 위해 Delegate 를 이용

// 스킬 완료 감지 예시
m_player->GetPlayerStateMachine()->OnQSkillCompleted.Push([this](bool& completed) {
    completed = IsQSkillCompleted();
});

// NickyQState에서 완료 상태 확인
auto qState = dynamic_pointer_cast<NickyQState>(currentState);
if (qState) {
    return qState->IsSkillComplete();
}

 

 

4.3 애니메이션 시퀀스 동적 관리

복잡한 스킬 애니메이션을 동적으로 구성할 수 있는 시스템 구현

// 차징 시간에 따른 동적 시퀀스 생성
void NickyAnimQState::ReleaseSkill() {
    vector<wstring> rushSequence = { L"Skill_01_Rush", L"Skill_01_End" };
    vector<float> rushDurations = { 
        min(m_chargeTime, 5.0f),  // 차징 시간만큼 재생
        (13.f / 25.f) / m_playSpeed 
    };
    
    // 런타임에서 시퀀스 생성 및 재생
    m_cachedAnimator->CreateSequence(L"Dynamic_Rush_Sequence", 
                                     rushSequence, rushDurations, false);
    m_cachedAnimator->PlaySequence(L"Dynamic_Rush_Sequence");
}

 

특징 

  • 런타임 시퀀스 생성으로 유연한 스킬 표현
  • 재생 속도 배수 적용 가능
  • 애니메이션 완료 감지 및 자동 정리

 

5. 성능 최적화

5.1 중복 처리 방지 시스템

// 각 스킬별 완료 체크 플래그 관리
bool m_qSkillCompletionChecked = false;

void CheckQSkillCompletion() {
    if (IsInState(PlayerStateType::Skill_1)) {
        if (m_qSkillCompletionChecked) return; // 이미 체크함
        
        bool qSkillCompleted = false;
        OnQSkillCompleted(qSkillCompleted);
        
        if (qSkillCompleted) {
            m_qSkillCompletionChecked = true; // 플래그 설정
            // 상태 전환 로직...
        }
    } else {
        m_qSkillCompletionChecked = false; // 상태 벗어나면 리셋
    }
}

 

5.2 스킬 피해 중복 방지

// 각 스킬별 완료 체크 플래그 관리
bool m_qSkillCompletionChecked = false;

void CheckQSkillCompletion() {
    if (IsInState(PlayerStateType::Skill_1)) {
        if (m_qSkillCompletionChecked) return; // 이미 체크함
        
        bool qSkillCompleted = false;
        OnQSkillCompleted(qSkillCompleted);
        
        if (qSkillCompleted) {
            m_qSkillCompletionChecked = true; // 플래그 설정
            // 상태 전환 로직...
        }
    } else {
        m_qSkillCompletionChecked = false; // 상태 벗어나면 리셋
    }
}

 

 

 

 

결론

시스템의 장점과 특징

  1. 데이터 기반 설계
    1. 메타데이터 중심  : 스킬 특성을 데이터로 분리하여 밸런스 조정이 용이
    2. 캐릭터별 독립성 : 각 캐릭터마다 고유한 스킬 설정 가능
  2. 확장성
    1. UI 리소스 등록
    2. Delegate 바인딩
  3. 실시간 상태 관리
    1. 상태머신 연동 : 플레이어와 애니메이션 상태머신이 유기적으로 연결
    2. 프레임별 업데이트 : 쿨타임과 채널링 상태를 실시간으로 관리
  4. 성과
    1. 8개의 서로 다른 스킬 (니키 4개, 비앙카 4개) 안정적 구현