<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>언리얼 학습 기록</title>
    <link>https://unrealstudyhome.tistory.com/</link>
    <description>unrealstudyhome 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 11 May 2026 07:04:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Turtle_Jun</managingEditor>
    <image>
      <title>언리얼 학습 기록</title>
      <url>https://tistory1.daumcdn.net/tistory/8117056/attach/e6bd17691c5646b4b8f560c1f63e90da</url>
      <link>https://unrealstudyhome.tistory.com</link>
    </image>
    <item>
      <title>최종 팀 프로젝트 커스터마이징 시스템 데이터화 및 UI 구현</title>
      <link>https://unrealstudyhome.tistory.com/112</link>
      <description>&lt;h1&gt;Customize 기능 구현&lt;/h1&gt;
&lt;h1&gt;UE5 모듈러 캐릭터 커스터마이징 시스템 구현 정리&lt;/h1&gt;
&lt;h2&gt;1. 개요&lt;/h2&gt;
&lt;p&gt;프로젝트에서는 캐릭터의 파츠(상의, 하의, 헤어, 장갑 등)를 런타임에서 자유롭게 변경하는 모듈형 시스템(Modular Character System)을 구현했다.&lt;/p&gt;
&lt;p&gt;주요 요구사항은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모듈 파츠 구조를 스켈레탈 메시 단위로 유연하게 교체&lt;/li&gt;
&lt;li&gt;여러 파츠를 조합할 수 있는 FastArray 기반 구조&lt;/li&gt;
&lt;li&gt;클라이언트 UI에서 선택한 파츠를 서버에서 장착 후 모든 클라이언트에 반영&lt;/li&gt;
&lt;li&gt;특정 액터 근처에서 커스터마이징 UI 자동 열림&lt;/li&gt;
&lt;li&gt;파츠 목록은 DataTable 기반으로 관리&lt;/li&gt;
&lt;li&gt;슬롯(부위)별 탭, 슬롯별 파츠 표시, 클릭하여 즉시 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래는 전체 구현 과정을 설명과 코드 중심으로 정리한 내용이다.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;캐릭터 모듈 정의&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvUoMx/dJMcagjGOBv/oSVlbe0SO9rzV1xyGsFN0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvUoMx/dJMcagjGOBv/oSVlbe0SO9rzV1xyGsFN0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvUoMx/dJMcagjGOBv/oSVlbe0SO9rzV1xyGsFN0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvUoMx%2FdJMcagjGOBv%2FoSVlbe0SO9rzV1xyGsFN0k%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IitXN/dJMcafd1guj/u68ksW5FnqY1MVk9m4HbO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IitXN/dJMcafd1guj/u68ksW5FnqY1MVk9m4HbO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IitXN/dJMcafd1guj/u68ksW5FnqY1MVk9m4HbO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIitXN%2FdJMcafd1guj%2Fu68ksW5FnqY1MVk9m4HbO1%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;캐릭터 파츠들을 파악한 후 Enum 형으로 파츠를 정의해준다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;파츠의 데이터 구조&lt;/li&gt;
&lt;li&gt;FastArrayReplication 기반 파츠 리스트&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.1 파츠 데이터 구조 (FCharacterPart)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// CharacterPartType.h

// 파츠 슬롯 정의 (모자, 상의, 하의, 몸통 등)
UENUM(BlueprintType)
enum class ECharacterPartSlot : uint8
{
    None,
    Backpacks, //    가방
    Beards, //    수염
    Body, //    몸 (피부 색)
    Boots, // 신발
    Brows, //    눈썹
    Eyes, // 눈
    Glasses, // 안경
    Gloves, // 장갑
    Hairs, // 머리카락
    Hats,    // 모자
    Pants, // 바지
    Suits,    // 한벌 옷
    Upper_Wear    // 상의
};

/*
 * 단일 파츠 정보 구조체 (어떤 슬롯에 어떤 메시를 쓸지 결정)
 */
USTRUCT(BlueprintType)
struct FCharacterPart
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &amp;quot;Cosmetic&amp;quot;)
    ECharacterPartSlot Slot = ECharacterPartSlot::None;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &amp;quot;Cosmetic&amp;quot;)
    TObjectPtr&amp;lt;USkeletalMesh&amp;gt; Mesh = nullptr;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.2 파츠 리스트 (FFastArraySerializer)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/*
 *    개별 적용된 파츠 배열의 개별 항목
 */
USTRUCT()
struct FAppliedCharacterPartEntry : public FFastArraySerializerItem
{
    GENERATED_BODY()

    // 적용된 파츠의 데이터(부위, 메시) / 네트워크 복제 ㅇ
    UPROPERTY()
    FCharacterPart Part;

    // 런타임에 생성된 스켈레탈 메쉬 컴포넌트 포인터    / 복제 X
    UPROPERTY(NotReplicated)
    TObjectPtr&amp;lt;USkeletalMeshComponent&amp;gt; SpawnedComponent = nullptr;
};

// 캐릭터에 적용된 모든 파츠 목록을 관리하는 구조체 / 네트워크 최적화
USTRUCT()
struct FCharacterPartList : public FFastArraySerializer
{
    GENERATED_BODY()

public:
    FCharacterPartList() : OwnerComponent(nullptr)
    {
    }

    FCharacterPartList(ULNCharacterPartComponent* InOwnerComponent) : OwnerComponent(InOwnerComponent)
    {
    }

    void PreReplicatedRemove(const TArrayView&amp;lt;int32&amp;gt; RemoveIndices, int32 FinalSize);
    void PostReplicatedAdd(const TArrayView&amp;lt;int32&amp;gt; AddIndices, int32 FinalSize);
    void PostReplicatedChange(const TArrayView&amp;lt;int32&amp;gt; ChangeIndices, int32 FinalSize);

    bool NetDeltaSerialize(FNetDeltaSerializeInfo&amp;amp; DeltaParms)
    {
        return FastArrayDeltaSerialize&amp;lt;FAppliedCharacterPartEntry, FCharacterPartList&amp;gt;(Entries, DeltaParms, *this);
    }

    // 스켈레탈 메쉬 컴포넌트 생성 후 부착하는 함수
    void SpawnMeshForEntry(FAppliedCharacterPartEntry&amp;amp; Entry);
    // 스켈레탈 메쉬 컴포넌트 제거하는 함수
    void DestroyMeshForEntry(FAppliedCharacterPartEntry&amp;amp; Entry);

    UPROPERTY()
    TArray&amp;lt;FAppliedCharacterPartEntry&amp;gt; Entries;

