언리얼 Documentation에 소개된 Multiplayer Programming Quick Start Guide를 많이 참고하며 기능들을 구현했다.

 

PlayerStart 액터를 맵에 두 개 배치하고 플레이어 2명, 멀티플레이 옵션을 ListenServer로 세팅하고 플레이하면 캐릭터의 움직임은 Character Movement 컴포넌트에 의해 레플리케이션 되지만 발사 입력이 들어왔을 때 생성되는 Bullet Actor에 대한 레플리케이션 설정은 해주지 않았기 때문에 총알을 발사해도 상대방 입장에서는 보이지 않는다.

 

RepNotify를 이용해 체력을 리플리케이트하고,  WeaponComponent의 Fire 함수를 RPC함수로 만들어 서버에서 먼저 Bullet이 생성되도록 한다.

 

RepNotify (변수 리플리케이션)

ReplicatedUsing 지정자는 네트워크를 통해 프로퍼티를 업데이트할 때 실행되는 콜백 함수를 지정한다.

CurHealth를 서버에서 변경할 때마다 서버에 연결된 각 클라이언트에서 OnRep_CurrentHealth가 실행된다.

 

Replicated로 지정된 프로퍼티를 리플리케이트하려면 반드시 이런 형식으로 GetLifetimeReplicatedProps 함수를 구현해줘야 한다.

 

순서

1. Bullet에서 ApplyPointDamage 함수를 호출하여 캐릭터는 TakeDamage 함수를 호출한다.

2. TakeDamage가 서버에서 SetCurrentHealth 함수를 호출하여 체력 값을 변경시킨다.

3. SetCurrentHealth는 서버에서 호출될 경우에만 CurHealth 값을 변경한다. 이것은 클라이언트들의 OnRep_CurrentHealth를 호출한다. OnRep_CurrentHealth는 플레이어의 체력 변경에 반응하는 모든 함수 기능을 실행하게 한다. (Role을 확인하여 서버, 클라이언트에 따라 다른 방식으로 반응할 수 있음)

서버에서 CurHealth를 변경해도 클라쪽에서만 OnRep_CurrentHealth가 호출되기 떄문에 OnHealthUpdate를 직접 호출해줘야 한다. C++과는 다르게 블루프린트로 작업하는 경우는 OnRep_CurrentHealth가 서버와 클라이언트 모두에 호출된다고 한다.

4. CurHealth가 각 클라이언트 캐릭터에 리플리케이트 된다.

5. 각 클라이언트는 OnRep_CurrentHealth를 호출한다.

6. OnRep_CurrentHealth 는 OnHealthUpdate를 호출한다.

 

RPC를 사용하지 않고 RepNotify를 사용한 위의 방법이 체력 변경을 리플리케이트하는 가장 효율적인 모델이라고 한다.

 

Bullet.cpp

액터의 리플리케이션은 단순히 bReplicates 플래그를 true로 설정해주면 끝이다.

Bullet의 생성자 내부에서 설정한다.

 

Bullet의 콜리전 컴포넌트에 대한 충돌 처리 또한 서버에서만 일어나도록 Role을 확인

 

RPC 함수

클라이언트에서 호출되는 모든 Fire은 서버에서의 Fire_Implementation으로 대체된다.

 

RPC 함수의 구현부는 _Implemntation의 접미사를 함수 이름에 추가해줘야 한다.

 

 

이제 리플리케이션을 적용하여 멀티플레이어 환경에서 상대 플레이어에 대한 데미지 적용과 총알의 동기화가 되었음을 확인했다. 사운드, 애니메이션 몽타주들은 아직 적용을 안해봤는데 해볼 예정이다.

결과물

 

유틸리티 매크로

디버그 시 언리얼 에디터 Outer log 창에 로그를 표시하기 위한 매크로를 만든다.

BULLETTIME 카테고리 선언 및 사용

 

Bullettime.cpp에 BULLETTIME 카테고리 정의

 

