처음 등장하는 클래스

 

GameInstance

ㄴ 레벨의 변화와 관계없이 게임 시작 후 계속 살아있기 떄문에 네트워크 연결 관련 코드를 작성하기에 좋다.

 

프로젝트 세팅 -> 맵 모드에서 세팅해줄 수 있다.

 

온라인 서브시스템 모듈

[프로젝트명].build.cs에 OnlineSubsystem 모듈과 OnlineSubsystemSteam 모듈을 추가한다.

 

DefaultEngine.ini 파일에 아래의 내용을 추가해준다.

 

메인 메뉴 UI

이전에는 메인 메뉴 UI 위젯을 블루프린트와 C++로 분리하여 관리하였으나 이번에는 굳이 분리하지 않고 블루프린트에서 비주얼 스크립팅을 하여 모두 처리해 보았다. 관리할 버튼도 많고 계층 구조도 복잡하기 때문이다.

 

MainMenu_UI 위젯

 

 

MainMenu_UI는 생성될 때 GameInstance를 찾아서 멤버 변수로 갖고 있는다.

 

각각의 버튼이 눌릴 때 오른쪽의 Widget Switcher에 보이는 하위 위젯이 인덱스에 따라 다르게 변경된다.

 

레벨 블루프린트

레벨마다 레벨 블루프린트가 존재하는데, 아래의 그래프는 MainMenu 레벨에 대한 그래프이다.

MainMenu 레벨이 시작할 때, MainMenu_UI 위젯을 생성하여 Viewport에 등록하고, 플레이어 컨트롤러의 마우스를 게임 내에서 보이게 설정한다.

 

 

액터의 수명 주기

https://docs.unrealengine.com/4.27/ko/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/Actors/ActorLifecycle/

 

캐릭터의 리스폰이 어떤 절차를 거쳐서 이루어질지 알기 위해 게임플레이 프레임워크와 액터 생성주기를 한 번 짚고 넘어가도록 한다.

 

게임 모드는 다음과 같은 순서로 액터들을 생성하고 배정한다.

 

1. 플레이어 컨트롤러 생성 - AGameMode의 Login에서 SpawnActor 호출

2. 플레이어 폰의 생성 -  Login완료 후 AGameMode의 PostLogin 마지막 부분에서 호출 - 호출을 따라가다 보면 SpawnDefaultPawnAtTransform 안에서 SpawnActor가 호출됨

3. 플레이어 컨트롤러가 플레이어 폰을 빙의 - PostLogin 내부의 함수 호출 트리 마지막 쯤에 FinishRestartPlayer에서 Possess함수 호출

 

세션쪽은 나중에 살펴보겠다

 

변경된 설계

원래 캐릭터의 멤버로 UBullettimePlayerHUD (UUserWidget)를 두었었는데, BullettimePlayerController의 멤버로 옮겼다.

 

이유 : 캐릭터의 경우 캐릭터가 죽고 다시 살아날 때마다 위젯이 생성되고 초기화된다. 플레이어 컨트롤러는 최초 한번 실행되면 게임이 끝날 때 까지 사라지지 않으므로 위젯을 두기에 적절한 위치이다.

 

리스폰

서버에서 피해를 입은 한 캐릭터의 체력이 0 이하가 되면 일어나는 일들

 

서버 측

6초 후에 캐릭터 Destroy() 되도록 타이머 걸기(시체 치우기)

리스폰 함수에 타이머를 걸기 (5초)

킬 데스 점수 처리하기

 

클라이언트측

HealthBar 초기화함수 타이머 걸기 (4초)

카메라 워킹 시작하기 (Timeline을 사용)

1인칭 메시 안보이게하고(Visible 끄기) 3인칭 메시 보이게 하기 (OwnerNoSee false로)

 

양쪽

래그돌 처리, 캡슐 컴포넌트 충돌 끄고 CharacterMovement 끄기

 

5초 후에 호출되도록 설정한 함수는 Client_RespawnCharacter RPC 함수이다.

클라이언트에서 각자의 PlayerController->Server_Respawn RPC 함수를 호출

서버에서 게임모드를 얻어 게임모드의 Server_RespawnRequested 함수를 호출, PlayerController를 인자로 전달

