<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>하임 님의 블로그</title>
    <link>https://ha2m.tistory.com/</link>
    <description>하임 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Wed, 29 Jul 2026 10:27:49 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>하임</managingEditor>
    <item>
      <title>내일배움캠프_Unreal/TIL - Day 154 (260713)</title>
      <link>https://ha2m.tistory.com/178</link>
      <description>&lt;h1&gt;대시 무적 + 스태미나 시스템 개편 - 흩어진 자원 로직을 설정 한 곳으로&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 대시 무적 &amp;mdash; 새 시스템 대신 패리 윈도우 패턴을 빌려왔다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회피에 무적을 넣는 방식은 여러 가지였는데, 이 프로젝트엔 이미 &lt;b&gt;패리 윈도우&lt;/b&gt;가 같은 골격을 쓰고 있었다: &lt;code&gt;AnimNotifyState&lt;/code&gt;가 몽타주 구간에서 활성 어빌리티의 hook을 부르고, 그 어빌리티가 GE로 상태 태그를 여닫는 구조. 무적도 결국 &quot;몽타주의 특정 프레임 구간 동안만 상태 태그 부여&quot;라 이 패턴을 그대로 빌려왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 베이스 어빌리티에 패리 훅과 동형으로 무적 훅을 두고, &lt;code&gt;GA_Dash&lt;/code&gt;가 override해 무적 GE를 여닫게 했다. 신규 &lt;code&gt;AnimNotifyState_IFrameWindow&lt;/code&gt;는 특정 어빌리티를 몰라도 되게 활성 어빌리티에 hook만 라우팅한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무적 판정 자체는 데미지 파이프라인의 &lt;b&gt;단일 진입점&lt;/b&gt;(&lt;code&gt;HandleIncomingDamage_Defense&lt;/code&gt;)에 한 조건만 추가하면 끝이었다. 환경 데미지 반환 직후에 두니, 낙하&amp;middot;화상(DoT)은 예외로 남기고 나머지 전투 피해만 무시하게 됐다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;// 환경(낙하 등)은 이미 위에서 반환됨
