<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>game.destroy()</title>
    <link>https://developer-destroy.tistory.com/</link>
    <description>game-destroy 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 3 Sep 2026 22:31:25 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>destroyer</managingEditor>
    <item>
      <title>[UE5] 캐릭터 메시에 물리 적용하기 (Control Physics 활용)</title>
      <link>https://developer-destroy.tistory.com/5</link>
      <description>&lt;h1&gt;적용 결과&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;최종 결과.gif&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4FplA/btsKxunB4qO/mKIS2AvW7Mdy3uSko0AZJK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4FplA/btsKxunB4qO/mKIS2AvW7Mdy3uSko0AZJK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4FplA/btsKxunB4qO/mKIS2AvW7Mdy3uSko0AZJK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/4FplA/btsKxunB4qO/mKIS2AvW7Mdy3uSko0AZJK/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;1026&quot; height=&quot;626&quot; data-filename=&quot;최종 결과.gif&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;Physic Control&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Physics Contro&lt;/b&gt;l은 Unreal Engine 5에서 새롭게 도입된 기능으로, 플레이어의 스켈레탈 메시에 &lt;b&gt;물리 효과를 적용&lt;/b&gt;할 수 있다. 이 기능은 애니메이션과 물리를 조화롭게 융합하여 &lt;b&gt;자연스러운 물리적 현상을 연출&lt;/b&gt;하는 데 중점을 두고 있다. 특히, Physics Control은 캐릭터가 피격을 당하거나 충돌하는 상황에서 현실감 넘치는 물리 반응을 구현하는 강력한 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능을 사용하면 애니메이션만으로는 연출하기 어려운 &lt;b&gt;다양한 물리적 반응을 시뮬레이션&lt;/b&gt;할 수 있어, 플레이어가 공격을 받았을 때 캐릭터의 움직임이 실제 세계에서의 반응처럼 자연스럽고 다이내믹하게 보이도록 만든다.&lt;/p&gt;
&lt;h1&gt;Physic Control 플러그인 설치&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1313&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOTyXA/btsKvDNpW96/oPyTWMtUoQkh43WeHBNDFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOTyXA/btsKvDNpW96/oPyTWMtUoQkh43WeHBNDFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOTyXA/btsKvDNpW96/oPyTWMtUoQkh43WeHBNDFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOTyXA%2FbtsKvDNpW96%2FoPyTWMtUoQkh43WeHBNDFk%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;1313&quot; height=&quot;688&quot; data-origin-width=&quot;1313&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;일단 플레이어에 추가해보기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Build.cs&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;PublicDependencyModuleNames.AddRange(new string[] { &quot;Core&quot;, &quot;CoreUObject&quot;, &quot;Engine&quot;, &quot;InputCore&quot;, &quot;EnhancedInput&quot;, &quot;AIModule&quot;, &quot;NavigationSystem&quot;, &quot;UMG&quot;,
    &quot;Niagara&quot;, &quot;CableComponent&quot;, &quot;MotionWarping&quot;, &quot;PhysicsControl&quot;});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;M4_BasePlayer&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// M4_BasePlayer.h
    UPROPERTY(EditAnywhere, BlueprintReadWrite,Category = &quot;Components&quot;)
    class UPhysicsControlComponent* PhysicsControlComp;

