개인프로젝트를 진행하며 Player 설계시 Weapon 관련 클래스들을 직접 소유,관리 하면 발생하는 문제점이 있었습니다.
- Player가 Weapon 생성,장착,교체 등 로직까지 관리하는 과다 책임 및 SRP 위반
- Weapon 클래스 수정시 PlayerCharacter를 직접 수정해야 할 수 있는 OCP 위반
- 반동 적용과 같은 무기 관련 로직이 Player에서 처리되며 낮은 유지보수성 및 확장성
이 외에도 다양한 문제점들이 있었지만, Player 클래스 내 Weapon만 추가했던 경우에도 코드가 복잡해지며 가독성이 낮아지는것을 확인하게되어 해당 클래스를 Actor Component를 사용한 리팩토링을 진행하게 되었습니다.
이번 포스팅에서는 Weapon 관련 프레임워크 설계에 대해서 작성하도록 하겠습니다.
Actor Component 적용 전
Player.cpp
void ABasePlayableCharacter::AttachWeapon()
{
if (!IsValid(WeaponClass)) return;
ABaseWeapon* CurrentWeapon = GetWorld()->SpawnActor<ABaseWeapon>(WeaponClass);
if (CurrentWeapon)
{
FAttachmentTransformRules AttachmentRules(EAttachmentRule::KeepRelative, true);
CurrentWeapon->AttachToComponent(GetMesh(), AttachmentRules, FName("WeaponSocket"));
CurrentWeapon->SetActorRelativeLocation(CurrentWeapon->GetSocketOffset());
CurrentWeapon->SetActorRelativeRotation(CurrentWeapon->GetSocketRotation());
}
}
적용 전 Player.cpp 내 총기를 스폰 및 부착을 담당하는 관련 로직입니다.
이러한 무기 관련 로직들을 Character 클래스 내 처리해도 정상적으로 실행이 되는것을 확인 할 수 있었습니다.
그러나 프로젝트 설계에 따른 Sound, Effect, Notify등 다양한 기능들이 포함될 예정이며 이러한 로직들을 Player에게
중앙 집중 방식으로 처리할 경우 객체지향의 원칙 및 특징들을 위반하며, 코드 가독성 또한 저하되는것을 확인했습니다.
이러한 문제점을 발견하여 해당 클래스에 부착할 수 있는 Actor Component 클래스를 사용하여 관리하게 되었습니다.
- Actor Component
- AActor 클래스에 동적으로 Attach할 수 있는 재사용 가능한 컴포넌트
- 독립적인 로직을 모듈화하여 여러 액터에서 공유 가능
- 서버/클라이언트 동기화를 지원하는 RPC 사용 가능
- 액터가 특정 기능에 의존하지않고, 컴포넌트가 기능을 담당함으로써 결합도를 낮춤
- 필요한 기능만 컴포넌트로 추가하여 불필요한 로직 최소화 가능
- Service Locator 패턴과 유사한 역할
Actor Component 적용 후
Player.cpp
void ABasePlayableCharacter::SpawnActorComponent()
{
WeaponComponent = CreateDefaultSubobject<UWeaponComponent>(TEXT("Weapon"));
if (!WeaponComponent)
{
UE_LOG(LogPlayer, Error, TEXT("WeaponComponent CDO is Null"));
}
}
// UI 관련 구현 후 이벤트 기반의 총기 장착 구현
// 현재 단위테스트용 하드코딩
void ABasePlayableCharacter::AttachWeapon()
{
if (WeaponComponent)
{
WeaponComponent->EquipWeapon(EWeaponType::Rifle, WeaponClass);
}
}
void ABasePlayableCharacter::SwitchCurrentWeapon(int32 WeaponType)
{
UE_LOG(LogPlayer, Warning, TEXT("WeaponType Call : %d"), int32(WeaponType));
if (WeaponComponent)
{
WeaponComponent->SwitchWeapon((EWeaponType)WeaponType);
}
}
Weapon Component.cpp
void UWeaponComponent::EquipWeapon(EWeaponType Slot, TSubclassOf<class ABaseWeapon> WeaponClass)
{
ACharacter* TargetOwner = Cast<ACharacter>(GetOwner());
if (!TargetOwner || !WeaponClass)
{
UE_LOG(LogWeapon, Error, TEXT("WeaponComponent_EquipWeapon Func Owner || WeaponClass nullptr"));
return;
}
if (EquipWeapons.Contains(Slot) && EquipWeapons[Slot])
{
EquipWeapons[Slot]->Destroy();
EquipWeapons[Slot] = nullptr;
}
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = TargetOwner;
SpawnParams.Instigator = TargetOwner->GetInstigator();
ABaseWeapon* NewWeapon = GetWorld()->SpawnActor<ABaseWeapon>(WeaponClass, SpawnParams);
if (NewWeapon)
{
NewWeapon->AttachToComponent(TargetOwner->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true),
WeaponAttachSocketName);
NewWeapon->SetActorRelativeLocation(NewWeapon->GetSocketOffset());
NewWeapon->SetActorRelativeRotation(NewWeapon->GetSocketRotation());
NewWeapon->SetActorEnableCollision(false);
EquipWeapons.Add(Slot, NewWeapon);
SwitchWeapon(Slot);
}
// SkeletalMesh 에 특정 소켓으로 부착
// KeepRelative : 상대적인 위치를 유지하며 부착
// true : 부착이 실패해도 실행을 계속
}
void UWeaponComponent::SwitchWeapon(EWeaponType Slot)
{
ACharacter* TargetOwner = Cast<ACharacter>(GetOwner());
if (!TargetOwner || !EquipWeapons.Contains(Slot))
{
UE_LOG(LogWeapon, Error, TEXT("WeaponComponent_SwitchWeapon Func Owner || Target Weapon Slot is nullptr"));
return;
}
if (CurrentWeapon)
{
UnEquipWeapon();
}
CurrentWeapon = EquipWeapons[Slot];
if (CurrentWeapon)
{
CurrentWeapon->SetActorHiddenInGame(false);
}
}
- Weapon 관련 로직 Component를 사용한 분리, 독립적인 모듈화 진행
- 재사용성 증가 및 유지보수성 용이, SRP,OCP 원칙을 지향하는 설계
- 멀티플레이 환경에서의 RPC를 독립적으로 관리 가능
이러한 이유로 Actor Component를 적용한 프레임 워크 설계를 진행하게 되었습니다.
'UE5 - Project > 개인 프로젝트 - FPS TPS' 카테고리의 다른 글
[UE5] UI와 연동한 레벨 변경하기 (ClientTravel) (0) | 2025.02.14 |
---|---|
[UE5] - UI를 사용한 키리맵핑 (0) | 2025.02.13 |
[UE5] - 데이터에셋과 UI 연동하기 (0) | 2025.02.11 |