언리얼 엔진을 활용한 멀티 플레이를 구현하기 위해 학습 하던 중, 리슨서버 및 데디케이티드 서버에 대해 학습하였습니다.

해당 과정에서 언리얼 엔진에서 제공하는 리슨서버 및 데디케이티드 서버의 특징, 차이점들을 작성하며

그 외 P2P 서버 및 클라우드 서버 등에서도 간략하게 알아보겠습니다.

 

 

멀티플레이 (MultiPlay)

  • 여러 플레이어가 네트워크를 통해 같은 게임 월드에서 상호작용 할 수 있도록 구현하는 시스템
  • 서버 - 클라이언트 구조로 이루어져있다.
  • 언리얼 엔진 내 리플리케이션, 세션을 활용하여 동작한다.
    • 서버 (Server) : 게임 상태 관리
      • 여러 클라이언트와 상호작용
      • 클라이언트에서 해킹당하면 안되는 데이터에 대한 처리
      • 플레이어의 상태 관리
    • 클라이언트 (Client) : 서버에서 관리하는 데이터를 가져와 게임을 플레이하는 방식
    • 리플리케이션(Replication) : 서버의 정보를 클라이언트에 동기화하는 기능
    • 세션 (Session) : 멀티플레이 게임에서 클라이언트들이 연결되는 로비 개념

 

리슨서버 (Listen Server)

  • 한 플레이어가 직접 서버 역할을 하며 동시에 클라이언트를 수행하는 방식
    • Player1 = Server + Client
    • Player2,3,4... = Client
  • 멀티플레이 관련 설정이 다른 서버 연결방식보다 쉽다.
  • 별도의 서버 운영이 필요 없으며, 서버 운영 비용이 발생하지 않는다.
  • 서버 호스트가 게임을 종료하면, 모든 플레이어가 해당 세션에서 종료된다.
  • 서버 호스트 PC성능에 비례하여 네트워크 성능이 저하 될 수 있다.
  • 보안에 취약하다.
    • 리슨서버는 게임 데이터를 노출시키는 형식. 데이터를 쉽게 조작 할 수 있다.
    • 클라이언트가 서버를 운영하는 방식이기 때문에 데이터를 조작하더라도 감지가 어렵다.
    • UFUNCTION(WithValidation)을 사용한 보안 강화 가능.
    • 클라우드 서버 환경에서는 토큰과 같은 보안방식을 사용하여 보안을 강화시킬 수 있다.

데디케이티드서버 (Dedicated Server)

  • 게임을 플레이하지 않는 독립적인 서버 존재, 클라이언트가 해당 서버에 접속하는 방식
  • 서버는 게임 상태 및 데이터를 관리, 클라이언트는 서버의 데이터를 받아온다.
  • 고정된 네트워크 환경에서 실행되므로 안정성이 뛰어나다.
  • 리슨서버와 다르게 호스트가 종료를 해도 게임이 중단되지 않는다.
  • 독립적인 서버를 통한 게임 데이터를 캡슐화 할 수 있다.
  • 별도 서버 운영 비용이 발생하며, 서버 유지 보수가 필요하다.
  • 고정된 서버 개수로 인해 접속자가 급등하거나 급락할 경우 서버 운영에 문제가 발생한다.

클라우드 서버 (Cloud Server)

AWS GameLift - https://aws.amazon.com/ko/blogs/korea/introducing-amazon-gamelift-anywhere-run-your-game-servers-on-your-own-infrastructure/

  • 네트워크를 통해 접근할 수 있는 가상 서버
  • 현재 게임 회사들이 가장 많이 사용하는 형태
  • 필요할 때만 서버를 생성하는 OnDemand Server 방식을 통해 서버 운영 비용을 절감할 수 있다.
  • AWS, SnowFlake, Azure 등의 클라우드 서비스를 통해 운영
  • 클라이언트 접속자가 급등하더라도 Auto Scaling기능을 통한 안전한 서버 운영 가능
  • 지역별 서버를 배포하므로 글로벌 서버의 경우에도 용이하다.
  • 클라우드 서비스 내 보안 시스템을 사용한 보안 기능을 강화시킬 수 있다.
    • AWS 모니터링, AWS AccessKey 등
  • 서버에 문제가 발생시 별도의 독립된 서버를 통해 모니터링, 프로파일링을 통해 빠른 복구가 가능하다.

 

게임 서버 품질을 보장하는 4가지

1. 안정성 (Stability)

  • 게임 서버가 얼마나 다운되지 않는가. (인듀어 테스트를 통한 검증)
  • 게임 서버가 얼마나 오작동을 하지 않는가.

2. 확장성 (Scalability)

  • 서버를 얼마나 많이 설치할 수 있는지
  • 사용자 수가 늘어나더라도 서비스 품질이 떨어지지 않고 유지되는지
    • 수직적 확장 방법 (Scale Up) : CPU,RAM 같은 서버 머신 부품 업그레이드
    • 수평적 확장 방법 (Scale Out) : 서버 머신의 개수 증설
    • 해당 방법은 데디케이티드 서버일 경우에 대한 방법. 클라우드 서비스에서는 Auto Scaling을 통한 자동 증설기능

3. 성능 (Performance)

  • 얼마나 빠르게 처리되는지
  • 게임 장르에 따른레이턴시(Latency) 고려
  • 코드 및 알고리즘 최적화
  • 서버 과부화 영역 분산
    • Code Profiling을 통한 함수처리 시간 확인, 개선
  • 네트워크 프로토콜 최적화
    • 메시지 양 줄이기 (ZLib, 양자화 등)

4. 관리 편의성

  • 백그라운드 프로세스에서도 프로그램이 실행되는지
    • 윈도우 : 서비스 / 리눅스 : 데몬

 

안정성을 고려한 서버 개발 방법

  • 치밀한 개발 및 단위테스트
    • 모든 함수 호출의 반환값을 반드시 체크하는 루틴
    • 자동화가 구현되어있는 자가 검증 (단위테스트, Unit Test)
  • 80 : 20법칙 유지
    • 성능의 80%는 20%의 소스 코드에서 나타난다는 파레토 법칙
    • 구조가 복잡해지더라도 성능 최적화 및 유지보수하기 쉬운 단순한 구조 방식 개발
  • 1인 이상의 코드리뷰
    • 코드리뷰를 통한 지식 공유, 버그 발견
  • 가정하지 말고 검증하기
    • 봇 테스트, 더미 클라이언트 테스트, 스트레스 테스트를 통한 검증
      • 입력 처리, 렌더링 과정이 생략된 프로그래밍된 행동을 반복하는 더미를 서버에 접속
      • 서버 성능 프로파일링을 통해 지표 확인
  • 불안정시 극복 과정
    • 서버가 다운되더라도 최대한 빠르게 복구
      • 서버 프로세스 중단시, 자동으로 복구되는 프로그램 설계
      • AWS 내 모니터링 시스템을 통한 자동 복구 프로그램 설계 가능
    • 서버가 다운되더라도 최대한 적은 서비스만 다운되게 설계
      • 서버 프로세스 분리를 통한 서버 장애 발생시에도 일부 서비스는 작동할 수 있는 설계
    • 서버 오작동에 대한 로깅하기

출처 및 참고내역

 

게임 서버 프로그래밍 교과서

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/networking-and-multiplayer-in-unreal-engine

https://unrealcommunity.wiki/linux-quickstart-faqs-8f7980

https://aws.amazon.com/ko/blogs/korea/introducing-amazon-gamelift-anywhere-run-your-game-servers-on-your-own-infrastructure/