Server_RespawnRequested 내부에서는 캐릭터 생성, 컨트롤러 Possess 시킴

 

 

AGameModeBase::RestartPlayerAtTransform의 존재를 나중에 정리하면서 알게 되었다. 클라이언트의 로그인이 일어날 때 최초로 실행되어 폰을 생성하고 PlayerController를 붙여주는 함수이다.

처음에 캐릭터를 초기화 하는데 사용될 수 있을 뿐만 아니라 캐릭터를 리스폰하는데에도 적절하게 사용될 수 있다는 생각이 들었다. 

 

일단 지금은 기능적으로 문제가 없어서 지금 구현해 놓은 것들을 그대로 두긴 하겠지만 앞으로 온라인 세션에 대해서도 더 많이 다룰 것이기 때문에 기능을 확장해 나감에 따라 앞서 만들어 놓은 것들을 바꿔야 할 때 수정할 생각이다.

 

카메라 워킹

캐릭터가 죽었을 때 Timeline을 사용하여 카메라가 뒤로 서서히 빠지면서 자신의 시체를 볼 수 있도록 했다.

 

캐릭터에 TimeLine 컴포넌트 두 개,  VectorCurve 애셋 두 개를 추가한다. 각각 카메라의 RelativeTransform과 RelativeRotation을 3초에 걸쳐서 부드럽게 변화시킨다.

CurveVector (위치)
TimelineVector 컴포넌트를 사용해 카메라의 움직임을 부드럽게 제어한다.

 

캐릭터는 CurHealth가 0이하로 떨어지면 죽게 만들어야 한다.

 

앞서 체력과 관련된 부분들을 서버에서 처리했기 때문에 마찬가지로 캐릭터 죽음에 관련된 처리도 서버에서 처리해야 한다.

 

서버 쪽 캐릭터가 데미지를 입을 때  OnHealthUpdate 함수가 호출되기 때문에 내부에서 CurHealth <= 0 을 체크하여 멀티캐스트로 OnPlayerDie를 실행시킨다.

 

멀티캐스트 RPC 함수 선언

 

Character의 OnHealthUpdate 내부 서버에서 멀티캐스트 RPC를 호출한면 서버와 클라이언트 모두에서 실행된다.

 

캐릭터가 죽으면 래그돌 연출을 적용하고 싶다. 콜리전 설정을 변경하여 적용할 수 있다.

공간 쿼리는 (레이캐스트, 스윕, 오버랩)

시뮬레이션은 (리짓 바디, 컨스트레인트) 

 

캐릭터 메시의 기본 프리셋은 Query Only와 Pawn으로 되어 있는데 

캐릭터 메시의 기본 콜리전 세팅

 

RagDoll 프리셋을 사용해도 되지만 RagDoll 프리셋은 Query도 허용하기 때문에 캐릭터가 죽은 이후에는 물리 시뮬레이션만 사용하고 싶기 때문에 CollisionEnabled와 ObjectType을 따로 지정해줬다.

 

그리고 메시의 SetSimulatePhysics를 true로 설정해주면 된다.

래그돌 물리 시뮬레이션은 리플리케이트되지 않기 때문에 각 클라이언트에서 시체가 다르게 보일 수 있

 

점수 누적

 

BullettimePlayerController를 새로 만들고 SetupInputComponent함수를 오버라이드 했다. tab키를 모든 플레이어 정보(이름 / 킬 / 데스)를 출력하는 함수에 바인딩했다.

 

멀티플레이어 게임에서는 캐릭터 스탯 관련 정보를 PlayerState에 저장하는 것이 좋다.

GameState에서는 모든 플레이어의 PlayerState에 접근할 수 있는데 GameState는 서버에 존재하면서 모든 클라이언트에 리플리케이트 되므로 

 

BullettimePlayerState를 만들고 다음과 같이 정의했다.

 

APlayerState 설명

 

PlayerState가 리플리케이트 된다고 들었어서 PlayerState에 새로 만들어준 변수들도 자동으로 리플리케이트 되는 줄 알았는데, APlayerState의 일부 RepNotify변수들만 리플리케이트 되는 것이었다. 직접 RepNotify 프로퍼티로 선언해주니 해결되었다. 

 

 

