팀 프로젝트를 완성시키며 발표, 종료를 하며 KPT 회고 진행시간이 있었습니다.

언리얼 엔진을 사용한 첫 프로젝트에서 진행된 만족스러운점, 아쉬운점, 그리고 해결방법들을 회고하는 시간을 가지게 되었습니다.

회고록을 진행하며 다음 프로젝트는 어떻게 기획, 설계해야하는지, 어떤것들을 기록해야하는 것인지를 스스로 학습 및 리마인드 하게 되었으며 해당 내용들을 포스팅을 통해 남기고자 합니다.

이번 포스팅에서는 팀 프로젝트를 진행하며 KPT 회고록 및 프로젝트 진행동안의 개인 피드백을 작성하겠습니다. 

 

KPT 회고

Keep : 현재 만족하고 있는 부분

Problem : 불편하게 느끼는 부분

Try : Problem에 대한 해결책, 당장 실행 가능한 것

 

프로젝트 개인 피드백

UI Part

  • 모작 게임에 대한 레퍼런스를 가지고 있음에도 추가 기능에 대한 미구현.
    이론적인 학습을 위주로 함으로써 개발속도가 느리다고 느껴졌다.
    • 기획단계에서 각각의 기능에 대한 제작 기간을 설계함으로써 기능 구현에 대한 타임라인 만들기
    • 개인프로젝트를 진행하며 개발속도 높이기
  • MVVM 설계 기반이지만, WidgetComponent를 활용한 HUD의 경우에는 MVC패턴의 구조다.
    초기 설계시 구체적인 설계를 하지 않음으로써 발생하는 문제들이 많다.
    • 기획단계에서 어떻게 구현할 것인지에 대한 UML, 시퀀스 다이어그램 작성하기
    • 개발자로써의 설계 및 기획하기 (장르, 담당파트만 정하지 않기)
  • 메모리 관리를 효율적으로 하지 않았다.
    • GC가 관리하지 않는 포인터 참조 변수들에 대한 소멸자 메모리 관리 습관화 하기
  • 최적화에 대한 근거가 없다.
    • 기능 구현 후 단위테스트 진행, 프로파일을 통한 성능 비교 후 우선순위 기록하기
    • 최종 단계에서 기대한 성능이 나오지 않는것들에 대한 최적화 리팩토링 작업
  • 일부 공통기능에 대한 유지보수성 낮은 로직
    • 객체지향 언어의 특징 및 원칙 기반의 설계 진행하기
  • 트러블슈팅을 진행하였지만, 기록하지 않았던 점
    • 발생하는 문제에 대한 원인, 해결 과정, 해결 결과에 대한 트러블슈팅 기록 습관화 하기

 

팀 프로젝트 회고

Keep

  1. 팀원들과 협업하며 프로젝트를 완성했다.
    • 초기 프로젝트 설계시에 필수/도전기능에 대한 구현을 성공적으로 구현. 책임감을 성취했다.
  2. 프로젝트를 진행,기획 하며 학습하지 않은 부분들을 주도적으로 찾아보며 프로젝트에 적용한 점
    • 프로젝트를 진행하며 담당 파트에 대한 추가 학습, 언리얼 기술 스택 향상
    • 주어진 동일한 기능 구현에 대한 심도있는 개발 능력
  3. 지속적인 팀 회의, 좋은 분위기
    • 왜 협업을 하는지에 대한 이유를 알 수 있었던점
    • 긍정적인 분위기 및 원활한 팀회의를 통한 빠른 문제 해결 능력 제공 및 추가 학습
  4. 확장성을 고려한 객체지향적인 개발
    • 당장의 게임 완성을 위한 하드코딩이 아닌, 확장성을 고려한 객체지향적인 코드 작성
    • 게임 내부가 완성도가 높았다.
  5. 담당파트의 구현 로직이 의도한대로 설계된 점
    • Stat FPS를 사용한 프로파일링 및 기획시 의도한 레벨 전환 구현으로 성취감 달성
  6. 아침/저녁 시간마다 회고를 진행한 부분
    • 회고를 통해 자기 자신이 얼마나 학습했는지에 대한 인지
    • 팀원들의 학습을 공유받으며 담당 파트 외에도 추가적인 학습기회 제공
  7. 협업시 Git을 활용한 스킬 향상
    • Git을 활용한 컨벤션에 대한 종류 및 장단점 학습
    • Branch Merge시 발생하는 Conflict원인 및 해결과정 등 병합하는 과정에 대한 이해도 향상
  8. 상황에 맞는 방어코드를 작성함으로써 빌드 오류가 발생하지 않은점.
    • 게임 실행시 크래쉬가 발생하지 않는 방어코드 작성. 패키징 고려한 코드 작성
    • 예외 발생시에 대한 방어코드 및 메모리관리를 철저하게 한 프로그램 내적 완성도 향상
    • 오류가 발생할 수 있는 부분에 대한 참조메모리, 콜스택들을 확인하며 트러블 슈팅을 할 수 있는 방법 학습
    • 어떠한 상황에 Equals, Valid, Check, Ensure를 사용하는지에 대한 방어코드 차이점 학습