'Unreal Engine > 네트워크' 카테고리의 다른 글

[UE5] - 리플리케이션 (Replication)  (0) 2025.02.04

팀 프로젝트 미니맵 UI 제작 관련 중 발생한 문제입니다.

※ 미니맵 관련 포스팅 - https://mynameiskgws.tistory.com/50

미니맵을 초기에 설계했을 때, 각 액터 컴포넌트에 타임 핸들러를 사용하여 일정 시간마다 위치값을 전송하려고 했습니다.

이 때 각 액터 컴포넌트들이 타임핸들러를 가지고 있을 경우, 미니맵에 100개의 몬스터가 렌더링 된다 하면 타이머도 100개를 소지하게 되며 메모리 낭비 및 오버헤드 발생 우려가 있었습니다.

이를 해결하기 위해 액터의 시간을 전송하는 싱글톤패턴 기반의 Minimap Manager를 설계하게 되었습니다.

 

UObject 기반의 Manager 클래스

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTargetUpdated, const TArray<FMinimapTargetLocation>&, UpdateLocations);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTargetRemoved, AActor*, RemovedTarget);

UCLASS()
class GUNFIREPARAGON_API UMinimapManager : public UObject
{
	GENERATED_BODY()
	
public:
	void Initialize();
	void RegisterTarget(AActor* Target);
	void UnRegisterTarget(AActor* Target);
	void RecvLocationToBroadCast();

public:
	UPROPERTY(BlueprintAssignable, Category = "Minimap")
	FOnTargetUpdated OnAllLocationsUpdated;

	UPROPERTY(BlueprintAssignable, Category = "Minimap")
	FOnTargetRemoved OnTargetRemoved;

private:
	TMap<AActor*,FVector> TargetLocations;
	FTimerHandle UpdateTimer;

	void UpdateTargetsLocation();
};

void UMinimapManager::Initialize()
{
	TargetLocations.Empty();
}

void UMinimapManager::RegisterTarget(AActor* Target)
{
	if(!GetWorld())
	{
		UE_LOG(LogTemp, Display, TEXT("World Is Null"));
		return;
	}
	if (IsValid(Target) && !TargetLocations.Contains(Target))
	{
		TargetLocations.Add(Target, Target->GetActorLocation());

		if (!UpdateTimer.IsValid())
		{
			UpdateTargetsLocation();
		}
		UE_LOG(LogTemp, Display, TEXT("Register Test : %s"), *Target->GetName());
	}
}

void UMinimapManager::UnRegisterTarget(AActor* Target)
{
	if (TargetLocations.Contains(Target))
	{
		TargetLocations.Remove(Target);
		OnTargetRemoved.Broadcast(Target);
	}

	if (TargetLocations.Num() == 0)
	{
		if (GetWorld())
		{
			GetWorld()->GetTimerManager().ClearTimer(UpdateTimer);
		}
	}
}

void UMinimapManager::RecvLocationToBroadCast()
{
	TArray<FMinimapTargetLocation> TargetData;

	for (const auto& Target : TargetLocations)
	{
		if (Target.Key)
		{
			FMinimapTargetLocation Data;
			Data.TargetActor = Target.Key;
			Data.Location = Target.Key->GetActorLocation();
			TargetData.Add(Data);
		}
		else
		{
			UnRegisterTarget(Target.Key);
		}
	}

	OnAllLocationsUpdated.Broadcast(TargetData);
}

void UMinimapManager::UpdateTargetsLocation()
{
	float UpdateCycleTime = 0.1f;
	if (GetWorld())
	{
		GetWorld()->GetTimerManager().SetTimer(UpdateTimer, this, &UMinimapManager::RecvLocationToBroadCast, UpdateCycleTime, true);
	}
}

 

해당 클래스를 기존에는 static Get을 통한 싱글톤 패턴의 기반으로 하였으나,

미니맵에 해당 액터클래스를 등록하는 과정에서 GetWorld()가 Null이 되는 로그가 출력되었습니다.

이를 해결하기 위해, GameInstance에서 등록을 하였지만, 마찬가지로 GetWorld()가 Null이 출력되며, 크래시되는 현상이 발생하였습니다.

해당 과정은 UObject 기반의 클래스를 정적 으로 사용시에 발생한 문제입니다. 

원인 분석

  • 게임 컨셉이 레벨과 레벨을 이동하는 과정이 발생한다.
  • 해당 과정에서 레벨이 다시 실행 될 경우, 액터 라이프사이클과 같이 새 월드를 가져온다.
  • 해당 과정에서 전역 클래스인 MinimapManager를 재 호출하게되며, 순서를 보장하지 않은 상태.
  • 이로인해 MinimapManager를 다시 불러오며 동시에 새 월드가 생성되기 전 호출되어 Null반환을 하게된다.
    • 순서가 보장이 되지않는다 -> 매 실행마다 출력되는 로그가 다름. Null일경우, 아닐경우 존재를 통해 검증.
  • --- 다른 원인 ---
  • AActor에서 파생된 모든 클래스는 배치 할 수 있는 클래스. 즉, 자신이 어느 World인지 인지 할 수 있다.
  • UObject는 GetWorld()를 호출 할 수 있으나, 배치 되지 않은 경우 의미가 없음.

 

해결 과정

  • 게임 시작부터 유지되는 GEngine()->GetWorld... 를 사용한 호출
    • 수명동안 다른 세계를 로드할 수 있으므로 특정 세계를 반환할 수 없음. 실패.
  • 게임 시작부터 끝까지 데이터를 가지고 있는 Instance에서 생성, 초기화에서 호출
    • 동일 현상, 원인 파악을 못했음. 실패.
    • 기본 자료형 또는 함수는 사용 할 수 있으나, 월드간 변화시 동일 월드가 아니라서 발생한것같다.
  • Actor기반의 클래스 내 호출하여 사용하기
    • 해당 액터의 GetWorld()를 가져오기 때문에 정상적으로 작동.
    • 하지만, BeginPlay()후 GetWorld()를 호출하여 사용하면 초기 설계시 고려한 문제가 발생한다. 실패.
  • Timer를 사용하지 않고, Delegate를 사용한 이벤트 기반으로 리팩토링
    • 특정 조건에 바인딩하여 이벤트 기반 데이터 전송. 구현 성공

 

정리

  • UObject에는 GetWorld() 함수가 존재하기는 하나, 실제로는 null을 반환 할 수 있다.
  • GEngine()->GetWorld()... 를 사용한 호출은 서로 다른 세계를 가져올 수 있어 적합하지 않다.
  • Instance 내 GetWorld()를 사용할 순 있지만, 정적 클래스에는 부적합 (추정)
  • Timer 사용을 하지 않은 Delegate 이벤트 바인딩, 전송으로 인한 우회, 구현 성공

 


참고내역

https://forums.unrealengine.com/t/gengine-getworld-return-null/377571

https://husk321.tistory.com/426

https://www.inflearn.com/community/questions/989392/getworld-%EA%B0%80-nullptr?srsltid=AfmBOookfoyJK0gL44HvTFW_aOKR6EPWRV0Fc8a7JPtwvrUCHOoRy5xI

 

'Unreal Engine > Debugging' 카테고리의 다른 글

[UE5] - 유효성 검사  (0) 2025.01.27

