<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Lykan Studio</title>
    <link>https://lykanstudio.tistory.com/</link>
    <description>GameProgramer 취업준비 중

onestone3647@gmail.com</description>
    <language>ko</language>
    <pubDate>Thu, 2 Jul 2026 18:44:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>한돌이</managingEditor>
    <image>
      <title>Lykan Studio</title>
      <url>https://tistory1.daumcdn.net/tistory/3750260/attach/3a53353fcec642ef9a804688bd50f7e6</url>
      <link>https://lykanstudio.tistory.com</link>
    </image>
    <item>
      <title>[Unreal Engine etc.] 언리얼 엔진에서 DS4Windows로 듀얼쇼크4를 연결하지 못하는 경우</title>
      <link>https://lykanstudio.tistory.com/121</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;979&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzOgIn/btsOCzfsZMl/bBIfKdkxA7vkKSc7WfjaL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzOgIn/btsOCzfsZMl/bBIfKdkxA7vkKSc7WfjaL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzOgIn/btsOCzfsZMl/bBIfKdkxA7vkKSc7WfjaL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzOgIn%2FbtsOCzfsZMl%2FbBIfKdkxA7vkKSc7WfjaL1%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;979&quot; height=&quot;631&quot; data-origin-width=&quot;979&quot; data-origin-height=&quot;631&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;프로필의 Emulated Controller가 Xbox360이 아닌 DualShock 4일 경우 언리얼 엔진에서 듀얼쇼크4를 연결하지 못할 수 있으므로 Xbox 360으로 설정해야합니다.&lt;/p&gt;</description>
      <category>Unreal Engine/etc.</category>
      <author>한돌이</author>
      <guid isPermaLink="true">https://lykanstudio.tistory.com/121</guid>
      <comments>https://lykanstudio.tistory.com/121#entry121comment</comments>
      <pubDate>Mon, 16 Jun 2025 16:47:19 +0900</pubDate>
    </item>
    <item>
      <title>[Project Replica] InteractionSystem 상호작용 시스템</title>
      <link>https://lykanstudio.tistory.com/120</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/ISjSfqfTx3U&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/ISjSfqfTx3U&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=ISjSfqfTx3U&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/ebSF6v/hyYU1MUxBs/JZ3UdLaIo7WqVpqQljKB30/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/cpf07u/hyYTfrg0k0/FkbiSmYaHnJnsWqb8abHX0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;[Project Replica] InteractionSystem 상호작용 시스템&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/ISjSfqfTx3U&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;플레이어 캐릭터와 다양한 오브젝트 간의 상호작용을 유연하게 처리할 수 있도록 구현한 상호작용 시스템입니다. 핵심 구조는 InteractionSystem 컴포넌트를 기반으로 하며, 상호작용 가능한 대상은 인터페이스 기반으로 분리하여 확장성과 재사용성을 고려했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시스템 구조 및 동작 방식&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상호작용 기능은 InteractSystem이라는 ActorComponent에 구현되어 있으며, 해당 컴포넌트를 소유한 액터는 상호작용을 수행하는 주체가 됩니다.&lt;/li&gt;