Problem 

  1. 추가기능을 대부분 구현하지 않은 점
    • 구현할 수 있는 추가기능들이 다양했지만, 프로젝트 완성까지 적용/시도 하지않았다.
  2. 완성된 기능에 대한 추가적인 개발을 하지 않은점
    • 코드에는 정답이 없다. 다른 방법으로 개발을 할 수 있었는데 하지 않았던 점
    • 코드 리팩토링을 추가적으로 하지 않았음
  3. 언리얼 엔진 내부에 대한 이해도 부족
    • 메모리 관리에 대한 이해도 부족으로 인한 메모리 해제를 제대로 적용하지 않은 점
    • 게임 인스턴스 클래스를 활용한 Object Pooling 적용 실패
    • 액터 라이프사이클에 대한 이해도 부족으로 인한 의도치 않은 결과 발생
  4. 느슨한 초기 기획 설계
    • LFS에 대한 이해도 부족으로 인한 Config 세팅을 개별로 적용하던 점
    • UML 클래스 다이어그램, 초기 GameMode 파일 등을 기획하지 않아 병합시 문제가 발생한점
    • 중복되는 파일,DA,DT들이 존재, 유지보수성이 낮아진 부분
    • Content,Source 폴더 내 디렉토리 구조가 준수되지 않아 팀원들의 파일을 찾기 힘든점
  5. 내부적으로 존재하는 중복되는 코드들에 대한 유지보수성
    • 프로젝트 완성을 목적으로 중복되는 코드들을 리팩토링 하지 않은점
  6. 트러블 슈팅에 대한 기록을 하지 않은점
    • 프로젝트 발표시 트러블 슈팅에 대한 정보 누락
    • 팀 회의를 통해 트러블 슈팅 원인, 해결과정을 인지하지만, 기록하지 않아 리마인드가 힘듬
  7. 최적화에 대한 근거 부족
    • 각자 최적화를 하기위한 설계를 하였지만, 이론상으로만 최적화가 좋아짐.
    • Profile CPU,GPU / Stat Fps를 활용한 성능 비교를 실행하지 않음

Try

  1. 추가기능을 대부분 구현하지 않은 것
    • Render Text와 같이 구현하지 않은것들, 피격 이펙트, 사운드와 같은 추가기능들을 초기 설계시에 디테일하게 기획한다.
    • 프로젝트 제작 기간에 맞는 계획 기간에 맞춘 설계 구현
  2. 완성된 기능에 대한 추가적인 개발을 하지 않은 것
    • 팀원간 회의를 통한 상시 피드백
    • 코드 리뷰를 통한 추가적인 기획 피드백
  3. 언리얼 엔진 내부구조에 대한 이해도 부족으로 인한 문제점
    • 부족한 내용들에 개념을 공식문서, 강의를 통한 추가 학습
    • 팀원들과 공유를 함으로써 개인적인 성장이 아닌 팀 위주의 학습 성장 태도
  4. 느슨한 초기 기획 설계
    • 게임 장르에 대한 설계만이 아닌 개발자로써의 기획 설계 추가
    • 각자의 아이디어를 제공, 적극적인 태도
  5. 내부적으로 존재하는 중복되는 코드 (유지보수성 관련)
    • 객체지향언어의 특징 및 원칙을 추가학습하며 적용하도록 습관화 하기
  6. 트러블 슈팅에 대한 기록을 하지 않은것
    • 기록 습관화
    • 발생 원인, 해결 과정, 해결 결과 세가지를 기록해두기(TIL 등)
  7. 최적화에 대한 근거
    • FPS, CPU, GPU 사용량을 개발하며 체크, 어떤것에서 많이 문제가 발생했는지 기록하기
    • 프로젝트 완성 후 최적화 작업 진행하기
    • 메모리 관리
  1.  

개발블로그에 작성하지 않았던 설계에 맞춘 구현을 했던 위젯들에 대해 작성하겠습니다.

개발 시 작성한 코드들에 대해서는 하단에 깃허브를 통해 첨부를 하겠습니다.

 

프로젝트 기획 시 UI 구현 목록

 