    UPROPERTY(NotReplicated)
    TObjectPtr&amp;lt;ULNCharacterPartComponent&amp;gt; OwnerComponent;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.3 TStructOpsTypeTraits (FastArray Serializer 사용 선언)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 템플릿 특수화
template &amp;lt;&amp;gt;
struct TStructOpsTypeTraits&amp;lt;FCharacterPartList&amp;gt; : public TStructOpsTypeTraitsBase2&amp;lt;FCharacterPartList&amp;gt;
{
    enum { WithNetDeltaSerializer = true };
};&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3. 캐릭터 파츠 변경을 담당하는 컴포넌트&lt;/h1&gt;
&lt;p&gt;컴포넌트 이름: &lt;code&gt;ULNCharacterPartComponent&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;3.1 파츠 적용 요청 함수 (서버 전용)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// LNCharacterPartComponent.h

#include &amp;quot;LNCharacterPartComponent.h&amp;quot;

#include &amp;quot;GameFramework/Character.h&amp;quot;
#include &amp;quot;Net/UnrealNetwork.h&amp;quot;

ULNCharacterPartComponent::ULNCharacterPartComponent()
    : AppliedCharacterPartList(this)    
{
    SetIsReplicatedByDefault(true);
}

void ULNCharacterPartComponent::GetLifetimeReplicatedProps(TArray&amp;lt;FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(ThisClass, AppliedCharacterPartList);    //적용된 파츠들 네트워크 복제
}

void ULNCharacterPartComponent::SetCharacterPart(const FCharacterPart&amp;amp; NewPart)
{
    if (!GetOwner() || !GetOwner()-&amp;gt;HasAuthority())
        return;

    //  Suits 들어오면 유지할 파츠만 남기기
    if (NewPart.Slot == ECharacterPartSlot::Suits)
    {
        // 유지할 슬롯 목록
        TSet&amp;lt;ECharacterPartSlot&amp;gt; SlotsToKeep =
        {
            ECharacterPartSlot::Body,
            ECharacterPartSlot::Eyes,
            ECharacterPartSlot::Brows,
            ECharacterPartSlot::Suits   // 자기가 들어갈 슬롯 포함
        };

        for (auto It = AppliedCharacterPartList.Entries.CreateIterator(); It; ++It)
        {
            if (!SlotsToKeep.Contains(It-&amp;gt;Part.Slot))
            {
                // 제거
                AppliedCharacterPartList.DestroyMeshForEntry(*It);
                It.RemoveCurrent();
                AppliedCharacterPartList.MarkArrayDirty();
            }
        }
    }
    //  여기까지 Suits 특수 로직 

    // 기존 슬롯 제거 및 Suits가 남아있다면 Suits 제거 (중복 슬롯 제거 및 Suits 제거)
    for (auto It = AppliedCharacterPartList.Entries.CreateIterator(); It; ++It)
    {
        if (It-&amp;gt;Part.Slot == NewPart.Slot || It-&amp;gt;Part.Slot == ECharacterPartSlot::Suits)
        {
            AppliedCharacterPartList.DestroyMeshForEntry(*It);
            It.RemoveCurrent();
            AppliedCharacterPartList.MarkArrayDirty();
            //break;
        }
    }

    if (!IsValid(NewPart.Mesh)) return;

    // 새로운 엔트리 추가
    FAppliedCharacterPartEntry&amp;amp; NewEntry = AppliedCharacterPartList.Entries.AddDefaulted_GetRef();
    NewEntry.Part = NewPart;

    AppliedCharacterPartList.SpawnMeshForEntry(NewEntry);
    AppliedCharacterPartList.MarkItemDirty(NewEntry);   // replication notify
}

USkeletalMeshComponent* ULNCharacterPartComponent::GetParentMeshComponent() const
{
    if (ACharacter* Char = Cast&amp;lt;ACharacter&amp;gt;(GetOwner()))
    {
        return Char-&amp;gt;GetMesh();
    }
    return nullptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 파츠 추가/제거 복제 처리&lt;/h3&gt;
&lt;p&gt;FastArraySerializer에서는 클라이언트가 직접 파츠를 생성하지 않고,&lt;/p&gt;
&lt;p&gt;서버 변경 후 아래 함수들이 클라이언트에서 자동 호출된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// CharacterPartType.cpp

#include &amp;quot;LNCharacterPartType.h&amp;quot;
#include &amp;quot;LNCharacterPartComponent.h&amp;quot;
#include &amp;quot;Components/SkeletalMeshComponent.h&amp;quot;

//    서버에서 파츠 제거되었다는 정보 도착시 클라이언트에서 호출
void FCharacterPartList::PreReplicatedRemove(const TArrayView&amp;lt;int32&amp;gt; RemoveIndices, int32 FinalSize)
{
    for (const int32 Index : RemoveIndices)
    {
        FAppliedCharacterPartEntry&amp;amp; Entry = Entries[Index];
        DestroyMeshForEntry(Entry);
    }
}

//    서버에서 새로운 파츠가 추가 되었다는 정보 도착시 클라이언트에서 호출
void FCharacterPartList::PostReplicatedAdd(const TArrayView&amp;lt;int32&amp;gt; AddIndices, int32 FinalSize)
{
    for (const int32 Index : AddIndices)
    {
        FAppliedCharacterPartEntry&amp;amp; Entry = Entries[Index];
        SpawnMeshForEntry(Entry);
    }
}

//    서버에서 기존 파츠의 정보가 변경될 때 클라에서 호출
void FCharacterPartList::PostReplicatedChange(const TArrayView&amp;lt;int32&amp;gt; ChangeIndices, int32 FinalSize)
{
    for (const int32 Index : ChangeIndices)
    {
        FAppliedCharacterPartEntry&amp;amp; Entry = Entries[Index];
        DestroyMeshForEntry(Entry);
        SpawnMeshForEntry(Entry);
    }
}

//    파츠 데이터를 기반으로 실제 스켈레탈 메시 컴포넌트를 생성하고 캐릭터에 부착
void FCharacterPartList::SpawnMeshForEntry(FAppliedCharacterPartEntry&amp;amp; Entry)
{
    if (!IsValid(OwnerComponent) || !IsValid(Entry.Part.Mesh)) return;

    USkeletalMeshComponent* ParentMesh = OwnerComponent-&amp;gt;GetParentMeshComponent();
    if (!IsValid(ParentMesh)) return;    

    AActor* OwnerActor = OwnerComponent-&amp;gt;GetOwner();

    FName ComponentName = *FString::Printf(TEXT(&amp;quot;CosmeticPart_%s&amp;quot;), *UEnum::GetValueAsString(Entry.Part.Slot));
    USkeletalMeshComponent* NewPartComponent = NewObject&amp;lt;USkeletalMeshComponent&amp;gt;(OwnerActor, ComponentName);

    NewPartComponent-&amp;gt;SetupAttachment(ParentMesh);

    NewPartComponent-&amp;gt;SetSkeletalMesh(Entry.Part.Mesh);

    // 모든 파츠 컴포넌트가 부모(몸통)의 애니를 그대로 따라가도록 설정
    NewPartComponent-&amp;gt;SetLeaderPoseComponent(ParentMesh);

    NewPartComponent-&amp;gt;RegisterComponent();

    Entry.SpawnedComponent = NewPartComponent;
}

// 파츠 제거될때 생성한 스켈레탈 메쉬를 안전하게 제거
void FCharacterPartList::DestroyMeshForEntry(FAppliedCharacterPartEntry&amp;amp; Entry)
{
    if (IsValid(Entry.SpawnedComponent))
    {
        Entry.SpawnedComponent-&amp;gt;DestroyComponent();
        Entry.SpawnedComponent = nullptr;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;4. 파츠 데이터 관리(DataTable 기반)&lt;/h1&gt;
&lt;p&gt;파츠는 DataTable을 사용해 부위별로 관리한다.&lt;/p&gt;
&lt;h3&gt;4.1 DataTable Row 구조 FPartRow&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// CharacterPartRow.h

#pragma once

#include &amp;quot;CoreMinimal.h&amp;quot;
#include &amp;quot;Engine/DataTable.h&amp;quot;
#include &amp;quot;LNCharacterPartRow.generated.h&amp;quot;

USTRUCT(BlueprintType)
struct FCharacterPartRow : public FTableRowBase
{
    GENERATED_BODY()

public:

    // UI에 표시할 아이콘
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TObjectPtr&amp;lt;UTexture2D&amp;gt; Icon;

    // 장착될 스케레탈 메쉬
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TObjectPtr&amp;lt;USkeletalMesh&amp;gt; Mesh;

};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4.2 카테고리 구조(FPartCategory)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// CharacterPartCategory.h

#pragma once

#include &amp;quot;CoreMinimal.h&amp;quot;
#include &amp;quot;Character/Components/LNCharacterPartType.h&amp;quot;
#include &amp;quot;LNCharacterPartCategory.generated.h&amp;quot;

USTRUCT(BlueprintType)
struct FCharacterPartCategory
{
    GENERATED_BODY()

public:
    // 어떤 파츠 슬롯인지 선택 ( 상/하의 등 )
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    ECharacterPartSlot Slot = ECharacterPartSlot::None;

    // 해당 파츠의 데이터 테이블
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TObjectPtr&amp;lt;UDataTable&amp;gt; CharacterPartTable = nullptr;

    // UI 에서 보여줄 이름
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FText DisplayName;
};&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;5. 커스터마이징 UI 구조&lt;/h1&gt;
&lt;p&gt;UI는 다음 구성으로 제작하였다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상단 탭 버튼 (각 부위)&lt;/li&gt;
&lt;li&gt;ScrollBox 내부 GridPanel (파츠 목록)&lt;/li&gt;
&lt;li&gt;파츠 아이템은 동적 생성 (WBP_PartItem)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.1 탭 클릭 시 파츠 로드 (LoadPartCategory 함수구현)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// CharacterCustomizeWidget.cpp

#include &amp;quot;UI/Widget/LNCharacterCustomizeWidget.h&amp;quot;

#include &amp;quot;Character/LNCharacter.h&amp;quot;
#include &amp;quot;Character/Components/LNCharacterPartComponent.h&amp;quot;
#include &amp;quot;Components/Button.h&amp;quot;
#include &amp;quot;UI/Widget/LNCharacterPartItemWidget.h&amp;quot;
#include &amp;quot;Components/GridPanel.h&amp;quot;
#include &amp;quot;Components/GridSlot.h&amp;quot;
#include &amp;quot;Hash/BuzHash.h&amp;quot;
#include &amp;quot;Player/Controller/LNControllerInGame.h&amp;quot;

void ULNCharacterCustomizeWidget::NativeConstruct()
{
    Super::NativeConstruct();
    Button_Close-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::HandleCloseWidget);
}

// WBP에서 CategoryTab 버튼 생성후LoadPartCategory 실행
void ULNCharacterCustomizeWidget::LoadPartCategory(ECharacterPartSlot PartSlot)
{
    // 현재 UI 화면 정리
    GridPanel_Parts-&amp;gt;ClearChildren();

    // 파츠 슬롯 저장
    CurrentSlot = PartSlot;

    for (const FCharacterPartCategory&amp;amp; Category : PartCategories)
    {
        if (Category.Slot != PartSlot) continue;

        ECharacterPartSlot CharPartSlot = Category.Slot;

        // 해당 슬롯의 DataTable 가져오기
        UDataTable* DataTable = Category.CharacterPartTable;

        if (!DataTable || !PartItemClass) return;

        TArray&amp;lt;FCharacterPartRow*&amp;gt; Rows;
        DataTable-&amp;gt;GetAllRows(TEXT(&amp;quot;Load&amp;quot;), Rows);

        int32 Index = 0;

        for (auto* Row : Rows)
        {
            ULNCharacterPartItemWidget* ItemWidget = CreateWidget&amp;lt;
                ULNCharacterPartItemWidget&amp;gt;(GetWorld(), PartItemClass);
            ItemWidget-&amp;gt;Init(Row-&amp;gt;Icon, CharPartSlot, Row-&amp;gt;Mesh);
            // PartItemWidget에 있는 OnclickedPartItem 델리게이트 바인딩
            ItemWidget-&amp;gt;OnClickedPartItem.AddDynamic(this, &amp;amp;ThisClass::HandlePartItemClicked);

            int32 RowIndex = Index / GridRowSize;
            int32 ColIndex = Index % GridRowSize;

            GridPanel_Parts-&amp;gt;AddChildToGrid(ItemWidget, RowIndex, ColIndex);
            Index++;
        }

        return;
    }
}

void ULNCharacterCustomizeWidget::HandlePartItemClicked(ECharacterPartSlot NewPartSlot, USkeletalMesh* Mesh)
{
    ALNControllerInGame* PC = Cast&amp;lt;ALNControllerInGame&amp;gt;(GetOwningPlayer());
    if (!IsValid(PC)) return;

    // Character컴포넌트의 SetCharacterPart 호출
    PC-&amp;gt;Server_RequestEquipPart(NewPartSlot, Mesh);
}

void ULNCharacterCustomizeWidget::HandleCloseWidget()
{
    ALNControllerInGame* PC = Cast&amp;lt;ALNControllerInGame&amp;gt;(GetOwningPlayer());
    if (!IsValid(PC)) return;

    PC-&amp;gt;Client_CloseCustomizeUI();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;6. 클라이언트 → 서버 파츠 변경 요청&lt;/h1&gt;
&lt;p&gt;UI에서 직접 파츠를 변경하는 것은 안 된다.&lt;/p&gt;
&lt;p&gt;서버가 권한(Authority)을 갖고 있으므로 PlayerController를 통한 Server RPC가 필수이다.&lt;/p&gt;
&lt;p&gt;6.1 PlayerController RPC&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// ControllerInGame.h

// ======= 캐릭터 파츠 요청
UFUNCTION(Server, Reliable)
void Server_RequestEquipPart(FGameplayTag SlotTag, USkeletalMesh* Mesh);

//======= 클라이언트 UI 열고 닫기

    // 커스터마이즈 UI 열기
    UFUNCTION(Client, Reliable, Category=&amp;quot;LN|Customize&amp;quot;)
    void Client_OpenCustomizeUI();

    // 커스터마이스 UI 닫기
    UFUNCTION(Client, Reliable, Category=&amp;quot;LN|Customize&amp;quot;)
    void Client_CloseCustomizeUI();

// ControllerInGame.cpp

void AMyPlayerController::Server_RequestEquipPart_Implementation(FGameplayTag SlotTag, USkeletalMesh* Mesh)
{
    APawn* Pawn = GetPawn();
    if (!Pawn) return;

    ULNCharacterPartComponent* Comp = Pawn-&amp;gt;FindComponentByClass&amp;lt;ULNCharacterPartComponent&amp;gt;();
    if (!Comp) return;

    FCharacterPart NewPart;
    NewPart.SlotTag = SlotTag;
    NewPart.Mesh = Mesh;

    Comp-&amp;gt;SetCharacterPart(NewPart);
}

void ALNControllerInGame::Client_OpenCustomizeUI_Implementation()
{
    if (!IsValid(CustomizeMenuClass)) return;

    if (CustomizeWidget == nullptr)
    {
        CustomizeWidget = CreateWidget(this, CustomizeMenuClass);
    }

    if (CustomizeWidget &amp;amp;&amp;amp; !CustomizeWidget-&amp;gt;IsInViewport())
    {
        CustomizeWidget-&amp;gt;AddToViewport();

        // Cusomize 중 캐릭이동 불가능
        FInputModeUIOnly InputMode;
        InputMode.SetWidgetToFocus(CustomizeWidget-&amp;gt;TakeWidget());
        InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
        SetInputMode(InputMode);

        bShowMouseCursor = true;
    }
}

void ALNControllerInGame::Client_CloseCustomizeUI_Implementation()
{
    if (CustomizeWidget &amp;amp;&amp;amp; CustomizeWidget-&amp;gt;IsInViewport())
    {
        CustomizeWidget-&amp;gt;RemoveFromParent();
    }

    FInputModeGameOnly InputMode;
    SetInputMode(InputMode);
    bShowMouseCursor = false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;7. 커스터마이징 스테이션 (상호작용 인터페이스)&lt;/h1&gt;
&lt;p&gt;플레이어가 특정 액터 근처로 접근 후 상호작용을 하면&lt;/p&gt;
&lt;p&gt;커스터마이징 메뉴가 열리는 액터를 구현&lt;/p&gt;
&lt;h3&gt;7.1 커스터마이징 스테이션 액터&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// LNCustomizeStation.h

#pragma once

#include &amp;quot;CoreMinimal.h&amp;quot;
#include &amp;quot;GameFramework/Actor.h&amp;quot;
#include &amp;quot;Interface/LNIInteractable.h&amp;quot;
#include &amp;quot;LNCustomizeStation.generated.h&amp;quot;

class UStaticMeshComponent;
class USphereComponent;

UCLASS()
class OURLONGNIGHT_API ALNCustomizeStation : public AActor, public ILNIInteractable
{
    GENERATED_BODY()

public:    
    ALNCustomizeStation();

    virtual void BeginPlay() override;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
    TObjectPtr&amp;lt;UStaticMeshComponent&amp;gt; StationMesh;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
    TObjectPtr&amp;lt;USphereComponent&amp;gt; TriggerSphere;

    virtual void LeftClickInteract(AActor* Interactor) override;
    virtual void RightClickInteract(AActor* Interactor) override;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// LNCustomizeStation.cpp

#include &amp;quot;Character/LNCustomizeStation.h&amp;quot;

#include &amp;quot;LNCharacter.h&amp;quot;
#include &amp;quot;Player/Controller/LNControllerInGame.h&amp;quot;
#include &amp;quot;Components/SphereComponent.h&amp;quot;

ALNCustomizeStation::ALNCustomizeStation()
{
    PrimaryActorTick.bCanEverTick = false;

    StationMesh = CreateDefaultSubobject&amp;lt;UStaticMeshComponent&amp;gt;(TEXT(&amp;quot;StationMesh&amp;quot;));
    RootComponent = StationMesh;

}

void ALNCustomizeStation::BeginPlay()
{
    Super::BeginPlay();

}

void ALNCustomizeStation::LeftClickInteract(AActor* Interactor)
{
    ALNCharacter* Character = Cast&amp;lt;ALNCharacter&amp;gt;(Interactor);
    if (!IsValid(Character)) return;

    ALNControllerInGame* PC = Cast&amp;lt;ALNControllerInGame&amp;gt;(Character-&amp;gt;GetController());
    if (!IsValid(PC)) return;

    // 커스터마이징 메뉴 생성
    PC-&amp;gt;Client_OpenCustomizeUI();
}

void ALNCustomizeStation::RightClickInteract(AActor* Interactor)
{
    // 필요시 구현
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.notion.so/Interact-2b69ce06ba76802b8f4bc040989c3ba6?pvs=21&quot;&gt;Interact 인터페이스 구현&lt;/a&gt; &lt;/p&gt;
&lt;p&gt;해당 기록 보드에서 생성한 Interactible 콜리전 프리셋을 해당 액터에 적용해준다.&lt;/p&gt;
&lt;h3&gt;9. 동작 흐름 정리&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;플레이어가 커스터마이징 스테이션 근처에 접근&lt;/li&gt;
&lt;li&gt;PlayerController(Client_OpenCustomizeUI) 호출&lt;/li&gt;
&lt;li&gt;UI에서 파츠 선택&lt;/li&gt;
&lt;li&gt;Server_RequestEquipPart() 실행&lt;/li&gt;
&lt;li&gt;서버에서 SetCharacterPart() 실행&lt;/li&gt;
&lt;li&gt;FastArrayReplication으로 모든 클라이언트에 변경 사항 동기화&lt;/li&gt;
&lt;li&gt;SpawnMeshForEntry / DestroyMeshForEntry 가 클라이언트에서 자동으로 호출&lt;/li&gt;
&lt;li&gt;실제 스켈레탈 메시 구성 갱신&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>언리얼 팀프로젝트</category>
      <author>Turtle_Jun</author>
      <guid isPermaLink="true">https://unrealstudyhome.tistory.com/112</guid>
      <comments>https://unrealstudyhome.tistory.com/112#entry112comment</comments>
      <pubDate>Thu, 4 Dec 2025 23:36:05 +0900</pubDate>
    </item>
    <item>
      <title>언리얼 팀프로젝트최종 팀 프로젝트 Interact 캐릭터에 적용</title>
      <link>https://unrealstudyhome.tistory.com/111</link>
      <description>&lt;p&gt;[이전 시간에 구현한 Interface] (&lt;a href=&quot;https://unrealstudyhome.tistory.com/109&quot;&gt;https://unrealstudyhome.tistory.com/109&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTO5fD/dJMcabWWwrN/uKcmLjApl7ZSdXASdslkf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTO5fD/dJMcabWWwrN/uKcmLjApl7ZSdXASdslkf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTO5fD/dJMcabWWwrN/uKcmLjApl7ZSdXASdslkf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTO5fD%2FdJMcabWWwrN%2FuKcmLjApl7ZSdXASdslkf0%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;┌─────────────────┐&lt;br&gt;│  ILNIInteractable                    │  ← 인터페이스&lt;br&gt;└────────┬────────┘&lt;br&gt;│ implements&lt;br&gt;┌────┴─────┬──────────┐&lt;br&gt;│                              │                              │&lt;br&gt;┌───▼───┐ ┌───▼───┐ ┌───▼───┐&lt;br&gt;│Character   │ │     기믹1       │ │     기믹2     │&lt;br&gt;└───────┘ └───────┘ └────────┘&lt;/p&gt;
&lt;p&gt;각 기믹들과 캐릭터에 한해서 상호작용 인터페이스를 상속 받아 구현을 한 후 &lt;/p&gt;
&lt;p&gt;캐릭터의 입력을 받아서 각자에 맞는 상호작용 로직을 호출하는 형식으로 구현함&lt;/p&gt;
&lt;p&gt;캐릭터에서 액터 상호작용 입력흐름 구조는 아래와 같다.&lt;/p&gt;
&lt;aside&gt;

&lt;p&gt;[클라이언트]&lt;br&gt;   ↓ Tick마다 감지&lt;br&gt;UpdateInteractableTarget()&lt;br&gt;   ↓ 인터페이스 구현 확인&lt;br&gt;CurrentInteractableTarget 설정&lt;br&gt;   ↓ 하이라이트 on/off&lt;/p&gt;
&lt;p&gt;[입력 발생]&lt;br&gt;   ↓ 마우스 클릭&lt;br&gt;OnLeftClick/RightClickInteract()&lt;br&gt;   ↓ 서버 RPC&lt;br&gt;Server_RequestLeftClick/RightClickInteract()&lt;br&gt;   ↓ 서버에서 검증&lt;br&gt;   ↓ 인터페이스 함수 호출&lt;br&gt;Interactable-&amp;gt;LeftClick/RightClickInteract(this)&lt;br&gt;   ↓&lt;br&gt;[타겟 액터가 자신의 로직 실행]&lt;/p&gt;
&lt;/aside&gt;

&lt;h2&gt;에디터 테스트 방법&lt;/h2&gt;
&lt;aside&gt;

&lt;p&gt;Interact 가능한 Object용  Object 채널과 콜리전 프리셋 을 생성한다. &lt;/p&gt;
&lt;p&gt;&lt;code&gt;프로젝트 세팅 &amp;gt; 콜리전에서 설정 가능&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;  &lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IxvTH/dJMcaaX2tOp/DbiTq0f3UKUMxdlOLfdWlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IxvTH/dJMcaaX2tOp/DbiTq0f3UKUMxdlOLfdWlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IxvTH/dJMcaaX2tOp/DbiTq0f3UKUMxdlOLfdWlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIxvTH%2FdJMcaaX2tOp%2FDbiTq0f3UKUMxdlOLfdWlK%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Si1xs/dJMcaiBIJ84/om7i6v8wX8q4BgJEUtlciK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Si1xs/dJMcaiBIJ84/om7i6v8wX8q4BgJEUtlciK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Si1xs/dJMcaiBIJ84/om7i6v8wX8q4BgJEUtlciK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSi1xs%2FdJMcaiBIJ84%2Fom7i6v8wX8q4BgJEUtlciK%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwBSib/dJMcaiBIJ85/hAMQqidtX5nKZMVIQajbQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwBSib/dJMcaiBIJ85/hAMQqidtX5nKZMVIQajbQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwBSib/dJMcaiBIJ85/hAMQqidtX5nKZMVIQajbQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwBSib%2FdJMcaiBIJ85%2FhAMQqidtX5nKZMVIQajbQk%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;테스트용 Interact 인터페이스를 상속받은 TestInteractObject 블루프린트 액터의 콜리전을 위처럼 Interactible로 설정해준다.&lt;/p&gt;
&lt;p&gt;이후 TestInteractObject.cpp 에서 LeftClick/RightClickInteract() 함수들을 각각 맞게 오버라이딩 해주면 된다.&lt;/p&gt;
&lt;/aside&gt;



&lt;pre&gt;&lt;code&gt;
void ALNCharacter::LeftClickInteract(AActor* Interactor)
{
    // 서버에서만 실행
    if (!HasAuthority())
    {
        UE_LOG(LogTemplateCharacter, Warning, TEXT(&amp;quot;LeftClickInteract called on client&amp;quot;));
        return;
    }

    if (!Interactor)
    {
        UE_LOG(LogTemplateCharacter, Warning, TEXT(&amp;quot;Interactor is null&amp;quot;));
        return;
    }

    // 캐릭터를 잡는 로직
    ALNCharacter* InteractorChar = Cast&amp;lt;ALNCharacter&amp;gt;(Interactor);
    if (!InteractorChar || !InteractorChar-&amp;gt;GrabComponent)
    {
        UE_LOG(LogTemplateCharacter, Warning, TEXT(&amp;quot;Invalid Interactor or GrabComponent&amp;quot;));
        return;
    }

    // GrabComponent를 통해 잡기 시도
    InteractorChar-&amp;gt;GrabComponent-&amp;gt;HandleGrabRequest(this);

    UE_LOG(LogTemplateCharacter, Log, TEXT(&amp;quot;[Server] %s grabbed by %s&amp;quot;),
           *GetName(), *Interactor-&amp;gt;GetName());
}

void ALNCharacter::RightClickInteract(AActor* Interactor)
{
    // 서버에서만 실행
    if (!HasAuthority())
    {
        UE_LOG(LogTemplateCharacter, Warning, TEXT(&amp;quot;RightClickInteract called on client&amp;quot;));
        return;
    }

    if (!Interactor || !GetCharacterMovement())
    {
        return;
    }

    ALNCharacter* InteractorChar = Cast&amp;lt;ALNCharacter&amp;gt;(Interactor);
    if (!InteractorChar)
    {
        return;
    }

    // 상호작용시전한 캐릭터의 밀기 애니메이션 동작
    InteractorChar-&amp;gt;Server_PlayMontage(InteractorChar-&amp;gt;CharacterPushMontage, InteractorChar-&amp;gt;MontagePlayRate);

    // 밀치는 방향 계산
    FVector LaunchDirection = InteractorChar-&amp;gt;GetActorForwardVector().GetSafeNormal();
    FVector LaunchVelocity = LaunchDirection * 1000.0f + FVector(0, 0, 300.0f);

    // 캐릭터를 날림
    LaunchCharacter(LaunchVelocity, true, true);

    UE_LOG(LogTemplateCharacter, Log, TEXT(&amp;quot;[Server] %s pushed by %s&amp;quot;),
           *GetName(), *Interactor-&amp;gt;GetName());
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt; 기존 캐릭터 상호작용 로직을 인터페이스를 사용하여 구현하였다.&lt;/p&gt;</description>
      <category>언리얼 팀프로젝트</category>
      <author>Turtle_Jun</author>
      <guid isPermaLink="true">https://unrealstudyhome.tistory.com/111</guid>
      <comments>https://unrealstudyhome.tistory.com/111#entry111comment</comments>
      <pubDate>Thu, 27 Nov 2025 21:40:10 +0900</pubDate>
    </item>
    <item>
      <title>캐릭터 상호작용 자료 서칭</title>
      <link>https://unrealstudyhome.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=IwujtxySCRE&amp;amp;t=2s&quot;&gt;How To Pick Up And Drag Ragdoll Bodies | Unreal Engine 5 Tutorial&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=IwujtxySCRE&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/sGnJV/hyZNYN1ugP/0i0F0Zek4tbGMxNE0zdeD1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/cbos5g/hyZNPXQn4g/QNQL2qJWTaxWqM6MEo5Gsk/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;How To Pick Up And Drag Ragdoll Bodies | Unreal Engine 5 Tutorial&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/IwujtxySCRE&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;캐릭터 상호작용으로 다른 플레이어를 잡았을때 끌고 올수 있는 방법에 대한 자료 서칭을 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Physics Constrain을 사용해서 캐릭터를 잡아 움직일 수 있도록 하면 될거같은데 위 영상도 참고하여 구현을 어떻게 할지 구상해보았다.&lt;/p&gt;</description>
      <category>언리얼 팀프로젝트</category>
      <author>Turtle_Jun</author>
      <guid isPermaLink="true">https://unrealstudyhome.tistory.com/110</guid>
      <comments>https://unrealstudyhome.tistory.com/110#entry110comment</comments>
      <pubDate>Fri, 14 Nov 2025 22:02:05 +0900</pubDate>
    </item>
    <item>
      <title>최종 팀 프로젝트 Interact 인터페이스 구현</title>
      <link>https://unrealstudyhome.tistory.com/109</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐릭터 Mouse R/L Click에 따라 상호작용을 할 수 있도록 하며 다형성을 위해 인터페이스로 구현을 해뒀다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐릭터에 맞는 Interact는 구현이 완료 되는 대로 글을 작성할 예정이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IqBXd/dJMcacan7C3/8uagRECI1dnDObGoRsrlsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IqBXd/dJMcacan7C3/8uagRECI1dnDObGoRsrlsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IqBXd/dJMcacan7C3/8uagRECI1dnDObGoRsrlsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIqBXd%2FdJMcacan7C3%2F8uagRECI1dnDObGoRsrlsK%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;722&quot; height=&quot;864&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;864&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>언리얼 팀프로젝트</category>
      <author>Turtle_Jun</author>
      <guid isPermaLink="true">https://unrealstudyhome.tistory.com/109</guid>
      <comments>https://unrealstudyhome.tistory.com/109#entry109comment</comments>
      <pubDate>Thu, 13 Nov 2025 22:26:30 +0900</pubDate>
    </item>
    <item>
      <title>최종 팀 프로젝트 Interact  인터페이스 및 Pickup 로직 수정 기획</title>
      <link>https://unrealstudyhome.tistory.com/108</link>
      <description>&lt;ul style=&quot;list-style-type: disc;&quot; data-pm-slice=&quot;0 0 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;애니메이션 및 동작 구현
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Interact, Pickup, 밀치기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;기존 Pikcup 로직 -&amp;gt; Component로 구현하기&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Interact : IInteractable 인터페이스 구현 후&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Character에서 Interact 로직 구현 &lt;a href=&quot;https://www.figma.com/board/WjUNGkBjPmes2o1owV4uaH/FigJam%EC%97%90-%EC%98%A4%EC%8B%A0-%EA%B2%83%EC%9D%84-%ED%99%98%EC%98%81%ED%95%A9%EB%8B%88%EB%8B%A4?node-id=0-1&amp;amp;p=f&amp;amp;t=InNcYWN8cqRCjyty-0&quot; data-prosemirror-mark-name=&quot;link&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;Figma&lt;/a&gt;참고&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x6glW/dJMcagKCmNC/x8I1JxkUH7suYDQCo4XsZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x6glW/dJMcagKCmNC/x8I1JxkUH7suYDQCo4XsZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x6glW/dJMcagKCmNC/x8I1JxkUH7suYDQCo4XsZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx6glW%2FdJMcagKCmNC%2Fx8I1JxkUH7suYDQCo4XsZk%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;592&quot; height=&quot;530&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;437&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBfh3K/dJMcahpd2Ye/Wy5OTuu9eu9x4YEUUVDWP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBfh3K/dJMcahpd2Ye/Wy5OTuu9eu9x4YEUUVDWP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBfh3K/dJMcahpd2Ye/Wy5OTuu9eu9x4YEUUVDWP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBfh3K%2FdJMcahpd2Ye%2FWy5OTuu9eu9x4YEUUVDWP0%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;794&quot; height=&quot;437&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;437&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>언리얼 팀프로젝트</category>
      <author>Turtle_Jun</author>
      <guid isPermaLink="true">https://unrealstudyhome.tistory.com/108</guid>
      <comments>https://unrealstudyhome.tistory.com/108#entry108comment</comments>
      <pubDate>Wed, 12 Nov 2025 22:55:04 +0900</pubDate>
    </item>
    <item>
      <title>PickUp 리플리케이션 및 콜리전 경고 메세지 문제 해결</title>
      <link>https://unrealstudyhome.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://unrealstudyhome.tistory.com/106&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2025.11.10 - [트러블 슈팅] - PickUp 기능&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762871090319&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;PickUp 기능&quot; data-og-description=&quot;트러블 슈팅픽업 시 들고있는 오브젝트가 리플리케이션 안되는 문제콜리전 프리셋을 에디터에서 수정시 픽업이 되지 않는 문제해결 방안 모색 콜리전 프로파일 채널을 추가하고 리플리케이션 &quot; data-og-host=&quot;unrealstudyhome.tistory.com&quot; data-og-source-url=&quot;https://unrealstudyhome.tistory.com/106&quot; data-og-url=&quot;https://unrealstudyhome.tistory.com/106&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b883Ie/hyZMIeHhOS/xlMocTc8Tfr1eWb8VQ41wK/img.png?width=800&amp;amp;height=351&amp;amp;face=0_0_800_351,https://scrap.kakaocdn.net/dn/daV9PS/hyZMEi4egG/mTfmsUYvjXWFTadNE8zUf0/img.png?width=800&amp;amp;height=351&amp;amp;face=0_0_800_351,https://scrap.kakaocdn.net/dn/rvzaw/hyZMFbc578/WGSjYLpev7xPO7qzKymWbk/img.png?width=1412&amp;amp;height=620&amp;amp;face=0_0_1412_620&quot;&gt;&lt;a href=&quot;https://unrealstudyhome.tistory.com/106&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://unrealstudyhome.tistory.com/106&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b883Ie/hyZMIeHhOS/xlMocTc8Tfr1eWb8VQ41wK/img.png?width=800&amp;amp;height=351&amp;amp;face=0_0_800_351,https://scrap.kakaocdn.net/dn/daV9PS/hyZMEi4egG/mTfmsUYvjXWFTadNE8zUf0/img.png?width=800&amp;amp;height=351&amp;amp;face=0_0_800_351,https://scrap.kakaocdn.net/dn/rvzaw/hyZMFbc578/WGSjYLpev7xPO7qzKymWbk/img.png?width=1412&amp;amp;height=620&amp;amp;face=0_0_1412_620');&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;PickUp 기능&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;트러블 슈팅픽업 시 들고있는 오브젝트가 리플리케이션 안되는 문제콜리전 프리셋을 에디터에서 수정시 픽업이 되지 않는 문제해결 방안 모색 콜리전 프로파일 채널을 추가하고 리플리케이션&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;unrealstudyhome.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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762872038132&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LNCharacter.h

#pragma region PickupSystem
	
protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = &quot;Pickup&quot;)
	UPhysicsHandleComponent* PhysicsHandle;

	UPROPERTY(ReplicatedUsing = OnRep_CarriedObject)
	TObjectPtr&amp;lt;AActor&amp;gt; CarriedObject;	// 잡고있는 오브젝트

	// 이전에 들고 있던 물체 추적용
	TObjectPtr&amp;lt;AActor&amp;gt; LastCarriedObject;

	UPROPERTY(EditDefaultsOnly, Category = &quot;Pickup&quot;)
	FName PickupSocketName = TEXT(&quot;PickupSocket&quot;);
	
	UPROPERTY(EditDefaultsOnly, Category = &quot;Pickup&quot;)
	float PickupDistance = 70.f;

	UPROPERTY(EditDefaultsOnly, Category = &quot;Pickup&quot;)
	FVector PickupOffset = FVector(0.f, 0.f, 0.f);

	UPROPERTY(EditDefaultsOnly, Category = &quot;Pickup&quot;)
	float PickupTraceRadius = 25.f;

	ECollisionEnabled::Type HeldItemCollisionType;	// 잡고있는 오브젝트의 콜리전 타입 저장
	
	void PickupObject(AActor* TargetActor);
	void ReleasePickupObject();

	// 실제 물리적 Attach/Detach 로직(서버와 클라 모두 실행) 
	void AttachCarriedObject();
	void DetachCarriedObject();
	
	FTimerHandle PickupUpdateTimerHandle;
	
#pragma endregion PickupSystem&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1762871162347&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LNCharacter.cpp

void ALNCharacter::HandlePickup(const FInputActionValue&amp;amp; Value)
{
	UE_LOG(LogTemplateCharacter, Warning, TEXT(&quot;PickupHandle&quot;));

	// 들고있다면 내려놓기
	if (IsValid(CarriedObject))
	{
		if (!HasAuthority())
		{
			ServerRPC_HandlePickup(CarriedObject, false);
		}
		else
		{
			ReleasePickupObject();
		}

		return;	// 내려놓고 종료
	}

	// 들고 있지 않다면 -&amp;gt; 전방 트레이스로 물체 찾기
	const FVector Start = GetActorLocation();
	const FVector End = Start + GetActorForwardVector() * PickupDistance;
	
	FHitResult Hit;
	FCollisionQueryParams Params;
	Params.AddIgnoredActor(this);
	float PicupTraceCapsuleHalfHeight = this-&amp;gt;GetCapsuleComponent()-&amp;gt;GetScaledCapsuleHalfHeight();

	
	bool bHit = GetWorld()-&amp;gt;SweepSingleByObjectType(
		Hit,
		Start,
		End,
		FQuat::Identity,
		ECC_PhysicsBody,
		FCollisionShape::MakeCapsule(PickupTraceRadius, PicupTraceCapsuleHalfHeight),
		Params
	);

	DrawDebugCapsule(GetWorld(), (Start + End) * 0.5f, PicupTraceCapsuleHalfHeight ,PickupTraceRadius, FQuat::Identity,
		bHit ? FColor::Green : FColor::Red, false, 1.0f, 0, 1.5f);
	
	if (bHit &amp;amp;&amp;amp; Hit.GetComponent() &amp;amp;&amp;amp; Hit.GetComponent()-&amp;gt;IsSimulatingPhysics())
	{
		AActor* HitActor = Hit.GetActor();
		if (HitActor)
		{
			if (!HasAuthority())
			{
				ServerRPC_HandlePickup(HitActor, true);
			}
			else
			{
				PickupObject(HitActor);
			}
		}
	}
}

void ALNCharacter::PickupObject(AActor* TargetActor)
{
	if (!TargetActor || !HasAuthority())	return;
	
	// 서버에서만 CarriedObject 설정 (이것이 복제되면서 OnRep이 호출됨)
	CarriedObject = TargetActor;
	bIsCarry = true;

	// 서버에서도 실제 Attach 수행
	AttachCarriedObject();
	
	UE_LOG(LogTemplateCharacter, Log, TEXT(&quot;[Server] PickupObject: %s&quot;), *TargetActor-&amp;gt;GetName());
}

void ALNCharacter::ReleasePickupObject()
{
	if (!CarriedObject || !HasAuthority())	return;

	// 서버에서도 실제 Detach 수행
	DetachCarriedObject();

	// CarriedObject를 nullptr로 설정 (이것이 복제되면서 OnRep이 호출됨)
	CarriedObject = nullptr;
	bIsCarry = false;

	UE_LOG(LogTemplateCharacter, Log, TEXT(&quot;[Server] ReleasePickupObject&quot;));
}

// OnRep 함수: CarriedObject가 변경될 때 클라이언트에서 호출됨
void ALNCharacter::OnRep_CarriedObject()
{
	// 이전에 들고 있던 물체가 있으면 먼저 Detach
	if (LastCarriedObject &amp;amp;&amp;amp; !CarriedObject)
	{
		DetachCarriedObject();
		UE_LOG(LogTemplateCharacter, Log, TEXT(&quot;[Client] OnRep_CarriedObject: Detached %s&quot;), *LastCarriedObject-&amp;gt;GetName());
	}
	
	// 새로 들 물체가 있으면 Attach
	if (CarriedObject)
	{
		AttachCarriedObject();
		UE_LOG(LogTemplateCharacter, Log, TEXT(&quot;[Client] OnRep_CarriedObject: Attached %s&quot;), *CarriedObject-&amp;gt;GetName());
	}
	
	// 현재 값을 LastCarriedObject에 저장
	LastCarriedObject = CarriedObject;
}

// 실제 물리적 Attach 로직 (서버와 클라이언트 모두에서 실행)
void ALNCharacter::AttachCarriedObject()
{
	if (!CarriedObject)	return;

	UPrimitiveComponent* PrimComp = Cast&amp;lt;UPrimitiveComponent&amp;gt;(CarriedObject-&amp;gt;GetRootComponent());
	
	if (PrimComp &amp;amp;&amp;amp; PrimComp-&amp;gt;IsSimulatingPhysics())
	{
		// 캐릭터와 충돌 방지
		HeldItemCollisionType = PrimComp-&amp;gt;GetCollisionEnabled();
		PrimComp-&amp;gt;IgnoreActorWhenMoving(this, true);
		PrimComp-&amp;gt;SetSimulatePhysics(false);
		PrimComp-&amp;gt;SetEnableGravity(false);
        PrimComp-&amp;gt;SetCollisionEnabled(ECollisionEnabled::NoCollision);

		// 들고있을 오브젝트 크기 기반 거리 계산
		FVector Origin, Extent;
		CarriedObject-&amp;gt;GetActorBounds(true, Origin, Extent);
		float ObjectSize = Extent.Size();
		float Distance = FMath::Clamp(ObjectSize * 0.5f, 40.f, 120.f);

		FVector SocketLocation = GetMesh()-&amp;gt;GetSocketLocation(&quot;PickupSocket&quot;);
		FVector AttachLocation = SocketLocation + GetActorForwardVector() * Distance;

		CarriedObject-&amp;gt;SetActorLocation(AttachLocation);
		CarriedObject-&amp;gt;AttachToComponent(
			GetMesh(),
			FAttachmentTransformRules::KeepWorldTransform,
			TEXT(&quot;PickupSocket&quot;)
		);
		
		UE_LOG(LogTemplateCharacter, Log, TEXT(&quot;[%s] AttachCarriedObject: %s&quot;), 
			HasAuthority() ? TEXT(&quot;Server&quot;) : TEXT(&quot;Client&quot;), 
			*CarriedObject-&amp;gt;GetName());
	}
}

// 실제 물리적 Detach 로직 (서버와 클라이언트 모두에서 실행)
void ALNCharacter::DetachCarriedObject()
{
	// LastCarriedObject를 사용 (OnRep에서 설정됨)
	AActor* ObjectToDetach = LastCarriedObject ? LastCarriedObject : CarriedObject;

	if (!ObjectToDetach)	return;

	if (UPrimitiveComponent* PrimComp = Cast&amp;lt;UPrimitiveComponent&amp;gt;(ObjectToDetach-&amp;gt;GetRootComponent()))
	{
		PrimComp-&amp;gt;SetCollisionEnabled(HeldItemCollisionType);
		PrimComp-&amp;gt;IgnoreActorWhenMoving(this, false);
		PrimComp-&amp;gt;SetSimulatePhysics(true);
		PrimComp-&amp;gt;SetEnableGravity(true);
	}

	ObjectToDetach-&amp;gt;DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
	ObjectToDetach-&amp;gt;SetOwner(nullptr);

	UE_LOG(LogTemplateCharacter, Log, TEXT(&quot;[%s] DetachCarriedObject: %s&quot;), 
		HasAuthority() ? TEXT(&quot;Server&quot;) : TEXT(&quot;Client&quot;),
		*ObjectToDetach-&amp;gt;GetName());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오브젝트가 Pickup 되고난 후 클라이언트에 복제가 되지 않은 이유론 클라이언트에서 CarriedObject 가 복제는 되나 Attach/Detach가 서버에서만 실행되는 문제가 있었음.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;543&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R2TJA/dJMcagDQwc4/8Zl7hjThv653mQrJKTHDSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R2TJA/dJMcagDQwc4/8Zl7hjThv653mQrJKTHDSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R2TJA/dJMcagDQwc4/8Zl7hjThv653mQrJKTHDSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR2TJA%2FdJMcagDQwc4%2F8Zl7hjThv653mQrJKTHDSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;543&quot; height=&quot;461&quot; data-origin-width=&quot;543&quot; data-origin-height=&quot;461&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 OnRep 함수를 위처럼 구현하여 클라이언트에서도 CarriedObject가 Attach/Detach되도록 구현하였다.&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;824&quot; data-origin-height=&quot;329&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beCt2T/dJMb99SfXrF/CUHlhMk6hK5aKwlFHQvC7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beCt2T/dJMb99SfXrF/CUHlhMk6hK5aKwlFHQvC7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beCt2T/dJMb99SfXrF/CUHlhMk6hK5aKwlFHQvC7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeCt2T%2FdJMb99SfXrF%2FCUHlhMk6hK5aKwlFHQvC7k%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;824&quot; height=&quot;329&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;329&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜리전 설정 순서를 Nocollision 으로 설정 후 Physics 설정을 수정하여서 위처럼 경고 메세지가 출력되었었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQ8hQ0/dJMcaacyF6e/lNztK26o1uHwuqH2rkcNc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQ8hQ0/dJMcaacyF6e/lNztK26o1uHwuqH2rkcNc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQ8hQ0/dJMcaacyF6e/lNztK26o1uHwuqH2rkcNc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQ8hQ0%2FdJMcaacyF6e%2FlNztK26o1uHwuqH2rkcNc1%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;752&quot; height=&quot;260&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nocollision 설정하는 순서를 위처럼 변경하여주면 해결된다!.&lt;/p&gt;</description>
      <category>트러블 슈팅</category>
      <author>Turtle_Jun</author>
      <guid isPermaLink="true">https://unrealstudyhome.tistory.com/107</guid>
      <comments>https://unrealstudyhome.tistory.com/107#entry107comment</comments>
      <pubDate>Tue, 11 Nov 2025 23:44:09 +0900</pubDate>
    </item>
    <item>
      <title>PickUp 기능</title>
      <link>https://unrealstudyhome.tistory.com/106</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (2).png&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBUFFW/dJMb99SfyyA/VRENZWi33N0D7Bkopoiyf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBUFFW/dJMb99SfyyA/VRENZWi33N0D7Bkopoiyf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBUFFW/dJMb99SfyyA/VRENZWi33N0D7Bkopoiyf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBUFFW%2FdJMb99SfyyA%2FVRENZWi33N0D7Bkopoiyf0%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;1412&quot; height=&quot;620&quot; data-filename=&quot;image (2).png&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;620&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-filename=&quot;image (1).png&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fj6VS/dJMb99SfyyB/ztq0O0FYEwMmZ3kRFF4xk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fj6VS/dJMb99SfyyB/ztq0O0FYEwMmZ3kRFF4xk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fj6VS/dJMb99SfyyB/ztq0O0FYEwMmZ3kRFF4xk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFj6VS%2FdJMb99SfyyB%2Fztq0O0FYEwMmZ3kRFF4xk0%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;1108&quot; height=&quot;645&quot; data-filename=&quot;image (1).png&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트러블 슈팅&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;콜리전 프리셋을 에디터에서 수정시 픽업이 되지 않는 문제&lt;/li&gt;
&lt;/ol&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>트러블 슈팅</category>
      <author>Turtle_Jun</author>
      <guid isPermaLink="true">https://unrealstudyhome.tistory.com/106</guid>
      <comments>https://unrealstudyhome.tistory.com/106#entry106comment</comments>
      <pubDate>Mon, 10 Nov 2025 21:17:00 +0900</pubDate>
    </item>
    <item>
      <title>최종 팀 프로젝트 Pickup 기능 구현 -2</title>
      <link>https://unrealstudyhome.tistory.com/105</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://unrealstudyhome.tistory.com/104&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2025.11.06 - [트러블 슈팅] - 최종 팀 프로젝트 Pickup 기능 구현&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762521356372&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;최종 팀 프로젝트 Pickup 기능 구현&quot; data-og-description=&quot;2025.11.05 - [언리얼 팀프로젝트] - 최종 팀 프로젝트 -캐릭터 모듈 컴포넌트 구현 최종 팀 프로젝트 -캐릭터 모듈 컴포넌트 구현2025.11.04 - [언리얼 팀프로젝트] - 최종 팀프로젝트 - 캐릭터 기본 애&quot; data-og-host=&quot;unrealstudyhome.tistory.com&quot; data-og-source-url=&quot;https://unrealstudyhome.tistory.com/104&quot; data-og-url=&quot;https://unrealstudyhome.tistory.com/104&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bOcSsb/hyZMyvWYgt/7Nz9aQHj86jfk36nSKRdqk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dtJHnS/hyZM3bTAoD/OapGiAoaPPQJWz7RDjrwJK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bqCBF5/hyZMAHiFpH/dRZCoEkAU2KccTu00qiAa1/img.png?width=635&amp;amp;height=539&amp;amp;face=0_0_635_539&quot;&gt;&lt;a href=&quot;https://unrealstudyhome.tistory.com/104&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://unrealstudyhome.tistory.com/104&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bOcSsb/hyZMyvWYgt/7Nz9aQHj86jfk36nSKRdqk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dtJHnS/hyZM3bTAoD/OapGiAoaPPQJWz7RDjrwJK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bqCBF5/hyZMAHiFpH/dRZCoEkAU2KccTu00qiAa1/img.png?width=635&amp;amp;height=539&amp;amp;face=0_0_635_539');&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;최종 팀 프로젝트 Pickup 기능 구현&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2025.11.05 - [언리얼 팀프로젝트] - 최종 팀 프로젝트 -캐릭터 모듈 컴포넌트 구현 최종 팀 프로젝트 -캐릭터 모듈 컴포넌트 구현2025.11.04 - [언리얼 팀프로젝트] - 최종 팀프로젝트 - 캐릭터 기본 애&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;unrealstudyhome.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;오늘은 PickUp 기능을 코드로 구현해보았는데 이전에 월드 다른 좌표에 붙는 문제는 해결하였으나 오브젝트가 붙는게 뭔가 자연스럽지 못한 문제가 있어서 수정 후 해당 글을 추가 하도록 할 예정&lt;/p&gt;</description>
      <category>언리얼 팀프로젝트</category>
      <author>Turtle_Jun</author>
      <guid isPermaLink="true">https://unrealstudyhome.tistory.com/105</guid>
      <comments>https://unrealstudyhome.tistory.com/105#entry105comment</comments>
      <pubDate>Fri, 7 Nov 2025 22:22:00 +0900</pubDate>
    </item>
    <item>
      <title>최종 팀 프로젝트 Pickup 기능 구현</title>
      <link>https://unrealstudyhome.tistory.com/104</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://unrealstudyhome.tistory.com/103&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2025.11.05 - [언리얼 팀프로젝트] - 최종 팀 프로젝트 -캐릭터 모듈 컴포넌트 구현&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762435156198&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;최종 팀 프로젝트 -캐릭터 모듈 컴포넌트 구현&quot; data-og-description=&quot;2025.11.04 - [언리얼 팀프로젝트] - 최종 팀프로젝트 - 캐릭터 기본 애니메이션 구현 최종 팀프로젝트 - 캐릭터 기본 애니메이션 구현2025.11.03 - [언리얼 팀프로젝트] - 최종 팀프로젝트 - 캐릭터 구현&quot; data-og-host=&quot;unrealstudyhome.tistory.com&quot; data-og-source-url=&quot;https://unrealstudyhome.tistory.com/103&quot; data-og-url=&quot;https://unrealstudyhome.tistory.com/103&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/baOUIZ/hyZMBF4TfH/bZKAvoktPk8VDUGCdRD68k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cwIL2m/hyZM9JAGDO/fdJFhgpfknckZXWq1qCOTK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/AO9YD/hyZMxXWxPn/p3s7CS3e6ejfMfbrjljWH0/img.png?width=635&amp;amp;height=539&amp;amp;face=0_0_635_539&quot;&gt;&lt;a href=&quot;https://unrealstudyhome.tistory.com/103&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://unrealstudyhome.tistory.com/103&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/baOUIZ/hyZMBF4TfH/bZKAvoktPk8VDUGCdRD68k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cwIL2m/hyZM9JAGDO/fdJFhgpfknckZXWq1qCOTK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/AO9YD/hyZMxXWxPn/p3s7CS3e6ejfMfbrjljWH0/img.png?width=635&amp;amp;height=539&amp;amp;face=0_0_635_539');&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;최종 팀 프로젝트 -캐릭터 모듈 컴포넌트 구현&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2025.11.04 - [언리얼 팀프로젝트] - 최종 팀프로젝트 - 캐릭터 기본 애니메이션 구현 최종 팀프로젝트 - 캐릭터 기본 애니메이션 구현2025.11.03 - [언리얼 팀프로젝트] - 최종 팀프로젝트 - 캐릭터 구현&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;unrealstudyhome.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;&lt;a href=&quot;https://www.youtube.com/watch?v=bCAlowYEYEI&amp;amp;list=LL&amp;amp;index=6&amp;amp;t=3s&quot;&gt;(1) 언리얼 엔진 5에서 물리적 객체를 운반하는 방법 - YouTube&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=bCAlowYEYEI&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bj7oBY/hyZMvsi3FS/khXd0KbJc8swXIcxMVJ9B1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/bAbuqw/hyZMH7mbhC/B8g6x7YIKtbWJjTbI0QB61/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;How to Carry Physical Objects in Unreal Engine 5&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/bCAlowYEYEI&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;위 영상을 참고하여 Pickup 기능을 구현해보았는데.. Pyshics Handle 에 붙여지는게 원하는곳에 안 붙는 버그가 있다.&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;416&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be0oAA/dJMcabic9ti/dokrJKgdM85tgT1DW6uy3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be0oAA/dJMcabic9ti/dokrJKgdM85tgT1DW6uy3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be0oAA/dJMcabic9ti/dokrJKgdM85tgT1DW6uy3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe0oAA%2FdJMcabic9ti%2FdokrJKgdM85tgT1DW6uy3K%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;416&quot; height=&quot;245&quot; data-origin-width=&quot;416&quot; data-origin-height=&quot;245&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;Handle 컴포넌트가 캐릭터 위치에 붙지 않아서 그런거같은데 Attach 해봐도 동일한 문제가 있어서 고민을 해봐야할 것 같다.&lt;/p&gt;</description>
      <category>트러블 슈팅</category>
      <author>Turtle_Jun</author>
      <guid isPermaLink="true">https://unrealstudyhome.tistory.com/104</guid>
      <comments>https://unrealstudyhome.tistory.com/104#entry104comment</comments>
      <pubDate>Thu, 6 Nov 2025 22:30:34 +0900</pubDate>
    </item>
    <item>
      <title>최종 팀 프로젝트 -캐릭터 모듈 컴포넌트 구현</title>
      <link>https://unrealstudyhome.tistory.com/103</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://unrealstudyhome.tistory.com/102&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2025.11.04 - [언리얼 팀프로젝트] - 최종 팀프로젝트 - 캐릭터 기본 애니메이션 구현&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762352724062&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;최종 팀프로젝트 - 캐릭터 기본 애니메이션 구현&quot; data-og-description=&quot;2025.11.03 - [언리얼 팀프로젝트] - 최종 팀프로젝트 - 캐릭터 구현 (이동/앉기/달리기) 최종 팀프로젝트 - 캐릭터 구현 (이동/앉기/달리기)캐릭터 이동/앉기/달리기를 구현해놓은 상태로 애님 블루&quot; data-og-host=&quot;unrealstudyhome.tistory.com&quot; data-og-source-url=&quot;https://unrealstudyhome.tistory.com/102&quot; data-og-url=&quot;https://unrealstudyhome.tistory.com/102&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3dLOI/hyZNe4ROeQ/CW9wtAKQDsfrpj4cfVPe91/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/JMBXz/hyZNewYfQF/8IaNgHMsserVXqAQ0hC79k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bbHtsU/hyZMEvSKJ0/tHZR5hii9jXl03vXj3C71K/img.png?width=1408&amp;amp;height=811&amp;amp;face=0_0_1408_811&quot;&gt;&lt;a href=&quot;https://unrealstudyhome.tistory.com/102&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://unrealstudyhome.tistory.com/102&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3dLOI/hyZNe4ROeQ/CW9wtAKQDsfrpj4cfVPe91/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/JMBXz/hyZNewYfQF/8IaNgHMsserVXqAQ0hC79k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bbHtsU/hyZMEvSKJ0/tHZR5hii9jXl03vXj3C71K/img.png?width=1408&amp;amp;height=811&amp;amp;face=0_0_1408_811');&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;최종 팀프로젝트 - 캐릭터 기본 애니메이션 구현&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2025.11.03 - [언리얼 팀프로젝트] - 최종 팀프로젝트 - 캐릭터 구현 (이동/앉기/달리기) 최종 팀프로젝트 - 캐릭터 구현 (이동/앉기/달리기)캐릭터 이동/앉기/달리기를 구현해놓은 상태로 애님 블루&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;unrealstudyhome.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;같은 팀원인 재근님의 이전 프로젝트에서 구현한 내용을 공유 받아 본 프로젝트 캐릭터와 연동을 하였다.&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;h4 data-ke-size=&quot;size20&quot;&gt;- CharacterPartType.h&lt;/h4&gt;
&lt;pre id=&quot;code_1762352786345&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;Net/Serialization/FastArraySerializer.h&quot;
#include &quot;CharacterPartType.generated.h&quot;

class UMJ_CharacterPartComponent;
class USkeletalMeshComponent;
class USkeletalMesh;


// 파츠 슬롯 정의 (모자, 상의, 하의, 몸통 등)
UENUM(BlueprintType)
enum class ECharacterPartSlot : uint8
{
	None,
	Top,	//	상의
	Dress,	//	원피스
	Hat,	//	모자
	Hair,	//	머리
	Gloves,	//	장갑
	Glasses,	//	안경
	Eyeballs,	//	눈
	Eyebrows,	//	눈썹
	Beard,	//	수염
	Shoes,	//	신발
	Bottoms,	//	하의
	Body,	//	몸 (피부 색)
	Bag	//	가방
};

/*
 * 단일 파츠 정보 구조체 (어떤 슬롯에 어떤 메시를 쓸지 결정)
 */
USTRUCT(BlueprintType)
struct FCharacterPart
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;Cosmetic&quot;)
	ECharacterPartSlot Slot = ECharacterPartSlot::None;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;Cosmetic&quot;)
	TObjectPtr&amp;lt;USkeletalMesh&amp;gt; Mesh = nullptr;
};

/*
 *	개별 적용된 파츠 배열의 개별 항목
 */
USTRUCT()
struct FAppliedCharacterPartEntry : public FFastArraySerializerItem
{
	GENERATED_BODY()

	// 적용된 파츠의 데이터(부위, 메시) / 네트워크 복제 ㅇ
	UPROPERTY()
	FCharacterPart Part;

	// 런타임에 생성된 스켈레탈 메쉬 컴포넌트 포인터	/ 복제 X
	UPROPERTY(NotReplicated)
	TObjectPtr&amp;lt;USkeletalMeshComponent&amp;gt; SpawnedComponent = nullptr;
};

// 캐릭터에 적용된 모든 파츠 목록을 관리하는 구조체 / 네트워크 최적화
USTRUCT()
struct FCharacterPartList : public FFastArraySerializer
{
	GENERATED_BODY()

public:
	FCharacterPartList() : OwnerComponent(nullptr)
	{
	}
	FCharacterPartList(UMJ_CharacterPartComponent* InOwnerComponent) : OwnerComponent(InOwnerComponent)
	{
	}

	void PreReplicateRemove(const TArrayView&amp;lt;int32&amp;gt; RemoveIndices, int32 FinalSize);
	void PostReplicatedAdd(const TArrayView&amp;lt;int32&amp;gt; AddIndices, int32 FinalSize);
	void PostReplicatedChange(const TArrayView&amp;lt;int32&amp;gt; ChangeIndices, int32 FinalSize);

	bool NetDeltaSerialize(FNetDeltaSerializeInfo&amp;amp; DeltaParms)
	{
		return FastArrayDeltaSerialize&amp;lt;FAppliedCharacterPartEntry, FCharacterPartList&amp;gt;(Entries,DeltaParms, *this);
	}

	// 스켈레탈 메쉬 컴포넌트 생성 후 부착하는 함수
	void SpawnMeshForEntry(FAppliedCharacterPartEntry&amp;amp; Entry);
	// 스켈레탈 메쉬 컴포넌트 제거하는 함수
	void DestroyMeshForEntry(FAppliedCharacterPartEntry&amp;amp; Entry);
	
	UPROPERTY()
	TArray&amp;lt;FAppliedCharacterPartEntry&amp;gt; Entries;

	UPROPERTY(NotReplicated)
	TObjectPtr&amp;lt;UMJ_CharacterPartComponent&amp;gt; OwnerComponent;
};

// 템플릿 특수화
template&amp;lt;&amp;gt;
struct TStructOpsTypeTraits&amp;lt;FCharacterPartList&amp;gt; : public TStructOpsTypeTraitsBase2&amp;lt;FCharacterPartList&amp;gt;
{
	enum {	WithNetDeltaSerializer = true	};
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;- CharacterPartType.cpp&lt;/h4&gt;
&lt;div style=&quot;background-color: #262626; color: #d0d0d0;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;#include &quot;CharacterPartType.h&quot;
#include &quot;MJ_CharacterPartComponent.h&quot;
#include &quot;Components/SkeletalMeshComponent.h&quot;

//  서버에서 파츠 제거되었다는 정보 도착시 클라이언트에서 호출
void FCharacterPartList::PreReplicateRemove(const TArrayView&amp;lt;int32&amp;gt; RemoveIndices, int32 FinalSize)
{
    for (const int32 Index : RemoveIndices)
    {
       FAppliedCharacterPartEntry&amp;amp; Entry = Entries[Index];
       DestroyMeshForEntry(Entry);
    }
}

//  서버에서 새로운 파츠가 추가 되었다는 정보 도착시 클라이언트에서 호출
void FCharacterPartList::PostReplicatedAdd(const TArrayView&amp;lt;int32&amp;gt; AddIndices, int32 FinalSize)
{
    for (const int32 Index : AddIndices)
    {
       FAppliedCharacterPartEntry&amp;amp; Entry = Entries[Index];
       SpawnMeshForEntry(Entry);
    }
}

//  서버에서 기존 파츠의 정보가 변경될 때 클라에서 호출
void FCharacterPartList::PostReplicatedChange(const TArrayView&amp;lt;int32&amp;gt; ChangeIndices, int32 FinalSize)
{
    for (const int32 Index : ChangeIndices)
    {
       FAppliedCharacterPartEntry&amp;amp; Entry = Entries[Index];
       DestroyMeshForEntry(Entry);
       SpawnMeshForEntry(Entry);
    }
}

//  파츠 데이터를 기반으로 실제 스켈레탈 메시 컴포넌트를 생성하고 캐릭터에 부착
void FCharacterPartList::SpawnMeshForEntry(FAppliedCharacterPartEntry&amp;amp; Entry)
{
    if (!IsValid(OwnerComponent) || !IsValid(Entry.Part.Mesh)) return;

    USkeletalMeshComponent* ParentMesh = OwnerComponent-&amp;gt;GetParentMeshComponent();
    if (!IsValid(ParentMesh)) return;  

    AActor* OwnerActor = OwnerComponent-&amp;gt;GetOwner();

    FName ComponentName = *FString::Printf(TEXT(&quot;CosmeticPart_%s&quot;), *UEnum::GetValueAsString(Entry.Part.Slot));
    USkeletalMeshComponent* NewPartComponent = NewObject&amp;lt;USkeletalMeshComponent&amp;gt;(OwnerActor, ComponentName);

    NewPartComponent-&amp;gt;SetupAttachment(ParentMesh);

    NewPartComponent-&amp;gt;SetSkeletalMesh(Entry.Part.Mesh);

    // 모든 파츠 컴포넌트가 부모(몸통)의 애니를 그대로 따라가도록 설정
    NewPartComponent-&amp;gt;SetLeaderPoseComponent(ParentMesh);

    NewPartComponent-&amp;gt;RegisterComponent();

    Entry.SpawnedComponent = NewPartComponent;
}

// 파츠 제거될때 생성한 스켈레탈 메쉬를 안전하게 제거
void FCharacterPartList::DestroyMeshForEntry(FAppliedCharacterPartEntry&amp;amp; Entry)
{
    if (IsValid(Entry.SpawnedComponent))
    {
       Entry.SpawnedComponent-&amp;gt;DestroyComponent();
       Entry.SpawnedComponent = nullptr;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- MJ_CharacterPartComponent.h&lt;/h4&gt;
&lt;pre id=&quot;code_1762352907249&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;CharacterPartType.h&quot;
#include &quot;MJ_CharacterPartComponent.generated.h&quot;


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class MIJUNG_API UMJ_CharacterPartComponent : public UActorComponent
{
	GENERATED_BODY()

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

	virtual void GetLifetimeReplicatedProps(TArray&amp;lt;FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const override;

	/*
	 * 지정된 파츠를 캐릭터에 장착하거나 교체 / 서버에서만 호출
	 * 장착할 파츠의 정보(부위, 메시)
	 */
	UFUNCTION(BlueprintCallable, Category = &quot;Cosmetic&quot;, meta = (CallInEditor = &quot;true&quot;))
	void SetCharacterPart(const FCharacterPart&amp;amp; NewPart);

	//	해당 컴포넌트가 제어해야할 주 스켈레탈 메쉬를 반환(몸통)
	// 해당 스켈레탈 메쉬를 기반으로 파츠들이 붙으며 애니도 동기화됨
	USkeletalMeshComponent* GetParentMeshComponent() const;
	
private:

	// 캐릭터에 적용된 모든 파츠 목록
	UPROPERTY(Replicated, Transient)
	FCharacterPartList AppliedCharacterPartList;
		
};&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;- MJ_CharacterPartComponent.cpp&lt;/h4&gt;
&lt;pre id=&quot;code_1762352975931&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;
#include &quot;Character/MJ_CharacterPartComponent.h&quot;

#include &quot;GameFramework/Character.h&quot;
#include &quot;Net/UnrealNetwork.h&quot;

UMJ_CharacterPartComponent::UMJ_CharacterPartComponent()
	: AppliedCharacterPartList(this)	
{
	SetIsReplicatedByDefault(true);
}

void UMJ_CharacterPartComponent::GetLifetimeReplicatedProps(TArray&amp;lt;FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(ThisClass, AppliedCharacterPartList);	//적용된 파츠들 네트워크 복제
}


void UMJ_CharacterPartComponent::SetCharacterPart(const FCharacterPart&amp;amp; NewPart)
{
	if (!GetOwner() || !GetOwner()-&amp;gt;HasAuthority())
		return;

	// 기존 슬롯 제거
	for (auto It = AppliedCharacterPartList.Entries.CreateIterator(); It; ++It)
	{
		if (It-&amp;gt;Part.Slot == NewPart.Slot)
		{
			AppliedCharacterPartList.DestroyMeshForEntry(*It);
			It.RemoveCurrent();
			AppliedCharacterPartList.MarkArrayDirty();
			break;
		}
	}

	if (!IsValid(NewPart.Mesh)) return;

	// 새 엔트리 추가
	FAppliedCharacterPartEntry&amp;amp; NewEntry = AppliedCharacterPartList.Entries.AddDefaulted_GetRef();
	NewEntry.Part = NewPart;
	AppliedCharacterPartList.SpawnMeshForEntry(NewEntry);
	AppliedCharacterPartList.MarkItemDirty(NewEntry);
}

USkeletalMeshComponent* UMJ_CharacterPartComponent::GetParentMeshComponent() const
{
	if (ACharacter* Char = Cast&amp;lt;ACharacter&amp;gt;(GetOwner()))
	{
		return Char-&amp;gt;GetMesh();
	}
	return nullptr;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 캐릭터 코드에서 위 컴포넌트를 사용하기 위해 아래처럼 코드 내용을 추가하면 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- MJCharacter.h&lt;/h4&gt;
&lt;pre id=&quot;code_1762353120055&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MJCharacter.h

#pragma once

#include &quot;CoreMinimal.h&quot;
#include &quot;GameFramework/Character.h&quot;
#include &quot;CharacterPartComponent.h&quot; // 추가
#include &quot;CharacterPartType.h&quot;      // 추가
#include &quot;MJCharacter.generated.h&quot;

class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
class UAnimMontage;
struct FInputActionValue;

DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);

UCLASS()
class MIJUNG_API AMJCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	AMJCharacter();

protected:

	//-----------------------------------------------------
	// ✅ 코스메틱 시스템
	//-----------------------------------------------------
public:
	// 캐릭터 외형 파츠 관리용 컴포넌트
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = &quot;Cosmetic&quot;)
	UCharacterPartComponent* CharacterPartComponent;

	// 서버에서 새로운 파츠 장착 요청을 처리 (리슨 서버용)
	UFUNCTION(Server, Reliable)
	void Server_EquipPart(const FCharacterPart&amp;amp; NewPart);

	// 블루프린트 또는 UI에서 호출 (클라이언트에서 실행)
	UFUNCTION(BlueprintCallable, Category = &quot;Cosmetic&quot;)
	void EquipPart(const FCharacterPart&amp;amp; NewPart);

	//-----------------------------------------------------
	// 기존 캐릭터 관련 변수/함수
	//-----------------------------------------------------


	// ... (기존 입력 액션들 그대로 유지)
	

};&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;- MJCharacter.cpp&lt;/h4&gt;
&lt;pre id=&quot;code_1762353159914&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MJCharacter.cpp

#include &quot;Character/MJCharacter.h&quot;
#include &quot;CharacterPartComponent.h&quot;
#include &quot;CharacterPartType.h&quot;
#include &quot;Camera/CameraComponent.h&quot;
#include &quot;GameFramework/SpringArmComponent.h&quot;
#include &quot;GameFramework/CharacterMovementComponent.h&quot;
#include &quot;EnhancedInputComponent.h&quot;
#include &quot;EnhancedInputSubsystems.h&quot;
#include &quot;Net/UnrealNetwork.h&quot;

DEFINE_LOG_CATEGORY(LogTemplateCharacter);

AMJCharacter::AMJCharacter()
{
	bReplicates = true;
	GetCharacterMovement()-&amp;gt;SetIsReplicated(true);

	// ✅ 코스메틱 시스템 컴포넌트 생성
	CharacterPartComponent = CreateDefaultSubobject&amp;lt;UCharacterPartComponent&amp;gt;(TEXT(&quot;CharacterPartComponent&quot;));
	AddOwnedComponent(CharacterPartComponent);

	// ✅ 리슨서버/클라 환경에서만 카메라 생성
	if (!IsRunningDedicatedServer())
	{
		CameraArm = CreateDefaultSubobject&amp;lt;USpringArmComponent&amp;gt;(TEXT(&quot;CameraArm&quot;));
		CameraArm-&amp;gt;SetupAttachment(RootComponent);
		CameraArm-&amp;gt;TargetArmLength = 400.0f;
		CameraArm-&amp;gt;bUsePawnControlRotation = true;

		FollowCamera = CreateDefaultSubobject&amp;lt;UCameraComponent&amp;gt;(TEXT(&quot;FollowCamera&quot;));
		FollowCamera-&amp;gt;SetupAttachment(CameraArm, USpringArmComponent::SocketName);
		FollowCamera-&amp;gt;bUsePawnControlRotation = false;
	}

	bIsSprinting = false;
}

void AMJCharacter::BeginPlay()
{
	Super::BeginPlay();

	// ✅ 초기 외형 세팅 (서버에서만)
	if (HasAuthority() &amp;amp;&amp;amp; CharacterPartComponent)
	{
		FCharacterPart BaseBody;
		BaseBody.Slot = ECharacterPartSlot::Body;
		BaseBody.Mesh = LoadObject&amp;lt;USkeletalMesh&amp;gt;(nullptr, TEXT(&quot;/Game/Characters/Parts/Meshes/SK_Body_Default.SK_Body_Default&quot;));
		if (BaseBody.Mesh)
		{
			CharacterPartComponent-&amp;gt;SetCharacterPart(BaseBody);
		}
	}
}

void AMJCharacter::EquipPart(const FCharacterPart&amp;amp; NewPart)
{
	// 클라이언트 &amp;rarr; 서버로 요청
	if (!HasAuthority())
	{
		Server_EquipPart(NewPart);
	}
	else
	{
		if (CharacterPartComponent)
		{
			CharacterPartComponent-&amp;gt;SetCharacterPart(NewPart);
		}
	}
}

void AMJCharacter::Server_EquipPart_Implementation(const FCharacterPart&amp;amp; NewPart)
{
	if (!IsValid(CharacterPartComponent)) return;
	CharacterPartComponent-&amp;gt;SetCharacterPart(NewPart);
}

void AMJCharacter::GetLifetimeReplicatedProps(TArray&amp;lt;FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(ThisClass, bIsSprinting);
}

void AMJCharacter::Move(const FInputActionValue&amp;amp; Value)
{
	// 기존 코드 그대로 유지
}

void AMJCharacter::Look(const FInputActionValue&amp;amp; Value)
{
	// 기존 코드 그대로 유지
}

// 나머지 기존 Sprint, Crouch, Interact 로직 그대로 유지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>언리얼 팀프로젝트</category>
      <author>Turtle_Jun</author>
      <guid isPermaLink="true">https://unrealstudyhome.tistory.com/103</guid>
      <comments>https://unrealstudyhome.tistory.com/103#entry103comment</comments>
      <pubDate>Wed, 5 Nov 2025 23:29:51 +0900</pubDate>
    </item>
  </channel>
</rss>