객체 지향적 설계 (책임 주도 설계)

협력에 필요한 책임을 결정하고 객체에게 책임을 할당하자 
( <-> 협력 대상 )

 

스킬 컴포넌트

ㄴ 캐릭터가 가진 스킬 목록 보유 ( 새로운 스킬을 넣었다 뺐다 할 것이 아니기 때문에 컨테이너가 아닌 프로퍼티로 )

ㄴ 키보드 입력 관련 처리 ( <-> 캐릭터의 InputComponent를 참조해서 동작과 Bind 해줘야 한다.  )

ㄴ 키보드 입력, 활성화 상태에 따라 스킬 시전 ( <-> Skill )

 

스킬

스킬이 가져야 하는 공통 속성

ㄴ 활성화 상태 (스킬이 비활성화 상태인 경우에는 입력을 받아도 사용이 되지 않아야 함)

ㄴ 쿨타임 

ㄴ 지속시간

 

스킬이 가져야 하는 공통 행동

ㄴ 스킬 시전하기

ㄴ 스킬 멈추기

 

만들고자 하는 스킬의 능력 (스킬 상속 받기)

1. 슬로우

ㄴ 카메라 효과 조정 (<-> Camera 혹은 CineCamera 흑백 효과)

ㄴ 게임 시간 느리게? 만들기 (<-> Timer Manager 모든 플레이어에게 적용)

 

2. 이동기

ㄴ 캐릭터가 움직이는 방향으로 순간적으로 도약 ( ? )

ㄴ 캐릭터 이펙트 적용 (잔상 효과를 주고 싶다) ( 메시 컴포넌트 -> 머터리얼 ? )

 

캐릭터 컴포넌트
ㄴ 캡슐 컴포넌트 (루트 컴포넌트)
  ㄴ 무기 컴포넌트
ㄴ 스킬 컴포넌트

 

스킬 컴포넌트 클래스는 보유한 스킬들을 단순히 관리하는 컴포넌트이다. 기능만 필요할 뿐 계층 구조에 따른 위치 정보가 필요하지 않기 때문에 Actor Component 클래스를 상속하도록 한다.

 

계획 수정..

내가 만들고자 하는 기능 제작을 도와주는 템플릿을 Gameplay Ability System 플러그인에서 제공하기 때문에 새로 Skill Component를 만들지 않고 플러그인을 활용하도록 한다. 플러그인 사용 방법을 익히면서 내가 생각한 설계 방법과 다른 점이 있는지 살펴 보는 것이 더 낫다고 판단했다.

 

게임플레이 어빌리티 시스템

https://docs.unrealengine.com/5.0/en-US/gameplay-ability-system-for-unreal-engine/

 

Gameplay Ability System

High-level view of the Gameplay Ability System

docs.unrealengine.com

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

언리얼 공식 유튜브 채널에서 소개하는 어빌리티 시스템

https://www.youtube.com/watch?app=desktop&v=dLbWVQajnfk 

언리얼 공식 영상은 아니지만 설명이 잘 되어 있음

 

https://github.com/tranek/GASDocumentation#concepts-a

 

GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer s

My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer sample project. - GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's Gamepl...

github.com

 

GameplayAbility System과 interact하고 싶은 액터는 스스로 Ability System 컴포넌트를 가지거나 그 컴포넌트를 가진 다른 액터에 접근하면 된다.

 

이 액터는 캐릭터, 플레이어 스테이트, 플레이어 컨트롤러가 될 수 있는데, 내가 만드는 게임에서는 캐릭터가 죽고 리스폰하는 상황이 발생하기 때문에 캐릭터에 이 컴포넌트를 추가하게 된다면 죽을 때마다 능력에 대한 정보가 사라지게 된다. 캐릭터가 아닌 액터를 사용해야 한다.

 

기존에 BullettimePlayerState에서 각 플레이어의 킬/데스 정보를 기록했으므로 이 액터에 Ability System을 상속받도록 하고, 킬/데스 정보 관리를 통합시킬 생각이다. 또한 Gameplay Ability는 비동기적으로 캐릭터 애니메이션이나 파티클/사운드 이펙트 처리, 사용자 입력, Replication 기능도 제공한다고 하니, 여기저기에 만들어 놓은 기능들을 천천히 옮겨야겠다.

 

