소개 및 배경
대난투 스타일의 사이드 뷰 멀티플레이어 게임을 언리얼 엔진 5로 개발하는 과정에서 가장 중요한 요소 중 하나는 카메라 시스템입니다. 특히 여러 플레이어가 동시에 화면에 표시되어야 하는 대난투 게임의 특성상, 기존 언리얼 엔진의 카메라 컴포넌트만으로는 한계가 있었습니다.
기본적으로 언리얼 엔진의 FollowCameraComponent는 단일 플레이어를 추적하는 용도로 설계되어 있어, 여러 플레이어와 보스 몬스터를 동시에 화면에 담아내거나, 특정 상황에 맞게 카메라 모드를 전환하는 기능이 부족했습니다.
이러한 한계를 극복하기 위해 기존 컴포넌트를 확장하여 다음 기능들을 구현했습니다:
- 여러 플레이어와 보스를 동시에 화면에 표시하는 그룹 카메라 모드
- 개인 카메라와 그룹 카메라 간 부드러운 전환
- 멀티플레이어 환경에서의 네트워크 지원
- 특정 영역에서 카메라 설정을 변경하는 확장된 트리거 시스템
기반 클래스 분석
FollowCameraComponent
기존의 FollowCameraComponent는 다음과 같은 기능을 제공합니다:
- 캐릭터 자동 추적 (위치와 회전 모두)
- 줌 거리, 리드 거리(캐릭터 진행 방향으로 카메라가 앞서가는 거리), 높이 등 설정
- 부드러운 보간을 통한 자연스러운 카메라 움직임
- 사이드 뷰 게임에 최적화된 카메라 설정
이 컴포넌트는 기본적인 기능은 훌륭하지만, 다음과 같은 제한이 있었습니다:
- 단일 타겟만 추적 가능
- 멀티플레이어를 위한 네트워크 기능 부족
- 다양한 카메라 모드나 전환 시스템 부재
CameraTrigger
CameraTrigger 클래스는 플레이어가 특정 영역에 진입하면 카메라 설정을 자동으로 변경해주는 기능을 제공합니다:
- 트리거 영역 진입/이탈 시 카메라 파라미터 변경
- 이전 카메라 설정으로 복원하는 기능 (UndoAfterEndOverlap)
- 부드러운 전환을 위한 블렌드 타임 설정
이 트리거 시스템도 유용하지만, 확장된 카메라 컴포넌트와 호환되도록 보완이 필요했습니다.
카메라 컴포넌트 확장 - SmashCameraComponent
클래스 구조 및 상속 관계
SmashCameraComponent는 기존 FollowCameraComponent를 상속받아 기본 기능을 유지하면서 새로운 기능을 추가했습니다:
UCLASS()
class SMASHBRAWL_API USmashCameraComponent : public UFollowCameraComponent
{
GENERATED_BODY()
// 추가 속성 및 메서드...
};
이렇게 상속 구조를 활용하면 기존 코드를 재작성할 필요 없이 필요한 기능만 확장할 수 있어 개발 효율성이 높아집니다.
카메라 모드 시스템
가장 중요한 확장 기능 중 하나는 두 가지 카메라 모드를 지원하는 시스템입니다:
// 카메라 모드 열거형
UENUM(BlueprintType)
enum class ECameraMode : uint8
{
Group UMETA(DisplayName = "Group"), // 그룹 모드 (모든 플레이어 표시)
Default UMETA(DisplayName = "Default") // 기본 모드 (개인 플레이어 추적)
};
- Default 모드: 기존 카메라처럼 단일 플레이어를 추적하는 모드
- Group 모드: 모든 플레이어와 주요 적(보스)이 화면에 표시되도록 자동 조절하는 모드
이 모드는 다음과 같이 설정할 수 있습니다:
void USmashCameraComponent::SetCameraMode(ECameraMode NewMode, float BlendTime)
{
// 이전 모드 저장 (복원 기능을 위해)
PreviousCameraMode = CurrentCameraMode;
// 새 모드 설정
CurrentCameraMode = NewMode;
// 모드에 맞는 설정 적용
ApplyCameraModeSettings(NewMode, BlendTime);
// 카메라 위치 및 기타 설정 업데이트...
}
각 모드에 맞는 설정을 자동으로 적용하여 사용자가 직접 모든 설정을 변경할 필요가 없도록 했습니다:
void USmashCameraComponent::ApplyCameraModeSettings(ECameraMode Mode, float BlendTime)
{
switch (Mode)
{
case ECameraMode::Default:
// 개인 모드 설정 - 더 가깝고 빠른 카메라
SetZoomDistance(800.0f, BlendTime);
SetLeadSpeed(3.0f);
// 기타 설정...
break;
case ECameraMode::Group:
// 그룹 모드 설정 - 더 넓은 뷰와 부드러운 움직임
SetZoomDistance(MinGroupZoomDistance, BlendTime);
SetLeadSpeed(1.5f);
// 기타 설정...
break;
}
}
다중 타겟 추적 시스템
그룹 모드의 핵심은 여러 타겟을 동시에 추적하는 기능입니다. 이를 위해 타겟 관리 시스템을 구현했습니다:
// 타겟 관리 함수
void USmashCameraComponent::AddTargetActor(AActor* TargetActor)
{
// 중복 방지 및 유효성 검사
if (!TargetActor || TargetActors.Contains(TargetActor))
{
return;
}
// 타겟 배열에 추가
TargetActors.Add(TargetActor);
// 첫 번째 타겟은 메인 타겟으로 설정
if (!MainTarget)
{
MainTarget = TargetActor;
}
}
그룹 모드에서는 모든 타겟이 화면에 표시되도록 카메라 위치와 줌을 자동으로 조정합니다:
void USmashCameraComponent::UpdateGroupCamera(float DeltaTime)
{
// 타겟이 하나거나 없으면 기본 모드처럼 처리
if (TargetActors.Num() <= 1)
{
UpdateDefaultCamera(DeltaTime);
return;
}
// 모든 타겟의 평균 위치 계산
FVector TargetCenterPos = FVector::ZeroVector;
int32 ValidTargetCount = 0;
// 경계 상자 계산용 초기값
FVector MinBound(MAX_FLT, MAX_FLT, MAX_FLT);
FVector MaxBound(-MAX_FLT, -MAX_FLT, -MAX_FLT);
// 모든 타겟 위치 처리
for (AActor* Target : TargetActors)
{
if (IsValid(Target))
{
FVector TargetPos = Target->GetActorLocation();
// 평균 위치 계산
TargetCenterPos += TargetPos;
ValidTargetCount++;
// 경계 상자 업데이트
MinBound.X = FMath::Min(MinBound.X, TargetPos.X);
MaxBound.X = FMath::Max(MaxBound.X, TargetPos.X);
// Y, Z 축도 동일하게 처리...
}
}
// 평균 위치 계산
if (ValidTargetCount > 0)
{
TargetCenterPos /= ValidTargetCount;
// 경계 상자 크기 계산
FVector BoundSize = MaxBound - MinBound;
// 최적의 줌 거리 계산 (사이드뷰 게임이므로 X축 기준)
float OptimalZoom = FMath::Clamp(
BoundSize.X * 0.8f + TargetPadding,
MinGroupZoomDistance,
MaxGroupZoomDistance
);
// 부드러운 줌 적용
float NewZoomDistance = FMath::FInterpTo(
CameraZoomDistance,
OptimalZoom,
DeltaTime,
ZoomSmoothingFactor
);
// 카메라 위치와 줌 조정...
}
}
이 방식으로 플레이어가 서로 멀어져도 모두 화면에 표시되며, 가까워지면 자연스럽게 줌인하여 상세한 액션을 볼 수 있습니다.
네트워크 복제 구현
멀티플레이어 게임에서는 네트워크 복제가 중요합니다. 필요한 속성만 선택적으로 복제하여 성능을 최적화했습니다:
void USmashCameraComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 복제할 속성들만 선택적으로 등록
DOREPLIFETIME(USmashCameraComponent, RegisteredPlayers);
DOREPLIFETIME(USmashCameraComponent, bAutoCameraEnabled);
DOREPLIFETIME(USmashCameraComponent, bIsMasterCamera);
// CurrentCameraMode는 의도적으로 복제하지 않음
}
특별히 주목할 점은 CurrentCameraMode를 복제 대상에서 제외한 것입니다. 이는 각 클라이언트가 자신의 화면에서 원하는 카메라 모드를 독립적으로 설정할 수 있게 하기 위한 의도적인 설계 결정이었습니다. 예를 들어, 한 플레이어는 그룹 모드로 전체 상황을 보고, 다른 플레이어는 개인 모드로 자신의 캐릭터에 집중할 수 있습니다.
마스터 카메라 시스템
여러 플레이어가 있는 환경에서 일관된 이벤트 처리를 위해 마스터 카메라 개념을 도입했습니다:
void USmashCameraComponent::BecomesMasterCamera()
{
// 권한 확인 (서버만 설정 가능)
if (GetOwner()->HasAuthority())
{
// 이미 마스터 카메라면 중복 처리 방지
if (bIsMasterCamera)
{
return;
}
// 기존 마스터 카메라가 있으면 해제
for (TActorIterator<ASmashCharacter> It(GetWorld()); It; ++It)
{
ASmashCharacter* Character = *It;
if (Character && Character != GetOwner())
{
USmashCameraComponent* CameraComp = Character->FindComponentByClass<USmashCameraComponent>();
if (CameraComp && CameraComp->IsMasterCamera())
{
CameraComp->StopBeingMasterCamera();
}
}
}
// 이 카메라를 마스터로 설정
bIsMasterCamera = true;
// 모든 클라이언트에게 알림
Multicast_BecomesMasterCamera();
}
else
{
// 클라이언트는 서버에 요청
Server_BecomesMasterCamera();
}
}
마스터 카메라는 보스 인트로 시퀀스, 중요 이벤트 카메라 효과 등 게임 전체적인 카메라 이벤트를 처리하는 역할을 담당합니다. 이로써 여러 플레이어가 있어도 일관된 시네마틱 경험을 제공할 수 있습니다.
카메라 효과
게임 피드백을 강화하기 위한 다양한 카메라 효과도 구현했습니다. 대표적으로 카메라 흔들림 효과:
void USmashCameraComponent::ShakeCamera(float Intensity, float Duration, float Falloff)
{
// 흔들림 상태 활성화 및 파라미터 설정
bIsCameraShaking = true;
ShakeIntensity = Intensity;
ShakeDuration = Duration;
ShakeTimer = 0.0f;
ShakeFalloff = Falloff;
// 원래 위치 저장 (효과 종료 후 복원용)
OriginalCameraLocation = GetComponentLocation();
}
void USmashCameraComponent::UpdateCameraShake(float DeltaTime)
{
// 타이머 업데이트
ShakeTimer += DeltaTime;
// 시간이 다 되면 효과 종료
if (ShakeTimer >= ShakeDuration)
{
bIsCameraShaking = false;
return;
}
// 시간에 따른 감쇠 계산
float RemainingPct = 1.0f - (ShakeTimer / ShakeDuration);
float CurrentIntensity = ShakeIntensity * FMath::Pow(RemainingPct, ShakeFalloff);
// 랜덤 오프셋 생성
FVector ShakeOffset;
ShakeOffset.X = FMath::RandRange(-1.0f, 1.0f) * CurrentIntensity;
ShakeOffset.Y = FMath::RandRange(-1.0f, 1.0f) * CurrentIntensity;
ShakeOffset.Z = FMath::RandRange(-1.0f, 1.0f) * CurrentIntensity;
// 카메라 오프셋에 적용
CameraLocationOffset += ShakeOffset;
}
이 효과는 보스의 강력한 공격이나 중요한 이벤트 발생 시 몰입감을 높이는 데 사용됩니다.
카메라 트리거 확장 - SmashCameraTrigger
기존 CameraTrigger 클래스를 확장하여 SmashCameraComponent와 원활하게 연동되는 SmashCameraTrigger를 구현했습니다:
UCLASS()
class SMASHBRAWL_API ASmashCameraTrigger : public ACameraTrigger
{
GENERATED_BODY()
// 커스텀 블렌드 시간 오버라이드
UPROPERTY(EditAnywhere, Category = "Camera|Preset")
float CustomBlendTime = 0.0f;
// 기타 속성 및 메서드...
};
트리거 동작 메커니즘
트리거의 핵심 기능은 플레이어가 특정 영역에 진입하면 카메라 설정을 변경하고, 영역을 벗어나면 이전 설정으로 돌아가는 것입니다:
void ASmashCameraTrigger::OnOverlapBegin_Implementation(class AActor* ThisActor, class AActor* OtherActor)
{
// 기본 트리거 기능 실행
Super::OnOverlapBegin(ThisActor, OtherActor);
// SmashCameraComponent 찾기
USmashCameraComponent* CameraComp = GetSmashCameraComponent(OtherActor);
if (CameraComp)
{
// 커스텀 블렌드 시간 또는 기본값 사용
float BlendTime = CustomBlendTime > 0.0f ?
CustomBlendTime : CameraComp->GetDefaultCameraBlendSpeed();
// 현재 모드와 다를 때만 변경
if (CameraComp->GetCameraMode() != ECameraMode::Default)
{
CameraComp->SetCameraMode(ECameraMode::Default, BlendTime);
}
}
}
이 방식의 큰 장점은 부모 클래스인 CameraTrigger의 UndoAfterEndOverlap 속성을 그대로 활용할 수 있다는 것입니다. 트리거에 진입하기 전 상태를 저장했다가, 트리거 영역을 벗어날 때 자동으로 복원합니다.
카메라 컴포넌트 찾기 및 캐싱
트리거가 작동할 때마다 매번 카메라 컴포넌트를 찾는 것은 비효율적이므로, 캐싱 메커니즘을 구현했습니다:
USmashCameraComponent* ASmashCameraTrigger::GetSmashCameraComponent(AActor* Actor)
{
// 이미 캐싱된 참조가 있고 유효하면 반환
if (SmashCameraComp && SmashCameraComp->GetOwner() == Actor)
{
return SmashCameraComp;
}
// SmashCharacter 먼저 확인
if (ASmashCharacter* SmashChar = Cast<ASmashCharacter>(Actor))
{
SmashCameraComp = SmashChar->SmashCameraComponent;
return SmashCameraComp;
}
// 직접 컴포넌트 찾기
if (USmashCameraComponent* FoundComp = Actor ? Actor->FindComponentByClass<USmashCameraComponent>() : nullptr)
{
SmashCameraComp = FoundComp;
return SmashCameraComp;
}
// 찾지 못한 경우
return nullptr;
}
이렇게 하면 같은 액터에 대해 중복 검색을 피하고 성능을 개선할 수 있습니다.
주요 도전과 해결책
다중 타겟 추적의 성능 최적화
그룹 카메라 모드에서 가장 큰 도전은 여러 타겟을 추적하면서도 성능을 유지하는 것이었습니다. 다음과 같은 최적화 전략을 적용했습니다:
- 조건부 계산: 타겟이 하나뿐이거나 없을 때는 그룹 모드 계산을 건너뜁니다.
- if (TargetActors.Num() <= 1) { UpdateDefaultCamera(DeltaTime); return; }
- 부드러운 보간: 매 프레임마다 설정을 급격히 변경하는 대신 보간을 통해 부드럽게 변경합니다.
- float NewZoomDistance = FMath::FInterpTo( CameraZoomDistance, OptimalZoom, DeltaTime, ZoomSmoothingFactor );
- 유효성 검사: 모든 계산 전에 타겟의 유효성을 확인하여 불필요한 연산을 방지합니다.
- if (IsValid(Target)) { // 계산 수행 }
네트워크 동기화와 클라이언트 독립성의 균형
멀티플레이어 게임에서는 네트워크 동기화와 클라이언트 독립성 사이의 균형이 중요합니다. 다음과 같은 접근 방식을 취했습니다:
- 선택적 복제: 모든 데이터를 복제하는 대신, 정말 필요한 데이터만 복제합니다.
- DOREPLIFETIME(USmashCameraComponent, RegisteredPlayers); DOREPLIFETIME(USmashCameraComponent, bIsMasterCamera); // CurrentCameraMode는 복제하지 않음
- 클라이언트 권한 위임: 사용자 경험과 직접 관련된 설정은 각 클라이언트가 제어하도록 합니다.
- // 각 클라이언트가 독립적으로 카메라 모드 선택 가능 void USmashCameraComponent::ToggleCameraMode() { if (CurrentCameraMode == ECameraMode::Group) SetCameraMode(ECameraMode::Default, 1.0f); else SetCameraMode(ECameraMode::Group, 1.0f); }
- 서버 권한 존중: 보안이나 게임 진행에 중요한 결정은 서버가 담당합니다.
- // 마스터 카메라 설정은 서버만 가능 if (GetOwner()->HasAuthority()) { // 마스터 카메라 설정 로직 } else { // 서버에 요청 Server_BecomesMasterCamera(); }
이런 접근 방식으로 네트워크 부하를 줄이면서도 원활한 멀티플레이어 경험을 제공할 수 있었습니다.
배운 점 및 향후 개선 방향
언리얼 엔진에서 컴포넌트 확장의 유연성
이번 작업을 통해 언리얼 엔진에서 기존 컴포넌트를 확장하는 방식의 유연성과 효율성을 배웠습니다. 처음부터 모든 것을 구현하는 대신 기존 기능을 활용하고 확장함으로써:
- 개발 시간 단축
- 기존 코드의 안정성 활용
- 필요한 부분만 집중적으로 개선
이러한 접근 방식은 기능 확장뿐 아니라 코드 유지보수 측면에서도 큰 이점을 제공합니다.
네트워크 복제를 위한 설계 원칙
멀티플레이어 게임에서 네트워크 복제는 항상 어려운 과제입니다. 이번 작업에서 효과적이었던 설계 원칙은 다음과 같습니다:
- 최소 복제 원칙: 정말 필요한 데이터만 복제하여 네트워크 트래픽 최소화
- 클라이언트 권한 분배: 사용자 경험 관련 요소는 각 클라이언트가 제어하도록 위임
- 명확한 RPC 체계: 서버-클라이언트 간 통신에 명시적인 RPC 함수 사용
이러한 원칙을 적용하면 네트워크 성능을 최적화하면서도 좋은 사용자 경험을 제공할 수 있습니다.
성능 최적화 가능성
현재 구현에서도 성능은 양호하지만, 대규모 멀티플레이어나 복잡한 환경에서는 추가 최적화가 필요할 수 있습니다:
- 계산 주기 조정: 매 프레임이 아닌 일정 간격으로 무거운 계산 수행
- // 예시: 0.1초마다 한 번만 계산 if (OptimalZoomTimer >= 0.1f) { CalculateOptimalZoomDistance(); OptimalZoomTimer = 0.0f; } OptimalZoomTimer += DeltaTime;
- 중요도 기반 시스템: 화면 중앙에 가까운 타겟이나 중요 타겟에 가중치 부여
- LOD(Level of Detail) 시스템: 카메라 거리에 따라 업데이트 빈도나 정밀도 조정
향후 추가 기능
현재 구현을 기반으로 다음과 같은 기능을 추가로 개발할 계획입니다:
- 보스 인트로 시퀀스: 보스 등장 시 특별한 카메라 시퀀스로 긴장감 조성
- 시네마틱 이벤트 트리거: 스토리 진행에 따른 자동 카메라 시퀀스
- 카메라 필터 효과: 게임 상황에 따른 시각 효과(블룸, 색상 그레이딩, 모션 블러 등)
- 플레이어별 설정: 각 플레이어가 자신의 카메라 설정을 커스터마이징할 수 있는 옵션
이러한 기능을 추가하면 게임의 시각적 경험과 몰입도를 더욱 향상시킬 수 있을 것입니다.
결론
이번 개발을 통해 언리얼 엔진 5에서 대난투 스타일 게임에 적합한 확장된 카메라 시스템을 성공적으로 구현했습니다. FollowCameraComponent와 CameraTrigger의 기존 기능을 유지하면서, 멀티플레이어 지원, 그룹 카메라 모드, 확장된 트리거 시스템 등 필요한 기능을 추가했습니다.
이 카메라 시스템은 다음과 같은 주요 이점을 제공합니다:
- 유연한 카메라 모드: 개인 모드와 그룹 모드 간 자유로운 전환으로 게임 상황에 따른 최적의 시점 제공
- 모든 플레이어 가시성 보장: 액션 게임에서 가장 중요한 요소인 모든 캐릭터의 위치와 상태를 항상 확인 가능
- 네트워크 최적화: 필요한 데이터만 복제하여 효율적인 네트워크 사용
- 확장 가능한 구조: 추가 기능과 효과를 쉽게 구현할 수 있는 견고한 아키텍처
카메라 시스템은 게임의 핵심 경험을 결정하는 중요한 요소입니다. 이번에 구현한 시스템은 대난투 스타일 게임의 빠른 액션과 다양한 상황에 유연하게 대응할 수 있는 견고한 기반을 제공할 것입니다. 향후 게임 개발이 진행됨에 따라 더 많은 기능과 최적화를 통해 이 시스템을 계속 발전시켜 나갈 예정입니다.
'UnrealCamp' 카테고리의 다른 글
| 프로젝트 기획서 [Smash Raid] (0) | 2025.04.02 |
|---|---|
| 03-13 (0) | 2025.03.13 |
| 12주차 Day5 (0) | 2025.03.07 |
| 12주차 Day2 (0) | 2025.03.04 |
| 10주차 Day3- Unreal Engine C++로 Enemy AI Base 구현하기 (0) | 2025.02.19 |