설계시 공통 작업

  • WBP 위젯들은 UUserWidget을 상속받은 각각의 클래스들을 통해 생성한다.
    • 성능 최적화 및 MVVM 설계의 Model 계층과의 유연한 데이터 수신을 위해
  • GameMode/GameInstance/PlayerController 등 런타임 환경에 잠시 생성되는 위젯들은 인스턴스 형식으로 생성한다.
    • Slate UI에 대한 개념을 이해하며, 생성 후 해당 함수 외에서 호출하지 않는 경우는 인스턴스 형식으로 생성.
  • 최적화작업을 위해 이벤트 기반의 로직을 구현한다.
    • HP가 변경되는 등의 단순한 UI들은 HP가 변경되는 순간 Broadcast를 통한 데이터 전송을 통한 최적화 작업
  • 객체지향 특징과 MVVM 아키텍처 기반의 설계
    • 느슨한 결합성을 유지하여 특정 View 추가/삭제시에 해당 UI외 다른 클래스들은 영향을 받지 않는다.
    • 유지보수성이 향상되며 위젯들은 독립적으로 실행 할 수 있다.
  • Path 경로 변경 시 문제를 방지하기위한 DataAsset/DataTable를 사용한 데이터 관리
    • 각각의 필요한 DA/DT를 생성, 관리하며 필요없는 데이터들을 포함하는것을 방지
    • 필요한 데이터들은 PK담당을 하는 값들을 Contains,IsA,Equals등의 비교를 통해 로직 내 연동처리
  • Widget들은 UUserWidget를 상속한 C++클래스에서 제작하며, 디자인을 위젯 블루프린트를 통해 작업한다.
    • GameMode,Instance등 연동시 유연한 작업을 위해, 디자인의 경우는 효율적으로 작업할 수 있다. 

 

1. Lobby

  • NativeConstruct
    • 위젯 생성시 FInputModeDataBase를 UIOnly로 변경, UI에서만 키 입력을 받을 수 있게 변경한다.
    • 생성된 위젯의 SetGamePause(GetWorld(),true)를 사용한 게임 정지
    • SetFocusable를 통한 키입력 추가 설정.
  • NativeOnKeyDown
    • 위젯이 생성 된 후 어떠한 Key를 입력받아도 해당 위젯을 닫을 수 있게 설계
    • 사용한 PlayerController의 FInputMode를 GameOnly 변경
    • 게임 속행을 위한 SetGamePause(GetWorld(),false) 변경
    • RemoveFromParent() 호출을 통한 Slate UI 내 제거, GC가 관리할 수 있게 변경하여 메모리 최적화
      • RemoveFromViewport()는 더이상 사용하지 않는 함수. 권장하지 않으며(Deprecate), 호출 시 해당 함수 내부에서 RemoveFromParent()를 호출한다.
    • SetFocusable 해제.
    • GameMode 내 설계한 레벨 변경 함수를 통한 다음 Level로 이동

 

 

2. InGame Player HUD

 

  • InGameMainWidget Class가 VM역할을 담당하며 각각의 위젯들을 관리한다.
    • 각각의 Minimap, PlayerStatus, Weapon, Crosshair등 위젯들에 대한 이벤트 바인딩 작업
    • Player,Monster과 같은 Model계층에서 이벤트 방식으로 전달한 데이터들에 대한 데코레이터 작업 후 전달.
      • Delegate는 옵저퍼 패턴을 기반으로 구현 할 수 있으며, AddDynamic을 통한 바인딩 된 함수들을 통해 추가작업을 하여 데코레이터 패턴 적용이 가능하다.
    • 불필요한 유효성 검사를 제거하기위한 리플렉션 및 meta = (BindWidget)을 통해 WBP내에서 바인딩 체크
    • IsBound를 통한 이벤트 및 위젯 바인딩 체크. 중복 바인딩 방지, 최적화 작업
    • 각각의 클래스 분리를 통한 객체지향 원칙 준수 및 유지보수성 향상

 

3. SelectWidget

 

  • 특정 조건에 따라 (상점 방문, 레벨업 등) 생성되는 VM담당 위젯
  • ItemWidgetBase를 기반의 Card/Item... Widgets 들을 상황에 맞게 생성한다.
    • 동일하게 가지는 ObjectName, Texture, Description, Button 등을 쉽게 관리 할 수 있다.
  • 해당 위젯에서는 Background Image만을 가지고 있으며, HorizontalBox/Overlay를 사용한 하위클래스 위젯 배치
  • 각각의 Button을 입력받을시 해당 위젯을 RemoveFromParent를 호출한 삭제, Player에게 해당 정보 전달

 

4. Monster HUD

  • WidgetComponent를 사용한 부착된 BaseEnemy에 대한 정보를 보여준다.
  • MVVM 기반이 아닌, MVC 기반의 연동
    • WidgetComponent는 View를 담당하지 않는다. Controller의 역할에 더 적합하다.
  • Raycast / 체력 변화 관련 Event 발생시에만 활성화 한 최적화 작업
    • TimerHandle를 사용한 Visibility 세팅

 