// 무적 중엔 전투 피해 전부 무시(데미지 0 &amp;rarr; BroadcastHitEvent 미호출로 넉백도 자동 스킵). DoT(화상)는 예외.
if (TargetASC-&amp;gt;HasMatchingGameplayTag(RetrieveGameplayTags::State_Player_Invincible)
    &amp;amp;&amp;amp; !SpecTags.HasTag(RetrieveGameplayTags::GameplayEvent_Attack_HitSuccess_Burn))
{
    return 0.f;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데미지를 0으로 만들면 넉백까지 공짜로 꺼지는 게 좋았다 &amp;mdash; 넉백은 &lt;code&gt;FinalDamage &amp;gt; 0&lt;/code&gt; 경로에서만 걸리기 때문. 대신 &lt;code&gt;EndAbility&lt;/code&gt;에서 무조건 무적 GE를 닫도록 해뒀다. i-frame 도중 다른 액션에 캔슬되면 무적이 영구히 남을 수 있어서다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;void UGA_Dash::EndAbility(...)
{
    // 어떤 종료 경로에서도 무적 GE 제거(i-frame 도중 캔슬 시 무적 영구 잔류 방지).
    CloseNotifyIFrameWindow();
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교훈: &quot;결과가 같은 견고한 패턴이 이미 있으면 새로 짜지 말고 빌려온다.&quot; 무적은 결국 패리 윈도우와 같은 모양이었고, 판정도 방어 로직이 모이는 한 곳에 한 줄이면 됐다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 스태미나 비용 GE를 다 지웠다 &amp;mdash; 네이티브 GE 하나 + 설정 맵&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 스태미나 소모는 어빌리티마다 &lt;code&gt;GE_Cost_Stamina_100&lt;/code&gt;, &lt;code&gt;GE_Cost_Stamina_50&lt;/code&gt;, &lt;code&gt;GE_StaminaDrain_Guard&lt;/code&gt;, &lt;code&gt;GE_StaminaRestore_Parry&lt;/code&gt; &amp;hellip; 이런 GE 에셋이 흩어져 있었다. 값 하나 바꾸려면 에셋을 일일이 열어야 해서, 밸런싱을 몇 번 하다 보니 이건 못 버티겠다 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 소모 로직을 베이스 어빌리티로 끌어올렸다. 각 어빌리티는 자기 &lt;code&gt;StaminaCostTag&lt;/code&gt; 하나만 들고, 베이스가 &lt;code&gt;CheckCost&lt;/code&gt;/&lt;code&gt;ApplyCost&lt;/code&gt;를 override해 &lt;b&gt;설정 맵에서 비용을 조회&lt;/b&gt;한다. 태그가 비면 그냥 무료다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;void URetrieveGameplayAbility::ApplyCost(...) const
{
    Super::ApplyCost(Handle, ActorInfo, ActivationInfo);
    FStaminaCostRow Row;
    if (GetStaminaCostRow(Row) &amp;amp;&amp;amp; Row.ActivationCost &amp;gt; 0.f)
        ApplyStaminaDelta(Handle, ActorInfo, ActivationInfo, -Row.ActivationCost);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 스태미나 증감에 쓰는 GE도 에셋으로 두지 않고 &lt;b&gt;네이티브 클래스&lt;/b&gt;로 만들었다. Instant + SetByCaller 모디파이어 하나라, 생성자에서 다 짤 수 있었다. 이걸로 소모&amp;middot;회복&amp;middot;가드 드레인&amp;middot;자연 회복이 전부 이 GE 하나를 재사용하고, 에디터에서 GE 에셋을 만들 필요가 사라졌다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;URetrieveStaminaCostEffect::URetrieveStaminaCostEffect()
{
    DurationPolicy = EGameplayEffectDurationType::Instant;
    FSetByCallerFloat SetByCaller;
    SetByCaller.DataTag = RetrieveGameplayTags::Data_Cost_Stamina;   // 음수=소모, 양수=회복

    FGameplayModifierInfo ModInfo;
    ModInfo.Attribute = UCombatAttributeSet::GetStaminaAttribute();
    ModInfo.ModifierOp = EGameplayModOp::Additive;
    ModInfo.ModifierMagnitude = FGameplayEffectModifierMagnitude(SetByCaller);
    Modifiers.Add(ModInfo);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덤으로 죽은 코드도 걷어냈다. 회복이 코드 주도로 바뀌면서 &lt;code&gt;StaminaRegenRate&lt;/code&gt; 어트리뷰트는 아무도 안 읽는 상태가 됐고(예전엔 회복 GE가 참조했을 수도 있는데 그마저 불투명했다), &lt;code&gt;FCharacterStats&lt;/code&gt;의 스태미나 필드와 &lt;code&gt;Data.Init.Stamina*&lt;/code&gt; 태그도 같이 제거했다. 스태미나는 적이 안 쓰는 플레이어 전용 개념이라 공용 캐릭터 스탯에 있던 게 애초에 어색했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. DT로 갈까 하다가 결국 설정 내장 맵으로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 소모값을 &lt;code&gt;DataTable&lt;/code&gt;(&lt;code&gt;DT_StaminaCost&lt;/code&gt;)에 넣었다. 그런데 소모하는 액션이 4~5개뿐이라 DT의 장점(수백 행 스프레드시트, CSV 임포트)이 하나도 안 살고, 행 키를 태그 문자열(&lt;code&gt;&quot;Ability.Player.Dash&quot;&lt;/code&gt;)로 잡다 보니 오타에 취약했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataAsset(TMap)도 후보였지만, 결국 &lt;code&gt;URetrieveStaminaSettings&lt;/code&gt;(DeveloperSettings)에 맵을 &lt;b&gt;직접&lt;/b&gt; 넣는 쪽으로 갔다. 태그 피커가 붙어 오타가 사라지고, 만들 에셋이 없어지고, 튜닝 위치가 Project Settings 한 화면으로 모인다. 이 프로젝트의 &lt;code&gt;WeaponSocketSettings&lt;/code&gt;가 이미 &lt;code&gt;config TMap&amp;lt;FGameplayTag, FStruct&amp;gt;&lt;/code&gt; 패턴을 쓰고 있어서 직렬화도 검증된 길이었다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;UPROPERTY(Config, EditAnywhere, Category = &quot;Stamina|Cost&quot;, meta = (Categories = &quot;Ability&quot;, ForceInlineRow))
TMap&amp;lt;FGameplayTag, FStaminaCostRow&amp;gt; StaminaCosts;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;FStaminaCostRow&lt;/code&gt;는 &lt;code&gt;FTableRowBase&lt;/code&gt; 상속을 남겨뒀다 &amp;mdash; 나중에 DT로 되돌릴 여지. 규모가 커지면 그때 옮기면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. &quot;전투 중에만&quot; 판정을 스탠스 태그로 짰다가 두 번 데였다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전투 중일 때만 질주가 스태미나를 먹게 하고 싶었다. 마침 &lt;code&gt;State.Player.Combat&lt;/code&gt; 태그가 있길래 그걸 조건으로 걸었다. 그런데 두 가지 증상이 나왔다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;적이 없는데 &lt;b&gt;제자리에서 공격키만 눌러도&lt;/b&gt; 직후 질주에 스태미나가 닳았다.&lt;/li&gt;
&lt;li&gt;몬스터에게 &lt;b&gt;추격당하는 중인데도&lt;/b&gt; 잠깐만 닳고 이후엔 비전투처럼 계속 회복됐다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 따라가 보니 &lt;code&gt;State.Player.Combat&lt;/code&gt;은 교전 상태가 아니라 &lt;b&gt;발검 스탠스&lt;/b&gt;였다. &lt;code&gt;CombatStanceComponent&lt;/code&gt;가 공격 입력에도 켜고(&amp;rarr; 제자리 공격 증상), 적 포착 시 1회만 켠 뒤 3.5초 디케이라 추격이 이어져도 꺼졌다(&amp;rarr; 추격 중 끊김). &quot;진짜 교전&quot;과는 다른 신호였던 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 추격 BGM을 켜는 &lt;code&gt;RetrieveMusicSubsystem&lt;/code&gt;이 이미 진짜 교전을 정확히 추적하고 있었다 &amp;mdash; 나를 포착한 몬스터 집합을 들고, 0.5초 폴링으로 죽거나 추적을 포기한 놈을 걷어내며, 집합이 비면 전투 종료로 본다. 그래서 판정을 이 신호로 갈아끼웠다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;const URetrieveMusicSubsystem* MusicSubsystem = World-&amp;gt;GetSubsystem&amp;lt;URetrieveMusicSubsystem&amp;gt;();
const bool bInCombat = MusicSubsystem &amp;amp;&amp;amp; MusicSubsystem-&amp;gt;IsCombatActive();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 김에 회복 규칙도 전투/탐험으로 갈랐다. 소모 후 회복 지연은 &lt;b&gt;전투 중에만&lt;/b&gt; 걸고, 비전투는 지연 없이 더 빠른 속도로 회복하게 했다. 전투 중엔 자원 관리가 팽팽하고, 탐험 중엔 스태미나 신경 안 쓰고 돌아다니는 게 맞다고 봤다.&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;const float RegenDelay = bInCombat ? Settings-&amp;gt;RegenDelaySeconds : 0.f;              // 지연은 전투 중에만
const float RegenRate  = bInCombat ? Settings-&amp;gt;RegenPerSecond : Settings-&amp;gt;OutOfCombatRegenPerSecond;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가드 지속 드레인도 똑같이 전투 중에만 닳게 맞췄다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교훈: 이름이 &lt;code&gt;Combat&lt;/code&gt;이라고 교전 상태인 건 아니었다. &quot;이 태그가 정확히 언제 켜지고 꺼지는가&quot;를 확인 안 하고 의미만 보고 쓴 게 실수였다. 필요한 신호(진짜 교전)는 엉뚱하게도 오디오 서브시스템이 이미 정확히 들고 있었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 상호배타 &amp;mdash; Guard는 되는데 BowAim만 안 됐던 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;달리기와 가드/조준을 동시에 못 하게, 나중에 누른 쪽이 이기도록 만들었다. 처음엔 입력 핸들러에서 처리했다: 달리기를 누르면 가드/조준을 취소하고, 가드/조준을 누르면 달리기를 끄고 &amp;mdash; 이때 입력 태그를 &lt;code&gt;Ability.Player.Guard&lt;/code&gt; / &lt;code&gt;Ability.Player.BowAim&lt;/code&gt;으로 비교했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Guard는 잘 됐는데 &lt;b&gt;BowAim만 Sprint와 완전히 무관&lt;/b&gt;하게 놀았다. 코드가 두 태그에 대해 완전히 대칭인데 하나만 안 되니, 차이는 태그 값밖에 없었다. 파고 보니 &lt;b&gt;BowAim의 입력 바인딩 태그가 &lt;code&gt;Ability.Player.BowAim&lt;/code&gt;이 아니었다&lt;/b&gt;(입력 태그는 데이터 에셋이라 코드엔 안 드러난다). 그래서 &lt;code&gt;if (InputTag == Ability_Player_BowAim)&lt;/code&gt;이 영영 안 걸렸고, 홀드 입력 해제도 대상을 못 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 태그를 알아내 맞추는 대신, &lt;b&gt;입력 태그 의존을 아예 없앴다&lt;/b&gt;. &quot;가드/조준이 달리기를 취소&quot; 방향을 각 어빌리티의 &lt;code&gt;ActivateAbility&lt;/code&gt;로 옮긴 것. 실제로 발동됐다는 건 무기 조건(&lt;code&gt;CanActivate&lt;/code&gt;)을 이미 통과했다는 뜻이라, 무기가 안 맞으면 달리기가 유지되는 것까지 공짜로 따라왔다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;// GA_Guard / GA_BowAim :: ActivateAbility (Commit 직후)
if (UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo())
    ASC-&amp;gt;SetLooseGameplayTagCount(RetrieveGameplayTags::State_Player_Sprinting, 0);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 &quot;달리기가 가드/조준 취소&quot; 방향은 &lt;b&gt;자산 태그&lt;/b&gt;로 처리했다. &lt;code&gt;CancelAbilities&lt;/code&gt;는 원래 자산 태그 기반이라 문제없었고, 홀드 입력 해제만 입력 태그 대신 자산 태그로 찾게 바꿨다. 이게 없으면 홀드형(WhileInputActive)이라 키를 계속 눌러둔 채 취소되면 다음 프레임에 바로 재발동돼 버린다(자동 재개).&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void URetrieveAbilitySystemComponent::ClearInputHeldForAbilityWithTag(const FGameplayTag&amp;amp; AbilityAssetTag)
{
    for (const FGameplayAbilitySpec&amp;amp; Spec : GetActivatableAbilities())
        if (Spec.Ability &amp;amp;&amp;amp; Spec.Ability-&amp;gt;GetAssetTags().HasTag(AbilityAssetTag))
            InputHeldSpecHandles.Remove(Spec.Handle);   // WhileInputActive 재시도 목록에서 제거 &amp;rarr; 자동 재개 차단
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교훈: 입력 태그 == 어빌리티 태그라고 &lt;b&gt;가정&lt;/b&gt;한 게 화근이었다. Guard가 우연히 그 가정을 만족해서 버그가 한동안 안 보였다. 데이터에 있는 값을 코드에서 짐작하지 말고, 아예 그 값에 의존하지 않는 축(여기선 어빌리티 발동/자산 태그)으로 옮기는 게 답이었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;❌ 이전 / 처음 시도&lt;/th&gt;
&lt;th&gt;✅ 이번 구현&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;대시 무적&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;패리 윈도우 패턴 재사용(ANS&amp;rarr;hook&amp;rarr;GE), 데미지 단일 진입점서 판정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스태미나 소모값&lt;/td&gt;
&lt;td&gt;어빌리티별 GE_Cost 에셋 분산&lt;/td&gt;
&lt;td&gt;설정 맵 1곳 + 네이티브 GE 하나(에디터 에셋 불필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;튜닝 저장소&lt;/td&gt;
&lt;td&gt;DataTable(문자열 키&amp;middot;오타 취약)&lt;/td&gt;
&lt;td&gt;DeveloperSettings 내장 맵(태그 피커)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전투 판정&lt;/td&gt;
&lt;td&gt;&lt;code&gt;State.Player.Combat&lt;/code&gt;(발검 스탠스, 오판)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MusicSubsystem::IsCombatActive()&lt;/code&gt;(진짜 교전)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;회복&lt;/td&gt;
&lt;td&gt;항상 동일&lt;/td&gt;
&lt;td&gt;전투=지연+느림 / 탐험=즉시+빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상호배타&lt;/td&gt;
&lt;td&gt;입력 태그 비교(BowAim 먹통)&lt;/td&gt;
&lt;td&gt;어빌리티 발동/자산 태그 기반(입력 태그 비의존)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무적은 &lt;b&gt;패리 윈도우 패턴을 그대로 빌려&lt;/b&gt; ANS&amp;rarr;hook&amp;rarr;GE로 배선하고, 판정은 방어 로직이 모이는 &lt;b&gt;단일 진입점에 한 줄&lt;/b&gt;. 데미지 0이면 넉백도 자동으로 꺼진다.&lt;/li&gt;
&lt;li&gt;흩어진 소모 GE 에셋을 지우고 &lt;b&gt;베이스 &lt;code&gt;CheckCost&lt;/code&gt;/&lt;code&gt;ApplyCost&lt;/code&gt; + 네이티브 GE + 설정 맵&lt;/b&gt;으로 통합. 밸런싱이 Project Settings 한 곳에서 끝난다.&lt;/li&gt;
&lt;li&gt;저장소는 규모(액션 4~5개)와 태그 키 특성 때문에 &lt;b&gt;DT보다 DeveloperSettings 내장 맵&lt;/b&gt;이 나았다. &lt;code&gt;FTableRowBase&lt;/code&gt;는 남겨 회귀 여지만 확보.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;State.Player.Combat&lt;/code&gt;은 교전이 아니라 &lt;b&gt;발검 스탠스&lt;/b&gt;였다 &amp;mdash; 태그 이름만 보고 쓰면 안 되고, 필요한 &quot;진짜 교전&quot; 신호는 이미 오디오 서브시스템(&lt;code&gt;IsCombatActive&lt;/code&gt;)에 있었다.&lt;/li&gt;
&lt;li&gt;상호배타 BowAim 버그의 뿌리는 &lt;b&gt;입력 태그 == 어빌리티 태그라는 가정&lt;/b&gt;. 데이터 값을 짐작하는 대신 그 값에 의존하지 않는 축(발동/자산 태그)으로 옮겨 해결.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내일배움캠프_Unreal/TIL</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>하임</author>
      <guid isPermaLink="true">https://ha2m.tistory.com/178</guid>
      <comments>https://ha2m.tistory.com/178#entry178comment</comments>
      <pubDate>Mon, 13 Jul 2026 21:13:32 +0900</pubDate>
    </item>
    <item>
      <title>내일배움캠프_Unreal/TIL - Day 153 (260710)</title>
      <link>https://ha2m.tistory.com/177</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;밸런싱&amp;middot;캔슬&amp;middot;개발 편의 &amp;mdash; 감도 단순화 &amp;middot; 스태프 프레일티 &amp;middot; PIE 인트로 스킵&lt;/b&gt;&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 감도 옵션 단순화 &amp;mdash; 배타 모드를 걷어내고 X/Y만&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마우스 감도에 통합/세부 배타 모드가 들어가 있었다(통합=단일 값, 세부=X/Y 따로). 그런데 직접 눌러보니 &lt;b&gt;&quot;슬라이더 하나냐 둘이냐&quot;를 고르자고&lt;/b&gt; 모드 플래그 + BP 통지 + 슬라이더 회색 처리 + 전용 핸들러 둘이 붙어 있었다. &lt;b&gt;기능 대비 UI&amp;middot;코드 표면이 과했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정작 필요한 건 X/Y를 따로 만지는 것뿐이라, 통합 경로(&lt;code&gt;bUseUnifiedSensitivity&lt;/code&gt;, 단일 &lt;code&gt;MouseSensitivity&lt;/code&gt;, 그 클램프, 모드 핸들러)를 통째로 지우고 축별 값만 남겼다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 곱셈도, 모드 분기도 없이 축별 값 그대로.
const float SensitivityX = Settings ? Settings-&amp;gt;MouseSensitivityX : 1.f;
const float SensitivityY = Settings ? Settings-&amp;gt;MouseSensitivityY : 1.f;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교훈: 선택지를 하나 더 주는 것보다, &lt;b&gt;정말 필요한 최소 UI로 수렴&lt;/b&gt;하는 게 낫다. 옵션이 단순해지고 감도 로직에서 분기가 통째로 사라졌다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. PIE 인트로 스킵 개발자 모드 &amp;mdash; 플래그를 둘로 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밸런싱은 붙였다 껐다를 수십 번 반복하는 일인데, PIE를 누를 때마다 &lt;b&gt;메인 메뉴 &amp;rarr; 진입 로딩 &amp;rarr; 오프닝 시네마틱&lt;/b&gt;을 매번 통과해야 했다. 부팅 스킵 플래그가 하나 있었지만(&lt;code&gt;bSkipMainMenuOnBoot&lt;/code&gt;) &lt;b&gt;로딩&amp;middot;시네마틱까지는 못 건너뛰었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 개발자용 스킵을 한 단계 더 두는 쪽으로 플래그를 둘로 나눔.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;if (bDeveloperSkipIntroFlow || bSkipMainMenuOnBoot)
{
    if (ARetrievePlayerController* RetrievePC = Cast&amp;lt;ARetrievePlayerController&amp;gt;(GetWorld()-&amp;gt;GetFirstPlayerController()))
        RetrievePC-&amp;gt;SetDeveloperSkipIntroFlow(bDeveloperSkipIntroFlow);   // &amp;rarr; bSkipEntryLoadingScreen
    BootstrapNewGameQuest();
    GS-&amp;gt;TransitionTo(ERetrieveSessionState::InGame);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;bDeveloperSkipIntroFlow&lt;/code&gt; &amp;mdash; &lt;b&gt;메뉴&amp;middot;진입 로딩&amp;middot;오프닝 시네마틱 전부&lt;/b&gt; 건너뛰고 바로 인게임. PC에 &quot;진입 로딩 스킵&quot;까지 전달.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bSkipMainMenuOnBoot&lt;/code&gt; &amp;mdash; 메뉴만 건너뛰고 &lt;b&gt;진입 로딩 화면은 남김.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기본값 &lt;code&gt;true &amp;rarr; false&lt;/code&gt;로 하향&lt;/b&gt;: 스킵은 개발 편의지 기본 동작이면 안 되니, 실수로 켠 채 빌드가 나가는 걸 막음.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 하나로 밸런싱 이터레이션 속도가 체감으로 확 붙었다. 개발 편의 플래그는 &lt;b&gt;명시적으로 켤 때만&lt;/b&gt; 동작하게 둔다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 전체 스탯 밸런싱 + 스태프 프레일티&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DT_CharacterStats&lt;/code&gt;를 훑으며 캐릭터 전반 스탯을 맞췄다. 손이 오래 간 건 &lt;b&gt;스태프&lt;/b&gt;. 스태프는 원거리라 근접보다 안전한데, 데미지&amp;middot;자원까지 편하면 사실상 상위호환이 돼버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;&quot;안전한 대신 무르다&quot;&lt;/b&gt;는 트레이드오프를 만들기로 하고, 스태프를 들면 최대 체력이 깎이는 프레일티 효과(&lt;code&gt;GE_StaffFrailty&lt;/code&gt;)를 만들어 스태프 어빌리티 세트(&lt;code&gt;DA_AbilitySet_Staff&lt;/code&gt;)가 &lt;b&gt;패시브로 부여&lt;/b&gt;하게 했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수치가 아니라 &lt;b&gt;무기 선택 자체에 리스크&lt;/b&gt;를 붙이는 방향 &amp;rarr; 스태프 데미지를 조금 세게 줘도 &quot;무른 대신 강한&quot; 원거리로 균형.&lt;/li&gt;
&lt;li&gt;GE를 어빌리티 세트에 실어 &lt;b&gt;데이터로 부여&lt;/b&gt;하니, 밸런스가 또 틀어져도 GE 값만 만지면 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 동작마다 캔슬 타이밍과 효과음&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강공&amp;middot;흡수(Absorb)&amp;middot;버스트 몽타주를 돌려보며 &lt;b&gt;캔슬 지점과 재생 배속(Play Rate)&lt;/b&gt;을 다시 잡았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;후딜이 길어 답답한 동작은 캔슬 창을 &lt;b&gt;앞당기고&lt;/b&gt;, 너무 빨리 끊겨 손맛이 없던 건 &lt;b&gt;늦췄다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;각 동작에 맞는 효과음도 몽타주 노티로 함께 걸어, &lt;b&gt;데미지 배율&amp;middot;소리&amp;middot;캔슬을 한 사이클에서&lt;/b&gt; 같이 맞춤.&lt;/li&gt;
&lt;li&gt;특히 버스트/콤보는 &quot;버스트로 콤보를 끊고 다시 콤보로 잇는&quot; 흐름이 어색하지 않게 창을 조율.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;❌ Before&lt;/th&gt;
&lt;th&gt;✅ After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;감도 옵션&lt;/td&gt;
&lt;td&gt;통합/세부 배타 모드(플래그&amp;middot;통지&amp;middot;핸들러)&lt;/td&gt;
&lt;td&gt;축별 X/Y만(분기 제거)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PIE 인트로&lt;/td&gt;
&lt;td&gt;매번 메뉴&amp;middot;로딩&amp;middot;시네마틱 통과&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bDeveloperSkipIntroFlow&lt;/code&gt;로 전부 스킵&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;부팅 스킵 기본값&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;(스킵 켜짐)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;(명시적으로 켤 때만)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스태프 밸런스&lt;/td&gt;
&lt;td&gt;원거리 + 준수한 스탯(상위호환)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GE_StaffFrailty&lt;/code&gt;로 최대 체력 감소(리스크)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동작 캔슬&lt;/td&gt;
&lt;td&gt;고정 후딜/배속&lt;/td&gt;
&lt;td&gt;동작별 캔슬 창&amp;middot;배속&amp;middot;효과음 조율&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;감도는 &lt;b&gt;배타 모드를 걷어내고 축별 X/Y만&lt;/b&gt; &amp;mdash; 선택지를 늘리기보다 필요한 최소 UI로 수렴, 로직 분기 제거.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;bDeveloperSkipIntroFlow&lt;/code&gt;&lt;/b&gt;로 메뉴&amp;middot;로딩&amp;middot;시네마틱을 통째로 스킵, 부팅 스킵 기본값은 &lt;code&gt;false&lt;/code&gt;로 내려 실수 방지 &amp;rarr; 밸런싱 이터레이션이 빨라짐.&lt;/li&gt;
&lt;li&gt;스태프는 수치가 아니라 &lt;b&gt;무기 선택에 리스크&lt;/b&gt;(&lt;code&gt;GE_StaffFrailty&lt;/code&gt; 최대 체력 감소)를 붙여 원거리 우위를 상쇄, GE라 데이터로 재조정 가능.&lt;/li&gt;
&lt;li&gt;캔슬은 동작별로 &lt;b&gt;창&amp;middot;배속&amp;middot;효과음을 한 사이클에서&lt;/b&gt; 조율 &amp;mdash; 값&amp;middot;노티 중심이라 반복 튜닝이 쉽다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내일배움캠프_Unreal/TIL</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>하임</author>
      <guid isPermaLink="true">https://ha2m.tistory.com/177</guid>
      <comments>https://ha2m.tistory.com/177#entry177comment</comments>
      <pubDate>Fri, 10 Jul 2026 23:26:52 +0900</pubDate>
    </item>
    <item>
      <title>내일배움캠프_Unreal/TIL - Day 153 (260709)</title>
      <link>https://ha2m.tistory.com/176</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;체감 및 타격감 변경 &amp;mdash; 감도 &amp;middot; FOV &amp;middot; 전투 SFX &amp;middot; 히트 피드백&lt;/b&gt;&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 감도 &amp;mdash; 곱셈 결합에서 배타(통합/세부) 모드로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 마우스 감도는 &lt;b&gt;&lt;code&gt;실제 감도 = 통합 배율(Master) &amp;times; 축별(X/Y)&lt;/code&gt;&lt;/b&gt; 구조였다. 둘 다 기본 1.0이라 평소엔 티가 안 났지만, X만 올리면 통합 배율까지 곱해져 &lt;b&gt;사람이 예상한 값과 실제 감도가 어긋났다&lt;/b&gt;. 노브가 둘인데 서로 곱해지는 게 문제.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌ Before&lt;/b&gt; &amp;mdash; 두 노브가 곱해짐&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const float Master = Settings ? Settings-&amp;gt;MouseSensitivity : 1.f;
const float SensitivityX = (Settings ? Settings-&amp;gt;MouseSensitivityX : 1.f) * Master;
const float SensitivityY = (Settings ? Settings-&amp;gt;MouseSensitivityY : 1.f) * Master;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ After&lt;/b&gt; &amp;mdash; 배타 모드. 통합이면 단일 값(양축 공통), 세부면 X/Y만&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;const bool bUnified = !Settings || Settings-&amp;gt;bUseUnifiedSensitivity;
const float SensitivityX = bUnified ? Settings-&amp;gt;MouseSensitivity : Settings-&amp;gt;MouseSensitivityX;
const float SensitivityY = bUnified ? Settings-&amp;gt;MouseSensitivity : Settings-&amp;gt;MouseSensitivityY;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI엔 모드 버튼(&lt;code&gt;Btn_SensMode_Unified&lt;/code&gt;/&lt;code&gt;_Detailed&lt;/code&gt;)을 붙이고, 현재 모드를 BP에 통지해 &lt;b&gt;안 쓰는 슬라이더를 회색 처리&lt;/b&gt;하도록 &lt;code&gt;OnSensitivityModeChanged&lt;/code&gt;를 열었다.&lt;/li&gt;
&lt;li&gt;어느 쪽이든 &lt;b&gt;값 하나만&lt;/b&gt; 최종 감도에 들어가서, 곱셈으로 예측이 어긋나던 문제가 사라짐.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void URetrieveSettingsPanelWidget::HandleSensModeUnified()
{
    if (bRefreshingControls) return;
    if (auto* S = GetUserSettings()) { S-&amp;gt;bUseUnifiedSensitivity = true; }
    RefreshCurrentPage();   // 슬라이더 값 갱신 + 모드 통지
    APPLY_PREVIEW(ERetrieveSettingsCategory::Controls);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. FOV &amp;mdash; ALSCamera 잔여 의존성 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FOV 조절이 예전엔 ALS 카메라 컴포넌트를 거쳤는데, 지금 구조에선 &lt;b&gt;그 경로 없이도 FOV가 먹고 있었다&lt;/b&gt;. &lt;code&gt;ALSCamera&lt;/code&gt; 모듈은 잔여 의존성만 남은 상태여서, 기능이 안 깨지는 걸 확인하고 잘라냈다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;- &quot;ALSCamera&quot;,                      // Retrieve.Build.cs 모듈 의존성
- #include &quot;AlsCameraComponent.h&quot;   // RetrieveSettingsSubsystem.cpp&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교훈: 쓰지 않는 카메라 모듈을 링크만 하고 있으면 빌드&amp;middot;의존성 그래프만 무거워진다. &lt;b&gt;동작 경로가 이미 바뀌었으면 잔여 의존성을 남기지 말 것.&lt;/b&gt; 모션블러 기본값도 이 김에 정리(옵션에서 토글 가능, 기본 ON).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 리스폰 버튼을 설정 패널 &amp;rarr; 시스템 메뉴로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강제 리스폰 버튼이 설정 패널(&lt;code&gt;Btn_Respawn&lt;/code&gt;)에 임시로 달려 있었다. 하지만 &quot;강제 리스폰&quot;은 &lt;b&gt;설정 항목이 아니라 인게임 액션&lt;/b&gt;이라 자리가 어색했고, 설정 패널에선 인게임인지 검사해 버튼을 숨기고 클릭 시 세션 상태를 재확인하는 &lt;b&gt;가드가 필요&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;시스템(포즈) 메뉴로 옮기니 그 가드가 통째로 사라짐.&lt;/b&gt; 시스템 메뉴는 인게임에서만 열리니까.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;void URetrieveSystemMenuWidget::HandleRespawnClicked()
{
    // 강제 리스폰 &amp;rarr; 사망 흐름(Result&amp;rarr;Retry)이 세션 상태를 전환하며 이 패널을 정리한다.
    if (ARetrievePlayerController* PC = Cast&amp;lt;ARetrievePlayerController&amp;gt;(GetOwningPlayer()))
        PC-&amp;gt;RequestUnstuck();
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교훈: 방어 코드가 늘어난다면 UI가 &lt;b&gt;엉뚱한 곳에 있다는 신호&lt;/b&gt;일 수 있다. 있어야 할 곳에 두니 가시성 가드&amp;middot;상태 재확인이 다 필요 없어졌다. 버튼 배선은 &lt;code&gt;BindWidgetOptional&lt;/code&gt;이라 WBP는 이름만 맞추면 됨.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 전투 SFX 팩 + 스윙&amp;middot;타격 노티&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때리는 맛의 절반은 소리였다. &lt;code&gt;Fantasy_Combat_02&lt;/code&gt; SFX 팩(스윙 Whoosh, Big Hit, Hitmark, Short Impact)을 임포트하고:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;스윙음&lt;/b&gt; &amp;mdash; 검방/스태프 콤보 몽타주에 사운드 노티로 심어 애니 타이밍에 정확히 붙임.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;히트 임팩트음&lt;/b&gt; &amp;mdash; 팩 사운드로 교체하고, 타격 순간의 &lt;code&gt;GameplayCue.Combat.Hit&lt;/code&gt; 큐(&lt;code&gt;GCN_Hit_Impact&lt;/code&gt;)가 물게 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소리를 코드가 아니라 &lt;b&gt;노티/큐로 데이터화&lt;/b&gt;하니, 이후 무기별로 소리만 갈아 끼우기 쉬워졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 타격감 &amp;mdash; 기본 넉백 400 + 히트 플래시 큐&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넉백이 약해 맞아도 밀리는 느낌이 없었다. 일반 히트에 적용되는 &lt;b&gt;전역 기본 넉백을 150 &amp;rarr; 400&lt;/b&gt;으로 올림. 콘솔 변수라 플레이 중 라이브 튜닝 가능.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// SetByCaller 넉백 없는 히트에 적용(보스/플레이어/DoT 제외). 콘솔 라이브 튜닝.
static TAutoConsoleVariable&amp;lt;float&amp;gt; CVarDefaultHitKnockback(
    TEXT(&quot;retrieve.Combat.DefaultHitKnockback&quot;), 400.f, TEXT(&quot;기본 히트 넉백 강도. 0=끔.&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 피격 순간 번쩍이는 히트 플래시(&lt;code&gt;GCN_HitFlash&lt;/code&gt; + 머티리얼 &lt;code&gt;M_HitFlash&lt;/code&gt;)를 큐로 얹어, &lt;b&gt;소리&amp;middot;넉백&amp;middot;플래시가 같은 타이밍에 겹치도록&lt;/b&gt; 함.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. (버그) 익명 네임스페이스 헬퍼 이름 충돌&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 합쳐 빌드하니 링크가 깨졌다. 상점 컴포넌트(&lt;code&gt;RetrieveShopComponent.cpp&lt;/code&gt;)의 익명 네임스페이스 헬퍼 이름이 다른 번역 단위의 동명 헬퍼와 충돌.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;- void SetInteractionTargetEnabled(AActor* Owner, bool bEnabled)
+ void SetShopInteractionTargetEnabled(AActor* Owner, bool bEnabled)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;익명 네임스페이스라고 이름을 흔하게 두면 옆 파일과 부딪힌다. 접두어(&lt;code&gt;Shop&lt;/code&gt;)로 구체화하는 걸로 끝.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;❌ Before&lt;/th&gt;
&lt;th&gt;✅ After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;마우스 감도&lt;/td&gt;
&lt;td&gt;통합 &amp;times; 축별 곱셈(예측 어긋남)&lt;/td&gt;
&lt;td&gt;배타 모드(통합=단일 값 / 세부=X/Y)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FOV 경로&lt;/td&gt;
&lt;td&gt;ALSCamera 모듈 의존 잔존&lt;/td&gt;
&lt;td&gt;잔여 모듈&amp;middot;include 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리스폰 버튼&lt;/td&gt;
&lt;td&gt;설정 패널 + 인게임 가드 이중&lt;/td&gt;
&lt;td&gt;시스템 메뉴(인게임 전용, 가드 불필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전투 사운드&lt;/td&gt;
&lt;td&gt;빈약/코드성&lt;/td&gt;
&lt;td&gt;SFX 팩 + 스윙 노티 + 히트 큐(데이터화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;히트 넉백&lt;/td&gt;
&lt;td&gt;150(약함)&lt;/td&gt;
&lt;td&gt;400 + 히트 플래시 큐(동시 타이밍)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;감도는 &lt;b&gt;곱셈 결합을 없애고 배타(통합/세부) 모드&lt;/b&gt;로 &amp;mdash; 노브가 서로 곱해져 예측이 어긋나던 문제를 제거.&lt;/li&gt;
&lt;li&gt;동작 경로가 바뀐 &lt;b&gt;ALSCamera 잔여 의존성&lt;/b&gt;은 모듈&amp;middot;include까지 잘라내 빌드&amp;middot;의존성을 가볍게.&lt;/li&gt;
&lt;li&gt;리스폰 버튼은 &lt;b&gt;인게임 전용 시스템 메뉴로 이동&lt;/b&gt; &amp;mdash; 방어 코드 증가는 UI 위치가 틀렸다는 신호였고, 옮기니 가드가 사라졌다.&lt;/li&gt;
&lt;li&gt;타격감은 &lt;b&gt;SFX 팩 + 스윙 노티 + 히트 큐 + 넉백 400 + 히트 플래시&lt;/b&gt;를 같은 타이밍에 겹쳐 완성.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내일배움캠프_Unreal/TIL</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>하임</author>
      <guid isPermaLink="true">https://ha2m.tistory.com/176</guid>
      <comments>https://ha2m.tistory.com/176#entry176comment</comments>
      <pubDate>Thu, 9 Jul 2026 20:57:12 +0900</pubDate>
    </item>
    <item>
      <title>내일배움캠프_Unreal/TIL - Day 152 (260708)</title>
      <link>https://ha2m.tistory.com/175</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;강제 리스폰과 낙하 정리 - 자기 피격이 '적중'으로 집계되던 버그&lt;/b&gt;&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 설계 결정 &amp;mdash; 텔레포트를 새로 짜지 말고 &quot;죽음&quot;을 재사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어를 마지막 체크포인트로 되돌리는 가장 쉬운 방법은 텔레포트지만, &lt;b&gt;즉시 텔레포트는 월드 파티션 스트리밍/로딩 커버를 안 거쳐&lt;/b&gt; 도착 지점이 로드되지 않은 채 스폰될 위험이 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 게임엔 이미 &lt;b&gt;사망 &amp;rarr; Result &amp;rarr; Retry &amp;rarr; 체크포인트 리스폰&lt;/b&gt; 흐름이 있고, 여기엔 로딩 커버&amp;middot;스트리밍 보장이 전부 들어 있음. &amp;rarr; &lt;b&gt;언스턱을 정규 사망 처리로 라우팅&lt;/b&gt;해서 그 흐름을 통째로 재사용.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[UI Btn_Respawn]
  └&amp;rarr; PC::RequestUnstuck &amp;rarr; Server_RequestUnstuck      (클라 &amp;rarr; 서버 RPC)
     └&amp;rarr; GameMode::HandleUnstuck    (IsRequestorHost 권한 가드)
        └&amp;rarr; HealthComponent::KillOwner()   (Health &amp;rarr; 0)
           └&amp;rarr; 정규 사망 파이프라인:
              GA_Die &amp;rarr; Channel.Player.Died &amp;rarr; HandlePlayerDied
              &amp;rarr; Result 화면 &amp;rarr; 자동 Retry &amp;rarr; 로딩 커버 + 마지막 체크포인트 리스폰&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교훈: &quot;A와 결과가 같은 견고한 경로 B가 이미 있으면, A를 새로 만들지 말고 &lt;b&gt;B로 진입시킨다&lt;/b&gt;.&quot; 실제 사망과 &lt;b&gt;100% 동일 경로&lt;/b&gt;라 언스턱만의 엣지케이스가 아예 안 생긴다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. &lt;code&gt;KillOwner&lt;/code&gt; &amp;mdash; Health를 직접 0으로 세팅해 사망을 &quot;단일 소스&quot;로 구동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사망 파이프라인의 트리거는 결국 &lt;b&gt;Health 0&lt;/b&gt;. 그래서 데미지 GE를 만들지 않고 &lt;b&gt;어트리뷰트 베이스를 직접 0으로&lt;/b&gt; 세팅 &amp;rarr; &lt;code&gt;HandleHealthChanged&lt;/code&gt;가 나머지를 알아서 굴림.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;void URetrieveHealthComponent::KillOwner()
{
    if (!AbilitySystemComponent || !AttributeSet) return;
    if (bDeathStarted) return;   // 이미 사망 처리 중이면 재진입 방지

    // GE 파이프라인 우회 &amp;mdash; Health 0 &amp;rarr; GA_Die &amp;rarr; ... &amp;rarr; Result.
    AbilitySystemComponent-&amp;gt;SetNumericAttributeBase(UCombatAttributeSet::GetHealthAttribute(), 0.f);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;재진입 가드&lt;/b&gt;: &lt;code&gt;bDeathStarted&lt;/code&gt;로 이미 죽는 중이면 no-op.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;SetNumericAttributeBase&lt;/code&gt; vs GE&lt;/b&gt;: 환경적 강제 사망은 데미지 계산&amp;middot;감쇠&amp;middot;면역이 필요 없으니 &lt;b&gt;베이스 직접 세팅&lt;/b&gt;이 더 정확. (실데미지가 아니라 &quot;상태를 죽음으로 만든다&quot;.)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 권한 라우팅 &amp;mdash; 클라 UI에서 시작해 서버에서 실행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스폰은 서버 권위 동작이라, 클라 UI 입력을 &lt;b&gt;RPC로 서버에 넘기고 서버에서만 실행&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// PlayerController (클라 진입점 &amp;rarr; 서버 RPC)
void ARetrievePlayerController::RequestUnstuck() { Server_RequestUnstuck(); }

void ARetrievePlayerController::Server_RequestUnstuck_Implementation()
{
    if (ARetrieveGameMode* GM = GetWorld()-&amp;gt;GetAuthGameMode&amp;lt;ARetrieveGameMode&amp;gt;())
        GM-&amp;gt;HandleUnstuck(this);
}

// GameMode (서버, 호스트만)
void ARetrieveGameMode::HandleUnstuck(APlayerController* Requestor)
{
    if (!IsRequestorHost(Requestor)) return;   // 요청자 권한 확인
    APawn* Pawn = Requestor ? Requestor-&amp;gt;GetPawn() : nullptr;
    if (auto* CombatPawn = Cast&amp;lt;ARetrieveAlsCombatCharacter&amp;gt;(Pawn))
        CombatPawn-&amp;gt;GetHealthComponent()-&amp;gt;KillOwner();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;GetAuthGameMode&amp;lt;T&amp;gt;()&lt;/code&gt;는 &lt;b&gt;서버에만 유효&lt;/b&gt;(클라에선 null) &amp;rarr; 자연스러운 서버 가드. 여기에 &lt;code&gt;IsRequestorHost&lt;/code&gt;로 한 겹 더.&lt;/li&gt;
&lt;li&gt;곁들여 &lt;code&gt;RequestQuitGame&lt;/code&gt;(포즈 팝업 게임 종료, 로컬 &lt;code&gt;QuitGame&lt;/code&gt;)도 같은 자리에 추가해 &quot;빠져나가는&quot; 동작을 한 곳에 모음.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. (버그 수정) 낙하 데미지가 원소 게이지를 채우던 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강제 리스폰을 검증하려면 일부러 절벽에서 떨어져 죽어봐야 했는데, 낙사를 반복하다 보니 &lt;b&gt;죽을 때마다 원소 게이지가 조금씩 차는&lt;/b&gt; 게 보였다. 원인은 공격자 측 &quot;적중&quot; 이벤트(원소 게이지 충전 등 &lt;b&gt;&quot;남을 때렸다&quot;&lt;/b&gt; 피드백)가, 낙사/환경 데미지(instigator == target인 self-damage)에도 발행되던 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌ Before&lt;/b&gt; &amp;mdash; 무조건 공격자 적중 이벤트 발행&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;EventData.EventTag = AttackerEventTag;
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AttackerActor, AttackerEventTag, EventData);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ After&lt;/b&gt; &amp;mdash; 자기 자신에게 입힌 데미지는 &quot;적중&quot; 아님 &amp;rarr; 제외&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;if (TargetActor != AttackerActor)   // self-damage(낙하 등) 제외
{
    EventData.EventTag = AttackerEventTag;
    UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AttackerActor, AttackerEventTag, EventData);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강제 리스폰을 붙이니 드러난 버그. &quot;적중&quot; 피드백은 &lt;b&gt;남을 때렸을 때만&lt;/b&gt; &amp;mdash; instigator와 target이 같으면 전투 적중이 아니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. (튜닝) 낙법 실패 착지 몽타주 배속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;착지 경직이 길게 느껴져, 낙법 실패 몽타주에 &lt;b&gt;재생 배속&lt;/b&gt; 파라미터를 추가(값만, 로직 동일).&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 헤더: EditDefaultsOnly, ClampMin=0.1
float LandingFailMontagePlayRate = 1.f;   // &amp;gt;1.0 = 짧게(빠르게)
// 재생:
PlayAnimMontage(LandingFailMontage, LandingFailMontagePlayRate);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직은 그대로 두고 파라미터 하나만 늘린 것이라, 애니 에셋을 건드리지 않고 착지 경직 길이를 튜닝할 수 있게 됨.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 올리기 전 죽은 에셋 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스태프 테스트용으로 만들어두고 안 쓰던 &lt;code&gt;OLD_AM&lt;/code&gt; 몽타주 7개(Blink/Burst/HeavyAttack 테스트본)를 삭제하고, 버스트 조합 데이터(&lt;code&gt;DT_SkillCombination&lt;/code&gt;)를 최신 값으로 갱신. 데이터 주도로 몽타주를 정리한 뒤 죽어 있던 에셋이라 이번에 같이 제거.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;❌ 안 했다면&lt;/th&gt;
&lt;th&gt;✅ 이번 구현&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;언스턱 방식&lt;/td&gt;
&lt;td&gt;즉시 텔레포트(스트리밍 미보장)&lt;/td&gt;
&lt;td&gt;정규 사망 흐름 라우팅(로딩 커버&amp;middot;체크포인트 재사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사망 트리거&lt;/td&gt;
&lt;td&gt;데미지 GE 조작&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SetNumericAttributeBase(Health, 0)&lt;/code&gt; + 재진입 가드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 권한&lt;/td&gt;
&lt;td&gt;클라에서 직접&lt;/td&gt;
&lt;td&gt;UI&amp;rarr;PC RPC&amp;rarr;GM(&lt;code&gt;IsRequestorHost&lt;/code&gt;)&amp;rarr;HealthComp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;낙하 데미지&lt;/td&gt;
&lt;td&gt;원소 게이지 충전됨(버그)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;instigator==target&lt;/code&gt;이면 적중 이벤트 제외&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;착지 경직&lt;/td&gt;
&lt;td&gt;고정 길이&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LandingFailMontagePlayRate&lt;/code&gt;로 배속 튜닝&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;강제 리스폰은 &lt;b&gt;새 텔레포트 로직 대신 이미 견고한 &quot;사망&amp;rarr;Result&amp;rarr;Retry&amp;rarr;체크포인트&quot; 흐름에 라우팅&lt;/b&gt; &amp;mdash; 로딩 커버/스트리밍/엣지케이스를 공짜로 얻는다.&lt;/li&gt;
&lt;li&gt;사망 트리거는 &lt;b&gt;Health를 직접 0으로&lt;/b&gt;(&lt;code&gt;SetNumericAttributeBase&lt;/code&gt;) 세팅해 파이프라인을 단일 소스로 구동, &lt;code&gt;bDeathStarted&lt;/code&gt;로 재진입 방지.&lt;/li&gt;
&lt;li&gt;서버 권위 동작은 &lt;b&gt;UI(클라) &amp;rarr; PC RPC &amp;rarr; GM 권한 가드 &amp;rarr; 실행&lt;/b&gt;으로 라우팅하고, &lt;code&gt;GetAuthGameMode&amp;lt;T&amp;gt;()&lt;/code&gt; null 특성이 자연스러운 서버 가드가 된다.&lt;/li&gt;
&lt;li&gt;&quot;적중&quot; 피드백은 &lt;b&gt;instigator &amp;ne; target일 때만&lt;/b&gt; &amp;mdash; self-damage(낙사 등)로 원소 게이지가 차는 버그를 이 한 줄로 막는다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내일배움캠프_Unreal/TIL</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>하임</author>
      <guid isPermaLink="true">https://ha2m.tistory.com/175</guid>
      <comments>https://ha2m.tistory.com/175#entry175comment</comments>
      <pubDate>Wed, 8 Jul 2026 21:39:29 +0900</pubDate>
    </item>
    <item>
      <title>내일배움캠프_Unreal/TIL - Day 151 (260707)</title>
      <link>https://ha2m.tistory.com/174</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;휴가&lt;/p&gt;</description>
      <category>내일배움캠프_Unreal/TIL</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>하임</author>
      <guid isPermaLink="true">https://ha2m.tistory.com/174</guid>
      <comments>https://ha2m.tistory.com/174#entry174comment</comments>
      <pubDate>Wed, 8 Jul 2026 21:39:05 +0900</pubDate>
    </item>
    <item>
      <title>내일배움캠프_Unreal/TIL - Day 150 (260706)</title>
      <link>https://ha2m.tistory.com/173</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;언스턱(강제 리스폰) - 새 로직 대신 &quot;정규 사망 흐름&quot; 재사용&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. &lt;code&gt;KillOwner&lt;/code&gt; - Health를 직접 0으로 세팅해 사망을 &quot;단일 소스&quot;로 구동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사망 파이프라인의 트리거는 결국 &lt;b&gt;Health 0&lt;/b&gt;. 그래서 데미지 GE를 만들지 않고 &lt;b&gt;어트리뷰트 베이스를 직접 0으로&lt;/b&gt; 세팅 &amp;rarr; &lt;code&gt;HandleHealthChanged&lt;/code&gt;가 나머지를 알아서 굴림.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;void URetrieveHealthComponent::KillOwner()
{
    if (!AbilitySystemComponent || !AttributeSet) return;
    if (bDeathStarted) return;   // 이미 사망 처리 중이면 재진입 방지

    // GE 파이프라인 우회 &amp;mdash; ResetHealth와 동일 기법. Health 0 &amp;rarr; GA_Die &amp;rarr; ... &amp;rarr; Result.
    AbilitySystemComponent-&amp;gt;SetNumericAttributeBase(UCombatAttributeSet::GetHealthAttribute(), 0.f);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;재진입 가드&lt;/b&gt;: &lt;code&gt;bDeathStarted&lt;/code&gt;로 이미 죽는 중이면 no-op.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;SetNumericAttributeBase&lt;/code&gt; vs GE&lt;/b&gt;: 환경적 강제 사망은 데미지 계산&amp;middot;감쇠&amp;middot;면역이 필요 없으니 &lt;b&gt;베이스 직접 세팅&lt;/b&gt;이 더 정확. (실데미지가 아니라 &quot;상태를 죽음으로 만든다&quot;.)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. UI 가드 이중화 + 버튼 부재 안전 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;강제 리스폰&quot; 버튼은 &lt;b&gt;인게임에서만&lt;/b&gt; 의미가 있음. 메인 메뉴에서 설정을 열면 숨겨야 하고, 세션 상태가 바뀐 뒤 눌릴 가능성도 막아야 함.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;// NativeConstruct &amp;mdash; 인게임이 아니면 버튼 숨김. WBP에 버튼이 아직 없으면 FindWidget이 null &amp;rarr; 안전 무시.
if (UButton* RespawnButton = FindWidget&amp;lt;UButton&amp;gt;(this, TEXT(&quot;Btn_Respawn&quot;)))
{
    const bool bInGame = GS &amp;amp;&amp;amp; GS-&amp;gt;GetSessionState() == ERetrieveSessionState::InGame;
    RespawnButton-&amp;gt;SetVisibility(bInGame ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
}

// HandleRespawn &amp;mdash; 클릭 시에도 세션 상태 재확인(가시성 가드가 뚫려도 방어)
if (!GS || GS-&amp;gt;GetSessionState() != ERetrieveSessionState::InGame) return;
if (auto* PC = Cast&amp;lt;ARetrievePlayerController&amp;gt;(GetOwningPlayer())) PC-&amp;gt;RequestUnstuck();
RequestClose();   // 요청 후 패널 닫아 게임 입력 복귀&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;이중 가드&lt;/b&gt;: 표시(Visibility) + 실행(핸들러) 양쪽에서 인게임 확인.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버튼 부재 내성&lt;/b&gt;: &lt;code&gt;FindWidget&lt;/code&gt; null 허용 &amp;rarr; C++ 선반영 후 WBP에 버튼을 나중에 추가해도 크래시 없음(배선 순서 독립).&lt;/li&gt;
&lt;li&gt;곁들여 &lt;code&gt;RequestQuitGame&lt;/code&gt;(포즈 팝업 게임 종료, 로컬 &lt;code&gt;QuitGame&lt;/code&gt;)도 추가.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. (버그 수정) 낙하 데미지로 원소 게이지가 차던 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자 측 &quot;적중&quot; 이벤트(원소 게이지 충전 등 &lt;b&gt;&quot;남을 때렸다&quot;&lt;/b&gt; 피드백)가, 낙사/환경 데미지(instigator == target인 self-damage)에도 발행되던 게 원인.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌ Before&lt;/b&gt; &amp;mdash; 무조건 공격자 적중 이벤트 발행&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;EventData.EventTag = AttackerEventTag;
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AttackerActor, AttackerEventTag, EventData);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ After&lt;/b&gt; &amp;mdash; 자기 자신에게 입힌 데미지는 &quot;적중&quot; 아님 &amp;rarr; 제외&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;if (TargetActor != AttackerActor)   // self-damage(낙하 등) 제외
{
    EventData.EventTag = AttackerEventTag;
    UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AttackerActor, AttackerEventTag, EventData);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙사 강제 리스폰을 붙이니 드러난 버그. &quot;적중&quot; 피드백은 &lt;b&gt;남을 때렸을 때만&lt;/b&gt; - instigator와 target이 같으면 전투 적중이 아니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. (튜닝) 낙법 실패 착지 몽타주 배속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;착지 경직이 길게 느껴져, 낙법 실패 몽타주에 &lt;b&gt;재생 배속&lt;/b&gt; 파라미터를 추가(값만, 로직 동일).&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 헤더: EditDefaultsOnly, ClampMin=0.1
float LandingFailMontagePlayRate = 1.f;
// 재생: 배속 전달 (&amp;gt;1.0 = 짧게)
PlayAnimMontage(LandingFailMontage, LandingFailMontagePlayRate);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;❌ 안 했다면&lt;/th&gt;
&lt;th&gt;✅ 이번 구현&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;사망 트리거&lt;/td&gt;
&lt;td&gt;데미지 GE 조작&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SetNumericAttributeBase(Health, 0)&lt;/code&gt; + 재진입 가드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리스폰 버튼&lt;/td&gt;
&lt;td&gt;항상 노출/무가드&lt;/td&gt;
&lt;td&gt;Visibility + 핸들러 재확인 + &lt;code&gt;FindWidget&lt;/code&gt; null 내성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;낙하 데미지&lt;/td&gt;
&lt;td&gt;원소 게이지 충전됨(버그)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;instigator==target&lt;/code&gt;이면 적중 이벤트 제외&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;착지 경직&lt;/td&gt;
&lt;td&gt;고정 길이&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LandingFailMontagePlayRate&lt;/code&gt;로 배속 튜닝&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사망 트리거는 &lt;b&gt;Health를 직접 0으로&lt;/b&gt;(&lt;code&gt;SetNumericAttributeBase&lt;/code&gt;) 세팅해 파이프라인을 단일 소스로 구동, &lt;code&gt;bDeathStarted&lt;/code&gt;로 재진입 방지..&lt;/li&gt;
&lt;li&gt;UI 버튼은 &lt;b&gt;표시&amp;middot;실행 이중 가드&lt;/b&gt; + &lt;code&gt;FindWidget&lt;/code&gt; null 내성으로, C++를 먼저 넣고 WBP 배선을 나중에 해도 안전하게 한다.&lt;/li&gt;
&lt;li&gt;&quot;적중&quot; 피드백은 &lt;b&gt;instigator &amp;ne; target일 때만&lt;/b&gt; &amp;mdash; self-damage(낙사 등)로 원소 게이지가 차는 버그를 이 한 줄로 막는다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내일배움캠프_Unreal/TIL</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>하임</author>
      <guid isPermaLink="true">https://ha2m.tistory.com/173</guid>
      <comments>https://ha2m.tistory.com/173#entry173comment</comments>
      <pubDate>Mon, 6 Jul 2026 23:54:31 +0900</pubDate>
    </item>
    <item>
      <title>내일배움캠프_Unreal/TIL - Day 149 (260703)</title>
      <link>https://ha2m.tistory.com/172</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Staff 전투 완성 - 얼음창 다중 투사체 &amp;middot; 지속 장판 &amp;middot; 부분 슈퍼아머 &amp;middot; 워프 옵션화&lt;/b&gt;&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Water Heavy 얼음창 - 다중 투사체 &quot;동시 소환 &amp;rarr; 개별 발사&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3발이 머리 주변에 &lt;b&gt;동시에 맺힌 뒤 각자 다른 타이밍에 날아가는&lt;/b&gt; 연출. 세 가지 장치가 필요했음.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(a) 지연 발사 - 스폰 후 제자리에 떠 있다 발사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProjectileMovement의 속도를 0으로 두면 (중력 0 가정) &lt;b&gt;제자리에 뜬 채 대기&lt;/b&gt; &amp;rarr; 타이머로 저장한 방향에 발사.&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;void AStaffProjectile::ArmDelayedLaunch(const FVector&amp;amp; Direction, float Speed, float Delay)
{
    PendingLaunchDirection = Direction;  PendingLaunchSpeed = Speed;
    ProjectileMovement-&amp;gt;Velocity = FVector::ZeroVector;
    ProjectileMovement-&amp;gt;InitialSpeed = 0.f;   // PMC가 안 움직임 = 스폰 위치에 정지
    World-&amp;gt;GetTimerManager().SetTimer(LaunchDelayTimerHandle, this, &amp;amp;AStaffProjectile::HandleDelayedLaunch, Delay, false);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(b) 소켓 앵커 + 오프셋 가산 - 좌/상/우 배치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존엔 &quot;소켓이 있으면 SpawnOffset 무시&quot;였음. 다중 스폰을 위해 &lt;b&gt;소켓을 앵커로 두고 그 위에 오프셋을 가산&lt;/b&gt;하는 모드(&lt;code&gt;bAddOffsetToSocketBase&lt;/code&gt;)를 추가.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;if (bUsedSocket &amp;amp;&amp;amp; Params.bAddOffsetToSocketBase)
    SpawnLocation += AvatarActor-&amp;gt;GetActorRotation().RotateVector(Params.SpawnOffset);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(c) 볼리 상호 충돌 무시 - 형제 창에 막혀 소멸 방지  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;먼저 발사된 창이, 아직 떠 있는 형제 창에 부딪혀 즉시 파괴&lt;/b&gt;되는 문제. &amp;rarr; 같은 볼리끼리 이동 스윕에서 서로 건너뛰게 등록.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// AStaffProjectile
void IgnoreOtherProjectile(AStaffProjectile* Other)
{ CollisionSphere-&amp;gt;IgnoreActorWhenMoving(Other, true); }   // 블로킹/오버랩 모두 무시

// GA_HeavyAttack::ScheduleProjectiles &amp;mdash; 오프셋 개수만큼 동시 스폰 후 O(n&amp;sup2;) 상호 등록
for (int32 i = 0; i &amp;lt; Offsets.Num(); ++i)
    Volley.Add(SpawnOneProjectile(Offsets[i], /*bAddOffsetToSocketBase=*/true, FireDelays[i]));
for (a: Volley) for (b: Volley) if (a != b) Volley[a]-&amp;gt;IgnoreOtherProjectile(Volley[b]);&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩토링: &lt;code&gt;SpawnProjectile()&lt;/code&gt;(void, 단일) &amp;rarr; &lt;code&gt;SpawnOneProjectile(offset, bAddToSocket, delay)&lt;/code&gt;(스폰된 투사체 &lt;b&gt;반환&lt;/b&gt;)로 바꿔 &lt;b&gt;레거시 단일 경로와 다중 볼리 경로가 같은 스폰 함수를 공유&lt;/b&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Water Burst 지속 장판(소용돌이) - 매 프레임 노티 + 적별 간격 데미지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신규 실행 타입 &lt;code&gt;EAttackExecutionType::AreaContinuous&lt;/code&gt;. 노티 구간 동안 &lt;b&gt;매 프레임 호출&lt;/b&gt;되며, 데미지는 적별 간격으로, 넉백은 매 프레임 방사.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;void UPlayerBurstComponent::DoAreaContinuousHit(const FBurstHitInstance&amp;amp; Hit, int32 HitIndex)
{
    // 중심 반경 오버랩 (AoeRadius 재사용) ...
    const double Now = World-&amp;gt;GetTimeSeconds();
    TMap&amp;lt;TWeakObjectPtr&amp;lt;AActor&amp;gt;, double&amp;gt;* TimeMap = &amp;amp;PerHitLastHitTime[HitIndex];  // 적별 마지막 타격 시각

    // 이중 넉백 방지: 데미지 틱에서 GE 방향성 넉백은 끄고, 방사 넉백을 단일 소스로.
    FBurstHitInstance DamageHit = Hit;
    DamageHit.KnockbackStrength = 0.f;
    DamageHit.KnockbackUpwardStrength = 0.f;

    for (Target : Overlaps)
    {
        bool bApplyDamage = true;   // 첫 진입 즉시 1틱
        if (const double* Last = TimeMap-&amp;gt;Find(Target)) bApplyDamage = (Now - *Last) &amp;gt;= Interval;
        if (bApplyDamage) { ApplyHitToTarget(Target, DamageHit, ...); TimeMap-&amp;gt;Add(Target, Now); }
        // + 매 프레임 중심&amp;rarr;바깥 방사 넉백 수집(보스 제외 옵션)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;핵심 자료구조&lt;/b&gt;: &lt;code&gt;PerHitLastHitTime&lt;/code&gt; = per-HitIndex의 &lt;code&gt;TMap&amp;lt;약참조 액터, 시각&amp;gt;&lt;/code&gt; &amp;rarr; 장판 위에 서 있는 &lt;b&gt;적마다&lt;/b&gt; 재적용 간격(&lt;code&gt;ContinuousDamageInterval&lt;/code&gt;)을 독립 관리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이중 넉백 방지  &lt;/b&gt;: 데미지 GE에도 방향성 넉백이 붙어 있으면 방사 넉백과 &lt;b&gt;두 소스가 충돌&lt;/b&gt; &amp;rarr; 데미지 틱에서는 GE 넉백을 끄고 &lt;b&gt;방사만&lt;/b&gt; 밈.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 공격 중 &quot;부분 슈퍼아머&quot; - 전역 억제 vs 비보스 억제 2단 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콤보/강공이 &lt;b&gt;잡몹 피격엔 안 끊기고 보스 공격엔 끊기게&lt;/b&gt;. 기존 전역 억제(버스트 슈퍼아머)에 &lt;b&gt;조건부 억제&lt;/b&gt;를 한 겹 더.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌ Before&lt;/b&gt; &amp;mdash; 억제 태그 1종(전역)만: 있으면 무조건 리액션 스킵&lt;br /&gt;&lt;b&gt;✅ After&lt;/b&gt; &amp;mdash; 2단 억제&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;// (1) 전역: 모든 공격자에 대해 스킵 (버스트 슈퍼아머 = State.Player.Bursting)
if (ASC-&amp;gt;HasAnyMatchingGameplayTags(ReactionSuppressionTags)) return;

// (2) 부분: 공격 중이면 '비보스' 공격자만 스킵 (보스는 통과)
if (ASC-&amp;gt;HasAnyMatchingGameplayTags(AttackStateSuppressionTags))   // State.Player.Attacking / UsingHeavyAttack
    if (!(bBossBypassesAttackSuppression &amp;amp;&amp;amp; IsInstigatorBoss(Payload)))
        return;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;보스 판정&lt;/b&gt;: 피격 이벤트 &lt;code&gt;Payload-&amp;gt;Instigator&lt;/code&gt;의 ASC에 &lt;code&gt;Monster.Type.Boss&lt;/code&gt; 태그가 있는지(&lt;code&gt;IsInstigatorBoss&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공용 안전장치&lt;/b&gt;: &lt;code&gt;AttackStateSuppressionTags&lt;/code&gt; 기본값이 &lt;b&gt;플레이어 공격 태그&lt;/b&gt;라, 이 태그가 없는 &lt;b&gt;적에게는 영향 없음&lt;/b&gt;(플레이어/적 공용 컴포넌트).&lt;/li&gt;
&lt;li&gt;데미지는 두 경우 모두 &lt;b&gt;정상 적용&lt;/b&gt; &amp;mdash; 억제되는 건 리액션(몽타주/어빌리티 캔슬)뿐.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. AttackWarp - 무타겟 시 입력방향 회전을 옵션화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌진 워프가 &lt;b&gt;타겟이 없으면 항상 이동 입력 방향으로 회전&lt;/b&gt;했음 &amp;rarr; 특정 콤보에선 원치 않는 재정렬. 플래그로 끌 수 있게.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;// 무타겟일 때만 해당. 타겟이 있으면 여전히 워프.
- if (bPlayerControlled &amp;amp;&amp;amp; !InputDir.IsNearlyZero())
+ if (bRotateToInputWhenNoTarget &amp;amp;&amp;amp; bPlayerControlled &amp;amp;&amp;amp; !InputDir.IsNearlyZero())&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원칙: &quot;무입력에도 정면 강제회전&quot; 버그를 잡을 때 raw 이동 입력만 쓰도록 한 데 더해, &lt;b&gt;회전 자체를 데이터로 on/off&lt;/b&gt; &amp;rarr; 콤보별로 재정렬을 허용/금지.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. (짧게) 무기 히트볼륨 &lt;code&gt;BoundsRadiusScale&lt;/code&gt; 기본값 3.0&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bounds 트레이스 반경 스케일 기본값을 &lt;code&gt;3.0&lt;/code&gt;으로 올리고 데이터에 반영. 판정이 얇게 느껴지던 걸 완화(값만 변경, 로직 동일).&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;❌ Before&lt;/th&gt;
&lt;th&gt;✅ After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;투사체 스폰&lt;/td&gt;
&lt;td&gt;단일 &lt;code&gt;SpawnProjectile()&lt;/code&gt;(void)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SpawnOneProjectile(...)&lt;/code&gt;(반환) &amp;mdash; 단일/볼리 공용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다중 발사&lt;/td&gt;
&lt;td&gt;순차 스폰&lt;/td&gt;
&lt;td&gt;동시 스폰 + 투사체별 &lt;code&gt;FireDelays[i]&lt;/code&gt; 발사 지연&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;소켓+오프셋&lt;/td&gt;
&lt;td&gt;소켓 있으면 오프셋 무시&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bAddOffsetToSocketBase&lt;/code&gt;로 앵커 위 가산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;볼리 충돌&lt;/td&gt;
&lt;td&gt;형제 창에 막혀 소멸&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IgnoreActorWhenMoving&lt;/code&gt;로 상호 무시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장판 데미지&lt;/td&gt;
&lt;td&gt;(없음)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AreaContinuous&lt;/code&gt; &amp;mdash; 적별 &lt;code&gt;TMap&amp;lt;Actor,시각&amp;gt;&lt;/code&gt; 간격 재적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장판 넉백&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;매 프레임 방사(데미지 틱 GE 넉백 끔=이중 방지)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;피격 억제&lt;/td&gt;
&lt;td&gt;전역 1종&lt;/td&gt;
&lt;td&gt;전역(모든 공격자) + 부분(비보스만, 보스 예외)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;워프 회전&lt;/td&gt;
&lt;td&gt;무타겟이면 항상 입력방향&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bRotateToInputWhenNoTarget&lt;/code&gt;로 on/off&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;동시 소환 &amp;rarr; 개별 발사&quot; 볼리는 &lt;b&gt;①속도 0 대기(지연 발사) ②소켓 앵커+오프셋 가산 ③형제끼리 &lt;code&gt;IgnoreActorWhenMoving&lt;/code&gt;&lt;/b&gt; 세 장치가 함께 필요. 특히 형제 충돌 무시를 빼면 먼저 나간 발사체가 소멸함.&lt;/li&gt;
&lt;li&gt;지속 장판은 &lt;b&gt;매 프레임 노티 + per-target &lt;code&gt;TMap&amp;lt;Actor,시각&amp;gt;&lt;/code&gt;&lt;/b&gt; 로 적별 간격 데미지, 넉백은 매 프레임 방사. &lt;b&gt;넉백 소스는 하나로&lt;/b&gt;(데미지 GE 넉백 끄기).&lt;/li&gt;
&lt;li&gt;슈퍼아머는 &lt;b&gt;전역/부분 2단&lt;/b&gt; &amp;mdash; 전역은 모든 공격자, 부분은 비보스만(보스는 &lt;code&gt;Instigator&lt;/code&gt; ASC의 &lt;code&gt;Monster.Type.Boss&lt;/code&gt;로 예외). 데미지는 항상 적용, 리액션만 억제.&lt;/li&gt;
&lt;li&gt;&quot;항상 하는 동작&quot;이 콤보마다 거슬리면 &lt;b&gt;데이터 플래그로 on/off&lt;/b&gt;(예: 워프 무타겟 회전) &amp;mdash; 코드 분기보다 데이터로 끄고 켠다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내일배움캠프_Unreal/TIL</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>하임</author>
      <guid isPermaLink="true">https://ha2m.tistory.com/172</guid>
      <comments>https://ha2m.tistory.com/172#entry172comment</comments>
      <pubDate>Fri, 3 Jul 2026 23:44:24 +0900</pubDate>
    </item>
    <item>
      <title>내일배움캠프_Unreal/TIL - Day 148 (260702)</title>
      <link>https://ha2m.tistory.com/171</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스태프 확장 - 부채꼴 판정 &amp;middot; 이벤트 착지 슬램 &amp;middot; 돌진 관통 &amp;middot; 원소 손 VFX&lt;/b&gt;&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 이벤트 기반 착지 슬램 - Tick 폴링이 아니라 &lt;code&gt;LandedDelegate&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공중 버스트(WindBurst)가 &lt;b&gt;바닥에 닿는 순간&lt;/b&gt;을 매 프레임 검사(Tick)로 잡지 않고, &lt;code&gt;ACharacter::LandedDelegate&lt;/code&gt;에 바인딩해 콜백으로 받음.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;// ActivateAbility &amp;mdash; 착지 슬램 버스트만 바인딩
if (bLandingImpactEnabled)
{
    AlsChar-&amp;gt;SetSuppressLandingRoll(true);                 // &amp;sect;2
    LandChar-&amp;gt;LandedDelegate.AddDynamic(this, &amp;amp;ThisClass::HandleLanded);
    BoundLandedCharacter = LandChar;
}

// HandleLanded &amp;mdash; 착지 순간 3가지
MoveComp-&amp;gt;StopMovementImmediately();                       // (1) 잔여 다이브 속도 제거 &amp;rarr; 그 자리 정지
ASC-&amp;gt;CurrentMontageJumpToSection(CachedLandingSection);   // (2) 착지 섹션 점프
CachedBurstComp-&amp;gt;ApplyLandingImpact(Center, Radius, ...); // (3) AoE 데미지 + 방사 넉백(보스 제외)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;(1)의 이유&lt;/b&gt;: 다이브 낙하 속도가 남으면 착지 후 앞으로 미끄러짐 &amp;rarr; &lt;code&gt;StopMovementImmediately()&lt;/code&gt;로 벽 앞이든 어디든 &lt;b&gt;딱 그 자리에&lt;/b&gt; 멈춤.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1회성 플래그 누수 방지  &lt;/b&gt;: &lt;code&gt;bLandingHandled&lt;/code&gt;로 중복 방지 + &lt;b&gt;&lt;code&gt;CleanupBurst()&lt;/code&gt;에서 착지 억제 플래그가 소비 안 된 채 종료되면 강제 해제&lt;/b&gt; + &lt;code&gt;UnbindLanded()&lt;/code&gt;로 델리게이트 구독 해제 &amp;rarr; 다음 착지로 새지 않게. 이벤트 바인딩은 &lt;b&gt;반드시 대칭 해제&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. ALS 착지 연출 억제 - Roll만으론 부족, Ragdoll도&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;쿵 하고 그냥 착지&quot; 연출을 위해 ALS 착지 자동 연출을 &lt;b&gt;이 착지 1회만&lt;/b&gt; 끔.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌ Before&lt;/b&gt; &amp;mdash; Roll만 억제 &amp;rarr; 착지 래그돌이 남아 캐릭터가 픽 쓰러짐&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Settings-&amp;gt;Rolling.bStartRollingOnLand = false;   // Roll만&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ After&lt;/b&gt; &amp;mdash; Roll + Ragdoll 둘 다 억제 후 원복&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const bool bPrevRoll    = Settings-&amp;gt;Rolling.bStartRollingOnLand;
const bool bPrevRagdoll = Settings-&amp;gt;Ragdolling.bStartRagdollingOnLand;
Settings-&amp;gt;Rolling.bStartRollingOnLand       = false;
Settings-&amp;gt;Ragdolling.bStartRagdollingOnLand = false;
Super::NotifyLocomotionModeChanged(PreviousLocomotionMode);
Settings-&amp;gt;Rolling.bStartRollingOnLand       = bPrevRoll;   // 원복
Settings-&amp;gt;Ragdolling.bStartRagdollingOnLand = bPrevRagdoll;
bSuppressLandingRoll = false; // 1회 소비&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴: &lt;b&gt;공유 세팅&lt;/b&gt;(ALS는 세팅을 전역으로 공유)을 잠깐 바꿔야 하면 &lt;b&gt;[저장 &amp;rarr; 끄기 &amp;rarr; Super 호출 &amp;rarr; 원복]&lt;/b&gt; 스코프로 감싼다. 영구 변경하면 다른 상황의 착지까지 망가짐.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 부채꼴(Cone) 판정 - 구체 오버랩 + 내적 각도 컷&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전용 콘 셰이프를 만들지 않고, &lt;b&gt;구체 오버랩으로 후보를 모은 뒤 내적으로 각도를 자름&lt;/b&gt;. 저렴하고 간단함.&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;const FVector Forward = Owner-&amp;gt;GetActorForwardVector().GetSafeNormal2D();  // 수평 투영
const float CosHalf = FMath::Cos(FMath::DegreesToRadians(ConeHalfAngleDeg));
// ...구체 오버랩으로 Overlaps 수집 후:
const FVector ToTarget = (Target-&amp;gt;GetActorLocation() - Origin).GetSafeNormal2D();
if (FVector::DotProduct(Forward, ToTarget) &amp;lt; CosHalf) continue;  // 반각 밖 &amp;rarr; 스킵&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;핵심&lt;/b&gt;: &lt;code&gt;Dot(전방, 대상방향) &amp;ge; cos(반각)&lt;/code&gt; 이면 부채꼴 안. &lt;code&gt;acos&lt;/code&gt; 없이 &lt;b&gt;코사인 비교&lt;/b&gt;로 처리.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GetSafeNormal2D&lt;/code&gt; = 높이차 무시하고 &lt;b&gt;수평 부채꼴&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;두 곳에서 재사용: &lt;b&gt;버스트/강공 공용&lt;/b&gt;(&lt;code&gt;EAttackExecutionType::Cone&lt;/code&gt; &amp;rarr; &lt;code&gt;DoConeHit&lt;/code&gt;)과 &lt;b&gt;콤보 스텝 광역&lt;/b&gt;(&lt;code&gt;GA_Attack::ApplyStepDamage&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콤보 스텝 콘 - 블레이드 판정 + 콘 광역을 &lt;b&gt;합산&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콤보 스텝에 &lt;code&gt;bUseCone&lt;/code&gt;를 켜면 &lt;b&gt;무기 블레이드 트레이스 결과에 콘 오버랩 결과를 더함&lt;/b&gt;(마지막 타 광역 등). &lt;code&gt;bWeaponTrace=false&lt;/code&gt;면 블레이드 없이 콘만(왼손 방출 스텝).&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// UE5: FHitResult는 액터를 HitObjectHandle로 들고 있음 &amp;rarr; GetActor()가 유효하려면 명시 세팅해야 함.
FHitResult ConeHit(ForceInit);
ConeHit.HitObjectHandle = FActorInstanceHandle(ConeTarget);   // ⚠️ 이거 없으면 후단에서 GetActor()==nullptr
ConeHit.ImpactPoint = ConeTarget-&amp;gt;GetActorLocation();
ConeHit.bBlockingHit = true;
AllHits.Add(ConeHit);   // 블레이드 히트 + 콘 히트 한 배열로 합쳐 뒤에서 일괄 데미지&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;중복 방지&lt;/b&gt;: &lt;code&gt;PerHitHitActors[HitIndex]&lt;/code&gt; 집합으로 노티 매 프레임 호출에도 &lt;b&gt;대상당 1회&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 돌진 중 적(Pawn) 관통 -콜리전을 끄지 말고 &quot;채널 응답&quot;만 토글&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌진(Dash) 공격이 적을 밀치며 &lt;b&gt;통과&lt;/b&gt;하되 &lt;b&gt;벽은 막혀야&lt;/b&gt; 함. 콜리전을 통째로 끄면(&lt;code&gt;SetActorEnableCollision&lt;/code&gt;) 벽도 통과 &amp;rarr; 대신 &lt;b&gt;Pawn 채널 응답만&lt;/b&gt; &lt;code&gt;Ignore&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// RetrieveGameplayAbility (공통 베이스 헬퍼 &amp;mdash; Burst/Heavy 공용)
void URetrieveGameplayAbility::SetAvatarPawnCollisionIgnored(bool bIgnore) const
{
    if (UCapsuleComponent* Capsule = Character-&amp;gt;GetCapsuleComponent())
        // 벽(WorldStatic/Dynamic)은 그대로 Block, 적(Pawn)만 통과.
        Capsule-&amp;gt;SetCollisionResponseToChannel(ECC_Pawn, bIgnore ? ECR_Ignore : ECR_Block);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;// GA_Burst::ActivateAbility / GA_HeavyAttack::RunExecutorPath &amp;mdash; 돌진 타입만 켜고
if (AttackType == EAttackExecutionType::Dash) SetAvatarPawnCollisionIgnored(true);
// CleanupBurst / EndAbility &amp;mdash; 반드시 복구(비돌진이면 no-op)
SetAvatarPawnCollisionIgnored(false);&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교훈: &quot;특정 대상만 통과&quot;는 &lt;b&gt;콜리전 on/off가 아니라 채널별 Response 전환&lt;/b&gt;. 켜기/복구는 GA 진입&amp;middot;종료에서 &lt;b&gt;대칭&lt;/b&gt;으로, 공용 로직은 베이스 클래스 헬퍼로 뺀다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 메테오 대각선 낙하 &amp;middot; 지면 스냅 &amp;middot; 도착 임팩트 VFX&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙하 소환물(&lt;code&gt;ABurstWorldActorBase&lt;/code&gt;)이 &lt;b&gt;수직으로만&lt;/b&gt; 떨어지던 걸 확장.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;필드 / 처리&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;대각선 낙하&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SpawnHorizontalOffset&lt;/code&gt;(FVector2D, 로컬 X전후/Y좌우)를 &lt;code&gt;SpawnHeightOffset&lt;/code&gt;(Z)과 합쳐 &lt;b&gt;액터 회전 기준 회전벡터&lt;/b&gt;로 시작점 산출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지면 스냅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bWorldSnapToGround&lt;/code&gt; &amp;rarr; 전방 스폰 지점 &lt;b&gt;아래로 라인트레이스&lt;/b&gt; &amp;rarr; 착지 Z를 지면에 맞춘 뒤 &lt;code&gt;WorldSpawnHeightOffset&lt;/code&gt;만큼 위에서 스폰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;판정 반경&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WorldActorRadius&lt;/code&gt; &lt;b&gt;데이터 우선&lt;/b&gt;, 0이면 컴포넌트 기본값 폴백&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;도착 임팩트&lt;/td&gt;
&lt;td&gt;지면 도착(모션 완료) 순간 &lt;code&gt;ImpactVFX&lt;/code&gt; &lt;b&gt;1회&lt;/b&gt; 스폰(&lt;code&gt;bImpactSpawned&lt;/code&gt; 중복 방지) &amp;mdash; BP 배선 없이 데이터로&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;protobuf&quot;&gt;&lt;code&gt;// 대각선: 로컬 오프셋을 액터 회전으로 돌려 월드 시작점
const FVector LocalStartOffset(SpawnHorizontalOffset.X, SpawnHorizontalOffset.Y, SpawnHeightOffset);
StartLocation = TargetLocation + GetActorRotation().RotateVector(LocalStartOffset);

// 지면 스냅: 시전자 높이가 아니라 착지 지점 지면 기준
if (ActiveSpec.bWorldSnapToGround &amp;amp;&amp;amp; World-&amp;gt;LineTraceSingleByChannel(GroundHit, Up, Down, ECC_Visibility, P))
    SpawnLocation.Z = GroundHit.ImpactPoint.Z;
SpawnLocation.Z += ActiveSpec.WorldSpawnHeightOffset;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 스태프 왼손 원소 VFX - 이벤트 구독 + diff 기반 재스폰 회피&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 원소(Fire/Water/Wind)에 맞는 Niagara를 &lt;b&gt;왼손 소켓에 상시 부착&lt;/b&gt;, 원소가 바뀔 때만 스왑. &lt;code&gt;URetrieveCosmeticData&lt;/code&gt;에 &lt;code&gt;TMap&amp;lt;원소태그, Niagara&amp;gt;&lt;/code&gt; 를 두어 &lt;b&gt;데이터 주도&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;// 원소 변경 이벤트(GameplayEvent.Element.ModeChange) 구독 &amp;rarr; RefreshCosmeticState &amp;rarr; RefreshHandVFX
// 원하는 VFX: Staff 장착 + 현재 원소 매핑이 있을 때만
UNiagaraSystem* DesiredVFX = (Staff &amp;amp;&amp;amp; Element.IsValid())
    ? CosmeticData-&amp;gt;HandVFXByElement.Find(CurrentElementTag)-&amp;gt;LoadSynchronous() : nullptr;

// ⭐ diff: 이미 같은 시스템이면 재스폰 안 함 (원소/무기 그대로면 유지 &amp;rarr; 깜빡임&amp;middot;누수 방지)
if (DesiredVFX == (SpawnedHandVFX ? SpawnedHandVFX-&amp;gt;GetAsset() : nullptr)) return;

// 교체: 기존 Destroy &amp;rarr; 새 원소 VFX를 손 소켓에 bAutoDestroy=false로 부착
SpawnedHandVFX = UNiagaraFunctionLibrary::SpawnSystemAttached(DesiredVFX, Mesh, HandVFXSocket, ...);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;소켓 부재 방어&lt;/b&gt;: 소켓이 스켈레톤에 없으면 루트에 붙어 하반신을 덮음 &amp;rarr; &lt;code&gt;DoesSocketExist&lt;/code&gt; 체크 후 &lt;b&gt;경고 로그&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;갱신 트리거&lt;/b&gt;: 무기 변경/원소 변경/장착 모두 &lt;code&gt;RefreshCosmeticState()&lt;/code&gt;로 수렴 &amp;rarr; 한 경로에서 스왑.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;❌ Before&lt;/th&gt;
&lt;th&gt;✅ After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;착지 감지&lt;/td&gt;
&lt;td&gt;Tick 폴링 경향&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LandedDelegate&lt;/code&gt; 이벤트 1회(+대칭 해제)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;착지 후 이동&lt;/td&gt;
&lt;td&gt;잔여 속도로 미끄러짐&lt;/td&gt;
&lt;td&gt;&lt;code&gt;StopMovementImmediately()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;착지 연출 억제&lt;/td&gt;
&lt;td&gt;Roll만&lt;/td&gt;
&lt;td&gt;Roll + Ragdoll (1회 후 원복)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;부채꼴 판정&lt;/td&gt;
&lt;td&gt;(없음)&lt;/td&gt;
&lt;td&gt;구체 오버랩 + &lt;code&gt;Dot &amp;ge; cos(반각)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;콤보 마지막 광역&lt;/td&gt;
&lt;td&gt;블레이드만&lt;/td&gt;
&lt;td&gt;블레이드 히트 + 콘 히트 &lt;b&gt;합산&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UE5 합성 히트&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GetActor()&lt;/code&gt; null&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HitObjectHandle = FActorInstanceHandle(...)&lt;/code&gt; 명시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;돌진 관통&lt;/td&gt;
&lt;td&gt;콜리전 on/off (벽도 통과)&lt;/td&gt;
&lt;td&gt;Pawn &lt;b&gt;채널 응답&lt;/b&gt;만 &lt;code&gt;Ignore&lt;/code&gt;, 벽은 Block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;소환물 낙하&lt;/td&gt;
&lt;td&gt;순수 수직&lt;/td&gt;
&lt;td&gt;대각선 + 지면 스냅 + 도착 임팩트 VFX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;손 원소 VFX&lt;/td&gt;
&lt;td&gt;(없음/BP)&lt;/td&gt;
&lt;td&gt;이벤트 구독 + diff 재스폰 회피(데이터 맵)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;착지 훅은 &lt;b&gt;Tick 폴링 말고 &lt;code&gt;LandedDelegate&lt;/code&gt;&lt;/b&gt; &amp;mdash; 착지 순간 &lt;code&gt;StopMovementImmediately&lt;/code&gt;로 미끄러짐 제거, 이벤트 바인딩&amp;middot;1회성 플래그는 정리 경로에서 &lt;b&gt;반드시 대칭 해제/복구&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;ALS &quot;그냥 쿵&quot; 착지는 &lt;b&gt;Roll + Ragdoll 둘 다&lt;/b&gt; 억제, 공유 세팅은 [저장&amp;rarr;끄기&amp;rarr;Super&amp;rarr;원복] 스코프.&lt;/li&gt;
&lt;li&gt;부채꼴은 전용 셰이프 없이 &lt;b&gt;구체 오버랩 + &lt;code&gt;Dot &amp;ge; cos(반각)&lt;/code&gt;&lt;/b&gt; 로 충분. 여러 판정 소스를 한 배열로 합칠 땐 &lt;b&gt;UE5 &lt;code&gt;FHitResult.HitObjectHandle&lt;/code&gt;을 명시&lt;/b&gt;해야 &lt;code&gt;GetActor()&lt;/code&gt;가 산다.&lt;/li&gt;
&lt;li&gt;&quot;특정 대상만 통과&quot;는 콜리전 on/off가 아니라 &lt;b&gt;채널별 Response 전환&lt;/b&gt;(&lt;code&gt;ECC_Pawn&lt;/code&gt; &amp;rarr; &lt;code&gt;Ignore&lt;/code&gt;), 벽은 Block 유지. 켜기/복구는 대칭 + 공용 베이스 헬퍼.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부착형 VFX는 &lt;b&gt;원하는 자산 == 현재 자산이면 재스폰 스킵&lt;/b&gt;(diff)으로 깜빡임&amp;middot;누수를 막고, 소켓 부재는 경고로 방어한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내일배움캠프_Unreal/TIL</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>하임</author>
      <guid isPermaLink="true">https://ha2m.tistory.com/171</guid>
      <comments>https://ha2m.tistory.com/171#entry171comment</comments>
      <pubDate>Thu, 2 Jul 2026 23:12:08 +0900</pubDate>
    </item>
    <item>
      <title>내일배움캠프_Unreal/TIL - Day 147 (260701)</title>
      <link>https://ha2m.tistory.com/170</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Burst Phase Dash&lt;/b&gt;&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 이동 파라미터를 &quot;행 단위&quot;에서 &quot;타격 단위(per-hit)&quot;로 이관&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제: 한 버스트 안에서 이동 페이즈를 못 나눔&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WindBurst는 &lt;b&gt;&quot;솟구쳐 오른 뒤 &amp;rarr; 전방으로 다이브&quot;&lt;/b&gt; 처럼 이동이 두 페이즈로 나뉨. 그런데 대시 값(거리/시간/상승)이 스킬 행(&lt;code&gt;FSkillCombination&lt;/code&gt;)에 &lt;b&gt;하나만&lt;/b&gt; 있어 한 세트밖에 못 씀.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결: 대시 값을 각 타격(&lt;code&gt;FBurstHitInstance&lt;/code&gt;)으로 내림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HitSequence&lt;/code&gt;의 &lt;b&gt;HitIndex별로 다른 이동&lt;/b&gt;을 주도록 per-hit 오버라이드를 추가. HitIndex 0 = 순수 상승, HitIndex 1 = 전방+하강.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// FBurstHitInstance (per-hit 오버라이드)
bool  bOverrideDashMotion = false;
float DashForwardDistance = 0.f;  // 음수 = 후방
float DashUpwardSpeed     = 0.f;  // 양수 = 상승 / 음수 = 하강
float DashLaunchDuration  = 0.2f; // 발사 속도 = 거리 / 시간&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주의 ⚠️&lt;/b&gt;: 공용 실행 spec(&lt;code&gt;FAttackExecutionSpec&lt;/code&gt;)의 &lt;b&gt;행 단위 대시는 Heavy 등도 쓰므로 유지&lt;/b&gt;. 버스트 행(&lt;code&gt;FSkillCombination&lt;/code&gt;)에서만 제거 &amp;rarr; 실행기는 &lt;code&gt;bOverrideDashMotion&lt;/code&gt;이면 per-hit, 아니면 spec 폴백으로 이원화.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// DoDashHit &amp;mdash; per-hit이면 타격 값, 아니면 spec(행) 값으로 폴백
const bool  bPerHitDash = Hit.bOverrideDashMotion;
const float DashDistance = bPerHitDash ? Hit.DashForwardDistance : ActiveSpec.DashDistance;
const float DashUpwardSpeed = bPerHitDash ? Hit.DashUpwardSpeed : ActiveSpec.DashUpwardSpeed;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교훈: 한 액션 안에서 &lt;b&gt;단계마다 값이 달라져야 하면&lt;/b&gt; 그 값을 &quot;행(1세트)&quot;이 아니라 &quot;타격 시퀀스(N개)&quot;로 내린다. 기존 공용 경로는 지우지 말고 &lt;b&gt;폴백으로 남겨&lt;/b&gt; 다른 공격(Heavy)이 계속 쓰게 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 상승 성분엔 &lt;code&gt;bOverrideZ&lt;/code&gt; &amp;mdash; 수평/수직 합성 발사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전방 속도(수평) + 상승/하강 속도(수직)를 &lt;b&gt;한 벡터로 합성&lt;/b&gt;해 발사. 여기서 &lt;code&gt;LaunchCharacter&lt;/code&gt;의 Z 덮어쓰기 여부가 핵심.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;FVector LaunchVelocity = Forward * (DashDistance / DashLaunchDuration);
LaunchVelocity.Z += DashUpwardSpeed;
// 상승 성분이 있으면 Z도 덮어써야 실제로 떠오름. 순수 전방이면 기존 낙하 Z 보존.
URetrieveKnockbackLibrary::LaunchSelf(Character, LaunchVelocity.GetSafeNormal(),
    LaunchVelocity.Size(), /*bOverrideXY=*/true, /*bOverrideZ=*/bHasUpward);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌ Before&lt;/b&gt; &amp;mdash; 순수 전방만 가정, Z는 항상 보존&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;LaunchSelf(Character, Forward, Speed, /*bOverrideXY=*/true, /*bOverrideZ=*/false);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ After&lt;/b&gt; &amp;mdash; 상승이 있을 때만 Z를 덮어써 실제로 떠오르게&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;트러블슈팅&lt;/b&gt;: &lt;code&gt;bOverrideZ=false&lt;/code&gt;면 상승 속도를 줘도 기존 수직 속도/중력에 묻혀 &lt;b&gt;안 떠오름&lt;/b&gt;. 상승 성분(&lt;code&gt;bHasUpward&lt;/code&gt;)이 있을 때만 &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;⚠️ 애님 세팅 전제&lt;/b&gt;: 다이브 애님은 &lt;b&gt;Root Motion OFF + Force Root Lock&lt;/b&gt;이어야 함. 안 그러면 루트모션이 이동 소유권을 가져가 &lt;code&gt;LaunchCharacter&lt;/code&gt;가 &lt;b&gt;팝백&lt;/b&gt;됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;❌ Before&lt;/th&gt;
&lt;th&gt;✅ After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;버스트 이동 값&lt;/td&gt;
&lt;td&gt;행(&lt;code&gt;FSkillCombination&lt;/code&gt;) 1세트&lt;/td&gt;
&lt;td&gt;per-hit(&lt;code&gt;FBurstHitInstance&lt;/code&gt;) &amp;rarr; 상승/다이브 페이즈 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;공용 실행기 대시&lt;/td&gt;
&lt;td&gt;행 값만&lt;/td&gt;
&lt;td&gt;spec 폴백 유지 (Heavy 등이 씀)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;발사 방향&lt;/td&gt;
&lt;td&gt;전방(수평)만&lt;/td&gt;
&lt;td&gt;수평 + 수직(상승/하강) 합성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상승 발사 Z&lt;/td&gt;
&lt;td&gt;항상 보존(&lt;code&gt;bOverrideZ=false&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;상승 성분 있으면 &lt;code&gt;bOverrideZ=true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 액션 안에서 &lt;b&gt;단계마다 이동이 달라지면&lt;/b&gt; 값을 행(1세트) &amp;rarr; 타격 시퀀스(per-hit)로 내린다. 기존 공용 경로는 지우지 말고 &lt;b&gt;폴백으로 남겨&lt;/b&gt; 다른 공격이 계속 쓰게.&lt;/li&gt;
&lt;li&gt;전방+상승을 &lt;b&gt;한 벡터로 합성&lt;/b&gt;해 발사하고, 상승 성분이 있을 때만 &lt;code&gt;bOverrideZ=true&lt;/code&gt;(아니면 안 떠오름).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LaunchCharacter&lt;/code&gt;로 띄우는 다이브 애님은 &lt;b&gt;Root Motion OFF / Force Root Lock&lt;/b&gt; 필수 &amp;mdash; 안 그러면 팝백.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내일배움캠프_Unreal/TIL</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>하임</author>
      <guid isPermaLink="true">https://ha2m.tistory.com/170</guid>
      <comments>https://ha2m.tistory.com/170#entry170comment</comments>
      <pubDate>Wed, 1 Jul 2026 20:47:49 +0900</pubDate>
    </item>
    <item>
      <title>내일배움캠프_Unreal/TIL - Day 146 (260630)</title>
      <link>https://ha2m.tistory.com/169</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;GAS 이벤트 기반 패리 카운터 &amp;amp; 가드공격 Bounds화&lt;/b&gt;&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 패리 카운터 - 입력 게이팅에서 이벤트 트리거로&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제: 카운터가 &quot;상태 태그 + 재입력&quot;에 묶여 있었음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패리 성공 후 카운터를, &lt;code&gt;State.Player.CanCounter&lt;/code&gt; 상태를 켜두고 &lt;b&gt;플레이어가 Attack을 다시 눌러야&lt;/b&gt; 나가는 방식이었음.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성공했는데 한 번 더 입력해야 하고, 그 사이 윈도우를 놓치면 불발.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CanCounter&lt;/code&gt; 상태 부여/해제와 &lt;b&gt;카운터 윈도우 GE&lt;/b&gt;(&lt;code&gt;CounterWindowEffect&lt;/code&gt;)까지 관리해야 함 &amp;rarr; 상태 머신이 무거움.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결: 패리 성공 순간 게임플레이 이벤트로 카운터 GA를 자동 발동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카운터 GA가 &lt;b&gt;특정 이벤트를 수신하면 스스로 발동&lt;/b&gt;하도록 트리거를 등록. 입력&amp;middot;상태가 아니라 이벤트가 발동 조건.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌ Before&lt;/b&gt; &amp;mdash; 상태 태그로 게이팅 (외부 입력이 실제 트리거)&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// GA_ParryCounter 생성자
ActivationRequiredTags.AddTag(RetrieveGameplayTags::State_Player_CanCounter);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ After&lt;/b&gt; &amp;mdash; 이벤트 자체가 발동 트리거&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;// GA_ParryCounter 생성자
FAbilityTriggerData CounterTrigger;
CounterTrigger.TriggerTag = RetrieveGameplayTags::GameplayEvent_Parry_Counter;
CounterTrigger.TriggerSource = EGameplayAbilityTriggerSource::GameplayEvent;
AbilityTriggers.Add(CounterTrigger);&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념 구분&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ActivationRequiredTags&lt;/code&gt; = &quot;이 태그가 &lt;b&gt;있어야&lt;/b&gt; 발동 가능&quot; &amp;mdash; 발동 트리거는 여전히 외부(입력).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AbilityTriggers&lt;/code&gt;(GameplayEvent) = &lt;b&gt;이벤트 수신 = 발동&lt;/b&gt;. 별도 입력/상태 불필요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 이벤트 송신 - &lt;code&gt;SendGameplayEventToActor&lt;/code&gt; + 페이로드로 대상 전달&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패리 성공 처리부(&lt;code&gt;GA_ParryBase::HandleParrySuccess&lt;/code&gt;)에서 이벤트를 쏨. 페이로드에 &lt;b&gt;맞은 대상&lt;/b&gt;을 실어 보내 카운터가 누굴 칠지까지 함께 넘김.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;FGameplayEventData CounterEvent;
CounterEvent.EventTag       = RetrieveGameplayTags::GameplayEvent_Parry_Counter;
CounterEvent.Instigator     = CounterAvatar;
CounterEvent.Target         = LastParriedAttacker.Get();
CounterEvent.OptionalObject = LastParriedAttacker.Get();
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(
    CounterAvatar, RetrieveGameplayTags::GameplayEvent_Parry_Counter, CounterEvent);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;부수 효과&lt;/b&gt;: 이벤트 방식으로 가니 &lt;code&gt;CounterWindowEffect&lt;/code&gt;(카운터 윈도우 GE) 적용 코드가 &lt;b&gt;통째로 불필요&lt;/b&gt; &amp;rarr; 상태 GE 하나 제거.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주의  &lt;/b&gt;: 신규 태그 &lt;code&gt;GameplayEvent.Parry.Counter&lt;/code&gt;는 &lt;code&gt;.cpp&lt;/code&gt;(&lt;code&gt;UE_DEFINE_...&lt;/code&gt;)와 &lt;code&gt;.h&lt;/code&gt;(&lt;code&gt;UE_DECLARE_...&lt;/code&gt;) &lt;b&gt;양쪽&lt;/b&gt; 선언해야 링크됨. 한쪽만 하면 링크 에러.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 가드공격 Bounds화 - 검과 방패의 다른 형상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평타&amp;middot;점프에서 만든 &lt;code&gt;URetrieveWeaponTraceLibrary&lt;/code&gt;를 &lt;b&gt;가드공격도 재사용&lt;/b&gt;. 공격 소스에 따라 다른 형상 API 호출.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;트레이스 소스&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;형상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;무기(검)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BuildBoundsTrace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;최장축 구체 스윕&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;방패&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BuildBoundsSphere&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;중심 구체 1개&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 검 &amp;mdash; 무기 메시 바운드 스윕. 성공하면 여기서 return, 실패 시 아래 소켓 폴백
FWeaponTraceSegment WeaponSeg;
if (URetrieveWeaponTraceLibrary::BuildBoundsTrace(TraceMesh, 1.0f, 0.f, SegCount, WeaponSeg)
    &amp;amp;&amp;amp; WeaponSeg.IsValidTrace())
{
    OutPoints = WeaponSeg.Points;
    ResolvedTraceRadius = WeaponSeg.Radius;
    return;
}
// &amp;darr; (Bounds 실패 시) 기존 소켓 경로 폴백 유지&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평타는 레거시 소켓 경로를 &lt;b&gt;삭제&lt;/b&gt;했지만, 가드공격은 &lt;b&gt;Bounds 우선 + 소켓 폴백&lt;/b&gt;을 남겨 안전하게 전환.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 방패 메시를 소켓으로 조회 - &lt;code&gt;GetEquippedMeshBySocket&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방패는 검과 &lt;b&gt;별도 메시 컴포넌트&lt;/b&gt;라, 여러 파트 중 &quot;어느 게 방패냐&quot;를 알아야 트레이스가 됨.&lt;br /&gt;&amp;rarr; 부착 소켓 이름(&lt;code&gt;&quot;Shield&quot;&lt;/code&gt;)으로 파트를 찾는 getter를 새로 추가.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;UMeshComponent* UWeaponComponent::GetEquippedMeshBySocket(FName AttachSocketName) const
{
    for (const FRetrieveEquippedWeaponPart&amp;amp; Part : WeaponAttachParts)
        if (Part.DrawnSocket == AttachSocketName &amp;amp;&amp;amp; IsValid(Part.Mesh))
            return Part.Mesh;
    return nullptr;
}

// 호출부 (BuildShieldTracePoints)
UMeshComponent* ShieldMesh = CachedWeaponComponent-&amp;gt;GetEquippedMeshBySocket(RetrieveWeaponSockets::Shield);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 &lt;code&gt;GetWeaponMeshForTrace&lt;/code&gt;(두 소켓을 다 가진 메시 1개 반환)로는 방패를 특정할 수 없어, &lt;b&gt;부착 위치 기준 조회&lt;/b&gt;가 별도로 필요했음.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. &lt;code&gt;mutable ResolvedTraceRadius&lt;/code&gt; - const 산출 메서드가 반경을 되돌려 씀&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bounds 트레이스는 &lt;b&gt;점뿐 아니라 반경도 메시 크기에서 산출&lt;/b&gt;됨. 그래서 스윕 반경을 데이터테이블 행의 고정 &lt;code&gt;TraceRadius&lt;/code&gt;가 아니라 &lt;b&gt;산출된 반경&lt;/b&gt;으로 써야 함.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문제&lt;/b&gt;: 트레이스 점을 만드는 &lt;code&gt;Build...TracePoints()&lt;/code&gt;들이 전부 &lt;code&gt;const&lt;/code&gt; 메서드인데, 산출 반경을 멤버에 저장해 스윕 단계로 넘겨야 함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결&lt;/b&gt;: &lt;code&gt;mutable&lt;/code&gt;로 선언해 const 메서드에서 대입 허용. 산출 성공이면 그 값, 실패(폴백)면 행 반경으로.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;mutable float ResolvedTraceRadius = 0.f;   // 헤더

// SweepAndApplyDamage &amp;mdash; 산출값 우선, 없으면 행 반경 폴백
const float TraceRadius = ResolvedTraceRadius &amp;gt; 0.f
    ? ResolvedTraceRadius
    : CachedWeaponData.TraceRadius;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교훈: 계산 결과가 &quot;점&quot;과 &quot;반경&quot;처럼 &lt;b&gt;여러 개&lt;/b&gt;면, const 산출 메서드에서 out 파라미터 하나로는 부족 &amp;rarr; &lt;code&gt;mutable&lt;/code&gt; 멤버로 부수 산출값을 실어 나르는 패턴이 유용(단, 상태 결합이 늘어나니 남용 주의).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;❌ Before&lt;/th&gt;
&lt;th&gt;✅ After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;카운터 발동&lt;/td&gt;
&lt;td&gt;&lt;code&gt;State.Player.CanCounter&lt;/code&gt; + Attack 재입력(수동)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AbilityTriggers&lt;/code&gt;(GameplayEvent) 자동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;발동 트리거 주체&lt;/td&gt;
&lt;td&gt;외부 입력&lt;/td&gt;
&lt;td&gt;이벤트 수신 자체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;카운터 윈도우 GE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CounterWindowEffect&lt;/code&gt; 적용&lt;/td&gt;
&lt;td&gt;불필요 &amp;rarr; 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대상 전달&lt;/td&gt;
&lt;td&gt;상태에서 별도 조회&lt;/td&gt;
&lt;td&gt;이벤트 페이로드(&lt;code&gt;Target&lt;/code&gt;)로 동봉&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가드공격 트레이스&lt;/td&gt;
&lt;td&gt;소켓 기반&lt;/td&gt;
&lt;td&gt;Bounds 우선 + 소켓 폴백&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;방패 메시 특정&lt;/td&gt;
&lt;td&gt;불가(소켓 2개 메시만)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GetEquippedMeshBySocket(&quot;Shield&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스윕 반경&lt;/td&gt;
&lt;td&gt;행 고정 &lt;code&gt;TraceRadius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;산출 반경(&lt;code&gt;mutable ResolvedTraceRadius&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GA 자동 발동은 &lt;code&gt;ActivationRequiredTags&lt;/code&gt;(상태 게이팅)보다 &lt;b&gt;&lt;code&gt;AbilityTriggers&lt;/code&gt; + &lt;code&gt;SendGameplayEventToActor&lt;/code&gt;&lt;/b&gt;(이벤트 트리거)가 결정적 &amp;mdash; 입력&amp;middot;상태 관리가 통째로 사라진다.&lt;/li&gt;
&lt;li&gt;이벤트 페이로드(&lt;code&gt;Instigator&lt;/code&gt;/&lt;code&gt;Target&lt;/code&gt;)에 &lt;b&gt;대상까지 실어&lt;/b&gt; 보내면 수신 측이 상태를 되조회할 필요가 없다.&lt;/li&gt;
&lt;li&gt;신규 게임플레이 태그는 &lt;code&gt;.h&lt;/code&gt;/&lt;code&gt;.cpp&lt;/code&gt; &lt;b&gt;양쪽 선언&lt;/b&gt; 필수(안 하면 링크 에러).&lt;/li&gt;
&lt;li&gt;트레이스 라이브러리는 &lt;b&gt;형상 API&lt;/b&gt;(검=스윕 / 방패=구체)로 나눠 재사용하고, 방패처럼 별도 메시는 &lt;b&gt;부착 소켓으로 조회&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;Bounds는 반경도 산출값 &amp;rarr; const 산출 메서드가 &lt;code&gt;mutable&lt;/code&gt; 멤버로 반경을 되돌려 쓰고, 스윕은 산출 반경 우선&amp;middot;행 반경 폴백.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>하임</author>
      <guid isPermaLink="true">https://ha2m.tistory.com/169</guid>
      <comments>https://ha2m.tistory.com/169#entry169comment</comments>
      <pubDate>Tue, 30 Jun 2026 21:03:14 +0900</pubDate>
    </item>
  </channel>
</rss>