게임모드 생성자에 로그를 찍어보았다. 생성자는 에디터 실행중에 호출되므로 에디터 실행 후 로그 창을 확인하면 로그가 남아있을 것이다.

 

Warning 수준, Bullettime 카테고리인 로그가 찍힌다.

로그는 중요도, 카테고리에 따라 필터링하여 확인 가능하다.

 

캐릭터와 카메라의 회전

캐릭터의 카메라가 바라보는 방향과 캐릭터의 Z축 회전 방향을 일치시키기 위한 설정

 

마우스의 횡이동에 따라 캐릭터를 Yaw 회전하도록 설정한다.

캐릭터 설정 : 폰을 각 회전축으로 사용자의 입력에 따라 회전시킬지 여부
카메라 컴포넌트의 설정 : 폰에 회전 입력이 들어오면 카메라 컴포넌트를 회전시킬지 여부

 

체력 시스템

체력은 캐릭터에서 프로퍼티로 관리한다.

일단 단순하게 초기 체력 100으로 시작하여 Bullet에 충돌시 10씩 체력이 감소하며 0이 되면 Character 액터를 Destroy하는 방식으로 구현해 놓았다.

 

Bullet 충돌 처리

충돌 처리는 Hit Event 대신에 Overlap 이벤트를 사용하여 구현하였다. Overlap 이벤트를 발생시키기 위해서는 두 오브젝트가 상대 오브젝트를 Overlap - Overlap 혹은 Overlap - Block하게 반응하도록 설정해야 하고, 양쪽 오브젝트 모두에 Generate Overlap Events가 체크되어 있어야 한다. 

 

Bullet 액터는 맵을 이루는 Static Mesh Actor(World Static 오브젝트)와 Overlap 시에 사라져야 하므로 World Static을 상대로 Overlap으로 설정하고 Bullet 간의 충돌은 처리하지 않을 것이므로 Bullet을 상대로 Ignore로 설정한다.

맵을 구성하는 static mesh에서 설정해준다.

 

총알은 벽이나 다른 플레이어에 충돌하면 사라지도록 한다. 자신(플레이어)과 충돌하는 경우에는 충돌 처리(총알의 파괴, 피격 처리)를 하지 않도록 한다.

Bullet 액터를 스폰할 때 해당 액터를 생성시킨 Owner를 정해주며 스폰하였는데, 이 정보를 Bullet의 Overlap 이벤트가 발생할 때 활용하여 스스로 체력을 깎는 현상을 방지한다.

총알 스폰할때 Owner를 자기 자신으로 지정하며 스폰함

 

Bullet의 overlap 발생할 경우 처리 과정

 

테스트로 BP_BullettimeCharacter를 월드에 하나 배치하고 10번 맞추었더니 사라졌다.

해결해야 할 문제

1. 1인칭 시점에서는 팔과 총만 보여야 하는데 카메라 시점에 따라 내 캐릭터의 몸체가 보인다. 플레이어의 카메라에서만 캐릭터 메시(몸체)가 보이지 않도록 해야 한다. 또한 1인칭 시점의 메시는 다른 플레이어들에게 보이지 않아야 한다.

 

2. 현재 3인칭 캐릭터 메시에 적용되어 있는 이동 애니메이션은 팔을 내린 상태로 움직인다. 다른 플레이어가 보았을 때 캐릭터가 항상 총을 들고 있게 만들어야 하기 때문에 상체에는 다른 애니메이션을 적용해야 한다. 

 

첫 번째 문제는 Mesh의 렌더링 설정에서 해결할 수 있다.

1인칭 메시에는 Only Owner See 를 true로, 3인칭 캐릭터 메시에는 Owner No See를 true로 설정한다.
더 이상 게임 플레이 중 내 캐릭터의 몸체가 보이지 않는다.

 

두 번째 문제

캐릭터 메시에 따로 총을 붙여야 한다.