5. PlayAnimWidget

  • Key 입력 또는 특정 상황에 맞는 애니메이션 연출
  • Ex) Dash 2D Effect, Hit Maker, Ending Credits...
  • 게임의 시각적 재미를 위한 추가 및 PlayAnimation, IsAnimationPlaying을 통한 애니메이션 중복 실행 제어

 

6. View (Minimap, WeaponStatus, Card ... )

  • VM 계층에서 전달받은 데이터들을 통해 Widget의 데이터들을 변경한다.
  • Model <-> ViewModel <-> View 설계로 인해 낮은 결합성을 유지하며, 클라이언트에게 보여주는 계층
  • 각 View들은 객체지향적으로 설계하여 독립적이며 각각의 책임만을 가질 수 있는 설계
    • PlayerStatus는 플레이어 체력/텍스처/실드 등을 관리, Weapon은 탄환, WeaponTexutre만 관리 등. 

 

Git Code Link

 

 

 

팀프로젝트를 진행하며 레벨간 전환시 월드에 액터들을 로드하는 시간동안 게임이 멈춘것처럼 진행하지 않았습니다.

이를 해결하기위해 로딩 스크린을 만들게되었으며 로딩 스크린이 출력하는 동안 언리얼 엔진 내부에서는 월드에 필요한 액터들을 로드하는 작업을 하기위해 비동기 방식으로 로딩 스크린을 구현하게되었습니다.

기존에는 언리얼 C++ 내부에서 비동기 방식의 로딩 스크린을 구현하고자 하였으나, 이를 위해 학습하던 중 

해당 플러그인을 사용하여 간단하게 로딩 스크린을 출력할 수 있게 되어 포스팅하게 되었습니다.

 

ASync Loading Screen Plugin - 언리얼 마켓플레이스

  • 비동기 레벨 로딩 중 커스텀 로딩 화면을 제공하는 플러그인
  • 2023 Epic MegaGrants  수상 및 여러 쇼케이스에서 우수 플러그인 선정
  • 비동기 로딩 (Load Stream Level())을 활용한 비동기 로딩 중 자동으로 로딩 화면 표시 기능
  • 배경이미지, 로딩 애니메이션 등 커스텀 UI 지원
  • 게임 시작 시 또는, 레벨 전환시마다 설정에 따른 로딩 화면 출력 가능
  • 프로젝트 세팅 내에서 간편하게 로딩 스크린 구현 가능

 

연동 과정

 

  • 마켓 플레이스 내 해당 플러그인 라이브러리 추가
  • 에픽 게임즈 내 사용할 엔진 버전에 플러그인 설치
  • Edit - Plugin 내 Async Loading Screen 추가 후 재시작
  • 연동 완료

 

적용 예시

 

Project Settings 내 Async Loading Screen을 사용한 설정이 가능하다.

 

  • Preload Background Images
    • 게임이 시작 시 Background Images로 추가한 이미지들을 미리 불러온다.
    • 하지만, 시작 시 로딩 스크린 외 스타트 스크린이 존재하지 않을 경우 처음 로딩스크린이 로드하는 시간동안 해당 미지가 출력되지 않는 현상이 있다.
  • StartUp Loading Screen
    • 게임 시작 시 한번만 보여줄 화면에 대한 설정
  • Default Loading Screen
    • 레벨 전환 간 로드시 설정한 값들을 보여주는 설정
  • LayOut
    • 화면 전환 간 Text, SideBar 등 Tip과같은 설명에 대한 추가 기능

 

 

  • BeginPlay에 ASync Loading Screen 관련 블루프린트 설정 추가
  • 독립형 게임 / 디바이스 내 실행
    • 뷰포트 실행의 경우 지원하지 않음

 

연동결과

  • 레벨 전환간 해당 로딩 스크린이 출력된다.
  • 설정에 따른 텍스트, 최소 스크린 출력시간, 랜덤 이미지 출력 등 다양한 지원 가능

이렇게 간단하게 해당 플러그인에 대해 작성해보았습니다.

장점은 간편하게 플러그인을 통한 비동기 로딩 스크린을 출력할 수 있었다는 좋은점이 있었지만,

로딩 시 얼마나 진행되었는지를 출력하는 로딩 바 같은 기능은 지원하지 않았습니다.

또한 뷰포트 내 실행시 사용이 불가능 한 점, C++클래스를 사용한 호출이 불가능 한 점 등 아쉬운 부분도 존재하였지만

사용할 로딩 이미지 외 동영상같은 출력 기능등을 지원함 및 블루프린트를 통한 빠른 테스트같은 장점이있었습니다.