언리얼 프로젝트 폴더 내 구조

  • [PrijectName].Sln
    • C++ 소스코드 및 빌드 설정 연결 파일
  • [ProjectName].uproject
    • 언리얼 에디터 프로젝트 설정 (콘텐츠, 맵, 플러그인 등)을 불러 올 때 사용하는 파일
  • .vsconfig
    • Visual Studio 프로젝트에서 사용되는 특정 설정 구성 요소 설치 요구사항 정의
    • 프로젝트를 열 때 Visual Studio가 필요한 워크로드 및 구성 요소를 자동으로 감지한다.
  • .vs
    • Visual Studio가 제공하는 자동으로 생성으로 생성되는 폴더
    • 디버깅, 솔루션 설정 관련 파일
  • Binaries
    • C++ 코드 빌드 시 생성되는 실행 파일 (.exe, dll)이 저장되는 폴더
  • Config
    • 게임 플레이, 렌더링, 입력 등의 설정을 담은 .ini 파일
    • 에디터 혹은 코드로 특정 설정을 바꾸면 해당 ini 파일이 수정된다.
      • DefaultEditor : 에디터 환경 설정 (뷰포트, UI 등)
      • DefaultEngine : 엔진 전반 설정(렌더링, 네트워킹 등)
      • DefaultGame : 게임 플레이 관련 설정(게임 모드, 플레이어 컨트롤러 클래스 등)
      • DefaultInput : 키보드, 마우스, 패드 등의 기본 입력들을 바인딩
  • Content
    • 언리얼 에디터의 콘텐츠 브라우저와 연결되어 있다.
    • 프로젝트 내 모든 에셋들을 포함하고 있는 폴더.
  • DerivedDataCache
    • 에셋, 셰이더 등을 빠르게 처리하기 위한 캐시 파일들을 저장하는 폴더.
  • Intermediate
    • C++ 컴파일시 생성되는 임시파일을 저장하는 폴더
  • Saved
    • 자동저장, 파일, 로그, 크래시 덤프들을 저장하는 폴더
  • Source
    • C++ 소스코드가 들어있는 폴더
    • 실제 게임 로직을 작성하는 곳
    • 빌드 설정 관련 주요 파일 포함
      • [ProjectName].Build.cs : 해당 프로젝트에 필요한 모듈, 라이브러리, 종속성 등 정의
      • [ProjectName].Target.cs, Editor.Target.cs : 각각 게임 실행 용, 에디터용 빌드 방식 정의

 

C++ 재 빌드 / 클래스 삭제 후 재 빌드시 

  • sln, vs, intermediate, binaries, DericedDataCache, Save들은 빌드 시 생성, 솔루션에 대한 빌드후 정보가 저장되는 파일
  • 해당 폴더 삭제 후 uproject -> generate visual studio project files를 통한 빌드
  • 클래스 삭제 / C++클래스 디렉토리 이동 / 빌드 실패등의 경우 사용하는 방법

 

언리얼 솔루션 구조

  • Engine
    • 언리얼 엔진 자체 소스코드 및 리소스가 담긴 폴더
    • 에디터 작동, 엔진 코어 관련 빌드 포함
  • Games
    • 개발자가 만든 프로젝트 코드들에 대한 폴더
    • Source, Config, .uproject 파일 등
      • Build.cs : 프로젝트 빌드 설정을 관리하는 파일
      • Public Dependency
        • 컴파일 대상 모듈 정의
        • 어떤 모듈을 포함할 지, 어떤 종속성을 가질지 결정한다.
        • 모듈을 포함하는 다른 모듈에서도 접근이 가능하다.
          • 이 모듈을 참조하는 다른 모듈에서도 해당 라이브러리를 사용할 수 있다.
        • 해당 프로젝트에서 필수적으로 사용하는 엔진 기능들 
          • Core : 엔진의 기본 기능들
          • CoreUObject : 리플렉션 시스템, GC
          • Engine : 게임 엔진의 주요 기능
          • InputCore : 입력 시스템
          • EnhancedInput : UE5에서 추가된 입력 시스템
          • UMG : 위젯 블루프린트와 관련된 모듈
      • PrivateDependency
        • 이 모듈 내에서만 사용 가능하며, 다른 모듈에서는 접근이 불가능하다.
        • 이 모듈을 참조하는 다른 모듈은 해당 의존성을 알 필요 없을 때
        • Slate, SlateCore같은 UI관련 특정 기능을 사용할 경우
  • Programs
    • 엔진 동작에 필요한 유틸리티 프로그램, 서버 모듈
  • Rules
    • 엔진, 게임 등 각 모듈의 빌드 규칙을 정의해놓은 파일들이 모여있는 곳
    • 모듈 의존성, 플러그인 활성화 여부, 빌드 대상 등을 제어한다.
  • Visualizers
    • 디버깅시 언리얼 엔진 관련 자료구조를 보기좋게 표시하기 위한 설정 파일

 

언리얼 빌드 프로세스

  • C++ 코드 수정시 컴파일 + Link과정을 통한 동적 라이브러리 DLL 생성
  • DLL이 언리얼 에디터에서 로드, 새로 작성한 로직(함수, 클래스 등)이 게임,에디터에 즉시 반영된다.
  • 소스파일 -> 전처리기-> 컴파일 -> 링커 -> 실행
    • 전처리기 (Pre-Processing)
      • 소스코드 주석 제거, define 치환 등 .cc 파일로 변환
    • 컴파일러
      • 어셈블리 파일로 변환, .s 파일 변환
    • 어세블러
      • 오브젝트 코드 파일로 변환. .o 파일 변환
    • 링커
      • 오브젝트 파일들을 묶어 실행 코드 파일로 변환
      • .o + .lib
    • 실행
      • .exe파일로 변환이 완료된 상태

 

빌드 구성 및 플랫폼

  • DebugGame
    • 게임 로직만 디버그 정보를 포함, 엔진은 최적화된 상태로 빌드한다.
    • 에디터가 아닌 독립 실행 파일 환경에서 디버깅이 가능하다.
  • DebugGame Editor
    • 에디터 환경에서 게임 로직을 디버그하기 편한 설정
    • 에디터 플레이 중 C++ 로직을 추적, 브레이크 포인트를 걸 때
  • Development
    • 디버그 정보를 최소화하여 실행 속도를 높인 개발용 빌드
    • 독립 실행 파일 환경테스트, 개발 단계에서 주로 쓰인다
  • Developmenet Editor
    • 에디터에서도 개발, 테스트를 원활히 할 수 있도록 구성된 빌드 모드
    • Live Coding 사용 시나리오, 기본모드
  • Shipping
    • 최종 사용자(User)에게 배포할 때 사용되는 릴리스 빌드
    • 디버그 정보 제거, 성능 최적화 극대화

 

  • #include "CoreMinimal.h"
    • 언리얼 엔진에서 자주 사용하는 기본 타입(FString, TArray)등 과 매크로(UE_LOG)등 각종 유틸리티 함수들을 정의하고있는 헤더 파일
  • #include "[ClassName].generated.h"
    • 언리얼 엔진의 리플렉션 시스템에서 필요한 코드를 자동으로 생성하기 위한 매크로
  • UCLASS()
    • 해당 클래스를 언리얼 엔진의 리플렉션 시스템에서 인식하도록 하는 매크로
    • 언리얼 에디터에서 이 클래스를 블루프린트로 확장 할 수 있게하며, 에디터의 여러 기능과 연동하도록 한다.
  • [SOLUTIONNAME]_API
    • 이 클래스를 외부 모드로 Export 하기 위한 매크로
    • DLL 등으로 빌드 할 때 필요한 선언
  • GENERATED_BODY()
    • UCLASS()와 같이 엔진 리플렉션에 필요한 코드를 자동으로 생성 해주는 매크로