서버의 캐릭터가 죽었을 때 AddScore()함수를 서버에서만 호출한다.

가장 최근에 이 캐릭터를 공격한 PlayerController 정보를 사용하여 각각의 캐릭터 컨트롤러가 가지는 PlayerState에 점수를 추가한다.

 

결과물

플레이어가 Tab 키를 누르면 모든 플레이어의 스탯 정보를 확인할 수 있다. (Listenserver 1, client 2로 세팅된 상황)

서버와 클라이언트에서 정상적으로 모든 플레이어의 킬 / 데스 정보를 확인할 수 있다.

 

일단은 기능 구현이 우선이므로 디버깅 텍스트를 띄우는 방식으로 구현했는데 나중에 Widget으로 띄울 생각이다.

 

리플리케이션 마무리

 

앞 단계에서 bullet과 체력에 대해서만 리플리케이션을 적용했고 OnFire함수를 변경했다. 기존에는 OnFire 함수에서 사운드 재생, 1인칭 3인칭 Fire anim montage를 재생했기 때문에 OnFire함수를 서버에서만 실행되는 RPC 함수로 바꾸게 되면 서버에서만 사운드와 애니메이션이 재생되는 문제가 있다.

 

1인칭 애니메이션 몽타주는 리플리케이션될 필요가 없고, 3인칭 애니메이션 몽타주와 사운드는 서버와 모든 클라이언트에서 관찰되어야 하므로 기존 OnFire내에서 호출되었던 함수들의 위치를 바꾸었다.

 

우선 1인칭 애니메이션 몽타주 재생은 리플리케이션될 필요가 없으므로 OnFire 함수 밖으로 빼낸다. 

 

WeaponComponent.cpp에 Play1PFireMontage 함수를 따로 UFUNCTION으로 만들고

캐릭터의 OnUseItem에 등록하여 캐릭터를 소유한 클라이언트에서만 1인칭 애니메이션 몽타주를 호출할 수 있도록 하였다.

 

3인칭 애니메이션 몽타주와 사운드 재생은 서버와 모든 클라이언트에서 호출될 수 있도록 멀티캐스트 RPC 함수인 Multi_OnFire함수를 만들어 내부에서 호출될 수 있도록 하였다. 구분을 위해 서버 RPC 함수의 이름은 Server_OnFire로 바꾸었고, Server_OnFire 함수에서 BulletActor를 스폰한 이후에 Multi_OnFire 함수를 호출하도록 구현했다. 

 

WeaponComponent에 Multicast RPC 함수를 하나 더 만들었다.

 

체력 UI 적용하기

언리얼에서 UI를 제작하는데 사용되는 다양한 프레임워크 (UMG, Slate HUD)가 있는데 가장 대중적인 UMG를 사용하도록 한다.

 

Bullettime.Build.cs에 UMG 모듈을 사용하겠다고 알려줘야 한다.

 

UserWidget 클래스를 상속받는 C++ 클래스 BullettimePlayerHUD를 생성한다.

UI 작업은 UI 블루프린트 에디터를 이용하여 작업하는 것이 편하므로 BullettimePlayerHUD를 상속받는 블루프린트 클래스를 이후에 상속받아 작업할 예정이다.

UPROPERTY 속성으로 meta = (BindWidget) 을 지정해주면 상속받는 블루프린트에서는 동일한 이름의 위젯(HealthBar)을 꼭 만들어줘야 한다.

 

SetHealth 함수는 최대 체력과 현재 체력을 인수로 받아 퍼센트 값을 HealthBar에 세팅한다.

 

일단 현재 체력을 BullettimeCharacter에서 관리하고 있고, OnHealthUpdate 함수도 정의되어 있으므로 여기에서 직접 SetHealth를 호출하여 체력이 변할 때 프로그레스 바도 변경되도록 한다.

캐릭터에 프로퍼티 두 개를 추가한다. 

PlayerHUDClass 프로퍼티는 HUD로 사용할 클래스

ㄴ 캐릭터 블루프린트 에디터에서 위젯 블루프린트를 지정해 줄 예정

 

PlayerHUD는 PlayerHUDClass로 생성할 인스턴스이다.

 

캐릭터의 BeginPlay함수에서 위젯을 생성한다.