// M4_BasePlayer.cpp
AM4_BasePlayer::AM4_BasePlayer()
{
     // Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    // ...

    PhysicsControlComp = CreateDefaultSubobject&amp;lt;UPhysicsControlComponent&amp;gt;(TEXT(&quot;PhysicsControlComp&quot;));
    PhysicsControlComp-&amp;gt;SetupAttachment(RootComponent);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Build.cs에 내용을 &lt;b&gt;Physics Control&lt;/b&gt;을 추가하고, 플레이어에 &lt;b&gt;UPhysicsComponent&lt;/b&gt;를 생성해준다.&lt;/p&gt;
&lt;h1&gt;Physics Control Component&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rFl4i/btsKvXdJGfW/TPyPWKjPENkGjYmkgLaac1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rFl4i/btsKvXdJGfW/TPyPWKjPENkGjYmkgLaac1/img.png&quot; data-alt=&quot;블루프린트로 본 Controls Physics Component&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rFl4i/btsKvXdJGfW/TPyPWKjPENkGjYmkgLaac1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrFl4i%2FbtsKvXdJGfW%2FTPyPWKjPENkGjYmkgLaac1%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;461&quot; height=&quot;712&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;블루프린트로 본 Controls Physics Component&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Detail 창에서는 주요 설정을 바로 조정할 수 있는 옵션이 거의 없다. &lt;b&gt;Physics Control Profile Asset&lt;/b&gt;을 설정하거나, 런타임 중에 직접 &lt;b&gt;노드를 활용하거나 코드를 작성&lt;/b&gt;해 Physics Control을 생성하는 방식으로 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 이 창에서는 디버그 시각화, 텔레포트 거리 임계값, 시각화 스케일과 같은 몇 가지 디버그 관련 옵션을 설정할 수 있다. 디버그 시각화를 활성화하면 물리 제어의 효과를 시각적으로 확인할 수 있어, 복잡한 물리 동작을 조정할 때 유용하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;691&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K8HDe/btsKvDs967l/7gPN2xe2WF5wEPQy5nmY4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K8HDe/btsKvDs967l/7gPN2xe2WF5wEPQy5nmY4k/img.png&quot; data-alt=&quot;Physics Control Profile Asset&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K8HDe/btsKvDs967l/7gPN2xe2WF5wEPQy5nmY4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK8HDe%2FbtsKvDs967l%2F7gPN2xe2WF5wEPQy5nmY4k%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;446&quot; height=&quot;691&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;691&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Physics Control Profile Asset&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Physics Control Profile Asset&lt;/b&gt;은 물리 제어에 필요한 사전 설정을 담은 에셋으로, 이를 활용하면 일관성 있는 물리 제어를 손쉽게 적용할 수 있다. 이 에셋을 활용하면 캐릭터의 &lt;b&gt;관절별로 필요한 물리적 제어를 설정&lt;/b&gt;할 수 있다. &lt;b&gt;Limb Setup Data&lt;/b&gt;나 &lt;b&gt;Modifier&lt;/b&gt;와 같은 세부 항목을 통해 각 부위별 물리 반응이나 물리 제어 강도를 조절할 수 있어, 더욱 세밀하고 정교한 제어가 가능하다.&lt;/p&gt;
&lt;h1&gt;Physics Control Profile Asset Base 만들기&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWNt2X/btsKxiViEi5/hwmX73ZR5Fa6dRVeSrTZM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWNt2X/btsKxiViEi5/hwmX73ZR5Fa6dRVeSrTZM0/img.png&quot; data-alt=&quot;LimbSetupData 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWNt2X/btsKxiViEi5/hwmX73ZR5Fa6dRVeSrTZM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWNt2X%2FbtsKxiViEi5%2FhwmX73ZR5Fa6dRVeSrTZM0%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;427&quot; height=&quot;274&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LimbSetupData 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LimbSetupData에 엘리먼트를 추가하고, Limb 이름과 그에 매칭되는 Bone 이름을 설정해준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Include Parent Bone&lt;/b&gt;: 부모 뼈를 제어 설정에 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Create World Space Controls&lt;/b&gt;: 월드 스페이스에서 동작하는 컨트롤 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Create Parent Space Controls&lt;/b&gt;: 부모의 공간에서 동작하는 컨트롤 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Create Body Modifiers&lt;/b&gt;: 캐릭터의 신체 구조에 대한 추가적인 수정이나 제어 옵션 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCUbuk/btsKuWmm4Hy/HqU67u6R10EeBfSlWXMNl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCUbuk/btsKuWmm4Hy/HqU67u6R10EeBfSlWXMNl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCUbuk/btsKuWmm4Hy/HqU67u6R10EeBfSlWXMNl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCUbuk%2FbtsKuWmm4Hy%2FHqU67u6R10EeBfSlWXMNl1%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;424&quot; height=&quot;82&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dFWrki/btsKwk7Hczm/ytzUBQwxKd6KVHHfyz6EAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dFWrki/btsKwk7Hczm/ytzUBQwxKd6KVHHfyz6EAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dFWrki/btsKwk7Hczm/ytzUBQwxKd6KVHHfyz6EAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdFWrki%2FbtsKwk7Hczm%2FytzUBQwxKd6KVHHfyz6EAk%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;424&quot; height=&quot;85&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;430&quot; data-origin-height=&quot;77&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJqVhP/btsKwmKZ6gh/KvhXLYKbVuOP98ULTIER50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJqVhP/btsKwmKZ6gh/KvhXLYKbVuOP98ULTIER50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJqVhP/btsKwmKZ6gh/KvhXLYKbVuOP98ULTIER50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJqVhP%2FbtsKwmKZ6gh%2FKvhXLYKbVuOP98ULTIER50%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;430&quot; height=&quot;77&quot; data-origin-width=&quot;430&quot; data-origin-height=&quot;77&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팔, 머리, 다리, 허리까지 LimbData를 세팅&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;491&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GkdtV/btsKwDr4dCp/kKVfR9UCkZRZ45WVvlQIr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GkdtV/btsKwDr4dCp/kKVfR9UCkZRZ45WVvlQIr1/img.png&quot; data-alt=&quot;기본 World, Parent Space Control Data 비활성화&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GkdtV/btsKwDr4dCp/kKVfR9UCkZRZ45WVvlQIr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGkdtV%2FbtsKwDr4dCp%2FkKVfR9UCkZRZ45WVvlQIr1%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;424&quot; height=&quot;491&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;491&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기본 World, Parent Space Control Data 비활성화&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 에셋은 Base Asset으로, 추후 만들 Player용 에셋에서 활성화 시켜주기 위해 부모 단에서는 비활성화 시켜주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;428&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mb3bf/btsKwjARnrB/pYXQJFdk3kkYkv8grwD49k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mb3bf/btsKwjARnrB/pYXQJFdk3kkYkv8grwD49k/img.png&quot; data-alt=&quot;Modifier Set&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mb3bf/btsKwjARnrB/pYXQJFdk3kkYkv8grwD49k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmb3bf%2FbtsKwjARnrB%2FpYXQJFdk3kkYkv8grwD49k%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;428&quot; height=&quot;361&quot; data-origin-width=&quot;428&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Modifier Set&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리를 적용할 부위를 그룹으로 모을 수 있게 설정하는 부분이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;431&quot; data-origin-height=&quot;381&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kEFSF/btsKvZCFsSd/519GfPmknyrlaaJVQOsmL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kEFSF/btsKvZCFsSd/519GfPmknyrlaaJVQOsmL0/img.png&quot; data-alt=&quot;Kinematic Profile 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kEFSF/btsKvZCFsSd/519GfPmknyrlaaJVQOsmL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkEFSF%2FbtsKvZCFsSd%2F519GfPmknyrlaaJVQOsmL0%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;431&quot; height=&quot;381&quot; data-origin-width=&quot;431&quot; data-origin-height=&quot;381&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kinematic Profile 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 물리를 적용받지 않고 애니메이션으로 동작할 Kinematic Profile을 만들었다.&lt;/p&gt;
&lt;h1&gt;Physics Control Profile Asset Player 만들기&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;175&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QCk81/btsKwQriZrg/kwRcwixv7rdLDVspM81v31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QCk81/btsKwQriZrg/kwRcwixv7rdLDVspM81v31/img.png&quot; data-alt=&quot;부모를 Base Asset으로 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QCk81/btsKwQriZrg/kwRcwixv7rdLDVspM81v31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQCk81%2FbtsKwQriZrg%2FkwRcwixv7rdLDVspM81v31%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;433&quot; height=&quot;175&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;175&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;부모를 Base Asset으로 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxyST1/btsKvLxURPr/LaCdYfTuCTsQxqkuXQZv7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxyST1/btsKvLxURPr/LaCdYfTuCTsQxqkuXQZv7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxyST1/btsKvLxURPr/LaCdYfTuCTsQxqkuXQZv7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxyST1%2FbtsKvLxURPr%2FLaCdYfTuCTsQxqkuXQZv7k%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;834&quot; height=&quot;624&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 프로필을 생성한다. BouncyWalk으로, 컨텐츠 예제의 BouncyWalk의 설정을 가져와 적용시켜주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;Linear Strength&lt;/b&gt;를 2로 설정하여 특정 방향으로 힘을 받을 때 반발력을 약간 높여주고, &lt;b&gt;Angular Strength&lt;/b&gt;는 1로 설정하여 회전하는 힘에는 반발력을 기본 수준으로 주도록 했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BouncyWalk 프로파일 주요 매개 변수 설명&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개 변수설명&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;b&gt;Linear Strength&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;선형 힘의 강도 설정. 높을 수록 애니메이션에 더 중점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Angular Strength&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;회전에 적용되는 힘의 강도를 설정. 값이 높을수록 애니메이션에 더 중점.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Custom Control Point&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;제어 지점을 커스텀 위치로 설정. (X, Y, Z 좌표)로 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Enabled&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;이 설정을 활성화할지 여부를 선택. 활성화 시 체크&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Use Custom Control Point&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;커스텀 제어 지점을 사용할지 선택. 선택하지 않으면 기본 제어 지점이 사용됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Use Skeletal Animation&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;스켈레탈 애니메이션을 사용할지 여부를 선택. 활성화 시 체크&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Disable Collision&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;충돌을 비활성화할지 여부를 선택. 체크 시 충돌 무시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Only Control Child Object&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;자식 객체만 제어할지 여부를 선택. 체크 시 부모 객체에 영향을 주지 않음.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYmrYg/btsKxpz9ICu/cmZZSvZnkLDQMOdWiI9Vr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYmrYg/btsKxpz9ICu/cmZZSvZnkLDQMOdWiI9Vr0/img.png&quot; data-alt=&quot;Modifier Updates 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYmrYg/btsKxpz9ICu/cmZZSvZnkLDQMOdWiI9Vr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYmrYg%2FbtsKxpz9ICu%2FcmZZSvZnkLDQMOdWiI9Vr0%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;776&quot; height=&quot;515&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Modifier Updates 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Limb에 대해 &lt;b&gt;Simulated&lt;/b&gt;를, &lt;b&gt;Feet Set&lt;/b&gt;에 대해서는 &lt;b&gt;Kinematic&lt;/b&gt;으로 설정해 두도록 &lt;b&gt;Modifier&lt;/b&gt;를 추가해주었다.&lt;/p&gt;
&lt;h1&gt;플레이어에 적용해보기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 다양한 프로필을 설정해두고, 특정 시점마다 Profile을 변경시켜 Control Physics적용을 변화시킬 수 있다. 여기서는 블루프린트를 이용해 간단하게 키 입력으로 테스트해 보겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/erwLHe/btsKvADqGNm/ey8H4RWwgpVxcnGScF18WK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/erwLHe/btsKvADqGNm/ey8H4RWwgpVxcnGScF18WK/img.png&quot; data-alt=&quot;Physics Control Comp에 피직스 컨트롤 프로필 에셋을 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/erwLHe/btsKvADqGNm/ey8H4RWwgpVxcnGScF18WK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FerwLHe%2FbtsKvADqGNm%2Fey8H4RWwgpVxcnGScF18WK%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;457&quot; height=&quot;310&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Physics Control Comp에 피직스 컨트롤 프로필 에셋을 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brP4EL/btsKxOsWMpU/IMpr7mUlJjBPk8CXHkXpn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brP4EL/btsKxOsWMpU/IMpr7mUlJjBPk8CXHkXpn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brP4EL/btsKxOsWMpU/IMpr7mUlJjBPk8CXHkXpn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrP4EL%2FbtsKxOsWMpU%2FIMpr7mUlJjBPk8CXHkXpn0%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;978&quot; height=&quot;279&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BeginPlay에서 &lt;b&gt;PhysicControl&lt;/b&gt;을 &lt;b&gt;Control Profile Asset&lt;/b&gt;을 이용하여 생성하는 노드와, &lt;b&gt;ControlProfile&lt;/b&gt;을 &lt;b&gt;BouncyWalk&lt;/b&gt;로 설정하는 노드를 추가.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;483&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mgSQ0/btsKwwUjvGg/2JGKxoyKKxIQlJLgikbWXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mgSQ0/btsKwwUjvGg/2JGKxoyKKxIQlJLgikbWXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mgSQ0/btsKwwUjvGg/2JGKxoyKKxIQlJLgikbWXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmgSQ0%2FbtsKwwUjvGg%2F2JGKxoyKKxIQlJLgikbWXK%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;724&quot; height=&quot;483&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;483&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T Key와 U Key에 각각 Kinematic과 BouncyWalk로 피직스 컨트롤 프로필을 변경시켜주는 노드를 연결해 둔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;BouncyWalkVSKinematic.gif&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brplpF/btsKw2r4CvF/yyhcfstiacF4CXHPhgi8ik/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brplpF/btsKw2r4CvF/yyhcfstiacF4CXHPhgi8ik/img.gif&quot; data-alt=&quot;BouncyWalk상태와 Kinematic 상태 비교&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brplpF/btsKw2r4CvF/yyhcfstiacF4CXHPhgi8ik/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/brplpF/btsKw2r4CvF/yyhcfstiacF4CXHPhgi8ik/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;1106&quot; height=&quot;615&quot; data-filename=&quot;BouncyWalkVSKinematic.gif&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;615&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;BouncyWalk상태와 Kinematic 상태 비교&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 상태가 충돌에 반응하는 방식의 차이를 선명하게 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BouncyWalk&lt;/b&gt; 상태에서는 충돌이 발생할 때마다 캐릭터의 메쉬가 &lt;b&gt;유연하게 흔들리며&lt;/b&gt; 생동감 넘치는 반응을 연출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, &lt;b&gt;Kinematic&lt;/b&gt; 상태에서는 캐릭터 메쉬가 &lt;b&gt;단단히 고정된 듯&lt;/b&gt; 움직인다. 충돌 시 캐릭터는 외부 힘에 의해 물리적 위치가 밀리긴 하지만, 자체적인 흔들림이나 탄성 있는 움직임은 없다.&lt;/p&gt;
&lt;h1&gt;C++ 코드로 사용&lt;/h1&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// M4_BasePlayer.h

// Physics Control Profile 이름 저장
FName Bounce = TEXT(&quot;BouncyWalk&quot;);
FName Kinematic = TEXT(&quot;Kinematic&quot;);
FName Soft = TEXT(&quot;SoftWalk&quot;);

// M4_BasePlayer.cpp
void AM4_BasePlayer::BeginPlay()
{
    Super::BeginPlay();

    // ...

    if (PhysicsControlComp)
    {
        // Control 생성
        PhysicsControlComp-&amp;gt;CreateControlsAndBodyModifiersFromControlProfileAsset(GetMesh(),nullptr, FName());
        // Control 프로필 설정 (기본을 키네마틱으로)
        PhysicsControlComp-&amp;gt;InvokeControlProfile(Kinematic);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 기본 설정으로 캐릭터는 &lt;b&gt;Kinematic&lt;/b&gt; 상태로 시작하여, 움직임이 고정된 상태에서 플레이를 진행하게 된다. 그러나 전투 중 피격, 추락 등 특별한 상황이 발생하면 프로필을 &lt;b&gt;BouncyWalk&lt;/b&gt; 또는 &lt;b&gt;SoftWalk&lt;/b&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;예를 들면, 피격 시에는 &lt;b&gt;BouncyWalk&lt;/b&gt; 프로필을 활성화하여 충격에 의해 몸이 크게 흔들리도록 하여 &lt;b&gt;긴장감 넘치는 전투 느낌을 살리고&lt;/b&gt;, 추락 시에는 &lt;b&gt;SoftWalk&lt;/b&gt; 상태로 부드러운 물리 반응을 추가하여 &lt;b&gt;착지의 충격을 표현&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;h1&gt;참고 자료&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Physic Control을 사용하는 예제 영상&lt;/li&gt;
&lt;li&gt;UE5 샘플 컨텐츠들을 모아놓은 프로젝트 컨텐츠 예제&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;783&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tYGRr/btsKvLEvNRq/MoW4gAT7rHXhlJdqFzEKGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tYGRr/btsKvLEvNRq/MoW4gAT7rHXhlJdqFzEKGk/img.png&quot; data-alt=&quot;컨텐츠 예제 다운로드 페이지 ( Fab )&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tYGRr/btsKvLEvNRq/MoW4gAT7rHXhlJdqFzEKGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtYGRr%2FbtsKvLEvNRq%2FMoW4gAT7rHXhlJdqFzEKGk%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;1550&quot; height=&quot;783&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;783&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컨텐츠 예제 다운로드 페이지 ( Fab )&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#&lt;/p&gt;</description>
      <category>JIN</category>
      <author>destroyer</author>
      <guid isPermaLink="true">https://developer-destroy.tistory.com/5</guid>
      <comments>https://developer-destroy.tistory.com/5#entry5comment</comments>
      <pubDate>Tue, 5 Nov 2024 10:29:19 +0900</pubDate>
    </item>
    <item>
      <title>[UE5] 인벤토리 시스템 - 인벤토리 UI</title>
      <link>https://developer-destroy.tistory.com/4</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프로젝트 개요&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여러 명이 하나의&lt;span style=&quot;color: #ee2323;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000; background-color: #f3c000;&quot;&gt;&lt;b&gt;스포츠 영상을 시청&lt;/b&gt;&lt;/span&gt;하면서 특정 대상(스포츠 팀 / 선수)을 &lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;&lt;b&gt;응원&lt;/b&gt;&lt;/span&gt;하는 플랫폼을 제공하는 메타버스 컨텐츠를 제작하는 프로젝트&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;인벤토리의 필요성&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스포츠 경기를 관람할 때 다이나믹한 응원을 위해 폭죽을 터트리고 응원봉을 흔들듯 여러 아이템이 있으면&lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;&lt;b&gt; 영상을 시청하는데 재미 요소&lt;/b&gt;&lt;/span&gt;를 더할 수 있을 것이라 판단했고, 여러 아이템을 자신이 가진 포인트로 구매하고 구&lt;span style=&quot;color: #000000;&quot;&gt;매한&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;아이템&lt;/span&gt; 소지하는 기능이 필요&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;하다 판단하여 인벤토리를 구현하게 되었다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;구현 개요&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인벤토리 UI는 플레이어가 갖고 있는 InventoryComponent를 참조해 &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;InventoryComponent의 Invetory 배열이 변경될 때마다 &lt;/span&gt;UI를 갱신한다. 키보드 입력 I를 통해 인벤토리 UI를 팝업시킬 수 있다. 인벤토리 UI를 통해 아이템을 사용 및 장착하고 인벤토리에서 아이템을 버리는 기능을 사용할 수 있다. 간단한 Inventory이기도 하고 Drag&amp;amp; Drop이 필수 기능은 아니라 생각해 넣지 않았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인벤토리 UI 구현&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 동영상 - Clipchamp로 제작 (3).gif&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOOoqk/btsKwtbVsGN/X2IEOhtwNvN3llc0XSTSCk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOOoqk/btsKwtbVsGN/X2IEOhtwNvN3llc0XSTSCk/img.gif&quot; data-alt=&quot;인벤토리 UI&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOOoqk/btsKwtbVsGN/X2IEOhtwNvN3llc0XSTSCk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bOOoqk/btsKwtbVsGN/X2IEOhtwNvN3llc0XSTSCk/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;426&quot; height=&quot;240&quot; data-filename=&quot;제목 없는 동영상 - Clipchamp로 제작 (3).gif&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인벤토리 UI&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 동영상 - Clipchamp로 제작.gif&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wKaAP/btsKvioXHa3/QfIh4aMEkAJfwWVvL8YDwK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wKaAP/btsKvioXHa3/QfIh4aMEkAJfwWVvL8YDwK/img.gif&quot; data-alt=&quot;인벤토리에서 사용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wKaAP/btsKvioXHa3/QfIh4aMEkAJfwWVvL8YDwK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/wKaAP/btsKvioXHa3/QfIh4aMEkAJfwWVvL8YDwK/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;426&quot; height=&quot;240&quot; data-filename=&quot;제목 없는 동영상 - Clipchamp로 제작.gif&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인벤토리에서 사용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 동영상 - Clipchamp로 제작 (1).gif&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eJXuIX/btsKwZ9chQz/Li73lHOMDBkk2A0JjVqCwK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eJXuIX/btsKwZ9chQz/Li73lHOMDBkk2A0JjVqCwK/img.gif&quot; data-alt=&quot;아이템 구매&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eJXuIX/btsKwZ9chQz/Li73lHOMDBkk2A0JjVqCwK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/eJXuIX/btsKwZ9chQz/Li73lHOMDBkk2A0JjVqCwK/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;426&quot; height=&quot;240&quot; data-filename=&quot;제목 없는 동영상 - Clipchamp로 제작 (1).gif&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아이템 구매&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget 생성자 (NativeContruct)&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730439675728&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UInventoryWidget::NativeConstruct()
{
	if (!Btn_ThrowAway-&amp;gt;OnClicked.IsBound())
	{
		Btn_ThrowAway-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;UInventoryWidget::ThrowAwaySelectedItem);
	}
	if (!Btn_Use-&amp;gt;OnClicked.IsBound())
	{
		Btn_Use-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;UInventoryWidget::UseItem);
	}
	if (!Btn_CostumeCategory-&amp;gt;OnClicked.IsBound())
	{
		Btn_CostumeCategory-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;UInventoryWidget::SelectCategory_Costume);
	}
	if (!Btn_ActiveCategory-&amp;gt;OnClicked.IsBound())
	{
		Btn_ActiveCategory-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;UInventoryWidget::SelectCategory_Active);
	}
	if (!Btn_EmotionCategory-&amp;gt;OnClicked.IsBound())
	{
		Btn_EmotionCategory-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;UInventoryWidget::SelectCategory_Emotion);
	}	
	if (!Btn_EmojiCategory-&amp;gt;OnClicked.IsBound())
	{
		Btn_EmojiCategory-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;UInventoryWidget::SelectCategory_Emoji);
	}
	if (!Btn_SoundCategory-&amp;gt;OnClicked.IsBound())
	{
		Btn_SoundCategory-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;UInventoryWidget::SelectCategory_Sound);
	}

	GI = Cast&amp;lt;UHG_GameInstance&amp;gt;(GetWorld()-&amp;gt;GetGameInstance());
	SelectedCategory = WB_SlotList_Active;
}&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;&lt;b&gt;※Constructor에서 AddDynamic으로 바인딩을 한 번 하고 다음에 또 바딩을 시도하면 오류 발생하므로 이벤트의 IsBound() 함수를 통해 이벤트가 바운딩 됐는지를 검사해야함. 충돌이 나거나 엔진이 터지진 않지만 UI에서의 기능들이 이상해진다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2467&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9kpn4/btsKtVMtdho/b9I3UVfNavv8lzyxVHrFd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9kpn4/btsKtVMtdho/b9I3UVfNavv8lzyxVHrFd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9kpn4/btsKtVMtdho/b9I3UVfNavv8lzyxVHrFd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9kpn4%2FbtsKtVMtdho%2Fb9I3UVfNavv8lzyxVHrFd0%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;2467&quot; height=&quot;456&quot; data-origin-width=&quot;2467&quot; data-origin-height=&quot;456&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인벤토리UI 초기화 함수 (InitInventoryUI())&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730440113176&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UInventoryWidget::InitInventoryUI()
{
	// 모든 WrapBox의 자식들을 없애준다. (해주지 않으면 아이템이 복사됨)
	WB_SlotList_Active-&amp;gt;ClearChildren();
	WB_SlotList_Costume-&amp;gt;ClearChildren();
	WB_SlotList_Emotion-&amp;gt;ClearChildren();
	WB_SlotList_Emoji-&amp;gt;ClearChildren();
	WB_SlotList_Sound-&amp;gt;ClearChildren();
	// 선택 슬롯도 초기화 (UX 적으로 인벤토리를 껐다 켰을 때 선택된 슬롯도 없는 것이 맞다고 판단)
	SelectedSlot = nullptr;
	DIsplaySelectedItemInfo();
	// 현재 이 인벤토리 위젯이 어떤 Pawn 에게 소유 되었는지 검사
	if (this-&amp;gt;GetOwningPlayer() != nullptr)
	{
		// 소유하고 있는 Pawn 이 있다면
		// 해당 Pawn 을 Player 로 Cast 한다.
		auto* OwningPlayer = Cast&amp;lt;AHG_Player&amp;gt;(this-&amp;gt;GetOwningPlayer()-&amp;gt;GetPawn());
		if (OwningPlayer)
		{
			// Cast 에 성공했다면
			// 해당 Player 가 가진 Inventory 의 모든 요소를 접근 
			for (auto slot : OwningPlayer-&amp;gt;InventoryComp-&amp;gt;Inventory)
			{
				// 각 슬롯(요소)별로 위젯을 만듦
				UHG_SlotWidget* SlotWidget = CreateWidget&amp;lt;UHG_SlotWidget&amp;gt;(this, SlotWidgetFactory);
				if (SlotWidget)
				{
					// 만드는데 성공했다면
					
					// 슬롯 위젯을 현재 접근 중인 인벤토리 슬롯의 데이터로 초기화해준다.
					SlotWidget-&amp;gt;InitSlot(slot);
					SlotWidget-&amp;gt;SetItemIcon();

					// 슬롯 위젯의 주인을 설정해준다. (인벤토리 UI에서 상대적인 자기 자신에 대한 정보를 알아야하기 때문)
					SlotWidget-&amp;gt;SetOwner(this);

					// 현재 접근하고 있는 슬롯의 카테고리에 맞는 WrapBox에 넣어주기 위한 EItemCategory 검사
					if (slot.ItemInfo.ItemCategory == EItemCategory::Category_Active)
					{
						WB_SlotList_Active-&amp;gt;AddChildToWrapBox(SlotWidget);
					}
					else if (slot.ItemInfo.ItemCategory == EItemCategory::Category_Emotion)
					{
						WB_SlotList_Emotion-&amp;gt;AddChildToWrapBox(SlotWidget);
					}
					else if (slot.ItemInfo.ItemCategory == EItemCategory::Category_Emoji)
					{
						WB_SlotList_Emoji-&amp;gt;AddChildToWrapBox(SlotWidget);
					}
					else if (slot.ItemInfo.ItemCategory == EItemCategory::Category_Sound)
					{
						WB_SlotList_Sound-&amp;gt;AddChildToWrapBox(SlotWidget);
					}
					else
					{
						WB_SlotList_Costume-&amp;gt;AddChildToWrapBox(SlotWidget);
						CheckEquipitem();
 						for (auto EquipSlot : EquipList)
 						{
							// 갖고있는 ItemData의 ItemName으로 현재 생성한 SlotWidget가 EquipList에 요소로 존재했었는지 검사
 							if (EquipSlot-&amp;gt;SlotInfo.ItemInfo.ItemName == SlotWidget-&amp;gt;SlotInfo.ItemInfo.ItemName)
 							{
								// EquipList에 존재했었다면
								
								// 아이템이 현재 장착되었는지 보여주기 위한 표시
								// HitTestInvisible =&amp;gt; 클릭 등의 입력을 비활성화 하는 옵션, 그냥 Visible로 하면 Slot에 있는 Button 의 입력을 Img_Equip 이 가져감
 								SlotWidget-&amp;gt;Img_Equip-&amp;gt;SetVisibility(ESlateVisibility::HitTestInvisible);

								// EquipList에서 기존에 장착했던 아이템을 다시 생성한 SlotWidget로 갱신해줌
 								EquipList[EquipList.Find(EquipSlot)] = SlotWidget;
 							}
 						}
					}
				}
			}
		}
	}
	// 인벤토리를 팝업하자마자 보이는 WrapBox 는 Active 로 설정
	SelectedCategory = WB_SlotList_Active;
	WS_Category-&amp;gt;SetActiveWidgetIndex(0);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아이템 버리기 (ThrowAwaySelectedItem())&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730450999079&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 아이템 버리기
void UInventoryWidget::ThrowAwaySelectedItem()
{
	if (SelectedSlot)
	{
		// 위젯의 주인 찾기  
		auto* Owner = Cast&amp;lt;AHG_Player&amp;gt;(this-&amp;gt;GetOwningPlayer()-&amp;gt;GetPawn());

		// 선택된 슬롯이 장착된 아이템 슬롯 리스트에 있는지 확인
		if (EquipList.Contains(SelectedSlot))
		{
			// 버튼의 텍스트를 장착하기로 바꿔줌
			TB_Use-&amp;gt;SetText(FText::FromString(TEXT(&quot;장착하기&quot;)));

			// 선택된 슬롯의 장착 이미지를 보이지 않게 함
			SelectedSlot-&amp;gt;Img_Equip-&amp;gt;SetVisibility(ESlateVisibility::Hidden);
			
			// 장착된 아이템 슬롯 리스트에서 선택된 슬롯 제거
			EquipList.Remove(SelectedSlot);
			
			// 인벤토리 위젯의 주인이 장착하고 있는 아이템을 장착 해제
			Owner-&amp;gt;UnequipItemToSocket(SelectedSlot-&amp;gt;SlotInfo.ItemInfo.ItemName);
		}
		// 현재 선택된 Category WrapBox의 자식 개수를 가져옴
		int32 ChildCount = SelectedCategory-&amp;gt;GetChildrenCount();
		// 자식의 수만큼 반복함
		for (int32 i = 0; i &amp;lt; ChildCount; i++)
		{
			// SelectedCategory 에서 i 번째 자식을  가져와서 UHG_SlotWidget 으로 Casting 
			UHG_SlotWidget* Child = Cast&amp;lt;UHG_SlotWidget&amp;gt;(SelectedCategory-&amp;gt;GetChildAt(i));

			// 선택한 슬롯과 SelectedCategory의 i 번째 자식이 같은지 확인
			if (Child == SelectedSlot)
			{
				// 같다면

				// NullCheck
				if (Owner)
				{
					// Owner의 인벤토리에서 선택된 슬롯의 데이터와 같은 아이템을 제거
					Owner-&amp;gt;InventoryComp-&amp;gt;RemoveFromInventory(SelectedSlot-&amp;gt;SlotInfo.ItemInfo, 1);
				}
				// 슬롯의 Quantity를 하나 줄임
				SelectedSlot-&amp;gt;SlotInfo.Quantity--;

				// 슬롯의 Quantity가 0이 되면
				if (SelectedSlot-&amp;gt;SlotInfo.Quantity == 0)
				{
					// 선택된 카테고리의 i번째 자식을 없애고
					SelectedCategory-&amp;gt;RemoveChildAt(i);
					// SelectedSlot을 비워줌
					SelectedSlot = nullptr;
				}
				// 현재의 슬롯 정보를 갱신
				DIsplaySelectedItemInfo();

				break;
			}

		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아이템 사용하기 (UseItem)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730451566194&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 아이템 사용하기
void UInventoryWidget::UseItem()
{
	// NullCheck
	if (SelectedSlot)
	{
		// NullCheck
		if (this-&amp;gt;GetOwningPlayer() != nullptr)
		{
			// 위젯의 주인 찾기  
			auto* OwningPlayer = Cast&amp;lt;AHG_Player&amp;gt;(this-&amp;gt;GetOwningPlayer()-&amp;gt;GetPawn());
			// NullCheck
			if (nullptr != OwningPlayer)
			{
				// 선택된 슬롯이 무엇인지 확인
				if (SelectedCategory == WB_SlotList_Active)
				{
					// Active에서 아이템을 사용했다면
					
					// 아이템을 월드에 스폰 시키고 사용하는 AHG_Player의 SpawnItem 함수 사용
					OwningPlayer-&amp;gt;SpawnItem(SelectedSlot-&amp;gt;SlotInfo.ItemInfo);

					// 아이템 1개 감소
					ThrowAwaySelectedItem();
				}
				else if (SelectedCategory == WB_SlotList_Emoji || SelectedCategory == WB_SlotList_Sound)
				{
					// 이모티콘 혹은 효과음 아이템을 사용했다면

					// 아이템을 월드에 스폰 시키고 사용하는 AHG_Player의 SpawnItem 함수 사용
					OwningPlayer-&amp;gt;SpawnItem(SelectedSlot-&amp;gt;SlotInfo.ItemInfo);
				}
				else if (SelectedCategory == WB_SlotList_Costume)
				{
					// GameInstance의 EquipSlotIndexList에 내가 선택한 슬롯의 아이템의 인덱스가 포함되어 있는지 확인 ==&amp;gt; 장착한 아이템인지 확인
					if (GI-&amp;gt;EquipSlotIndexList.Contains(SelectedSlot-&amp;gt;MyIndex))
					{
						// 장착한 아이템이라면

						// 사용하는 버튼의 텍스트를 해제하기에서 장착하기로 변경
						TB_Use-&amp;gt;SetText(FText::FromString(TEXT(&quot;장착하기&quot;)));

						// 선택한 슬롯의 장착 표시 이미지를 보이지 않게 함
						SelectedSlot-&amp;gt;Img_Equip-&amp;gt;SetVisibility(ESlateVisibility::Hidden);

						// EquipList에서 선택한 슬롯을 제거
						EquipList.Remove(SelectedSlot);

						// GameInstance의 EquipSlotIndexList에서 현재 슬롯의 인덱스 제거
						GI-&amp;gt;EquipSlotIndexList.Remove(GetSlotIndexInWB(SelectedSlot));

						// 플레이어 아이템 장착 해제
						OwningPlayer-&amp;gt;UnequipItemToSocket(SelectedSlot-&amp;gt;SlotInfo.ItemInfo.ItemName);
					}
					else
					{
						// 장착하지 않았다면 

						// 사용하는 버튼의 텍스트를 장착하기에서 해제하기로 변경
						TB_Use-&amp;gt;SetText(FText::FromString(TEXT(&quot;해제하기&quot;)));

						// 선택한 슬롯의 장착 표시 이미지를 보이게 함
						SelectedSlot-&amp;gt;Img_Equip-&amp;gt;SetVisibility(ESlateVisibility::HitTestInvisible);

						// EquipList에서 선택한 슬롯을 추가
						EquipList.Add(SelectedSlot);

						// GameInstance의 EquipSlotIndexList에서 현재 슬롯의 인덱스 추가
						GI-&amp;gt;EquipSlotIndexList.Add(GetSlotIndexInWB(SelectedSlot));

						// 플레이어 아이템 장착
						OwningPlayer-&amp;gt;EquipItemToSocket(SelectedSlot-&amp;gt;SlotInfo.ItemInfo);
					}
				}
				else if(SelectedCategory == WB_SlotList_Emotion)
				{
					// 이모션 아이템을 사용했다면

					// 플레이어가 Montage를 실행하도록 함
					OwningPlayer-&amp;gt;ServerRPC_Emotion(SelectedSlot-&amp;gt;SlotInfo.ItemInfo.Montage);
				}
			}
		}
	}
}&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;&lt;b&gt;※ 아이템을 사용하면 아이템이 소모되는 코드를 ThrowAwaySelectedItem() 함수를 사용하였는데 본인이 진행한 프로젝트에선 아이템을 인벤토리에서 버렸을 때 월드에 버려지지 않고 그냥 사라지는 방식으로 기획되었기 때문에 아이템을 버리는 로직을 그대로 사용하였지만 아이템을 버렸을 때 월드에 아이템이 스폰되어야 한다면 ThrowAwaySelectedItem()을 아이템을 사용하는 코드에 사용하는 것은 바람직하지 못함&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GetSlotIndexInWB()&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730451896423&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 현재 선택한 슬롯이 부모에서 몇 번째 자식인지 반환받는 함수
int32 UInventoryWidget::GetSlotIndexInWB(UWidget* SlotWidget)
{
	if (!WB_SlotList_Costume || !SlotWidget) return -1;

	const TArray&amp;lt;UWidget*&amp;gt;&amp;amp; Slots = WB_SlotList_Costume-&amp;gt;GetAllChildren();

	int32 Index = Slots.IndexOfByKey(SlotWidget);

	return Index;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CheckEquipitem()&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730451915996&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 레벨 이동간 장착 아이템에 대한 정보를 유지하기 위한 함수
void UInventoryWidget::CheckEquipitem()
{
	if (GI-&amp;gt;EquipSlotIndexList.Num() &amp;lt;= 0) return;

	for (auto n : GI-&amp;gt;EquipSlotIndexList)
	{
		UHG_SlotWidget* EquipSlot = Cast&amp;lt;UHG_SlotWidget&amp;gt;(WB_SlotList_Costume-&amp;gt;GetChildAt(n));
		if (EquipSlot)
		{
			EquipSlot-&amp;gt;Img_Equip-&amp;gt;SetVisibility(ESlateVisibility::HitTestInvisible);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카테고리를 바꾸는 버튼이 클릭 됐을 때 실행되는 함수들&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730452116665&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Active 카테고리 버튼이 클릭 됐을 때 실행되는 함수
void UInventoryWidget::SelectCategory_Active()
{
	SelectedCategory = WB_SlotList_Active;
	WS_Category-&amp;gt;SetActiveWidgetIndex(0);
	TB_Use-&amp;gt;SetText(FText::FromString(TEXT(&quot;사용하기&quot;)));
}

// Costume 카테고리 버튼이 클릭 됐을 때 실행되는 함수
void UInventoryWidget::SelectCategory_Costume()
{
	SelectedCategory = WB_SlotList_Costume;
	WS_Category-&amp;gt;SetActiveWidgetIndex(1);

	if (SelectedSlot != nullptr &amp;amp;&amp;amp; EquipList.Contains(SelectedSlot))
	{
		TB_Use-&amp;gt;SetText(FText::FromString(TEXT(&quot;해제하기&quot;)));
	}
	else
	{
		TB_Use-&amp;gt;SetText(FText::FromString(TEXT(&quot;장착하기&quot;)));
	}
}

// Emotion 카테고리 버튼이 클릭 됐을 때 실행되는 함수
void UInventoryWidget::SelectCategory_Emotion()
{
	SelectedCategory = WB_SlotList_Emotion;
	WS_Category-&amp;gt;SetActiveWidgetIndex(2);
	TB_Use-&amp;gt;SetText(FText::FromString(TEXT(&quot;사용하기&quot;)));
}

// Emoji 카테고리 버튼이 클릭 됐을 때 실행되는 함수
void UInventoryWidget::SelectCategory_Emoji()
{
	SelectedCategory = WB_SlotList_Emoji;
	WS_Category-&amp;gt;SetActiveWidgetIndex(3);
	TB_Use-&amp;gt;SetText(FText::FromString(TEXT(&quot;사용하기&quot;)));
}

// Sound 카테고리 버튼이 클릭 됐을 때 실행되는 함수
void UInventoryWidget::SelectCategory_Sound()
{
	SelectedCategory = WB_SlotList_Sound;
	WS_Category-&amp;gt;SetActiveWidgetIndex(4);
	TB_Use-&amp;gt;SetText(FText::FromString(TEXT(&quot;사용하기&quot;)));
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SlotWidget이 클릭되었을 때 선택된 Slot의 아이템 정보를 표시하는 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730452183222&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UInventoryWidget::DisplaySelectedItemInfo()
{	
	// 선택된 슬롯이 있는지 없는지 검사
	if (SelectedSlot)
	{
		// 있다면 선택된 슬롯의 정보로 UI를 갱신
		Img_SelectedItem-&amp;gt;SetBrushFromTexture(SelectedSlot-&amp;gt;SlotInfo.ItemInfo.ItemIcon);
		TB_ItemName-&amp;gt;SetText(FText::FromString(SelectedSlot-&amp;gt;SlotInfo.ItemInfo.ItemName));
		TB_Price-&amp;gt;SetText(FText::AsNumber((SelectedSlot-&amp;gt;SlotInfo.ItemInfo.ItemPrice)));
		TB_Quantity-&amp;gt;SetText(FText::AsNumber(SelectedSlot-&amp;gt;SlotInfo.Quantity));   
	}
	else
	{
		// 없다면 아무것도 표시하지 않음
		Img_SelectedItem-&amp;gt;SetBrushFromMaterial(DefaultImage);
		TB_ItemName-&amp;gt;SetText(FText::FromString(TEXT(&quot;&quot;)));
		TB_Price-&amp;gt;SetText(FText::FromString(TEXT(&quot;&quot;)));
		TB_Quantity-&amp;gt;SetText(FText::FromString(TEXT(&quot;&quot;)));
	}
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>SIN</category>
      <category>ue 인벤토리</category>
      <category>UE5</category>
      <category>ue5 인벤토리</category>
      <category>unrealengine</category>
      <category>인벤토리</category>
      <author>shinhongk</author>
      <guid isPermaLink="true">https://developer-destroy.tistory.com/4</guid>
      <comments>https://developer-destroy.tistory.com/4#entry4comment</comments>
      <pubDate>Mon, 4 Nov 2024 18:26:07 +0900</pubDate>
    </item>
    <item>
      <title>[UE5] 인벤토리 시스템 - 인벤토리 기본 구조</title>
      <link>https://developer-destroy.tistory.com/3</link>
      <description>&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프로젝트 개요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 명이 하나의 &lt;b&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;스포츠 영상을 시청&lt;/span&gt;&lt;/b&gt;하면서 &lt;b&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;특정 대상(스포츠 팀 / 선수)을 응원&lt;/span&gt;&lt;/b&gt;하는 플랫폼을 제공하는 메타버스 컨텐츠를 제작하는 프로젝트&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;인벤토리의 필요성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스포츠 경기를 관람할 때 다이나믹한 응원을 위해 폭죽을 터트리고 응원봉을 흔들듯 여러 아이템이 있으면 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;영상을 시청하는데 재미 요소&lt;/b&gt;&lt;/span&gt;를 더할 수 있을 것이라 판단했고, 여러 아이템을 자신이 가진 포인트로 구매하고 &lt;b&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;구매한 아이템을 관리 기능&lt;/span&gt;&lt;/b&gt;이 필요하다 판단하여 인벤토리를 구현하게 되었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;구현 개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인벤토리는 배열로 구현하고자 하였고 하나의 인벤토리의 슬롯에 대한 구조체와 아이템의 정보에 대한 구조체를 만들고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이템 정보를 하나의 Row로 하는 DataTable을 만들어 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;구매, 사용, 획득 간에 DataTable을 참조&lt;/b&gt;&lt;/span&gt;하여 실제 아이템 오브젝트를 인벤토리에 넣는 것이 아닌 &lt;b&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;아이템에 대한 정보를 인벤토리에 담으려 함&lt;/span&gt;.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인벤토리 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dy9Awk/btsKseZNEAs/3ZQLlBQsSHYau4b4Llp1T0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dy9Awk/btsKseZNEAs/3ZQLlBQsSHYau4b4Llp1T0/img.png&quot; data-alt=&quot;SlotStruct 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dy9Awk/btsKseZNEAs/3ZQLlBQsSHYau4b4Llp1T0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdy9Awk%2FbtsKseZNEAs%2F3ZQLlBQsSHYau4b4Llp1T0%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;629&quot; height=&quot;614&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SlotStruct 구조&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;SlotStruct는 구조체로 인벤토리 한 칸에 대한 정보를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SlotStruct는 ItemData라는 구조체와 SlotQuantity라는 int32 자료형의 변수를 멤버로 갖고 있다.&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;ItemData는 아이템이 가져야 할 기본적인 데이터를 멤버로 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ItemData에 == 연산자를 다중정의한 이유는 두 개의 아이템이 같은 아이템인지 비교할 때, 아이템 데이터를 비교하여 판단하기 위함이다.&lt;/p&gt;
&lt;pre id=&quot;code_1730363456902&quot; class=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;USTRUCT(BlueprintType)
struct FItemData : public FTableRowBase
{
	GENERATED_USTRUCT_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FString ItemName;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	UTexture2D* ItemIcon = nullptr;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 ItemPrice;

	UPROPERTY(EditDefaultsOnly)
	TSubclassOf&amp;lt;AHG_ItemBase&amp;gt; ItemClass;

	UPROPERTY(EditDefaultsOnly)
	EItemCategory ItemCategory;

	bool operator==(const FItemData&amp;amp; Other) const
	{
		return ItemName == Other.ItemName;
	}
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1730363461228&quot; class=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;USTRUCT(Atomic, BlueprintType)
struct FSlotStruct
{
	GENERATED_BODY()
public:

	UPROPERTY(EditAnywhere)
	int32 Quantity = 0;
    
	UPROPERTY(EditDefaultsOnly)
	FItemData ItemInfo;
};&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;인벤토리는 SlotStruct라는 구조체를 하나의 원소로 갖는 1차원 배열로 선언&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;52&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FAB0N/btsKrxMAYpz/BOsk0ui9UnzRYYkhrybIMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FAB0N/btsKrxMAYpz/BOsk0ui9UnzRYYkhrybIMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FAB0N/btsKrxMAYpz/BOsk0ui9UnzRYYkhrybIMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFAB0N%2FbtsKrxMAYpz%2FBOsk0ui9UnzRYYkhrybIMk%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;410&quot; height=&quot;52&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;52&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unreal Editor에서 ItemData를 하나의 행으로 하는 DataTable 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ Unreal Editor에서 DataTable을 만들 때, C++로 구현한 구조체를 DataTable의 RowData로 설정하려면 FTableRowBase를 상속받아야 함.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;34&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvFtbU/btsKqZiBsH8/3VPUpKKZpI7GMu2OT25TW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvFtbU/btsKqZiBsH8/3VPUpKKZpI7GMu2OT25TW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvFtbU/btsKqZiBsH8/3VPUpKKZpI7GMu2OT25TW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvFtbU%2FbtsKqZiBsH8%2F3VPUpKKZpI7GMu2OT25TW0%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;427&quot; height=&quot;34&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;34&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1809&quot; data-origin-height=&quot;577&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oZthQ/btsKrBVHLMS/lswv7MaDruOGVSgreGFGW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oZthQ/btsKrBVHLMS/lswv7MaDruOGVSgreGFGW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oZthQ/btsKrBVHLMS/lswv7MaDruOGVSgreGFGW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoZthQ%2FbtsKrBVHLMS%2Flswv7MaDruOGVSgreGFGW1%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;1809&quot; height=&quot;577&quot; data-origin-width=&quot;1809&quot; data-origin-height=&quot;577&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인벤토리 기능 함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 인벤토리 관련 기능은 Player 클래스에서 분리하고 PlayerInventoryComponent라는 클래스에서 따로 관리하고 Player가 PlayerInventoryComponent를 컴포넌트로 갖도록 하는 구조&lt;/p&gt;
&lt;pre id=&quot;code_1730363512138&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#pragma once

#include &quot;CoreMinimal.h&quot;
#include &quot;Components/ActorComponent.h&quot;
#include &quot;SlotStruct.h&quot;
#include &quot;HG_PlayerInventoryComponent.generated.h&quot;

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class METACHEERINGROOM_API UHG_PlayerInventoryComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	// Sets default values for this component's properties
	UHG_PlayerInventoryComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:
	// Called every frame
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

	// 인벤토리의 최대 크기
	UPROPERTY(EditDefaultsOnly)
	int32 InventorySize;
	
    	// FSlotStruct을 요소로 갖는 배열 (실제 인벤토리)
	TArray&amp;lt;FSlotStruct&amp;gt; Inventory;

	// 인벤토리에 아이템 추가
	// Parameter
	// Item : 인벤토리에 추가할 아이템의 정보(FItemData 구조체)
	// Quantity : 인벤토리에 넣을 아이템 개수
	void AddtoInventory(FItemData Item, int32 Quantity);

	//  인벤토리에 아이템이 존재하는지 존재한다면 그 아이템의 인덱스를 반환
	// Parameter 
	// ItemName : 검사하고싶은 아이템 이름
	// Return
	// int32, 찾은 아이템의 인덱스, 없으면 -1 반환
	int32 FindSlot(FString ItemName);

	// 인벤토리에서 아이템 제거
	// Parameter
	// Item : 인벤토리에 제거할 아이템의 정보(FItemData 구조체)
	// Quantity : 인벤토리에서 제거할 아이템 개수
	void RemoveFromInventory(FItemData Item, int32 Quantity);
    
    	// 아이템 정보를 담는 데이터 테이블
	UPROPERTY(EditDefaultsOnly)
	class UDataTable* ItemDataTable;
	
    	// 컴포넌트를 갖고있는 플레이어의 정보
	class AHG_Player* CompOwner;
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;void AddtoInventory(FItemData Item, int32 Quantity)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730364572421&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UHG_PlayerInventoryComponent::AddtoInventory(FItemData Item, int32 Quantity)
{
	// 만약 인벤토리의 크기가 최대 크기가 넘는지 확인
	if (Inventory.Num() &amp;lt;= InventorySize)
	{
		// 인벤토리에 아이템이 존재하는지 확인
		if (FindSlot(Item.ItemName) != -1)
		{
        		// 만약 인벤토리에 아이템이 존재한다면
            		// 인벤토리 배열에서 찾은 아이템 인덱스의 Quantity를 증가
			Inventory[FindSlot(Item.ItemName)].Quantity += Quantity;
		}
		else
		{
        	// 만약 인벤토리에 아이템이 존재하지 않는다면
            	// SlotStruct를 만들어서
			FSlotStruct Temp;
            	// 인벤토리에 더하려는 아이템 정보를 슬롯의 아이템 정보로 초기화
			Temp.ItemInfo = Item;
            	// SlotStruct의 Quantity (초깃값 0)에 매개변수 Quantity를 더해줌
			Temp.Quantity += Quantity;
            	// Inventory에 Temp라는 슬롯 추가 
			Inventory.Add(Temp);
		}
	}
   	// GameInstance의 인벤토리 정보 갱신
	CompOwner-&amp;gt;GI-&amp;gt;CurrentInventory = Inventory;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;int32 FindSlot(FString ItemName);&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730364908557&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int32 UHG_PlayerInventoryComponent::FindSlot(FString ItemName)
{
	// 인벤토리 배열의 모든 인덱스 순회
	for (int32 i = 0; i &amp;lt; Inventory.Num(); i++)
	{	
    		// 만약 현재 순회 중인 인덱스의 요소의 ItemName이 매개변수로 전달된 ItemName과 같다면
		if (Inventory[i].ItemInfo.ItemName == ItemName)
		{
        		// 현재의 인덱스를 반환
			return i;
		}
	}
    	// 모두 순회했지만 찾지 못했다면 -1을 반환
	return -1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;void&amp;nbsp;RemoveFromInventory(FItemData&amp;nbsp;Item,&amp;nbsp;int32&amp;nbsp;Quantity);&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730365181495&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UHG_PlayerInventoryComponent::RemoveFromInventory(FItemData Item, int32 Quantity)
{
	// Item이라는 아이템 데이터를 갖는 슬롯이 존재하는지 확인
	if (FindSlot(Item.ItemName) != -1)
	{
    		// 존재한다면
        	// 찾은 인덱스의 아이템의 Quantity를 줄여준다.
        	// 여기서 인덱스의 Quantity가 0 이하로 내려가지 않도록 Clamp를 통해 최솟값을 0이 되도록함
		Inventory[FindSlot(Item.ItemName)].Quantity = FMath::Clamp(Inventory[FindSlot(Item.ItemName)].Quantity - Quantity, 0, InventorySize);
		// 만약 Quantity가 0이 됐다면
        	if (Inventory[FindSlot(Item.ItemName)].Quantity == 0)
		{ 
        		// 인벤토리에서 찾은 인덱스의 요소를 제거
			Inventory.RemoveAt(FindSlot(Item.ItemName));
		}
	}
    	// GameInstance의 현재 Inventory정보 저장
	CompOwner-&amp;gt;GI-&amp;gt;CurrentInventory = Inventory;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SIN</category>
      <category>UE5</category>
      <category>ue5 인벤토리</category>
      <category>unrealengine</category>
      <category>인벤토리</category>
      <author>shinhongk</author>
      <guid isPermaLink="true">https://developer-destroy.tistory.com/3</guid>
      <comments>https://developer-destroy.tistory.com/3#entry3comment</comments>
      <pubDate>Fri, 1 Nov 2024 20:57:26 +0900</pubDate>
    </item>
    <item>
      <title>[UE5] 플레이어 공격의 충돌 처리 - ( ShapeComponent Sweep ) [2]</title>
      <link>https://developer-destroy.tistory.com/2</link>
      <description>&lt;h1&gt;최종 결과 영상&lt;/h1&gt;
&lt;p&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/OnsMTcPuS-Y?si=xqCpiW6Cils9c3ID&quot; width=&quot;560&quot; height=&quot;315&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;h1&gt;베지어 곡선&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uW9wO/btsKoPgukVq/E4DqBztbmmcPHt3j1sFkT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uW9wO/btsKoPgukVq/E4DqBztbmmcPHt3j1sFkT0/img.png&quot; data-alt=&quot;2차 베지어 곡선&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uW9wO/btsKoPgukVq/E4DqBztbmmcPHt3j1sFkT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuW9wO%2FbtsKoPgukVq%2FE4DqBztbmmcPHt3j1sFkT0%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;572&quot; height=&quot;324&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;324&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2차 베지어 곡선&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베지어 곡선은, 조절점을 사용해 정의하는, 특별한 형태의 곡선이다. 2차 베지어 곡선은 &lt;b&gt;시작점, 중간점, 끝점&lt;/b&gt;으로 만들어지는 곡선으로, 시작에서 중간점까지, 중간에서 끝점까지의 1차 베지어 곡선을 구한다. 시간 T에 대한 각각의 베지어 곡선의 지점 별로 위 그림과 같이 베지어 곡선을 한 번 더 구함으로써 곡선이 만들어지게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nUxlW/btsKp8zeiJq/9Q0q1NRdkVD7e7qkfHd110/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nUxlW/btsKp8zeiJq/9Q0q1NRdkVD7e7qkfHd110/img.gif&quot; data-alt=&quot;베지어 곡선 구성 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nUxlW/btsKp8zeiJq/9Q0q1NRdkVD7e7qkfHd110/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/nUxlW/btsKp8zeiJq/9Q0q1NRdkVD7e7qkfHd110/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;612&quot; height=&quot;488&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;488&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;이 베지어 곡선을 이용해 이전 프레임과 현재 프레임의 위치를 직선이 아닌 곡선 이동 하도록 수정하고자 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAgx1L/btsKpvazR3d/RNcw8QEHzKrvKM85iJtO2K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAgx1L/btsKpvazR3d/RNcw8QEHzKrvKM85iJtO2K/img.gif&quot; data-alt=&quot;이전 프레임과 현재 프레임(초록색)의 중간 지점(빨간색)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAgx1L/btsKpvazR3d/RNcw8QEHzKrvKM85iJtO2K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cAgx1L/btsKpvazR3d/RNcw8QEHzKrvKM85iJtO2K/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;1184&quot; height=&quot;545&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;545&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;우선 이전 프레임과 현재 프레임의 중간 지점을 구해서 디버그로 찍어보았다. 이 중간점을 기준으로 베지어 곡선을 만들어도, 세 점이 일직선 상에 있기 때문에 곡선이 만들어지지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;외적으로 제어점 구하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;549&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buv7bR/btsKpCgs9ft/gNEd5JZF7IKdb7dECIL4R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buv7bR/btsKpCgs9ft/gNEd5JZF7IKdb7dECIL4R0/img.png&quot; data-alt=&quot;외적을 이용해 적절한 곡선을 만들 제어점 위치 구하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buv7bR/btsKpCgs9ft/gNEd5JZF7IKdb7dECIL4R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbuv7bR%2FbtsKpCgs9ft%2FgNEd5JZF7IKdb7dECIL4R0%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;745&quot; height=&quot;549&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;549&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;1. 이전 프레임 위치에서 현재 프레임 위치까지의 벡터(P2 - P1)의 단위벡터를 구한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 구한 단위벡터에 UP 벡터를 외적하여 수직이 되는 방향 벡터를 구한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. P2-P1의 중간 지점으로부터 구한 수직 방향으로 (P2-P1)의 거리의 절반만큼 제어점을 이동시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 이동시킨 제어점을 기준으로 베지어 곡선을 구한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NS5F8/btsKo1gIwu5/kAQ6Voyixbkf9USuzdtoL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NS5F8/btsKo1gIwu5/kAQ6Voyixbkf9USuzdtoL0/img.png&quot; data-alt=&quot;프레임 별 위치 (초록색) 제어점(빨간색)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NS5F8/btsKo1gIwu5/kAQ6Voyixbkf9USuzdtoL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNS5F8%2FbtsKo1gIwu5%2FkAQ6Voyixbkf9USuzdtoL0%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;726&quot; height=&quot;441&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;441&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;각각의 프레임 위치에 대해 수직으로 뻗은 베지어 곡선 제어점을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 제어점을 기준으로 베지어 곡선을 만들도록 코드를 수정하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;베지어 곡선 적용한 코드&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void AM4_PlayerWeaponSword::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    UCapsuleComponent* CapsuleComp = Cast&amp;lt;UCapsuleComponent&amp;gt;(CollisionComp);
    FVector CurrentWeaponLoc = CapsuleComp-&amp;gt;GetComponentLocation();
    float CapsuleHalfHeight = CapsuleComp-&amp;gt;GetScaledCapsuleHalfHeight();
    float CapsuleRadius = CapsuleComp-&amp;gt;GetScaledCapsuleRadius();
    // 이전 위치가 설정되지 않은 경우 초기화
    if (PrevWeaponLoc.IsZero())
    {
        PrevWeaponLoc = CurrentWeaponLoc;
        return;
    }

    // 충돌 감지가 활성화된 경우에만 실행
    if (bOnCollision)
    {
        FVector Trajectory = CurrentWeaponLoc - PrevWeaponLoc;
        // 원형 궤적을 만들기 위한 제어점 계산
        FVector Forward = (CurrentWeaponLoc - PrevWeaponLoc).GetSafeNormal();
        FVector Right = FVector::CrossProduct(Forward, FVector::UpVector);
        FVector ControlPoint = (PrevWeaponLoc + CurrentWeaponLoc) * 0.5f;
        // 베지어 곡선 중간점 위치 살짝 틀어주기
        ControlPoint += Right * FVector::Dist(PrevWeaponLoc, CurrentWeaponLoc) * 0.5f;
        int NumSteps = FMath::CeilToInt(Trajectory.Size() / 30.0f); // 보간 스텝 수 결정
        for (int32 i = 0; i &amp;lt;= NumSteps; ++i)
        {
            float Alpha = i / static_cast&amp;lt;float&amp;gt;(NumSteps);
            FVector InterpolatedPosition = BezierLerp(PrevWeaponLoc, ControlPoint, CurrentWeaponLoc, Alpha);
           // 각 보간된 위치에서 충돌 감지 실행 ( 구현 예정 )

            // 디버그 캡슐을 표시하여 궤적 시각화
            DrawDebugCapsule(GetWorld(), InterpolatedPosition, CapsuleHalfHeight, CapsuleRadius, GetActorQuat(), FColor::Green, false, 1.f);
        }
    }
    // 이전 위치 업데이트
    PrevWeaponLoc = CurrentWeaponLoc;
}

FVector AM4_PlayerWeaponSword::BezierLerp(const FVector&amp;amp; Start, const FVector&amp;amp; Control, const FVector&amp;amp; End, float T)
{
    FVector Lerp1 = FMath::Lerp(Start, Control, T);
    FVector Lerp2 = FMath::Lerp(Control, End, T);
    return FMath::Lerp(Lerp1, Lerp2, T);
}&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;1037&quot; data-origin-height=&quot;539&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rZ5YZ/btsKpfFMMFL/pC2g1MIVEVrxxKr0LKVVUK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rZ5YZ/btsKpfFMMFL/pC2g1MIVEVrxxKr0LKVVUK/img.gif&quot; data-alt=&quot;베지어 곡선을 적용한 궤적&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rZ5YZ/btsKpfFMMFL/pC2g1MIVEVrxxKr0LKVVUK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/rZ5YZ/btsKpfFMMFL/pC2g1MIVEVrxxKr0LKVVUK/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;1037&quot; height=&quot;539&quot; data-origin-width=&quot;1037&quot; data-origin-height=&quot;539&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;어느정도 보완이 된 모습은 보이지만, 아직 완벽하게 보간되지는 않는다.&lt;/p&gt;
&lt;h1&gt;회전 보간&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 콜리전의 회전을 부드럽게 보간하려고 한다. 회전의 경우 정확하게 보간하기 위해 쿼터니언을 사용해 보간했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회전 보간까지 적용한 최종 코드&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void AM4_PlayerWeaponSword::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    UCapsuleComponent* CapsuleComp = Cast&amp;lt;UCapsuleComponent&amp;gt;(CollisionComp);
    FVector CurrentWeaponLoc = CapsuleComp-&amp;gt;GetComponentLocation();
    // 현재 회전
    FQuat CurrentRotation = GetActorQuat();
    float CapsuleHalfHeight = CapsuleComp-&amp;gt;GetScaledCapsuleHalfHeight();
    float CapsuleRadius = CapsuleComp-&amp;gt;GetScaledCapsuleRadius();
    // 이전 위치가 설정되지 않은 경우 초기화
    if (PrevWeaponLoc.IsZero())
    {
        PrevWeaponLoc = CurrentWeaponLoc;
        PrevWeaponRot = CurrentRotation;
        return;
    }

    // 충돌 감지가 활성화된 경우에만 실행
    if (bOnCollision)
    {
        FVector Trajectory = CurrentWeaponLoc - PrevWeaponLoc;
        // 원형 궤적을 만들기 위한 제어점 계산
        FVector Forward = (CurrentWeaponLoc - PrevWeaponLoc).GetSafeNormal();
        FVector Right = FVector::CrossProduct(Forward, FVector::UpVector);
        FVector ControlPoint = (PrevWeaponLoc + CurrentWeaponLoc) * 0.5f;
        // 베지어 곡선 중간점 위치 살짝 틀어주기
        ControlPoint += Right * FVector::Dist(PrevWeaponLoc, CurrentWeaponLoc) * 0.5f;
        int NumSteps = FMath::CeilToInt(Trajectory.Size() / 30.0f); // 보간 스텝 수 결정
        for (int32 i = 0; i &amp;lt;= NumSteps; ++i)
        {
            float Alpha = i / static_cast&amp;lt;float&amp;gt;(NumSteps);
            FVector InterpolatedPosition = BezierLerp(PrevWeaponLoc, ControlPoint, CurrentWeaponLoc, Alpha);
            // 회전 보간 추가
            FQuat InterpolatedRotation = FQuat::Slerp(PrevWeaponRot, CurrentRotation, Alpha);
            // 각 보간된 위치에서 충돌 감지 실행 ( 구현 예정 )

            // 디버그 캡슐을 표시하여 궤적 시각화
            DrawDebugCapsule(GetWorld(), InterpolatedPosition, CapsuleHalfHeight, CapsuleRadius, InterpolatedRotation, FColor::Green, false, 1.f);
        }
    }
    // 이전 위치 업데이트
    PrevWeaponLoc = CurrentWeaponLoc;
    // 이전 회전 업데이트
    PrevWeaponRot = CurrentRotation;
}&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;1184&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mGChS/btsKoZcapDa/njJwZ4fQnZ6BBED9UYrgyK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mGChS/btsKoZcapDa/njJwZ4fQnZ6BBED9UYrgyK/img.gif&quot; data-alt=&quot;최종 보간 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mGChS/btsKoZcapDa/njJwZ4fQnZ6BBED9UYrgyK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/mGChS/btsKoZcapDa/njJwZ4fQnZ6BBED9UYrgyK/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;1184&quot; height=&quot;545&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최종 보간 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;대 만 족&lt;/h1&gt;</description>
      <category>JIN</category>
      <category>actiongame</category>
      <category>Bezier curve</category>
      <category>Collision</category>
      <category>game</category>
      <category>UE5</category>
      <category>unrealengine</category>
      <category>베지어 곡선</category>
      <author>destroyer</author>
      <guid isPermaLink="true">https://developer-destroy.tistory.com/2</guid>
      <comments>https://developer-destroy.tistory.com/2#entry2comment</comments>
      <pubDate>Thu, 31 Oct 2024 06:00:10 +0900</pubDate>
    </item>
    <item>
      <title>[UE5] 플레이어 공격의 충돌 처리 ( ShapeComponent Sweep ) [1]</title>
      <link>https://developer-destroy.tistory.com/1</link>
      <description>&lt;h1&gt;플레이어 타격 충돌 처리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전투 시스템에서 타격 판정은 매우 중요한 요소이다. 정교하게 타격 판정이 이루어 질 수록 타격 감각을 세밀하게 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 속도가 빠른 애니메이션의 경우, 프레임 단위로 충돌 체크를 하면 타격 판정에 빈 구간이 눈에 띄게 나타난다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;타격 판정 Before.gif&quot; data-origin-width=&quot;746&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7MqF3/btsKnsS38LL/vXKYQeS5d3GduJvPHLk8I0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7MqF3/btsKnsS38LL/vXKYQeS5d3GduJvPHLk8I0/img.gif&quot; data-alt=&quot;타격에 빈 구간이 많은 기본 충돌 체크&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7MqF3/btsKnsS38LL/vXKYQeS5d3GduJvPHLk8I0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b7MqF3/btsKnsS38LL/vXKYQeS5d3GduJvPHLk8I0/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;746&quot; height=&quot;434&quot; data-filename=&quot;타격 판정 Before.gif&quot; data-origin-width=&quot;746&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;타격에 빈 구간이 많은 기본 충돌 체크&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;기본 선형 보간&lt;/h1&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void AM4_PlayerWeaponSword::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    FVector CapsuleLocation = CapsuleComp-&amp;gt;GetComponentLocation();
    FVector CurrentWeaponLoc = CapsuleComp-&amp;gt;GetComponentLocation();
    float CapsuleHalfHeight = CapsuleComp-&amp;gt;GetScaledCapsuleHalfHeight();
    float CapsuleRadius = CapsuleComp-&amp;gt;GetScaledCapsuleRadius();

    // 이전 위치가 설정되지 않은 경우 초기화
    if (PrevWeaponLoc.IsZero())
    {
        PrevWeaponLoc = CurrentWeaponLoc;
        return;
    }

    // 충돌 감지가 활성화된 경우에만 실행
    if (bOnCollision)
    {
        FVector Trajectory = CurrentWeaponLoc - PrevWeaponLoc;
        int NumSteps = FMath::CeilToInt(Trajectory.Size() / 20.0f); // 보간 스텝 수 결정

        for (int i = 0; i &amp;lt;= NumSteps; ++i)
        {
            float Alpha = i / static_cast&amp;lt;float&amp;gt;(NumSteps);
            FVector InterpolatedPosition = FMath::Lerp(PrevWeaponLoc, CurrentWeaponLoc, Alpha);

            // 보간된 위치에서의 충돌 처리 추가하기

            // 디버그 캡슐을 표시하여 궤적 시각화
            DrawDebugCapsule(GetWorld(), InterpolatedPosition, CapsuleHalfHeight, CapsuleRadius, GetActorRotation().Quaternion(), FColor::Green, false, 0.1f);

        }

    }
    // 이전 위치 업데이트
    PrevWeaponLoc = CurrentWeaponLoc;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 위치를 저장해두고, 보간 Step만큼 반복하여 현재 위치와 이전 위치를 선형 보간하는 방식으로 구현했다.&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;608&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wONTi/btsKpuvCgQa/vUh5ea3k8fpGqqV92TBic0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wONTi/btsKpuvCgQa/vUh5ea3k8fpGqqV92TBic0/img.png&quot; data-alt=&quot;키 프레임 콜리전 위치 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wONTi/btsKpuvCgQa/vUh5ea3k8fpGqqV92TBic0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwONTi%2FbtsKpuvCgQa%2FvUh5ea3k8fpGqqV92TBic0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;608&quot; height=&quot;472&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;472&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;다음과 같이 키 프레임 이 존재한다고 했을 때&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;668&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mBeCb/btsKqB1NRI5/y2GVM3YuBlixN5a8ffQ9aK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mBeCb/btsKqB1NRI5/y2GVM3YuBlixN5a8ffQ9aK/img.png&quot; data-alt=&quot;위치 보간 작용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mBeCb/btsKqB1NRI5/y2GVM3YuBlixN5a8ffQ9aK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmBeCb%2FbtsKqB1NRI5%2Fy2GVM3YuBlixN5a8ffQ9aK%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;668&quot; height=&quot;522&quot; data-origin-width=&quot;668&quot; data-origin-height=&quot;522&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;이러한 방식으로 콜리전의 위치를 보간 하여 빈 공간을 메꾸는 방식&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;746&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/16Uhi/btsKpvH5skm/PBMyEKnmYYHD6pi1YSXpOK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/16Uhi/btsKpvH5skm/PBMyEKnmYYHD6pi1YSXpOK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/16Uhi/btsKpvH5skm/PBMyEKnmYYHD6pi1YSXpOK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/16Uhi/btsKpvH5skm/PBMyEKnmYYHD6pi1YSXpOK/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;746&quot; height=&quot;434&quot; data-origin-width=&quot;746&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 공간이 확연이 사라진 것을 볼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 검을 휘두른다고 생각했을 때 머릿속에서 생각나는 궤적과는 차이가 나는 모습. 검이 원형을 그리며 부드럽게 그려지지 않고 각진 형태로 그려진다.&lt;/p&gt;</description>
      <category>JIN</category>
      <category>actiongame</category>
      <category>Collision</category>
      <category>sweep</category>
      <category>UE5</category>
      <category>unrealengine</category>
      <category>게임</category>
      <category>게임개발</category>
      <author>destroyer</author>
      <guid isPermaLink="true">https://developer-destroy.tistory.com/1</guid>
      <comments>https://developer-destroy.tistory.com/1#entry1comment</comments>
      <pubDate>Tue, 29 Oct 2024 17:19:06 +0900</pubDate>
    </item>
  </channel>
</rss>