<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Crat</title>
    <link>https://crat.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 1 Aug 2026 18:35:43 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Crat3</managingEditor>
    <item>
      <title>32-16. 로그 라이크 - (3) 게임플레이 어빌리티 - (6) 뽑기 로직 리펙토링</title>
      <link>https://crat.tistory.com/594</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;0. 개요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 HUD - PlayerController - PlayerState - GameMode 간 다량의 델리게이트를 통해서 뽑기 로직을 실행했으나, 이 과정에서 구조가 복잡해지고 콜백 함수의 증가로 인해 로직 파악에 어려움이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 언리얼의 레플리케이션 시스템을 이용하여 단순화하고, 온라인에서 작동하도록 수정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. PlayerState에서 멤버 변수 레플리케이션 하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 뽑는 로직은 서버인 GameMode에서 진행하고, 뽑은 카드를 멤버 변수로 저장하고 있다가 UI에서 노출하는 방법을 사용할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 멤버 변수를 레플리케이션 하도록 지정하고 콜백 함수를 활용하여 UI를 띄우는 방식의 구조로 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 뽑은 카드를 멤버 변수로 저장하기 - 레플리케이션으로 지정&lt;/p&gt;
&lt;pre id=&quot;code_1752036767415&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	UPROPERTY(ReplicatedUsing = OnRep_UpgradeCardInfo)
	TArray&amp;lt;FAuraAbilityUpgradeInfo&amp;gt; ReplicatedCardInfo;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1752036792242&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	UFUNCTION()
	void OnRep_UpgradeCardInfo();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 델리게이트 선언&lt;/p&gt;
&lt;pre id=&quot;code_1752036848027&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCardsInitialized, TArray&amp;lt;FAuraAbilityUpgradeInfo&amp;gt;&amp;amp;, InitializedCards);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 델리게이트는 HUD에서 바인딩할 것이며, 호출되면 뷰 포트에 위젯을 추가하고 카드의 뷰 모델들을 초기화한다.&lt;/p&gt;
&lt;pre id=&quot;code_1752036928867&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	UPROPERTY()
	FOnCardsInitialized OnUpgradeCardsInitializedDelegate;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2-1) AuraHUD - 델리게이트 바인딩&lt;/p&gt;
