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 로직 그대로 유지
'언리얼 팀프로젝트' 카테고리의 다른 글
| 최종 팀 프로젝트 Interact 인터페이스 및 Pickup 로직 수정 기획 (0) | 2025.11.12 |
|---|---|
| 최종 팀 프로젝트 Pickup 기능 구현 -2 (0) | 2025.11.07 |
| 최종 팀프로젝트 - 캐릭터 기본 애니메이션 구현 (0) | 2025.11.04 |
| 최종 팀프로젝트 - 캐릭터 구현 (이동/앉기/달리기) (0) | 2025.11.03 |
| 멀티 플레이(데디서버) 물속 들어갔을때 체력 줄어드는 기믹 구현 완료 (0) | 2025.10.02 |