1. IAbilitySystemInterface 인터페이스를 상속하여 GetAbilitySystemComponent를 오버라이드 해야 한다.

ㄴ Getter 함수를 만들어 줘야 한다는 뜻이다.

 

2. 액터가 Ability를 사용하기 위해서는 Ability System 컴포넌트가 해당 Ability를 승인해야 한다.

ㄴ 어빌리티 시스템은 액터에게 승인해준 어빌리티의 리스트를 들고 있다.

ㄴ 어빌리티 시스템은 Attribute도 들고 있다.

 

3. 어빌리티는 FGameplayAbilitySpec 구조체로 정의되는데, 어빌리티 시스템 컴포넌트의 GiveAbility함수 호출을 통해 승인된다. 이 함수는 FGameplayAbilitySpecHandle을 반환하는데 이 구조체를 통해 어빌리티 기능을 취소시킬 수 있다.

어빌리티의 실행 주기

ㄴ CanActivateAbility : 실행 전에 실행 가능한 상태인지 확인한다.

ㄴ CallActivateAbility : 실행 가능한 상태인지 확인하지 않고 어빌리티 코드를 실행한다.ActivateAbility를 오버라이드 하여 커스텀 기능을 추가해야 한다.

Actor나 컴포넌트에서 사용하는 tick 함수 같은 처리 방식이 아닌 task를 사용하여 작업을 처리한다. Task들의 결과를 핸들링하기 위해 C++에서는 델리게이트를 사용하고, 블루프린트에서는 노드를 output execution pin에 연결하여 처리한다.

CommitAbility 함수는 Ability를 실행하는데 필요한 비용(마나, 쿨타임 적용 등)을 적용하는 함수이다.CancelAbility 함수는 Ability를 취소하는 함수이다. CommitAbility와 다르게 외부에서 호출가능하고 OnGamePlayAbilityCancelled를 오버라이딩하여 캔슬되었을 경우에 처리할 로직을 추가할 수 있다.

ㄴ TryActivateAbilityCanActivateAbility + CallActivateAbility

어빌리티 실행이 끝난 후에는 EndAbility를 호출해주어야 한다. (그렇지 않으면 어빌리티가 계속 동작하는 것으로 인식해서 미래에 이 어빌리티를 사용하지 못하거나 이 어빌리티가 막고있는 다른 어빌리티를 사용하지 못하게 된다.) 

 

4. 태그

ㄴ 어빌리티 사이에 어떻게 상호작용할지 결정하는데 사용된다.

ㄴ 각각의 어빌리티는 태그의 집합을 가진다.

ㄴ 태그는 계층 구조로 이뤄져 있다.

Cancel : 이 어빌리티가 실행되는 동안 이미 실행되고 있던 다른 어빌리티의 실행을 취소한다.

Block : 이 어빌리티가 실행되는 동안 태그가 매칭되는 다른 어빌리티들의 실행을 막는다.

 

5. 어빌리티 시스템 컴포넌트는 다양한 방법으로 어빌리티를 활성화할 수 있도록 다양한 함수들을 갖고 있다.

Input code

tag

AbilitySpecHandles

 

6. AttributeSet, Attribute

ㄴ UAttributeSet을 상속받는 커스텀 AttributeSet클래스를 정의한다. 그리고 어빌리티 시스템 컴포넌트를 가진 액터의 프로퍼티로써 위치하도록 한다.

ㄴ 액터의 BeginPlay에서 어빌리티 시스템 컴포넌트->GetSet<커스텀AttributeSet> 함수를 호출하여 AttributeSet을 초기화한다.

ㄴ 각각의 Attribute는 FGameplayAttributeData 구조체로 정의된다. (UPROPERTY로 정의해야 리플렉션 시스템에 보인다)