Controller에서 레벨을 열거나, UParticleSystemComponent를 사용한 파티클 생성, 월드에 배치되어있는 액터를 검색하기 등등 다양한 경우에서 인스턴스화 하지않은 UGameplayStatics를 사용하게 되었습니다.

게임 프로젝트를 제작하며 계속되서 사용되는 정적으로 사용되는 해당 클래스가 어떠한 기능을 하는지 알아보게되었으며,

학습한 자료들을 이번 포스팅에 작성하겠습니다.

 

UGameplayStatics

  • 게임 플레이 관련 유틸리티 함수를 제공하는 정적(Static) 클래스
    • 유틸리티 기능을 빠르게 호출하며, 대부분 전역적인 게임 월드에 의존한다.
    • 따라서 매번 객체를 만들지 않고 정적 클래스 및 함수로 설계되어 빠르게 호출 할 수 있다.
  • 인스턴스를 생성하지 않고 직접 호출한다.
    • UGameplayStatics::FunctionName()
  • 오디오, 내비게이션, 액터 생성,파괴, 게임 레벨 관리, 데미지/충돌 처리, 세이브/로드 등 게임 제작에 필요한 시스템들을 쉽게 사용할 수 있도록 도와준다.
    • 공통 기능들을 쉽게 호출할 수 있도록 설계된 유틸리티 클래스
  • 엔진의 다양한 시스템과 통합되어있어 AI, UI, Physcis등과 연계가 가능하다.
  • 빠르고 간편하게 사용할 수 있으며, 객체 생성이 불필요하여 메모리 관리 시 용이하다.
    • 동적 환경에서 객체를 만들고 삭제하는 메모리 생성값을 줄여 오버헤드를 줄일 수 있다.
  • 남용하거나 잘못된 사용시 성능적으로 문제가 발생 할 수 있다.
    • Ex) GetAllActorsOfClass : 월드에 배치된 모든 액터들을 검색한다.

 

UGameplayStatics::Function(...)

오디오 / 사운드

  • PlaySoundAtLocation
    • 특정 위치에서 사운드 재생
  • PlaySound2D
    • 2D 사운드 재생
  • SpawnSoundAtLocation
    • 게임 내 특정 위치에 사운드 생성, 재생

 

위치 / 내비게이션

  • GetPlayerPawn
    • 특정 플레이어의 Pawn 반환한다.
  • GetPlayerCharacter
    • 특정 플레이어의 Character 반환한다.
  • GetAllActorsOfClass
    • 특정 클래스의 모든 액터를 반환한다.
  • GetActorOfClass
    • 특정 클래스의 첫번째 액터 반환
  • ProjectPointToNavigation
    • 특정 지점을 내비게이션 메시상의 유효한 위치로 변환한다.

 

액터 생성/ 파괴

  • SpawnActor
    • 게임 월드에 새로운 액터 동적으로 생성
    • GetWorld()->SpawnActor와 동일하다.
    • GetWorld()->SpawnActorDeferred  = BeginDeferredActorSpawnFromClass 
  • DestroyActor
    • 특정 액터 삭제

 

게임 흐름 및 레벨 관리

  • OpenLevel
    • 특정 레벨을 불러온다.
  • GetCurrentLevelName
    • 현재 레벨의 이름을 반환한다.
  • SetGamePaused
    • 게임을 일시정지 / 해제한다
  • SetGlobalTimeDilation
    • 게임의 전체적인 시간의 흐름을 조정한다.

 

데미지 및 충돌처리

  • ApplyDamage
    • 액터에 피해를 가한다.
  • ApplyRadialDamage
    • 특정 반경 내에 있는 모든 액터에 피해를 가한다.
  • ApplyPointDamage
    • 특정 지점에서 한개의 액터에게만 피해를 가한다.

 

카메라 및 뷰 관련 기능

  • GetPlayerCameraManager
    • 특정 플레이어의 CameraManager 반환
  • SetGlobalTimeDilation
    • 전체 게임 속도를 조정하여 슬로우 모션 또는 빠른 진행 효과 연출

 

세이브 / 로드

  • CreateSaveGameObject
    • 세이브 데이터 객체 생성
  • SaveGameSlot
    • 데이터를 특정 슬룻에 저장
  • LoadGameFromSlot
    • 특정 슬룻에서 저장된 데이터를 불러온다.

토이프로젝트를 준비하며 프로젝트 설계시 고려해야하는 부분이 여러가지 있었습니다.

객체 관리, 게임모드, UI 등등 다양한 부분에서 설계를 해야했지만, 게임 장르 및 플레이 타입에 대해 가장 먼저 고민하게 되었습니다.

그 중 멀티플레이를 목적으로한 프로젝트 개발을 결정하게 되었으며, 멀티플레이 환경에서 서버-클라이언트간 어떻게

데이터를 처리하는지에 대해 알아보며 언리얼의 RPC, 리플리케이션, POD 등 다양한 용어들을 학습하게 되었습니다.

그 중 가장 멀티플레이 환경에서 데이터를 동기화하는 방법인 리플리케이션에 대해 알아보도록 하겠습니다.

 

리플리케이션 (Replication)

  • 멀티플레이어 환경을 구축할 때 서버와 클라이언트 간의 데이터를 동기화 하는 기능
  • 서버가 권한(Server Authority)을 가지며, 이를 기반으로 리플리케이션 시스템 사용
    • 게임 서버가 진짜 상태를 결정하고, 이를 클라이언트에 전송하는 방식
    • Ex) Player1 이동 -> 서버 : 이동에 대한 결과 -> Player 2,3,4... 에게 반영 (동기화)
    • 객체의 속성, 위치, 애니메이션 등 필요한 정보를 클라이언트에 전송
    • 클라이언트는 서버의 정보를 기반으로 화면을 업데이트 한다.
    • 해당 과정을 통해 중요한 게임 로직은 서버에서 실행되며, 클라이언트들은 서버에서 데이터를 받아 로컬에서 표시하는 역할을 한다.
  • 리플리케이션은 액터(Actor)를 기준으로  실행된다. 
  • 리플리케이션 대상은 Object, Component, Property, Function이 될 수 있다.
    • Object : 캐릭터, 아이템, 차량 등
    • Component : Mesh, Audio, Collision 등
    • Property : 객체의 위치, 속도, 체력, 점수 등
    • Function(RPC) : 문열기, 공격 이벤트, 총 발사
  • RPC : 데이터 통신
  • Replicated, Replicated Notify : 데이터 복제

 

리플리케이션 설정 방법 ,종류

 

1. 변수 리플리케이션 (Property Replication)

  • UPROPERTY(Replicated) 설정
  • GetLifetimeReplicateProps 함수 내 리플리케이션 변수 등록
  • Replicated Notify 설정

 

// UPROPERTY Replicated

UPROPERTY(Replicated)
int32 PlayerHealth;

// GetLifetimeReplicatedProps

void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(AMyCharacter, PlayerHealth);
}

// Rep Notify
UPROPERTY(ReplicatedUsing = OnRep_PlayerHealth)
int32 PlayerHealth;

UFUNCTION()
void OnRep_PlayerHealth()
{
    // 체력 값이 변경될 때 실행할 로직
}

 

 