총을 잡고 있는 애니메이션을 구해와서 상체에만 적용해야 한다.

 

애니메이션 스타터 팩의 애니메이션을 활용한다.

마켓 플레이스에서 이 애셋을 프로젝트에 추가했다. 

이 애니메이션 스타터 팩에서 제공하는 애니메이션은 SK_Mannequin(UE4)이라는 스켈레톤 메시를 대상으로 하기 때문에 현재 프로젝트에서 사용하는 SKM_Quinn(UE5) 스켈레톤 메시에 맞게 애니메이션 리타기팅이 필요하다.

 

애셋에 포함되어 있던 리타게터를 사용하여 애니메이션을 하나 더 만들어낼 수 있다.

 

새로 만든 애니메이션을 UE5의 스켈레톤 메시에 붙일 수 있다.

 

레이어 애니메이션을 활용하여 상체에만 애니메이션을 적용한다.

애니메이션에서 애님 몽타주를 생성한다.

 

새로 만든 몽타주를 DefaultGroup.UpperBody 슬롯에 연동시켰다.

 

기존 ABP_Manny 애니메이션 블루프린트 애님 그래프에 다음과 같이 노드를 추가했다.

 

 

이제 3인칭 메시에 총을 붙이고, 격발 애니메이션 몽타주를 적용할 차례이다.

이 과정을 진행하던 도중에 많은 것들이 바뀌었다. 기존에는 총 블루프린트 액터를 따로 만들고 액터 안에 Weapon 컴포넌트와 총 스켈레탈 메시 컴포넌트를 뒀었다. 이제는 총 액터를 따로 두지 않고 그냥 캐릭터 안에 Weapon 컴포넌트와 1인칭 3인칭 총 메시를 다 포함시켰다.

이전 포스팅에서는 캐릭터 블루프린트 이벤트 그래프에서 액터를 생성하고 이벤트를 연결했는데 다 없애고 코드로 옮겼다.

각 컴포넌트의 초기화는 캐릭터의 생성자에서 담당하고, 이벤트 등록은 BeginPlay에서 진행하였다.

3인칭 메시의 손 관절에 소켓을 추가하여 총 메시를 손 위치에 붙여 (AttachToComponent함수를 사용) 캐릭터에 대한 애니메이션이 발생할 때 자연스럽게 총도 같이 움직이게 되었다.

 

 

캐릭터 아래 수정된 컴포넌트 계층

 

이벤트를 생성자에서 붙이면 해당 스크립트를 상속받는 블루프린트에서는 생성자가 호출되지 않는 문제를 몰라서 헤맸지만, BeginPlay로 옮겨 해결하였다.

 

이제는 1인칭 메시(팔)와 3인칭 메시(캐릭터)가 따로 존재하며, 각 메시에 다른 애니메이션이 적용되고, 좌클릭하여 fire 함수를 호출했을때 각각의 메시에 붙은 애님 몽타주가 재생된다.

3인칭 시점 템플릿이 앞으로의 프로젝트를 진행하는데 있어서 더 적합해보여 1인칭 템플릿의 애셋을 3인칭 템플릿으로 이주하여 진행한다.

 

1인칭 시점 Skeletal Mesh와 Camera 컴포넌트를 생성하고 붙이는 스크립트를 캐릭터에 추가했다.
spring arm이 사라지고 카메라 컴포넌트가 직접 capsule 컴포넌트에 붙었고, 아래에 1인칭 Mesh가 붙어있다.

 

원래는 월드 맵에 떠있는 총을 캐릭터가 먹었을 때 캐릭터가 장착할 수 있게 되어있었으나 캐릭터가 총을 든 채로 시작하게 만들기 위해 캐릭터 블루프린트 BeginPlay 이벤트가 발생했을 때, BP_Rifle 액터를 스폰하고, 그 액터를 1인칭 메시 컴포넌트에 붙였다. 