이번 포스팅에서는 로딩스크린이 왜 비동기로 출력해야하는지, 그리고 C++를 사용한 유연한 비동기 스크린 제작 같은 과정을 추가하지않았지만, 다음에 로딩 스크린을 구현하게되면 C++내 Load Stream Level()와 같은 기능을 사용하여 상세하게 다뤄보도록 하겠습니다.

 


출처 및 참고내역

https://github.com/truong-bui/AsyncLoadingScreen  

https://www.youtube.com/watch?v=TRpFxT9zGu8

실행 결과

 

 

MVVM패턴 / C++  과정

  • 델리게이트를 사용하여 액터 클래스 / 거리를 건내주는 ActorComponent 기반의 클래스
  • ActorComponent를 부착할 플레이어/몬스터 클래스
  • ViewModel 역할의 MainWidget 클래스 (UUserWidget)
  • View 역할의 MininapWidget 클래스 (UUserWidget)
  • Minimap 내 배치할 MinimapIcon 클래스 (UUserWidget)

※ 미니맵에 현재 LandScape를 보여주기 위해서는 Player에게 카메라를 부착, RenderTarget을 사용한 Texture를 사용.

 

MinimapTracker (ActorComponent)

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnActorLocation, ACharacter* ,Target, float, Distance);

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class GUNFIREPARAGON_API UMinimapTracker : public UActorComponent
{
	GENERATED_BODY()
protected:
	virtual void BeginPlay() override;
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

private:
	FVector LastLocation;
	void CheckLocationChange(float DeltaSeconds, FVector OldLocation, FVector OldVelocity);

	UFUNCTION()
	void OnOwnerMoved(float DeltaSeconds, FVector OldLocation, FVector OldVelocity);
		
public:
	static FOnActorLocation OnActorLocation;
};

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

	ACharacter* Character = Cast<ACharacter>(GetOwner());
	if (Character)
	{
		LastLocation = Character->GetActorLocation();

		Character->OnCharacterMovementUpdated.AddDynamic(this, &UMinimapTracker::OnOwnerMoved);
	}
}

void UMinimapTracker::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);

	ACharacter* Character = Cast<ACharacter>(GetOwner());
	if (Character)
	{
		Character->OnCharacterMovementUpdated.RemoveDynamic(this, &UMinimapTracker::OnOwnerMoved);
	}
}

void UMinimapTracker::CheckLocationChange(float DeltaSeconds, FVector OldLocation, FVector OldVelocity)
{
	ACharacter* OwnerCharacter = Cast<ACharacter>(GetOwner());
	ACharacter* PlayerCharacter = Cast<ACharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));

	if (!OwnerCharacter || !PlayerCharacter) return;

	FVector CurrentLocation = OwnerCharacter->GetActorLocation();
	float Distance = FVector::Dist(CurrentLocation, PlayerCharacter->GetActorLocation());

	if (!CurrentLocation.Equals(LastLocation, 5.0f))
	{
		OnActorLocation.Broadcast(OwnerCharacter, Distance);
		LastLocation = CurrentLocation;
	}
}

void UMinimapTracker::OnOwnerMoved(float DeltaSeconds, FVector OldLocation, FVector OldVelocity)
{
	CheckLocationChange(DeltaSeconds, OldLocation, OldVelocity);
}

 

FOnActorLocation Delegate

  • Target : 미니맵 아이콘을 적용할 대상(Actor Component)를 가지고 있는 Owner를 전송
  • Dist : 게임 프로젝트 기획 시 싱글프로젝트 기반. PlayerController Index 0 빙의 캐릭터와의 거리를 계산

OnCharacterMovementUpdated

  • Character 내 구현되어있는 델리게이트 시그니처. DeltaSecond, OldLocation, OldVelocity 반환
  • 매 Tick마다 전송을 하는것이 아닌, 캐릭터가 이동했을 경우 데이터를 전송함으로써 최적화 하기 위해 사용

OnOwnerMoved / CheckLocationChange

  • 이벤트 바인딩 및 필터링 / OnActorLocation 내 BroadCast하기 전 계산 로직
  • OnOwnerMoved는 이벤트 바인딩 역할하여 전송만 하지만, SRP 및 확장성을 고려하여 분리

현재 로직의 문제점

  • 캐릭터가 움직임을 감지했을 경우 (OnCharacterMovementUpdated)가 호출된 경우만 값이 변경된다.
  • 초기 생성 위치를 전송하지 않는다 / Character가 소멸될 경우에만 제거된다.
    •  BeginPlay시 BroadCast 추가 / Character isHidden() == true 시 BroadCast.

 

MainWidget

UFUNCTION()
void OnMinimapUpdated(ACharacter* Target, float Distance);

// Minimap Rendering Character Distance
float MaxRenderDistance = 1600.f;