2. 함수 리플리케이션 (Func Replication) / RPC(Remote Procedure Call)

  • UFUNCTION(Server/Client/Multicast, Reliable)
    • Server : 서버에서만 실행. (Client -> Server)
      • 공격 요청, 스킬 사용 등의 로직
    • Client : 클라이언트에서만 실행. (Server -> Target Client)
      • HUD 업데이트
    • MultiCast : 서버에서 호출, 모든 클라이언트에 실행 (Server -> All Client)
      • 폭발 이벡트, 애니메이션 재생, 게임 결과 등의 로직
// Server RPC
UFUNCTION(Server, Reliable, WithValidation)
void ServerFire();

// Client RPC
UFUNCTION(NetMulticast, Reliable)
void MulticastExplosion();

// Multicast RPC
UFUNCTION(Client, Reliable)
void ClientShowHUD();

 

 

 

3. 액터 리플리케이션

  • 생성자 { bReplicates = true; , SetReplicateMovement(true);}
    • bReplicates = false시 모든 플레이어가 권한(Authority)를 가지게 되며, 플레이어 1이 이동시 다른 플레이어들도 이동한다.
    • 해당 액터의 Movement를 Replicated하게 해준다.

액터 스폰시 SetReplicates(true);

 

// Constructor Replicated Settings
AMyActor::AMyActor()
{
    bReplicates = true; // 서버가 Authority를 가지게 한다. 서버에 정보를 알리고 모든 Client에 전파할 수 있다.
    SetReplicateMovement(true)  // 해당 액터의 Movement를 Replicated한다.
}

// 액터 스폰시 Replicated
AYourActor::Func()
{
    AMyActor* SpawnedActor = GetWorld()->SpawnActor<AMyActor>(MyActorClass, SpawnLocation, SpawnRotation);
    SpawnedActor->SetReplicates(true);
}

 

 

 

Replicated VS Replicated Notify VS RPC

기능 Replicated Replicated Notify (RepNotify) RPC (Remote Procedure Call)
설명 동기화를 통한 데이터 복제 복제 후 특정 시점에 지정한 함수 호출 즉시 함수 호출, 통신 방식
데이터
변경 방식
서버에서 변경하면 클라이언트가 자동으로 반영 서버에서 변경하면 클라이언트가 반영 + 추가 함수 실행 클라이언트 또는 서버에서 직접 함수 실행
변경 감지 기능   복제된 시점에서OnRep_Func() 호출  
동기화
시점
서버에서 변경되면 다음 네트워크 업데이트 시 자동 동기화 서버에서 변경되면 다음 네트워크 업데이트 시 자동 동기화 후 함수 실행 함수가 호출되는 즉시
사용 예시 플레이어 체력, 점수, 탄약 개수 등 체력 변경 시 UI 업데이트, 특수 효과 실행 공격 실행, 문 열기,
이펙트 재생

 

스파르타 코딩클럽 내 프로젝트를 진행하며 액터를 스폰시키는 프로젝트가 있었습니다.

프로젝트를 진행하며 런타임 환경 내 액터를 스폰시키기위해 SpawnActor함수를 호출하며 동시에 추상클래스를 기반으로 확장성있는 액터 구조를 설계하였지만, 공통 프로퍼티를 설정하던 중 SpawnActor를 사용할 경우 BeginPlay가 바로 호출되어 추가로 설정할 수 없던 문제가 있었습니다.

해당 경우를 해결하기위해 SpawnActorDeferred를 사용하게 되었으며, 이를 계기로 기존의 SpawnActor와의 차이점 및 특징에 대해 알아보겠습니다.

 

SpawnActor + 오브젝트 생성

 

SpawnActorDeferred<T>

  • 기존의 SpawnActor은 액터가 생성되며, 즉시 BeginPlay()가 실행된다.
  • 생성된 액터에 추가적인 설정을 적용하며, FinishSpawning()을 호출하여 액터를 생성 할 수 있다.
  • 생성하는 액터에 대해 컴포넌트 및 프로퍼티들을 추가/변경이 가능하다.
  • Spawn시킨 후, 작업(추가적인 설정) 진행 후 FinishSpawning()을 사용한 BeginPlay()를 실행하는 구조.
  • Parameters
    • UClass* class : 생성할 액터 클래스
    • FTransform const& Transform : 액터의 초기 위치 및 회전 값
    • AActor* Owner = nullptr : 생성된 액터의 소유자(Owner)
    • APawn* Instigator : 생성된 액터의 행위자(Ex) 공격을 가한 Pawn)
    • ESpawnActorCollisionHandlingMethod CollisionHandlingOverride : 충돌 시 스폰 방식 설정
      • AlwaysSpawn : 충돌 여부와 상관없이 무조건 스폰
      • AdjustIfPossibleButAlwaysSpawn : 충돌 위치를 조정하고 무조건 스폰
      • AdjustIfPossibleButDon'tSpawnIfColliding : 충돌된 위치를 조정, 그래도 충돌시 스폰하지 않는다.
      • DontSpawnIfColliding : 충돌하는 경우 스폰하지 않는다.

 

예시코드

void AMoveActorSpawner::SpawnActorSettingWithRand(TSubclassOf<AMoveActorBase>& SpawnData)
{
	AMoveActorBase* SpawnActor = GetWorld()->SpawnActorDeferred<AMoveActorBase>(SpawnData, FTransform(FRotator::ZeroRotator, FVector::ZeroVector), this, nullptr,
		ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn);

	if (SpawnActor)
	{
		// ..액터 생성 전에 확인하고 싶은 로직
		// .. 추가 설정하고 싶은 로직
		SpawnActor->FinishSpawning(FTransform(FRotator::ZeroRotator, FVector::ZeroVector));
	}
}

 

 

FActorSpawnParameters

  • SpawnActorDeferred/SpawnActor에서 스폰 관련 옵션을 설정 할 수 있는 구조체
  • SpawnActor를 사용하여 지연시 Timer 또는 FActorSpawnParameters 내 bDeferConstruction = true 방식을 통해 스폰을 지연 시킬 수 있다.
  • SpawnActor + bDeferConstruction = true가 SpawnActorDeferred.

 

게임 플레이 프레임워크(Game Play Framework)

  • 언리얼 엔진을 통한 게임 개발 시 기본적으로 제공되는 핵심 클래스와 시스템들의 집합
  • GameMode, GameState, PlayerController, PlayerState, HUD 같은 클래스들이 존재
    • 플레이어 세부정보(해당 세션의 게임 작업 기여도, 킬로그, 캐릭터 클래스 등)
    • 게임의 현재 상태에 대한 정보(게임 전체 작업 진행도, 진행 시간 등)
    • 승리 조건에 대한 정보
    • 위젯 및 HUD
    • 입력

 

 

UE GameFramework - https://medium.com/project-asura/leveraging-ue4-gameplay-framework-for-our-multiplayer-game-41724f2e1dcd

 

 

Game/Player... + Base

  • 해당 클래스들의 부모 클래스
  • Base를 제외한 클래스들은 게임을 설계시 필요한 기능들을 추가적으로 구현해둔 클래스들이다.
  • Ex) GameMode
    • 언리얼에서 제공하는 멀티플레이 기능을 거의 포함, 싱글플레이 가능
    • GameState, PlayState에 대한 연동 활성화
  • GameModeBase
    • GameMode를 단순화 시키고 효율화 시킨 버전
    • 멀티플레이 관련 로직이 일부 포함.
    • 싱글플레이 / 멀티플레이 로직을 직접 구현하고 싶을 경우 

※ GameMode VS GameModeBase 및 주요 함수

 