&lt;pre id=&quot;code_1752036957867&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AAuraHUD::InitOverlay(APlayerController *PC, APlayerState *PS, UAbilitySystemComponent *ASC, UAttributeSet *AS)
{
    // 위젯과 위젯 컨트롤러 생성
    checkf(OverlayWidgetClass, TEXT(&quot;Overlay Widget not Initialized. BP_AuraHUD&quot;));
    checkf(OverlayWidgetControllerClass, TEXT(&quot;Overlay Widget Controller not Initialized. BP_AuraHUD&quot;));

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    if (AAuraPlayerState* AuraPS = Cast&amp;lt;AAuraPlayerState&amp;gt;(PS))
    {
        AuraPS-&amp;gt;OnUpgradeCardsInitializedDelegate.AddDynamic(this, &amp;amp;AAuraHUD::HandleRandomAbilityUpgradeInfos);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) 레플리케이션 변수의 Setter 함수 내에서 델리게이트 호출&lt;/p&gt;
&lt;pre id=&quot;code_1752036815312&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void SetUpgradeCardInfo(const TArray&amp;lt;FAuraAbilityUpgradeInfo&amp;gt;&amp;amp; NewCard);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1752036822575&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AAuraPlayerState::SetUpgradeCardInfo(const TArray&amp;lt;FAuraAbilityUpgradeInfo&amp;gt;&amp;amp; NewCard)
{
    ReplicatedCardInfo = NewCard;
    OnUpgradeCardsInitializedDelegate.Broadcast(ReplicatedCardInfo);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레플리케이션 변수의 정보를 저장하고 UI로 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. AuraGameMode에서 카드 뽑기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) PlayerController에서 서버로 카드 뽑기 요청하기 - 서버 RPC 함수&lt;/p&gt;
&lt;pre id=&quot;code_1752037080619&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	UFUNCTION(Server, Reliable, BlueprintCallable)
	void Server_CreateCardSelection(AActor* InteractedActor);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752037095211&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AAuraPlayerController::Server_CreateCardSelection_Implementation(AActor* InteractedActor)
{
    if (!HasAuthority())
        return;
    
    // 게임 모드에게 카드 뽑기를 요청
    if (AAuraGameModeBase* GameMode = Cast&amp;lt;AAuraGameModeBase&amp;gt;(GetWorld()-&amp;gt;GetAuthGameMode()))
    {
        if (APawn* InteractedPawn = Cast&amp;lt;APawn&amp;gt;(InteractedActor))
        {
            if (APlayerController* PC = Cast&amp;lt;APlayerController&amp;gt;(InteractedPawn-&amp;gt;GetController()))
            {
                // GameMode의 카드 뽑기 로직 호출
                GameMode-&amp;gt;HandleInitializeCards(PC);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 함수를 블루프린트나 C++ 코드 어디에선가 호출하면&lt;/p&gt;
&lt;pre id=&quot;code_1752037228426&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AAuraGameModeBase::HandleInitializeCards(APlayerController* PC)
{
	if (AAuraPlayerController* AuraPC = Cast&amp;lt;AAuraPlayerController&amp;gt;(PC))
	{
		if (AAuraPlayerState* AuraPS = AuraPC-&amp;gt;GetPlayerState&amp;lt;AAuraPlayerState&amp;gt;())
		{
			auto RandomUpgradeInfos = GetRandomUpgradeInfosForActivatedAbility_Three(AuraPS);
			AuraPS-&amp;gt;SetUpgradeCardInfo(RandomUpgradeInfos);

			// 카드 내 선택 버튼 브로드캐스트
			AuraPC-&amp;gt;OnCardSelectedDelegate.Broadcast();
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임모드 내의 이 함수가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카드를 뽑는 메인 로직은 PlayerState의 함수를 그대로 GameMode로 가지고와 수정해서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752037505111&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    // 델리게이트 바인딩
    OnCardSelectedDelegate.AddUObject(this ,&amp;amp;AAuraPlayerController::HandleCardSelectionInitialized);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 델리게이트는 카드 뽑기를 완료했을 때 호출하는 델리게이트이다.&lt;/p&gt;
&lt;pre id=&quot;code_1752037528592&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AAuraPlayerController::HandleCardSelectionInitialized()
{
    if (AAuraHUD* AuraHUD = Cast&amp;lt;AAuraHUD&amp;gt;(GetHUD()))
    {
        if (UMVVM_CardSelection* CardSelectionViewModel = AuraHUD-&amp;gt;GetCardSelectionViewModel())
        {
            for (int32 i = 0; i &amp;lt; CardSelectionViewModel-&amp;gt;GetNumCards(); ++i)
            {
                if (UMVVM_AbilityCard* CardViewModel = CardSelectionViewModel-&amp;gt;GetCardViewModelByIndex(i))
                {
                    CardViewModel-&amp;gt;OnUpgradeSelectedDelegate.AddDynamic(this, &amp;amp;AAuraPlayerController::HandleAbilityCardSelected);
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 뷰 모델에 대해 선택 버튼에 업그레이드 적용 콜백 함수를 바인딩한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 카드 뽑기 로직&lt;/p&gt;
&lt;pre id=&quot;code_1752037270551&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	UFUNCTION()
	TArray&amp;lt;FAuraAbilityUpgradeInfo&amp;gt; GetRandomUpgradeInfosForActivatedAbility_Three(AAuraPlayerState* AuraPS);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1752037281375&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;TArray&amp;lt;FAuraAbilityUpgradeInfo&amp;gt; AAuraGameModeBase::GetRandomUpgradeInfosForActivatedAbility_Three(
	AAuraPlayerState* AuraPS)
{
    TArray&amp;lt;FGameplayTag&amp;gt; ActivatedAbilityTags = AuraPS-&amp;gt;GetAllActiveAbilityTags();
    TArray&amp;lt;FGameplayTag&amp;gt; InActivatedAbilityTags;
	InActivatedAbilityTags.Empty();
    
    TArray&amp;lt;FAuraAbilityUpgradeInfo&amp;gt; RandomUpgradeInfos;
    RandomUpgradeInfos.Empty();

    // 글로벌 업그레이드 할당
    ActivatedAbilityTags.Add(FGameplayTag::RequestGameplayTag(&quot;Abilities.Fire&quot;));
    ActivatedAbilityTags.Add(FGameplayTag::RequestGameplayTag(&quot;Abilities.Arcane&quot;));
    ActivatedAbilityTags.Add(FGameplayTag::RequestGameplayTag(&quot;Abilities.Lightning&quot;));

    // 어빌리티 습득 또는 레벨업 업그레이드 할당
    TArray&amp;lt;FGameplayTag&amp;gt; AllAbilitiesTags = FAuraGameplayTags::Get().GameplayAbilitiesTags;
    for (auto AbilityTag : AllAbilitiesTags)
    {
        InActivatedAbilityTags.AddUnique(AbilityTag);
    }
    
    // 주어진 어빌리티 태그에 대해 랜덤한 업그레이드 태그 뽑기
    if (UAbilityUpgradeInfo* Info = AbilityUpgradeInfo)
    {
        int ActivatedAbilityTagsNum = ActivatedAbilityTags.Num();
        int RandValue0 = UKismetMathLibrary::RandomIntegerInRange(0, ActivatedAbilityTagsNum-1);
        int RandValue1 = UKismetMathLibrary::RandomIntegerInRange(0, ActivatedAbilityTagsNum-1);
        int RandValue2 = UKismetMathLibrary::RandomIntegerInRange(0, ActivatedAbilityTagsNum-1);

        // 랜덤으로 뽑은 어빌리티 태그는 중복 가능 -&amp;gt; 파이어볼트 업그레이드 2개 노출 가능
        const auto Tag0 = ActivatedAbilityTags[RandValue0];
        const auto Tag1 = ActivatedAbilityTags[RandValue1];
        const auto Tag2 = ActivatedAbilityTags[RandValue2];
        
        FGameplayTag UpgradeTag0;
        FGameplayTag UpgradeTag1;
        FGameplayTag UpgradeTag2;

        // 루프 함수 제한
        int CurrentAttempt = 0;
        
        // 동일한 태그가 없을 때까지 뽑기
        while (UpgradeTag0 == UpgradeTag1 || UpgradeTag1 == UpgradeTag2 || UpgradeTag0 == UpgradeTag2)
        {
            UpgradeTag0 = Info-&amp;gt;GetRandomUpgradeTagForAbility(Tag0);
            UpgradeTag1 = Info-&amp;gt;GetRandomUpgradeTagForAbility(Tag1);
            UpgradeTag2 = Info-&amp;gt;GetRandomUpgradeTagForAbility(Tag2);

            CurrentAttempt++;

            if (CurrentAttempt &amp;gt; 30)
                break;
        }

        RandomUpgradeInfos.Add(Info-&amp;gt;GetUpgradeInfoForUpgradeTag(UpgradeTag0));
        RandomUpgradeInfos.Add(Info-&amp;gt;GetUpgradeInfoForUpgradeTag(UpgradeTag1));
        RandomUpgradeInfos.Add(Info-&amp;gt;GetUpgradeInfoForUpgradeTag(UpgradeTag2));
    }

    return RandomUpgradeInfos;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카드의 글로벌 데미지(또는 효과) 증가 업그레이드와 획득(또는 비획득)한 스펠 레벨업 카드, 보유한 어빌리티의 업그레이드 정보 가져오기 등 세 가지의 로직이 모두 섞여있기 때문에 추후에 분리해야 한다.&lt;/p&gt;</description>
      <category>UE 5 스터디/Gameplay Ability System(GAS)</category>
      <category>c++</category>
      <category>unreal</category>
      <category>로그라이크</category>
      <category>리펙토링</category>
      <author>Crat3</author>
      <guid isPermaLink="true">https://crat.tistory.com/594</guid>
      <comments>https://crat.tistory.com/594#entry594comment</comments>
      <pubDate>Wed, 9 Jul 2025 14:06:11 +0900</pubDate>
    </item>
    <item>
      <title>24-3. 엔진 업그레이드 - (2) MVVM - 로드 및 저장 UI 버그 수정</title>
      <link>https://crat.tistory.com/593</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;0. 개요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 엔진의 버전이 올라가면서 베타 스테이지에 있던 MVVM 플러그인에 약간 변화가 생기면서 기존의 MVVM 위젯이 작동하지 않은 버그가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 버그가 발생한 이유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UE 5.3 부터 MVVM 플러그인이 변화했는데, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;뷰 모델의 생성 타입(Creation Type)이 수동(Manual)일 때, &lt;/span&gt;뷰 모델의 필드 노티파이가 위젯의 어떤 요소에도 바인드 되지 않았다면, 위젯에서 뷰 모델을 할당하는 데에 실패하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 버그를 해결하기 위해서는 '필드 노티파이를 위젯의 어떤 한 요소에라도 바인딩'해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 버그 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 로드 슬롯 타입 별 필드 노티파이 바인드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1-1) MVVM_LoadSlot 뷰 모델&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVVM_LoadSlot 내의 멤버 변수 중 하나였던 로드 슬롯 이름을 필드 노티파이로 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이름은 플레이할 때 실제로 노출되지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1751956061246&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	UPROPERTY(EditAnywhere, BlueprintReadWrite, FieldNotify, Setter, Getter, meta = (AllowPrivateAccess = &quot;true&quot;))
	FString LoadSlotName;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1751956070838&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public:
	/* Getter, Setter */
	void SetPlayerName(FString InPlayerName);
	void SetLevel(int32 InLevel);
	void SetMapName(FString InMapName);
	void SetLoadSlotName(FString InSlotName);

	FString GetPlayerName() const { return PlayerName; }
	int32 GetLevel() const { return Level;}
	FString GetMapName() const { return MapName; }
	FString GetLoadSlotName() const {return LoadSlotName;}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #1e1e1e; color: #dcdcdc;&quot;&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void UMVVM_LoadSlot::SetLoadSlotName(FString InSlotName)
{
    UE_MVVM_SET_PROPERTY_VALUE(LoadSlotName, InSlotName);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1-2) 뷰 모델 내의 필드 노티파이 변수 바인드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WBP_LoadSlot 의 여러 종류 위젯 내에 텍스트 박스를 만들고 위의 슬롯 이름을 바인드한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1493&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OyqQY/btsO9VBDdPy/2My5jOrHOCtPs9D1Dmlp90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OyqQY/btsO9VBDdPy/2My5jOrHOCtPs9D1Dmlp90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OyqQY/btsO9VBDdPy/2My5jOrHOCtPs9D1Dmlp90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOyqQY%2FbtsO9VBDdPy%2F2My5jOrHOCtPs9D1Dmlp90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1493&quot; height=&quot;354&quot; data-origin-width=&quot;1493&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) WBP_LoadScreen&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;165&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NhF5Z/btsO8lBOZw9/K0vWvhkBZ9dMZ1L78oSUfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NhF5Z/btsO8lBOZw9/K0vWvhkBZ9dMZ1L78oSUfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NhF5Z/btsO8lBOZw9/K0vWvhkBZ9dMZ1L78oSUfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNhF5Z%2FbtsO8lBOZw9%2FK0vWvhkBZ9dMZ1L78oSUfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;637&quot; height=&quot;165&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;165&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 할당 된 BP Load Screen View Model이 null로 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 위젯의 뷰 모델은 Property Path로 설정되어 있어 특정 함수로 뷰 모델을 감지하는데, 뷰 모델의 필드 노티파이가 바인드 되지 않았기 때문에 뷰 모델이 null로 나타나고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 뷰 모델 바인드를 진행한다.&lt;/p&gt;</description>
      <category>UE 5 스터디/Gameplay Ability System(GAS)</category>
      <category>bug</category>
      <category>c++</category>
      <category>MVVM</category>
      <category>ue 5.3</category>
      <category>unreal</category>
      <author>Crat3</author>
      <guid isPermaLink="true">https://crat.tistory.com/593</guid>
      <comments>https://crat.tistory.com/593#entry593comment</comments>
      <pubDate>Tue, 8 Jul 2025 19:42:39 +0900</pubDate>
    </item>
    <item>
      <title>32-15. 로그 라이크 - (7) 데미지 계산(ExecCalc) - (1) 속성 추가 데미지 업그레이드</title>
      <link>https://crat.tistory.com/592</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;0. 개요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성 데미지를 증가시키는 게임플레이 어빌리티 업그레이드 태그를 가지고 있으면 해당 스펠 데미지를 증가시키도록 데미지 계산 식을 수정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 속성 추가 데미지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) Native 게임플레이 태그 생성&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1e1e; color: #dcdcdc;&quot;&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;FGameplayTag Upgrades_Fire_Increase10PercentDamage;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #1e1e1e; color: #dcdcdc;&quot;&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;FGameplayTag Upgrades_Arcane_Increase10PercentDamage;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #1e1e1e; color: #dcdcdc;&quot;&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;FGameplayTag Upgrades_Lightning_Increase10PercentDamage;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1751446641005&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	GameplayTags.Upgrades_Fire_Increase10PercentDamage = UGameplayTagsManager::Get().AddNativeGameplayTag(
	FName(&quot;Upgrades.Fire.Increase10PercentDamage&quot;),
	FString(&quot;Increase Damage of Fire SpellDamage&quot;)
	);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1751446651869&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	GameplayTags.Upgrades_Arcane_Increase10PercentDamage = UGameplayTagsManager::Get().AddNativeGameplayTag(
	FName(&quot;Upgrades.Arcane.Increase10PercentDamage&quot;),
	FString(&quot;Increase Damage of Arcane SpellDamage&quot;)
	);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1751446661069&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	GameplayTags.Upgrades_Lightning_Increase10PercentDamage = UGameplayTagsManager::Get().AddNativeGameplayTag(
	FName(&quot;Upgrades.Lightning.Increase10PercentDamage&quot;),
	FString(&quot;Increase Damage of Lightning SpellDamage&quot;)
	);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) ExecCalc_Damage의 Execute_Implementation 수정&lt;/p&gt;
&lt;pre id=&quot;code_1751446703101&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;		// 속성 저항이 데미지를 퍼센트로 무시함
		DamageTypeValue *= (100.f - Resistance) / 100.f;
		Damage += DamageTypeValue;

		if (AAuraCharacter* AuraCharacter = Cast&amp;lt;AAuraCharacter&amp;gt;(SourceAvatar))
		{
			if (AAuraPlayerState* AuraPS = AuraCharacter-&amp;gt;GetPlayerState&amp;lt;AAuraPlayerState&amp;gt;())
			{
				// 화염 데미지 증가
				if (AuraPS-&amp;gt;HasUpgradeTag(Tags.Upgrades_Fire_Increase10PercentDamage))
				{
					int Stacks = AuraPS-&amp;gt;GetUpgradeTagCount(Tags.Upgrades_Fire_Increase10PercentDamage);

					Damage += Damage * 0.1f * Stacks;
				}
				// 전기 데미지 증가
				if (AuraPS-&amp;gt;HasUpgradeTag(Tags.Upgrades_Lightning_Increase10PercentDamage))
				{
					int Stacks = AuraPS-&amp;gt;GetUpgradeTagCount(Tags.Upgrades_Lightning_Increase10PercentDamage);

					Damage += Damage * 0.1f * Stacks;
				}
				// 화염 데미지 증가
				if (AuraPS-&amp;gt;HasUpgradeTag(Tags.Upgrades_Arcane_Increase10PercentDamage))
				{
					int Stacks = AuraPS-&amp;gt;GetUpgradeTagCount(Tags.Upgrades_Arcane_Increase10PercentDamage);

					Damage += Damage * 0.1f * Stacks;
				}
			}
		}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>UE 5 스터디/Gameplay Ability System(GAS)</category>
      <category>c++</category>
      <category>execcalc</category>
      <category>unreal</category>
      <category>로그라이크</category>
      <author>Crat3</author>
      <guid isPermaLink="true">https://crat.tistory.com/592</guid>
      <comments>https://crat.tistory.com/592#entry592comment</comments>
      <pubDate>Wed, 2 Jul 2025 17:59:17 +0900</pubDate>
    </item>
    <item>
      <title>24-2. 엔진 업그레이드 - (1) 게임플레이 어빌리티 - (1) 아케인 파편 수정</title>
      <link>https://crat.tistory.com/591</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;0. 개요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향상된 입력 시스템의 변화에 따라 어빌리티 활성화 이후 InputPress 함수가 호출되도록 바뀌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경된 구조를 이용하고 기존의 블루프린트 로직을 C++로 이관한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 범위 표시기 - MagicCircle 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어빌리티 내에서 Show Magic Circle / Hide Magic Circle을 이용하여 BP_MagicCircle을 생성 / 제거하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때, UI에 나타나는 문구는 게임플레이 큐를 이용하는데, 이 게임플레이 큐를 각 어빌리티 내에서 부여하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 로직을 MagicCircle 내로 옮긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) MagicCircle&lt;/p&gt;
&lt;pre id=&quot;code_1751277155440&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCircleInitialized, AActor*, AvatarActor);

UCLASS()
class AURA_API AMagicCircle : public AActor
{
	GENERATED_BODY()
	
public:	
	AMagicCircle();
	virtual void Tick(float DeltaTime) override;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	TObjectPtr&amp;lt;UDecalComponent&amp;gt; MagicCircleDecal;

	UPROPERTY(BlueprintAssignable)
	FOnCircleInitialized CircleInitialized;

	UPROPERTY(BlueprintAssignable)
	FOnCircleInitialized RemoveCircle;
	
protected:
	virtual void BeginPlay() override;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블루프린트 델리게이트를 추가하고 매개 변수로 액터를 넘겨준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) BP_MagicCircle&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;849&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S47Vr/btsOZuqdeU5/wK0WEKxvSPUois8e30nal1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S47Vr/btsOZuqdeU5/wK0WEKxvSPUois8e30nal1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S47Vr/btsOZuqdeU5/wK0WEKxvSPUois8e30nal1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS47Vr%2FbtsOZuqdeU5%2FwK0WEKxvSPUois8e30nal1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;462&quot; height=&quot;458&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;849&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r5Vmj/btsOWfoiY4b/7b5oTISGXfMJuUkkLIB8SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r5Vmj/btsOWfoiY4b/7b5oTISGXfMJuUkkLIB8SK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r5Vmj/btsOWfoiY4b/7b5oTISGXfMJuUkkLIB8SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr5Vmj%2FbtsOWfoiY4b%2F7b5oTISGXfMJuUkkLIB8SK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;608&quot; height=&quot;292&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;529&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;델리게이트에 각각 게임플레이 큐를 추가하는 이벤트와 태그를 제거하는 이벤트를 바인딩한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 아케인 파편 어빌리티 - C++&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블루프린트 이벤트 그래프의 로직 대부분을 C++ 코드로 변경한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 마우스 적중 결과 처리 함수&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1e1e; color: #dcdcdc;&quot;&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void UArcaneShards::ReceivedMouseHitResult(const FGameplayAbilityTargetDataHandle&amp;amp; TargetDataHandle)
{
    if (!TargetDataHandle.IsValid(0))
       return;

    const FGameplayAbilityTargetData* TargetData = TargetDataHandle.Get(0);
    const FHitResult* HitResult = TargetData-&amp;gt;GetHitResult();

    // 마우스 히트 정보를 멤버 변수로 만듬
    CurrentMouseLocation = HitResult-&amp;gt;ImpactPoint;
    NumPoints = GetAbilityLevel();

    GroundPoints.Empty();

    // 어빌리티 업그레이드 확인
    CheckAbilityUpgrades(FAuraGameplayTags::Get().Abilities_Arcane_ArcaneShards);
    
    FTransform PointCollectionTransform;
    PointCollectionTransform.SetLocation(CurrentMouseLocation);

    PointCollection = GetWorld()-&amp;gt;SpawnActorDeferred&amp;lt;APointCollection&amp;gt;(
       PointCollectionClass,
       PointCollectionTransform,
       GetAvatarActorFromActorInfo(),
       nullptr,
       ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

    PointCollection-&amp;gt;FinishSpawning(PointCollectionTransform);

    GroundPoints = PointCollection-&amp;gt;GetGroundPoints(FMath::Min(NumPoints, MaxNumShards));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TargetDataUnderMouse가 반환하는 데이터 핸들을 직접 받아 바닥에 포인트 컬렉션을 생성하는 작업까지 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 타이머 설정 함수&lt;/p&gt;
&lt;pre id=&quot;code_1751363347856&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UArcaneShards::ReadyToSpawnShards()
{
	AActor* AvatarActor = GetAvatarActorFromActorInfo();

	// 범위 표시기 제거
	if (AvatarActor-&amp;gt;Implements&amp;lt;UPlayerInterface&amp;gt;())
	{
		IPlayerInterface::Execute_HideMagicCircle(AvatarActor);
	}

	// UI 범위기 메시지 제거
	RemoveRangeSpellHelpMessage(AvatarActor);
	
	if (UWorld* World = GetWorld())
	{
		World-&amp;gt;GetTimerManager().SetTimer(ShardSpawnTimer, this, &amp;amp;UArcaneShards::SpawnShards,SpawnShardsDeltaTime, true);
	}

	// 쿨다운, 코스트 적용
	CommitAbility(GetCurrentAbilitySpecHandle(),
		GetCurrentActorInfo(),
		GetCurrentActivationInfo());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범위 표시기를 제거하고 타이머를 생성하여 DeltaTime 변수에 따라서 파편 기둥을 소환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) 파편 기둥 소환 - 타이머 핸들의 콜백 함수&lt;/p&gt;
&lt;pre id=&quot;code_1751363406618&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UArcaneShards::SpawnShards()
{
	AActor* AvatarActor = GetAvatarActorFromActorInfo();
	
	if (Idx &amp;lt; GroundPoints.Num())
	{
		// 파편 위치 계산
		ShardSpawnLocation = GroundPoints[Idx]-&amp;gt;GetComponentTransform().GetLocation();
		ShardSpawnRotation = GroundPoints[Idx]-&amp;gt;GetComponentTransform().GetRotation().Rotator();

		TArray&amp;lt;AActor*&amp;gt; ActorsToIgnore;
		ActorsToIgnore.Empty();
		ActorsToIgnore.Add(AvatarActor);
		
		TArray&amp;lt;AActor*&amp;gt; OutOverlappingActors;
		OutOverlappingActors.Empty();

		// 데미지 입힐 플레이어 계산
		UAuraAbilitySystemLibrary::GetLivePlayersWithinRadius(
			AvatarActor,
			OutOverlappingActors,
			ActorsToIgnore,
			RadialDamageOuterRadius,
			ShardSpawnLocation);

		for (AActor* DamagedActor : OutOverlappingActors)
		{
			if (!IsValid(DamagedActor))
				continue;

			// 아군이라면 건너뛰기
			if (!UAuraAbilitySystemLibrary::IsNotFriend(DamagedActor, AvatarActor))
			{
				continue;
			}

			// 클래스 디폴트를 이용하여 데미지 이펙트 파라미터 생성
			FVector DirectionOverride = DamagedActor-&amp;gt;GetActorLocation() - ShardSpawnLocation;
			FDamageEffectParams Params = MakeDamageEffectParamsFromClassDefaults(
				DamagedActor,
				ShardSpawnLocation,
				true,
				DirectionOverride,
				true,
				DirectionOverride,
				true,
				45.f);

			// 데미지 적용
			UAuraAbilitySystemLibrary::ApplyDamageEffect(Params);
		}
		
		// 아케인 파편 기둥 이펙트 생성
		FGameplayCueParameters CueParams;
		CueParams.Location = ShardSpawnLocation;
		CueParams.Normal = UKismetMathLibrary::GetRightVector(ShardSpawnRotation);
			
		K2_ExecuteGameplayCueWithParams(FGameplayTag::RequestGameplayTag(&quot;GameplayCue.ArcaneShards&quot;), CueParams);
	}
	
	if (Idx &amp;gt;= GroundPoints.Num())
		EndSpawnShards();
	else
		Idx++;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파편 기둥을 하나씩 생성하면서 데미지를 입히고, 기둥 플레이 큐를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4) 파편 소환 종료&lt;/p&gt;
&lt;pre id=&quot;code_1751363444044&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UArcaneShards::EndSpawnShards()
{
	PointCollection-&amp;gt;Destroy();

	EndAbility(
		GetCurrentAbilitySpecHandle(),
		GetCurrentActorInfo(),
		GetCurrentActivationInfo(),
		true, false);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트 컬렉션을 파괴하고 어빌리티를 종료한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(5) EndAbility&lt;/p&gt;
&lt;pre id=&quot;code_1751363500104&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UArcaneShards::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
                               const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
	Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);

	auto* AvatarActor = GetAvatarActorFromActorInfo();
	if (AvatarActor-&amp;gt;Implements&amp;lt;UPlayerInterface&amp;gt;())
	{
		IPlayerInterface::Execute_HideMagicCircle(GetAvatarActorFromActorInfo());
	}
	
	// 데미지 입힘 종료
	if (UWorld* World = GetWorld())
	{
		World-&amp;gt;GetTimerManager().ClearTimer(ShardSpawnTimer);
	}

	// 포인트 컬렉션이 아직도 파괴되지 않았으면 파괴
	if (IsValid(PointCollection))
		PointCollection-&amp;gt;Destroy();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범위 표시기가 사라지지 않았을 경우를 대비하여 범위 표시기를 제거한다. 포인트 컬렉션도 마찬가지이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 아케인 파편 어빌리티 - 블루프린트 이벤트 그래프&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;984&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpyl2b/btsO0vws9Pf/4jYMjJORFkZ1kcQLXOdhq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpyl2b/btsO0vws9Pf/4jYMjJORFkZ1kcQLXOdhq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpyl2b/btsO0vws9Pf/4jYMjJORFkZ1kcQLXOdhq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdpyl2b%2FbtsO0vws9Pf%2F4jYMjJORFkZ1kcQLXOdhq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;984&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;984&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 범위 지정 데칼과 자동 이동 중지 로직은 유지하고, 나머지 로직은 C++ 함수로 대체한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EndAbility 이벤트도 제거한다.&lt;/p&gt;</description>
      <category>UE 5 스터디/Gameplay Ability System(GAS)</category>
      <category>c++</category>
      <category>gas</category>
      <category>ue 5.6</category>
      <category>unreal</category>
      <category>엔진 업그레이드</category>
      <author>Crat3</author>
      <guid isPermaLink="true">https://crat.tistory.com/591</guid>
      <comments>https://crat.tistory.com/591#entry591comment</comments>
      <pubDate>Mon, 30 Jun 2025 20:12:44 +0900</pubDate>
    </item>
    <item>
      <title>24-1. 엔진 업그레이드 - 컴파일 오류 해결</title>
      <link>https://crat.tistory.com/590</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1. Target.cs 변경&lt;/p&gt;
&lt;pre id=&quot;code_1751267094957&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using UnrealBuildTool;
using System.Collections.Generic;

public class AuraEditorTarget : TargetRules
{
	public AuraEditorTarget(TargetInfo Target) : base(Target)
	{
		Type = TargetType.Editor;
		DefaultBuildSettings = BuildSettingsVersion.V5;
		IncludeOrderVersion = EngineIncludeOrderVersion.Latest;
		CppStandard = CppStandardVersion.Cpp20;

		ExtraModuleNames.AddRange( new string[] { &quot;Aura&quot; } );
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- C++ 표준이 C++ 17에서 C++ 20으로 변경되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 빌드 세팅 버전이 V2에서 V5로 변경되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 코드 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) FOverlapResult 구조체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 버전과 다르게 오버랩 된 결과를 사용하기 위해서는 아래의 헤더를 첨부해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1751266595252&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;Engine/OverlapResult.h&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 어빌리티의 게임플레이 태그 컨테이너 접근&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어빌리티 스펙의 어빌리티 태그 컨테이너에 접근할 때, 직접 접근하지 않고 Getter로 접근하도록 변경되었다.&lt;/p&gt;
&lt;pre id=&quot;code_1751266924387&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        FGameplayAbilitySpec LoadedAbilitySpec = FGameplayAbilitySpec(LoadedAbilityClass, Data.AbilityLevel);
        LoadedAbilitySpec.DynamicAbilityTags.AddTag(Data.AbilitySlot);
        LoadedAbilitySpec.DynamicAbilityTags.AddTag(Data.AbilityStatus);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1751267006984&quot; class=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;        FGameplayAbilitySpec LoadedAbilitySpec = FGameplayAbilitySpec(LoadedAbilityClass, Data.AbilityLevel);
        LoadedAbilitySpec-&amp;gt;GetDynamicSpecSourceTags().AddTag(Data.AbilitySlot);
        LoadedAbilitySpec-&amp;gt;GetDynamicSpecSourceTags().AddTag(Data.AbilityStatus);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어빌리티의 에셋 태그 컨테이너 에 접근할 때, 직접 접근하지 않고 Getter로 접근하도록 변경되었다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1e1e; color: #dcdcdc;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void UAuraPassiveAbility::ReceiveDeactivate(const FGameplayTag&amp;amp; AbilityTag)
{
    if (AbilityTags.HasTagExact(AbilityTag))
    {
       EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1751266861356&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UAuraPassiveAbility::ReceiveDeactivate(const FGameplayTag&amp;amp; AbilityTag)
{
	if (GetAssetTags().HasTagExact(AbilityTag))
	{
		EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) 입력 레플리케이션 변경&lt;/p&gt;
&lt;pre id=&quot;code_1751267951469&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, AbilitySpec.Handle, AbilitySpec.ActivationInfo.GetActivationPredictionKey());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향상된 입력(Enhanced Input)의 변경점과 더불어 GAS 내부의 레플리케이션 변경으로 인해 비 인스턴스화 된 어빌리티에 접근할 수 없게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향상된 입력이 변경됨에 따라서 AbilitySpecInputPressed가 ASC에서 호출되면 자동으로 어빌리티의 InputPress 함수가 호출되도록 변경되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4) 게임플레이 이펙트가 부여하는 태그 컨테이너에 접근 변화&lt;/p&gt;
&lt;pre id=&quot;code_1751274126869&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;     Effect-&amp;gt;InheritableOwnedTagsContainer.AddTag(DebuffTag);
    
     // 스턴 상태
     if (DebuffTag.MatchesTagExact(GameplayTags.Debuff_Stun))
     {
         Effect-&amp;gt;InheritableOwnedTagsContainer.AddTag(GameplayTags.Player_Block_CursorTrace);
         Effect-&amp;gt;InheritableOwnedTagsContainer.AddTag(GameplayTags.Player_Block_InputHeld);
         Effect-&amp;gt;InheritableOwnedTagsContainer.AddTag(GameplayTags.Player_Block_InputPressed);
         Effect-&amp;gt;InheritableOwnedTagsContainer.AddTag(GameplayTags.Player_Block_InputReleased);
     }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 게임플레이 이펙트가 액터에 적용되기 전에 특정 태그를 추가하려면 GrantedTag를 활용하도록 변경되었다.&lt;/p&gt;
&lt;pre id=&quot;code_1751274187140&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    FGameplayEffectSpec* MutableSpec = new FGameplayEffectSpec(Effect, EffectContext, 1.f/*Debuff Level*/);
    if (MutableSpec)
    {
        FAuraGameplayEffectContext* AuraContext = static_cast&amp;lt;FAuraGameplayEffectContext*&amp;gt;(EffectContext.Get());
        TSharedPtr&amp;lt;FGameplayTag&amp;gt; DebuffDamageType = MakeShareable(new FGameplayTag(DamageType));
        AuraContext-&amp;gt;SetDamageType(DebuffDamageType);

        // 디버프 태그 적용
        MutableSpec-&amp;gt;DynamicGrantedTags.AddTag(DebuffTag);
        if (DebuffTag.MatchesTagExact(GameplayTags.Debuff_Stun))
        {
            MutableSpec-&amp;gt;DynamicGrantedTags.AddTag(GameplayTags.Player_Block_CursorTrace);
            MutableSpec-&amp;gt;DynamicGrantedTags.AddTag(GameplayTags.Player_Block_InputHeld);
            MutableSpec-&amp;gt;DynamicGrantedTags.AddTag(GameplayTags.Player_Block_InputPressed);
            MutableSpec-&amp;gt;DynamicGrantedTags.AddTag(GameplayTags.Player_Block_InputReleased);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>UE 5 스터디/Gameplay Ability System(GAS)</category>
      <category>c++</category>
      <category>gas</category>
      <category>ue 5.6</category>
      <category>unreal</category>
      <category>엔진 업그레이드</category>
      <author>Crat3</author>
      <guid isPermaLink="true">https://crat.tistory.com/590</guid>
      <comments>https://crat.tistory.com/590#entry590comment</comments>
      <pubDate>Mon, 30 Jun 2025 16:29:28 +0900</pubDate>
    </item>
    <item>
      <title>24. 언리얼 엔진 업그레이드 (UE 5.2 -&amp;gt; UE 5.6)</title>
      <link>https://crat.tistory.com/589</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1. GAS (Gameplay Ability System) 주요 변경점&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;968&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZFfYg/btsOWCDJWC9/FVZ6us2Sa9GOxkbSWVgGW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZFfYg/btsOWCDJWC9/FVZ6us2Sa9GOxkbSWVgGW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZFfYg/btsOWCDJWC9/FVZ6us2Sa9GOxkbSWVgGW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZFfYg%2FbtsOWCDJWC9%2FFVZ6us2Sa9GOxkbSWVgGW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1311&quot; height=&quot;968&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;968&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 쿨다운 게임플레이 이펙트 변화&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1155&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CCRYO/btsOWjKGVk7/iYgn0YHkDCCK65j3gdCbDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CCRYO/btsOWjKGVk7/iYgn0YHkDCCK65j3gdCbDK/img.png&quot; data-alt=&quot;GameplayTag removal in GameplayEffect Component does not work? : r/unrealengine&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CCRYO/btsOWjKGVk7/iYgn0YHkDCCK65j3gdCbDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCCRYO%2FbtsOWjKGVk7%2FiYgn0YHkDCCK65j3gdCbDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1155&quot; height=&quot;198&quot; data-origin-width=&quot;1155&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GameplayTag removal in GameplayEffect Component does not work? : r/unrealengine&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 버전에서는 쿨다운 게임플레이 이펙트를 생성하여 해당 이펙트 내에서 Granted Tag로 쿨다운 태그를 지정하여 어빌리티에 연결하는 방법을 사용했으나, 이제는 쿨다운이 모듈화되어 컴포넌트 배열에서 'Target Tags Gameplay Effect Component'를 추가하여 태그를 할당하고 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 어빌리티 시스템 초기화&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MbeMC/btsOW7crMl6/J7h3PkMCGk4s5iyXZdp591/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MbeMC/btsOW7crMl6/J7h3PkMCGk4s5iyXZdp591/img.png&quot; data-alt=&quot;GAS 예시 프로젝트 
 GitHub - Narxim/Narxim-GAS-Example: A basic setup for using Epic's Gameplay Ability System.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MbeMC/btsOW7crMl6/J7h3PkMCGk4s5iyXZdp591/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMbeMC%2FbtsOW7crMl6%2FJ7h3PkMCGk4s5iyXZdp591%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;526&quot; height=&quot;721&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GAS 예시 프로젝트 
 GitHub - Narxim/Narxim-GAS-Example: A basic setup for using Epic's Gameplay Ability System.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 시작 이후에, 캐릭터의 기본 어트리뷰트를 초기화하는 방법이 새로 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) Native 게임플레이 태그 선언 변화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 버전에서는 DefaultGameplayTags.ini 에 게임플레이 태그를 등록하여 사용하거나, C++ 코드 상에서 정의하여 사용하는 방법이 있었으나, 최근 버전에서는 C++ 코드 상에서 정의하여 사용하는 방법만 유효하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4) Enhanced Input 변경점&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;549&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zYL5C/btsOYFFM2RP/n7xhrHtqPRE8Lw3mYklOCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zYL5C/btsOYFFM2RP/n7xhrHtqPRE8Lw3mYklOCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zYL5C/btsOYFFM2RP/n7xhrHtqPRE8Lw3mYklOCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzYL5C%2FbtsOYFFM2RP%2Fn7xhrHtqPRE8Lw3mYklOCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;897&quot; height=&quot;549&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;549&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Input Action을 태그와 연결하고, 태그가 부여되면 어빌리티를 활성화하는 로직에서 직접 어빌리티가 입력에 반응하도록 변경되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 새로운 어빌리티 태스크 노드가 추가되었다.&lt;/p&gt;</description>
      <category>UE 5 스터디/Gameplay Ability System(GAS)</category>
      <author>Crat3</author>
      <guid isPermaLink="true">https://crat.tistory.com/589</guid>
      <comments>https://crat.tistory.com/589#entry589comment</comments>
      <pubDate>Mon, 30 Jun 2025 15:07:26 +0900</pubDate>
    </item>
    <item>
      <title>32-14. 로그 라이크 - (3) 게임플레이 어빌리티 - (5) 화염 폭풍 스펠</title>
      <link>https://crat.tistory.com/588</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;0. 개요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범위를 지정하여 사용하면 적을 추적함과 동시에 적을 빨아들이며 도트(DoT ; Damage over Time) 데미지를 가하는 화염 폭풍 스펠을 설계한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 게임플레이 어빌리티 기초 설계&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) Native 게임플레이 태그 생성&lt;/p&gt;
&lt;pre id=&quot;code_1750998963570&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	FGameplayTag Abilities_Fire_Firenado;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1750998945369&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	GameplayTags.Abilities_Fire_Firenado = UGameplayTagsManager::Get().AddNativeGameplayTag(
		FName(&quot;Abilities.Fire.Firenado&quot;),
		FString(&quot;Firenado Ability&quot;)
	);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 스펠 메뉴에 어빌리티 태그 할당&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xPlWy/btsOTJJp7TI/6eZkA7rTKrkKyLwLz5VPl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xPlWy/btsOTJJp7TI/6eZkA7rTKrkKyLwLz5VPl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xPlWy/btsOTJJp7TI/6eZkA7rTKrkKyLwLz5VPl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxPlWy%2FbtsOTJJp7TI%2F6eZkA7rTKrkKyLwLz5VPl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1082&quot; height=&quot;416&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) DA_AbilityInfo에 데이터 추가&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;381&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BrTRB/btsOVdIKkB6/cv9vKXjuJ04bdmzwlLKxC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BrTRB/btsOVdIKkB6/cv9vKXjuJ04bdmzwlLKxC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BrTRB/btsOVdIKkB6/cv9vKXjuJ04bdmzwlLKxC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBrTRB%2FbtsOVdIKkB6%2Fcv9vKXjuJ04bdmzwlLKxC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1310&quot; height=&quot;381&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;381&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4) 애님 몽타주 생성 및 모션워핑 노티파이, 게임플레이 큐 노티파이 추가&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1989&quot; data-origin-height=&quot;1004&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EFBUy/btsOVfT6wMN/JKl2OJqwj52LmkilVsO3i1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EFBUy/btsOVfT6wMN/JKl2OJqwj52LmkilVsO3i1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EFBUy/btsOVfT6wMN/JKl2OJqwj52LmkilVsO3i1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEFBUy%2FbtsOVfT6wMN%2FJKl2OJqwj52LmkilVsO3i1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1989&quot; height=&quot;1004&quot; data-origin-width=&quot;1989&quot; data-origin-height=&quot;1004&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 파이어 토네이도 액터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) C++ 코드&lt;/p&gt;
&lt;pre id=&quot;code_1751000237128&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UCapsuleComponent;

UCLASS()
class AURA_API AAuraFireTornado : public AActor
{
	GENERATED_BODY()

	AAuraFireTornado();

protected:
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaSeconds) override;
	virtual void Destroyed() override;

public:
	void SetDamageDeltaSecond(float InTime) {DamageDeltaSecond = InTime;}
	void SetDamageRadius(float InDamageRadius) {DamageRadius = InDamageRadius;}
	void SetFollowRadius(float InFollowRadius) {FollowRadius = InFollowRadius;}

protected:
	UFUNCTION()
	void ApplyDamageAndKnockback();
	
	bool IsValidOverlap(AActor* OtherActor);

public:
	UPROPERTY(BlueprintReadWrite, meta=(ExposeOnSpawn = true))
	FDamageEffectParams DamageEffectParams;
	
protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	TObjectPtr&amp;lt;UCapsuleComponent&amp;gt; Capsule;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TObjectPtr&amp;lt;UStaticMeshComponent&amp;gt; Mesh;
	
	UPROPERTY(VisibleAnywhere)
	TObjectPtr&amp;lt;UAudioComponent&amp;gt; LoopingSoundComponent;
	
private:
	UPROPERTY(EditDefaultsOnly)
	float LifeSpan = 15.f;
	
	UPROPERTY(EditDefaultsOnly)
	float MovementSpeed = 1.f;
	
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta=(AllowPrivateAccess=true))
	float DamageDeltaSecond = 0.2f;

	UPROPERTY(EditDefaultsOnly)
	float FollowRadius = 600.f;
	
	UPROPERTY(EditDefaultsOnly)
	float DamageRadius = 300.f;

	UPROPERTY(EditDefaultsOnly)
	float SpinDegreePerSecond = -360.f;
	
	UPROPERTY(EditAnywhere)
	TObjectPtr&amp;lt;USoundBase&amp;gt; LoopingSound;

	UPROPERTY(BlueprintReadWrite, meta=(AllowPrivateAccess = true))
	TArray&amp;lt;AActor*&amp;gt; OverlappingActors;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데미지 로직이 액터 내에 위치하기 때문에, 적을 끌어오는 범위, 유지 시간, 데미지 범위를 외부에서(게임플레이 어빌리티에서) 설정할 수 있도록 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1-1) 생성자 / 비긴 플레이 / 파괴&lt;/p&gt;
&lt;pre id=&quot;code_1751001408515&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;AAuraFireTornado::AAuraFireTornado()
{
	PrimaryActorTick.bCanEverTick = false;

	Mesh = CreateDefaultSubobject&amp;lt;UStaticMeshComponent&amp;gt;(&quot;Mesh&quot;);
	SetRootComponent(Mesh);

	Capsule = CreateDefaultSubobject&amp;lt;UCapsuleComponent&amp;gt;(&quot;Capsule&quot;);
	
	Capsule-&amp;gt;SetCollisionObjectType(ECC_Projectile);
	Capsule-&amp;gt;SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	Capsule-&amp;gt;SetCollisionResponseToAllChannels(ECR_Ignore);
	Capsule-&amp;gt;SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap);
	Capsule-&amp;gt;SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Overlap);
	Capsule-&amp;gt;SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
	Capsule-&amp;gt;SetupAttachment(GetRootComponent());
}

void AAuraFireTornado::BeginPlay()
{
	Super::BeginPlay();
	SetLifeSpan(LifeSpan);
	SetReplicateMovement(true);
	
	LoopingSoundComponent = UGameplayStatics::SpawnSoundAttached(LoopingSound, GetRootComponent());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트에 대한 기본 설정을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스태틱 메시를 루트로 지정하고 캡슐을 하위로 지정하여 메시 크기에 따라 캡슐도 유동적으로 스케일링되도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1751001507209&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AAuraFireTornado::Destroyed()
{
	if (LoopingSoundComponent)
	{
		LoopingSoundComponent-&amp;gt;Stop();
		LoopingSoundComponent-&amp;gt;DestroyComponent();
	}
	
	Super::Destroyed();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1-2) 틱 - 적을 향해 추적하여 따라가기&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1e1e; color: #dcdcdc;&quot;&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void AAuraFireTornado::Tick(float DeltaSeconds)
{
    Super::Tick(DeltaSeconds);

    FRotator DeltaRotation = FRotator(0.f, SpinDegreePerSecond * DeltaSeconds, 0.f);
    AddActorWorldRotation(DeltaRotation);

    if (!OverlappingActors.IsEmpty())
    {
       AActor* TargetActor = OverlappingActors.Last();

       if (!IsValid(TargetActor))
          return;

       FVector TornadoLocation = GetActorLocation();
       FVector ActorLocation = TargetActor-&amp;gt;GetActorLocation();

       ActorLocation.Z = TornadoLocation.Z;

       FVector NewLocation = FMath::VInterpTo(TornadoLocation, ActorLocation, DeltaSeconds, MovementSpeed);
       SetActorLocation(NewLocation, true);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데미지를 주는 함수 내에서 획득한 Overlapping Actors 배열을 이용하여 가장 외곽에 있는 적을 추적하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선형 보간을 이용하여 서서히 움직이게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1-3) 데미지, 넉백 함수&lt;/p&gt;
&lt;pre id=&quot;code_1751001627595&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AAuraFireTornado::ApplyDamageAndKnockback()
{
	OverlappingActors.Empty();
	
	TArray&amp;lt;AActor*&amp;gt; IgnoreActors;
	IgnoreActors.AddUnique(GetOwner());
	
	UAuraAbilitySystemLibrary::GetLivePlayersWithinRadius(
		this,
		OverlappingActors,
		IgnoreActors,
		FollowRadius,
		GetActorLocation());

	// 1. 중앙으로 끌어 당김
	// 2. 틱 데미지 부여
	// 3. 범위 밖의 액터를 향해 느리게 움직임
	
	for (AActor* OtherActor : OverlappingActors)
	{
		if (IsValidOverlap(OtherActor) == false)
			continue;

		if (HasAuthority())
		{
			if (auto* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
			{
				bool bIsKnockBack = true;
				
				// 벡터를 구하고 정규화
				FVector ToCenter = GetActorLocation() - OtherActor-&amp;gt;GetActorLocation();
				ToCenter.Z = GetActorLocation().Z - Capsule-&amp;gt;GetScaledCapsuleHalfHeight();
				ToCenter.Normalize();
				
				// 보스는 넉백 무효
				if (OtherActor-&amp;gt;ActorHasTag(FName(&quot;Boss&quot;)))
				{
					bIsKnockBack = false;
					DamageEffectParams.KnockbackChance = 0.f;
				}

				if (bIsKnockBack)
				{
					const FVector KnockbackDirection = ToCenter;
					const FVector KnockbackForce = KnockbackDirection * DamageEffectParams.KnockbackForceMagnitude;
					DamageEffectParams.KnockbackForce = KnockbackForce;
				}

				FVector DistanceVector = (OtherActor-&amp;gt;GetActorLocation()-GetActorLocation());
				float Distance = DistanceVector.Length();
				
				if (Distance &amp;lt;= DamageRadius)
				{
					const FVector DeathImpulse = ToCenter * DamageEffectParams.DeathImpulseMagnitude;
					DamageEffectParams.DeathImpulse = DeathImpulse;
					DamageEffectParams.TargetAbilitySystemComponent = TargetASC;
				
					UAuraAbilitySystemLibrary::ApplyDamageEffect(DamageEffectParams);
				}
			}
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 파이어 토네이도 액터 주위로 '견인 범위'를 검사하여 겹치는 액터를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 보스가 아닌 적을 대상으로 파이어 토네이도 액터 중심으로 넉백 벡터를 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 파이어 토네이도 액터와 대상 사이의 거리를 체크하여 '데미지 범위' 내에 있으면 데미지 게임플레이 이펙트를 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1-4) 겹침 판정 검증 함수&lt;/p&gt;
&lt;pre id=&quot;code_1751001770274&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bool AAuraFireTornado::IsValidOverlap(AActor* OtherActor)
{
	if (DamageEffectParams.SourceAbilitySystemComponent == nullptr)
		return false;

	AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent-&amp;gt;GetAvatarActor();

	if (SourceAvatarActor == OtherActor)
		return false;

	if (!UAuraAbilitySystemLibrary::IsNotFriend(SourceAvatarActor, OtherActor))
		return false;

	return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인과 아군 대상으로는 데미지를 입힐 수 없도록 함수를 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1-5) 타이머 설정&lt;/p&gt;
&lt;pre id=&quot;code_1751002451750&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	FTimerHandle TimerHandle;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1751002466626&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AAuraFireTornado::BeginPlay()
{
	Super::BeginPlay();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// 타이머 설정
	FTimerDelegate TimerDelegate;
	TimerDelegate.BindUFunction(this, FName(&quot;ApplyDamageAndKnockback&quot;));

	GetWorldTimerManager().SetTimer(
		TimerHandle,
		TimerDelegate,
		DamageDeltaSecond,
		true
		);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데미지 델타 초마다 콜백 함수가 호출되도록 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 블루프린트 생성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;893&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbHK9r/btsOVEfaBrT/1uzFgDANmUQhMXZfMVlbok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbHK9r/btsOVEfaBrT/1uzFgDANmUQhMXZfMVlbok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbHK9r/btsOVEfaBrT/1uzFgDANmUQhMXZfMVlbok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbHK9r%2FbtsOVEfaBrT%2F1uzFgDANmUQhMXZfMVlbok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2012&quot; height=&quot;893&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;893&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간에 따라 제자리 회전하도록 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시를 추가하고 머티리얼을 반투명한 재질로 변경한다. 이후 나이아가라 시스템을 추가하고 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 화염 폭풍 게임플레이 어빌리티&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) C++ 코드&lt;/p&gt;
&lt;pre id=&quot;code_1751003268701&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class AAuraFireTornado;

UCLASS()
class AURA_API UAuraFirenado : public UAuraDamageGameplayAbility
{
	GENERATED_BODY()

public:
	virtual FString GetDescription(int32 Level, const UObject* WorldContextObject);
	virtual FString GetNextLevelDescription(int32 Level, const UObject* WorldContextObject);

	UFUNCTION(BlueprintCallable)
	AAuraFireTornado* SpawnTornado();

	UFUNCTION(BlueprintCallable)
	AAuraFireTornado* SpawnTornadoToLocation(const FVector&amp;amp; Location);
	
private:
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf&amp;lt;AAuraFireTornado&amp;gt; FireTornadoClass;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta=(AllowPrivateAccess=true))
	float DamageDeltaSecond = 0.2f;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta=(AllowPrivateAccess=true))
	float SpawnTime = 5.f;
	
	UPROPERTY(EditDefaultsOnly)
	float FollowRadius = 600.f;
	
	UPROPERTY(EditDefaultsOnly)
	float DamageRadius = 300.f;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1-1) 스펠 설명&lt;/p&gt;
&lt;pre id=&quot;code_1751003288658&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FString UAuraFirenado::GetDescription(int32 Level, const UObject* WorldContextObject)
{
	const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
	const float ManaCost = FMath::Abs(GetManaCost(Level));
	const float Cooldown = GetCoolDown(Level);
	const float MagicAttackPower = UAuraAbilitySystemLibrary::GetAttributeValue(WorldContextObject, FAuraGameplayTags::Get().Attributes_Secondary_MagicAttackPower);
	const int32 MagicPowerDamage = MagicAttackPower * MagicPowerCoefficient.GetValue();
	
	return FString::Printf(TEXT(
		&quot;&amp;lt;Title&amp;gt;화염 폭풍&amp;lt;/&amp;gt;\n&amp;lt;Small&amp;gt;레벨 &amp;lt;/&amp;gt;&amp;lt;Level&amp;gt;%d&amp;lt;/&amp;gt;\n&amp;lt;Small&amp;gt;마나 &amp;lt;/&amp;gt;&amp;lt;ManaCost&amp;gt;%.1f&amp;lt;/&amp;gt;\n&amp;lt;Small&amp;gt;쿨타임 &amp;lt;/&amp;gt;&amp;lt;Cooldown&amp;gt;%.1f&amp;lt;/&amp;gt;\n&amp;lt;Default&amp;gt;해당 범위에 총 &amp;lt;/&amp;gt;&amp;lt;Damage&amp;gt;%d&amp;lt;/&amp;gt;&amp;lt;Default&amp;gt;의 피해를 입히는 화염 기둥을 &amp;lt;/&amp;gt;&amp;lt;Num&amp;gt;%.1f초&amp;lt;/&amp;gt;&amp;lt;Default&amp;gt; 동안 소환합니다.&amp;lt;/&amp;gt;\n&amp;lt;Small&amp;gt;최대 %.f의 거리에 있는 적을 추적하며, %.f 거리 내의 적에게 데미지를 입힙니다.&amp;lt;/&amp;gt;&quot;),
		Level,
		ManaCost,
		Cooldown,
		ScaledDamage + MagicPowerDamage,
		SpawnTime,
		FollowRadius,
		DamageRadius
	);
}

FString UAuraFirenado::GetNextLevelDescription(int32 Level, const UObject* WorldContextObject)
{
	const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
	const float ManaCost = FMath::Abs(GetManaCost(Level));
	const float Cooldown = GetCoolDown(Level);
	const float MagicAttackPower = UAuraAbilitySystemLibrary::GetAttributeValue(WorldContextObject, FAuraGameplayTags::Get().Attributes_Secondary_MagicAttackPower);
	const int32 MagicPowerDamage = MagicAttackPower * MagicPowerCoefficient.GetValue();
	
	return FString::Printf(TEXT(
		&quot;&amp;lt;Title&amp;gt;다음 레벨: &amp;lt;/&amp;gt;\n&amp;lt;Small&amp;gt;레벨 &amp;lt;/&amp;gt;&amp;lt;Level&amp;gt;%d&amp;lt;/&amp;gt;\n&amp;lt;Small&amp;gt;마나 &amp;lt;/&amp;gt;&amp;lt;ManaCost&amp;gt;%.1f&amp;lt;/&amp;gt;\n&amp;lt;Small&amp;gt;쿨타임 &amp;lt;/&amp;gt;&amp;lt;Cooldown&amp;gt;%.1f&amp;lt;/&amp;gt;\n&amp;lt;Default&amp;gt;해당 범위에 총 &amp;lt;/&amp;gt;&amp;lt;Damage&amp;gt;%d&amp;lt;/&amp;gt;&amp;lt;Default&amp;gt;의 피해를 입히는 화염 기둥을 &amp;lt;/&amp;gt;&amp;lt;Num&amp;gt;%.1f초&amp;lt;/&amp;gt;&amp;lt;Default&amp;gt; 동안 소환합니다.&amp;lt;/&amp;gt;\n&amp;lt;Small&amp;gt;최대 %.f의 거리에 있는 적을 추적하며, %.f 거리 내의 적에게 데미지를 입힙니다.&amp;lt;/&amp;gt;&quot;),
		Level,
		ManaCost,
		Cooldown,
		ScaledDamage + MagicPowerDamage,
		SpawnTime,
		FollowRadius,
		DamageRadius
	);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1-2) 파이어 토네이도 위치에 소환하는 함수&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1e1e; color: #dcdcdc;&quot;&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;AAuraFireTornado* UAuraFirenado::SpawnTornadoToLocation(const FVector&amp;amp; Location)
{
    FTransform SpawnTransform;
    SpawnTransform.SetLocation(Location);

    // 월드에 파이어볼 생성
    AAuraFireTornado* Tornado = GetWorld()-&amp;gt;SpawnActorDeferred&amp;lt;AAuraFireTornado&amp;gt;(
       FireTornadoClass,
       SpawnTransform,
       GetOwningActorFromActorInfo(),
       CurrentActorInfo-&amp;gt;PlayerController-&amp;gt;GetPawn(),
       ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

    Tornado-&amp;gt;SetDamageDeltaSecond(DamageDeltaSecond);
    Tornado-&amp;gt;SetFollowRadius(FollowRadius);
    Tornado-&amp;gt;SetDamageRadius(DamageRadius);
    
    Tornado-&amp;gt;DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
    Tornado-&amp;gt;SetOwner(GetAvatarActorFromActorInfo());

    Tornado-&amp;gt;FinishSpawning(SpawnTransform);

    return Tornado;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 게임플레이 어빌리티 블루프린트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 클래스 디폴트&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;919&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xf1jc/btsOTt0OFj6/IE80DEajzgO8dPSrfFtlj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xf1jc/btsOTt0OFj6/IE80DEajzgO8dPSrfFtlj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xf1jc/btsOTt0OFj6/IE80DEajzgO8dPSrfFtlj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxf1jc%2FbtsOTt0OFj6%2FIE80DEajzgO8dPSrfFtlj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;351&quot; height=&quot;641&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;919&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이어 토네이도 클래스를 지정하고 값을 작성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;221&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oag1I/btsOVIIBRqi/AshWZ01VlUe9sExBauputK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oag1I/btsOVIIBRqi/AshWZ01VlUe9sExBauputK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oag1I/btsOVIIBRqi/AshWZ01VlUe9sExBauputK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foag1I%2FbtsOVIIBRqi%2FAshWZ01VlUe9sExBauputK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;179&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;221&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범위 지정 데칼을 사용하기 때문에 Activation Owned Tags에 WaitForExecute 태그를 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 범위 지정 데칼 사용 및 오토 런 중지&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1713&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqCxMm/btsOUcrq02M/vvRXKS9jKEdUeRluPBoT61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqCxMm/btsOUcrq02M/vvRXKS9jKEdUeRluPBoT61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqCxMm/btsOUcrq02M/vvRXKS9jKEdUeRluPBoT61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqCxMm%2FbtsOUcrq02M%2FvvRXKS9jKEdUeRluPBoT61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1713&quot; height=&quot;411&quot; data-origin-width=&quot;1713&quot; data-origin-height=&quot;411&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI1Nfj/btsOUGZP5jz/atXhlHzUimgAA0016XzTUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI1Nfj/btsOUGZP5jz/atXhlHzUimgAA0016XzTUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI1Nfj/btsOUGZP5jz/atXhlHzUimgAA0016XzTUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI1Nfj%2FbtsOUGZP5jz%2FatXhlHzUimgAA0016XzTUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;462&quot; height=&quot;288&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 게임플레이 큐를 적용하면 DA_MessageInfo를 통해 등록된 메시지가 출력된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcfeuu/btsOVtd6Lay/H0Y6nxXK863rLl1elcKSqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcfeuu/btsOVtd6Lay/H0Y6nxXK863rLl1elcKSqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcfeuu/btsOVtd6Lay/H0Y6nxXK863rLl1elcKSqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcfeuu%2FbtsOVtd6Lay%2FH0Y6nxXK863rLl1elcKSqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;543&quot; height=&quot;224&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 마우스 액션 검증&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1703&quot; data-origin-height=&quot;653&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx2rEa/btsOVa0wiju/EOkrOtP9lJ7IpKZWw05UaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx2rEa/btsOVa0wiju/EOkrOtP9lJ7IpKZWw05UaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx2rEa/btsOVa0wiju/EOkrOtP9lJ7IpKZWw05UaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx2rEa%2FbtsOVa0wiju%2FEOkrOtP9lJ7IpKZWw05UaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1703&quot; height=&quot;653&quot; data-origin-width=&quot;1703&quot; data-origin-height=&quot;653&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마우스 입력, 혹은 어빌리티가 할당된 키가 눌리면 마우스 위치를 저장하고 모션 워핑을 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애님 몽타주를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) 노티파이 대기 - 토네이도 소환&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mAaZN/btsOUa1ohAt/3rmycTJGaYad0fFbaWJMmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mAaZN/btsOUa1ohAt/3rmycTJGaYad0fFbaWJMmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mAaZN/btsOUa1ohAt/3rmycTJGaYad0fFbaWJMmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmAaZN%2FbtsOUa1ohAt%2F3rmycTJGaYad0fFbaWJMmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;865&quot; height=&quot;346&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽타주 내의 노티파이를 대기하는 노드를 추가하고, 마우스 위치에 토네이도를 소환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4) 토네이도 위치 조정, 소환 시간 설정, 이펙트 출력, 어빌리티 종료&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;507&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpf7E8/btsOUEntrru/6oZdcGNtcVxXS1wkiNli11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpf7E8/btsOUEntrru/6oZdcGNtcVxXS1wkiNli11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpf7E8/btsOUEntrru/6oZdcGNtcVxXS1wkiNli11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpf7E8%2FbtsOUEntrru%2F6oZdcGNtcVxXS1wkiNli11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1710&quot; height=&quot;507&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 로직을 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(5) EndAbility&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2053&quot; data-origin-height=&quot;241&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqnozi/btsOUr9K4Eu/wPDrNDC1aloUZaKa6ImBK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqnozi/btsOUr9K4Eu/wPDrNDC1aloUZaKa6ImBK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqnozi/btsOUr9K4Eu/wPDrNDC1aloUZaKa6ImBK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbqnozi%2FbtsOUr9K4Eu%2FwPDrNDC1aloUZaKa6ImBK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2053&quot; height=&quot;241&quot; data-origin-width=&quot;2053&quot; data-origin-height=&quot;241&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어빌리티가 종료될 때 다시 한 번 캐릭터의 오토 런을 중단하고 타이머 핸들을 초기화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>UE 5 스터디/Gameplay Ability System(GAS)</category>
      <category>c++</category>
      <category>gas</category>
      <category>unreal</category>
      <category>게임플레이 어빌리티</category>
      <category>로그라이크</category>
      <author>Crat3</author>
      <guid isPermaLink="true">https://crat.tistory.com/588</guid>
      <comments>https://crat.tistory.com/588#entry588comment</comments>
      <pubDate>Fri, 27 Jun 2025 15:03:31 +0900</pubDate>
    </item>
    <item>
      <title>32-13. 로그 라이크 - (6) 보스 - (3) 보스 속성 개선, 공격력 개선, 스펠 개선</title>
      <link>https://crat.tistory.com/586</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 보스 체력 및 기본 속성(Attributes) 개선&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 각 캐릭터 블루프린트의 Attribute Gameplay Effects를 지정해도 반영이 되지 않는 버그가 있었는데, 이는 캐릭터 클래스 인포 데이터 에셋을 참조해서 공용 게임플레이 이펙트를 공유하기 때문이었다. 따라서 아래 부분을 변경한다.&lt;/p&gt;
&lt;pre id=&quot;code_1750325231412&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UAuraAbilitySystemLibrary::InitializeDefaultAttributes(const UObject* WorldContextObject, ECharacterClass CharacterClass, float Level, UAbilitySystemComponent* ASC)
{
	AActor* AvatarActor = ASC-&amp;gt;GetAvatarActor();
	AAuraCharacterBase* AvatarCharacter = Cast&amp;lt;AAuraCharacterBase&amp;gt;(AvatarActor);
	
	// 액터의 클래스 정보 가져오기
	UCharacterClassInfo* CharacterClassInfo = GetCharacterClassInfo(AvatarActor);
	if (CharacterClassInfo == nullptr)
		return;

	FCharacterClassDefaultInfo ClassDefaultInfo = CharacterClassInfo-&amp;gt;GetClassDefaultInfo(CharacterClass);

	

	// 이펙트 스펙을 위한 컨텍스트 핸들 생성
	FGameplayEffectContextHandle PrimaryAttributesContextHandle = ASC-&amp;gt;MakeEffectContext();
	PrimaryAttributesContextHandle.AddSourceObject(AvatarActor);

	FGameplayEffectContextHandle SecondaryAttributesContextHandle = ASC-&amp;gt;MakeEffectContext();
	SecondaryAttributesContextHandle.AddSourceObject(AvatarActor);

	FGameplayEffectContextHandle VitalAttributesContextHandle = ASC-&amp;gt;MakeEffectContext();
	VitalAttributesContextHandle.AddSourceObject(AvatarActor);

	// 이펙트 적용을 위한 이펙트 스펙 생성 후 적용 - ClassClassInfo가 아닌 클래스 디폴트를 참조하도록 수정됨
	const FGameplayEffectSpecHandle PrimaryAttributesSpecHandle = ASC-&amp;gt;MakeOutgoingSpec(AvatarCharacter-&amp;gt;DefaultPrimaryAttributes, Level, PrimaryAttributesContextHandle);
	ASC-&amp;gt;ApplyGameplayEffectSpecToSelf(*PrimaryAttributesSpecHandle.Data.Get());

	const FGameplayEffectSpecHandle SecondaryAttributesSpecHandle = ASC-&amp;gt;MakeOutgoingSpec(AvatarCharacter-&amp;gt;DefaultSecondaryAttributes, Level, SecondaryAttributesContextHandle);
	ASC-&amp;gt;ApplyGameplayEffectSpecToSelf(*SecondaryAttributesSpecHandle.Data.Get());

	const FGameplayEffectSpecHandle VitalAttributesSpecHandle = ASC-&amp;gt;MakeOutgoingSpec(CharacterClassInfo-&amp;gt;VitalAttributes, Level, VitalAttributesContextHandle);
	ASC-&amp;gt;ApplyGameplayEffectSpecToSelf(*VitalAttributesSpecHandle.Data.Get());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 보스 전용 PrimaryAttributes 와 SecondaryAttributes 게임플레이 이펙트를 만들어 클래스 디폴트에 세팅한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 보스 공격력 개선&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) GE_SecondaryAttributes_Boss_Shaman에서 마법 공격력 계산&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpZh2g/btsOIzM99h6/i52CLk6bwkFNiPFvqpH6J1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpZh2g/btsOIzM99h6/i52CLk6bwkFNiPFvqpH6J1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpZh2g/btsOIzM99h6/i52CLk6bwkFNiPFvqpH6J1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpZh2g%2FbtsOIzM99h6%2Fi52CLk6bwkFNiPFvqpH6J1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1636&quot; height=&quot;442&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) GA_EnemyFirebolt / GA_EnemyArcaneShards에 마법 공격력 계수 할당&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi7qMI/btsOG9aLWHr/wQVXjmf1xUIPaSEny2kIuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi7qMI/btsOG9aLWHr/wQVXjmf1xUIPaSEny2kIuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi7qMI/btsOG9aLWHr/wQVXjmf1xUIPaSEny2kIuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi7qMI%2FbtsOG9aLWHr%2FwQVXjmf1xUIPaSEny2kIuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;504&quot; height=&quot;178&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCFYBl/btsOH8hYCMh/DuICDTcfU1gNHsl3m3aP30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCFYBl/btsOH8hYCMh/DuICDTcfU1gNHsl3m3aP30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCFYBl/btsOH8hYCMh/DuICDTcfU1gNHsl3m3aP30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCFYBl%2FbtsOH8hYCMh%2FDuICDTcfU1gNHsl3m3aP30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;464&quot; height=&quot;179&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 보스 소환 스펠 개선&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보스가 소환수를 소환할 때 플레이어가 맵 끝에 있으면, 공중에 소환수를 소환하게 되어 맵 밖에 소환이 되는 버그가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1750330119060&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;TArray&amp;lt;FVector&amp;gt; UAuraSummonAbility::GetSpawnLocations()
{
	const FVector Forward = GetAvatarActorFromActorInfo()-&amp;gt;GetActorForwardVector();
	const FVector Location = GetAvatarActorFromActorInfo()-&amp;gt;GetActorLocation();

	int Attempt = 0;
	
	const float DeltaSpread = SpawnSpread / NumMinions;
	const FVector LeftSpread = Forward.RotateAngleAxis(-SpawnSpread / 2, FVector::UpVector);
	TArray&amp;lt;FVector&amp;gt; SpawnLocations;

	int idx = 0;
	
	while ( idx &amp;lt; NumMinions )
	{
		const FVector Direction = LeftSpread.RotateAngleAxis(DeltaSpread * idx, FVector::UpVector);
		FVector DirectionVector = Direction * FMath::FRandRange(MinSpawnDistance, MaxSpawnDistance - 50.f * Attempt);
		FVector ChosenSpawnLocation = Location + DirectionVector;

		FHitResult Hit;
			
		if (GetWorld()-&amp;gt;LineTraceSingleByChannel(Hit, ChosenSpawnLocation + FVector(0.f, 0.f, 400.f), ChosenSpawnLocation - FVector(0.f, 0.f, 400.f), ECC_Visibility))
		{
			if (Hit.bBlockingHit)
			{
				ChosenSpawnLocation = Hit.ImpactPoint;
				SpawnLocations.Add(ChosenSpawnLocation);
				idx++;
			}
		}
			
		Attempt ++;

		if (Attempt &amp;gt; 15)
			break;
	}
	return SpawnLocations;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 for 루프를 while 루프로 대체하고, 라인 트레이스의 불리언 값을 이용해 충돌이 실패했을 때 다시 값을 뽑고, 최대 소환 거리에서 50 * 시도 횟수 만큼의 거리를 빼어 라인 트레이스를 다시 계산한다.&lt;/p&gt;</description>
      <category>UE 5 스터디/Gameplay Ability System(GAS)</category>
      <category>c++</category>
      <category>unreal</category>
      <category>로그라이크</category>
      <category>언리얼</category>
      <author>Crat3</author>
      <guid isPermaLink="true">https://crat.tistory.com/586</guid>
      <comments>https://crat.tistory.com/586#entry586comment</comments>
      <pubDate>Thu, 19 Jun 2025 18:39:52 +0900</pubDate>
    </item>
    <item>
      <title>32-12. 로그 라이크 - (6) 보스 - (2) 보스 체력 바</title>
      <link>https://crat.tistory.com/585</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;0. 개요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보스 전용 체력 바를 만들고, 전투가 활성화되면 보스의 체력 바를 생성하고 업데이트한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 보스 체력 바&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) WBP_EnemyBoss_ProgressBar 생성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdkOl7/btsOGOYn64l/W6Fn9eSkkoDfNZIDLRpLKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdkOl7/btsOGOYn64l/W6Fn9eSkkoDfNZIDLRpLKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdkOl7/btsOGOYn64l/W6Fn9eSkkoDfNZIDLRpLKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdkOl7%2FbtsOGOYn64l%2FW6Fn9eSkkoDfNZIDLRpLKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1533&quot; height=&quot;230&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2035&quot; data-origin-height=&quot;717&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qXmuL/btsOHDJmm37/sK7dtUMlaBl1TdBdxcUFik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qXmuL/btsOHDJmm37/sK7dtUMlaBl1TdBdxcUFik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qXmuL/btsOHDJmm37/sK7dtUMlaBl1TdBdxcUFik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqXmuL%2FbtsOHDJmm37%2FsK7dtUMlaBl1TdBdxcUFik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2035&quot; height=&quot;717&quot; data-origin-width=&quot;2035&quot; data-origin-height=&quot;717&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 WBP_ProgressBar의 로직을 그대로 복사하여 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) WBP_EnemyBoss_HealthBar 생성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1235&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EX6V0/btsOHJvPvs2/9I1ymkNz8UTOuPoP3GeQmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EX6V0/btsOHJvPvs2/9I1ymkNz8UTOuPoP3GeQmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EX6V0/btsOHJvPvs2/9I1ymkNz8UTOuPoP3GeQmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEX6V0%2FbtsOHJvPvs2%2F9I1ymkNz8UTOuPoP3GeQmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1235&quot; height=&quot;715&quot; data-origin-width=&quot;1235&quot; data-origin-height=&quot;715&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 체력 바의 WBP_PictureFrame이 존재하고, 이 이미지를 체력바의 위젯 컨트롤러인 AuraEnemy의 멤버 변수로 추가하고 해당 변수를 가져와 초기화하도록 할 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1750320965418&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	TObjectPtr&amp;lt;UTexture&amp;gt; PictureFrameImage;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보스 블루프린트의 클래스 디테일에서 텍스처를 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2-1) Construct 이벤트&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1635&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F3o0T/btsOIppG3WR/6JMBJY7F7ZMnTScy7mertk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F3o0T/btsOIppG3WR/6JMBJY7F7ZMnTScy7mertk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F3o0T/btsOIppG3WR/6JMBJY7F7ZMnTScy7mertk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF3o0T%2FbtsOIppG3WR%2F6JMBJY7F7ZMnTScy7mertk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1635&quot; height=&quot;244&quot; data-origin-width=&quot;1635&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 체력 바와는 다르게 작동하기 때문에, 생성된 직후에 체력과 최대 체력을 설정하도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1709&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc3k5p/btsOIXAk8yW/48zSgHfTN5kRXuEYTJhJuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc3k5p/btsOIXAk8yW/48zSgHfTN5kRXuEYTJhJuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc3k5p/btsOIXAk8yW/48zSgHfTN5kRXuEYTJhJuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc3k5p%2FbtsOIXAk8yW%2F48zSgHfTN5kRXuEYTJhJuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1709&quot; height=&quot;916&quot; data-origin-width=&quot;1709&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 체력 변화와 최대 체력 변화 델리게이트를 바인딩한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2-2) 보스 몬스터 사망 시 즉시 위젯 제거&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dx0joa/btsOJOoPrVk/Zn5tiKNPsUaFQvah7mAaRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dx0joa/btsOJOoPrVk/Zn5tiKNPsUaFQvah7mAaRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dx0joa/btsOJOoPrVk/Zn5tiKNPsUaFQvah7mAaRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdx0joa%2FbtsOJOoPrVk%2FZn5tiKNPsUaFQvah7mAaRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1300&quot; height=&quot;459&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) BTS_UpdateBossHealthBar - BT Services 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 체력 바 로직은 각 캐릭터의 BeginPlay에서 컴포넌트로 추가 후 체력 변화 델리게이트 호출, 위젯 컨트롤러 바인딩을 수행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 BT Services 내부에서 수행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1445&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bljKwN/btsOHlPtQF4/cwYdweFvE5qrcrKO6GftNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bljKwN/btsOHlPtQF4/cwYdweFvE5qrcrKO6GftNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bljKwN/btsOHlPtQF4/cwYdweFvE5qrcrKO6GftNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbljKwN%2FbtsOHlPtQF4%2FcwYdweFvE5qrcrKO6GftNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1445&quot; height=&quot;555&quot; data-origin-width=&quot;1445&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BT Services 내에서 위젯을 생성하여 위젯 컨트롤러를 연결한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;849&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sKPkY/btsOJymi6VO/t3YpdPvkoWKyvyQltkr0Ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sKPkY/btsOJymi6VO/t3YpdPvkoWKyvyQltkr0Ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sKPkY/btsOJymi6VO/t3YpdPvkoWKyvyQltkr0Ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsKPkY%2FbtsOJymi6VO%2Ft3YpdPvkoWKyvyQltkr0Ik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;786&quot; height=&quot;276&quot; data-origin-width=&quot;849&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 뷰 포트에 추가한다.&lt;/p&gt;</description>
      <category>UE 5 스터디/Gameplay Ability System(GAS)</category>
      <category>c++</category>
      <category>unreal</category>
      <category>로그 라이크</category>
      <category>언리얼</category>
      <category>체력바</category>
      <author>Crat3</author>
      <guid isPermaLink="true">https://crat.tistory.com/585</guid>
      <comments>https://crat.tistory.com/585#entry585comment</comments>
      <pubDate>Thu, 19 Jun 2025 17:24:21 +0900</pubDate>
    </item>
    <item>
      <title>32-11. 로그 라이크 - (6) 보스 - (1) 비헤이비어 트리 수정, BTTasks 수정, 몬스터 사이 회피(Avoidance) 설정 , 아케인 파편 디버그</title>
      <link>https://crat.tistory.com/584</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;0. 개요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 몬스터의 AI를 수정하여 보스 형태의 몬스터를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 기존의 AI 공격 로직 파악&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 엘리멘탈리스트 클래스의 비헤이비어 트리&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2049&quot; data-origin-height=&quot;931&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSCtaX/btsOFWPq7p0/iYYozH7T01kh1ZveMZqRnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSCtaX/btsOFWPq7p0/iYYozH7T01kh1ZveMZqRnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSCtaX/btsOFWPq7p0/iYYozH7T01kh1ZveMZqRnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSCtaX%2FbtsOFWPq7p0%2FiYYozH7T01kh1ZveMZqRnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2049&quot; height=&quot;931&quot; data-origin-width=&quot;2049&quot; data-origin-height=&quot;931&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워리어, 아쳐, 엘리멘탈리스트 3개의 직업군 중, 마법을 활용하는 클래스의 비헤이비어 트리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EQS 쿼리를 사용하여 발사 위치를 계산하고, 공격을 시행하게 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 엘리멘탈리스트 클래스의 공격 태스크&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1531&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/neM7A/btsOHjPQ0sA/YrOpqcsksWMDyLYLtx5wj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/neM7A/btsOHjPQ0sA/YrOpqcsksWMDyLYLtx5wj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/neM7A/btsOHjPQ0sA/YrOpqcsksWMDyLYLtx5wj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FneM7A%2FbtsOHjPQ0sA%2FYrOpqcsksWMDyLYLtx5wj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1531&quot; height=&quot;548&quot; data-origin-width=&quot;1531&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baU5Tg/btsOF3Hn69S/5ViQltM4CHOmEM6hb4Aq1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baU5Tg/btsOF3Hn69S/5ViQltM4CHOmEM6hb4Aq1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baU5Tg/btsOF3Hn69S/5ViQltM4CHOmEM6hb4Aq1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaU5Tg%2FbtsOF3Hn69S%2F5ViQltM4CHOmEM6hb4Aq1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;682&quot; height=&quot;301&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;367&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격 태스크를 살펴보면, 기본적으로 소환한 미니언의 갯수를 체크하여 Minion Spawn Threshold의 갯수보다 미니언의 갯수가 적으면 Summon Tag를 이용하여 GA_Summon 어빌리티를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미니언의 갯수가 적절하면 Ability Tag를 Attack 태그(Abilities.Attack)으로 설정하여, 파이어볼트 공격을 하는 데, 이는 엘리멘탈리스트 직업군 캐릭터인 샤먼 몬스터의 몽타주에서 해당 태그를 전달하고 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) 엘리멘탈리스트(샤먼)의 공격 몽타주&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1970&quot; data-origin-height=&quot;859&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUIjG7/btsOGKUVxNB/pHQGxZP1KxMcc8Q5uH1ARK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUIjG7/btsOGKUVxNB/pHQGxZP1KxMcc8Q5uH1ARK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUIjG7/btsOGKUVxNB/pHQGxZP1KxMcc8Q5uH1ARK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUIjG7%2FbtsOGKUVxNB%2FpHQGxZP1KxMcc8Q5uH1ARK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1970&quot; height=&quot;859&quot; data-origin-width=&quot;1970&quot; data-origin-height=&quot;859&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애님 노티파이가 Montage.Attack.1로 지정되어 있음을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 보스 캐릭터 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 보스 캐릭터 블루프린트&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2075&quot; data-origin-height=&quot;1201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpUNp5/btsOFm153Sf/CGHK3VkUvt484BckTe5SnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpUNp5/btsOFm153Sf/CGHK3VkUvt484BckTe5SnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpUNp5/btsOFm153Sf/CGHK3VkUvt484BckTe5SnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpUNp5%2FbtsOFm153Sf%2FCGHK3VkUvt484BckTe5SnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2075&quot; height=&quot;1201&quot; data-origin-width=&quot;2075&quot; data-origin-height=&quot;1201&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시 크기를 크게 설정하고 이에 맞춰 캡슐 크기를 맞춘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격 몽타주를 추가하고 올바른 몽타주 태그를 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 몽타주 복제 후 애님 노티파이 변경&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1987&quot; data-origin-height=&quot;830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bprudm/btsOFy2fs82/cGepvtBjOHaCOIbvFc7fLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bprudm/btsOFy2fs82/cGepvtBjOHaCOIbvFc7fLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bprudm/btsOFy2fs82/cGepvtBjOHaCOIbvFc7fLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbprudm%2FbtsOFy2fs82%2FcGepvtBjOHaCOIbvFc7fLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1987&quot; height=&quot;830&quot; data-origin-width=&quot;1987&quot; data-origin-height=&quot;830&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하게 하려는 어빌리티의 태그와 애님 노티파이의 태그를 일치시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) BTTasks 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소환, 아케인 파편 스펠, 파이어 볼트 스펠을 각각의 BT 태스크로 나누고, 비헤이비어 트리를 재구성할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3-1) BTT_Boss_Summon&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8bzjw/btsOHkn4fZg/zBa7d9u8cZ1lnhbIyRSBY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8bzjw/btsOHkn4fZg/zBa7d9u8cZ1lnhbIyRSBY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8bzjw/btsOHkn4fZg/zBa7d9u8cZ1lnhbIyRSBY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8bzjw%2FbtsOHkn4fZg%2FzBa7d9u8cZ1lnhbIyRSBY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1544&quot; height=&quot;515&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3-2) BTT_Boss_ArcaneShards&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cE0sY0/btsOG0p0aXc/klHygZkqFWu71045vKYR3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cE0sY0/btsOG0p0aXc/klHygZkqFWu71045vKYR3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cE0sY0/btsOG0p0aXc/klHygZkqFWu71045vKYR3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcE0sY0%2FbtsOG0p0aXc%2FklHygZkqFWu71045vKYR3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1533&quot; height=&quot;404&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3-3) BTT_Boss_FireBolt&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;363&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmftFe/btsOGNLg4hi/75L2UgordRX2BFRSxt3o0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmftFe/btsOGNLg4hi/75L2UgordRX2BFRSxt3o0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmftFe/btsOGNLg4hi/75L2UgordRX2BFRSxt3o0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmftFe%2FbtsOGNLg4hi%2F75L2UgordRX2BFRSxt3o0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1268&quot; height=&quot;363&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;363&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4) 보스 비헤이비어 트리 재구성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHuuGA/btsOF3gJMbK/tjEDCYJXBhuNVOhSpKBYo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHuuGA/btsOF3gJMbK/tjEDCYJXBhuNVOhSpKBYo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHuuGA/btsOF3gJMbK/tjEDCYJXBhuNVOhSpKBYo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHuuGA%2FbtsOF3gJMbK%2FtjEDCYJXBhuNVOhSpKBYo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1088&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1088&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하수인 소환, 아케인 파편 스펠, 파이어 볼트 스펠에 대하여 시퀀스 노드를 배치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하수인 소환 어빌리티는 내부적으로 소환수 갯수가 Threshold 이하이면 발동될 것이고, 아케인 파편 스펠은 쿨다운이 존재하므로 쿨다운이 있는 상태에서는 어빌리티가 자동으로 취소될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 일반적인 상황(아케인 파편 스펠 사용 직후, 소환수 일정 마리 이상)에서는 플레이어를 향하여 파이어볼트 공격을 실행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 디버그&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 소환수와 보스가 아무 동작도 하지 않는 상황&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 소환수와 보스가 EQS 쿼리를 실행하는 과정에서 획득한 MoveToLocation으로 이동하려고 할 때 서로가 서로의 진로를 방해하기 때문에 발생하는 버그이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;243&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Atn1Z/btsOF5ZUMu9/yf2B1hROej7tJJJKgTGx80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Atn1Z/btsOF5ZUMu9/yf2B1hROej7tJJJKgTGx80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Atn1Z/btsOF5ZUMu9/yf2B1hROej7tJJJKgTGx80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAtn1Z%2FbtsOF5ZUMu9%2Fyf2B1hROej7tJJJKgTGx80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;302&quot; height=&quot;196&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;243&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 몬스터 캐릭터 대상으로 RVO 회피 사용을 체크하고, 반경을 적절히 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소환수 몬스터의 경우 일정 가중치를 추가 해주면, 보스 몬스터의 움직임을 더 고려하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTVUjJ/btsOHfmQ1If/K7bGD7GkONxDbFK0DDOlWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTVUjJ/btsOHfmQ1If/K7bGD7GkONxDbFK0DDOlWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTVUjJ/btsOHfmQ1If/K7bGD7GkONxDbFK0DDOlWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTVUjJ%2FbtsOHfmQ1If%2FK7bGD7GkONxDbFK0DDOlWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;364&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) ExecCalc에서 보스가 아케인 파편 스펠로 데미지를 입힐 때 크래시가 발생하는 버그&lt;/p&gt;
&lt;pre id=&quot;code_1750236783957&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 방사형 피해 계산
		if (UAuraAbilitySystemLibrary::IsRadialDamage(EffectContextHandle))
		{
			if (ICombatInterface* CombatInterface = Cast&amp;lt;ICombatInterface&amp;gt;(TargetAvatar))
			{
				CombatInterface-&amp;gt;GetOnDamageDelegate().AddLambda([&amp;amp;](float DamageAmount)
					{
						DamageTypeValue = DamageAmount;
						if (CombatInterface)
							CombatInterface-&amp;gt;GetOnDamageDelegate().Clear();
					});
			}

			FVector DamageOrigin = UAuraAbilitySystemLibrary::GetRadialDamageOrigin(EffectContextHandle);
			DamageOrigin.Z += 100.f;
			
			UGameplayStatics::ApplyRadialDamageWithFalloff(
				TargetAvatar,
				DamageTypeValue,
				0.f, // 최소 대미지
				DamageOrigin,
				UAuraAbilitySystemLibrary::GetRadialDamageInnerRadius(EffectContextHandle),
				UAuraAbilitySystemLibrary::GetRadialDamageOuterRadius(EffectContextHandle),
				1.f, // 감쇠 계수
				UDamageType::StaticClass(), // 
				TArray&amp;lt;AActor*&amp;gt;(), // IgnoreActor
				SourceAvatar, // DamageCauser
				nullptr); // Controller Instigator
		}

		Damage += DamageTypeValue;
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서 데미지를 계산하는 ExecCalc 클래스의 계산 함수의 전투 인터페이스의 델리게이트를 바인딩하고 클리어하는 과정에서 댕글링 포인터가 유발되어 크래시가 발생하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 여러개의 아케인 파편이 생성되는 과정에서 이전 파편에 플레이어 캐릭터가 사망한 직후, 다음 파편이 방사형 데미지를 계산할 때 널 포인터가 되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한편으로 위의 코드를 사용한 이유는, 데미지 감쇠를 ApplyRadialDamageWithFalloff 로 계산하고, 해당 데미지를 TakeDamage에서 델리게이트 브로드캐스트로 데미지 양을 전달받아 할당하려 하기 위함이었으나, ExecCalc는 데미지를 계산하는 역할만 해야하고 이외의 어떠한 역할도 하면 안된다(델리게이트 바인딩, 호출 등등).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 코드를 안전하게 리팩토링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2-1) ExecCalc_Damage - 방사형 데미지 계산&lt;/p&gt;
&lt;pre id=&quot;code_1750236997860&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	float CalculateRadialDamage(const AActor* TargetActor, float BaseDamage, const FVector&amp;amp; Origin, float InnerRadius, float OuterRadius, float DamageFalloff) const;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1750237016012&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;float UExecCalc_Damage::CalculateRadialDamage(const AActor* TargetActor, float BaseDamage, const FVector&amp;amp; Origin, float InnerRadius, float OuterRadius,
	float DamageFalloff) const
{
	FVector TargetLocation = TargetActor-&amp;gt;GetActorLocation();

	// 타겟 - 데미지 중심 = 중심에서 타겟까지의 벡터
	FVector DamageVector = TargetLocation - Origin;
	float DistanceToTarget = DamageVector.Length();

	const TRange&amp;lt;float&amp;gt; DistanceRange(InnerRadius, OuterRadius);
	const TRange&amp;lt;float&amp;gt; DamageScaleRange(DamageFalloff, 0.f);

	const float DamageScale = FMath::GetMappedRangeValueClamped(DistanceRange, DamageScaleRange, DistanceToTarget);
	const float Damage = BaseDamage * DamageScale;
	return Damage;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방사형 데미지의 변수를 받아 TRange로 만들고, GetMappedRangeValueClamped 함수를 이용하여 퍼센트를 꺼내올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 선형 감쇠에 적합하도록 설정되었으므로, 지수형 감쇠를 위하면 리팩토링이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2-2) 데미지 계산식 수정&lt;/p&gt;
&lt;pre id=&quot;code_1750237125657&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;		// 속성 저항이 데미지를 퍼센트로 무시함
		DamageTypeValue *= (100.f - Resistance) / 100.f;
		Damage += DamageTypeValue;
		
		// 방사형 피해 계산
		if (UAuraAbilitySystemLibrary::IsRadialDamage(EffectContextHandle))
		{
			Damage = CalculateRadialDamage(
				TargetAvatar,
				DamageTypeValue,
				UAuraAbilitySystemLibrary::GetRadialDamageOrigin(EffectContextHandle),
				UAuraAbilitySystemLibrary::GetRadialDamageInnerRadius(EffectContextHandle),
				UAuraAbilitySystemLibrary::GetRadialDamageOuterRadius(EffectContextHandle),
				1.0f);
		}
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방사형 피해를 속성 저항 계산 이후로 보내고, 새로 만든 커스텀 함수를 사용하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>UE 5 스터디/Gameplay Ability System(GAS)</category>
      <category>c++</category>
      <category>디버그</category>
      <category>로그 라이크</category>
      <category>보스</category>
      <category>언리얼</category>
      <category>크래시</category>
      <author>Crat3</author>
      <guid isPermaLink="true">https://crat.tistory.com/584</guid>
      <comments>https://crat.tistory.com/584#entry584comment</comments>
      <pubDate>Wed, 18 Jun 2025 18:01:54 +0900</pubDate>
    </item>
  </channel>
</rss>