ㄴ 각각의 Attribute들에 대해 Getter와 Setter를 빠르게 만들 수 있는 매크로가 있다. +레퍼런스를 반환하는 Property Getter

ㄴ AttributeSet은 Attribute이 바뀌었을때 실행되는 PostGameplayEffectExecute 콜백 함수를 오버라이딩하면 편하다. 내부에서 어떤 속성이 바뀌었는지 확인하여 상황에 맞는 로직을 실행하도록 만들 수 있다.

ㄴ 체력이 최소 체력과 최대 체력을 벗어나지 않도록 하는 로직을 사용한다면 여기에 구현할 수 있다.

ㄴ 인게임 리액션을 발동시키는 로직을 사용한다면 여기에 구현할 수 있다.(캐릭터의 체력이 0이 되었을 때 캐릭터를 죽게하도록)

ㄴ 속성의 Default 값들을 설정하는데 데이터 테이블 에셋을 사용할 수 있다. 액터 블루프린트의 어빌리티 시스템 컴포넌트에서 설정 가능하다. 데이터 테이블은 AttributeMetaData 구조체로 만들어야 한다.

ㄴ 데이터 테이블에서 Row Name을 [AttributeSet 클래스 이름].속성 으로 지정한다.

ㄴ Attribute는 기준 값과 현재 값을 주시한다.

 

+ 주의할 점

AttributeSet과 Attribute는 블루프린트에서 생성될 수 없다 C++에서만 가능하다.

다른 Attribute에서 SetAttribute를 호출하지 말고 Gameplay effect System을 사용하자. (캡슐화)

어빌리티 시스템 컴포넌트는 최대한 높은 레벨의 액터에 붙여주자 (내 프로젝트의 경우 캐릭터 < 플레이어 스테이트)

AttributeSet을 아무데나 붙이지 말고 AbilitySystemComponent를 가진 액터에만 붙이자

 

7. GameplayEffects

ㄴ Attribute를 수정하는 역할을 한다.

ㄴ 지속시간 (즉발, 무한, 유한)을 갖는다.

ㄴ 블루프린트 에디터에서 지속시간, 적용 값 등을 조정할 수 있다.

ㄴ UGameplayEffectExecutionCalculation 클래스를 상속받는 커스텀 클래스 (예를 들면 DamageExecution 클래스)를 정의하고 Execute_Implementation 함수를 구현해주어야 한다.

ㄴ 이 함수의 내부에서는 인자를 통해 이펙트의 source와 target에 대한 액터, 태그 정보를 얻을 수 있고 그 정보들로 값을 계산한다.

ㄴ 계산한 값을 AddOutputModifer를 호출함으로써 타깃의 속성 값에 연산한다.

 

블루프린트에서 이펙트를 적용하는 방법

ApplyGameEffectToTarget에 소스, 타겟 어빌리티 시스템 컴포넌트를 넘겨주어 양쪽에 효과를 적용시킨다

ApplyGameEffectToSelf는 소스 어빌리티 시스템 컴포넌트를 넘겨주어 한 쪽에만 효과를 적용시킨다.

 

Effect의 주요 프로퍼티

ㄴ Duration

지속기간

ㄴ Modifiers and Executions

attribute를 어떻게 수정할 것인지, 어떤 계산식을 사용할 것인지

ㄴ Application Requirements

이펙트를 적용하는 조건 (어떤 태그가 있거나, 없거나, 무작위)을 정한다.

ㄴ Granted Abilities

액터에게 어빌리티를 부여

ㄴ Stacking

이미 버프나 디버프를 가진 타깃에 다시 적용할때와 같은 상황에서 어떤 정책을 사용하여 처리할 것인지

ㄴ Gameplay Cue Display

비주얼 이펙트 네트워크 친화적으로 다룰 수 있게 해줌

 

8. Gameplaycue

ㄴ 각각의 비주얼 이펙트가 Cue (GameplayCueNotifyStatic 혹은 GameplayCueNotifyActor 상속) 에 대응됨, 각각 태그를 설정함 (Static은 한번 실행되고 사라지는 이펙트에, Actor는 실제로 월드에 생성되어 액터처럼 다뤄짐)