BeginPlay 이벤트(게임 시작시 발동)에서 Rifle 액터를 스폰하고, 캐릭터의 1인칭 메시 컴포넌트에 붙였다.

 

1인칭 메시가 총을 잘 잡고 있다.
Weapon 컴포넌트의 캐릭터 레퍼런스를 채워서 Weapon의 이벤트 함수가 호출되었을 때 캐릭터가 올바르게 동작하도록 한다.

 

좌클릭 시 이벤트가 호출되어 구체가 앞으로 발사된다.

 

프로젝트 이름은 BullettimeFPS라고 정했다. 심플한 FPS장르에 플레이어들이 제한된 게이지를 소모하여 월드의 시간을 느리게 할 수 있는 능력을 갖도록 하고 싶기 때문이고 (슈퍼 핫 같은 느낌), 이것이 일반적인 FPS 게임과 차별점이 될 것이다.

 

에픽 게임즈에서 제공하는 FirstPerson 템플릿을 사용하여 프로젝트를 시작하려 한다. 그렇기 때문에 우선 이 템플릿의 코드를 분석하는 것부터 시작하기로 했다. 6개의 소스 파일 (헤더포함하면 12개)이 기본적으로 생성된다.

 

BullettimeFPS

BullettimeFPSCharacter

BullettimeFPSGameMode

BullettimeFPSProjectile

TP_PickUpComponent

TP_WeaponComponent

 

BullettimeFPS

IMPLEMENT_PRIMARY_GAME_MODULE 매크로를 사용하여 primary 모듈을 등록 (FDefaultGameMoudleImpl)

 

여러 모듈을 사용할 경우 IMPLEMENT_GAME_MODULE 매크로를 사용하여 모듈들을 추가할 수 있고, 모듈들은 [게임명]\Source[모듈명] 디렉터리 아래에 들어가게 되는데 이 샘플 프로젝트는 primary 모듈만을 사용하기 때문에 Source 디렉터리만 존재한다.

 

게임 전용 모듈을 추가할 때는 모듈에 대한 .Build.cs 파일을 만들고, 이 모듈에 대한 레퍼런스를 게임의 Target.cs 파일에 추가하면 된다. 이렇게 하면 언리얼 빌드 툴이 자동으로 모듈을 발견하여 부가 게임 DLL 파일을 컴파일한다.

 

BullettimeFPSCharacter, BP_FirstPersonCharacter

ACharacter를 상속받는다.

블루프린트 BP_FirstPersonCharacter가 이 클래스를 상속받아 메시 설정 (블루프린트 에디터에서 설정해야 함)

SkeletalMeshComponent : 1인칭 시점에서 보일 팔

CameraComponent : Character에 기본적으로 달려있는 Capsule 컴포넌트에 SetupAttachment로 카메라 컴포넌트를 붙인다.

SetupPlayerInputComponent (Pawn의 인터페이스) : Project setting > Input 에 설정된 Binding과 동작을 매핑

Dynamic multicast 델리게이트가 선언되어 있다(FOnUseItem) : 아이템을 

 

BullettimeFPSGameMode

AGameModeBase를 상속받는다. (AGameMode는 AGameModeBase의 자식 클래스인데 멀티플레이어 슈팅게임 환경에는 AGameMode가 적합다고 한다. 그렇기 때문에 이후 AGameMode를 상속받도록 전환하겠다)

FClassFinder를 사용하여 BP_FirstPersonCharacter를 찾고, DefaultPawnClass 프로퍼티에 등록한다.

 

의문점

게임 Play 시 Spawn되는 액터들은 누가 생성해주는가?

outliner에 노랑색 텍스트로 표시되는 액터들은 게임 시작과 동시에 spawn된다.

 

액터 간 종속 관계를 조사해봤더니 이런 형태이다.

 

AGameModeBase의 생성자 부분 : 엔진이 초기화 되는 런타임 과정에서 GameModeBase의 생성자에 의해 CDO가 생성된다. 이후 변경되지 않는다.

 