Game Mode

  • 게임이 시작될 때 플레이어 스폰, 팀 배정, 게임 승패 조건 등 게임의 승리 조건 및 규칙을 정의한다.
  • 해당 세션의 최대 입장 가능 플레이어 인원, 게임에 들어오는 방식, 일시정지, 레벨 전환 등을 정의한다.
  • DefaultPawnClass, PlayerControllerClass 등을 사용하여 플레이어가 연결될 폰 및 컨트롤러들을 설정한다.
  • 게임당 한개만 존재, 서버에서만 실행되며, 클라이언트에서는 직접 수정 할 수 없다.
  • DefaultPawnClass, HUD Class, PlayerControllerClass, SpectatorClass, GameStateClass, PlayerStateClass 등을 설정 할 수 있다.

[UE5.5.1] GameMode Blueprints - Class

 

 

Game State

  • 서버에만 속하지만, 모든 클라이언트에 복제된다.
    • 클라이언트가 게임의 상태를 모니터링 할 수 있도록 도와준다.
  • 게임에서 규칙과 관련된 이벤트 발생 시 추적, 모든 플레이어와 공유해야 하는 데이터 들을 정의한다.
  • 해당 데이터(정보)들은 GameState를 통해 저장하는것이 적합하며, 모든 데이터들은 동기화(Sync)된다.
    • 모든 클라이언트들이 공통적으로 영향을 미치는 데이터들을 저장하는 클래스
  • 게임이 실행된 시간, 각 개별 플레이어가 게임에 참여한 시간 및 플레이어의 현재 상태
  • 게임이 시작되었는지에 대한 여부, 게임의 작업 진행도 등

 

Game Instance

  • 게임의 전체 수명동안 유지되는 클래스
  • 게임의 시작부터 종료까지 존재하며, 레벨 간 상태유지 및 글로벌 데이터 관리에 적합하다.
  • 싱글톤 구조로 설계되어있으며, 레벨이 전환되어도 삭제되지 않는다.
  • 멀티플레이어 환경에서 호스트 및 연결관리를  위한 기능들을 포함하고 있다.
    • 네트워크 관련 이벤트(연결 성공, 실패, 종료 등)을 포함하고있다.
  • 게임이 시작될 때 자동으로 생성된다.
  • 많은 데이터들을 저장시 의존성 문제가 발생한다.
    • 글로벌환경의 State들을 관리시에 동기화 혹은 충돌 가능성이 있다.
    • 이를 해결하기 위해서는 데이터를 개별 클래스로 나누고 GameInstance에서 참조하도록 설계해야한다.
  • 게임 재시작시에도 GameInstance는 초기화 되지않으며, 명시적으로 데이터를 재설정 해야한다.

 

Player State

  • GameState가 전체 게임 상태를 관리한다면, PlayerState는 플레이어별 데이터를 관리한다.
  • 서버에서 생성된 후 Replicate를 통해 클라이언트와 동기화 하는 데이터
  • 서버와 연결된 다른 클라이언트도 알아야 되는 정보
    • 플레이어 사망처리
  • 플레이어만 알아도 괜찮은 정보 -> Pawn Class, PlayerController
    • Tab키를 누른 게임 상태창같은 경우

 

PlayerController

  • 플레이어의 입력을 받아 Possess된 Pawn 또는 UI들을 제어한다.
  • 클라이언트에서 입력을 받아 서버로 요청을 보낸다.
  • 로컬 플레이어 전용 기능(UI, 카메라 조작)등을 처리한다.
  • 플레이어의 입력을 감지하고 서버로 전송하는 역할

 


출처 및 참고내역

https://medium.com/project-asura/leveraging-ue4-gameplay-framework-for-our-multiplayer-game-41724f2e1dcd

https://ebs12373.tistory.com/225

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/gameplay-framework-in-unreal-engine

언리얼 엔진을 사용하여 학습하던 중, nullptr 또는 캐스팅된 클래스가 개발자가 의도한 클래스인지 확인하기위해 

객체 또는 유효성 검사를 하였습니다.

유효성 검사중 Succeeded, Assert, IsValid, ==nullptr 등 다양한 함수를 사용하게 되었으며,

각각 어떠한 상황에 써야지 알맞는지 학습해보았습니다.

이번 포스팅에서는 상황에 맞는 유효성 검사를 수행하는 방법에 대해 알아보겠습니다. 

 

유효성 검사

  • 객체, 클래스, 상태 등이 올바르고 기대된 조건을 충족하는지 확인하는 방법
  • 입력 데이터, 객체 상태, 연산 결과 등이 유효한지 검증한다.
  • 디버깅, 런타임등의 다양한 환경에서 프로그램의 예기치 않은 동작 혹은 오류를 방지 할 수 있다.
  • 오류 발생 위치를 명확하게 알 수 있으므로, 디버깅 과정에서 시간을 절약 할 수 있다.
  • 코드의 신뢰성 및 유지보수성을 향상시킨다.
  • 객체가 삭제되거나, 메모리에서 해제된 상태에서도 참조하는 경우(Dangling Pointer)문제 방지 가능

 

== nullptr

  • 단순히 포인터가 nullptr인지 확인한다.
  • UE엔진의 Class, Actor, Object들은 모두 포인터 방식.
    • 객체 유효성 검사시 가장 쉬운 방법
  • GC객체를 감지하지 못해 IsValid보다는 안전하지 않다
  • GC가 필요하지 않은 경우에는 적합하다.
    • 삭제된 경우, !Actor은 유효할 수 있다고 판단 할 수 있다.
    • 일반 자료형(int32*, float*), 스마트포인터, UClass 리플렉션이 없는 클래스 등

 