UserWidget은 PlayerController에 소유되어야 하기 때문에 캐릭터의 PlayerController을 찾아 CreateWidget의 인자로 넣어준다.

서버의 캐릭터에서 SetCurrentHealth 함수가 호출되면 CurHealth의 값이 바뀌어 각 클라이언트에 CurHealth의 값이 리플리케이트 되고, RepNotify의 콜백함수를 거쳐 OnHealthUpdate가 실행되면 클라이언트가 조종하는 캐릭터인 경우에만 UI 값을 세팅한다.

 

 BullettimePlayerHUD를 상속받는 BP_PlayerHUD 블루프린트 클래스를 생성하고 다음과 같이 구성했다.

간단하게 Vertical Box 안에 텍스트 하나와 HealthBar 이름을 가진 ProgressBar를 만들어 줬다.

 

마지막으로 캐릭터 블루프린트 에디터에서 위젯 블루프린트를 설정해 주었다.

 

결과

언리얼 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의 접미사를 함수 이름에 추가해줘야 한다.

 

 

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

결과물

 

액터의 역할

언리얼 엔진의 네트워크 프레임워크는 기본적으로 서버/클라이언트 모델을 기준으로 만들어졌다.

 

서버는 클라이언트의 최신 상태를 유지하기 위해 액터를 사용한다. 서버가 특정 클라이언트를 업데이트할 때, 지난 업데이트 시점 이후 변경되었다고 보는 연관 액터를 수집한 다음, 액터의 최신 상태를 유지하기에 충분할 만큼의 정보를 클라이언트에 전송한다.

 

네트워크 모드 유형

NM_Standalone

ㄴ 로컬 머신에서 실행되며 원격 머신에서 클라이언트를 받지 않는 서버 (싱글 플레이어, 로컬 멀티플레이 게임에 적합)

 

NM_DedicatedServer

ㄴ 로컬 플레이어가 없어 사운드, 그래픽, 사용자 입력, 기타 플레이어 관련 기능을 제거하여 보다 효율적인 실행이 가능한 전용 서버. 신뢰 서버에서 호스팅되는 멀티플레이어 게임에 사용되며 경쟁형 MOBA, MMO, 온라인 슈팅 게임 등 퍼포먼스와 신뢰성이 중요한 서버가 필요한 경우에 사용

 

NM_ListenServer

ㄴ 로컬 플레이어가 있는 서버, 사용자는 써드 파티 서버 없이 게임을 구성하여 플레이할 수 있음. 호스트에 네트워크 지연시간이 없어 호스트 플레이어에게 약간 유리하게 적용될 수 있음.

 

NM_Client 

ㄴ 서버가 아닌 유일한 모드. 데디케이티드 또는 리슨 서버로 접속되는 클라이언트

 

서버의 역할

게임플레이 흐름의 구동을 담당 (서버의 GameMode 액터를 통해 구동되고, GameMode액터의 중요한 상태를 GameState 액터에 반영하면, GameState가 각 클라이언트에 리플리케이트)

ㄴ 새 맵으로 이동할 때가 되었음을 클라에게 알리기

ㄴ 액터의 리플레케이션

 

접속 프로세스 예시

1. 클라가 서버에 접속요청

2. 서버가 수락하면 현재 맵을 전송

3. 서버는 클라가 맵을 로드할 때까지 기다림

4. 로드 이후 AGameModeBase::PreLogin을 호출

ㄴ 서버에 접근 시도중인 플레이어를 수락 또는 거부할 수 있음

5. 수락되면 서버는 AGameModeBase::Login을 호출

ㄴ 새로운 PlayerController를 만들고 새로 접속된 클라이언트에 리플리케이트 시킨다. 클라이언트의 접속 과정에서 임시로 사용하고 있던 PlayerController를 대체한다.

ㄴ APlayerController::BeginPlay가 호출된다.

6. AGameModeBase::PostLogin이 호출된다. 이후 서버가 PlayerController에서 RPC 함수 호출을 시작해도 안전하다.

 

캐릭터의 레플리케이션

네트워크 환경에서 캐릭터는 캐릭터가 가지는 Character Movement 컴포넌트 때문에 레플리케이션된다. (클라이언트-서버 네트워킹이 자동으로 탑재되어 있다.)

 