ㄴ 액터큐의 경우 캐릭터에 attatch 하거나 인스턴스 복제관련 속성이 추가적으로 존재한다.

ㄴ 이펙트가 진행되는 동안 visual effect를 트리거하기 위해 사용

ㄴ Effect의 Display 속성에서 GameplayCue 태그를 설정해 두면 이펙트가 적용될 때 해당 태그를 가진 큐를 적용시킨다.

 

9. Ability 블루프린트와 GameplayAbilityTask

ㄴ 여러가지 종류의 Task가 존재한다. (Move to Location, Wait Gameplay Event, Wait for Confirm Input, Wait Delay 등)

ㄴ GameplayAbility Task는 Ability를 실행시키는 가장 대중적인 방법이다.

ㄴ Abiltiy task는 ability 블루프린트에서 노드로 표현된다. 

ㄴ Task 노드는 여러 개의 실행 경로를 가진다.

ㄴ 어빌리티가 활성화 되어 있는 동안 특정 유형의 태그가 있는 다른 능력이 발생하지 않도록 차단할 수 있다.

ㄴ 커스텀 task도 만들 수 있다.

ㄴ CommitAbility 노드는 AbilitySystemComponent에게 ability의 비용을 적용하라고 알려주는 역할이다.

ㄴ Wait Gameplay Event 노드를 생성하면 태그가 일치하는 이벤트를 기다린다 (listen). Event가 Received 되었을 때 이펙트 적용되도록 연결하여 이펙트를 처리할 수 있다. 

ㄴ 이벤트는 캐릭터나 플레이어 스테이트의 Send Gameplay Event to Actor 노드를 실행할 경우 전달된다.

ㄴ Apply Root Motion Jump Force 노드는 캐릭터 무브먼트 컴포넌트를 멈추고 이 노드를 대신 실행하게 한다. ability 실행 도중 캐릭터의 움직임을 제어할 수 있다.

ㄴ ApplyGameEffectToTarget 노드는 타겟을 Target Data라는 액터 배열을 인자로 받아 여러 타겟에 대해 이펙트를 일으킬 수 있다.

 

10. 레플리케이션

ㄴ 어빌리티의 내부 상태와 Gameplay Event의 레플리케이션을 지원한다.

ㄴ 어빌리티에는 멀티 플레이어 게임에서 어빌리티를 어떻게 레플리케이트 할 것인지에 대한 정책이 있다.

Local Predicted

ㄴ 서버가 클라이언트를 믿어줄 것이라 생각하고 클라이언트가 즉시 어빌리티를 실행, 빠른 반응성과 정확도

Local Only

ㄴ 클라이언트에서만 어빌리티가 실행됨

Server Initiated

ㄴ 서버에서 어빌리티가 실행되고 클라이언트로 전파되는 방식, 빠른 액션의 어빌리티는 Local Predicted 만큼은 부드럽지 않을 수 있다.

Server Only

ㄴ 서버에서만 실행되고 레플리케이트되지 않는 어빌리티, 어빌리티가 server-authoritative data에만 영향을 주도록 하고, 이 data들은 client로 replicate 되도록 한다.

 

11. Instancing Policy

ㄴ 어빌리티가 실행될 때마다 새로운 객체가 생성되기 떄문에, 어빌리티의 실행이 잦은 경우에 성능적인 문제가 생길 수 있다. 사용자가 기능성과 성능 사이를 타협할 수 있도록 생성 정책이 있다.

Instanced per Execution

ㄴ 실행할 때마다 객체 스폰, 블루프린트 그래프 사용 가능, 모든 멤버 변수를 기본값으로 초기화 & 접근 가능, 오버헤드가 크기 때문에 자주 사용하지 않는 어빌리티에 적합함

ㄴ 롤로 치면 평타는 부적합, 궁극기는 적합

Instanced per Actor

ㄴ 어빌리티가 처음 실행될 때 액터가 어빌리티의 인스턴스를 하나 생성하고 미래의 실행에서는 이것을 재활용