void UUserWidget::OnMinimapUpdated(ACharacter* Target, float Distance)
{
	if (Distance <= MaxRenderDistance)
	{
		if (!MinimapWidget->ActiveIcons.Contains(Target))
		{
			MinimapWidget->AddMinimapIcon(Target);
		}

		MinimapWidget->UpdateActorIcon(Target, Target->GetActorLocation());
	}
	else
	{
		MinimapWidget->RemoveMinimapIcon(Target);
	}
}
  • ActorComponent에서 BroadCast한 데이터들을 Minimap에 전송하는 단계.
  • 델리게이트 시그니처를 바인딩하여 해당 함수를 통해 Model 데이터들을 View계층에 전달한다.

 

MinimapIcon

UCLASS()
class GUNFIREPARAGON_API UIngameMinimapIcon : public UUserWidget
{
	GENERATED_BODY()
	
public:	
	UFUNCTION()
	void SetIconTexture(UTexture2D* Texture);
	
	UFUNCTION()
	void SetVisibilityBasedOnDistance(float Distance, float MaxDistance);

protected:
	UPROPERTY(meta = (BindWidget))
	class UImage* IconImage;
};

void UIngameMinimapIcon::SetIconTexture(UTexture2D* Texture)
{
	if (IconImage)
	{
		IconImage->SetBrushFromTexture(Texture, true);
	}
}

void UIngameMinimapIcon::SetVisibilityBasedOnDistance(float Distance, float MaxDistance)
{
	SetVisibility(Distance > MaxDistance ? ESlateVisibility::Hidden : ESlateVisibility::Visible);
}

 

미니맵에 배치할 아이콘에 대한 위젯.

  • SetIconTexture - 동적으로 액터에 적합한 텍스처를 변경하기 위함
  • SetVisibilityBasedOnDistance - 플레이어와의 거리에 따른 아이콘 상태 변경

 

Minimap.cpp

// 전송받은 Target에게 어떤 아이콘을 입힐지, 디자이너 패널 내 어디에 배치할지 결정하는 로직
void UInGameMinimap::AddMinimapIcon(ACharacter* Target)
{
	if (!Target || ActiveIcons.Contains(Target) || !MinimapIconClass) return;

	UIngameMinimapIcon* NewIcon = CreateWidget<UIngameMinimapIcon>(this, MinimapIconClass);
	if (NewIcon)
	{
		RenderCanvas->AddChild(NewIcon);
		ActiveIcons.Add(Target, NewIcon);

		if (Target == PlayerCharacter)
		{
			NewIcon->SetIconTexture(PlayerIconTexture);
		}
		else
		{
			NewIcon->SetIconTexture(EnemyIconTexture);
		}
		int32 ChildCount = RenderCanvas->GetChildrenCount();

		UpdateActorIcon(Target, Target->GetActorLocation());
	}
}

// ICon 삭제에 관련된 로직
void UInGameMinimap::RemoveMinimapIcon(ACharacter* Target)
{
	if (UIngameMinimapIcon** IconPtr = ActiveIcons.Find(Target))
	{
		if (UIngameMinimapIcon* Icon = *IconPtr)
		{
			Icon->RemoveFromParent();
		}
		ActiveIcons.Remove(Target);
	}
}

// 특정 타겟의 거리계산 및 아이콘 배치 위치, 렌더링 여부
void UInGameMinimap::UpdateActorIcon(ACharacter* RenderTarget, FVector WorldLocation)
{
	if (!RenderTarget || !RenderCanvas) return;

	FVector2D MinimapPosition = IconRenderPosition(WorldLocation);

	if (ActiveIcons.Contains(RenderTarget))
	{
		UIngameMinimapIcon* Icon = ActiveIcons[RenderTarget];

		Icon->SetRenderTranslation(MinimapPosition);
		float Distance = FVector::Dist(PlayerCharacter->GetActorLocation(), WorldLocation);
		Icon->SetVisibilityBasedOnDistance(Distance, MaxRenderDistance);
	}
}

// 플레이어의 전방벡터를 받아와, 미니맵에 렌더링되는 방향을 계산한다.
FVector2D UInGameMinimap::IconRenderPosition(FVector WorldLocation)
{
	if (!PlayerCharacter || !RenderCanvas) return FVector2D::ZeroVector;

	FVector Offset = WorldLocation - PlayerCharacter->GetActorLocation();
	Offset *= MinimapScale;

	FVector Forward = PlayerCharacter->GetActorForwardVector(); 
	FVector Right = PlayerCharacter->GetActorRightVector(); 

	float RotatedX = FVector::DotProduct(Offset, Forward);
	float RotatedY = FVector::DotProduct(Offset, Right);

	FVector2D MinimapCenter = FVector2D(RenderCanvas->GetCachedGeometry().GetLocalSize()) * 0.5f;
	FVector2D MinimapPosition = MinimapCenter + FVector2D(RotatedY, -RotatedX);

	UE_LOG(LogTemp, Warning, TEXT("WorldLocation: (%.1f, %.1f) -> MinimapPosition: (%.1f, %.1f)"),
		WorldLocation.X, WorldLocation.Y, MinimapPosition.X, MinimapPosition.Y);

	return MinimapPosition;
}

 

 