UObject 인스턴스 생성 시 생성자 호출은 발생하지만, 생성자 코드가 사용되지 않고 모듈 로딩 중 초기화된 CDO 값이 스폰될 액터에 복사된다.

 

DefaultPawnClass, PlayerControllerClass .. 들이 전부 TSubclassOf 템플릿 자료형으로 정의되어 있는데, 왜 그런 것인지

찾아보았다.  

TSubClassOf는 UClass 타입의 안정성을 보장해 주는 템플릿 클래스라고 한다. (UClass* 대신에) TSubClassOf 템플릿을 사용하면 에디터에서 특정 클래스의 파생 클래스만 선택할 수 있도록 제한할 수 있다. 

 

리플렉션 (프로퍼티 시스템)

리플렉션은 C++에서 지원하지 않기 때문에 언리얼에서 자체적으로 지원하는 시스템이다.

프로그램이 실행 시간에 자기 자신(객체 정보)을 조사하는 기능 이라고 한다.

 

Unreal Build Tool (UBT)는 마킹된 헤더가 최소 하나 들어있는 모듈들을 기억하고, 그 헤더들 중 지난 컴파일 이후 업데이트 된 헤더가 있다면 UHT를 다시 실행한다. UBT는 또한 현재 프로젝트의 폴더 구조와 소스 파일들을 분석해 작업하는 플랫폼(윈도우, 맥)에 맞는 개발 도구 환경(윈도우는 Visual studio)을 자동으로 생성해준다.

 

헤더파일 상단에 #include "파일이름.generated.h"를 추가해주면 해당 헤더 파일에 리플렉션이 있는 유형으로 마킹한다.

이제 헤더 파일에 U로 시작하는 매크로(UCLASS, UFUNCTION, UPROPERTY 등) 를 사용하여 함수나 멤버 변수, 클래스를 리플렉션 시스템에 보이게 할 수 있다.

 

Unreal Header Tool (UHT)는 프로젝트 컴파일 이전에 리플렉션 시스템에 보이는 유형들을 수집한다. 

UHT는 언리얼 관련 클래스 메타데이터에 대한 C++ 헤더를 파싱하고 Intermediate 폴더에 .generated.h, .gen.cpp 와 같은 코드(메타 헤더 파일, 메타 소스 파일)를 생성해준다. 메타 정보들은 UClass라는 특별한 클래스를 통해 보관된다.

 

컴파일 단계에서는 개별 언리얼 오브젝트(UObject) 마다 UClass가 생성되고 (UClass의 인스턴스가 생성되는 것은 아님)

실행 초기의 런타임 과정에서는 하나의 언리얼 오브젝트마다 UClass 인스턴스와 CDO(Class Default Object) 인스턴스가 생성된다.

CDO는 클래스 생성자에 의해 생성된 후 변경되지 않는다. 이후 추가되는 인스턴스는 CDO를 통해 복제되어 생성된다.

 

이렇게 추가된 언리얼 오브젝트들은 에디터에서 편집할 수 있게 되고 런타임에서 관리할 수 있게 된다.

 

참조: 

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

 

https://blog.naver.com/destiny9720/220934112532

 

[1-5] 클래스 기본 객체 (Class Default Object)

안녕하세요 여러분~ 이번 강좌에서는 하나의 모듈에서 다른 모듈을 참조하는 기능을 구현해보겠습니다. 모...

blog.naver.com

 

BullettimeFPSProjectile (포물체)

총을 줍고 마우스 왼클릭을 했을 때 발사되는 포물체들에 적용되는 코드이다.

포물선의 움직임을 제어하는 ProjectileMovementComponent가 있다. 속도, 최대속도 등의 옵션을 설정할 수 있다.