ㄴ 실행 사이에 멤버 변수의 값 정리가 요구된다.

ㄴ 레플리케이션에 이상적인 방법

Non-Instanced

ㄴ 가장 효율적으로 동작하는 정책

ㄴ 인스턴스를 새로 생성하지 않고 CDO를 사용한다.

ㄴ 레플리케이션, RPC 안됨

ㄴ 어빌리티가 C++로 작성되어야 한다.

ㄴ 어빌리티 실행 도중 멤버 변수 값을 변경할 수 없다.

ㄴ 자주 사용되고 많은 캐릭터에 의해 사용되는 어빌리티에 적합하다.

 

세션 생성 옵션

FCreateServerInfo 구조체를 만들고 세션 이름, 최대 인원수, LAN 환경을 멤버로 갖도록 한다.

 

방을 만들고자 하는 사용자는 이러한 내용을 설정하고 CreateServer 버튼을 누른다.

 

 

HostMatch_UI의 CreateServer 버튼을 누르면 OnClick 함수가 호출되는데 OnClick 함수 내부에서는 GameInstance를 찾고 캐스팅하여 CreateServer 함수를 호출하게 된다.

 

인자로 들어오는 FCreateServerInfo 구조체 변수는 HostMatch_UI의 블루프린트에서 만들어서 보내준다.

세션을 만들 때 SessionSettings에 없는 속성 (서버 이름같은 경우) Set 함수를 통해 Key와 Value 방식을 통해 전달하고 받을 수 있다.

 

세션 목록 검색 / 참가

FindSessions 함수를 통해 얻을 수 있는 정보는 다음의 구조체로 정리할 수 있다.

PlayerCountStr은 "CurrentPlayers / MaxPlayers" 문자열이다.

 

ServerArrayIndex는 세션 검색 결과 배열에 해당 세션이 몇 번째로 저장되어 있는지를 뜻한다. 각각의 세션에 대한 위젯(방)이 같은 순서로 보여지기 때문에 의미 있는 정보이다.

사용자가 여러 개의 세션을 찾았을 때 어느 세션에 입장할 것인지를 결정하는 데 필요하다.

 

Join 버튼을 누르면 호스가 만든 세션에 참가한다. 

Players가 0/2로 보이는 것은 아마 LAN으로만 테스트해서? 그런 것 같은데 잘 모르겠다. 

 

앞으로의 개발 방향

지금까지 개발한 내용은 여러 개의 튜토리얼을 참고하여 단순하게 구현한 멀티플레이어 FPS 게임의 프로토타입이라고 볼 수 있다. 앞으로 여기에 기능을 추가해 나가면서 실감나고 재미있는 시스템을 구현하고자 한다.

앞으로 어떤 것들을 해볼까 하면 당장 떠오르는 것들은 5가지 정도가 있다.

 

1. 특수 기술을 사용할 때 셰이더를 적용하기 

2. 지형 지물을 생성하는 툴을 제작 (DirectX로) , 생성한 모델을 넣어보기

3. 캐릭터의 도약, 순간이동 능력, 획득할 수 있는 아이템 구현하기

4. 다양한 머터리얼 제작해보고 적용해보기

5. 대중적인 플러그인 다뤄보기

 

처음에는 서버도 직접 구현하는 것을 목표로 했으나 할 수 있다면 가장 마지막에 도전해 볼 생각이다..

스팀 온라인 서브시스템 사용하기

DefeaultEngine.ini -> [OnlineSubSystem]의 DefaultPlatformService=Steam으로 설정

 

 

CreateServer 함수에서 SessionSettings의

bIsLANMatch = false 설정

bUseObbiesIfAvailable = true 설정

 

FindServer함수에서 SessionSearch의 bIsLanQuery = false 설정

 

각자 다른 스팀 ID로 로그인 되어있어야 한다.

프로젝트를 패키징하여 다른 컴퓨터로 옮겨 테스틀 진행하는 것은 시간이 많이 소요 되므로 처음에 한 번 연결이 잘 되는 것을 확인한 후에는 DefeaultEngine.ini -> [OnlineSubSystem]의 DefaultPlatformService=Null로 설정해 빠르게 테스트할 수 있도록 한다.