※ 현재 로직은 하드코딩된 상태입니다. (확장성을 고려하지 않은상태)

 

UpdateActorIcon

  • 파라미터 타겟에 대한 미니맵과 뷰포트간 실시간 동기화를 위한 로직
  • 해당 로직을 ActiveIcons에 삽입되어있는 Target들을 매 Tick마다 이동값을 계산하여 정합성을 만족한다.

Add/RemoveIcon

  • ActorComponent에 대한 값을 전달받은 후 어떤 텍스처를 입혀줄지 결정하는 단계 / 제거하는 단계
  • 플레이어 / 그외 플레이어(몬스터) 에 대한 렌더링만을 담당한다.
  • 그 외 액터를 추가하여 텍스처를 결정하고 싶은 경우 DataAsset/Table를 활용한 Contains / IsA 검증 후 텍스처 결정.

IconRenderPosition

  • 플레이어를 미니맵 내 정중앙 배치. (MinimapCenter)
  • 플레이어의 전방벡터와 거리를 계산하여 렌더링 할 다른 캐릭터의 위치값 계산, 변환
    • WorldLocation (FVector 3D) -> WidgetLocation (FVector 2D)

 

https://www.youtube.com/watch?v=vBKFwI_JZpE

 

 

출처 및 학습자료


https://www.youtube.com/watch?v=xWbJiCSwxSE&list=PLNTm9yU0zou6xdEqL2QSSfanaZ3KA_vU7&index=4

지난번 포스팅에서 팀 프로젝트에 대한 기획 중 어떤것들을 구현할 것인지에 대해 간단한 설계를 하였습니다.

이번 포스팅에서는 각각의 요소에서 어떤 기능들을 구현할 것인지, 그리고 해당 클래스들을 어떻게 관리할 것인지에 대해 기획한 프로젝트 내 UI들에 대해서는 지난번 포스팅한 MVVM 디자인 패턴을 적용할 것이며, 어떤 방식으로 적용할것인지에대해서는 UML 클래스 다이어그램을 사용하여 작성하겠습니다.

프로젝트를 진행하며 주요 인게임 내 주요 기능에대한 설계만을 포스팅하였습니다.

※ - FPS 팀프로젝트 초기 설계

※ - MVVM이란?  

 

UML 클래스 다이어그램 - InGame

 

 

인게임 - HUD

Gunfire Reborn - 인게임 주요 HUD

 

InGame내 항상 표현하게되는 HUD입니다.

각각 View 계층 역할을 담당하는 UUserWidget 기반의 C++클래스내 UTextBlock, UProgressBar, UImage 등을 

BindWidget을 사용하여 위젯 블루프린트 내 바인딩이 되어있지 않은 경우를 컴파일을 제한하는 형식의 유효성 검사를 언리얼에서 진행합니다.

따라서 해당 HUD 내 동적으로 변경되는 위젯들에 대해서 개발자가 유효성 검사를 하지 않아도 되어 유지보수성 및 코드가독성이 향상되었습니다.

 