&lt;li&gt;상호작용 대상은 Interactable 인터페이스를 상속하여 구현되며, 이 인터페이스를 통해 상호작용 관련 데이터를 제공하고 상호작용을 실행합니다.&lt;/li&gt;
&lt;li&gt;플레이어 캐릭터의 CapsuleComponent에 오버랩된 액터들 중 Interactable 인터페이스를 상속한 액터만을 필터링하여InteractableActors 배열에 저장합니다.&lt;/li&gt;
&lt;li&gt;상호작용을 시작할 때는 InteractionSystem의 SelectInteractableIndex에 해당하는 액터를 선택하여 해당 액터의 Interactable 인터페이스 함수를 호출함으로써 상호작용을 실행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시스템 등록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;InteractionSystem은 캐릭터에 등록될 때 OnRegister() 함수에서 BindInteractEvent() 함수를 호출하여 초기 설정을 수행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748413003307&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UPRInteractionSystemComponent::BindInteractEvent()
{
	if (GetPROwner() &amp;amp;&amp;amp; GetPROwner()-&amp;gt;GetCapsuleComponent())
	{
		UCapsuleComponent* PROwnerCapsuleComponent = GetPROwner()-&amp;gt;GetCapsuleComponent();
		PROwnerCapsuleComponent-&amp;gt;OnComponentBeginOverlap.AddDynamic(this, &amp;amp;UPRInteractionSystemComponent::InteractableBeginOverlap);
		PROwnerCapsuleComponent-&amp;gt;OnComponentEndOverlap.AddDynamic(this, &amp;amp;UPRInteractionSystemComponent::InteractableEndOverlap);
	}

	OnInteractableActorsChanged.AddDynamic(this, &amp;amp;UPRInteractionSystemComponent::UpdateProgressBarOnInteractableActorsChanged);
	OnInteractableIndexChanged.AddDynamic(this, &amp;amp;UPRInteractionSystemComponent::UpdateProgressBarOnInteractableIndexChanged);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;BindInteractEvent() 함수는 캐릭터의 CapsuleComponent의 Overlap 이벤트에 상호작용 가능한 액터를 감지하는 함수를 바인딩하고, 상호작용 대상 목록(InteractableActors 배열) 변경을 알리는 OnInteractableActorsChanged 델리게이트와, 선택된 인덱스가 변경되었음을 알리는 OnInteractableIndexChanged 델리게이트에 각각 프로그래스바를 갱신하는 함수들을 연결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상호작용 가능한 액터 감지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;BindInteractEvent() 함수가 정상적으로 호출되었다면, 캐릭터의 CapsuleComponent에서 OnComponentBeingOverlap 이벤트가 발생했을 때 InteractableBeginOverlap() 함수가 실행됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748416122720&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UPRInteractionSystemComponent::InteractableBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult&amp;amp; SweepResult)
{
	if (CanAddInteractableActor(OtherActor))
	{
		InteractableActors.AddUnique(OtherActor); 

		// 처음 추가 되었을 때 프로그래스바를 업데이트합니다.
		if (InteractableActors.Num() == 1)
		{
			CurrentInteractableActor = OtherActor;
		}

		OnInteractableActorsChanged.Broadcast(GetInteractableActors());
		OnInteractableIndexChanged.Broadcast(SelectInteractableIndex);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 함수는 오버랩된 액터가 Interactable 인터페이스를 상속하고 있는지 확인한 후, 조건을 만족할 경우 해당 액터를 InteractableActors 배열에 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;배열이 처음으로 채워지는 경우에는 CurrentInteractableActor를 해당 액터로 지정하며, 이후 OnInteractableActorsChanged와 OnInteractableIndexChanged 델리게이트를 실행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748416511385&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UPRInteractionSystemComponent::InteractableEndOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (IsInteractableActor(OtherActor))
	{
		const int32 RemoveIndex = InteractableActors.IndexOfByKey(OtherActor);
		AActor* CurrentTarget = GetInteractableActor();
		const int32 CurrentIndex = InteractableActors.IndexOfByKey(CurrentTarget);

		// 제거 대상이 현재 상호작용 중인 액터일 경우
		if (OtherActor == CurrentTarget)
		{
			// 진행중인 상호작용을 초기화
			ResetInteractionState();

			// 배열의 마지막 항목일 경우 SelectInteractableIndex 감소
			if (RemoveIndex != 0 &amp;amp;&amp;amp; RemoveIndex == InteractableActors.Num() - 1)
			{
				--SelectInteractableIndex;
			}

			InteractableActors.Remove(OtherActor);
			CurrentInteractableActor = GetInteractableActor();
		}
		else
		{
			// 상호작용 중인 액터보다 앞에 있는 액터가 제거됐을 경우 SelectInteractableIndex를 보정
			if (RemoveIndex &amp;lt; CurrentIndex)
			{
				--SelectInteractableIndex;
			}
			
			InteractableActors.Remove(OtherActor);
		}

		OnInteractableIndexChanged.Broadcast(SelectInteractableIndex);
		OnInteractableActorsChanged.Broadcast(GetInteractableActors());
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;반대로 OnComponentEndOverlap 이벤트가 발생하면 InteractEndOverlap() 함수가 호출되어, 오버랩이 끝난 액터가 상호작용 가능한 대상이었다면 InteractableActors 배열에서 해당 액터를 제거합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이때, 제거되는 액터가 현재 선택된 상호작용 대상(CurrentInteractableActor)일 경우 상호작용 상태를 초기화하고, 선택 인덱스(SelectInteractableIndex)를 상황에 맞게 조정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이후 동일하게 OnInteractableActorsChanged와 OnInteractableIndexChanged 델리게이트를 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상호작용 실행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 입력에 따라 상호작용 방식(Press/Hold/Tap)을 분기하고, 상호작용 진행 상황을 프로그래스바로 표시합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;StartInteract() 함수를 실행하여 상호작용을 시작합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748418889412&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UPRInteractionSystemComponent::StartInteract()
{
	CurrentInteractableActor = GetInteractableActor();
	if (IsValid(CurrentInteractableActor))
	{
		const FPRInteractionData&amp;amp; InteractionData = IPRInteractable::Execute_GetInteractionData(CurrentInteractableActor);
		switch (InteractionData.InteractionType)
		{
		case EPRInteractionType::Hold:
			OnHoldInteraction.AddDynamic(this, &amp;amp;UPRInteractionSystemComponent::UpdateHoldInteraction);
			OnInteractionEnded.AddDynamic(this, &amp;amp;UPRInteractionSystemComponent::InitializeInteractProgressBarWidget);
			break;
		case EPRInteractionType::Tap:
			InteractionTap();
			break;
		case EPRInteractionType::Press:
		default:
			ExecuteInteract();
			break;
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 함수는 현재 선택된 상호작용 대상 액터(CurrentInteractableActor)를 가져와 유효성을 검사하고, 해당 액터의 FPRInteractionData 구조체에서 설정된 상호작용 방식(EPRInteractionType)을 확인한 뒤 분기 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;상호작용 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;시스템은 다음의 3가지 상호작용 방식을 제공합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;방식&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Press&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;버튼을 한 번 눌러 즉시 상호작용을 실행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Hold&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;일정 시간 동안 입력을 유지해야 상호작용이 완료됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Tap&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;정해진 횟수만큼 버튼을 눌러야 상호작용이 실행됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Hold 및 Tap 방식의 경우, 상호작용 대상의 위치에 프로그래스 바를 생성하여 실시간 진행 상태를 시각적으로 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Press&lt;/h4&gt;
&lt;pre id=&quot;code_1748498055151&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bool UPRInteractionSystemComponent::ExecuteInteract()
{
	AActor* Target = GetInteractableActor();
	if (IsValid(Target))
	{
		if (IsValid(GetPROwner()))
		{
			const FRotator LookAtRotation = UKismetMathLibrary::FindLookAtRotation(GetPROwner()-&amp;gt;GetActorLocation(), Target-&amp;gt;GetActorLocation());
			GetPROwner()-&amp;gt;SetActorRotation(FRotator(0.0f, LookAtRotation.Yaw,0.0f));
		}

		// 상호작용 애니메이션 추가 예정

		bool bResult = IPRInteractable::Execute_Interact(Target, GetPROwner());
		
		UpdateInteractProgressBarWidget();
		ResetInteractionState();

		return bResult;
	}

	return false;
}

void UPRInteractionSystemComponent::ResetInteractionState()
{
	OnHoldInteraction.RemoveDynamic(this, &amp;amp;UPRInteractionSystemComponent::UpdateHoldInteraction);
	
	CurrentInteractableActor = nullptr;
	TapInputCount = 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Press 방식은 버튼을 한 번 누르면 즉시 상호작용이 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;ExecuteInteract() 함수가 호출되며, 내부에서는 GetInteractableActor()를 통해 현재 상호작용 대상을 가져온 뒤, 플레이어가 해당 대상 방향을 바라보도록 회전시킵니다. 이후 상호작용 대상이 구현한 Interact(AActor* Interactor) 인터페이스 함수가 실행됩니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;Hold&lt;/h4&gt;
&lt;pre id=&quot;code_1748498391926&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UPRInteractionSystemComponent::UpdateHoldInteraction(const float ElapsedSeconds)
{
	CurrentInteractableActor = GetInteractableActor();
	if (IsValid(CurrentInteractableActor))
	{
		const float HoldDuration = IPRInteractable::Execute_GetInteractionData(GetInteractableActor()).HoldDuration;
	
		if (IsValid(ProgressBarWidget))
		{
			ProgressBarWidget-&amp;gt;SetProgressPercent(ElapsedSeconds / HoldDuration);
		}

		if (ElapsedSeconds &amp;gt; HoldDuration)
		{
			ExecuteInteract();
			UnbindOnInteractionOngoing();
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Hold 방식은 버튼을 일정 시간 동안 누르고 있어야 상호작용이 완료됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;지속적으로 호출되는 UpdateHoldInteraction(const float ElapsedSeconds) 함수에서 ElapsedSeconds를 기준으로, 인터페이스를 통해 가져온 상호작용 데이터(FPRInteractionData)에서 필요한 유지 시간(HoldDuration)을 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;ElapsedSeconds / HoldDuration의 비율로 프로그래스바가 실시간으로 업데이트되며, 유지 시간이 충족되면 ExecuteInteract() 함수를 호출하여 상호작용을 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Tap&lt;/h4&gt;
&lt;pre id=&quot;code_1748498912139&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UPRInteractionSystemComponent::InteractionTap()
{
	++TapInputCount;

	CurrentInteractableActor = GetInteractableActor();
	if (IsValid(CurrentInteractableActor))
	{
		const int32 TapCount = IPRInteractable::Execute_GetInteractionData(GetInteractableActor()).TapCount;
		if (TapCount &amp;lt;= 0)
		{
			// 횟수가 설정되어 있지 않으면 바로 상호작용을 실행합니다.
			ExecuteInteract();
		}
		else
		{
			if (IsValid(ProgressBarWidget))
			{
				ProgressBarWidget-&amp;gt;SetProgressPercent(static_cast&amp;lt;float&amp;gt;(TapInputCount) / static_cast&amp;lt;float&amp;gt;(TapCount));

				if (TapInputCount &amp;gt;= TapCount)
				{
					ExecuteInteract();
				}
			}
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Tap 방식은 버튼을 여러 번 눌러야 상호작용이 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;InteractionTap() 함수가 호출될 때마다 TapInputCount가 증가하고, 인터페이스를 통해 얻은 TapCount에 도달하면 ExecuteInteract() 함수가 실행됩니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;상호작용 정보 (InteractionData)&lt;/h3&gt;
&lt;pre id=&quot;code_1747814299767&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 상호작용에 대한 정보를 나타내는 구조체입니다.
 */
USTRUCT(Atomic, BlueprintType)
struct FPRInteractionData
{
	GENERATED_BODY()

public:
	FPRInteractionData()
		: Icon(nullptr)
		, InteractionText(FText::GetEmpty())
		, InteractionType(EPRInteractionType::Press)
		, HoldDuration(4.0f)
		, HoldInProgressText(FText::GetEmpty())
		, TapCount(4)
		, bUseItemAcquisitionNotification(false)
	{}

	FPRInteractionData(UTexture2D* NewIcon, FText NewInteractionText, EPRInteractionType NewInteractionType,
						FText NewHoldInProgressText, float NewHoldDuration, int32 NewTapCount, bool bNewbUseItemAcquisitionNotification)
		: Icon(NewIcon)
		, InteractionText(NewInteractionText)
		, InteractionType(NewInteractionType)
		, HoldDuration(NewHoldDuration)
		, HoldInProgressText(NewHoldInProgressText)
		, TapCount(NewTapCount)
		, bUseItemAcquisitionNotification(bNewbUseItemAcquisitionNotification)
	{}

public:
	/** 상호작용 아이콘입니다. 아이템 또는 행동을 시각적으로 나타냅니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRInteractionData&quot;)
	UTexture2D* Icon;
	
	/** 상호작용 대상의 이름 또는 설명입니다. (예: '오래된 코등이', '문 열기') */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRInteractionData&quot;)
	FText InteractionText ;

	/** 상호작용 방식입니다. Press, Hold, Tap 중 하나입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRInteractionData&quot;)
	EPRInteractionType InteractionType;
	
	/** Hold 방식일 때, 상호작용이 실행되기까지 누르고 있어야 하는 시간입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;PRInteractionData&quot;, meta = (EditCondition = &quot;InteractionType == EPRInteractionType::Hold&quot;, ClampMin = &quot;0.0&quot;))
	float HoldDuration;

	/** Hold 방식일 때, 표시할 진행 상태 문구입니다. (예: &quot;열기 진행 중...&quot;) */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;PRInteractionData&quot;, meta = (EditCondition = &quot;InteractionType == EPRInteractionType::Hold&quot;, ClampMin = &quot;0.0&quot;))
	FText HoldInProgressText;

	/** Tap 방식일 때, 상호작용을 실행하기 위해 눌러야 하는 횟수입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;PRInteractionData&quot;, meta = (EditCondition = &quot;InteractionType == EPRInteractionType::Tap&quot;, ClampMin = &quot;0&quot;))
	int32 TapCount;
	
	/** 아이템 획득 알림의 사용 여부입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRInteractionData&quot;)
	bool bUseItemAcquisitionNotification;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;상호작용에 필요한 다양한 데이터를 하나로 관리하기 위해 FPRInteractionData 구조체를 설계하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 구조체는 아이콘, 상호작용 설명 텍스트, 상호작용 방식(Press/Hold/Tap), 진행 중 문구, 그리고 입력 지속 시간 또는 누적 횟수 등 상호작용 수행에 필요한 핵심 정보를 포함하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이러한 정보를 통합해둠으로써, 개별 오브젝트의 상호작용 동작을 더 유연하게 정의하고 제어할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로그래스바 UI&lt;/h2&gt;
&lt;pre id=&quot;code_1748329733275&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UPRInteractionSystemComponent::UpdateInteractProgressBarWidget()
{
	if (GetInteractableActor()
		&amp;amp;&amp;amp; GetPRPlayerController()
		&amp;amp;&amp;amp; ProgressBarWidgetComponent
		&amp;amp;&amp;amp; ProgressBarWidgetClassReference)
	{
		AActor* Target = GetInteractableActor();
		if (IsValid(Target))
		{
			const FPRInteractionData&amp;amp; InteractionData = IPRInteractable::Execute_GetInteractionData(Target);
			switch (InteractionData.InteractionType)
			{
			case EPRInteractionType::Hold:
			case EPRInteractionType::Tap:
				InitializeOrUpdateProgressBarWidget(InteractionData);
				break;
			case EPRInteractionType::Press:
			default:
				ProgressBarWidgetComponent-&amp;gt;SetWidget(nullptr);
				break;
			}
		}
	}
	else
	{
		ProgressBarWidgetComponent-&amp;gt;SetWidget(nullptr);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Hold 방식과 Tap 방식의 상호작용에서는 플레이어가 얼마나 상호작용을 수행했는지 시각적으로 피드백할 필요가 있습니다. 이를 위해 프로그래스바 UI를 구현하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;프로그래스바는 다음과 같은 시점에서 UpdateInteractProgressBarWidget() 함수를 통해 위젯을 생성하거나 제거하며, 상태를 최신화합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상호작용 대상이 변경되었을 때&lt;/li&gt;
&lt;li&gt;상호작용이 진행 중일 때&lt;/li&gt;
&lt;li&gt;상호작용이 완료되었을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이러한 동작을 통해 플레이어는 직관적으로 상호작용의 진행 상황을 파악할 수 있으며, 입력 피드백이 즉각적으로 전달되어 몰입감을 높일 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아이템 획득과 알림&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;플레이어가 아이템을 획득했을 때, 해당 아이템의 이름과 이이콘을 화면에 표시하여 직관적인 피드백을 제공하기 위해 아이템 획득 알림 UI를 구현하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이를 위해 플레이어 컨트롤러 클래스는 IPRPlayerControllerInterface를 상속하며, 다음과 같은 방식으로 알림을 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UDNTR/btsOiruUllx/stBsaNbPtx7oXNNNWLoomk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UDNTR/btsOiruUllx/stBsaNbPtx7oXNNNWLoomk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UDNTR/btsOiruUllx/stBsaNbPtx7oXNNNWLoomk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUDNTR%2FbtsOiruUllx%2FstBsaNbPtx7oXNNNWLoomk%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;913&quot; height=&quot;518&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1748503574760&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void APRPlayerController::CreateItemAcquisitionNotify_Implementation(const FText&amp;amp; NewItemName, UTexture2D* NewItemIcon)
{
	if (GetInGameHUDWidget() &amp;amp;&amp;amp; GetInGameHUDWidget()-&amp;gt;GetItemAcquisitionList())
	{
		UPRItemAcquisitionListWidget* NotifyList = GetInGameHUDWidget()-&amp;gt;GetItemAcquisitionList();
		if (NotifyList)
		{
			NotifyList-&amp;gt;AddItemAcquisitionNotification(NewItemName, NewItemIcon);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;아이템 획득 알림을 생성할 경우 CreateItemAcquisitionNotify(const FText&amp;amp; NewItemName, UTexture2D* NewItemIcon) 함수를 호출합니다. 이 함수는 인터페이스 기반으로 구현되어 있어, 컨트롤러가 어떤 방식으로든 이를 재정의 UI 반응을 정의할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;함수 내부에서는 AddItemAcquisitionNotification(const FText&amp;amp; NewItemName, UTexture2D* NewItemIcon) 함수를 통해 아이콘과 아이템 이름을 담은 슬롯 위젯을 생성하고 알림 리스트에 추가하여 화면에 출력합니다.&lt;/p&gt;</description>
      <category>Project/Replica</category>
      <category>Project Replica</category>
      <author>한돌이</author>
      <guid isPermaLink="true">https://lykanstudio.tistory.com/120</guid>
      <comments>https://lykanstudio.tistory.com/120#entry120comment</comments>
      <pubDate>Mon, 19 May 2025 16:23:49 +0900</pubDate>
    </item>
    <item>
      <title>[Unreal Engine etc.] Control Rig Debug를 에디터의 게임화면에서 표시하는 콘솔 명령어</title>
      <link>https://lykanstudio.tistory.com/117</link>
      <description>&lt;pre id=&quot;code_1724395637426&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;a.AnimNode.controlRig.Debug 1&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Unreal Engine/etc.</category>
      <category>Unreal Engine</category>
      <author>한돌이</author>
      <guid isPermaLink="true">https://lykanstudio.tistory.com/117</guid>
      <comments>https://lykanstudio.tistory.com/117#entry117comment</comments>
      <pubDate>Fri, 23 Aug 2024 15:47:28 +0900</pubDate>
    </item>
    <item>
      <title>[Unreal Engine etc.] Unreal Engine 5에서 애니메이션을 좌우 반전하여 재생하기 / 좌우 반전 애니메이션 생성하기</title>
      <link>https://lykanstudio.tistory.com/116</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;애니메이션 에셋을 사용하다가 좌우 반전이 된 애니메이션을 사용하고 싶지만 직접 만들지 않고 구입한 에셋인 경우 좌우 반전된 애니메이션이 없을 수도 있습니다. Unreal Engine 5에서 좌우 반전된 애니메이션이 없어도 애니메이션을 좌우 반전하고 좌우 반전된 애니메이션을 생성할 수 있습니다. 방법은 다음과 같습니다.&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;좌우 반전하여 재생하는 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;519&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buJJoJ/btsI9h4q8DR/3Axnlwpev7CXXWAvkes3R1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buJJoJ/btsI9h4q8DR/3Axnlwpev7CXXWAvkes3R1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buJJoJ/btsI9h4q8DR/3Axnlwpev7CXXWAvkes3R1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuJJoJ%2FbtsI9h4q8DR%2F3Axnlwpev7CXXWAvkes3R1%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;859&quot; height=&quot;519&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;519&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;753&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmSgCA/btsI62O3ruc/7wIL3ZbzyE7EOFDRnLglt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmSgCA/btsI62O3ruc/7wIL3ZbzyE7EOFDRnLglt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmSgCA/btsI62O3ruc/7wIL3ZbzyE7EOFDRnLglt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmSgCA%2FbtsI62O3ruc%2F7wIL3ZbzyE7EOFDRnLglt1%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;685&quot; height=&quot;753&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;753&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;사용할 스켈레톤을 선택하고 수락을 클릭합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;899&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xVYE8/btsI7nytu0E/342BfLbPrqZUBIKVkORPb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xVYE8/btsI7nytu0E/342BfLbPrqZUBIKVkORPb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xVYE8/btsI7nytu0E/342BfLbPrqZUBIKVkORPb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxVYE8%2FbtsI7nytu0E%2F342BfLbPrqZUBIKVkORPb0%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;986&quot; height=&quot;899&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;899&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&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0wx8i/btsI9aYEeUG/uoxdk4pXeFZFGwP36ljsdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0wx8i/btsI9aYEeUG/uoxdk4pXeFZFGwP36ljsdK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;721&quot; data-origin-width=&quot;955&quot; style=&quot;width: 37.4241%; margin-right: 10px;&quot; data-widthpercent=&quot;37.86&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0wx8i/btsI9aYEeUG/uoxdk4pXeFZFGwP36ljsdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0wx8i%2FbtsI9aYEeUG%2Fuoxdk4pXeFZFGwP36ljsdK%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;955&quot; height=&quot;721&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKtqL2/btsI79l3lz4/ili1L9ZfeDMjgwrqfJIxn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKtqL2/btsI79l3lz4/ili1L9ZfeDMjgwrqfJIxn1/img.png&quot; data-origin-width=&quot;889&quot; data-origin-height=&quot;409&quot; data-is-animation=&quot;false&quot; style=&quot;width: 61.4131%;&quot; data-widthpercent=&quot;62.14&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKtqL2/btsI79l3lz4/ili1L9ZfeDMjgwrqfJIxn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKtqL2%2FbtsI79l3lz4%2Fili1L9ZfeDMjgwrqfJIxn1%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;889&quot; height=&quot;409&quot;/&gt;&lt;/span&gt;&lt;/div&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;애니메이션 블루프린트에서 Mirror를 검색하여 이전에 생성한 미러 데이터 테이블의 이름이 적힌 노드를 연결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2024-08-19 17-14-09.gif&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byfx4D/btsI7W1wr98/d7vTuzcMx5og7K0EAkc0AK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byfx4D/btsI7W1wr98/d7vTuzcMx5og7K0EAkc0AK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byfx4D/btsI7W1wr98/d7vTuzcMx5og7K0EAkc0AK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/byfx4D/btsI7W1wr98/d7vTuzcMx5og7K0EAkc0AK/img.gif&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;1430&quot; height=&quot;804&quot; data-filename=&quot;2024-08-19 17-14-09.gif&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;804&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&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/COIWh/btsI7fU57ey/xY3K5XiUrxhWwttdeWpE50/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/COIWh/btsI7fU57ey/xY3K5XiUrxhWwttdeWpE50/img.gif&quot; data-is-animation=&quot;true&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;702&quot; data-filename=&quot;2024-08-19 17-16-16.gif&quot; style=&quot;width: 49.4138%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/COIWh/btsI7fU57ey/xY3K5XiUrxhWwttdeWpE50/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCOIWh%2FbtsI7fU57ey%2FxY3K5XiUrxhWwttdeWpE50%2Fimg.gif&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;1248&quot; height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csCd8z/btsI790LRBr/X79TKbpTx3RMTCVhLersE0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csCd8z/btsI790LRBr/X79TKbpTx3RMTCVhLersE0/img.gif&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;640&quot; data-is-animation=&quot;true&quot; data-filename=&quot;2024-08-19 17-16-38.gif&quot; style=&quot;width: 49.4234%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csCd8z/btsI790LRBr/X79TKbpTx3RMTCVhLersE0/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsCd8z%2FbtsI790LRBr%2FX79TKbpTx3RMTCVhLersE0%2Fimg.gif&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;1138&quot; height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/div&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;좌우 반전한 애니메이션을 생성하는 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;637&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj5KaW/btsI8SxlVCq/zOf7sS6cMZsrbsmueKJuk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj5KaW/btsI8SxlVCq/zOf7sS6cMZsrbsmueKJuk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj5KaW/btsI8SxlVCq/zOf7sS6cMZsrbsmueKJuk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj5KaW%2FbtsI8SxlVCq%2FzOf7sS6cMZsrbsmueKJuk0%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;913&quot; height=&quot;637&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;637&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;983&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/56Okm/btsI7lt08y6/uoPQGiBJw4uTa3ozedewjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/56Okm/btsI7lt08y6/uoPQGiBJw4uTa3ozedewjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/56Okm/btsI7lt08y6/uoPQGiBJw4uTa3ozedewjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F56Okm%2FbtsI7lt08y6%2FuoPQGiBJw4uTa3ozedewjk%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;1899&quot; height=&quot;983&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;983&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1889&quot; data-origin-height=&quot;961&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVcXGP/btsI8arJAet/DpgrzILbNcgK40G9WiEKzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVcXGP/btsI8arJAet/DpgrzILbNcgK40G9WiEKzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVcXGP/btsI8arJAet/DpgrzILbNcgK40G9WiEKzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVcXGP%2FbtsI8arJAet%2FDpgrzILbNcgK40G9WiEKzk%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;1889&quot; height=&quot;961&quot; data-origin-width=&quot;1889&quot; data-origin-height=&quot;961&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1897&quot; data-origin-height=&quot;975&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qk4CA/btsI9uCALwK/79cPPnmTkrUlmhxsDbZoZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qk4CA/btsI9uCALwK/79cPPnmTkrUlmhxsDbZoZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qk4CA/btsI9uCALwK/79cPPnmTkrUlmhxsDbZoZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQk4CA%2FbtsI9uCALwK%2F79cPPnmTkrUlmhxsDbZoZ0%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;1897&quot; height=&quot;975&quot; data-origin-width=&quot;1897&quot; data-origin-height=&quot;975&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0lkMY/btsI8P8vqXg/x37NLfGHZlx2tZStsrdmY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0lkMY/btsI8P8vqXg/x37NLfGHZlx2tZStsrdmY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0lkMY/btsI8P8vqXg/x37NLfGHZlx2tZStsrdmY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0lkMY%2FbtsI8P8vqXg%2Fx37NLfGHZlx2tZStsrdmY1%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;1263&quot; height=&quot;305&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;305&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1689&quot; data-origin-height=&quot;845&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVcX36/btsI7pv9nUK/nSg1Iytc9YsRvme6SuNiJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVcX36/btsI7pv9nUK/nSg1Iytc9YsRvme6SuNiJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVcX36/btsI7pv9nUK/nSg1Iytc9YsRvme6SuNiJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVcX36%2FbtsI7pv9nUK%2FnSg1Iytc9YsRvme6SuNiJk%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;1689&quot; height=&quot;845&quot; data-origin-width=&quot;1689&quot; data-origin-height=&quot;845&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;애니메이션의 트랙을 마우스 오른쪽 클릭한 후 프로퍼티 탭의 미러 데이터 테이블에 생성한 리머 데이터 테이블을 선택합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;983&quot; data-origin-height=&quot;813&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zo4Sc/btsI7nk0aas/p3GfjZs8wOshrjc5SD8jV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zo4Sc/btsI7nk0aas/p3GfjZs8wOshrjc5SD8jV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zo4Sc/btsI7nk0aas/p3GfjZs8wOshrjc5SD8jV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzo4Sc%2FbtsI7nk0aas%2Fp3GfjZs8wOshrjc5SD8jV1%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;983&quot; height=&quot;813&quot; data-origin-width=&quot;983&quot; data-origin-height=&quot;813&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;시퀸서에서 메쉬를 마우스 오른쪽 클릭하여 링크된 애니메이션 시퀀스 생성을 클릭합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/co52yI/btsI9aK8QvQ/zOVx9ninbbqKfbDKp0qne1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/co52yI/btsI9aK8QvQ/zOVx9ninbbqKfbDKp0qne1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/co52yI/btsI9aK8QvQ/zOVx9ninbbqKfbDKp0qne1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fco52yI%2FbtsI9aK8QvQ%2FzOVx9ninbbqKfbDKp0qne1%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;685&quot; height=&quot;559&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;559&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wLju4/btsI9e7LQYT/Po3kZZFfhU5N6RQkmMgWp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wLju4/btsI9e7LQYT/Po3kZZFfhU5N6RQkmMgWp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wLju4/btsI9e7LQYT/Po3kZZFfhU5N6RQkmMgWp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwLju4%2FbtsI9e7LQYT%2FPo3kZZFfhU5N6RQkmMgWp0%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;601&quot; height=&quot;559&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;559&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;429&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HQvC1/btsI9uvPAPF/PKf3sFxB4je6NpdfBo7J60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HQvC1/btsI9uvPAPF/PKf3sFxB4je6NpdfBo7J60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HQvC1/btsI9uvPAPF/PKf3sFxB4je6NpdfBo7J60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHQvC1%2FbtsI9uvPAPF%2FPKf3sFxB4je6NpdfBo7J60%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;429&quot; height=&quot;183&quot; data-origin-width=&quot;429&quot; data-origin-height=&quot;183&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2024-08-19 17-37-16.gif&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckACcC/btsI7L6RTHf/eu3F44VMWfdwDTugFniIZ1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckACcC/btsI7L6RTHf/eu3F44VMWfdwDTugFniIZ1/img.gif&quot; data-alt=&quot;기존의 애니메이션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckACcC/btsI7L6RTHf/eu3F44VMWfdwDTugFniIZ1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ckACcC/btsI7L6RTHf/eu3F44VMWfdwDTugFniIZ1/img.gif&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;1156&quot; height=&quot;650&quot; data-filename=&quot;2024-08-19 17-37-16.gif&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;650&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기존의 애니메이션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2024-08-19 17-37-29.gif&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Md8pU/btsI9eNsri8/AVGK5prUkpPteY2DwjCMEK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Md8pU/btsI9eNsri8/AVGK5prUkpPteY2DwjCMEK/img.gif&quot; data-alt=&quot;좌우 반전하여 생성한 애니메이션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Md8pU/btsI9eNsri8/AVGK5prUkpPteY2DwjCMEK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/Md8pU/btsI9eNsri8/AVGK5prUkpPteY2DwjCMEK/img.gif&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;1122&quot; height=&quot;632&quot; data-filename=&quot;2024-08-19 17-37-29.gif&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;좌우 반전하여 생성한 애니메이션&lt;/figcaption&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;</description>
      <category>Unreal Engine/etc.</category>
      <category>Unreal Engine 5</category>
      <author>한돌이</author>
      <guid isPermaLink="true">https://lykanstudio.tistory.com/116</guid>
      <comments>https://lykanstudio.tistory.com/116#entry116comment</comments>
      <pubDate>Mon, 19 Aug 2024 17:41:23 +0900</pubDate>
    </item>
    <item>
      <title>[Unreal Engine C++] 에디터의 디테일 패널에서 속성을 수정할 때 호출되는 함수 PostEditChangeProperty / PostEditChangeChainProperty</title>
      <link>https://lykanstudio.tistory.com/115</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;PostEditChangeProperty 함수와 PostEditChangeChainProperty 함수는 Unreal Engine의 에디터에서만 실행되는 함수입니다. 이 함수들은 에디터에서 변수 값이 변경될 때 추가적인 처리를 할 수 있게 해줍니다. 이러한 변경 사항은 주로 디테일 패널에서 속성을 수정할 때 발생합니다. 에디터에서만 실행되는 함수 이므로 WITH_EDITOR 매크로를 사용했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1720764783182&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .h
#if WITH_EDITOR
protected:
	virtual void PostEditChangeProperty(struct FPropertyChangedEvent&amp;amp; PropertyChangedEvent) override;
	virtual void PostEditChangeChainProperty(struct FPropertyChangedChainEvent&amp;amp; PropertyChangedEvent) override;
#endif

// .cpp
#if WITH_EDITOR
void UPRAISpawnArea::PostEditChangeProperty(struct FPropertyChangedEvent&amp;amp; PropertyChangedEvent)
{
}

void UPRAISpawnArea::PostEditChangeChainProperty(struct FPropertyChangedChainEvent&amp;amp; PropertyChangedEvent)
{
}
#endif&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;PostEditChangeProperty&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;PostEditChangeProperty 함수는 디테일 패널에서 단일 속성의 값이 변경될 때 호출되며 변경된 속성에 대해 단일 속성 변경 이벤트를 처리합니다.&amp;nbsp;함수에 주어진 FPRopertyChangedEvent 구조체의 Property의 Name을 GET_MEMBER_NAME_CHECKED 매크로로 해당 클래스의 변경된 변수의 이름을 비교하여 일치할 경우 구현한 동작을 실행하는 방식입니다. 사용하는 방법은 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1720765306040&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include &quot;CoreMinimal.h&quot;
#include &quot;Components/SceneComponent.h&quot;
#include &quot;PRAISpawnArea.generated.h&quot;

/**
 * 범위내에 존재하는 AISpawnPoint를 탐색하는 SceneComponent 클래스입니다.
 */
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class PROJECTREPLICA_API UPRAISpawnArea : public USceneComponent
{
	GENERATED_BODY()

public:	
	UPRAISpawnArea();

#if WITH_EDITOR
protected:
	/** 디테일 패널에서 단일 속성의 값이 변경될 때 변경된 속성에 대해 단일 속성 변경 이벤트를 처리합니다. */
	virtual void PostEditChangeProperty(struct FPropertyChangedEvent&amp;amp; PropertyChangedEvent) override;
#endif

private:
	/** SpawnArea의 Index입니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = &quot;PRAISpawnArea&quot;, meta = (AllowPrivateAccess = &quot;true&quot;))
	int32 SpawnAreaIndex;

	/** SpawnAreaBox의 범위입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;PRAISpawnArea&quot;, meta = (AllowPrivateAccess = &quot;true&quot;, DisplayPriority = 0))
	FVector SpawnAreaBoxExtent;
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1720765068128&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .cpp

UPRAISpawnArea::UPRAISpawnArea()
{
	SpawnAreaIndex = INDEX_NONE;
	SpawnAreaBoxExtent = FVector(100.0f, 100.0f, 100.0f);
}

#if WITH_EDITOR
void UPRAISpawnArea::PostEditChangeProperty(struct FPropertyChangedEvent&amp;amp; PropertyChangedEvent)
{
	Super::PostEditChangeProperty(PropertyChangedEvent);

	if(PropertyChangedEvent.Property)
	{
		FName PropertyName = PropertyChangedEvent.GetPropertyName();
		PR_LOG_WARNING(&quot;PostEditChangeProperty PropertyName: %s&quot;, *PropertyName.ToString());
		if(PropertyName == GET_MEMBER_NAME_CHECKED(UPRAISpawnArea, SpawnAreaBoxExtent))
		{
			PR_LOG_WARNING(&quot;Find PostEditChangeProperty PropertyName: %s&quot;, *PropertyName.ToString());
		}
		else if(PropertyName == GET_MEMBER_NAME_CHECKED(UPRAISpawnArea, SpawnAreaIndex))
		{
			PR_LOG_WARNING(&quot;Find PostEditChangeProperty PropertyName: %s&quot;, *PropertyName.ToString());
		}
		else
		{
			PR_LOG_WARNING(&quot;PostEditChangeProperty Property False&quot;);
		}
	}
}
#endif&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1271&quot; data-origin-height=&quot;915&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csD8RA/btsIyhqF9Ng/pCL9Xbqg6TJr2HOiRQEAB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csD8RA/btsIyhqF9Ng/pCL9Xbqg6TJr2HOiRQEAB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csD8RA/btsIyhqF9Ng/pCL9Xbqg6TJr2HOiRQEAB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsD8RA%2FbtsIyhqF9Ng%2FpCL9Xbqg6TJr2HOiRQEAB1%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;1271&quot; height=&quot;915&quot; data-origin-width=&quot;1271&quot; data-origin-height=&quot;915&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;해당 클래스를 월드에 배치하고 디테일 패널에서 기본 값이 FVector(100.0f, 100.0f, 100.0f)인 SpawnAreaBoxExtent와 -1인 SpawnAreaIndex의 값을 변경한 결과는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1907&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOrMSn/btsIxlnbSlB/33HB6vMNrNgtoAWVdCciFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOrMSn/btsIxlnbSlB/33HB6vMNrNgtoAWVdCciFK/img.png&quot; data-alt=&quot;SpawnAreaBoxExtent의 X값을 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOrMSn/btsIxlnbSlB/33HB6vMNrNgtoAWVdCciFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOrMSn%2FbtsIxlnbSlB%2F33HB6vMNrNgtoAWVdCciFK%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;1907&quot; height=&quot;101&quot; data-origin-width=&quot;1907&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaBoxExtent의 X값을 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;103&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rzhsq/btsIyFY1hNx/56yDC6RvfT28MyU9QcK9b1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rzhsq/btsIyFY1hNx/56yDC6RvfT28MyU9QcK9b1/img.png&quot; data-alt=&quot;SpawnAreaBoxExtent의 Y값을 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rzhsq/btsIyFY1hNx/56yDC6RvfT28MyU9QcK9b1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRzhsq%2FbtsIyFY1hNx%2F56yDC6RvfT28MyU9QcK9b1%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;1899&quot; height=&quot;103&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;103&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaBoxExtent의 Y값을 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1895&quot; data-origin-height=&quot;107&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEk4w7/btsIx9fgGXH/JCINzlgjzliD9C6i15hbm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEk4w7/btsIx9fgGXH/JCINzlgjzliD9C6i15hbm1/img.png&quot; data-alt=&quot;SpawnAreaBoxExtent의 Z값을 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEk4w7/btsIx9fgGXH/JCINzlgjzliD9C6i15hbm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEk4w7%2FbtsIx9fgGXH%2FJCINzlgjzliD9C6i15hbm1%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;1895&quot; height=&quot;107&quot; data-origin-width=&quot;1895&quot; data-origin-height=&quot;107&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaBoxExtent의 Z값을 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1906&quot; data-origin-height=&quot;117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2sqdd/btsIyxfTtwd/WrYMgHuzKKPS8O03rkh9aK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2sqdd/btsIyxfTtwd/WrYMgHuzKKPS8O03rkh9aK/img.png&quot; data-alt=&quot;FVector 값을 붙여넣기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2sqdd/btsIyxfTtwd/WrYMgHuzKKPS8O03rkh9aK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2sqdd%2FbtsIyxfTtwd%2FWrYMgHuzKKPS8O03rkh9aK%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;1906&quot; height=&quot;117&quot; data-origin-width=&quot;1906&quot; data-origin-height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FVector 값을 붙여넣기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GKrlC/btsIywBhQi0/nefQz9xJ65ig0EFxkk6VF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GKrlC/btsIywBhQi0/nefQz9xJ65ig0EFxkk6VF1/img.png&quot; data-alt=&quot;SpawnAreaBoxExtent의 디폴트 값으로 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GKrlC/btsIywBhQi0/nefQz9xJ65ig0EFxkk6VF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGKrlC%2FbtsIywBhQi0%2FnefQz9xJ65ig0EFxkk6VF1%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;1902&quot; height=&quot;125&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaBoxExtent의 디폴트 값으로 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DRduD/btsIyx7S44r/06eLx0lJkv5ZvAvsdUUIYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DRduD/btsIyx7S44r/06eLx0lJkv5ZvAvsdUUIYK/img.png&quot; data-alt=&quot;SpawnAreaIndex의 값을 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DRduD/btsIyx7S44r/06eLx0lJkv5ZvAvsdUUIYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDRduD%2FbtsIyx7S44r%2F06eLx0lJkv5ZvAvsdUUIYK%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;1908&quot; height=&quot;111&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaIndex의 값을 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;107&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pbgfU/btsIwSr0RbX/UETkFtkKtW7dSD3OC7uLB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pbgfU/btsIwSr0RbX/UETkFtkKtW7dSD3OC7uLB0/img.png&quot; data-alt=&quot;SpawnAreaIndex의 디폴트 값으로 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pbgfU/btsIwSr0RbX/UETkFtkKtW7dSD3OC7uLB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpbgfU%2FbtsIwSr0RbX%2FUETkFtkKtW7dSD3OC7uLB0%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;1902&quot; height=&quot;107&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;107&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaIndex의 디폴트 값으로 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;PostEditChangeChainProperty&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;PostEditChangeChainProperty 함수는 디테일 패널에서 구조체의 값이 변경되거나, 여러 속성이 연쇄적으로 변경될 때 호출되며, 여러 속성의 연쇄적 변경을 한 번에 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;PostEditChangeProperty와 마찬가지로 함수에 주어진 FPropertyChangedChainEvent 구조체를 사용하지만, PostEditChangeChainProperty는 Property가 아닌 PropertyChain 구조체를 사용하였습니다. 해당 구조체를 사용하여 GET_MEMBER_NAME_CHECKED 매크로로 변수의 이름을 비교하여 구현하면 됩니다. 이번에는 PostEditChangeChainProperty를 사용하여 각 함수 값에 출력되는 Name을 출력하기만 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1720767127443&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .h

#if WITH_EDITOR
protected:
	/** 디테일 패널에서 구조체의 값이 변경되거나, 여러 속성이 연쇄적으로 변경될 때 여러 속성의 연쇄적 변경을 한 번에 처리합니다. */
	virtual void PostEditChangeChainProperty(struct FPropertyChangedChainEvent&amp;amp; PropertyChangedEvent) override;
#endif&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1720767147967&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .cpp

#if WITH_EDITOR
void UPRAISpawnArea::PostEditChangeChainProperty(struct FPropertyChangedChainEvent&amp;amp; PropertyChangedEvent)
{
	Super::PostEditChangeChainProperty(PropertyChangedEvent);
	
	const FName PropertyStrName = PropertyChangedEvent.GetPropertyName();
	const FName MemberPropertyName = PropertyChangedEvent.GetMemberPropertyName();
	PR_LOG_ERROR(&quot;PostEditChangeChainProperty PropertyName: %s&quot;, *PropertyStrName.ToString());
	PR_LOG_ERROR(&quot;PostEditChangeChainProperty MemberPropertyName: %s&quot;, *MemberPropertyName.ToString());

	const FName HeadName = PropertyChangedEvent.PropertyChain.GetHead()-&amp;gt;GetValue()-&amp;gt;GetFName();
	const FName TailName = PropertyChangedEvent.PropertyChain.GetTail()-&amp;gt;GetValue()-&amp;gt;GetFName();
	PR_LOG_ERROR(&quot;PostEditChangeChainProperty Head: %s&quot;, *HeadName.ToString());
	PR_LOG_ERROR(&quot;PostEditChangeChainProperty Tail: %s&quot;, *TailName.ToString());

	const FName ActivateNodeName = PropertyChangedEvent.PropertyChain.GetActiveNode()-&amp;gt;GetValue()-&amp;gt;GetFName();
	const FName ActivateMemberNodeName = PropertyChangedEvent.PropertyChain.GetActiveMemberNode()-&amp;gt;GetValue()-&amp;gt;GetFName();
	PR_LOG_ERROR(&quot;PostEditChangeChainProperty MemberNodeName: %s&quot;, *ActivateNodeName.ToString());
	PR_LOG_ERROR(&quot;PostEditChangeChainProperty MemberNodeName: %s&quot;, *ActivateMemberNodeName.ToString());
}
#endif&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;147&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IjV33/btsIyYYuqI7/RzOLv9scknwQ0wYFezMNYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IjV33/btsIyYYuqI7/RzOLv9scknwQ0wYFezMNYK/img.png&quot; data-alt=&quot;SpawnAreaBoxExtent의 X값을 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IjV33/btsIyYYuqI7/RzOLv9scknwQ0wYFezMNYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIjV33%2FbtsIyYYuqI7%2FRzOLv9scknwQ0wYFezMNYK%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;1910&quot; height=&quot;147&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;147&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaBoxExtent의 X값을 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1919&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SZEra/btsIwCXf175/kChKcfFrKZFruEeQtwvMdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SZEra/btsIwCXf175/kChKcfFrKZFruEeQtwvMdK/img.png&quot; data-alt=&quot;SpawnAreaBoxExtent의 Y값을 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SZEra/btsIwCXf175/kChKcfFrKZFruEeQtwvMdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSZEra%2FbtsIwCXf175%2FkChKcfFrKZFruEeQtwvMdK%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;1919&quot; height=&quot;131&quot; data-origin-width=&quot;1919&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaBoxExtent의 Y값을 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uMrHs/btsIwyHLwE9/rXiX2Aa4DDOQulIkJleKk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uMrHs/btsIwyHLwE9/rXiX2Aa4DDOQulIkJleKk0/img.png&quot; data-alt=&quot;SpawnAreaBoxExtent의 Z값을 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uMrHs/btsIwyHLwE9/rXiX2Aa4DDOQulIkJleKk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuMrHs%2FbtsIwyHLwE9%2FrXiX2Aa4DDOQulIkJleKk0%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;1908&quot; height=&quot;159&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaBoxExtent의 Z값을 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;173&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dx54lj/btsIyXZApg7/93xgkcSpiINF6YRTzJnL9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dx54lj/btsIyXZApg7/93xgkcSpiINF6YRTzJnL9K/img.png&quot; data-alt=&quot;FVector 값을 붙여넣기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dx54lj/btsIyXZApg7/93xgkcSpiINF6YRTzJnL9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdx54lj%2FbtsIyXZApg7%2F93xgkcSpiINF6YRTzJnL9K%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;1918&quot; height=&quot;173&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;173&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FVector 값을 붙여넣기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tzqRz/btsIyf0R8uf/kx9TShKcq77rWvdiEKx3nK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tzqRz/btsIyf0R8uf/kx9TShKcq77rWvdiEKx3nK/img.png&quot; data-alt=&quot;SpawnAreaBoxExtent의 디폴트 값으로 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tzqRz/btsIyf0R8uf/kx9TShKcq77rWvdiEKx3nK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtzqRz%2FbtsIyf0R8uf%2Fkx9TShKcq77rWvdiEKx3nK%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;1914&quot; height=&quot;143&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;143&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaBoxExtent의 디폴트 값으로 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1916&quot; data-origin-height=&quot;149&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y2rB9/btsIwMZ3DqJ/uK94nWzYMSe2CjjgKrMDY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y2rB9/btsIwMZ3DqJ/uK94nWzYMSe2CjjgKrMDY1/img.png&quot; data-alt=&quot;SpawnAreaIndex의 값을 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y2rB9/btsIwMZ3DqJ/uK94nWzYMSe2CjjgKrMDY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy2rB9%2FbtsIwMZ3DqJ%2FuK94nWzYMSe2CjjgKrMDY1%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;1916&quot; height=&quot;149&quot; data-origin-width=&quot;1916&quot; data-origin-height=&quot;149&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaIndex의 값을 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1912&quot; data-origin-height=&quot;145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qFDHY/btsIwodWmLc/4Fs58kuK5BvMnu9gyJV6mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qFDHY/btsIwodWmLc/4Fs58kuK5BvMnu9gyJV6mk/img.png&quot; data-alt=&quot;SpawnAreaIndex의 디폴트 값으로 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qFDHY/btsIwodWmLc/4Fs58kuK5BvMnu9gyJV6mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqFDHY%2FbtsIwodWmLc%2F4Fs58kuK5BvMnu9gyJV6mk%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;1912&quot; height=&quot;145&quot; data-origin-width=&quot;1912&quot; data-origin-height=&quot;145&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpawnAreaIndex의 디폴트 값으로 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Unreal Engine/C++</category>
      <category>Unreal Engine</category>
      <author>한돌이</author>
      <guid isPermaLink="true">https://lykanstudio.tistory.com/115</guid>
      <comments>https://lykanstudio.tistory.com/115#entry115comment</comments>
      <pubDate>Fri, 12 Jul 2024 16:29:57 +0900</pubDate>
    </item>
    <item>
      <title>[Unreal Engine etc.] 언리얼 엔진의 메타 지정자들을 정리한 사이트</title>
      <link>https://lykanstudio.tistory.com/114</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://benui.ca/unreal/docs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://benui.ca/unreal/docs/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720079519580&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Unreal Engine Documentation&quot; data-og-description=&quot;Unreal Engine UI programmer&quot; data-og-host=&quot;benui.ca&quot; data-og-source-url=&quot;https://benui.ca/unreal/docs/&quot; data-og-url=&quot;https://benui.ca/unreal/docs/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dVhAls/hyWvIA4Lk2/7EMifcak1N8yrWVnB2YkTK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/btphSY/hyWvX55TjE/1qbfHAfodX9QBD1yv3fslK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot;&gt;&lt;a href=&quot;https://benui.ca/unreal/docs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://benui.ca/unreal/docs/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dVhAls/hyWvIA4Lk2/7EMifcak1N8yrWVnB2YkTK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/btphSY/hyWvX55TjE/1qbfHAfodX9QBD1yv3fslK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Unreal Engine Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Unreal Engine UI programmer&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;benui.ca&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Unreal Engine/etc.</category>
      <category>Unreal Engine</category>
      <author>한돌이</author>
      <guid isPermaLink="true">https://lykanstudio.tistory.com/114</guid>
      <comments>https://lykanstudio.tistory.com/114#entry114comment</comments>
      <pubDate>Thu, 4 Jul 2024 16:52:13 +0900</pubDate>
    </item>
    <item>
      <title>[Project Replica] PoolableInterface / EffectSytstem</title>
      <link>https://lykanstudio.tistory.com/113</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=aYSHRXrJcyg&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=aYSHRXrJcyg&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=aYSHRXrJcyg&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/c2QyDP/hyWoPBOlGi/uxJD1MY6iVAyNuxWW1yg41/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;[Project Replica] ObjectPoolSystem / EffectSystem / PoolableInterface&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/aYSHRXrJcyg&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/onestone3647/Portfolio_ProjectReplica.git&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/onestone3647/Portfolio_ProjectReplica.git&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719472368677&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - onestone3647/Portfolio_ProjectReplica: Seo Won Seok Protfolio&quot; data-og-description=&quot;Seo Won Seok Protfolio. Contribute to onestone3647/Portfolio_ProjectReplica development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/onestone3647/Portfolio_ProjectReplica.git&quot; data-og-url=&quot;https://github.com/onestone3647/Portfolio_ProjectReplica&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AOVxS/hyWrWM5CyS/FIJ79sfLEJBSKhrchrzF40/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/onestone3647/Portfolio_ProjectReplica.git&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/onestone3647/Portfolio_ProjectReplica.git&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AOVxS/hyWrWM5CyS/FIJ79sfLEJBSKhrchrzF40/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - onestone3647/Portfolio_ProjectReplica: Seo Won Seok Protfolio&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Seo Won Seok Protfolio. Contribute to onestone3647/Portfolio_ProjectReplica development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://lykanstudio.tistory.com/109&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://lykanstudio.tistory.com/109&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719381863257&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Project Replica] EffectSystem&quot; data-og-description=&quot;https://youtu.be/qzUb8AgOTD4&amp;nbsp;&amp;nbsp;&amp;nbsp;이펙트를 액터 컴포넌트에서 ObjectPool로 관리하는 기능을 구현했습니다.&amp;nbsp;이펙트를 액터를 사용하여 구현할 필요는 없지만, 나중에 구현할 TimeStop 기능에서 GlobalTimeDilati&quot; data-og-host=&quot;lykanstudio.tistory.com&quot; data-og-source-url=&quot;https://lykanstudio.tistory.com/109&quot; data-og-url=&quot;https://lykanstudio.tistory.com/109&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gGMF4/hyWrPAbhEU/0KAKi8Z2ZEyk2sOQyeDuQ1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/Lummk/hyWrYw7meS/9F0nA6KFldtK9wDiCK8gK0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://lykanstudio.tistory.com/109&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://lykanstudio.tistory.com/109&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gGMF4/hyWrPAbhEU/0KAKi8Z2ZEyk2sOQyeDuQ1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/Lummk/hyWrYw7meS/9F0nA6KFldtK9wDiCK8gK0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Project Replica] EffectSystem&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://youtu.be/qzUb8AgOTD4&amp;nbsp;&amp;nbsp;&amp;nbsp;이펙트를 액터 컴포넌트에서 ObjectPool로 관리하는 기능을 구현했습니다.&amp;nbsp;이펙트를 액터를 사용하여 구현할 필요는 없지만, 나중에 구현할 TimeStop 기능에서 GlobalTimeDilati&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;lykanstudio.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;이전에 구현한 EffectSystem에 PoolableInterface와 BaseObjectPoolSystem을 적용하였습니다. EffectSystem은 크게 NiagaraEffect와 ParticleEffect(Cascade)로 구분하여 ObjectPool을 생성합니다. 각 ObjectPool은 해당 이펙트의 포인터 값을 Key로, 이펙트 액터를 보관한 TArray를 Value로 가지는 TMap으로 구성되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기능 설명&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이펙트를 종류별로 ObjectPool 패턴으로 관리합니다.&lt;/li&gt;
&lt;li&gt;DataTable의 정보를 기반으로 EffectPool의 제거 및 초기화를 수행합니다.&lt;/li&gt;
&lt;li&gt;보관한 이펙트의 활성화 및 비활성화를 관리합니다.&lt;/li&gt;
&lt;li&gt;동적으로 생성한 이펙트의 Index와 수명을 관리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;부모 이펙트 액터 클래스&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;PoolableInterface&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;부모 이펙트 액터 클래스는 PoolableInterface 인터페이스를 구현하여 이펙트 액터를 EffectSystem에서 효율적으로 관리하고 재사용할수 있도록 하였습니다. 인터페이스를 통해 이펙트 액터를 표준화된 방식으로 초기화, 활성화, 비활성화하며, 수명을 관리할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719474326068&quot; class=&quot;cs&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffect.h

protected:
	/** 오브젝트가 활성화 되었는지 확인하는 함수입니다. */
	virtual bool IsActivate_Implementation() const override;
	
	/** 오브젝트를 활성화하는 함수입니다. */
	virtual void Activate_Implementation() override;

	/** 오브젝트를 비활성화하는 함수입니다. */
	virtual void Deactivate_Implementation() override;
	
	/** 오브젝트의 PoolIndex를 반환하는 함수입니다. */
	virtual int32 GetPoolIndex_Implementation() const override;

	/** 수명을 반환하는 함수입니다. */
	virtual float GetLifespan_Implementation() const override;

	/**
	 * 수명을 설정하는 함수입니다.
	 * 
	 * @param NewLifespan 설정할 수명입니다.
	 */	
	virtual void SetLifespan_Implementation(float NewLifespan) override;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719474328381&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bool APREffect::IsActivate_Implementation() const
{
	return bActivate;
}

void APREffect::Activate_Implementation()
{
	ActivateEffect();
}

void APREffect::Deactivate_Implementation()
{
	DeactivateEffect();
}

int32 APREffect::GetPoolIndex_Implementation() const
{
	return PoolIndex;
}

float APREffect::GetLifespan_Implementation() const
{
	return GetEffectLifespan();
}

void APREffect::SetLifespan_Implementation(float NewLifespan)
{
	SetEffectLifespan(NewLifespan);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;수명 관리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이펙트 액터는 활성화되는 동시에 수명만큼 타이머를 시작하며, 타이머가 만료되면 이펙트 액터를 비활성화하는 함수를 호출합니다. 이를 통해 이펙트 액터의 수명을 자동으로 관리합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719474410131&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffect.cpp

void APREffect::ActivateEffect(bool bReset)
{
	bActivate = true;
	SetActorHiddenInGame(!bActivate);

	UKismetSystemLibrary::DrawDebugSphere(GetWorld(), GetActorLocation(), 50.0f, 12, FLinearColor::White, 3.0f);

	// 이펙트의 수명을 설정합니다. 이펙트의 수명이 끝나면 이펙트를 비활성화합니다.
	SetEffectLifespan(EffectLifespan);
}

void APREffect::SetEffectLifespan(float NewLifespan)
{
	EffectLifespan = NewLifespan;
	if(bActivate)
	{
		if(NewLifespan &amp;gt; 0.0f)
		{
			// 수명이 0보다 클 경우, 즉 새로운 수명이 설정된 경우 타이머를 설정합니다.
			GetWorldTimerManager().SetTimer(EffectLifespanTimerHandle, this, &amp;amp;APREffect::OnDeactivate, NewLifespan);
		}
		else
		{
			// 수명이 0보다 작거나 같을 경우, 즉 이펙트의 수명이 무한대인 경우 이전에 설정한 타이머를 지워 제한된 수명을 가지지 않게 합니다.
			GetWorldTimerManager().ClearTimer(EffectLifespanTimerHandle);
		}
	}
}

void APREffect::OnDeactivate()
{
	IPRPoolableInterface::Execute_Deactivate(this);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;자식 이펙트(NiagaraEffect, ParticleEffect) 액터 클래스&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Effect 관리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;자식 이펙트 액터 클래스에서는 무보 클래스의 활성화 함수인 ActivateEffect 함수와 비활성화 함수인 DeactivateEffect 함수를 오버라이드하여 각 이펙트 컴포넌트의 활성화 또는 비활성화를 구현하였습니다. 이를 통해 각 이펙트 컴포넌트가 부모 클래스의 활성화 및 비활성화 메커니즘을 따르면서도 개별적인 이펙트 관리가 가능해졌습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719475061766&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PRNiagaraEffect.cpp

APRNiagaraEffect::APRNiagaraEffect()
{
	NiagaraEffect = CreateDefaultSubobject&amp;lt;UNiagaraComponent&amp;gt;(TEXT(&quot;NiagaraEffect&quot;));
	SetRootComponent(NiagaraEffect);
}

void APRNiagaraEffect::ActivateEffect(bool bReset)
{
	Super::ActivateEffect();

	if(IsValid(NiagaraEffect))
	{
		NiagaraEffect-&amp;gt;Activate(bReset);
	}
}

void APRNiagaraEffect::DeactivateEffect()
{
	Super::DeactivateEffect();

	if(IsValid(NiagaraEffect))
	{
		NiagaraEffect-&amp;gt;Deactivate();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719475085486&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PRParticleEffect.cpp

APRParticleEffect::APRParticleEffect()
{
	ParticleEffect = CreateDefaultSubobject&amp;lt;UParticleSystemComponent&amp;gt;(TEXT(&quot;ParticleEffect&quot;));
	SetRootComponent(ParticleEffect);
}

void APRParticleEffect::ActivateEffect(bool bReset)
{
	Super::ActivateEffect();

	if(IsValid(ParticleEffect))
	{
		ParticleEffect-&amp;gt;Activate(bReset);
	}
}

void APRParticleEffect::DeactivateEffect()
{
	Super::DeactivateEffect();

	if(IsValid(ParticleEffect))
	{
		ParticleEffect-&amp;gt;Deactivate();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;EffectSystem&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이펙트 액터 클래스를 ObjectPool 패턴으로 관리하는, BaseObjectPoolSystem을 상속하는 ActorComponent 클래스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Effect 관리&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;349&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJs1ny/btsIcB5eBxi/c95fuwsC2DrKpU6XvreEfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJs1ny/btsIcB5eBxi/c95fuwsC2DrKpU6XvreEfK/img.png&quot; data-alt=&quot;NiagaraPool&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJs1ny/btsIcB5eBxi/c95fuwsC2DrKpU6XvreEfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJs1ny%2FbtsIcB5eBxi%2Fc95fuwsC2DrKpU6XvreEfK%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;349&quot; height=&quot;354&quot; data-origin-width=&quot;349&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;NiagaraPool&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;EffectPool은 각 이펙트의 종류별로 구분된 2차원 컨테이너 형식으로 구현했습니다. 각 이펙트의 종류에 따라 이펙트의 포인터 값을 Key로, 해당 이펙트 액터들을 담은 TArray를 Value로 하는 TMap을 사용하여 관리합니다. C++에서 TMap의 Key나 Value로 TArray를 직접 사용할 수 없기 때문에, 구조체를 만들어 TArray를 포함한 해당 구조체를 Value로 사용했습니다. 구현한 구조체는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719386298438&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * NiagaraEffect를 보관하는 Pool을 나타내는 구조체입니다.
 */
USTRUCT(Atomic, BlueprintType)
struct FPRNiagaraEffectPool
{
	GENERATED_BODY()

public:
	FPRNiagaraEffectPool()
		: PooledEffects()
	{}

	FPRNiagaraEffectPool(const TArray&amp;lt;TObjectPtr&amp;lt;APRNiagaraEffect&amp;gt;&amp;gt;&amp;amp; NewPooledEffects)
		: PooledEffects(NewPooledEffects)
	{}

public:
	/** Pool에 보관된 NiagaraEffect들의 Array입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRNiagaraSystemPool&quot;)
	TArray&amp;lt;TObjectPtr&amp;lt;APRNiagaraEffect&amp;gt;&amp;gt; PooledEffects;
};

/**
 * 여러 NiagaraEffect Pool을 보관하는 구조체입니다.
 */
USTRUCT(Atomic, BlueprintType)
struct FPRNiagaraEffectObjectPool
{
	GENERATED_BODY()

public:
	FPRNiagaraEffectObjectPool()
		: Pool()
	{}

	FPRNiagaraEffectObjectPool(const TMap&amp;lt;TObjectPtr&amp;lt;UNiagaraSystem&amp;gt;, FPRNiagaraEffectPool&amp;gt;&amp;amp; NewPool)
		: Pool(NewPool)
	{}

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRNiagaraEffectObjectPool&quot;)
	TMap&amp;lt;TObjectPtr&amp;lt;UNiagaraSystem&amp;gt;, FPRNiagaraEffectPool&amp;gt; Pool;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;EffectPool 초기화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;초기화 함수는 ObjectPoolSystem의 InitializeObjectPool 함수를 오버라이드하여 구현했습니다. 이 함수는 각 이펙트에 해당하는 EffectPool을 초기화하는 함수를 호출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719387736039&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffectPoolSystemComponent.cpp

void UPREffectSystemComponent::InitializeObjectPool()
{
	InitializeNiagaraPool();
	InitializeParticlePool();
}

void UPREffectSystemComponent::InitializeNiagaraPool()
{
	ClearAllNiagaraPool();
	
	// NiagaraSystemPoolSettings 데이터 테이블을 기반으로 NiagaraSystemObjectPool을 생성합니다.
	if(NiagaraPoolSettingsDataTable)
	{
		TArray&amp;lt;FName&amp;gt; RowNames = NiagaraPoolSettingsDataTable-&amp;gt;GetRowNames();
		for(const FName&amp;amp; RowName : RowNames)
		{
			FPRNiagaraEffectPoolSettings* NiagaraSettings = NiagaraPoolSettingsDataTable-&amp;gt;FindRow&amp;lt;FPRNiagaraEffectPoolSettings&amp;gt;(RowName, FString(&quot;&quot;));
			if(NiagaraSettings)
			{
				CreateNiagaraPool(*NiagaraSettings);
			}
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;초기화 전, 기존의 EffectPool을 모두 제거합니다. 이후, DataTable의 정보를 기반으로 새로운 EffectPool을 생성합니다. DataTable의 행 구조를 구성하는 구조체는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719387847038&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * NiagaraEffectPool의 설정 값을 나타내는 구조체입니다.
 */
USTRUCT(Atomic, BlueprintType)
struct FPRNiagaraEffectPoolSettings : public FTableRowBase
{
	GENERATED_BODY()

public:
	FPRNiagaraEffectPoolSettings()
		: NiagaraSystem(nullptr)
		, PoolSize(0)
		, EffectLifespan(0.0f)
	{}

	FPRNiagaraEffectPoolSettings(TObjectPtr&amp;lt;UNiagaraSystem&amp;gt; NewNiagaraSystem, int32 NewPoolSize, float NewEffectLifespan)
		: NiagaraSystem(NewNiagaraSystem)
		, PoolSize(NewPoolSize)
		, EffectLifespan(NewEffectLifespan)
	{}

public:
	/** Pool에 넣을 NiagaraSystem입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRNiagaraEffectPoolSettings&quot;)
	TObjectPtr&amp;lt;UNiagaraSystem&amp;gt; NiagaraSystem;

	/** Pool의 크기입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRNiagaraEffectPoolSettings&quot;)
	int32 PoolSize;

	/** 이펙트의 수명입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRNiagaraEffectPoolSettings&quot;)
	float EffectLifespan;

public:
	/**
	 * 주어진 NiagaraEffectPoolSettings와 같은지 확인하는 ==연산자 오버로딩입니다.
	 * 
	 * @param TargetNiagaraEffectPoolSettings 비교하는 NiagaraEffectPoolSettings와 같은지 확인할 NiagaraEffectPoolSettings입니다.
	 * @return 주어진 NiagaraEffectPoolSettings와 같을 경우 true를 반환합니다. 그렇지 않을 경우 false를 반환합니다.
	 */
	FORCEINLINE bool operator==(const FPRNiagaraEffectPoolSettings&amp;amp; TargetNiagaraEffectPoolSettings) const
	{
		return this-&amp;gt;NiagaraSystem == TargetNiagaraEffectPoolSettings.NiagaraSystem
				&amp;amp;&amp;amp; this-&amp;gt;PoolSize == TargetNiagaraEffectPoolSettings.PoolSize
				&amp;amp;&amp;amp; this-&amp;gt;EffectLifespan == TargetNiagaraEffectPoolSettings.EffectLifespan;
	}

	/**
	 * 주어진 NiagaraEffectPoolSettings와 같지 않은지 확인하는 !=연산자 오버로딩입니다.
	 * 
	 * @param TargetNiagaraEffectPoolSettings 비교하는 NiagaraEffectPoolSettings와 같지 않은지 확인할 NiagaraEffectPoolSettings입니다.
	 * @return 주어진 NiagaraEffectPoolSettings와 같지 않을 경우 true를 반환합니다. 그렇지 않을 경우 false를 반환합니다.
	 */
	FORCEINLINE bool operator!=(const FPRNiagaraEffectPoolSettings&amp;amp; TargetNiagaraEffectPoolSettings) const
	{
		return this-&amp;gt;NiagaraSystem != TargetNiagaraEffectPoolSettings.NiagaraSystem
				|| this-&amp;gt;PoolSize != TargetNiagaraEffectPoolSettings.PoolSize
				|| this-&amp;gt;EffectLifespan != TargetNiagaraEffectPoolSettings.EffectLifespan;
	}
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;605&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rsNQn/btsIe1oIgYf/GAmTh9RLToqmLWQMV73ehK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rsNQn/btsIe1oIgYf/GAmTh9RLToqmLWQMV73ehK/img.png&quot; data-alt=&quot;NigaraEffectPool의 DataTable&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rsNQn/btsIe1oIgYf/GAmTh9RLToqmLWQMV73ehK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrsNQn%2FbtsIe1oIgYf%2FGAmTh9RLToqmLWQMV73ehK%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;1084&quot; height=&quot;605&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;605&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;NigaraEffectPool의 DataTable&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;구조체는 생성할 이펙트, Pool의 크기, 이펙트의 수명으로 구성되며, 구조체 간 비교르 위한 연산자 오버로딩도 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;EffectPool 제거&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;제거 함수는 ObjectPoolSystem의 ClearAllObjectPool 함수를 오버라이드하여 구현하였습니다. 이 함수는 각 이펙트에 해당하는 EffectPool을 제거하는 함수를 호출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719388117850&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffectSystemComponent.cpp

void UPREffectSystemComponent::ClearAllObjectPool()
{
	ClearAllNiagaraPool();
	ClearAllParticlePool();
}

void UPREffectSystemComponent::ClearAllNiagaraPool()
{
	ActivateNiagaraIndexList.List.Empty();
	UsedNiagaraIndexList.List.Empty();
	ClearDynamicDestroyNiagaraList(DynamicDestroyNiagaraList);
	ClearNiagaraPool(NiagaraPool);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;활성화된 이펙트의 Index 목록과 사용된 이펙트의 Index 목록을 제거하고, 동적으로 생성된 이펙트르 제거하기 위한 TimerHandle 목록도 제거합니다. 이후, EffectPool을 순회하며 존재하는 이펙트 액터를 제거합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;EffectPool 생성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;EffectPool은 이펙트 액터의 설정 값인 EffectPoolSettings 구조체를 인자로 받아 생성하게 됩니다. 주어진 설정값을 기반으로 여러개의 이펙트 액터를 생성하여 풀에 보관합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719477632478&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffectSystem.h

private:
	/**
	 * 주어진 NiagaraPool의 설정 값을 바탕으로 NiagaraPool을 생성하는 함수입니다.
	 *
	 * @param NiagaraPoolSettings NiagaraPool을 생성할 설정 값입니다.
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PRObjectPoolSystem|NiagaraSystem&quot;)
	void CreateNiagaraPool(const FPRNiagaraEffectPoolSettings&amp;amp; NiagaraPoolSettings);
    
	/**
	 * 주어진 NiagaraSystem을 월드에 PRNiagaraEffect로 Spawn하는 함수입니다.
	 *
	 * @param NiagaraSystem 월드에 Spawn할 NiagaraSystem입니다.
	 * @param PoolIndex 월드에 Spawn한 NiagaraSystem이 NiagaraPool에서 사용하는 Index 값입니다. 
	 * @param Lifespan PRNiagaraEffect의 수명입니다.
	 * @return 월드에 Spawn PRNiagaraEffect입니다.
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PRObjectPoolSystem|NiagaraSystem&quot;)
	APRNiagaraEffect* SpawnNiagaraEffectInWorld(UNiagaraSystem* NiagaraSystem, int32 PoolIndex, float Lifespan);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719476579824&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffectSystem.cpp

void UPREffectSystemComponent::CreateNiagaraPool(const FPRNiagaraEffectPoolSettings&amp;amp; NiagaraPoolSettings)
{
	if(GetWorld() &amp;amp;&amp;amp; NiagaraPoolSettings.NiagaraSystem)
	{
		FPRNiagaraEffectPool NewNiagaraEffectPool;

		// PoolSize만큼 NiagaraEffect를 월드에 Spawn한 후 NewNiagaraEffectPool에 보관합니다.
		for(int32 Index = 0; Index &amp;lt; NiagaraPoolSettings.PoolSize; Index++)
		{
			APRNiagaraEffect* SpawnNiagaraEffect = SpawnNiagaraEffectInWorld(NiagaraPoolSettings.NiagaraSystem, Index, NiagaraPoolSettings.EffectLifespan);
			if(IsValid(SpawnNiagaraEffect))
			{
				NewNiagaraEffectPool.PooledEffects.Emplace(SpawnNiagaraEffect);
			}
		}

		// 초기화된 NewNiagaraEffectPool를 NiagaraPool에 추가하고 ActivateNiagaraIndexList를 생성합니다.
		NiagaraPool.Pool.Emplace(NiagaraPoolSettings.NiagaraSystem, NewNiagaraEffectPool);
	}
}

APRNiagaraEffect* UPREffectSystemComponent::SpawnNiagaraEffectInWorld(UNiagaraSystem* NiagaraSystem, int32 PoolIndex, float Lifespan)
{
	if(!GetWorld() || !NiagaraSystem || !GetPROwner())
	{
		return nullptr;
	}

	// NiagaraEffect를 생성합니다.
	APRNiagaraEffect* NiagaraEffect = GetWorld()-&amp;gt;SpawnActor&amp;lt;APRNiagaraEffect&amp;gt;(APRNiagaraEffect::StaticClass());
	if(!IsValid(NiagaraEffect))
	{
		// NiagaraEffect 생성에 실패하면 함수를 종료하고 nullptr을 반환합니다.
		return nullptr;
	}

	// NiagaraEffect를 초기화합니다.
	NiagaraEffect-&amp;gt;InitializeNiagaraEffect(NiagaraSystem, GetPROwner(), PoolIndex, Lifespan);

	// NiagaraEffect의 OnEffectDeactivateDelegate 이벤트에 대한 콜백 함수를 바인딩합니다.
	NiagaraEffect-&amp;gt;OnEffectDeactivateDelegate.AddDynamic(this, &amp;amp;UPREffectSystemComponent::OnNiagaraEffectDeactivate);

	return NiagaraEffect;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;지정한 위치에 이펙트 생성 / 특정 Component에 이펙트 부착&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1719554891185&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffectSystem.h

	/**
	 * NiagaraEffect를 지정한 위치에 Spawn하는 함수입니다.
	 *
	 * @param SpawnEffect Spawn할 NiagaraEffect
	 * @param Location NiagaraEffect를 생성할 위치
	 * @param Rotation NiagaraEffect에 적용한 회전 값
	 * @param Scale NiagaraEffect에 적용할 크기
	 * @param bEffectAutoActivate true일 경우 NiagaraEffect를 Spawn하자마다 NiagaraEffect를 실행합니다. false일 경우 NiagaraEffect를 실행하지 않습니다.
	 * @param bReset 처음부터 다시 재생할지 여부
	 * @return 지정한 위치에 Spawn한 NiagaraEffect입니다.
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PREffectSystem|NiagaraEffect&quot;)
	APRNiagaraEffect* SpawnNiagaraEffectAtLocation(UNiagaraSystem* SpawnEffect, FVector Location, FRotator Rotation = FRotator::ZeroRotator, FVector Scale = FVector(1.0f), bool bEffectAutoActivate = true, bool bReset = false);
	
	/**
	 * NiagaraEffect를 지정한 Component에 부착하여 Spawn하는 함수입니다.
	 *
	 * @param SpawnEffect Spawn할 NiagaraEffect
	 * @param Parent NiagaraEffect를 부착할 Component
	 * @param AttachSocketName 부착할 소켓의 이름
	 * @param Location NiagaraEffect를 생성할 위치
	 * @param Rotation NiagaraEffect에 적용한 회전 값
	 * @param Scale NiagaraEffect에 적용할 크기
	 * @param bEffectAutoActivate true일 경우 NiagaraEffect를 Spawn하자마다 NiagaraEffect를 실행합니다. false일 경우 NiagaraEffect를 실행하지 않습니다.
	 * @param bReset 처음부터 다시 재생할지 여부
	 * @return 지정한 Component에 부착하여 Spawn한 NiagaraEffect입니다.
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PREffectSystem|NiagaraEffect&quot;)
	APRNiagaraEffect* SpawnNiagaraEffectAttached(UNiagaraSystem* SpawnEffect, USceneComponent* Parent, FName AttachSocketName, FVector Location, FRotator Rotation, FVector Scale = FVector(1.0f), bool bEffectAutoActivate = true, bool bReset = false);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719554934295&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffectSystem.cpp

APRNiagaraEffect* UPREffectSystemComponent::SpawnNiagaraEffectAtLocation(UNiagaraSystem* SpawnEffect, FVector Location, FRotator Rotation, FVector Scale, bool bEffectAutoActivate, bool bReset)
{
	APRNiagaraEffect* ActivateableNiagaraEffect = InitializeNiagaraEffect(SpawnEffect);
	if(IsValid(ActivateableNiagaraEffect))
	{
		// NiagaraEffect를 활성화하고 Spawn할 위치와 회전값, 크기, 자동실행 여부를 적용합니다.
		ActivateableNiagaraEffect-&amp;gt;SpawnEffectAtLocation(Location, Rotation, Scale, bEffectAutoActivate, bReset);
	
		return ActivateableNiagaraEffect;
	}

	return nullptr;
}

APRNiagaraEffect* UPREffectSystemComponent::SpawnNiagaraEffectAttached(UNiagaraSystem* SpawnEffect,	USceneComponent* Parent, FName AttachSocketName, FVector Location, FRotator Rotation, FVector Scale, bool bEffectAutoActivate, bool bReset)
{
	APRNiagaraEffect* ActivateableNiagaraEffect = InitializeNiagaraEffect(SpawnEffect);
	if(IsValid(ActivateableNiagaraEffect))
	{
		// NiagaraEffect를 활성화하고 Spawn하여 부착할 Component와 위치, 회전값, 크기, 자동실행 여부를 적용합니다.
		ActivateableNiagaraEffect-&amp;gt;SpawnEffectAttached(Parent, AttachSocketName, Location, Rotation, Scale, EAttachLocation::KeepWorldPosition, bEffectAutoActivate, bReset);
	
		return ActivateableNiagaraEffect;
	}

	return nullptr;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;월드에 Spawn한 이펙트 액터를 지정한 위치 혹은 특정 Component에 부착을 하기전 지정한 이펙트에 해당하는 이펙트 액터를 초기화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Effect 초기화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;활성화 가능한 이펙트 액터를 가져옵니다. 만약 활성화 가능한 이펙트 액터가 동적으로 생성된 경우, 이를 제거하기 위한 타이머가 작동 중이라면 타이머를 정지합니다. 그런 다음 이펙트 액터의 Index를 ActivateIndexList에 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719557894193&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffectSystem.cpp

APRNiagaraEffect* UPREffectSystemComponent::InitializeNiagaraEffect(UNiagaraSystem* SpawnEffect)
{
	APRNiagaraEffect* ActivateableNiagaraEffect = GetActivateableNiagaraEffect(SpawnEffect);
	
	// 유효하지 않는 NiagaraEffect이거나 풀링 가능한 객체가 아니면 nullptr를 반환합니다.
	if(!IsValid(ActivateableNiagaraEffect) || !IsPoolableObject(ActivateableNiagaraEffect))
	{
		return nullptr;
	}

	// 동적으로 생성한 NiagaraEffect일 경우 DynamicObjectDestroyTimer가 작동 중이라면 DynamicObjectDestroyTimer를 정지합니다.
	FTimerHandle* DynamicObjectDestroyTimer = DynamicDestroyNiagaraList.FindTimerHandleForNiagaraEffect(*ActivateableNiagaraEffect);
	if(DynamicObjectDestroyTimer)
	{
		GetWorld()-&amp;gt;GetTimerManager().ClearTimer(*DynamicObjectDestroyTimer);
	}
	
	// 해당 NiagaraSystem를 처음 활성화하는 경우 ActivateNiagaraEffectIndexList를 생성합니다.
	if(!IsCreateActivateNiagaraIndexList(SpawnEffect))
	{
		CreateActivateNiagaraIndexList(SpawnEffect);
	}

	// 활성화된 NiagaraEffect의 Index를 ActivateNiagaraIndexList에 저장합니다.
	const int32 PoolIndex = GetPoolIndex(ActivateableNiagaraEffect);
	ActivateNiagaraIndexList.GetIndexesForNiagaraSystem(*SpawnEffect)-&amp;gt;Add(PoolIndex);
	
	return ActivateableNiagaraEffect;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;활성화 가능한 Effect 반환&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;주어진 NiagaraSystem에 해당하는 Pool이 생성되었는지 확인합니다. 생성되지 않았다면, nullptr을 반환합니다. 생성된 Pool에서 해당 NigaraSystem의 Pool 내에서 활성화되지 않은 이펙트 액터를 찾으면 이를 반환합니다. Pool 내 모든 이펙트 액터가 활성화된 경우, 새로운 이펙트 액터를 동적으로 생성하여 반환합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719558223052&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffectSystem.cpp

APRNiagaraEffect* UPREffectSystemComponent::GetActivateableNiagaraEffect(UNiagaraSystem* NiagaraSystem)
{
	// NiagaraSystem이 유효하지 않을 경우 nullptr을 반환합니다.
	if(!NiagaraSystem)
	{
		return nullptr;
	}

	// 해당 NiagaraSystem에 해당하는 Pool이 생성되었는지 확인합니다.
	if(!IsCreateNiagaraPool(NiagaraSystem))
	{
		// 지정된 NiagaraSystem에 해당하는 Pool이 없으면 nullptr을 반환합니다.
		return nullptr;
	}

	// NiagaraPool에서 해당 NiagaraSystem의 Pool을 얻습니다.
	FPRNiagaraEffectPool* PoolEntry = NiagaraPool.Pool.Find(NiagaraSystem);
	if(!PoolEntry)
	{
		// 지정된 NiagaraSystem가 없으면 nullptr을 반환합니다.
		return nullptr;
	}

	// 활성화할 NiagaraEffect입니다.	
	APRNiagaraEffect* ActivateableNiagaraEffect = nullptr;

	// PoolEntry에서 활성화되지 않은 NiagaraEffect를 찾습니다.
	for(const auto&amp;amp; NiagaraEffect : PoolEntry-&amp;gt;PooledEffects)
	{
		if(!IsActivateNiagaraEffect(NiagaraEffect))
		{
			ActivateableNiagaraEffect = NiagaraEffect;

			break;
		}
	}

	// PoolEntry의 모든 NiagaraEffect가 활성화되었을 경우 새로운 NiagaraEffect를 생성합니다.
	if(!ActivateableNiagaraEffect)
	{
		ActivateableNiagaraEffect = SpawnDynamicNiagaraEffectInWorld(NiagaraSystem);
	}
	
	return ActivateableNiagaraEffect;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Effect 동적 생성&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1719560275359&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffectSystem.cpp

APRNiagaraEffect* UPREffectSystemComponent::SpawnDynamicNiagaraEffectInWorld(UNiagaraSystem* NiagaraSystem)
{
	if(!NiagaraSystem)
	{
		return nullptr;
	}
	
	APRNiagaraEffect* DynamicNiagaraEffect = nullptr;
	
	// Critical Section 시작
	FCriticalSection CriticalSection;
	CriticalSection.Lock();

	// 해당 NiagaraSystem의 UsedNiagaraIndexList가 생성되었는지 확인하고, 없으면 생성합니다.
	if(!IsCreateUsedNiagaraIndexList(NiagaraSystem))
	{
		CreateUsedNiagaraIndexList(NiagaraSystem);
	}

	// UsedNiagaraIndexList에서 해당 NiagaraSystem의 UsedIndexList를 얻습니다.
	FPRUsedIndexList* UsedIndexList = UsedNiagaraIndexList.List.Find(NiagaraSystem);
	if(UsedIndexList == nullptr)
	{
		// 지정된 NiagaraSystem이 없습니다.
		return nullptr;
	}

	// 사용 가능한 Index를 구합니다.
	const int32 NewIndex = FindAvailableIndex(UsedIndexList-&amp;gt;Indexes);
	// 사용 가능한 Index를 UsedIndexList에 추가합니다.
	UsedIndexList-&amp;gt;Indexes.Add(NewIndex);

	// Critical Section 끝
	CriticalSection.Unlock();

	// 새로운 NiagaraEffect를 생성하고 초기화합니다.
	const FPRNiagaraEffectPoolSettings NiagaraEffectSettings = GetNiagaraEffectPoolSettingsFromDataTable(NiagaraSystem);
	if(NiagaraEffectSettings != FPRNiagaraEffectPoolSettings())
	{
		// 데이터 테이블에 NiagaraEffect의 설정 값을 가지고 있을 경우 설정 값의 Lifespan을 적용합니다.
		DynamicNiagaraEffect = SpawnNiagaraEffectInWorld(NiagaraSystem, NewIndex, NiagaraEffectSettings.EffectLifespan);
	}
	else
	{
		// 데이터 테이블에 NiagaraEffect의 설정 값을 가지고 있지 않을 경우 DynamicLifespan을 적용합니다.
		DynamicNiagaraEffect = SpawnNiagaraEffectInWorld(NiagaraSystem, NewIndex, DynamicLifespan);
	}
		
	// OnDynamicNiagaraEffectDeactivate 함수를 바인딩합니다.
	DynamicNiagaraEffect-&amp;gt;OnEffectDeactivateDelegate.AddDynamic(this, &amp;amp;UPREffectSystemComponent::OnDynamicNiagaraEffectDeactivate);

	// NiagaraPool에서 해당 NiagaraSystem의 Pool을 얻습니다.
	FPRNiagaraEffectPool* PoolEntry = NiagaraPool.Pool.Find(NiagaraSystem);
	if(!PoolEntry)
	{
		// Pool이 없을 경우 생성한 NiagaraEffect를 제거하고 nullptr을 반환합니다.
		DynamicNiagaraEffect-&amp;gt;ConditionalBeginDestroy();
		
		return nullptr;
	}

	// 새로 생성한 NiagaraEffect를 PoolEntry에 추가합니다.
	PoolEntry-&amp;gt;PooledEffects.Emplace(DynamicNiagaraEffect);

	return DynamicNiagaraEffect;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이펙트 액터를 동적으로 생성할 때 FCirticalSection을 사용하여 다중 그레드 환경에서의 안정성을 보장합니다. 이는 이펙트 액터를 생성하고 Index를 부여하는 과정에서 문제가 발생하지 않도록 하기 위함입니다. 크리티컬 섹션 내부에서 이펙트 액터에 Index를 부여한 후, 크리티컬 섹션을 종료합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그 후, 주어진 이펙트에 해당하는 설정 값을 DataTable에서 가져와 이를 기반으로 이펙트 액터를 생성하고 초기화합니다. DataTable에 설정 값이 존재하면 해당 값을 사용하고, 존재하지 않으면 기본적으로 DynamicLifespan 값을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;새로 생성된 이펙트 액터의 OnEffectDeativateDelegate 델리게이트에 OnDynamicEffectDeactivate 함수를 바인딩합니다. 이 함수는 이펙트 액터가 비활성화된 후 타이머를 실행하여 일정 시간이 지난 후 이펙트 액터를 월드에서 제거하는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그 다음, EffectPool에서 해당 이펙트 액터의 Pool을 찾습니다. 만약 Pool 존재하지 않을 경우, 생성된 이펙트 액터를 제거하고 nullptr을 반환합니다. 이는 모든 이펙트 액터는 DataTable의 설정 값에 의해 생성되기 때문에, DataTable에 없는 이펙트 액터는 생성하지 않기 위해서 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Effect 비활성화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이펙트 액터가 비활성화되면 OnEffectDeactivateDelegate 델리게이트가 실행됩니다. 이 델리게이트는 이펙트 액터를 월드에 생성할 때 바인딩된 OnEffectDeactivate 함수를 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;OnEffectDeactivate 함수는 EffectSystem의 활성화된 이펙트 액터의 Index를 보관하는 ActivateIndexList에서 비활성화된 이펙트 액터의 Index를 제거합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719477482790&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffect.h

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnEffectDeactivate, APREffect*, Effect);

public:
	/** 이펙트가 비활성화될 때 실행하는 델리게이트입니다. */
	FOnEffectDeactivate OnEffectDeactivateDelegate;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719477494788&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PREffect.cpp

void APREffect::DeactivateEffect()
{
	bActivate = false;
	SetActorHiddenInGame(!bActivate);

	// 이펙트에 설정된 모든 타이머를 초기화합니다.
	GetWorldTimerManager().ClearAllTimersForObject(this);

	// 비활성화 델리게이트를 호출합니다.
	OnEffectDeactivateDelegate.Broadcast(this);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719477692579&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// EffectSystem.cpp

void UPREffectSystemComponent::OnNiagaraEffectDeactivate(APREffect* TargetEffect)
{
	APRNiagaraEffect* TargetNiagaraEffect = Cast&amp;lt;APRNiagaraEffect&amp;gt;(TargetEffect);
	// 유효하지 않는 NiagaraEffect이거나 풀링 가능한 NiagaraEffect가 아니거나 NiagaraComponent가 없으면 반환합니다.
	if(!IsValid(TargetNiagaraEffect)
		|| !IsPoolableObject(TargetNiagaraEffect)
		|| !TargetNiagaraEffect-&amp;gt;GetNiagaraEffect())
	{
		return;
	}

	// TargetNiagaraEffect가 활성화된 상태라면 비활성화합니다.
	if(IsActivateNiagaraEffect(TargetNiagaraEffect))
	{
		FPRActivateIndexList* ActivateIndexList = ActivateNiagaraIndexList.List.Find(TargetNiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
		if(ActivateIndexList)
		{
			// 비활성화된 TargetNiagaraEffect의 Index를 제거합니다.
			ActivateIndexList-&amp;gt;Indexes.Remove(GetPoolIndex(TargetNiagaraEffect));
		}
	}
}

void UPREffectSystemComponent::OnDynamicNiagaraEffectDeactivate(APREffect* TargetEffect)
{
	APRNiagaraEffect* TargetNiagaraEffect = Cast&amp;lt;APRNiagaraEffect&amp;gt;(TargetEffect);
	// 유효하지 않는 NiagaraEffect이거나 풀링 가능한 NiagaraEffect가 아니거나 NiagaraComponent가 없으면 반환합니다.
	if(!IsValid(TargetNiagaraEffect)
		|| !IsPoolableObject(TargetNiagaraEffect)
		|| !TargetNiagaraEffect-&amp;gt;GetNiagaraEffect())
	{
		return;
	}

	if(DynamicLifespan &amp;gt; 0.0f)
	{
		// 동적 수명이 끝난 후 NiagaraEffect를 제거하도록 타이머를 설정합니다.
		FTimerHandle DynamicLifespanTimerHandle;
		FTimerDelegate DynamicLifespanDelegate = FTimerDelegate::CreateUObject(this, &amp;amp;UPREffectSystemComponent::OnDynamicNiagaraEffectDestroy, TargetNiagaraEffect);
		GetWorld()-&amp;gt;GetTimerManager().SetTimer(DynamicLifespanTimerHandle, DynamicLifespanDelegate, DynamicLifespan, false);

		// TimerHandle을 추가합니다.
		FPRDynamicDestroyObject* DynamicDestroyObject = DynamicDestroyNiagaraList.List.Find(TargetNiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
		if(DynamicDestroyObject)
		{
			DynamicDestroyObject-&amp;gt;TimerHandles.Emplace(TargetNiagaraEffect, DynamicLifespanTimerHandle);
		}
		else
		{
			FPRDynamicDestroyObject NewDynamicDestroyObject;
			NewDynamicDestroyObject.TimerHandles.Emplace(TargetNiagaraEffect, DynamicLifespanTimerHandle);
			DynamicDestroyNiagaraList.List.Emplace(TargetNiagaraEffect-&amp;gt;GetNiagaraEffectAsset(), NewDynamicDestroyObject);
		}
	}
	else
	{
		// 동적 수명이 없을 경우 타이머를 실행하지 않고 바로 NiagaraEffect를 제거합니다.
		OnDynamicNiagaraEffectDestroy(TargetNiagaraEffect);
	}
}

void UPREffectSystemComponent::OnDynamicNiagaraEffectDestroy(APRNiagaraEffect* TargetNiagaraEffect)
{
	// DynamicObjectDestroyTimer를 제거합니다.
	FPRDynamicDestroyObject* DynamicDestroyObject = DynamicDestroyNiagaraList.List.Find(TargetNiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
	if(DynamicDestroyObject)
	{
		FTimerHandle* TimerHandle = DynamicDestroyObject-&amp;gt;TimerHandles.Find(TargetNiagaraEffect);
		GetWorld()-&amp;gt;GetTimerManager().ClearTimer(*TimerHandle);
		DynamicDestroyObject-&amp;gt;TimerHandles.Remove(TargetNiagaraEffect);
	}

	// NiagaraSystem의 UsedObjectIndex를 얻습니다.
	FPRUsedIndexList* UsedIndexList = UsedNiagaraIndexList.List.Find(TargetNiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
	if(UsedIndexList)
	{
		// 사용 중인 Index를 제거합니다.
		UsedIndexList-&amp;gt;Indexes.Remove(GetPoolIndex(TargetNiagaraEffect));
	}

	// NiagaraSystem의 Pool이 생성되었는지 확인합니다.
	if(IsCreateNiagaraPool(TargetNiagaraEffect-&amp;gt;GetNiagaraEffectAsset()))
	{
		// NiagaraPool에서 NiagaraEffect를 제거합니다.
		FPRNiagaraEffectPool* PoolEntry = NiagaraPool.Pool.Find(TargetNiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
		if(PoolEntry)
		{
			PoolEntry-&amp;gt;PooledEffects.Remove(TargetNiagaraEffect);
		}
	}
		
	TargetNiagaraEffect-&amp;gt;ConditionalBeginDestroy();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;동적으로 생성된 이펙트 액터의 경우, OnEffectDeactivateDelegate 델리게이트에 추가로 바인딩된 OnDynamicEffectDeactivate 함수도 호출됩니다.&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;OnDynamicEffectDeactivate 함수는 다음 작업을 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;DynamicLifespan이 0보다 큰 경우&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;동적 수명이 끝난 후 이펙트 액터를 제거하도록 타이머를 설정합니다.&lt;/li&gt;
&lt;li&gt;타이머 델리게이트를 생성하고 타이머를 설정한 후, DynamicDestroyList에 이펙트 액터와 타이머 핸들을 추가합니다.&lt;/li&gt;
&lt;li&gt;타이머가 만료되면 OnDynamicEffectDestroy 함수를 호출합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;DynamicLifespan이 0 이하인 경우 타이머를 사용하지 않고 즉시 OnDynamicEffectDestroy 함수를 호출하여 이펙트 액터를 제거합니다.&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;OnDynamicEffectDestroy 함수는 다음 작업을 수행합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;주어진 이펙트 액터의 타이머 핸들을 제거하고 타이머를 클리어합니다.&lt;/li&gt;
&lt;li&gt;UsedIndexList에서 해당 이펙트 액터의 Index를 제거합니다.&lt;/li&gt;
&lt;li&gt;해당 이펙트 액터를 EffectPool에서 제거합니다.&lt;/li&gt;
&lt;li&gt;이펙트 액터를 월드에서 제거합니다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Project/Replica</category>
      <category>Project Replica</category>
      <category>Unreal Engine</category>
      <author>한돌이</author>
      <guid isPermaLink="true">https://lykanstudio.tistory.com/113</guid>
      <comments>https://lykanstudio.tistory.com/113#entry113comment</comments>
      <pubDate>Sat, 29 Jun 2024 02:52:00 +0900</pubDate>
    </item>
    <item>
      <title>[Project Replica] PoolableInterface / BaseObjectPoolSystem</title>
      <link>https://lykanstudio.tistory.com/112</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/onestone3647/Portfolio_ProjectReplica&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/onestone3647/Portfolio_ProjectReplica&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719300324140&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - onestone3647/Portfolio_ProjectReplica: Seo Won Seok Protfolio&quot; data-og-description=&quot;Seo Won Seok Protfolio. Contribute to onestone3647/Portfolio_ProjectReplica development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/onestone3647/Portfolio_ProjectReplica&quot; data-og-url=&quot;https://github.com/onestone3647/Portfolio_ProjectReplica&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ci1xQc/hyWoM5ZDIU/PMIZsusjpL7Kfk8yu0ORGK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/onestone3647/Portfolio_ProjectReplica&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/onestone3647/Portfolio_ProjectReplica&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ci1xQc/hyWoM5ZDIU/PMIZsusjpL7Kfk8yu0ORGK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - onestone3647/Portfolio_ProjectReplica: Seo Won Seok Protfolio&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Seo Won Seok Protfolio. Contribute to onestone3647/Portfolio_ProjectReplica development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;ObjectPool로 관리하는 오브젝트들을 보다 효율적으로 관리하기 위해 PoolableInterface 인터페이스 클래스를 도입했습니다. 이를 통해 오브젝트 관리를 보다 체계적으로 할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기능 설명&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;BaseObjectPoolSystem 또는 BaseObjectPoolSystem을 상속한 ActorComponent 클래스는 PoolableInterface 인터페이스를 상속하는 오브젝트를 ObjectPool 패턴으로 관리합니다.&lt;/li&gt;
&lt;li&gt;DataTable의 정보를 기반으로 오브젝트 풀의 제거 및 초기화를 수행합니다.&lt;/li&gt;
&lt;li&gt;보관하는 오브젝트의 활성화 및 비활성화를 관리합니다.&lt;/li&gt;
&lt;li&gt;동적으로 생성한 오브젝트의 Index와 수명을 관리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;PoolableInterface&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;BaseObjectPoolSystem 또는 BaseObjectPoolSystem을 상속한 ActorComponent 클래스에서 관리하는 오브젝트들은 PoolableInterface 인터페이스 클래스를 상속하여야 합니다. PoolableInterface는 BaseObjectPoolSystem에서 관리할 수 있는 오브젝트들이 따라야 하는 인터페이스를 정의합니다. PoolableInterface는 다음과 같은 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인터페이스 정의 및 함수 선언&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;모든 함수는 블루프린트에서 호출 및 오버라이딩을 할 수 있도록 UFUNCTION의 BlueprintCallable, BlueprintNativeEvent를 사용하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;505&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHA3mQ/btsH94AfiJS/YAsw5RKkDQTtUtuUhMaKN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHA3mQ/btsH94AfiJS/YAsw5RKkDQTtUtuUhMaKN0/img.png&quot; data-alt=&quot;블루프린트에서 인터페이스 함수 호출과 인터페이스 함수의 노드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHA3mQ/btsH94AfiJS/YAsw5RKkDQTtUtuUhMaKN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHA3mQ%2FbtsH94AfiJS%2FYAsw5RKkDQTtUtuUhMaKN0%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;789&quot; height=&quot;505&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;505&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;블루프린트에서 인터페이스 함수 호출과 인터페이스 함수의 노드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;965&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KPGVP/btsH98bztQR/kb3PYKLFEjWhPVChfGvTgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KPGVP/btsH98bztQR/kb3PYKLFEjWhPVChfGvTgK/img.png&quot; data-alt=&quot;블루프린트에서 인터페이스 함수의 오버라이딩과 인터페이스 이벤트 노드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KPGVP/btsH98bztQR/kb3PYKLFEjWhPVChfGvTgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKPGVP%2FbtsH98bztQR%2Fkb3PYKLFEjWhPVChfGvTgK%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;965&quot; height=&quot;428&quot; data-origin-width=&quot;965&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;블루프린트에서 인터페이스 함수의 오버라이딩과 인터페이스 이벤트 노드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;활성화 여부 확인&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719297449226&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	/** 활성화 되었는지 확인하는 함수입니다. */
	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = &quot;Poolable&quot;)
	bool IsActivate() const;
	virtual bool IsActivate_Implementation() const = 0;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;활성화 및 비활성화&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719297458810&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	/** 활성화하는 함수입니다. */
	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = &quot;Poolable&quot;)
	void Activate();
	virtual void Activate_Implementation() = 0;
    
    	/** 비활성화하는 함수입니다. */
	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = &quot;Poolable&quot;)
	void Deactivate();
	virtual void Deactivate_Implementation() = 0;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Pool Index&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719297478286&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	/** PoolIndex를 반환하는 함수입니다. */
	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = &quot;Poolable&quot;)
	int32 GetPoolIndex() const;
	virtual int32 GetPoolIndex_Implementation() const = 0;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;수명 관리&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719297490363&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	/** 수명을 반환하는 함수입니다. */
	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = &quot;Poolable&quot;)
	float GetLifespan() const;
	virtual float GetLifespan_Implementation() const = 0;

	/**
	 * 수명을 설정하는 함수입니다.
	 * 
	 * @param NewLifespan 설정할 수명입니다.
	 */
	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = &quot;Poolable&quot;)
	void SetLifespan(float NewLifespan);
	virtual void SetLifespan_Implementation(float NewLifespan) = 0;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;BaseObjectPoolSystem&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;오브젝트 풀링 시스템을 관리하는 기본 ActorComponent 클래스입니다. 이 클래스는 오브젝트 풀링을 효율적으로 관리하기 위해 설계되었으며, 오브젝트의 활성화, 비활성화, 수명 관리, 그리고 동적 오브젝트 제거 등의 기능을 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;주요 변수&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1719303746578&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.h

protected:
	/**
	 * 동적으로 생성한 오브젝트의 수명입니다.
	 * 동적으로 생성한 오브젝트가 비활성화 되었을 때, 해당 시간이 지난 후 오브젝트를 제거합니다. 
	 */ 
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRBaseObjectPoolSystem&quot;)
	float DynamicLifespan;

	/** 동적으로 생성하는 ObjectPool의 PoolSize입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRBaseObjectPoolSystem&quot;, meta = (ClampMin = &quot;1&quot;))
	int32 DynamicPoolSize;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;관련 구조체&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1719303817583&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CommonStruct.h

/**
 * 활성화된 Index를 보관하는 구조체입니다.
 */
USTRUCT(Atomic, BlueprintType)
struct FPRActivateIndexList
{
	GENERATED_BODY()

public:
	/** 활성화된 인덱스를 보관하는 Set입니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = &quot;ActivateIndexList&quot;)
	TSet&amp;lt;int32&amp;gt; Indexes;
};

/**
 * 이전에 사용된 Index를 보관하는 구조체입니다. 
 */
USTRUCT(Atomic, BlueprintType)
struct FPRUsedIndexList
{
	GENERATED_BODY()

public:
	/** 이전에 사용된 Index를 보관하는 Set입니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = &quot;UsedIndexList&quot;)
	TSet&amp;lt;int32&amp;gt; Indexes;
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719303786901&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.h

/**
 * 클래스별로 활성화된 오브젝트들의 Index를 보관하는 구조체입니다.
 */
USTRUCT(Atomic, BlueprintType)
struct FPRActivateObjectIndexList
{
	GENERATED_BODY()

public:
	FPRActivateObjectIndexList()
		: List()
	{}

	FPRActivateObjectIndexList(const TMap&amp;lt;TSubclassOf&amp;lt;UObject&amp;gt;, FPRActivateIndexList&amp;gt;&amp;amp; NewList)
		: List(NewList)
	{}

public:
	/** 클래스 레퍼런스와 활성화된 Index를 보관하는 Map입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRAcitvateObjectIndexList&quot;)
	TMap&amp;lt;TSubclassOf&amp;lt;UObject&amp;gt;, FPRActivateIndexList&amp;gt; List;

public:
	/**
	 * 주어진 오브젝트에 해당하는 Indexes를 반환하는 함수입니다.
	 *
	 * @param ObjectToFind Indexes를 찾을 오브젝트입니다.
	 * @return Indexes를 찾을 경우 Indexes를 반환합니다. 못찾았을 경우 nullptr을 반환합니다.
	 */
	TSet&amp;lt;int32&amp;gt;* GetIndexesForObject(UObject* ObjectToFind)
	{
		if(!IsValid(ObjectToFind))
		{
			return nullptr;
		}

		TSubclassOf&amp;lt;UObject&amp;gt; ActorClass = ObjectToFind-&amp;gt;GetClass();
		FPRActivateIndexList* ActivateIndexList = List.Find(ActorClass);
		if(ActivateIndexList)
		{
			return &amp;amp;ActivateIndexList-&amp;gt;Indexes;
		}
		
		return nullptr;
	}
};

/**
 * 클래스별로 이전에 사용된 오브젝트들의 Index를 보관하는 구조체입니다.
 */
USTRUCT(Atomic, BlueprintType)
struct FPRUsedObjectIndexList
{
	GENERATED_BODY()

public:
	FPRUsedObjectIndexList()
		: List()
	{}

	FPRUsedObjectIndexList(const TMap&amp;lt;TSubclassOf&amp;lt;UObject&amp;gt;, FPRUsedIndexList&amp;gt;&amp;amp; NewList)
		: List(NewList)
	{}

public:
	/** 클래스 레퍼런스와 해당 클래스의 이전에 사용된 Index를 보관하는 Map입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRUsedObjectIndexList&quot;)
	TMap&amp;lt;TSubclassOf&amp;lt;UObject&amp;gt;, FPRUsedIndexList&amp;gt; List;
};

/**
 * 동적으로 생성한 오브젝트와 해당 오브젝트를 제거하는 TimerHandle을 관리하는 구조체입니다.
 */
USTRUCT(Atomic, BlueprintType)
struct FPRDynamicDestroyObject
{
	GENERATED_BODY()

public:
	FPRDynamicDestroyObject()
		: TimerHandles()
	{}

	FPRDynamicDestroyObject(const TMap&amp;lt;TObjectPtr&amp;lt;UObject&amp;gt;, FTimerHandle&amp;gt;&amp;amp; NewTimerHandles)
		: TimerHandles(NewTimerHandles)
	{}

public:
	/** 오브젝트와 해당 오브젝트를 제거하는 TimerHandle을 보관하는 Map입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRDynamicDestroyObject&quot;)
	TMap&amp;lt;TObjectPtr&amp;lt;UObject&amp;gt;, FTimerHandle&amp;gt; TimerHandles;
};

/**
 * 동적으로 생성한 오브젝트 목록을 클래스별로 보관하는 구조체입니다.
 */
USTRUCT(Atomic, BlueprintType)
struct FPRDynamicDestroyObjectList
{
	GENERATED_BODY()

public:
	FPRDynamicDestroyObjectList()
		: List()
	{}

	FPRDynamicDestroyObjectList(const TMap&amp;lt;TSubclassOf&amp;lt;UObject&amp;gt;, FPRDynamicDestroyObject&amp;gt;&amp;amp; NewList)
		: List(NewList)
	{}
	
public:
	/** 클래스 레퍼런스와 동적으로 생성한 오브젝트와 해당 오브젝트를 제거하는 TimerHandle을 보관한 Map입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;PRDynamicDestroyObjectPool&quot;)
	TMap&amp;lt;TSubclassOf&amp;lt;UObject&amp;gt;, FPRDynamicDestroyObject&amp;gt; List;

public:
	/**
	 * 주어진 오브젝트에 해당하는 TimerHandle을 반환하는 함수입니다.
	 *
	 * @param ObjectToFind TimerHandle을 찾을 오브젝트입니다.
	 * @return TimerHandle을 찾았을 경우 TimerHandle을 반환합니다. 못 찾았을 경우 nullptr을 반환합니다.
	 */
	FTimerHandle* FindTimerHandleForObject(UObject&amp;amp; ObjectToFind)
	{
		if(!IsValid(&amp;amp;ObjectToFind))
		{
			return nullptr;
		}

		FPRDynamicDestroyObject* DestroyObjects = List.Find(ObjectToFind.GetClass());
		if(DestroyObjects)
		{
			FTimerHandle* FoundTimerHandle = DestroyObjects-&amp;gt;TimerHandles.Find(ObjectToFind);
			if(FoundTimerHandle)
			{
				return FoundTimerHandle;
			}
		}

		return nullptr;
	}
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;주요 역할 및 기능&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;오브젝트 풀 초기화 및 제거&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719301054860&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.h

public:
	/** 기존의 ObjectPool을 제거하고, 새로 ObjectPool을 생성하여 초기화하는 함수입니다. */
	UFUNCTION(Blueprintable, Category = &quot;PRBaseObjectPoolSystem&quot;)
	virtual void InitializeObjectPool();&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719301071739&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.cpp

void UPRBaseObjectPoolSystemComponent::InitializeObjectPool()
{
	// 자식 클래스에서 오버라이딩하여 사용합니다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;InitializeObjectPool 함수는 기존의 오브젝트 풀을 제거하고 새로 오브젝트 풀을 생성하여 초기화하는 함수입니다. BaseObjectPoolSystem을 상속하는 자식 클래스에서 오버라이딩하여 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719301022288&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.h

public:
	/** 모든 ObjectPool을 제거하는 함수입니다. */
	UFUNCTION(Blueprintable, Category = &quot;PRBaseObjectPoolSystem&quot;)
	virtual void ClearAllObjectPool();&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719300987237&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.cpp

void UPRBaseObjectPoolSystemComponent::ClearAllObjectPool()
{
	// 자식 클래스에서 오버라이딩하여 사용합니다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;ClearAllObjectPool 함수는 모든 오브젝트 풀을 제거하는 함수입니다. BaseObjectPoolSystem을 상속하는 자식 클래스에서 오버라이딩하여 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;풀링 가능한 오브젝트 확인&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719302596765&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.h

public:
	/**
	 * 주어진 객체가 유효한 풀링 가능한 객체인지 확인하는 함수입니다.
	 *
	 * @param PoolableObject 확인할 객체입니다.
	 * @return 객체가 유효하고 PRPoolableInterface를 구현하는 경우 true를 반환합니다. 그렇지 않으면 false를 반환합니다.
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PRBaseObjectPoolSystem&quot;)
	bool IsPoolableObject(UObject* PoolableObject) const;

	/**
	 * 주어진 오브젝트의 클래스가 유효한 풀링 가능한 오브젝트 클래스인지 확인하는 함수입니다.
	 *
	 * @param PoolableObjectClass 확인할 오브젝트 클래스입니다.
	 * @return 오브젝트의 클래스가 유효하고 PRPoolableInterface를 구현하는 경우 true를 반환합니다. 그렇지 않으면 false를 반환합니다.
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PRBaseObjectPoolSystem&quot;)
	bool IsPoolableObjectClass(TSubclassOf&amp;lt;UObject&amp;gt; PoolableObjectClass) const;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719302629757&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.cpp

bool UPRBaseObjectPoolSystemComponent::IsPoolableObject(UObject* PoolableObject) const
{
	return IsValid(PoolableObject) &amp;amp;&amp;amp; PoolableObject-&amp;gt;GetClass()-&amp;gt;ImplementsInterface(UPRPoolableInterface::StaticClass());
}

bool UPRBaseObjectPoolSystemComponent::IsPoolableObjectClass(TSubclassOf&amp;lt;UObject&amp;gt; PoolableObjectClass) const
{
	return IsValid(PoolableObjectClass) &amp;amp;&amp;amp; PoolableObjectClass-&amp;gt;ImplementsInterface(UPRPoolableInterface::StaticClass());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;각 함수는 입력 받은 인자가 유효하고, PoolableInterface를 상속하고 있는지 ImplementsInterface 함수로 확인하여 풀링 가능한지 확인하는 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;오브젝트 활성화 및 비활성화&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719302849542&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.h

public:
	/**
	 * 주어진 객체를 활성화하는 함수입니다.
	 * 
	 * @param PoolableObject 활성화할 객체입니다.
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PRBaseObjectPoolSystem&quot;)
	void ActivateObject(UObject* PoolableObject);

	/**
	 * 주어진 객체를 비활성화하는 함수입니다.
	 * 
	 * @param PoolableObject 비활성화할 객체입니다.
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PRBaseObjectPoolSystem&quot;)
	void DeactivateObject(UObject* PoolableObject);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719302908815&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.cpp

void UPRBaseObjectPoolSystemComponent::ActivateObject(UObject* PoolableObject)
{
	if(IsPoolableObject(PoolableObject))
	{
		IPRPoolableInterface::Execute_Activate(PoolableObject);
	}
}

void UPRBaseObjectPoolSystemComponent::DeactivateObject(UObject* PoolableObject)
{
	if(IsPoolableObject(PoolableObject))
	{
		IPRPoolableInterface::Execute_Deactivate(PoolableObject);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;주어진 객체를 활성화하거나 비활성화하는 함수입니다. 주어진 객체가 유효한 풀링 가능한 오브젝트인지 확인한 후, 조건이 충족되면 PoolableInterface의 Activate 함수 또는 Deactivate 함수를 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;오브젝트 속성 관리&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719303398155&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.h

public:
	/**
 	 * 주어진 객체의 PoolIndex를 반환하는 함수입니다.
 	 * 
 	 * @param PoolableObject PoolIndex를 반환할 객체입니다.
 	 * @return 주어진 객체가 유효하고 풀링 가능한 객체일 경우 객체의 PoolIndex를 반환합니다.
 	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PRBaseObjectPoolSystem&quot;)
	int32 GetPoolIndex(UObject* PoolableObject) const;

	/**
	  * 주어진 객체의 Lifespan을 반환하는 함수입니다.
	  * 
	  * @param PoolableObject Lifespan을 반환할 객체입니다.
	  * @return 주어진 객체가 유효하고 풀링 가능한 객체일 경우 객체의 Lifespan을 반환합니다.
	  */
	UFUNCTION(BlueprintCallable, Category = &quot;PRBaseObjectPoolSystem&quot;)
	float GetLifespan(UObject* PoolableObject) const;

	/**
	  * 주어진 객체의 Lifespan을 설정하는 함수입니다.
	  * 
	  * @param PoolableObject Lifespan을 설정할 객체입니다.
	  */
	UFUNCTION(BlueprintCallable, Category = &quot;PRBaseObjectPoolSystem&quot;)
	void SetLifespan(UObject* PoolableObject, float NewLifespan);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719303427200&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.cpp

float UPRBaseObjectPoolSystemComponent::GetLifespan(UObject* PoolableObject) const
{
	if(IsPoolableObject(PoolableObject))
	{
		return IPRPoolableInterface::Execute_GetLifespan(PoolableObject);
	}
	
	return INDEX_NONE;
}

void UPRBaseObjectPoolSystemComponent::SetLifespan(UObject* PoolableObject, float NewLifespan)
{
	if(IsPoolableObject(PoolableObject))
	{
		IPRPoolableInterface::Execute_SetLifespan(PoolableObject, NewLifespan);
	}
}

int32 UPRBaseObjectPoolSystemComponent::GetPoolIndex(UObject* PoolableObject) const
{
	if(IsPoolableObject(PoolableObject))
	{
		return IPRPoolableInterface::Execute_GetPoolIndex(PoolableObject);
	}

	// 유효하지 않은 Index를 반환합니다.
	return INDEX_NONE;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;주어진 객체가 유요한 풀링 가능한 오브젝트인지 확인한 후, 조건이 충족되면 해당 PoolableInterface의 함수를 실행하는 함수들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용 가능한 Index 찾기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719303535392&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.h

public:
	/**
	 * 사용 가능한 Index를 찾아 반환하는 함수입니다.
	 * 
	 * @param UsedIndexes 이미 사용 중인 Index 목록입니다.
	 * @return 사용 가능한 Index를 반환합니다.
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PRBaseObjectPoolSystem&quot;)
	int32 FindAvailableIndex(const TSet&amp;lt;int32&amp;gt;&amp;amp; UsedIndexes);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719303554725&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.cpp

int32 UPRBaseObjectPoolSystemComponent::FindAvailableIndex(const TSet&amp;lt;int32&amp;gt;&amp;amp; UsedIndexes)
{
	int32 NewIndex = 0;
	while(UsedIndexes.Contains(NewIndex))
	{
		NewIndex++;
	}

	return NewIndex;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;주어진 Set을 순회하며 사용 가능한 Index를 찾아 반환하는 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;동적으로 생성한 오브젝트 관리&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719303692621&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.h

protected:
	/**
	 * 동적으로 생성한 오브젝트을 제거하는 함수입니다.
	 * 
	 * @param TargetDynamicDestroyObjectList 제거할 동적으로생성한 오브젝트의 목록입니다.
	 */
	UFUNCTION(Blueprintable, Category = &quot;PRBaseObjectPoolSystem&quot;)
	virtual void ClearDynamicDestroyObjectList(FPRDynamicDestroyObjectList&amp;amp; TargetDynamicDestroyObjectList);
    
 protected:
	/**
	 * Template 함수를 사용하여 동적으로 생성한 오브젝트들을 제거하는 함수입니다.
	 *
	 * @tparam KeyType Map의 키 값의 유형입니다. ObjectPoolSystem마다 다를 수 있습니다.
	 * @param List 동적으로 생성한 오브젝트의 목록입니다.
	 */
	template &amp;lt;typename KeyType&amp;gt;
	void ClearDynamicDestroyObjects(TMap&amp;lt;KeyType, FPRDynamicDestroyObject&amp;gt;&amp;amp; List)
	{
		for(auto&amp;amp; ListEntry : List)
		{
			FPRDynamicDestroyObject&amp;amp; DynamicDestroyObject = ListEntry.Value;
			if(&amp;amp;DynamicDestroyObject)
			{
				// DynamicDestroyObject의 모든 타이머를 해제하고 오브젝트를 제거합니다.
				for(auto&amp;amp; TimerEntry : DynamicDestroyObject.TimerHandles)
				{
					if(IsValid(TimerEntry.Key))
					{
						// 타이머를 해제합니다.
						GetWorld()-&amp;gt;GetTimerManager().ClearTimer(TimerEntry.Value);

						// 오브젝트를 제거합니다.
						TimerEntry.Key-&amp;gt;ConditionalBeginDestroy();		// 오브젝트를 안전하게 제거하는 함수입니다. 가비지 컬렉션 대상이 되기 전에 수동으로 메모리에서 해제합니다.
						TimerEntry.Key = nullptr;
					}
				}

				DynamicDestroyObject.TimerHandles.Empty();
			}
		}

		List.Empty();
	}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719303928787&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BaseObjectPoolSystem.cpp

void UPRBaseObjectPoolSystemComponent::ClearDynamicDestroyObjectList(FPRDynamicDestroyObjectList&amp;amp; TargetDynamicDestroyObjectList)
{
	ClearDynamicDestroyObjects(TargetDynamicDestroyObjectList.List);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;동적으로 생성된 오브젝트들은 활성화된 후 비활성화 상태가 되면 각 ObjectPoolSystem의 동적으로 생성된 오브젝트와 TimerHandle을 보관하는 Map에 저장하게 됩니다. 이 Map의 키 값은 ObjectPoolSystem마다 유형이 다를 수 있기 때문에, Template 함수를 사용하여 동적으로 생성된 오브젝트들을 제거하는 로직을 구현하였습니다.&lt;/p&gt;</description>
      <category>Project/Replica</category>
      <category>Project Replica</category>
      <category>Unreal Engine</category>
      <author>한돌이</author>
      <guid isPermaLink="true">https://lykanstudio.tistory.com/112</guid>
      <comments>https://lykanstudio.tistory.com/112#entry112comment</comments>
      <pubDate>Tue, 25 Jun 2024 17:45:30 +0900</pubDate>
    </item>
    <item>
      <title>[etc.] 크롬 F12(개발자 도구)로 다운로드 할 수 없는 동영상 다운로드 하기 / .ts파일로 쪼개진 동영상을 합치기</title>
      <link>https://lykanstudio.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;크롬의 F12(개발자 도구)로 다운로드 할 수 없는 영상은 보통 파일을 쪼개서 몇개씩 나누어 송출하는 방식을 사용하기 때문에 다운로드한다고 해도 .ts 파일형식으로 여러개로 쪼개져서 다운로드 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이럴경우 .ts 파일 형식으로 쪼개진 파일을 한 번에 다운로드하고, 다운로드한 파일을 하나로 합치는 방법을 정리하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;필요한 파일&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;총 2개의 파일이 필요한데 이 파일들은 같은 폴더 안에 존재해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ffmpeg.exe&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 글에 파일을 첨부하고 싶지만 파일의 용량이 20MB를 넘기 때문에 아래의 공식사이트의 다운로드 페이지에서 다운로드 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.ffmpeg.org/download.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.ffmpeg.org/download.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716970371739&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Download FFmpeg&quot; data-og-description=&quot;If you find FFmpeg useful, you are welcome to contribute by donating. More downloading options Git Repositories Since FFmpeg is developed with Git, multiple repositories from developers and groups of developers are available. Release Verification All FFmpe&quot; data-og-host=&quot;www.ffmpeg.org&quot; data-og-source-url=&quot;https://www.ffmpeg.org/download.html&quot; data-og-url=&quot;https://www.ffmpeg.org/download.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.ffmpeg.org/download.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.ffmpeg.org/download.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Download FFmpeg&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;If you find FFmpeg useful, you are welcome to contribute by donating. More downloading options Git Repositories Since FFmpeg is developed with Git, multiple repositories from developers and groups of developers are available. Release Verification All FFmpe&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.ffmpeg.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;855&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFC8xs/btsHFNZKH0M/npBkSbDN3tyKNTW9JibKx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFC8xs/btsHFNZKH0M/npBkSbDN3tyKNTW9JibKx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFC8xs/btsHFNZKH0M/npBkSbDN3tyKNTW9JibKx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFC8xs%2FbtsHFNZKH0M%2FnpBkSbDN3tyKNTW9JibKx1%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;1137&quot; height=&quot;855&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;855&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;사이트에 접속하게 되면 왼쪽 하단의 윈도우 로고에 마우스를 올리게 되면 2가지의 링크가 나오게 된다. 둘 중 아무 링크를 들어가도 상관없지만 필자는 위에 있는 링크를 클릭하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1127&quot; data-origin-height=&quot;777&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IKxrO/btsHGYlHeta/XSjXWxIIc2mBewI47hDw61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IKxrO/btsHGYlHeta/XSjXWxIIc2mBewI47hDw61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IKxrO/btsHGYlHeta/XSjXWxIIc2mBewI47hDw61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIKxrO%2FbtsHGYlHeta%2FXSjXWxIIc2mBewI47hDw61%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;1127&quot; height=&quot;777&quot; data-origin-width=&quot;1127&quot; data-origin-height=&quot;777&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dT3n9B/btsHFLOuZJV/COgONc4AieYCpAfuS35djK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dT3n9B/btsHFLOuZJV/COgONc4AieYCpAfuS35djK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dT3n9B/btsHFLOuZJV/COgONc4AieYCpAfuS35djK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdT3n9B%2FbtsHFLOuZJV%2FCOgONc4AieYCpAfuS35djK%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;765&quot; height=&quot;179&quot; data-origin-width=&quot;765&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;다운로드한 파일을 압축을 해제한 후, 압축 해제한 폴더의 bin 폴더에 ffmpeg.exe 파일이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;VideoDownload.bat&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/eNhc2n/btsHGuSNqe1/AAUB9A18GkjfkO1LYg51fK/VideoDownload.bat?attach=1&amp;amp;knm=tfile.bat&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;VideoDownload.bat&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&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;pre id=&quot;code_1716970830389&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ffmpeg -i &quot;Address&quot; -codec copy FileName.mp4&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;메모장을 저장한 후, 메모장의 이름을 수정한다. 이 때 메모장의 이름이 MyVideo.txt라고 할 경우 MyVideo.bat으로 파일의 확장자를 .txt에서 .bat으로 변경한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;동영상 주소 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;513&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bL0r7p/btsHG91Hklt/zkSAN22hF4T9MqZA5pFHu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bL0r7p/btsHG91Hklt/zkSAN22hF4T9MqZA5pFHu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bL0r7p/btsHG91Hklt/zkSAN22hF4T9MqZA5pFHu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL0r7p%2FbtsHG91Hklt%2FzkSAN22hF4T9MqZA5pFHu0%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;674&quot; height=&quot;513&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;513&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;동영상을 재생하고 있는 크롬에서 F12(개발자 도구)를 실행한다. 이 때 사이트를 Ctrl+R이나 F5로 새로고침해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Network 탭을 클릭하고 검색창에 m3u8을 입력한다. 검색된 리스트에 있는 파일을 마우스 오른쪽 클릭한 후 Copy -&amp;gt; Copy URL로 주소를 복사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;VideoDownload.bat 수정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;VideoDownload.bat 파일을 메모장에서 열고 코드를 수정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1716971442890&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ffmpeg -i &quot;Address&quot; -codec copy FileName.mp4&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;위의 코드에서 &quot;&quot;안에 있는 Address를 지우고 이전에 복사한 동영상 주소를 붙여넣는다. 뒤의 FileName은 자신이 원하는 파일명으로 변경하고 .mp4나 .mkv처럼 확장자를 변경하고 메모장을 저장한다.&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;이제 저장한 Videodownload.bat을 실행하면 cmd 창이 뜬 후 종료되면 Videodownload.bat와 같은 폴더에 설정한 파일명으로 동영상이 저장되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>etc.</category>
      <category>.etc</category>
      <author>한돌이</author>
      <guid isPermaLink="true">https://lykanstudio.tistory.com/110</guid>
      <comments>https://lykanstudio.tistory.com/110#entry110comment</comments>
      <pubDate>Wed, 29 May 2024 17:35:39 +0900</pubDate>
    </item>
    <item>
      <title>[Project Replica] EffectSystem</title>
      <link>https://lykanstudio.tistory.com/109</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/qzUb8AgOTD4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/qzUb8AgOTD4&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=qzUb8AgOTD4&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/cqsHVz/hyVVHo15K7/9kQa0f7txTVK9G4COme6kk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/qzUb8AgOTD4&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&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;이펙트를 액터 컴포넌트에서 ObjectPool로 관리하는 기능을 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이펙트를 액터를 사용하여 구현할 필요는 없지만, 나중에 구현할 TimeStop 기능에서 GlobalTimeDilation 대신 액터의 CustomTimeDilation을 사용할 수 있도록 하기 위해서 액터와 이를 관리할 액터 컴포넌트를 사용하여 구현하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기능 설명&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DataTable의 정보를 기반으로 이펙트를 생성합니다. 이때 생성된 이펙트는 초기에 비활성화 상태입니다.&lt;/li&gt;
&lt;li&gt;EffectSystem의 EffectPool에서 특정 Key를 사용하여 해당 이펙트의 Pool을 찾습니다. 이때, Pool이 존재하지 않는 경우 동적으로 Pool을 생성합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이펙트의 Pool에서 활성화할 수 있는 이펙트를 검색합니다. 활성화 가능한 이펙트가 있는 경우 해당 이펙트를 활성화시킵니다. 만약 활성화 가능한 이펙트가 없는 경우에는 새로운 이펙트를 동적으로 생성하여 활성화합니다.&lt;/li&gt;
&lt;li&gt;활성화된 이펙트의 수명이 다한 경우 해당 이펙트를 비활성화합니다.&lt;/li&gt;
&lt;li&gt;동적으로 생성된 이펙트는 수명이 다하면 비활성화되고, 이후에 일정 시간이 지나면 제거됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;부모 이펙트 액터 클래스&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;부모 이펙트 액터 초기화&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1714033811691&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// APREffect.h

/**
 * 이펙트를 초기화하는 함수입니다.
 *
 * @param NewEffectOwner 이펙트의 소유자
 * @param NewPoolIndex 이펙트 풀의 Index
 * @param NewLifespan 이펙트의 수명
 */
UFUNCTION(BlueprintCallable, Category = &quot;PREffect&quot;)
virtual void InitializeEffect(AActor* NewEffectOwner = nullptr, int32 NewPoolIndex= -1, float NewLifespan = 0.0f);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714033319596&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// APREffect.cpp

void APREffect::InitializeEffect(AActor* NewEffectOwner, int32 NewPoolIndex, float NewLifespan)
{
	// 이펙트를 비활성화 상태로 설정합니다.
	bActivate = false;
	SetActorHiddenInGame(!bActivate);

	EffectOwner = NewEffectOwner;
	PoolIndex = NewPoolIndex;
	EffectLifespan = NewLifespan;

	// 이펙트에 설정된 모든 타이머를 초기화합니다.
	GetWorldTimerManager().ClearAllTimersForObject(this);

	// 비활성화 델리게이트에 바인딩된 함수를 제거합니다.
	OnEffectDeactivateDelegate.Clear();
}&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;이펙트 액터가 월드에 Spawn되면 초기화 함수를 호출하여 이펙트 액터를 비활성화 상태로 설정합니다. 비활성화 상태로 설정된 이펙트 액터는 SetActorHiddenInGame 함수를 사용하여 게임에서 숨깁니다. 이후, EffectOwner(이펙트를 사용하는 액터), PoolIndex(EffectSystem의 EffectPool에서 사용하는 Index), EffectLifespan(이펙트 액터의 수명)을 설정합니다. 설정 후, 이펙트 액터에 설정된 모든 타이머를 초기화하며, 이펙트 액터를 비활성화할 때 실행하는 델리게이트에 바인딩된 함수를 제거합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;부모 이펙트 액터 활성화 / 비활성화&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1714033777551&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void APREffect::Activate()
{
	bActivate = true;
	SetActorHiddenInGame(!bActivate);

	// 이펙트의 수명을 설정합니다. 이펙트의 수명이 끝나면 이펙트를 비활성화합니다.
	SetEffectLifespan(EffectLifespan);
}

void APREffect::Deactivate()
{
	bActivate = false;
	SetActorHiddenInGame(!bActivate);

	// 이펙트에 설정된 모든 타이머를 초기화합니다.
	GetWorldTimerManager().ClearAllTimersForObject(this);

	// 비활성화 델리게이트를 호출합니다.
	OnEffectDeactivateDelegate.Broadcast(this);
}&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;이펙트 액터를 활성화 함수는 SetActorHiddenInGame 함수를 사용하여 이펙트 액터를 게임에서 보이게 합니다. 보이게 한 이펙트 액터의 수명을 설정하며, 수명을 설정하는 동안 타이머도 같이 설정합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이펙트 액터 비활성화 함수는 SetActorHiddenInGame 함수를 사용하여 이펙트 액터를 게임에서 숨깁니다. 이펙트 액터에 설정된 모든 타이머를 초기화하고 비활성화 델리게이트를 호출합니다.&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;pre id=&quot;code_1714109690169&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// APREffect.h

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnEffectDeactivate, APREffect*, Effect);

public:
	/** 이펙트가 비활성화될 때 실행하는 델리게이트입니다. */
	FOnEffectDeactivate OnEffectDeactivateDelegate;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이펙트 액터 수명 설정&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1714033866801&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// APREffect.cpp

void APREffect::SetEffectLifespan(float NewLifespan)
{
	EffectLifespan = NewLifespan;
	if(bActivate)
	{
		if(NewLifespan &amp;gt; 0.0f)
		{
			// 수명이 0보다 클 경우, 즉 새로운 수명이 설정된 경우 타이머를 설정합니다.
			GetWorldTimerManager().SetTimer(EffectLifespanTimerHandle, this, &amp;amp;APREffect::Deactivate, NewLifespan);
		}
		else
		{
			// 수명이 0보다 작거나 같을 경우, 즉 이펙트의 수명이 무한대인 경우 이전에 설정한 타이머를 지워 제한된 수명을 가지지 않게 합니다.
			GetWorldTimerManager().ClearTimer(EffectLifespanTimerHandle);
		}
	}
}&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;수명이 0.0f보다 클 경우, 즉 새로운 수명이 설정된 경우 타이머를 설정하여 수명이 다할 경우 이펙트 액터를 비활성화하는 함수를 실행합니다. 수명이 0.0f 보다 작을 경우, 즉 이펙트의 수명이 무한대인 경우 이전에 설정한 타이머를 제거하여 제한된 수명을 가지지 않게 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;자식 이펙트(NiagaraEffect, ParticleEffect) 액터 클래스&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;부모 이펙트 액터 클래스를 상속하는 자식 이펙트 액터 클래스는 사용하는 이펙트에 따라 NiagaraEffect 액터 클래스와 ParticleEffect 액터 클래스로 구분하여 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1714110475706&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// APRNiagaraEffect.h

private:
	/** Spawn한 NiagaraEffect입니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = &quot;PRNiagaraEffect&quot;, meta = (AllowPrivateAccess = &quot;true&quot;))
	TObjectPtr&amp;lt;UNiagaraComponent&amp;gt; NiagaraEffect;
    
// APRNiagaraEffect.cpp

APRNiagaraEffect::APRNiagaraEffect()
{
	NiagaraEffect = CreateDefaultSubobject&amp;lt;UNiagaraComponent&amp;gt;(TEXT(&quot;NiagaraEffect&quot;));
	SetRootComponent(NiagaraEffect);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714110533948&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// APRParticleEffect.h

private:
	/** Spawn한 ParticleEffect입니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = &quot;PRParticleEffect&quot;, meta = (AllowPrivateAccess = &quot;true&quot;))
	TObjectPtr&amp;lt;UParticleSystemComponent&amp;gt; ParticleEffect;
    
// APRParticleEffect.cpp

APRParticleEffect::APRParticleEffect()
{
	ParticleEffect = CreateDefaultSubobject&amp;lt;UParticleSystemComponent&amp;gt;(TEXT(&quot;ParticleEffect&quot;));
	SetRootComponent(ParticleEffect);
}&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;NiagaraEffect 액터는 NiagaraComponent를 RootComponent로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;ParticleEffect 액터는 ParticleSystemComponent 를 RootComponent로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이펙트 액터 초기화&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1714110702521&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// APRNiagaraEffect.h

public:
	/**
	 * 인자로 받은 NiagaraSystem을 기반으로 NiagaraEffect를 초기화하는 함수입니다.
	 *
	 * @param NiagaraSystem 사용할 이펙트
	 * @param NewEffectOwner 이펙트의 소유자
	 * @param NewPoolIndex 이펙트 풀의 Index
	 * @param NewLifespan 이펙트의 수명
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PRNiagaraEffect&quot;)
	void InitializeNiagaraEffect(UNiagaraSystem* NiagaraSystem = nullptr, AActor* NewEffectOwner = nullptr, int32 NewPoolIndex= -1, float NewLifespan = 0.0f);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714461510760&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;// APRNiagaraEffect.cpp

void APRNiagaraEffect::InitializeNiagaraEffect(UNiagaraSystem* NiagaraSystem, AActor* NewEffectOwner, int32 NewPoolIndex, float NewLifespan)
{
	InitializeEffect(NewEffectOwner, NewPoolIndex, NewLifespan);

	if(IsValid(NiagaraSystem))
	{
		NiagaraEffect-&amp;gt;SetAsset(NiagaraSystem);
	}
}&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;NiagaraEffect 액터는 초기화할 때 NiagaraComponent의 Asset을 사용할 NiagaraSystem으로 설정합니다.&lt;br /&gt;&amp;nbsp;ParticleEffect&amp;nbsp;액터를&amp;nbsp;초기화할&amp;nbsp;때&amp;nbsp;ParticleSystemComponent의&amp;nbsp;Template를&amp;nbsp;사용할&amp;nbsp;ParticleSystem으로&amp;nbsp;설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;이펙트 관리 컴포넌트(EffectSystemComponent) 클래스&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Onwer인 캐릭터가 사용하는 이펙트를 NiagaraEffect 액터와 ParticleEffect 액터로 구분하여 관리하는 컴포넌트 클래스입니다. 각각 사용하는 클래스와 이름만 다를뿐 관리하는 방법은 동일합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1714115628305&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.h

/**
 * NiagaraEffect의 풀입니다.
 */
USTRUCT(Atomic, BlueprintType)
struct FPRNiagaraEffectPool
{
	GENERATED_BODY()

public:
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = &quot;PRNiagaraEffectPool&quot;)
	TArray&amp;lt;TObjectPtr&amp;lt;APRNiagaraEffect&amp;gt;&amp;gt; Effects;
};

/**
 * 동적으로 생성한 NiagaraEffect 및 해당 NiagaraEffect를 제거할 때 사용하는 TimerHandle을 저장한 구조체입니다. 
 */
USTRUCT(Atomic, BlueprintType)
struct FPRDynamicNiagaraEffectPool
{
	GENERATED_BODY()

public:
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = &quot;DynamicNiagaraEffectPool&quot;)
	TMap&amp;lt;TObjectPtr&amp;lt;APRNiagaraEffect&amp;gt;, FTimerHandle&amp;gt; Effects;
};

private:
	/** 월드에 Spawn한 NiagaraEffect의 Pool입니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = &quot;PREffectSystem|NiagaraEffect&quot;, meta = (AllowPrivateAccess = &quot;true&quot;))
	TMap&amp;lt;TObjectPtr&amp;lt;UNiagaraSystem&amp;gt;, FPRNiagaraEffectPool&amp;gt; NiagaraEffectPool;

	/** 월드에 동적 Spawn한 NiagaraEffect의 Pool입니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = &quot;PREffectSystem|NiagaraEffect&quot;, meta = (AllowPrivateAccess = &quot;true&quot;))
	TMap&amp;lt;TObjectPtr&amp;lt;UNiagaraSystem&amp;gt;, FPRDynamicNiagaraEffectPool&amp;gt; DynamicNiagaraEffectPool;&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;이펙트 액터는 오브젝트 풀 패턴으로 관리합니다. 이펙트 풀은 TMap를 사용하며, 각 이펙트가 사용하는 이펙트 시스템 클래스 포인터를 Key값으로 사용합니다. Value값은 배열을 사용하기 위해 구조체를 선언하고, 구조체 내에서 이펙트 액터 클래스 포인터의 배열을 변수로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;EffectPool 초기화&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1714117676122&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// APRBaseCharacter.h
private:
	/** 이펙트를 관리하는 ActorComponent 클래스입니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = &quot;EffectSystem&quot;, meta = (AllowPrivateAccess = &quot;true&quot;))
	TObjectPtr&amp;lt;class UPREffectSystemComponent&amp;gt; EffectSystem;

public:
	/** ObjectPoolSystem을 반환하는 함수입니다. */
	FORCEINLINE class UPREffectSystemComponent* GetEffectSystem() const { return EffectSystem; }

// APRBaseCharacter.cpp
APRBaseCharacter::APRBaseCharacter()
{
	...
    
	// EffectSystem
	EffectSystem = CreateDefaultSubobject&amp;lt;UPREffectSystemComponent&amp;gt;(TEXT(&quot;EffectSystem&quot;));
}

void APRBaseCharacter::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	...

	// EffectSystem
	GetEffectSystem()-&amp;gt;InitializeEffectPool();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714117693212&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.cpp

void UPREffectSystemComponent::InitializeEffectPool()
{
	// 기존의 EffectPool을 초기화합니다.
	EmptyAllEffectPool();

	// DataTable의 정보를 기반으로 NiagaraEffectPool을 생성합니다.
	if(NiagaraEffectSettingsDataTable != nullptr)
	{
		TArray&amp;lt;FName&amp;gt; RowNames = NiagaraEffectSettingsDataTable-&amp;gt;GetRowNames();
		for(const FName&amp;amp; RowName: RowNames)
		{
			const FPRNiagaraEffectSettings* NiagaraEffectSettings = NiagaraEffectSettingsDataTable-&amp;gt;FindRow&amp;lt;FPRNiagaraEffectSettings&amp;gt;(RowName, FString(&quot;&quot;));
			if(NiagaraEffectSettings != nullptr)
			{
				CreateNiagaraEffectPool(*NiagaraEffectSettings);
			}
		}
	}
	
	// DataTable의 정보를 기반으로 ParticleEffectPool을 생성합니다.
	if(ParticleEffectSettingsDataTable != nullptr)
	{
		TArray&amp;lt;FName&amp;gt; RowNames = ParticleEffectSettingsDataTable-&amp;gt;GetRowNames();
		for(const FName&amp;amp; RowName: RowNames)
		{
			const FPRParticleEffectSettings* ParticleEffectSettings = ParticleEffectSettingsDataTable-&amp;gt;FindRow&amp;lt;FPRParticleEffectSettings&amp;gt;(RowName, FString(&quot;&quot;));
			if(ParticleEffectSettings != nullptr)
			{
				CreateParticleEffectPool(*ParticleEffectSettings);
			}
		}
	}
}&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;컴포넌트를 캐릭터에 부착하고 캐릭터의 PostInitializeComponent 함수에서 InitializeEffectPool 함수를 사용하여 모든 EffectPool을 초기화한 후, 데이터 테이블의 정보를 기반으로 EffectPool을 새로 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;EffectPool 생성&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1714119306741&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.h

/**
 * NiagaraEffectPool의 설정 값을 나타내는 구조체입니다.
 */
USTRUCT(Atomic, BlueprintType)
struct FPRNiagaraEffectSettings : public FTableRowBase
{
	GENERATED_BODY()

public:
	FPRNiagaraEffectSettings()
		: NiagaraSystem(nullptr)
		, PoolSize(0)
		, Lifespan(0.0f)
	{}

	FPRNiagaraEffectSettings(TObjectPtr&amp;lt;UNiagaraSystem&amp;gt; NewNiagaraSystem, int32 NewPoolSize, float NewLifespan)
		: NiagaraSystem(NewNiagaraSystem)
		, PoolSize(NewPoolSize)
		, Lifespan(NewLifespan)
	{}

public:
	/** 생성할 NiagaraSystem입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;PRNiagaraEffectSettings&quot;)
	TObjectPtr&amp;lt;UNiagaraSystem&amp;gt; NiagaraSystem;

	/** Pool에 넣을 초기에 생성할 NiagaraEffect의 수입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;PRNiagaraEffectSettings&quot;)
	int32 PoolSize;

	/** 이펙트의 수명입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;PRNiagaraEffectSettings&quot;)
	float Lifespan;

public:
	/**
	 * 인자로 받은 NiagaraEffectSettings와 같은지 판별하는 ==연산자 오버로딩입니다.
	 * 
	 * @param NewNiagaraEffectSettings 비교하는 NiagaraEffectSettings와 같은지 판별할 NiagaraEffectSettings입니다.
	 * @return 인자로 받은 NiagaraEffectSettings와 같을 경우 true를 다를 경우 false를 반환합니다.
	 */
	FORCEINLINE bool operator==(const FPRNiagaraEffectSettings&amp;amp; NewNiagaraEffectSettings) const
	{
		return this-&amp;gt;NiagaraSystem == NewNiagaraEffectSettings.NiagaraSystem
				&amp;amp;&amp;amp; this-&amp;gt;PoolSize == NewNiagaraEffectSettings.PoolSize
				&amp;amp;&amp;amp; this-&amp;gt;Lifespan == NewNiagaraEffectSettings.Lifespan;
	}

	/**
	 * 인자로 받은 NiagaraEffectSettings와 다른지 판별하는 !=연산자 오버로딩입니다.
	 * 
	 * @param NewNiagaraEffectSettings 비교하는 NiagaraEffectSettings와 같은지 판별할 NiagaraEffectSettings입니다.
	 * @return 인자로 받은 NiagaraEffectSettings와 다를 경우 true를 같을 경우 false를 반환합니다.
	 */
	FORCEINLINE bool operator!=(const FPRNiagaraEffectSettings&amp;amp; NewNiagaraEffectSettings) const
	{
		return this-&amp;gt;NiagaraSystem != NewNiagaraEffectSettings.NiagaraSystem
				&amp;amp;&amp;amp; this-&amp;gt;PoolSize != NewNiagaraEffectSettings.PoolSize
				&amp;amp;&amp;amp; this-&amp;gt;Lifespan != NewNiagaraEffectSettings.Lifespan;
	}
};

private:
	/** 인자로 받은 NiagaraEffectSettings를 기반으로 NiagaraEffectPool을 생성하는 함수입니다. */
	UFUNCTION(BlueprintCallable, Category = &quot;PREffectSystem|NiagaraEffect&quot;)
	void CreateNiagaraEffectPool(FPRNiagaraEffectSettings NiagaraEffectSettings);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714119353377&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.cpp

void UPREffectSystemComponent::CreateNiagaraEffectPool(FPRNiagaraEffectSettings NiagaraEffectSettings)
{
	if(GetWorld() != nullptr &amp;amp;&amp;amp; NiagaraEffectSettings.NiagaraSystem != nullptr)
	{
		// NiagaraEffectPool에 추가할 Pair를 초기화하고 NiagaraEffect를 생성하여 추가합니다.
		FPRNiagaraEffectPool Pair;
		for(int32 Index = 0; Index &amp;lt; NiagaraEffectSettings.PoolSize; Index++)
		{
			APRNiagaraEffect* SpawnNiagaraEffect = SpawnNiagaraEffectInWorld(NiagaraEffectSettings.NiagaraSystem, Index, NiagaraEffectSettings.Lifespan);
			if(IsValid(SpawnNiagaraEffect))
			{
				Pair.Effects.Emplace(SpawnNiagaraEffect);
			}
		}

		// 초기화된 Pair를 NiagaraEffectPool에 추가하고 ActivateNiagaraEffectIndexList와 UsedNiagaraEffectIndexList를 생성합니다.
		NiagaraEffectPool.Emplace(NiagaraEffectSettings.NiagaraSystem, Pair);
		CreateActivateNiagaraEffectIndexList(NiagaraEffectSettings.NiagaraSystem);
		CreateUsedNiagaraEffectIndexList(NiagaraEffectSettings.NiagaraSystem);
	}
}

void UPREffectSystemComponent::CreateActivateNiagaraEffectIndexList(UNiagaraSystem* NiagaraSystem)
{
	ActivateNiagaraEffectIndexList.Emplace(NiagaraSystem);
}

void UPREffectSystemComponent::CreateUsedNiagaraEffectIndexList(UNiagaraSystem* NiagaraSystem)
{
	FPRNiagaraEffectPool* Pair = NiagaraEffectPool.Find(NiagaraSystem);
	if(Pair == nullptr)
	{
		// 지정된 NiagaraEffect가 업습니다.
		return;
	}
	
	FPRUsedIndexList UsedEffectIndexList;
	for(const auto&amp;amp; NiagaraEffect : Pair-&amp;gt;Effects)
	{
		UsedEffectIndexList.Indexes.Add(NiagaraEffect-&amp;gt;GetPoolIndex());
	}

	UsedNiagaraEffectIndexList.Emplace(NiagaraSystem, UsedEffectIndexList);
}&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;EffectPool은 FPREffectSettings 구조체를 인자로 받아 생성합니다. EffectSettings 구조체는 생성할 이펙트의 System 포인터와 EffectPool의 크기(생성할 이펙트 액터의 수), 이펙트의 수명을 변수로 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;PoolSize만큼 이펙트 액터를 월드에 Spawn한 후, 생성한 이펙트 액터를 배열로 보관하는 FPREffectPool 구조체에 저장한 후 생성한 이펙트의 System 포인터를 Key값으로 하고, FPREffectPool 구조체를 Value값으로 하여 EffectPool에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;활성화된 이펙트의 Index를 저장하는 ActivateEffectIndexList와 이펙트 액터를 생성하여 사용된 이펙트의 Index를 저장하는 UsedEffectIndexList를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;UsedEffectIndexList는 PoolSize를 초과하여 이펙트 액터를 생성하게 되거나 데이터 테이블에 존재하지 않는 이펙트 액터를 생성할 때 사용하게 되는 동적 생성에서 사용하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spawn 이펙트 액터 InWorld&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1714120393018&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.h

private:
	/** 인자로 받은 NiagaraSystem를 월드에 APRNiagaraEffect로 Spawn하는 함수입니다. */
	UFUNCTION(BlueprintCallable, Category = &quot;PREffectSystem|NiagaraEffect&quot;)
	APRNiagaraEffect* SpawnNiagaraEffectInWorld(UNiagaraSystem* NiagaraSystem, int32 PoolIndex = -1, float Lifespan = 0.0f);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714120453980&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.cpp
APRNiagaraEffect* UPREffectSystemComponent::SpawnNiagaraEffectInWorld(UNiagaraSystem* NiagaraSystem, int32 PoolIndex, float Lifespan)
{
	if(!GetWorld() || !NiagaraSystem || !GetPROwner())
	{
		return nullptr;
	}

	// NiagaraEffect를 생성합니다.
	APRNiagaraEffect* NiagaraEffect = GetWorld()-&amp;gt;SpawnActor&amp;lt;APRNiagaraEffect&amp;gt;(APRNiagaraEffect::StaticClass());
	if(!IsValid(NiagaraEffect))
	{
		// NiagaraEffect 생성에 실패하면 함수를 종료하고 nullptr을 반환합니다.
		return nullptr;
	}

	// NiagaraEffect를 초기화합니다.
	NiagaraEffect-&amp;gt;InitializeNiagaraEffect(NiagaraSystem, GetPROwner(), PoolIndex, Lifespan);

	// NiagaraEffect의 OnEffectDeactivateDelegate 이벤트에 대한 콜백 함수를 바인딩합니다.
	NiagaraEffect-&amp;gt;OnEffectDeactivateDelegate.AddDynamic(this, &amp;amp;UPREffectSystemComponent::OnNiagaraEffectDeactivate);

	return NiagaraEffect;
}

void UPREffectSystemComponent::OnNiagaraEffectDeactivate(APREffect* Effect)
{
	APRNiagaraEffect* NiagaraEffect = Cast&amp;lt;APRNiagaraEffect&amp;gt;(Effect);
	if(IsValid(NiagaraEffect) &amp;amp;&amp;amp; NiagaraEffect-&amp;gt;GetNiagaraEffect() != nullptr)
	{
		FPRActivateIndexList* ActivateIndexList = ActivateNiagaraEffectIndexList.Find(NiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
		if(ActivateIndexList != nullptr)
		{
			// 비활성화된 NiagaraEffect의 Index를 ActivateIndexList에서 제거합니다.
			ActivateIndexList-&amp;gt;Indexes.Remove(NiagaraEffect-&amp;gt;GetPoolIndex());
		}
	}
}&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;이펙트 액터는 SpawnActor 함수를 사용하여 월드에 Spawn한 후, 각 이펙트 액터의 InitializeEffect 함수를 호출하여 초기화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이후에 이펙트 액터가 수명을 다하여 비활성화될 때 호출하는 델리게이트에 OnEffectDeactivate 함수를 바인딩합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;OnEffectDeactivate 함수는 활성화된 이펙트 액터의 Index를 저장한 ActivateEffectIndexList에서 비활성화된 이펙트 액터의 Index를 제거하는 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이펙트 액터 SpawnAtLocation / SpawnAttached&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1714236371288&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.h

public:
	/**
	 * 이펙트를 지정한 위치에 Spawn하는 함수입니다.
	 *
	 * @param SpawnEffect Spawn할 이펙트
	 * @param Location 이펙트를 생성할 위치
	 * @param Rotation 이펙트에 적용한 회전 값
	 * @param Scale 이펙트에 적용할 크기
	 * @param bEffectAutoActivate true일 경우 이펙트를 Spawn하자마다 이펙트를 실행합니다. false일 경우 이펙트를 실행하지 않습니다.
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PREffectSystem|NiagaraEffect&quot;)
	APRNiagaraEffect* SpawnNiagaraEffectAtLocation(UNiagaraSystem* SpawnEffect, FVector Location, FRotator Rotation = FRotator::ZeroRotator, FVector Scale = FVector(1.0f), bool bEffectAutoActivate = true);

	
	/**
	 * 이펙트를 지정한 Component에 부착하여 Spawn하는 함수입니다.
	 *
	 * @param SpawnEffect Spawn할 이펙트
	 * @param Parent 이펙트를 부착할 Component
	 * @param AttachSocketName 부착할 소켓의 이름
	 * @param Location 이펙트를 생성할 위치
	 * @param Rotation 이펙트에 적용한 회전 값
	 * @param Scale 이펙트에 적용할 크기
	 * @param bEffectAutoActivate true일 경우 이펙트를 Spawn하자마다 이펙트를 실행합니다. false일 경우 이펙트를 실행하지 않습니다.
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PREffectSystem|NiagaraEffect&quot;)
	APRNiagaraEffect* SpawnNiagaraEffectAttached(UNiagaraSystem* SpawnEffect, USceneComponent* Parent, FName AttachSocketName, FVector Location, FRotator Rotation, FVector Scale, bool bEffectAutoActivate = true);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714236423201&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.cpp

APRNiagaraEffect* UPREffectSystemComponent::SpawnNiagaraEffectAtLocation(UNiagaraSystem* SpawnEffect, FVector Location, FRotator Rotation, FVector Scale, bool bEffectAutoActivate)
{
	APRNiagaraEffect* ActivateableNiagaraEffect = GetActivateableNiagaraEffect(SpawnEffect);
	if(IsValid(ActivateableNiagaraEffect))
	{
		// NiagaraEffect를 활성화하고 Spawn할 위치와 회전값, 크기, 자동실행 여부를 적용합니다.
		ActivateableNiagaraEffect-&amp;gt;SpawnEffectAtLocation(Location, Rotation, Scale, bEffectAutoActivate);

		// 활성화된 NiagaraEffect의 Index를 ActivateNiagaraEffectIndexList에 추가합니다.
		ActivateNiagaraEffectIndexList.Find(SpawnEffect)-&amp;gt;Indexes.Add(ActivateableNiagaraEffect-&amp;gt;GetPoolIndex());
	}
	
	return ActivateableNiagaraEffect;
}

APRNiagaraEffect* UPREffectSystemComponent::SpawnNiagaraEffectAttached(UNiagaraSystem* SpawnEffect, USceneComponent* Parent, FName AttachSocketName, FVector Location, FRotator Rotation, FVector Scale, bool bEffectAutoActivate)
{
	APRNiagaraEffect* ActivateableNiagaraEffect = GetActivateableNiagaraEffect(SpawnEffect);
	if(IsValid(ActivateableNiagaraEffect))
	{
		// NiagaraEffect를 활성화하고 Spawn하여 부착할 Component와 위치, 회전값, 크기, 자동실행 여부를 적용합니다.
		ActivateableNiagaraEffect-&amp;gt;SpawnEffectAttached(Parent, AttachSocketName, Location, Rotation, Scale, bEffectAutoActivate);

		// 활성화된 NiagaraEffect의 Index를 ActivateNiagaraEffectIndexList에 추가합니다.
		ActivateNiagaraEffectIndexList.Find(SpawnEffect)-&amp;gt;Indexes.Add(ActivateableNiagaraEffect-&amp;gt;GetPoolIndex());
	}
	
	return ActivateableNiagaraEffect;
}&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;이펙트 액터를 지정한 위치에 Spawn하거나 Component에 부착할 때 GetActivateableEffect 함수를 사용하여 활성화할 수 있는 이펙트 액터를 가져옵니다. 가져온 각 이펙트 액터를 Spawn한 후, 활성화된 이펙트 액터의 Index를 ActivateEffectIndexList에 추가합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;활성화할 수 있는 이펙트 액터 반환&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1714239266143&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.h


private:
	/** 인자에 해당하는 활성화할 수 있는 NiagaraSystem을 반환하는 함수입니다. */
	UFUNCTION(BlueprintCallable, Category = &quot;PREffectSystem|NiagaraEffect&quot;)
	APRNiagaraEffect* GetActivateableNiagaraEffect(UNiagaraSystem* NiagaraSystem);

private:
	/** 동적으로 생성한 Effect의 수명입니다. */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;PREffectSystem&quot;, meta = (AllowPrivateAccess = &quot;true&quot;))
	float DynamicLifespan;

	/** 월드에 Spawn한 NiagaraEffect의 Pool입니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = &quot;PREffectSystem|NiagaraEffect&quot;, meta = (AllowPrivateAccess = &quot;true&quot;))
	TMap&amp;lt;TObjectPtr&amp;lt;UNiagaraSystem&amp;gt;, FPRNiagaraEffectPool&amp;gt; NiagaraEffectPool;

	/** 월드에 동적 Spawn한 NiagaraEffect의 Pool입니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = &quot;PREffectSystem|NiagaraEffect&quot;, meta = (AllowPrivateAccess = &quot;true&quot;))
	TMap&amp;lt;TObjectPtr&amp;lt;UNiagaraSystem&amp;gt;, FPRDynamicNiagaraEffectPool&amp;gt; DynamicNiagaraEffectPool;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714456165002&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.cpp

APRNiagaraEffect* UPREffectSystemComponent::GetActivateableNiagaraEffect(UNiagaraSystem* NiagaraSystem)
{
	// 해당 NiagaraEffect에 해당하는 Pool이 생성되었는지 확인하고, 없으면 생성합니다.
	if(!IsCreateNiagaraEffectPool(NiagaraSystem))
	{
		FPRNiagaraEffectSettings NiagaraEffectSettings = FPRNiagaraEffectSettings(NiagaraSystem, 1, DynamicLifespan);
		CreateNiagaraEffectPool(NiagaraEffectSettings);
	}

	// NiagaraEffectPool에서 해당 NiagaraEffect의 Pool을 얻습니다.
	FPRNiagaraEffectPool* Pair = NiagaraEffectPool.Find(NiagaraSystem);
	if(Pair == nullptr)
	{
		// 지정된 NiagaraEffect가 업습니다.
		return nullptr;
	}

	APRNiagaraEffect* ActivateableNiagaraEffect = nullptr;

	// Pair에서 활성화되지 않은 NiagaraEffect를 찾아 활성화합니다.
	for(const auto&amp;amp; NiagaraEffect : Pair-&amp;gt;Effects)
	{
		if(IsValid(NiagaraEffect) &amp;amp;&amp;amp; !IsActivateNiagaraEffect(NiagaraEffect))
		{
			ActivateableNiagaraEffect = NiagaraEffect;
			break;
		}
	}

	// Pair의 모든 NiagaraEffect가 활성화되었을 경우 새로운 NiagaraEffect를 생성합니다.
	if(ActivateableNiagaraEffect == nullptr)
	{
		ActivateableNiagaraEffect = SpawnDynamicNiagaraEffectInWorld(NiagaraSystem);
	}
	
	// 동적으로 생성된 NiagaraEffect일 경우 DynamicEffectDestroyTimer를 정지합니다.
	FPRDynamicNiagaraEffectPool* DynamicEffectPool = DynamicNiagaraEffectPool.Find(ActivateableNiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
	if(DynamicEffectPool != nullptr)
	{
		FTimerHandle* DynamicDestroyTimer = DynamicEffectPool-&amp;gt;Effects.Find(ActivateableNiagaraEffect);
		if(DynamicDestroyTimer != nullptr)
		{
			GetWorld()-&amp;gt;GetTimerManager().ClearTimer(*DynamicDestroyTimer);			
		}
	}
	
	return ActivateableNiagaraEffect;
}&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;인자로 받은 이펙트에 해당하는 EffectPool이 생성되었는지 확인하고, 없을 경우 EffectPool을 생성합니다. 이 때 생성하는 EffectPool의 크기는 1이며, 생성한 이펙트 액터의 수명은 DynamicListspan 값입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;EffectPool이 이미 생성되었을 경우에는 Pool을 가져옵니다. 가져온 Pool에서 활성화되지 않은 이펙트 액터를 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;만약 Pool의 모든 이펙트 액터가 활성화되어 있는 경우에는 SpawnDynamicEffectInWorld 함수를 사용하여 새로운 이펙트 액터를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;활성화할 이펙트 액터가 동적으로 생성된 이펙트 액터일 경우 동적으로 생성한 이펙트 액터를 제거하는 DynamicEffectDestroyTimer를 정지하고, 활성화할 이펙트 액터를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이펙트 액터 동적 생성&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1714457649607&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.h

private:
	/**
	 * 사용 가능한 Index를 찾아 반환하는 함수입니다. 
	 * 
	 * @param UsedIndexes 이미 사용 중인 Index 목록
	 * @return 사용가능한 Index
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;PREffectSystem&quot;)
	int32 FindAvailableIndex(const TSet&amp;lt;int32&amp;gt;&amp;amp; UsedIndexes);

private:
	/** 인자로 받은 NiagaraSystem를 월드에 APRNiagaraEffect로 동적 Spawn하는 함수입니다. */
	UFUNCTION(BlueprintCallable, Category = &quot;PREffectSystem|NiagaraEffect&quot;)
	APRNiagaraEffect* SpawnDynamicNiagaraEffectInWorld(UNiagaraSystem* NiagaraSystem);	

	/** 인자로 받은 NiagaraEffect의 설정 값을 데이터 테이블에서 가져오는 함수입니다. */
	UFUNCTION(BlueprintCallable, Category = &quot;PREffectSystem|NiagaraEffect&quot;)
	FPRNiagaraEffectSettings GetNiagaraEffectSettingsFromDataTable(UNiagaraSystem* NiagaraSystem) const; 
    
	/** 인자로 받은 NiagaraEffect의 설정 값을 데이터 테이블에서 가져오는 함수입니다. */
	UFUNCTION(BlueprintCallable, Category = &quot;PREffectSystem|NiagaraEffect&quot;)
	FPRNiagaraEffectSettings GetNiagaraEffectSettingsFromDataTable(UNiagaraSystem* NiagaraSystem) const; 
    
private:
	/**
	 * 동적으로 생성된 NiagaraEffect의 Index를 추적하기 위한 Index 목록입니다.
	 * 동적으로 생성한 NiagaraEffect의 Index를 부여할 때 해당 Index를 여기에 저장합니다.
	 */
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = &quot;PREffectSystem|NiagaraEffect&quot;, meta = (AllowPrivateAccess = &quot;true&quot;))
	TMap&amp;lt;TObjectPtr&amp;lt;UNiagaraSystem&amp;gt;, FPRUsedIndexList&amp;gt; UsedNiagaraEffectIndexList;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714457684464&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.cpp

int32 UPREffectSystemComponent::FindAvailableIndex(const TSet&amp;lt;int32&amp;gt;&amp;amp; UsedIndexes)
{
	int32 NewIndex = 0;
	while(UsedIndexes.Contains(NewIndex))
	{
		NewIndex++;
	}

	return NewIndex;
}

APRNiagaraEffect* UPREffectSystemComponent::SpawnDynamicNiagaraEffectInWorld(UNiagaraSystem* NiagaraSystem)
{
	APRNiagaraEffect* ActivateableNiagaraEffect = nullptr;

	FPRUsedIndexList* UsedEffectIndexList = UsedNiagaraEffectIndexList.Find(NiagaraSystem);
	if(UsedEffectIndexList == nullptr)
	{
		// 지정된 NiagaraEffect가 없습니다.
		return nullptr;
	}

	// 사용 가능한 Index를 구합니다.
	const int32 NewIndex = FindAvailableIndex(UsedEffectIndexList-&amp;gt;Indexes);

	// 사용 가능한 Index를 UsedEffectIndexList에 저장합니다.
	UsedEffectIndexList-&amp;gt;Indexes.Add(NewIndex);

	// 새로운 NiagaraEffect를 생성하고 초기화합니다.
	const FPRNiagaraEffectSettings NiagaraEffectSettings = GetNiagaraEffectSettingsFromDataTable(NiagaraSystem);
	if(NiagaraEffectSettings != FPRNiagaraEffectSettings())
	{
		// 데이터 테이블에 NiagaraEffect의 설정 값을 가지고 있을 경우 설정 값의 Lifespan을 적용합니다.
		ActivateableNiagaraEffect = SpawnNiagaraEffectInWorld(NiagaraSystem, NewIndex, NiagaraEffectSettings.Lifespan);
	}
	else
	{
		// 데이터 테이블에 NiagaraEffect의 설정 값을 가지고 있지 않을 경우 DynamicLifespan을 적용합니다.
		ActivateableNiagaraEffect = SpawnNiagaraEffectInWorld(NiagaraSystem, NewIndex, DynamicLifespan);
	}
		
	if(!IsValid(ActivateableNiagaraEffect))
	{
		// 지정된 NiagaraEffect가 없습니다.
		return nullptr;
	}
		
	// ActivateableNiagaraEffect의 OnNiagaraEffectDeactivate 이벤트에 대한 콜백 함수를 바인딩합니다.
	// 동적으로 생성된 NiagaraEffect에 대한 추가로 비활성화하는 함수입니다.
	ActivateableNiagaraEffect-&amp;gt;OnEffectDeactivateDelegate.AddDynamic(this, &amp;amp;UPREffectSystemComponent::OnDynamicNiagaraEffectDeactivate);

	// NiagaraEffectPool에서 해당 NiagaraEffect의 Pool을 얻습니다.
	FPRNiagaraEffectPool* Pair = NiagaraEffectPool.Find(NiagaraSystem);
	if(Pair == nullptr)
	{
		// 지정된 NiagaraEffect가 업습니다.
		return nullptr;
	}
	
	// 새로 생성한 NiagaraEffect를 Pair에 저장합니다.
	Pair-&amp;gt;Effects.Emplace(ActivateableNiagaraEffect);

	return ActivateableNiagaraEffect;
}

FPRNiagaraEffectSettings UPREffectSystemComponent::GetNiagaraEffectSettingsFromDataTable(UNiagaraSystem* NiagaraSystem) const
{
	if(NiagaraEffectSettingsDataTable != nullptr)
	{
		TArray&amp;lt;FName&amp;gt; RowNames = NiagaraEffectSettingsDataTable-&amp;gt;GetRowNames();
		for(const FName&amp;amp; RowName: RowNames)
		{
			FPRNiagaraEffectSettings* NiagaraEffectSettings = NiagaraEffectSettingsDataTable-&amp;gt;FindRow&amp;lt;FPRNiagaraEffectSettings&amp;gt;(RowName, FString(&quot;&quot;));
			if(NiagaraEffectSettings != nullptr &amp;amp;&amp;amp; NiagaraEffectSettings-&amp;gt;NiagaraSystem == NiagaraSystem)
			{
				return *NiagaraEffectSettings;
			}
		}
	}

	return FPRNiagaraEffectSettings();
}&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;동적으로 이펙트 액터를 생성하는 함수는 독립적으로 사용하지 않습니다. 활성화 가능한 이펙트 액터를 가져오는 함수에서만 사용합니다. 만약 인자로 받은 이펙트의 EffectPool이 없어서 EffectPool을 생성할 때 UsedEffectIndexList도 같이 이 생성되기 때문에, 인자로 받은 이펙트의 UsedEffectIndexList가 존재하지 않을 경우 인자로 받은 이펙트의 EffectPool이 없으므로 nullptr을 반환하고 종료합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;UsedEffectIndexList 함수를 사용하여 동적으로 생성할 이펙트 액터가 사용 가능한 Index를 가져옵니다. 가져온 사용 가능한 Index는 UsedEffectIndexList에 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;새로운 이펙트 액터를 생성할 때 데이터 테이블에 인자로 받은 이펙트에 해당하는 설정 값(EffectSettings 구조체)가 있을 경우 설정 값의 Lifespan(수명)을 적용하여 생성합니다. 데이터 테이블에 설정 값이 존재하지 않을 경우 이펙트 액터의 수명은 DynamicLifespan 값을 적용하여 생성합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이펙트 액터를 생성하였을 경우, 이펙트 액터가 비활성화 될때 실행하는 OnEffectDeactivateDelegate 델리게이트에 OndynamicEffectDeactivate 함수를 바인딩합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;새로 생성한 이펙트 액터를 EffectPool에 저장하고, 새로 생성한 이펙트 액터를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이펙트 액터의 비활성화&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1714458892082&quot; class=&quot;actionscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;// APREffect.h

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnEffectDeactivate, APREffect*, Effect);

public:
	/** 이펙트가 비활성화될 때 실행하는 델리게이트입니다. */
	FOnEffectDeactivate OnEffectDeactivateDelegate;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714458878969&quot; class=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;// APREffect.cpp

void APREffect::Activate()
{
	bActivate = true;
	SetActorHiddenInGame(!bActivate);

	// 이펙트의 수명을 설정합니다. 이펙트의 수명이 끝나면 이펙트를 비활성화합니다.
	SetEffectLifespan(EffectLifespan);
}

void APREffect::Deactivate()
{
	bActivate = false;
	SetActorHiddenInGame(!bActivate);

	// 이펙트에 설정된 모든 타이머를 초기화합니다.
	GetWorldTimerManager().ClearAllTimersForObject(this);

	// 비활성화 델리게이트를 호출합니다.
	OnEffectDeactivateDelegate.Broadcast(this);
}&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;앞에서 설명한대로, 각 이펙트 액터를 각자 수명을 가지며 좌표나 컴포넌트에 부착하여 활성화할 경우 수명만큼의 타이머를 설정합니다. 수명이 다할 때 타이머가 작동하여 이펙트는 비활성화되고, OnEffectDeactivateDelegate 델리게이트가 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;EffectSystem에서 생성한 이펙트 액터의 OnEffectDeactivateDelegate 델리게이트에는 OnEffectDeactivate 함수가 기본적으로 바인딩되어 있습니다. 또한, 동적으로 생성한 이펙트 액터의 경우 OnDynamicEffectDeacitvate 함수가 추가로 바인딩됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1714459191389&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.h

/**
 * 동적으로 생성한 NiagaraEffect 및 해당 NiagaraEffect를 제거할 때 사용하는 TimerHandle을 저장한 구조체입니다. 
 */
USTRUCT(Atomic, BlueprintType)
struct FPRDynamicNiagaraEffectPool
{
	GENERATED_BODY()

public:
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = &quot;DynamicNiagaraEffectPool&quot;)
	TMap&amp;lt;TObjectPtr&amp;lt;APRNiagaraEffect&amp;gt;, FTimerHandle&amp;gt; Effects;
};

private:
	/** 월드에 동적 Spawn한 NiagaraEffect의 Pool입니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = &quot;PREffectSystem|NiagaraEffect&quot;, meta = (AllowPrivateAccess = &quot;true&quot;))
	TMap&amp;lt;TObjectPtr&amp;lt;UNiagaraSystem&amp;gt;, FPRDynamicNiagaraEffectPool&amp;gt; DynamicNiagaraEffectPool;

private:
	/** 인자로 받은 NiagaraEffect가 비활성화될 때 실행하는 함수입니다. */
	UFUNCTION()
	void OnNiagaraEffectDeactivate(APREffect* Effect);

	/** 동적으로 생성한 NiagaraEffect가 비활성화될 때 실행하는 함수입니다. */
	UFUNCTION()
	void OnDynamicNiagaraEffectDeactivate(APREffect* Effect);

	/** 동적으로 생성한 NiagaraEffect를 제거하는 함수입니다. */
	UFUNCTION()
	void DynamicNiagaraEffectDestroy(APRNiagaraEffect* NiagaraEffect);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714459226676&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UPREffectSystemComponent.cpp

void UPREffectSystemComponent::OnNiagaraEffectDeactivate(APREffect* Effect)
{
	APRNiagaraEffect* NiagaraEffect = Cast&amp;lt;APRNiagaraEffect&amp;gt;(Effect);
	if(IsValid(NiagaraEffect) &amp;amp;&amp;amp; NiagaraEffect-&amp;gt;GetNiagaraEffect() != nullptr)
	{
		FPRActivateIndexList* ActivateIndexList = ActivateNiagaraEffectIndexList.Find(NiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
		if(ActivateIndexList != nullptr)
		{
			// 비활성화된 NiagaraEffect의 Index를 ActivateIndexList에서 제거합니다.
			ActivateIndexList-&amp;gt;Indexes.Remove(NiagaraEffect-&amp;gt;GetPoolIndex());
		}
	}
}

void UPREffectSystemComponent::OnDynamicNiagaraEffectDeactivate(APREffect* Effect)
{
	APRNiagaraEffect* NiagaraEffect = Cast&amp;lt;APRNiagaraEffect&amp;gt;(Effect);
	if(IsValid(NiagaraEffect))
	{
		if(DynamicLifespan &amp;gt; 0.0f)
		{
			// 타이머로 동적 수명이 끝난 후 DynamicNiagaraEffectDestroy 함수를 실행합니다.
			FTimerHandle DynamicEffectDestroyTimerHandle;
			FTimerDelegate DynamicEffectDestroyDelegate = FTimerDelegate::CreateUObject(this, &amp;amp;UPREffectSystemComponent::DynamicNiagaraEffectDestroy, NiagaraEffect);
			GetWorld()-&amp;gt;GetTimerManager().SetTimer(DynamicEffectDestroyTimerHandle, DynamicEffectDestroyDelegate, DynamicLifespan, false);

			// 타이머를 DynamicDestroyNiagaraEffectList에 저장합니다.
			FPRDynamicNiagaraEffectPool* DynamicPair = DynamicNiagaraEffectPool.Find(NiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
			if(DynamicPair != nullptr)
			{
				DynamicPair-&amp;gt;Effects.Emplace(NiagaraEffect, DynamicEffectDestroyTimerHandle);
			}
			else
			{
				FPRDynamicNiagaraEffectPool DynamicEffectPool;
				DynamicEffectPool.Effects.Emplace(NiagaraEffect, DynamicEffectDestroyTimerHandle);
				DynamicNiagaraEffectPool.Emplace(NiagaraEffect-&amp;gt;GetNiagaraEffectAsset(), DynamicEffectPool);
			}
		}
		else
		{
			// 동적 수명이 없을 경우 타이머를 실행하지 않고 바로 NiagaraEffect를 제거합니다.
			DynamicNiagaraEffectDestroy(NiagaraEffect);
		}
	}
}

void UPREffectSystemComponent::DynamicNiagaraEffectDestroy(APRNiagaraEffect* NiagaraEffect)
{
	// DynamicNiagaraEffectPool에서 해당 NiagaraEffect를 제거합니다.
	FPRDynamicNiagaraEffectPool* DynamicEffectPool = DynamicNiagaraEffectPool.Find(NiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
	if(DynamicEffectPool != nullptr)
	{
		DynamicEffectPool-&amp;gt;Effects.Remove(NiagaraEffect);
	}

	// UsedNiagaraEffectIndexList에서 해당 NiagaraEffect의 PoolIndex를 제거합니다.
	FPRUsedIndexList* UsedIndexList = UsedNiagaraEffectIndexList.Find(NiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
	if(UsedIndexList != nullptr)
	{
		UsedIndexList-&amp;gt;Indexes.Remove(NiagaraEffect-&amp;gt;GetPoolIndex());
	}

	// NiagaraEffectPool에서 해당 NiagaraEffect를 제거하고, 월드에서 제거합니다.
	FPRNiagaraEffectPool* EffectPool = NiagaraEffectPool.Find(NiagaraEffect-&amp;gt;GetNiagaraEffectAsset());
	if(EffectPool != nullptr)
	{
		EffectPool-&amp;gt;Effects.Remove(NiagaraEffect);
		NiagaraEffect-&amp;gt;Destroy();
	}
}&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;이펙트 액터가 비활성화되면, 델리게이트에 바인딩된 OnEffectDeactivate 함수가 실행되어 ActivateIndexList에서 비활성화된 이펙트 엑터의 Index를 제거합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;동적으로 생성된 이펙트 액터의 경우, OnEffectDeactivate 함수와 OnDynamicEffectDeactivate 함수가 실행됩니다. 동적으로 생성된 이펙트 액터를 비활성화된 후 DynamicLifespan 만큼의 타이머를 설정하여, 이펙트 액터를 Key 값으로 하고 타이머를 Value 값으로 하는 DynamicDestroyEffectList에 저장합니다. 타이머는 DynamicLifespan만큼 지나면 DynamicEffectDestroy 함수를 실행하여 동적으로 생성된 이펙트 액터를 월드에서 제거합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;정리하자면, 일반적인 이펙트 액터는 수명이 다하면 비활성화되어 월드에 존재하지만, 동적으로 생성한 이펙트 액터의 경우 수명이 다하면 비활성화되어 또 다른 동적 수명이 작동됩니다. 동적 수명이 다할 경우 월드에서 제거됩니다. 도중에 이펙트 액터가 활성화되면 동적 수명의 타이머를 제거하고, 다시 비활성화될 경우 동적 수명을 작동합니다.&lt;/p&gt;</description>
      <category>Project/Replica</category>
      <category>Project Replica</category>
      <category>Unreal Engine 5</category>
      <author>한돌이</author>
      <guid isPermaLink="true">https://lykanstudio.tistory.com/109</guid>
      <comments>https://lykanstudio.tistory.com/109#entry109comment</comments>
      <pubDate>Tue, 30 Apr 2024 16:19:46 +0900</pubDate>
    </item>
  </channel>
</rss>