Collision 컴포넌트를 가지고 있는데, 이 컴포넌트가 가지고 있는 델리게이트(OnComponentHit)에 이 클래스에서 만든 OnHit 메서드를 등록한다. OnHit 이벤트는 물리 시뮬레이션이 적용된 다른 액터와 충돌하면 그 액터에 힘을 가하고 자기 자신은 Destroy하도록 한다.

 

TP_PickUpComponent, TP_WeaponComponent, BP_Rifle

두 컴포넌트는 블루프린트 액터 (BP_Rifle)에 컴포넌트로 추가되어 있다.

게임 시작시 플레이어가 총을 들고 있는 것이 아니라 근처로 가서 주워야 하기 때문에 PickUpComponent가 붙었고, 

총을 주운 이후 좌클릭을 했을 때 발사체를 생성하고, 사운드 효과를 주기 위해 WeaponComponent가 붙었다.

 

앞서 설명한 클래스들은 액터를 상속받았기 때문에 클래스 이름에 접두사 A가 붙었지만, 이 클래스들은 컴포넌트를 상속받고 있기 때문에 (각각 USphereComponent와 UActorComponent를 상속받는다) 접두사 U가 붙는다.

 

PickUpComponent의 상위 클래스인 SphereComponent의 상위 클래스인 PrimitiveComponent에 정의된 OnComponentBeginOverlap 델리게이트에 OnSphereBeginOverlap라는 이름의 사용자 정의 함수를 등록시킨다.

OnSphereBeginOverlap에서는 OnPickUp 이라는 델리게이트를 BroadCast 시킨다.

OnPickUp 델리게이트는 ABullettimeFPSCharacter* 자료형의 인자를 하나 받는 함수를 등록할 수 있는데, 이 샘플에서는 BP_Rifle 블루프린트의 이벤트 그래프에서 TP_WeaponComponent의 AttachWeapon함수를 연결시킴으로써 등록한다.

 

일인칭 템플릿에 대한 공식 Docs

https://docs.unrealengine.com/5.0/ko/first-person-template-in-unreal-engine/

 

일인칭 템플릿

언리얼 엔진의 일인칭 템플릿에 대해 소개합니다.

docs.unrealengine.com

포트폴리오로써 사용할 게임을 드디어 제작하려고 합니다. 이제는 혼자서 게임을 개발할 역량을 갖췄다고 생각하기 때문입니다.

 

FPS 장르를 선택한 이유는 제가 가장 좋아하고 오랫동안 즐겨왔던 게임 장르이고, 다양한 개발 테크닉을 적용해 볼 수 있을 것이라 생각했기 때문입니다. 멀티 플레이 게임을 생각하고 있기 때문에 서버 쪽도 최소한의 기능만 제공할 수 있도록 간단하게 만들어보려 합니다.

 

개발 방향성은 핵심적인 기능들을 확실하게 구현하는 것을 목표로 먼저 개발하고, 세부적인 부분은 버전을 업그레이드 하면서 고쳐 나가는 방식으로 생각 중입니다. 개발을 진행하면서 어떤 기능을 왜 이런 방식으로 구현을 했는지, 다른 방식은 어떤 것들이 있을지를 생각해보고 기록할 것입니다.

 

아무 것도 없는 상태에서 게임을 제작하려는 것이 벌써부터 막막하긴 하지만, 비슷한 장르로 만들어진 샘플 프로젝트나 인터넷에 도움이 될 글들이 많기 때문에 할 수 있을 것이라 생각합니다.

 

개발에 도움이 될 샘플 프로젝트(TPS 장르)

 

남에게 보여줬을 때 부끄럽지 않을 코드를 생산하는 것이 목표이기 때문에 따로 개발 기한은 정해두지 않았습니다.

애셋은 그냥 무료 애셋을 사용할 예정입니다.

구체적인 기획 내용은 구체적으로 기록할 예정은 없고, 해당 부분 개발을 진행할 때 같이 정리할 생각입니다.

대강 작성해본 기획 스토리보드

+ Recent posts