개발자는 네트워크 Movement를 커스텀하기 위해 Character Movement 컴포넌트를 프레임워크로써 사용할 수도 있다.

 

Character Movement 컴포넌트의 TickComponent 함수에서는 PerformMovement 함수를 호출하여 플레이어의 input에 따른 예상 가속도를 계산한 뒤에 최종 움직임을 캐릭터에 적용한다. PerformMovement에서는 movement 종류(걷기, 떨어지기) 에 따라 다른 물리 함수를 적용하여 움직임을 계산한다.

 

네트워크를 사용하지 않는 환경에서는 PerformMovement가 틱마다 직접적으로 호출되지만 네트워크 게임에서는 다른 함수들에 의해 간접적으로 호출된다.

 

Network Role

Character Movement 컴포넌트는 이 컴포넌트를 소유하는 캐릭터의 network role에 따라 어떻게 replicate할지를 결정한다.

 

1. Autonomous Proxy

ㄴ 클라이언트의 플레이어가 조종하는 캐릭터

 

2. Authority

ㄴ 서버에서 존재하는 캐릭터

 

3. Simulated Proxy

ㄴ 클라이언트에 존재하는데 AI나 다른 클라이언트에 의해 조종되는 캐릭터

 

Movement Replication 정리

1. TickComponent 함수에서는 프레임에 대한 가속도 및 회전 변화를 계산하고, 로컬 제어 캐릭터의 경우 PerformMovement, 네트워크 클라이언트의 경우 ReplicateMoveToServer를 호출하여 이동을 캐릭터에 적용한다.

 

2. ReplicateMoveToServer 함수는 이동 대기목록(PendingMove list)에 이동을 저장하고 PerformMovement를 호출하여 ServerMove에 movement 파라미터, 클라이언트 액터의 결과 위치, 타임스탬프를 전달한다. ServerMove RPC함수를 호출한다. 

 

3. 서버에서 실행되는 ServerMove에서는 movement 파라미터를 디코딩한 정보를 토대로 캐릭터에 이동을 적용한다.

이동 결과 위치와 응답 시간의 차이를 비교하여 차이가 충분히 큰 경우 서버는 ClientAdjustPosition RPC 함수를 호출하여 클라이언트상 액터의 위치를 서버 버전의 위치로 조정한다. bUpdatePosition 플래그를 true로 설정한다.

 

4. 다음 프레임의 TickComponent 함수에서 bUpdatePosition 플래그가 true라면 ClientUpdatePosition함수를 호출하는데, 

여기에서는 서버 버전의 위치로 조정된 이동의 시점 이후 입력된 이동들(Pending move list에 저장된)을 모두 리플레이한다. (클라이언트와 서버의 Latency 사이에 발생한 입력들에 대한 처리가 필요하기 때문에) 

bUpdatePosition 플래그는 다시 false로 바꿔준다.

 

5. 다른 클라이언트가 소유한 액터(Simulated Proxy)의 움직임 업데이트 대부분은 매 틱마다 호출되는 SimulateMovement 함수와 내부에서 호출되는 MoveSmooth에서 이루어진다. 서버 업데이트를 받았을 때 simulated proxy의 위치가 튀어보이는것을 방지하기 위해 SmoothClientPosition 함수를 사용하여 캐릭터 표현 위치에 스무딩 작업을 해 준다.

 

 

https://docs.unrealengine.com/4.26/en-US/InteractiveExperiences/Networking/CharacterMovementComponent/

 

Character Movement Component

Detailed explanation of Character Movement Component

docs.unrealengine.com

'언리얼 5 > 정리' 카테고리의 다른 글

게임 플레이 아키텍처, 스마트 포인터, 태스크  (0) 2022.10.18
개발 구성, 엔진 아키텍처  (0) 2022.10.18
액터 종류  (0) 2022.10.18
레벨, 월드 세팅, 월드 파티션  (0) 2022.10.15
애셋과 레퍼런스  (0) 2022.10.14

유틸리티 매크로

디버그 시 언리얼 에디터 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 함수를 호출했을때 각각의 메시에 붙은 애님 몽타주가 재생된다.

+ Recent posts