언리얼 팀프로젝트

최종 팀 프로젝트 -캐릭터 모듈 컴포넌트 구현

Turtle_Jun 2025. 11. 5. 23:29

2025.11.04 - [언리얼 팀프로젝트] - 최종 팀프로젝트 - 캐릭터 기본 애니메이션 구현

 

최종 팀프로젝트 - 캐릭터 기본 애니메이션 구현

2025.11.03 - [언리얼 팀프로젝트] - 최종 팀프로젝트 - 캐릭터 구현 (이동/앉기/달리기) 최종 팀프로젝트 - 캐릭터 구현 (이동/앉기/달리기)캐릭터 이동/앉기/달리기를 구현해놓은 상태로 애님 블루

unrealstudyhome.tistory.com

 

같은 팀원인 재근님의 이전 프로젝트에서 구현한 내용을 공유 받아 본 프로젝트 캐릭터와 연동을 하였다.

 

 

- CharacterPartType.h

#pragma once

#include "CoreMinimal.h"
#include "Net/Serialization/FastArraySerializer.h"
#include "CharacterPartType.generated.h"

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 = "Cosmetic")
	ECharacterPartSlot Slot = ECharacterPartSlot::None;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cosmetic")
	TObjectPtr<USkeletalMesh> Mesh = nullptr;
};

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

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

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

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

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

	void PreReplicateRemove(const TArrayView<int32> RemoveIndices, int32 FinalSize);
	void PostReplicatedAdd(const TArrayView<int32> AddIndices, int32 FinalSize);
	void PostReplicatedChange(const TArrayView<int32> ChangeIndices, int32 FinalSize);

	bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
	{
		return FastArrayDeltaSerialize<FAppliedCharacterPartEntry, FCharacterPartList>(Entries,DeltaParms, *this);
	}

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

	UPROPERTY(NotReplicated)
	TObjectPtr<UMJ_CharacterPartComponent> OwnerComponent;
};

// 템플릿 특수화
template<>
struct TStructOpsTypeTraits<FCharacterPartList> : public TStructOpsTypeTraitsBase2<FCharacterPartList>
{
	enum {	WithNetDeltaSerializer = true	};
};

 

 

 

- CharacterPartType.cpp

#include "CharacterPartType.h"
#include "MJ_CharacterPartComponent.h"
#include "Components/SkeletalMeshComponent.h"

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

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

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

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

    USkeletalMeshComponent* ParentMesh = OwnerComponent->GetParentMeshComponent();
    if (!IsValid(ParentMesh)) return;  

    AActor* OwnerActor = OwnerComponent->GetOwner();

    FName ComponentName = *FString::Printf(TEXT("CosmeticPart_%s"), *UEnum::GetValueAsString(Entry.Part.Slot));
    USkeletalMeshComponent* NewPartComponent = NewObject<USkeletalMeshComponent>(OwnerActor, ComponentName);

    NewPartComponent->SetupAttachment(ParentMesh);

    NewPartComponent->SetSkeletalMesh(Entry.Part.Mesh);

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

    NewPartComponent->RegisterComponent();

    Entry.SpawnedComponent = NewPartComponent;
}

// 파츠 제거될때 생성한 스켈레탈 메쉬를 안전하게 제거
void FCharacterPartList::DestroyMeshForEntry(FAppliedCharacterPartEntry& Entry)
{
    if (IsValid(Entry.SpawnedComponent))
    {
       Entry.SpawnedComponent->DestroyComponent();
       Entry.SpawnedComponent = nullptr;
    }
}

 

 

- MJ_CharacterPartComponent.h

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CharacterPartType.h"
#include "MJ_CharacterPartComponent.generated.h"


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<FLifetimeProperty>& OutLifetimeProps) const override;

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

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

	// 캐릭터에 적용된 모든 파츠 목록
	UPROPERTY(Replicated, Transient)
	FCharacterPartList AppliedCharacterPartList;
		
};

 

 

- MJ_CharacterPartComponent.cpp


#include "Character/MJ_CharacterPartComponent.h"

#include "GameFramework/Character.h"
#include "Net/UnrealNetwork.h"

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

void UMJ_CharacterPartComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

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


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

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

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

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

USkeletalMeshComponent* UMJ_CharacterPartComponent::GetParentMeshComponent() const
{
	if (ACharacter* Char = Cast<ACharacter>(GetOwner()))
	{
		return Char->GetMesh();
	}
	return nullptr;
}

 

 

 

이전 캐릭터 코드에서 위 컴포넌트를 사용하기 위해 아래처럼 코드 내용을 추가하면 된다.

- MJCharacter.h

// MJCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "CharacterPartComponent.h" // 추가
#include "CharacterPartType.h"      // 추가
#include "MJCharacter.generated.h"

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 = "Cosmetic")
	UCharacterPartComponent* CharacterPartComponent;

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

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

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


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

};

 

 

 

- MJCharacter.cpp

// MJCharacter.cpp

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

DEFINE_LOG_CATEGORY(LogTemplateCharacter);

AMJCharacter::AMJCharacter()
{
	bReplicates = true;
	GetCharacterMovement()->SetIsReplicated(true);

	// ✅ 코스메틱 시스템 컴포넌트 생성
	CharacterPartComponent = CreateDefaultSubobject<UCharacterPartComponent>(TEXT("CharacterPartComponent"));
	AddOwnedComponent(CharacterPartComponent);

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

		FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
		FollowCamera->SetupAttachment(CameraArm, USpringArmComponent::SocketName);
		FollowCamera->bUsePawnControlRotation = false;
	}

	bIsSprinting = false;
}

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

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

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

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

void AMJCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(ThisClass, bIsSprinting);
}

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

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

// 나머지 기존 Sprint, Crouch, Interact 로직 그대로 유지