
멀티플레이어 게임을 위한 게임플레이를 개발하려면 게임의 액터(Actors) 에 리플리케이션(Replication) 을 구현해야 합니다. 게임 세션의 호스트 역할을 하는 서버 전용 함수 기능과 세션에 연결하는 플레이어를 나타내는 클라이언트 도 설계해야 합니다. 간단한 멀티플레이어 게임플레이 제작 프로세스를 보여드리는 이 가이드에서는 다음을 학습하게 됩니다.
- 베이스 액터에 리플리케이션을 추가하는 방법
- 네트워크 게임에서 무브먼트 컴포넌트(Movement Components) 를 활용하는 방법
- 변수 에 리플리케이션을 추가하는 방법
- 변수가 변경될 때 RepNotify 를 사용하는 방법
- C++에서 원격 프로시저 콜(Remote Procedure Calls, RPC) 을 사용하는 방법
- 함수 내에서 수행된 호출에 필터를 적용하기 위해 액터의 네트워크 역할(Network Role) 을 확인하는 방법
최종 결과물은 플레이어들이 서로에게 폭발하는 발사체를 날릴 수 있는 3인칭 게임입니다. 작업의 대부분은 발사체를 만들고 캐릭터에 대미지 반응을 추가하는 일이 될 것입니다.
시작하기에 앞서 데디케이티드 서버 및 네트워크 개요 페이지의 기본 정보를 살펴보실 것을 적극 권장합니다. 이 가이드와 리플리케이션 콘셉트를 사용하지 않는 일인칭 슈팅 튜토리얼을 비교해 보셔도 좋습니다.
1. 필수 구성
-
에디터(Editor) 를 열고 새 프로젝트(New Project) 를 생성합니다. 사용할 세팅은 다음과 같습니다.
- C++ 프로젝트
- 삼인칭(Third-Person) 템플릿 사용
- 시작용 콘텐츠(Starter Content) 포함
- 데스크톱(Desktop) 타기팅
이 세팅을 적용한 뒤 프로젝트 이름은 ThirdPersonMP 로 하고 생성(Create) 버튼을 클릭하여 계속합니다. 프로젝트의 C++ 파일이 생성되고 언리얼 에디터 에 ThirdPersonExampleMap 이 자동으로 열릴 것입니다.
-
씬에 서 있는 ThirdPersonCharacter 를 삭제 후 맵 안에 플레이어 스타트(PlayerStart) 를 두 개 넣습니다. 플레이어 스타트는 씬에 기본으로 포함되고 수동 배치된 ThirdPersonCharacter 대신 플레이어의 스폰을 처리할 것입니다.
대부분의 템플릿에서 폰과 캐릭터의 리플리케이션은 디폴트로 활성화되어 있습니다. 이 예시에서 ThirdPersonCharacter는 자동으로 움직임을 리플리케이트하는 캐릭터 무브먼트 컴포넌트(Character Movement Component) 를 이미 가지고 있습니다.
캐릭터 무브먼트 컴포넌트가 리플리케이션을 어떻게 처리하고, 함수 기능을 어떻게 확장하는지에 대해서는 캐릭터 무브먼트 컴포넌트 가이드를 참조하세요.
캐릭터의 스켈레탈 메시(Skeletal Mesh) 와 애니메이션 블루프린트(Animation Blueprint) 등의 외관 컴포넌트는 리플리케이트되지 않습니다. 그러나 캐릭터 속도와 같이 게임플레이 및 무브먼트와 연관성이 있는 변수는 리플리케이트됩니다. 애니메이션 블루프린트는 업데이트될 때 이러한 변수를 읽습니다. 이 방식으로 각 클라이언트의 캐릭터 사본이 비주얼을 업데이트합니다. 이 프로세스는 게임플레이 변수의 정확한 업데이트와 일관성을 갖도록 수행됩니다. 마찬가지로 게임플레이 프레임워크는 플레이어 스타트에 자동으로 캐릭터를 스폰하고 캐릭터에 플레이어 컨트롤러(Player Controllers) 를 할당합니다.
이 프로젝트로 서버를 시작하고 클라이언트가 거기에 참여하기만 해도 멀티플레이어 게임은 작동합니다. 그러나 플레이어는 아바타로 이동과 점프밖에 할 수 없을 것입니다. 따라서 추가적인 멀티플레이어 게임플레이를 만들어야 합니다.
2. RepNotify로 플레이어 체력 리플리케이트하기
플레이어에게는 게임플레이 도중 대미지를 입을 수 있는 체력 값이 필요합니다. 이 값은 리플리케이트해야 하며, 모든 클라이언트는 각 플레이어의 체력에 대한 정보를 동기화했습니다. 플레이어가 대미지를 입을 때는 플레이어에게 피드백을 제공해야 합니다. 이 섹션에서는 RepNotify를 사용하여 RPC에 의존하지 않고 변수에 대한 모든 필수 업데이트를 동기화하는 방법을 보여드립니다.
참고로 '역할'은 각각 'GetLocalRole()'과 'GetRemoteRole()'로 대체되었습니다. 아래의 일부 섹션에는 이전에 사용되었던 '역할'이 남아 있을 수 있으니 변경되었다는 사실을 기억하세요.
-
ThirdPersonMPCharacter.h
를 엽니다.protected
아래 다음 프로퍼티를 추가합니다. -
ThirdPersonMPCharacter.hThirdPersonMPCharacter.h
를 엽니다.protected
아래 다음 프로퍼티를 추가합니다.protected: /** 플레이어의 최대 체력. 체력의 최댓값입니다. 이 값은 스폰 시 시작되는 캐릭터의 체력 값입니다.*/ UPROPERTY(EditDefaultsOnly, Category = "Health") float MaxHealth; /** 플레이어의 현재 체력. 0이 되면 죽은 것으로 간주됩니다.*/ UPROPERTY(ReplicatedUsing = OnRep_CurrentHealth) float CurrentHealth; /** 현재 체력에 가해진 변경에 대한 RepNotify*/ UFUNCTION() void OnRep_CurrentHealth();
플레이어의 체력이 변경되는 방식을 엄격하게 제어해야 하므로, 체력 값에 다음과 같은 제약을 넣습니다.
MaxHealth
는 리플리케이트되지 않으며 디폴트만 편집 가능합니다. 이 값은 모든 플레이어에 대해 사전에 계산되며, 전혀 변경되지 않습니다.CurrentHealth
는 리플리케이트되지만 블루프린트에서 편집하거나 액세스할 수 없습니다.MaxHealth
와CurrentHealth
는 모두protected
되어 외부 C++ 클래스로부터의 액세스를 방지합니다.AThirdPersonMPCharacter
또는 여기서 파생된 다른 클래스 내에서만 수정 가능합니다.
이는 라이브 게임플레이 도중 플레이어의 CurrentHealth
또는 MaxHealth
에 원치 않는 변경이 발생할 위험성을 최소화합니다. 이후 단계에는 이 값을 구하고 변경하는 다른 퍼블릭 함수를 제공하게 됩니다.
Replicated
지정자는 서버에서 액터의 사본을 활성화하여 변수 값이 변경될 때마다 연결된 모든 클라이언트에 해당 변수 값을 리플리케이트합니다. ReplicatedUsing
도 똑같은 작업을 수행하지만 RepNotify 함수를 설정하도록 지원합니다. 이 함수는 클라이언트가 리플리케이트된 데이터를 성공적으로 수신할 때 트리거됩니다. OnRep_CurrentHealth
를 사용하여 이 변수의 변경을 기반으로 각 클라이언트에 업데이트를 수행할 것입니다.
-
ThirdPersonMPCharacter.cppThirdPersonMPCharacter.cpp
를 엽니다. 상단의#include "GameFramework/SpringArmComponent.h"
아래 다음#include
구문을 추가합니다.#include "Net/UnrealNetwork.h" #include "Engine/Engine.h"
이는
GEngine
내에서AddOnscreenDebugMessage
함수 액세스와 변수 리플리케이션에 필요한 함수 기능을 제공합니다. 이를 사용하여 메시지를 화면에 출력할 것입니다. -
ThirdPersonMPCharacter.cppThirdPersonMPCharacter.cpp
에서AThirdPersonMPCharacter
생성자 하단에 다음 코드를 추가합니다.//플레이어 체력 초기화 MaxHealth = 100.0f; CurrentHealth = MaxHealth;
이렇게 하면 플레이어의 체력이 초기화됩니다. 이 캐릭터의 새 사본이 생성될 때마다 현재 체력이 최대 체력 값으로 설정될 것입니다.
-
ThirdPersonMPCharacter.hThirdPersonMPCharacter.h
에서 다음 퍼블릭 함수 선언을AThirdPersonMPCharacter
생성자 바로 뒤에 추가합니다./** 프로퍼티 리플리케이션 */ void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
-
ThirdPersonMPCharacter.cppThirdPersonMPCharacter.cpp
에서 이 함수에 다음 구현을 추가합니다.////////////////////////////////////////////////////////////////////////// // 리플리케이트된 프로퍼티 void AThirdPersonMPCharacter::GetLifetimeReplicatedProps(TArray <FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); //현재 체력 리플리케이트 DOREPLIFETIME(AThirdPersonMPCharacter, CurrentHealth); }
GetLifetimeReplicatedProps
함수는Replicated
지정자로 지정된 모든 프로퍼티를 리플리케이트하며, 프로퍼티의 리플리케이트 방식을 구성하도록 지원합니다. 여기서는 가장 기본적인CurrentHealth
구현을 사용합니다. 리플리케이트가 필요한 프로퍼티를 추가할 때는 반드시 이 함수도 추가해야 합니다.GetLifetimeReplicatedProps
의Super
버전을 호출해야 합니다. 그러지 않으면 액터의 부모 클래스에서 상속받은 프로퍼티가 부모 클래스에서 리플리케이트하도록 지정되어 있더라도 리플리케이트되지 않습니다. -
ThirdPersonMPCharacter.hThirdPersonMPCharacter.h
에서Protected
아래 다음의 함수 선언을 추가합니다.protected: /** 업데이트되는 체력에 반응. 서버에서는 수정 즉시 호출, 클라이언트에서는 RepNotify에 반응하여 호출*/ void OnHealthUpdate();
-
ThirdPersonMPCharacter.cppThirdPersonMPCharacter.cpp
에 다음 구현을 추가합니다.void AThirdPersonMPCharacter::OnHealthUpdate() { //클라이언트 전용 함수 기능 if (IsLocallyControlled()) { FString healthMessage = FString::Printf(TEXT("You now have %f health remaining."), CurrentHealth); GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, healthMessage); if (CurrentHealth <= 0) { FString deathMessage = FString::Printf(TEXT("You have been killed.")); GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, deathMessage); } } //서버 전용 함수 기능 if (GetLocalRole() == ROLE_Authority) { FString healthMessage = FString::Printf(TEXT("%s now has %f health remaining."), *GetFName().ToString(), CurrentHealth); GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, healthMessage); } //모든 머신에서 실행되는 함수 /* 여기에 대미지 또는 사망의 결과로 발생하는 특별 함수 기능 배치 */ }
이 함수를 사용하여 플레이어의
CurrentHealth
변경에 대응하는 업데이트를 수행할 것입니다. 현재 그 기능은 화면상의 디버그 메시지로 제한되지만 기능이 더 추가될 수 있습니다. 예를 들어OnDeath
함수는 사망 애니메이션을 트리거하기 위해 모든 머신에서 호출됩니다.OnHealthUpdate
는 리플리케이트되지 않아서 모든 디바이스에서 수동으로 호출해야 한다는 점에 유의하세요. -
ThirdPersonMPCharacter.cppThirdPersonMPCharacter.cpp
에서OnRep_CurrentHealth
에 다음 구현을 추가합니다.void AThirdPersonMPCharacter::OnRep_CurrentHealth() { OnHealthUpdate(); }
변수는 항상 리플리케이트되는 것이 아니라 값이 변경될 때마다 리플리케이트되며,
RepNotify
는 리플리케이트된 변수 값을 클라이언트가 성공적으로 받을 때마다 실행됩니다. 그러므로 플레이어의CurrentHealth
를 서버에서 변경할 때마다 각 연결된 클라이언트에서OnRep_CurrentHealth
가 실행된다고 예상할 수 있습니다. 따라서OnRep_CurrentHealth
는 클라이언트의 머신에서OnHealthUpdate
를 호출하기에 이상적인 위치입니다.
3. 플레이어가 대미지에 반응하게 만들기
플레이어의 체력을 구현했으니, 이 클래스 밖에서 플레이어의 체력을 수정할 방법을 만들어야 합니다.
-
ThirdPersonMPCharacter.hThirdPersonMPCharacter.h
에서Public
아래 다음 함수 선언을 추가합니다.public: /** 최대 체력 게터*/ UFUNCTION(BlueprintPure, Category="Health") FORCEINLINE float GetMaxHealth() const { return MaxHealth; } /** 현재 체력 게터*/ UFUNCTION(BlueprintPure, Category="Health") FORCEINLINE float GetCurrentHealth() const { return CurrentHealth; } /** 현재 체력 세터. 값을 0과 MaxHealth 사이로 범위제한하고 OnHealthUpdate를 호출합니다. 서버에서만 호출되어야 합니다.*/ UFUNCTION(BlueprintCallable, Category="Health") void SetCurrentHealth(float healthValue); /** 대미지를 받는 이벤트. APawn에서 오버라이드됩니다.*/ UFUNCTION(BlueprintCallable, Category = "Health") float TakeDamage( float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser ) override;
GetMaxHealth
및GetCurrentHealth
함수는 C++와 블루프린트에서AThirdPersonMPCharacter
밖의 플레이어 체력 값에 액세스할 수 있는 게터를 제공합니다.const
함수를 사용하면 이 값의 수정을 허용하지 않으면서 안전하게 가져올 수 있습니다. 플레이어의 체력을 설정하고 대미지를 입히기 위한 함수도 선언하겠습니다. -
ThirdPersonMPCharacter.cppThirdPersonMPCharacter.cpp
에서SetCurrentHealth
에 다음 구현을 추가합니다.void AThirdPersonMPCharacter::SetCurrentHealth(float healthValue) { if (GetLocalRole() == ROLE_Authority) { CurrentHealth = FMath::Clamp(healthValue, 0.f, MaxHealth); OnHealthUpdate(); } }
SetCurrentHealth
를 사용하면AThirdPersonMPCharacter
외부에서 통제된 방식으로 플레이어의CurrentHealth
를 수정할 수 있습니다. 리플리케이트되는 함수는 아니지만, 액터의 네트워크 역할이ROLE_Authority
임을 확인하여 이 함수가 호스팅된 게임 서버에서 호출될 때만 실행되도록 제한하는 것입니다. 이 제한은CurrentHealth
를 0과 플레이어의MaxHealth
사이 값으로 범위제한하여CurrentHealth
를 유효하지 않은 값으로 설정할 수 없게 합니다. 또한OnHealthUpdate
를 호출하여 서버와 클라이언트 모두 이 함수에 대해 병렬 호출을 갖게 합니다. 이 부분은 서버가 RepNotify를 수신하지 않기 때문에 필요합니다.이러한 '세터' 함수가 모든 변수에 필요한 것은 아니지만, 플레이 도중 빈번하게 변경되는 민감한 게임플레이 변수에는 있는 것이 좋습니다. 여러 소스에 의해 수정될 수 있는 변수의 경우 특히 그렇습니다. 변수에 대한 실시간 변경을 더 일관되고, 디버그하기 쉽고, 새 함수 기능으로 확장하기 쉽게 만들어 주므로 싱글 플레이어 게임과 멀티 플레이어 게임에 모두 권장되는 모범 관행입니다.
-
ThirdPersonMPCharacter.cppThirdPersonMPCharacter.cpp
에서TakeDamage
에 다음 구현을 추가합니다.float AThirdPersonMPCharacter::TakeDamage(float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) { float damageApplied = CurrentHealth - DamageTaken; SetCurrentHealth(damageApplied); return damageApplied; }
액터에 대미지를 적용하는 내장 함수는 해당 액터에 대한 기본
TakeDamage
함수를 호출합니다. 여기서는SetCurrentHealth
를 사용하여 간단한 방식으로 체력 감소를 구현하겠습니다.
지금까지 이 섹션을 따라 하셨다면 액터에 대미지를 적용하는 플로는 다음과 같습니다.
- 외부 액터 또는 함수가 캐릭터에
CauseDamage
를 호출하고, 캐릭터는TakeDamage
함수를 호출합니다. TakeDamage
가SetCurrentHealth
를 호출하여 서버에서 플레이어의 현재 체력 값을 변경합니다.SetCurrentHealth
가 서버에서OnHealthUpdate
를 호출하여 플레이어의 체력 변경에 반응하는 모든 함수 기능을 실행하게 합니다.CurrentHealth
가 해당 캐릭터와 연결된 모든 클라이언트의 사본에 리플리케이트됩니다.- 각 클라이언트는 새
CurrentHealth
값을 서버로부터 받으면OnRep_CurrentHealth
를 호출합니다. OnRep_CurrentHealth
가OnHealthUpdate
를 호출하여 각 클라이언트가 동일한 방식으로 새CurrentHealth
값에 반응하게 합니다.
이 구현에는 두 가지 장점이 있습니다. 첫째, 두 핵심 함수 SetCurrentHealth
및 OnHealthUpdate
를 중심으로 새 함수 기능을 추가하여 워크플로를 압축합니다. 이렇게 하면 향후 코드를 관리하고 확장하기가 더 쉬워집니다. 둘째, 이 구현은 서버, 클라이언트, NetMulticast RPC를 사용하지 않고 CurrentHealth
의 리플리케이션에만 의존하여 모든 필수 변경을 트리거하므로 네트워크 전반에 전송되는 정보의 양이 압축됩니다. 어떤 함수를 구현하든 CurrentHealth
는 리플리케이트되어야 하므로, 이는 체력 변경을 리플리케이트하는 가장 효율적인 모델입니다.
4. 발사체와 리플리케이션 생성
-
언리얼 에디터 내에서 툴(Tool) 메뉴 또는 콘텐츠 브라우저(Content Browser) 를 사용하여 새로운 C++ 클래스(New C++ Class) 를 생성합니다.
-
부모 클래스 선택(Choose Parent Class) 메뉴에서 액터(Actor) 를 부모 클래스로 선택하고 다음(Next) 을 클릭합니다.
이미지를 클릭하면 최대 크기로 볼 수 있습니다.
-
새 액터 이름(Name Your New Actor) 메뉴에서 클래스를 ThirdPersonMPProjectile 로 명명하고 클래스 생성(Create Class) 을 클릭합니다.
이미지를 클릭하면 최대 크기로 볼 수 있습니다.
-
ThirdPersonMPProjectile.hThirdPersonMPProjectile.h
를 열고 클래스 정의의public
아래 다음 코드를 추가합니다.public: // 콜리전 테스트에 사용되는 스피어 컴포넌트 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components") class USphereComponent* SphereComponent; // 오브젝트의 비주얼 표현을 제공하는 스태틱 메시 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components") class UStaticMeshComponent* StaticMesh; // 발사체 움직임을 처리하는 무브먼트 컴포넌트 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components") class UProjectileMovementComponent* ProjectileMovementComponent; // 발사체가 다른 오브젝트에 영향을 미치고 폭발할 때 사용되는 파티클 UPROPERTY(EditAnywhere, Category = "Effects") class UParticleSystem* ExplosionEffect; //이 발사체가 가할 대미지 타입과 대미지 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Damage") TSubclassOf<class UDamageType> DamageType; //이 발사체가 가하는 대미지 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Damage") float Damage;
이 선언의 각 유형에서는
class
키워드를 먼저 사용해야 합니다. 이렇게 하면 모두 변수 선언일 뿐 아니라 자체 클래스의 전방 선언이 되므로, 클래스가 헤더 파일에서 분명하게 인식됩니다. 다음 단계에 이를 위한#include
를 CPP 파일에 추가할 것입니다.여기서 선언하는 프로퍼티는 다음을 제공할 것입니다.
- 발사체의 비주얼 표현이 될 스태틱 메시 컴포넌트(Static Mesh Component)
- 콜리전 테스트에 사용되는 스피어(Sphere) 컴포넌트
- 발사체를 움직이기 위한 발사체 무브먼트 컴포넌트(Projectile Movement Component)
- 이후 단계에서 폭발 이펙트를 스폰할 때 사용할 파티클 시스템(Particle System) 레퍼런스
- 대미지 이벤트에 사용할 대미지 타입
- 캐릭터가 이 발사체에 맞았을 때 체력을 얼마나 줄일지 나타내는 대미지(Damage) float 값
그러나 아직 정의된 것은 없습니다.
캐릭터 무브먼트 컴포넌트와 마찬가지로 발사체 무브먼트 컴포넌트도
bReplicates
가True
로 설정된 경우 소속된 액터가 움직이면 자동으로 리플리케이션을 처리합니다. -
ThirdPersonMPProjectile.cppThirdPersonMPProjectile.cpp
를 열고 파일 상단의#include
구문 중#include "ThirdPersonMPProjectile.h"
아래 다음 코드를 추가합니다.#include "Components/SphereComponent.h" #include "Components/StaticMeshComponent.h" #include "GameFramework/ProjectileMovementComponent.h" #include "GameFramework/DamageType.h" #include "Particles/ParticleSystem.h" #include "Kismet/GameplayStatics.h" #include "UObject/ConstructorHelpers.h"
이 실습에서 모두 사용해 볼 것입니다. 첫 4개는 사용할 컴포넌트입니다.
GamePlayStatics.h
는 기본 게임플레이 함수에 대한 액세스를,ConstructorHelpers.h
는 컴포넌트 구성에 유용한 생성자 함수에 대한 액세스를 제공합니다. -
ThirdPersonMPProjectile.cppAThirdPersonMPProjectile
생성자 내부에 다음 코드를 추가합니다.bReplicates = true;
bReplicates
변수는 이 액터가 리플리케이트 대상임을 게임에 알립니다. 기본적으로 액터는 해당 액터를 스폰하는 머신에서만 로컬로 존재합니다.bReplicates
를True
로 설정하면, 액터의 권위 있는 사본이 서버에 존재하는 한 액터를 연결된 모든 클라이언트에 리플리케이트하려 할 것입니다. -
ThirdPersonMPProjectile.cppAThirdPersonMPProjectile
생성자 내부에 다음 코드를 추가합니다.//발사체와 콜리전의 루트 컴포넌트 역할을 할 SphereComponent 정의 SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent")); SphereComponent->InitSphereRadius(37.5f); SphereComponent->SetCollisionProfileName(TEXT("BlockAllDynamic")); RootComponent = SphereComponent;
이 코드는 오브젝트가 생성되었을 때 SphereComponent를 정의하여 발사체 콜리전을 제공합니다.
-
ThirdPersonMPProjectile.cppAThirdPersonMPProjectile
생성자 내부에 다음 코드를 추가합니다.//비주얼 표현을 담당할 메시 정의 static ConstructorHelpers::FObjectFinder<UStaticMesh> DefaultMesh(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere")); StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh")); StaticMesh->SetupAttachment(RootComponent); //사용할 메시 에셋이 발견되면 스태틱 메시와 위치/스케일 설정 if (DefaultMesh.Succeeded()) { StaticMesh->SetStaticMesh(DefaultMesh.Object); StaticMesh->SetRelativeLocation(FVector(0.0f, 0.0f, -37.5f)); StaticMesh->SetRelativeScale3D(FVector(0.75f, 0.75f, 0.75f)); }
이 코드는 비주얼 표현으로 사용하는 StaticMeshComponent를 정의합니다. 코드는 StarterContent 안의 Shape_Sphere 메시를 자동으로 찾아서 채워 넣으려고 시도합니다. 스피어 또한 스케일 조절되어 SphereComponent의 크기에 맞게 정렬됩니다.
-
ThirdPersonMPProjectile.cppAThirdPersonMPProjectile
생성자 내부에 다음 코드를 추가합니다.static ConstructorHelpers::FObjectFinder<UParticleSystem> DefaultExplosionEffect(TEXT("/Game/StarterContent/Particles/P_Explosion.P_Explosion")); if (DefaultExplosionEffect.Succeeded()) { ExplosionEffect = DefaultExplosionEffect.Object; }
ExplosionEffect
에 대한 에셋 레퍼런스를 StarterContent 내의 P_Explosion 에셋으로 설정합니다. -
ThirdPersonMPProjectile.cppAThirdPersonMPProjectile
생성자 내부에 다음 코드를 추가합니다.//발사체 무브먼트 컴포넌트 정의 ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement")); ProjectileMovementComponent->SetUpdatedComponent(SphereComponent); ProjectileMovementComponent->InitialSpeed = 1500.0f; ProjectileMovementComponent->MaxSpeed = 1500.0f; ProjectileMovementComponent->bRotationFollowsVelocity = true; ProjectileMovementComponent->ProjectileGravityScale = 0.0f;
이 코드는 발사체에 대해 발사체 무브먼트 컴포넌트를 정의합니다. 이 컴포넌트는 리플리케이트되며, 이 컴포넌트가 서버에서 수행하는 모든 움직임은 클라이언트 측에서 재생성될 것입니다.
-
ThirdPersonMPProjectile.cppAThirdPersonMPProjectile
생성자 내부에 다음 코드를 추가합니다.DamageType = UDamageType::StaticClass(); Damage = 10.0f;
이 코드는 발사체가 액터에 가하는 대미지의 크기와 대미지 이벤트에 사용할 대미지 타입을 초기화합니다. 아직 새 대미지 타입을 정의하지 않았으므로 여기서는 베이스
UDamageType
으로 초기화합니다.
5. 발사체가 대미지를 유발하게 만들기
지금까지 따라 하셨다면 서버에서 발사체를 스폰할 수 있고, 이 발사체가 모든 클라이언트에서 나타나고 움직일 것입니다. 발사체가 벽이나 차단 오브젝트에 부딪치면 멈출 것입니다. 이제 발사체가 플레이어에게 대미지를 가하게 해야 하고, 세션에 연결된 모든 클라이언트에게 폭발 이펙트를 보여줘야 합니다.
-
ThirdPersonMPProjectile.hThirdPersonMPProjectile.h
에서Protected
아래 다음 코드를 추가합니다.protected: virtual void Destroyed() override;
-
ThirdPersonMPProjectile.cppThirdPersonMPProjectile.cpp
에서 이 함수에 다음 구현을 추가합니다.void AThirdPersonMPProjectile::Destroyed() { FVector spawnLocation = GetActorLocation(); UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, spawnLocation, FRotator::ZeroRotator, true, EPSCPoolMethod::AutoRelease); }
Destroyed
함수는 액터가 소멸될 때마다 호출됩니다. 파티클 이미터 자체는 일반적으로 리플리케이트되지 않지만, 액터 소멸은 리플리케이트되므로 서버에서 이 투사체를 소멸시킵니다. 이 함수는 연결된 각 클라이언트에서 각 사본을 소멸시킬 때 호출됩니다. 그 결과 발사체가 소멸되면 모든 플레이어가 폭발 이펙트를 보게 됩니다. -
ThirdPersonMPProjectile.hThirdPersonMPProjectile.h
에서Protected
아래 다음 코드를 추가합니다.UFUNCTION(Category="Projectile") void OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
-
ThirdPersonMPProjectile.cppThirdPersonMPProjectile.cpp
에서 이 함수에 다음 구현을 추가합니다.void AThirdPersonMPProjectile::OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit) { if ( OtherActor ) { UGameplayStatics::ApplyPointDamage(OtherActor, Damage, NormalImpulse, Hit, GetInstigator()->Controller, this, DamageType); } Destroy(); }
발사체가 오브젝트에 부딪칠 때 호출할 함수입니다. 오브젝트가 유효한 액터와 부딪칠 경우
ApplyPointDamage
함수를 호출하여 콜리전이 발생한 위치에 대미지를 가할 것입니다. 한편 모든 콜리전은 어떤 표면에 충돌했든 이 액터를 소멸시키면서 폭발 이펙트를 일으킬 것입니다. -
ThirdPersonMPProjectile.cppThirdPersonMPProjectile.cpp
에서RootComponent = SphereComponent
아래의AThirdPersonMPProjectile
생성자에 다음 코드를 추가합니다.//발사체 충돌 함수를 히트 이벤트에 등록 if (GetLocalRole() == ROLE_Authority) { SphereComponent->OnComponentHit.AddDynamic(this, &AThirdPersonMPProjectile::OnProjectileImpact); }
OnProjectileImpact
함수를 발사체의 주요 콜리전 컴포넌트가 될 스피어 컴포넌트의OnComponentHit
이벤트에 등록합니다. 서버만 이 게임플레이 로직을 실행하도록OnProjectileImpact
등록 전에GetLocalRole() == ROLE_Authority
을 확인합니다.
6. 발사체 발사하기
-
언리얼 에디터 를 열고 화면 상단의 편집(Edit) 드롭다운 메뉴에서 프로젝트 세팅(Project Settings) 을 엽니다.
-
엔진(Engine) 섹션에서 입력(Input) 을 클릭하여 프로젝트의 입력 세팅을 엽니다. 바인딩(Bindings) 섹션을 펼쳐서 새 엔트리를 추가합니다. "Fire "라고 명명한 다음 왼쪽 마우스 버튼(Left Mouse Button) 을 이 액션이 바인딩될 키로 선택합니다.
이미지를 클릭하면 최대 크기로 볼 수 있습니다.
-
ThirdPersonMPCharacter.cppThirdPersonMPCharacter.cpp
에서#include "Engine/Engine.h"
줄 아래 다음#include
를 추가합니다.#include "ThirdPersonMPProjectile.h"
이렇게 하면 캐릭터 클래스가 발사체 타입을 인식하고 스폰할 것입니다.
-
ThirdPersonMPCharacter.hThirdPersonMPCharacter.h
에서protected
아래 다음 코드를 추가합니다.protected: UPROPERTY(EditDefaultsOnly, Category="Gameplay|Projectile") TSubclassOf<class AThirdPersonMPProjectile> ProjectileClass; /** 발사 딜레이, 단위는 초. 테스트 발사체의 발사 속도를 제어하는 데 사용되지만, 서버 함수의 추가분이 SpawnProjectile을 입력에 직접 바인딩하지 않게 하는 역할도 합니다.*/ UPROPERTY(EditDefaultsOnly, Category="Gameplay") float FireRate; /** true인 경우 발사체를 발사하는 프로세스 도중입니다. */ bool bIsFiringWeapon; /** 무기 발사 시작 함수*/ UFUNCTION(BlueprintCallable, Category="Gameplay") void StartFire(); /** 무기 발사 종료 함수. 호출되면 플레이어가 StartFire를 다시 사용할 수 있습니다.*/ UFUNCTION(BlueprintCallable, Category = "Gameplay") void StopFire(); /** 발사체를 스폰하는 서버 함수*/ UFUNCTION(Server, Reliable) void HandleFire(); /** 스폰 사이에 발사 속도 딜레이를 넣는 타이머 핸들*/ FTimerHandle FiringTimer;
발사체 발사에 사용할 변수와 함수입니다.
HandleFire
는 이 튜토리얼에서 구현하는 유일한 RPC로, 서버에서 발사체를 스폰합니다.Server
지정자가 있기 때문에 클라이언트에서 이를 호출하려는 모든 시도는 네트워크를 통해 서버의 권위 있는 캐릭터로 전달됩니다.HandleFire
에는Reliable
지정자도 있기 때문에 호출될 때마다 신뢰할 수 있는 RPC의 큐 등록에 배치되며, 서버가 수신에 성공하면 큐 등록에서 제거됩니다. 이는 서버가 이 함수의 호출을 확실하게 수신하도록 보장합니다. 그러나 너무 많은 RPC가 제거 없이 동시에 배치되면 신뢰할 수 있는 RPC의 큐 등록이 추가분을 유발할 수 있으며, 그 경우 사용자가 강제로 연결 해제됩니다. 그러므로 플레이어가 이 함수를 호출하는 빈도에 주의해야 합니다. -
ThirdPersonMPCharacter.cppThirdPersonMPCharacter.cpp
에서AThirdPersonMPCharacter
생성자 하단에 다음 코드를 추가합니다.//발사체 클래스 초기화 ProjectileClass = AThirdPersonMPProjectile::StaticClass(); //발사 속도 초기화 FireRate = 0.25f; bIsFiringWeapon = false;
이 코드는 발사체 발사를 처리하는 데 필요한 변수를 초기화합니다.
-
ThirdPersonMPCharacter.cppThirdPersonMPCharacter.cpp
에 다음 구현을 추가합니다.void AThirdPersonMPCharacter::StartFire() { if (!bIsFiringWeapon) { bIsFiringWeapon = true; UWorld* World = GetWorld(); World->GetTimerManager().SetTimer(FiringTimer, this, &AThirdPersonMPCharacter::StopFire, FireRate, false); HandleFire(); } } void AThirdPersonMPCharacter::StopFire() { bIsFiringWeapon = false; } void AThirdPersonMPCharacter::HandleFire_Implementation() { FVector spawnLocation = GetActorLocation() + ( GetActorRotation().Vector() * 100.0f ) + (GetActorUpVector() * 50.0f); FRotator spawnRotation = GetActorRotation(); FActorSpawnParameters spawnParameters; spawnParameters.Instigator = GetInstigator(); spawnParameters.Owner = this; AThirdPersonMPProjectile* spawnedProjectile = GetWorld()->SpawnActor<AThirdPersonMPProjectile>(spawnLocation, spawnRotation, spawnParameters); }
StartFire
는 플레이어가 발사 프로세스를 개시하기 위해 로컬 머신에서 호출하는 함수입니다. 다음 기준에 따라 사용자가HandleFire
를 얼마나 자주 호출할 수 있는지 제한합니다.- 사용자는 이미 발사체를 발사하는 중일 때 또 발사할 수 없습니다. 이 상태는
StartFire
가 호출될 때true
로 설정되는bFiringWeapon
으로 지정됩니다. bFiringWeapon
은StopFire
가 호출되어야false
로 설정됩니다.StopFire
는FireRate
길이 타이머가 종료되면 호출됩니다.
사용자가 발사체를 발사하면 다시 발사할 수 있기까지
FireRate
초만큼 기다려야 한다는 뜻입니다. 이 함수는StartFire
가 어떤 입력에 바인딩되어 있든 일관되게 기능합니다. 예를 들어 사용자가 '발사' 명령을 마우스 휠 스크롤 같은 부적절한 입력에 바인딩하거나 버튼을 빠르게 연타하는 경우에도 이 함수는 적절한 간격을 두고 실행되며,HandleFire
호출로 사용자의 신뢰할 수 있는 함수 큐 등록에 추가분을 유발하지 않습니다.HandleFire
는 서버 RPC이며 CPP 파일의 구현에서는 반드시 함수 이름에 접미사_Implementation
이 추가되어야 하기 때문입니다. 여기서의 구현은 캐릭터의 회전 제어를 사용하여 카메라가 향한 방향을 구한 다음 해당 방향으로 발사체를 스폰하여 플레이어가 조준할 수 있게 합니다. 그러면 발사체의 발사체 무브먼트 컴포넌트가 해당 방향을 향한 이동을 처리합니다. - 사용자는 이미 발사체를 발사하는 중일 때 또 발사할 수 없습니다. 이 상태는
-
ThirdPersonMPCharacter.cppThirdPersonMPCharacter.cpp
에서 함수SetupPlayerInputComponent
하단에 다음을 추가합니다.// 발사체 발사 처리 PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AThirdPersonMPCharacter::StartFire);
StartFire
를 이 섹션 첫 단계에 생성한 발사(Fire) 입력 액션과 바인딩하여 사용자가 활성화할 수 있게 합니다.
7. 게임 테스트하기
-
에디터에서 프로젝트를 엽니다. 편집(Edit) 드롭다운 메뉴를 클릭하고 에디터 개인설정(Editor Preferences) 을 엽니다.
-
레벨 에디터(Level Editor) 섹션으로 가서 플레이(Play) 메뉴를 클릭합니다. 멀티플레이어 옵션(Multiplayer Options) 을 찾고 플레이 넷 모드(Play Net Mode) 를 Play As Listen Server 로 변경합니다. 또한 클라이언트 플레이 수(Play Number of Clients) 를 2 로 설정합니다.
이미지를 클릭하면 최대 크기로 볼 수 있습니다.
-
플레이(Play) 버튼을 누릅니다. 메인 에디터에서 플레이(PIE) 창이 서버로서 멀티플레이어 세션을 시작하고, 보조 PIE 창이 열려 클라이언트로서 연결될 것입니다.
최종 결과
이미지를 클릭하면 최대 크기로 볼 수 있습니다.
게임 내의 두 플레이어는 서로가 움직이는 것을 볼 수 있고, 서로를 향해 커스텀 발사체를 발사할 수 있습니다. 한 플레이어가 커스텀 발사체에 맞으면 두 플레이어 모두에게 폭발 파티클이 보이고, 맞은 플레이어는 얼마나 대미지를 받았는지, 현재 체력은 얼마인지 알려주는 '히트' 메시지를 받습니다. 세션의 나머지 플레이어에게는 아무것도 보이지 않습니다. 플레이어의 체력이 0으로 줄어들면 사망했다는 메시지를 받습니다.
이 가이드를 완료하셨으니 변수와 컴포넌트 리플리케이션의 개요, 네트워크 역할을 다루는 방법, RPC를 사용하기에 적절한 시기 등 C++에서 멀티플레이어 함수 기능을 만드는 데 필요한 기초를 파악하셨을 것입니다. 이 정보를 바탕으로 언리얼의 서버-클라이언트 모델에서 여러분만의 멀티플레이어 게임을 만들 수 있을 것입니다.
직접 해 보기
네트워크 멀티플레이어 프로그래밍 실력을 더 키우려면 다음을 시도해 보세요.
- 발사체의 OnHit 함수 기능을 확장하여 발사체가 타깃에 부딪쳤을 때 폭발 반경을 시뮬레이션하는 스피어 트레이스를 생성하는 등의 추가 이펙트 넣기
- ThirdPersonMPProjectile을 확장하고 ProjectileMovement 컴포넌트를 실험하면서 다르게 행동하는 새 변수 생성하기
- ThirdPersonMPCharacter의 TakeDamage 함수를 확장하여 플레이어 폰을 처치하고 리스폰하기
- 로컬 플레이어 컨트롤러에 HUD를 추가하여 리플리케이트된 정보를 표시하거나 클라이언트 함수에 반응하게 하기
- DamageTypes를 사용하여 플레이어가 처치되었을 때 개인화된 메시지 생성하기
- 게임 모드, 플레이어 상태, 게임 상태를 활용하여 매치를 조정하는 규칙과 플레이어 통계 및 점수판을 만들기
코드 샘플
ThirdPersonMPProjectile.h // Copyright 1998-2022 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ThirdPersonMPProjectile.generated.h"
UCLASS()
class THIRDPERSONMP_API AThirdPersonMPProjectile : public AActor
{
GENERATED_BODY()
public:
// 이 액터 프로퍼티의 디폴트값 설정
AThirdPersonMPProjectile();
protected:
// 게임 시작 또는 스폰 시 호출
virtual void BeginPlay() override;
public:
// 프레임마다 호출
virtual void Tick(float DeltaTime) override;
public:
// 콜리전 테스트에 사용되는 스피어 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
class USphereComponent* SphereComponent;
// 오브젝트의 비주얼 표현을 제공하는 스태틱 메시
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
class UStaticMeshComponent* StaticMesh;
// 발사체 움직임을 처리하는 무브먼트 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
class UProjectileMovementComponent* ProjectileMovementComponent;
// 발사체가 다른 오브젝트에 영향을 미치고 폭발할 때 사용되는 파티클
UPROPERTY(EditAnywhere, Category = "Effects")
class UParticleSystem* ExplosionEffect;
//이 발사체가 가할 대미지 타입과 대미지
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Damage")
TSubclassOf<class UDamageType> DamageType;
//이 발사체가 가하는 대미지
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Damage")
float Damage;
protected:
virtual void Destroyed() override;
UFUNCTION(Category = "Projectile")
void OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
};
ThirdPersonMPProjectile.cpp
// Copyright 1998-2022 Epic Games, Inc. All Rights Reserved.
#include "ThirdPersonMPProjectile.h"
#include "Components/SphereComponent.h"
#include "Components/StaticMeshComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "GameFramework/DamageType.h"
#include "Particles/ParticleSystem.h"
#include "Kismet/GameplayStatics.h"
#include "UObject/ConstructorHelpers.h"
// 디폴트값 설정
AThirdPersonMPProjectile::AThirdPersonMPProjectile()
{
// 이 액터가 프레임마다 Tick()을 호출하도록 설정합니다. 이 설정이 필요 없는 경우 비활성화하면 퍼포먼스가 향상됩니다.
PrimaryActorTick.bCanEverTick = true;
bReplicates = true;
//발사체와 콜리전의 루트 컴포넌트 역할을 할 SphereComponent 정의
SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent"));
SphereComponent->InitSphereRadius(37.5f);
SphereComponent->SetCollisionProfileName(TEXT("BlockAllDynamic"));
RootComponent = SphereComponent;
//발사체 충돌 함수를 히트 이벤트에 등록
if (GetLocalRole() == ROLE_Authority)
{
SphereComponent->OnComponentHit.AddDynamic(this, &AThirdPersonMPProjectile::OnProjectileImpact);
}
//비주얼 표현을 담당할 메시 정의
static ConstructorHelpers::FObjectFinder<UStaticMesh> DefaultMesh(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
StaticMesh->SetupAttachment(RootComponent);
//사용할 메시 에셋이 발견되면 스태틱 메시와 위치/스케일 설정
if (DefaultMesh.Succeeded())
{
StaticMesh->SetStaticMesh(DefaultMesh.Object);
StaticMesh->SetRelativeLocation(FVector(0.0f, 0.0f, -37.5f));
StaticMesh->SetRelativeScale3D(FVector(0.75f, 0.75f, 0.75f));
}
static ConstructorHelpers::FObjectFinder<UParticleSystem> DefaultExplosionEffect(TEXT("/Game/StarterContent/Particles/P_Explosion.P_Explosion"));
if (DefaultExplosionEffect.Succeeded())
{
ExplosionEffect = DefaultExplosionEffect.Object;
}
//발사체 무브먼트 컴포넌트 정의
ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement"));
ProjectileMovementComponent->SetUpdatedComponent(SphereComponent);
ProjectileMovementComponent->InitialSpeed = 1500.0f;
ProjectileMovementComponent->MaxSpeed = 1500.0f;
ProjectileMovementComponent->bRotationFollowsVelocity = true;
ProjectileMovementComponent->ProjectileGravityScale = 0.0f;
DamageType = UDamageType::StaticClass();
Damage = 10.0f;
}
// 게임 시작 또는 스폰 시 호출
void AThirdPersonMPProjectile::BeginPlay()
{
Super::BeginPlay();
}
// 프레임마다 호출
void AThirdPersonMPProjectile::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AThirdPersonMPProjectile::Destroyed()
{
FVector spawnLocation = GetActorLocation();
UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, spawnLocation, FRotator::ZeroRotator, true, EPSCPoolMethod::AutoRelease);
}
void AThirdPersonMPProjectile::OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
if (OtherActor)
{
UGameplayStatics::ApplyPointDamage(OtherActor, Damage, NormalImpulse, Hit, GetInstigator()->Controller, this, DamageType);
}
Destroy();
}
ThirdPersonMPCharacter.h
// Copyright 1998-2022 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "ThirdPersonMPCharacter.generated.h"
UCLASS(config=Game)
class AThirdPersonMPCharacter : public ACharacter
{
GENERATED_BODY()
/** 캐릭터 뒤에 카메라를 배치하는 카메라 붐 */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
class USpringArmComponent* CameraBoom;
/** 카메라 따라가기 */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
class UCameraComponent* FollowCamera;
public:
AThirdPersonMPCharacter();
/** 프로퍼티 리플리케이션 */
void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
/** 베이스 회전 속도, 단위는 도(º)/초. 다른 스케일 값 조절로 인해 최종 회전 속도가 영향을 받을 수 있습니다. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Input)
float TurnRateGamepad;
protected:
/** 앞뒤 입력으로 호출 */
void MoveForward(float Value);
/** 좌우 입력으로 호출 */
void MoveRight(float Value);
/**
* 입력을 통해 호출되어 지정된 속도로 회전
* @param Rate 정규화된 비율이며, 1.0인 경우 지정된 회전 속도의 100%를 의미합니다.
*/
void TurnAtRate(float Rate);
/**
* 입력을 통해 호출되어 지정된 속도로 올려다보기/내려다보기
* @param Rate 정규화된 비율이며, 1.0인 경우 지정된 회전 속도의 100%를 의미합니다.
*/
void LookUpAtRate(float Rate);
/** 터치 입력 시작 시 핸들러 */
void TouchStarted(ETouchIndex::Type FingerIndex, FVector Location);
/** 터치 입력 중지 시 핸들러 */
void TouchStopped(ETouchIndex::Type FingerIndex, FVector Location);
// APawn 인터페이스
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
// APawn 인터페이스 종료
public:
/** CameraBoom 서브오브젝트 반환 **/
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
/** FollowCamera 서브오브젝트 반환 **/
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
protected:
/** 플레이어의 최대 체력. 체력의 최댓값입니다. 이 값은 스폰 시 시작되는 캐릭터의 체력 값입니다.*/
UPROPERTY(EditDefaultsOnly, Category = "Health")
float MaxHealth;
/** 플레이어의 현재 체력. 0이 되면 죽은 것으로 간주됩니다.*/
UPROPERTY(ReplicatedUsing = OnRep_CurrentHealth)
float CurrentHealth;
/** 현재 체력에 가해진 변경에 대한 RepNotify*/
UFUNCTION()
void OnRep_CurrentHealth();
/** 업데이트되는 체력에 반응. 서버에서는 수정 즉시 호출, 클라이언트에서는 RepNotify에 반응하여 호출*/
void OnHealthUpdate();
public:
/** 최대 체력 게터*/
UFUNCTION(BlueprintPure, Category = "Health")
FORCEINLINE float GetMaxHealth() const { return MaxHealth; }
/** 현재 체력 게터*/
UFUNCTION(BlueprintPure, Category = "Health")
FORCEINLINE float GetCurrentHealth() const { return CurrentHealth; }
/** 현재 체력 세터. 값을 0과 MaxHealth 사이로 범위제한하고 OnHealthUpdate를 호출합니다. 서버에서만 호출되어야 합니다.*/
UFUNCTION(BlueprintCallable, Category = "Health")
void SetCurrentHealth(float healthValue);
/** 대미지를 받는 이벤트. APawn에서 오버라이드됩니다.*/
UFUNCTION(BlueprintCallable, Category = "Health")
float TakeDamage(float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
protected:
UPROPERTY(EditDefaultsOnly, Category = "Gameplay|Projectile")
TSubclassOf<class AThirdPersonMPProjectile> ProjectileClass;
/** 발사 딜레이, 단위는 초. 테스트 발사체의 발사 속도를 제어하는 데 사용되지만, 서버 함수의 추가분이 SpawnProjectile을 입력에 직접 바인딩하지 않게 하는 역할도 합니다.*/
UPROPERTY(EditDefaultsOnly, Category = "Gameplay")
float FireRate;
/** true인 경우 발사체를 발사하는 프로세스 도중입니다. */
bool bIsFiringWeapon;
/** 무기 발사 시작 함수*/
UFUNCTION(BlueprintCallable, Category = "Gameplay")
void StartFire();
/** 무기 발사 종료 함수. 호출되면 플레이어가 StartFire를 다시 사용할 수 있습니다.*/
UFUNCTION(BlueprintCallable, Category = "Gameplay")
void StopFire();
/** 발사체를 스폰하는 서버 함수*/
UFUNCTION(Server, Reliable)
void HandleFire();
/** 스폰 사이에 발사 속도 딜레이를 넣는 타이머 핸들*/
FTimerHandle FiringTimer;
};
ThirdPersonMPCharacter.cpp
// Copyright 1998-2022 Epic Games, Inc. All Rights Reserved.
#include "ThirdPersonMPCharacter.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "GameFramework/SpringArmComponent.h"
#include "Net/UnrealNetwork.h"
#include "Engine/Engine.h"
#include "ThirdPersonMPProjectile.h"
//////////////////////////////////////////////////////////////////////////
// AThirdPersonMPCharacter
AThirdPersonMPCharacter::AThirdPersonMPCharacter()
{
// 콜리전 캡슐 크기 설정
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
// 입력에 대한 회전 속도 설정
TurnRateGamepad = 50.f;
// 컨트롤러 회전 시 회전하지 않습니다. 카메라에만 영향을 미치도록 합니다.
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
// 캐릭터 무브먼트 환경설정
GetCharacterMovement()->bOrientRotationToMovement = true; // 캐릭터가 입력 방향으로 이동
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f); // 위의 캐릭터가 이동하는 회전 속도
// 참고: 이 변수를 비롯한 많은 변수는 조정하기 위해 다시 컴파일하지 않고도
// 캐릭터 블루프린트에서 미세조정하여 반복작업 시간을 단축할 수 있습니다.
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->MaxWalkSpeed = 500.f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
// 카메라 붐 생성(콜리전 있을 시 플레이어 쪽으로 들어옴)
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 400.0f; // 캐릭터 뒤의 카메라가 이 거리에서 따라옴
CameraBoom->bUsePawnControlRotation = true; // 컨트롤러 기반으로 암 회전
// 카메라 따라가기 생성
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // 카메라를 붐 끝에 어태치하여 붐이 컨트롤러 오리엔테이션에 맞추어 조절되도록 함
FollowCamera->bUsePawnControlRotation = false; // 카메라가 암 기준으로 회전하지 않음
// 참고: 캐릭터로부터 상속받는 메시 컴포넌트에 대한 스켈레탈 메시와 애님 블루프린트 레퍼런스는
// C++ 직접 콘텐츠 레퍼런스를 방지하기 위해 이름이 ThirdPersonCharacter인 파생 블루프린트 에셋에서 설정됨
//플레이어 체력 초기화
MaxHealth = 100.0f;
CurrentHealth = MaxHealth;
//발사체 클래스 초기화
ProjectileClass = AThirdPersonMPProjectile::StaticClass();
//발사 속도 초기화
FireRate = 0.25f;
bIsFiringWeapon = false;
}
//////////////////////////////////////////////////////////////////////////
// 입력
void AThirdPersonMPCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
// 게임플레이 키 바인딩 설정
check(PlayerInputComponent);
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
PlayerInputComponent->BindAxis("Move Forward / Backward", this, &AThirdPersonMPCharacter::MoveForward);
PlayerInputComponent->BindAxis("Move Right / Left", this, &AThirdPersonMPCharacter::MoveRight);
// 2가지 버전의 회전 바인딩이 있어 서로 다른 종류의 디바이스를 다양한 방식으로 처리할 수 있습니다.
// 'turn'은 마우스와 같은 절대 델타를 제공하는 디바이스를 처리합니다.
// 'turnrate'는 아날로그 조이스틱과 같이 변화의 속도를 취급할 디바이스에 사용합니다.
PlayerInputComponent->BindAxis("Turn Right / Left Mouse", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("Turn Right / Left Gamepad", this, &AThirdPersonMPCharacter::TurnAtRate);
PlayerInputComponent->BindAxis("Look Up / Down Mouse", this, &APawn::AddControllerPitchInput);
PlayerInputComponent->BindAxis("Look Up / Down Gamepad", this, &AThirdPersonMPCharacter::LookUpAtRate);
// 터치 디바이스 처리
PlayerInputComponent->BindTouch(IE_Pressed, this, &AThirdPersonMPCharacter::TouchStarted);
PlayerInputComponent->BindTouch(IE_Released, this, &AThirdPersonMPCharacter::TouchStopped);
// 발사체 발사 처리
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AThirdPersonMPCharacter::StartFire);
}
void AThirdPersonMPCharacter::TouchStarted(ETouchIndex::Type FingerIndex, FVector Location)
{
Jump();
}
void AThirdPersonMPCharacter::TouchStopped(ETouchIndex::Type FingerIndex, FVector Location)
{
StopJumping();
}
void AThirdPersonMPCharacter::TurnAtRate(float Rate)
{
// 속도 정보로부터 이 프레임에 대한 델타 계산
AddControllerYawInput(Rate * TurnRateGamepad * GetWorld()->GetDeltaSeconds());
}
void AThirdPersonMPCharacter::LookUpAtRate(float Rate)
{
// 속도 정보로부터 이 프레임에 대한 델타 계산
AddControllerPitchInput(Rate * TurnRateGamepad * GetWorld()->GetDeltaSeconds());
}
void AThirdPersonMPCharacter::MoveForward(float Value)
{
if ((Controller != nullptr) && (Value != 0.0f))
{
// 앞쪽 찾기
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
// 앞쪽 벡터 구하기
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
AddMovementInput(Direction, Value);
}
}
void AThirdPersonMPCharacter::MoveRight(float Value)
{
if ( (Controller != nullptr) && (Value != 0.0f) )
{
// 오른쪽 찾기
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
// 오른쪽 벡터 구하기
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
// 해당 방향으로 이동 추가
AddMovementInput(Direction, Value);
}
}
//////////////////////////////////////////////////////////////////////////
// 리플리케이트된 프로퍼티
void AThirdPersonMPCharacter::GetLifetimeReplicatedProps(TArray <FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
//현재 체력 리플리케이트
DOREPLIFETIME(AThirdPersonMPCharacter, CurrentHealth);
}
void AThirdPersonMPCharacter::OnHealthUpdate()
{
//클라이언트 전용 함수 기능
if (IsLocallyControlled())
{
FString healthMessage = FString::Printf(TEXT("You now have %f health remaining."), CurrentHealth);
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, healthMessage);
if (CurrentHealth <= 0)
{
FString deathMessage = FString::Printf(TEXT("You have been killed."));
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, deathMessage);
}
}
//서버 전용 함수 기능
if (GetLocalRole() == ROLE_Authority)
{
FString healthMessage = FString::Printf(TEXT("%s now has %f health remaining."), *GetFName().ToString(), CurrentHealth);
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, healthMessage);
}
//모든 머신에서 실행되는 함수
/*
여기에 대미지 또는 사망의 결과로 발생하는 특별 함수 기능 배치
*/
}
void AThirdPersonMPCharacter::OnRep_CurrentHealth()
{
OnHealthUpdate();
}
void AThirdPersonMPCharacter::SetCurrentHealth(float healthValue)
{
if (GetLocalRole() == ROLE_Authority)
{
CurrentHealth = FMath::Clamp(healthValue, 0.f, MaxHealth);
OnHealthUpdate();
}
}
float AThirdPersonMPCharacter::TakeDamage(float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
float damageApplied = CurrentHealth - DamageTaken;
SetCurrentHealth(damageApplied);
return damageApplied;
}
void AThirdPersonMPCharacter::StartFire()
{
if (!bIsFiringWeapon)
{
bIsFiringWeapon = true;
UWorld* World = GetWorld();
World->GetTimerManager().SetTimer(FiringTimer, this, &AThirdPersonMPCharacter::StopFire, FireRate, false);
HandleFire();
}
}
void AThirdPersonMPCharacter::StopFire()
{
bIsFiringWeapon = false;
}
void AThirdPersonMPCharacter::HandleFire_Implementation()
{
FVector spawnLocation = GetActorLocation() + (GetActorRotation().Vector() * 100.0f) + (GetActorUpVector() * 50.0f);
FRotator spawnRotation = GetActorRotation();
FActorSpawnParameters spawnParameters;
spawnParameters.Instigator = GetInstigator();
spawnParameters.Owner = this;
AThirdPersonMPProjectile* spawnedProjectile = GetWorld()->SpawnActor<AThirdPersonMPProjectile>(spawnLocation, spawnRotation, spawnParameters);
}