세션(방) 리스트 검색

MainMenu_UI의 Join 버튼을 누르면 FindSession이 실행되고, SessionResults에 검색 결과들이 배열로 저장된다.

 

FindSession의 결과로 얻을 수 있는 MaxPlayer, CurrentPlayers 정보 + 서버 이름을 갖는 구조체를 정의한다.

이 구조체 하나만을 인수로 갖는 다이나믹 멀티캐스트 델리게이트 FServerDel을 선언해준다.

 

FindSession 이후 호출되는 OnFindSessionComplete에서는 FindSession의 결과로 얻은 FOnlineSessionSearchResult의 배열을 순회하면서  (검색된 세션의 개수만큼의 요소를 가지는 배열) ServerListDel를 브로드캐스트 해준다.

브로드캐스트 해줄 때마다 블루프린트에서 바인딩한 ServerReceived 커스텀 이벤트가 실행되어 위젯을 만들어주게 된다.

 

 

처음에 Join 버튼을 누르면 위젯 스위처에 의해 오른쪽 부분이 방 리스트를 선택할 수 있는 UI로 바뀌고,

새로운 검색을 위해 Scroll Box의 자식들을 Clear 해준다.

 

 

만들어진 위젯은 ScrollBox의 자식으로 들어간다. 

 

 

방 한 개 마다 생성되는 위젯은 반응하지 않는 텍스트 하나로 구성되어 있다. 

 

만들어진 세션들을 검색할 수 있다.

 

세션 만들기 

게임 인스턴스의 BeginPlay에서 온라인 서브 시스템에서 세션 인터페이스를 얻고

 

UI의 Host 버튼의 OnClick에서 CreateServer를 호출하면 세션 세팅 이후에 세션 인터페이스->CreateSession 함수를 호출하도록 한다.

 

세션 생성 작업이 끝난 후 자동으로 실행시키고 싶은 함수(예를 들면 Lobby 레벨로 이동)은 GameInstance Init에서 OnCreateSessionCompleteDelegates델리게이트에 함수를 바인드 해주면 된다.

 

세션 찾기

// SessionSearch는 FOnlineSessionSearch의 TSharedPtr

// 멤버로 추가

TSharedPtr<FOnlineSessionSearch> SessionSearch;

 

SessionSearch = MakeShareable(new FOnlineSessionSearch());

// SessionSearch에 검색 쿼리에 대한 정보를 설정할 수 있음

SessionSearch->QuerySettings.Set (쿼리 세팅)

 

검색에 대한 결과는 SessionSearch->SearchResults에서 받아볼 수 있다. (TArray<FOnlineSessionSearchResult> 자료형)

SessionInterface->FindSessions(0, SessionSearch.ToSharedRef());

 

세선 참가

SearchResults[0]은 세션 찾을 때 얻은 SessionSearch->SearchResults의 첫 번째 요소이다. 세션에 참가할 때 인자로 사용된다.

JoinSession은 로컬 유저 아이디, 세션 이름, 세션 인자를 받아 호출될 수 있다.

SessionInterface->JoinSession(0, "My Session", SearchResults[0]);

 

세션 생성과 마찬가지로 Join 이후에 실행시키고 싶은 작업은 

OnJoinSessionCompleteDelegates 델리게이트에 등록한 OnJoinSessionComplete 함수 내부에서 작성해주면 된다.

 

테스트에 사용한 UProject 파일 실행 인수

 

배치 파일을 만들고 [언리얼 에디터 경로] [Uproject 경로] [옵션 인자]

게임을 실행하며 로그를 확인할 수 있는 명령 인수

 

에디터에서 Play 버튼을 눌러 테스트하지 않고 배치 파일을 여러개 실행하여 호스트와 조인이 잘 작동하는지 테스트할 수 있다.

 

처음 등장하는 클래스

 

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를 만들어 줬다.

 

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

 

결과

+ Recent posts