MainWidget (ViewModel)

  • Model에서 동적으로 변화되는 값들을 View계층으로 전달하기 위한 위젯 클래스
  • 인게임 내 주요 HUD 및 동적으로 Create,Remove되는 위젯들을 관리
  • 생성보다 Visibility설정을 통한 성능이 좋은 경우 : BindWidget
  • 동적인 환경에서 Create,Remove하는게 성능이 좋은 경우 : TSubclassOf<UUserWidget>, Cast
  • Player, Monster에서 선언한 델리게이트들에 대한 이벤트 바인딩 진행, View계층에 전달.

 

  • Minimap
    • 현재 진입한 레벨의 이름 출력
      • GetWorld()->GetMapName()을 사용한 현재 레벨이름 가져오기.
      • TextBlock->SetText()를 사용한 현재 레벨 출력
    • 현재 진입한 레벨의 진행 시간
      • UGamePlayStatics::GetRealTimeSecond()를 사용한 진행 시간.
      • Text 형식 ("%02d / %02d") , Minutes, Seconds
    • 현재 플레이어 위치 기반의 미니맵 구현
      • DataAsset 또는 DataTable을 사용한 UTexture2D저장
      • IsA 함수 또는 설정한 EnumClass 를 활용한 클래스 타입 비교.
      • Monster, Portal, Player 각각 저장되어있는 Texture2D를 사용한 표시
  • CurrentGold
    • InGame 진입 후 현재 획득한 재화에 관련된 UI
  • PlayerStatus
    • 선택한 Player의 Texture 출력
    • HP,Shield 관련 UProgressBar, UTextBlock를 사용한 체력/실드 표시
      • Model계층이 되는 Player의 Current HP,Shield, Max HP,Shield DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams를 사용한 이벤트 바인딩
      • Tick 호출이 아닌 이벤트 호출시마다 UI변경을 통한 최적화
      • Model에서 BroadCast한 값을 바로 받아오는것이 아닌, VM역할의 MainWidget에서 이벤트 바인딩 후 전달
  • Skill
    • 선택한 Playable Character 기반의 고유 스킬에 따른 Texture
    • Animation을 사용한 쿨타임 시 시각적 표현
  • Weapon/PlayerDodge
    • 공용 로직 Dodge 관련 쿨타임 애니메이션을 사용한 시각적 표시
    • 투척물 관련 UI
    • 현재 사용중인 Weapon의 Texture, CurrentAmmo,MaxAmmo 관련 TextBlock
      • PlayerCharacter 내 Ammo관련 델리게이트 이벤트 바인딩을 사용한 이벤트 처리
  • CrossHairs
    • 공격 입력 IA 호출시 CrossHairs RenderTranslation을 사용한 Spread
    • Projectile 적중 대상이 Monster 클래스인 경우, Animation 관련 Crosshair Visible
      • Target이 치명적인 공격을 받은 경우 Color 변경

 

GitHub

 

프로젝트 소개

  • 프로젝트 명 : GunFire : Paragon
  • 제작 기간 : [25.02.17 ~ 25.03.07]
  • 프로젝트 소개
    • UE5 엔진의 주요 기능을 활용한 로그라이크 기반의 FPS 싱글플레이 슈터게임
    • 건파이어리본의 시스템을 계승받아 만든 게임
  • 담당파트 : UI/UX
    • UMG / UUserWidget 기반의 C++ 클래스를 사용한 로비, 인게임, 결과창 관련 UI 제작
    • 플레이어 정보 및 AI와의 상호작용 관련 UI 제작

 

기술 스택 및 개발 도구

  • C++ 17 (v14.38) / MSVC v143
  • Unreal5.5
  • VisualStudio 2022
  • Git Hub, Git LFS Plugin
  • Notion

 

5W1H

  •  

 

 

담당 파트 UI / UX 구현 로직

인게임 입장 전 메인 로비

건파이어 리본 - 인게임 UI

 

  • 버튼을 통한 관리
    • 도전 시작 - 설정된 Level 오픈
    • 게임 설정 - 게임 설정 (조작키, 사운드, 화면크기 관련 설정)
    • 게임 종료 - 플레이 게임 종료

 

인게임 UI/UX

건파이어 리본 - 인게임 UI

  • Player
    • Texture : 선택된 플레이어 캐릭터 Texture
    • HP, Shield : 현재 플레이어의 실드, 체력
    • Shift, Skill, Item CoolTime : 스킬 사용 가능 및 불가능시의 관련 애니메이션 작동
    • Current Gold : 플레이어가 인게임에서 획득한 골드. 상인과 상호작용 할 수 있다.
  • Weapon
    • 사용중인 무기의 Texture
    • 현재 무기의 남은 탄약 / 장전시 최대 탄약
    • 선택한 탄약의 타입 관련 UI

 

건파이어 리본 - 인게임 UI

  • UObject
    • 플레이어의 버프, 디버프 관련 획득한 패시브 UI
    • 획득한 아이템을 우측 상단을 통해 표시
  • Minimap
    • 플레이어 중심의 존재하는 적 관련 UI를 나타낸다.

 

건파이어 리본 - 인게임 UI

  • 크로스헤어
    • 무기에 따른 조준점 변경 (필수 기능 구현 후 추가 기능)
  • 데미지 히트 관련 위젯
    • 플레이어가 가한 공격력에 따른 Text 출력, 피격지점을 기반으로 Animation 처리
  • 레이캐스트 충돌시 관련 AI HUD
    • 공격받은 또는, 조준중인 대상에 대한 HUD 출력
    • HP, AI Name관련 HUD 출력

 

건파이어 리본 - 카드 선택 UI

  • UObject 기반의 패시브 오브젝트 선택 UI
  • 선택한 UI를 통해 플레이어 강화
  • 3중 택1, 선택한 아이템만 적용
  • 패시브 오브젝트 Name, Texture, Description 출력

 

게임 종료 UI/UX

건파이어 리본 - 결과창 UI

 

  • 플레이한 캐릭터 Texture
  • 플레이 타임
  • 총 가한 데미지
  • 획득한 아이템
  • 사용한 총기

 

+ Recent posts