IsValid

  • 동적으로 생성된 객체의 유효성 검사에 특화된 함수
  • UObject를 상속한 객체의 포인터에 대한 유효성 검사
  • GC에 의해 삭제되었는지 확인 가능
  • nullptr 과 GC를 IsValid를 사용하여 확인 할 수 있다.
  • nullptr만 확인하기에는 오버헤드가 발생할 우려가 있다.
    • 단순한 nullptr 체크는 == nullptr를 사용하는것이 더 효율적이다.
  • AActor, UActorComponent 객체가 동적으로 생성된 경우 상태를 확인하기 위해 사용한다.
    • 파괴된 액터에 접근하여 크래시를 방지한다.
    • 삭제 중(IsPendingKill()인 경우, IsValid를 사용하여 삭제를 취소 할 수있다.

 

IsPendingKill

  • GC에 의해 삭제 예정 상태, 삭제 중인지 확인하고 싶을 경우 사용한다.
  • 객체가 삭제 중이지만 메모리가 여전히 존재한 경우에 사용한다.
  • nullptr의 상황은 확인하지 않는다.
  • 삭제중인 액터에 접근시, 크래시 발생 우려가 있으므로 IsPendingKill를 사용하여 확인한다.
  • 타이머/델리게이트에서 삭제된 객체를 확인한다.

 

Check

  • UE에서 디버깅중 유효성 검사를 위해 사용한다.
  • 코드에서 특성 조건이 항상 참이어야 하는 경우에 사용한다.
  • 조건이 실패시에 에디터 크래시, Assert를 출력한다.
  • 디버거에서 해당 지점을 중단점으로 잡아 잘못된 코드경로를 빠르게 확인 할  수 있다.
  • 논리적으로 실행이 안되는 코드가 실행되었을 때 크래시를 발생시키는 경우
  • Shipping 빌드시에는 사용하지 않는다.
    • Shipping 빌드시에 런타임 유효성 검사는 Ensure가 더 적합하다.
  • 개발자의 실수로 인한 경우에도 크래시를 유발 할 수 있으므로, Ensure 또는 if문을 사용한 검사가 더 효율적일 수 있다.

Ensure

  • Check와 비슷하지만, 크래시가 아닌 경고만 출력한다.
  • 조건 실패시에도 계속 실행 될 수 있다.
  • 사용자 입력의 경우 실수 가능성이 있어 Ensure를 사용하여 조건이 실패해도 계속 실행해야 하는 경우 사용한다.

Succeeded

  • 함수 실행 결과가 성공했는지에 대해 확인한다.
  • FOptionalResult, FHitResult, ConstructorHelpers같은 함수 호출 결과를 검사하는 경우 사용한다.

 

Assert

  • 조건이 실패하면 디버거 브레이크 포인트를 트리거한다.
  • Shipping 빌드시 비활성화 된다.

 

Verify

  • Shipping 빌드에서도 조건 검사를 수행한다.
  • 디버깅 및 릴리즈 환경 모두에서 조건 검증이 필요한 경우에 적합하다.
  • 실패시에 Check와 같은 크래시가 발생한다.
    • Shipping빌드 환경에서는 크래시 대신 무시된다.
    • 실패해도 크래시 대신 검증 로그를 남기고 실행을 계속해야 할 때 사용한다.
  • 특정 조건이 항상 참이여야 하는 경우에 사용하여, 런타임 환경에서도 코드 안정성을 보장한다.

 

IsTemplate

  • 객체가 CDO인지 확인하고 싶을 때
  • 클래스의 기본 값을 설정하거나, 특정 작업을 CDO에만 적용해야 하는 경우에 사용한다.
  • 게임 플레이 도중 특정 로직이 CDO에서 실행하지 않도록 제한한다.

 

GEngine→AddOnScreenDebugMessage

  • 디버깅시 메시지를 출력하고 싶을 경우 사용한다.
  • 디버깅 중 상태를 확인하기 쉽다.
  • 런타임 디버깅용으로만 사용된다.

 

 

객체 클래스 검사 및 캐스팅시의 차이점  (IsA VS Cast<T>())

 

bool UObject::IsA(const UClass* Class) const;

  • UClass타입을 확인하고자 할 때 사용한다.
  • 객체가 해당 클래스이거나, 그 클래스에서 파생된 클래스인 경우 true 반환
  • 객체 타입을 확인할 때 사용한다.
  • 런타임시 타입 정보를 확인 할 수 있다.
  • 실제 클래스 타입을 확인할 때 사용한다.

 

Cast<T>(Obj)

  • 객체를 지정된 클래스 타입으로 캐스팅 한다.
  • 타입으로 캐스팅 된 객체 포인터를 반환한다.
  • 캐스팅이 실패한 경우 nullptr를 반환한다.
  • 객체를 특정 타입으로 변환하여 작업을 수행하고자 할 때 사용한다.

 

 

 

정리

  • 디버깅 : Check, Ensure, Assert
  • 런타임 조건 검증 및 경고 : Ensure, EnsureMsgf, EnsureAlways
  • UObject 포인터 유효성 확인 : IsValid, IsPendingKill, IsValidLowLevel
  • Shipping 빌드 : Verify, Succeeded
  • 디버깅 메시지 출력 : GEngine→AddOnScreenDebugMessage

'Unreal Engine > Debugging' 카테고리의 다른 글

[UE5] UObject 클래스 내 GetWorld Null 반환  (0) 2025.02.26

액터 (Actor)

  • 컨텐츠를 구성하는 기본 단위
    • class AActor : public UObject
  • 월드안에 배치되어 상호작용 하거나 동작할 수 있는것.
  • 입력처리를 위한 컨트롤러가 없다. 
    • Pawn, Character에서의 사용 가능성 및 GetOwner()를 사용한 컨트롤러 접근을 위해 Actor.h내 전방선언
    • Actor 기반의 Trigger를 참조하거나 상호작용하기위해
    • 입력 이벤트를 바인딩하여 입력처리가 가능하다 (문열기 상호작용 등)
  • 입력이 필요없거나 환경 요소 및 비활성 오브젝트 구현시 사용
  • Translation : SetActorLocation / AddActor[Local,World]Offset
  • Rotate : SetActorRotation / AddActorWorld[Local,World]Rotation
  • Transform : SetActorTransform(FTransform)

 

폰 (Pawn)

  • Actor를 상속받는 하위클래스
    • class APawn : public AActor, public INavAgentInterface
  • 컨트롤러를 통해 AI 조작 또는 플레이어 빙의가 가능한 객체
  • SetupPlayerInputComponent를 사용한 입력처리 가능
  • C++ Class 생성시 RootComponent는 nullptr이다.
    • 개발자가 명시적으로 Component를 추가해야한다.
  • 언리얼 엔진 내 내비게이션 기능을 통해 이동 제어 가능
    • NavMovementComponent 기반의 Component를 통해 제어
    • 월드 내 내비게이션 메시를 기반으로 경로를 계산한다. (NavMeshBoundsVolume)
    • AIController과 연동하여 MoveToLocation, MoveToActor를 사용한 이동 제어
  • AI Controller / Player Controller를 통해 조작이 필요한 객체들을 대상으로 사용한다.
    • 차량, 드론, 비행기 등
    • AI - 인간형이 아닌 오브젝트
  • Rotation : AddController[Roll,Pitch,Yaw]Input
  • AI : MoveToLocation / MoveToActor

 

 

캐릭터 (Character)

  • Pawn을 상속받는 하위클래스
    • class ACharacter : public APawn
  • 인간형 폰이며, 플레이어 또는 NPC의 주요 컴포넌트들을 포함하고 있다.
    • CapsuleComponent(RootComponent) : 충돌처리
    • SkeletalMeshComponent : 캐릭터의 3D모델 및 애니메이션 렌더링
    • CharacterMovementComponent : 이동(걷기, 점프, 중력, 앉기 등)처리
  • 멀티플레이 네트워크 환경에서 캐릭터들의 움직임을 자동으로 동기화 한다.
    • Character 생성자 내 리플리케이션 활성화가 되어있다. 
  • CharacterMovementComponent 내 추가 이동 함수
    • LaunchCharacter - 물리처리를 통한 이동
    • MaxWalkSpeed - 걷기 속도 변경
      • Crouch()시 기존의 AddMovementInput()과의 속도차이를 볼 수 있다.

 

 

Character.cpp 생성자 - Default Value, Default Component Setting

ACharacter::ACharacter(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
  // ...
  
	bUseControllerRotationPitch = false;
	bUseControllerRotationRoll = false;
	bUseControllerRotationYaw = true;

  // CapsuleComponent
	CapsuleComponent = CreateDefaultSubobject<UCapsuleComponent>(ACharacter::CapsuleComponentName);
	CapsuleComponent->InitCapsuleSize(34.0f, 88.0f);
	CapsuleComponent->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
	RootComponent = CapsuleComponent;

  // Arrow Component
	ArrowComponent = CreateEditorOnlyDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
	if (ArrowComponent)
	{
    // ...
	}

  // CharacterMovement
	CharacterMovement = CreateDefaultSubobject<UCharacterMovementComponent>(ACharacter::CharacterMovementComponentName);
	if (CharacterMovement)
	{
		CharacterMovement->UpdatedComponent = CapsuleComponent;
	}

  // SkeletalMesh
	Mesh = CreateOptionalDefaultSubobject<USkeletalMeshComponent>(ACharacter::MeshComponentName);
	if (Mesh)
	{
    // ...
	}

  // GravityDirection
	ReplicatedGravityDirection = UCharacterMovementComponent::DefaultGravityDirection;
}

 

언리얼엔진을 통한 드론을 구현하던 중, 마우스 X,Y에 대한 값을 입력받아 각각 Yaw와 Pitch를 회전하는 로직을 구현했습니다.

해당 방식에서는 bUseControllerRotationYaw,Pitch = true를 설정하여 컨트롤러 입력에 따른 Pawn이 회전 할 수 있도록 AddActorLocalRotation() 함수를 사용하였습니다.

제가 의도한 결과는 마우스 입력에 따른 Pawn이 각각 Pitch,Yaw만 움직이게 하는것입니다.

 

그러나 실행 결과는 Roll이 값이 같이 회전하는 문제가 발생하여 이러한 문제가 어떤것인지 학습하였습니다.

학습 결과 이러한 현상을 짐벌 락(Gimbal Lock)이라고 할 수 있으며, 이러한 문제가 발생한 이유에 대해 알아보았습니다.

 

이번 포스팅에서는 오일러 각과 짐벌 락, 해결방법 등에 대해 알아보겠습니다.

 

오일러각

Euler - Wikipedia

  • 3D회전을 세가지 축(X,Y,Z)을 기준으로 나눠서 표현하는 방식
  • 언리얼 엔진 내부에서 최종 업데이트시 행렬을 사용하는 것을 직관적이고 효율적으로 사용하기위해
    • 기존의 회전 행렬은 추가 데이터 요구 및 계산량이 많다.
  • Roll  (X축 회전)
  • Pitch(Y축 회전)
  • Yaw (Z축 회전)
  • X,Y,Z 세가지 축을 순서대로 돌린 각도의 정보를 저장하여 3개의 숫자로 표현하는 방식
  • 회전 각도를 숫자 (0~360 / -180 ~ 180)으로 표현하여 직관적이다.
  • 오일러 각의 회전 순서에 따라 다른 결과가 나올 수 있다.

오일러 각의 적용 순서

  • 오일러 각은 한번에 적용되는것이 아닌, 각 표전 기저 벡터 축을 중심으로 독립적으로 일어나는 회전
  • 3가지 축에 적용 순서를 어떠한 순서로 적용할 지에 따라 6가지의 경우의 수가 있다.
    • Yaw -> Pitch -> Roll
    • Yaw -> Roll -> Pitch
    • Pitch -> Roll -> Yaw
    • Pitch -> Yaw -> Roll
    • Roll -> Yaw -> Pitch
    • Roll -> Pitch -> Yaw
  • 언리얼 엔진 : Yaw -> Roll -> Pitch 순서의 각 적용
    • 실제 연산 순서 : Pitch -> Yaw -> Roll
    • FQuat(쿼터니언)은 축과 각도를 한번에 표현하여 회전 순서와 무관하게 정확한 회전 제공
  • 적용 순서에 따라 축이 변경되며, 최종 회전 방향 적용 순서에 따라 결과가 다르다.

 

FRotator : Engine/Source/Runtime/Core/Public/Math/Rotate.h

struct TRotator
{

	// Can't have a UE_REQUIRES in the declaration because of the forward declarations, so check for allowed types here.
	static_assert(std::is_floating_point_v<T>, "TRotator only supports float and double types.");

public:
	using FReal = T;

	/** Rotation around the right axis (around Y axis), Looking up and down (0=Straight Ahead, +Up, -Down) */
	T Pitch;

	/** Rotation around the up axis (around Z axis), Turning around (0=Forward, +Right, -Left)*/
	T Yaw;

	/** Rotation around the forward axis (around X axis), Tilting your head, (0=Straight, +Clockwise, -CCW) */
	T Roll;

 

언리얼 엔진의 실제 연산 순서 : Engine/.../Math/UnrealMath.cpp

template<typename T>
UE::Math::TRotator<T> UE::Math::TRotator<T>::MakeFromEuler(const UE::Math::TVector<T>& Euler)
{
	return UE::Math::TRotator<T>(Euler.Y, Euler.Z, Euler.X);
}



 

 

 

짐벌락

짐벌(Gimbal)

Gimbal - Wikipedia

  • 물체가 회전하도록 중심축을 가진 구조물
  • 3차원 공간에 놓인 강체의 방향은 오일러 각도를 사용하여 세번의 회전을 통해 얻을 수 있다.

 

짐벌락(Gimbal Lock)

  • 오일러 각을 사용한 회전 중 두 개 또는 세개의 축이 겹치게되어 회전각이 소실되는 상황
  • 특정 방향으로 회전하려고 할 때 의도하지 않은 회전이 발생하거나, 특정 방향으로의 회전이 불가능해지는 문제
  • 고리가 다른 고리에 자식으로 붙어있는 관계이기때문에 각 축은 서로 종속적이다.

 

짐벌락 발생 예시

  1. 초기상태 (Yaw 30, Pitch 45, Roll 0)
  2. Pitch += 90 회전 (Yaw 0, Pitch 90, Roll 0) -> Yaw Roll 축이 겹쳐진 짐벌락 발생
    1. X축과 Z축이 동일한 평면에 놓이게 된다.
  3. Yaw / Roll 적용
    1. Z축과 Y축이 같은 축을 기준으로 회전
    2. 회전 방향이 원화는 회전을 표현하지 못하거나, 왜곡된다.

 

해결방법

  • Pitch 제한
    • 값을 +- 90 범위로 제한하여 짐벌락 상황을 제외하는 방어코드 작성
  •  쿼너니언(Quaternion)
    • FQuat
    • 오일러 각처럼 순차적으로 회전을 적용하지 않는다.
    • 회전 축(Vector3)과 각도(Scalar)를 단일 벡터로 표현하므로, 회전 순서에 영향을 받지 않는다.
  • 행렬 (Matrix)
    • FMatrix, FTransform
    • 회전 순서를 적용하지 않고, 모든 회전을 동시에 처리
    • 새로운 회전의 대한 연산은 기존행렬과 곱셈으로 누적
    • 회전이 축에 독립적으로 표현되어 정렬로 인한 문제 발생하지 않음

 


출처

 

https://daekyoulibrary.tistory.com/entry/%EA%B2%8C%EC%9E%84-%EC%88%98%ED%95%99-18-%EC%98%A4%EC%9D%BC%EB%9F%AC-%EA%B0%81Euler-angle

 

[게임 수학] #18 | 오일러 각(Euler angle)

*인프런  강의를 듣고 공부한 글입니다.   1. 회전 행렬 구현에서의 문제점3차원 공간에는 세 개의 표준 기저 벡터 \(e_1\), \(e_2\), \(e_3\) 가 존재합니다. 이 세 개의 기저 벡터를 통해 회전을 구현

daekyoulibrary.tistory.com

https://continue96.tistory.com/138

 

게임 수학 | 3부 3차원 콘텐츠 제작 | 10장 3차원 공간

10장 3차원 공간: 입체 공간의 생성10.1 3차원 공간의 설계3차원 공간 좌표계는 두 가지로 구분한다.오른손 좌표계(right-handed coordinate system)왼손 좌표계(left-handed coordinate system) 3차원 공간 좌표계를

continue96.tistory.com

 

'Unreal Engine > Etc' 카테고리의 다른 글

UE5 - Destructible Mesh(Dynamic Mesh Component)  (0) 2024.12.27

+ Recent posts