[UE4] Network Quick Start

윤정민·2022년 9월 3일
0

Unreal Engine

목록 보기
18/33
post-thumbnail

시작용 콘텐츠를 포함한 3인칭 프로젝트에서 진행됨

1. RepNotify로 플레이어 체력 리플리케이트

모든 클라이언트가 각 플레이어의 체력에 대해 동기환된 정보를 갖도록 리플리케이트 되어야한다.
또한, 대미지를 입는 플레이어에게 피드백을 제공해야 한다.
RepNotify를 사용하여 RPC에 의존하지 않고 변수에 대한 모든 필수 업데이트를 동기화해보자

(1) 체력값과 RepNotify함수 설정

1) [프로젝트명]Character.h

protected:
/** 플레이어의 최대 체력. 체력의 상한선이자 스폰될 때 가지고 시작하는 체력 값입니다.*/
UPROPERTY(EditDefaultsOnly, Category = "Health")
float MaxHealth;

/** 플레이어의 현재 체력. 0이 되면 죽은 것으로 간주됩니다.*/
UPROPERTY(ReplicatedUsing=OnRep_CurrentHealth)
float CurrentHealth;

/** 현재 체력에 가해진 변경에 대한 RepNotify*/
UFUNCTION()
void OnRep_CurrentHealth();
  • MaxHealth는 리플리케이트 되지 않으며 디폴트만 가능하다.
  • CurrentHealth는 리플리케이트 되지만 블루프린트에서 편집및 액세스가 불가능하다.
  • protected를 사용하며 두 변수 모두 외부 c++클래스로부터의 액세스를 방지한다. 이는 라이브 게임 플레이 도중 두 변수의 원치 않는 변경이 발생할 위험을 최소화한다.
  • Replicated : 서버에서 액터의 사본을 활성화하여 변수 값이 변경될 때마다 연결된 모든 클라이언트에 해당 변수 값을 리플리케이트한다.
  • ReplicatedUsing : 같은 역할을 하지만 리플리케이트된 데이터를 성공적으로 수신하면 트리거되는 RepNotify 함수의 설정을 지원한다. 여기서는 OnRep_CurrentHealth를 사용하여 변경을 기반으로 각 클라이언트에 업데이트를 수행할 것이다.

2) [프로젝트명]Character.cpp

include
#include "Net/UnrealNetwork.h"
#include "Engine/Engine.h"
생성자
//플레이어 체력 초기화
MaxHealth = 100.0f;
CurrentHealth = MaxHealth;

(2) GetLifetimeReplicatedProps함수 설정

1) [프로젝트명]Character.h

/** 프로퍼티 리플리케이션 */
void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

2) [프로젝트명]Character.cpp

GetLifetimeReplicatedProps
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    //현재 체력 리플리케이트
    DOREPLIFETIME(AThirdPersonMPCharacter, CurrentHealth);
  • GetLifetimeReplicatedProps함수는 Replicated 지정자로 지정된 모든 프로퍼티를 리플리케이트하며, 프로퍼티의 리플리케이트 방식을 구성하도록 지원한다. 여기서는 기본적인 CurrentHealth 구현을 사용한다.
  • 리플리케이트가 필요한 프로퍼티를 추가할 때는 반드시 이 함수도 추가하여야한다.

(3) OnHealthUpdate 구현

1) [프로젝트명]Character.h

protected:
/** 업데이트되는 체력에 반응. 서버에서는 수정 즉시 호출, 클라이언트에서는 RepNotify에 반응하여 호출*/
void OnHealthUpdate();

2) [프로젝트명]Character.cpp

//클라이언트 전용 함수 기능
    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);
    }

    //모든 머신에서 실행되는 함수 
    /*  
        여기에 대미지 또는 사망의 결과로 발생하는 특별 함수 기능 배치 
    */
  • CurrnentHealth 변경에 대응하는 업데이트를 수행한다.
  • 추가 함수에는 OnDeath와 같은 함수가 추가될 수 있다.
  • OnHealthUpdate는 리플리케이트 되지 않아 모든 디바이스에서 수동으로 호출해야한다.

(4) 트리거 함수에 OnHealthUpdate 추가

1) [프로젝트명]Character.cpp

void [프로젝트명]Character::OnRep_CurrentHealth()
{
    OnHealthUpdate();
}

2. 플레이어가 대미지에 반응하게 만들기

(1) 체력 관리 함수들 선언

1) [프로젝트명]Character.h

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++과 블루프린트에서 '[프로젝트명]Character' 밖의 플레이어 체력 값에 엑세스할 수 있는 게터를 제공한다.

(2) SetCurrentHealth 구현

1) [프로젝트명]Character.cpp

void A[프로젝트명]Character::SetCurrentHealth(float healthValue)
{
    if (GetLocalRole() == ROLE_Authority)
    {
        CurrentHealth = FMath::Clamp(healthValue, 0.f, MaxHealth);
        OnHealthUpdate();
    }
}
  • A[프로젝트명]Character 외부에서 통제된 방식으로 CurrentHealth를 수정할 수 있다.
  • 리플리케이트되는 함수는 아니지만, 액터의 네트워크 역할이 ROLE_Authority 임을 확인하여 이 함수가 게임을 호스팅하는 서버에서 호출될 때만 실행되도록 제한한다.
  • CurrentHealth 의 값을 0에서 플레이어의 MaxHealth 사이로 범위제한하여 CurrentHealth 를 유효하지 않은 값으로 설정할 수 없게 했다.
  • OnHealthUpdate 를 호출하여 서버와 클라이언트 양쪽에서 이 함수를 병렬 호출하게 했다.

(2) TakeDamage 구현

1) [프로젝트명]Character.cpp

float A[프로젝트명]Character::TakeDamage(float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    float damageApplied = CurrentHealth - DamageTaken;
    SetCurrentHealth(damageApplied);
    return damageApplied;
}

3. 발사체 생성

(1) 새 c++ 클래스 생성

  • 부모클래스는 actor로 설정한다.

(2) 컴포넌트, 파티클 및 대미지 변수 설정

1) Projectile.h

// 콜리전 테스트에 사용되는 스피어 컴포넌트
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;

2) Projectile.cpp

#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"
생성자
bReplicates = true;
  • 리플리케이트 하도록 설정
SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent"));
SphereComponent->InitSphereRadius(37.5f);
SphereComponent->SetCollisionProfileName(TEXT("BlockAllDynamic"));
RootComponent = SphereComponent;
  • 발사체와 콜리전의 루트 컴포넌트 역할을 할 SphereComponent 정의
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;
  • 대미지의 크기와 대미지 이벤트에 사용할 대미지 타입을 초기화 한다. 이때, 아직 새 대미지 타입을 정의하지 않았으니 베이스 UDamageType 으로 초기화 하자.

4. 발사체가 대미지를 유발하도록 만들기

(1) Destroyed 함수

1) Projectile.h

virtual void Destroyed() override;
  • Destroyed 함수 오버라이딩

2) Projectile.cpp

void AProjectile::Destroyed()
{
    FVector spawnLocation = GetActorLocation();
    UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, spawnLocation, FRotator::ZeroRotator, true, EPSCPoolMethod::AutoRelease);
}
  • Destroyed 함수는 액터가 소멸될 때마다 호출된다.
  • 서버에서 발사체가 소멸되면 연결된 각 클라이언트에서 해당 발사체의 사본이 소멸되면서 이 함수가 호출된다.
  • 그 결과 발사체가 소멸되면 모든 플레이어가 폭발 이펙트를 보게된다.

(2) Projectile - Actor 충돌시 함수 구현

1) Projectile.h

protected:
UFUNCTION(Category="Projectile")
void OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

2) Projectile.cpp

OnProjectileImpact
void AProjectile::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();
}
  • projectile과 충돌한 actor가 유효할 시(!- nullptr) ApplyPointDamage 함수 실행
생성자
if (GetLocalRole() == ROLE_Authority)
{
    SphereComponent->OnComponentHit.AddDynamic(this, &AProjectile::OnProjectileImpact);
}
  • 충돌시 OnProjectileImpact 함수 실행
  • 서버에만 이 게임플레이 로직을 실행하도록 GetLocalRole() == ROLE_Authority 을 확인함

5. 발사체 발사하기

(1) Fire 바인딩

  • fire를 왼쪽 마우스로 바인딩 해주자.

(2) 발사 구현

1) [프로젝트명]Character.h

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;

1) [프로젝트명]Character.cpp

header
#include "Projectile.h"
셍성자
//발사체 클래스 초기화
ProjectileClass = AThirdPersonMPProjectile::StaticClass();
//발사 속도 초기화
FireRate = 0.25f;
bIsFiringWeapon = false;
  • 변수들 초기화
함수들 구현
void A[프로젝트명]Character::StartFire()
{
    if (!bIsFiringWeapon)
    {
        bIsFiringWeapon = true;
        UWorld* World = GetWorld();
        World->GetTimerManager().SetTimer(FiringTimer, this, &AThirdPersonMPCharacter::StopFire, FireRate, false);
        HandleFire();
    }
}

void A[프로젝트명]Character::StopFire()
{
    bIsFiringWeapon = false;
}

void A[프로젝트명]Character::HandleFire_Implementation()
{
    FVector spawnLocation = GetActorLocation() + ( GetControlRotation().Vector()  * 100.0f ) + (GetActorUpVector() * 50.0f);
    FRotator spawnRotation = GetControlRotation();

    FActorSpawnParameters spawnParameters;
    spawnParameters.Instigator = GetInstigator();
    spawnParameters.Owner = this;

    AThirdPersonMPProjectile* spawnedProjectile = GetWorld()->SpawnActor<AThirdPersonMPProjectile>(spawnLocation, spawnRotation, spawnParameters);
}
SetupPlayerInputComponent
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &A[프로젝트명]Character::StartFire);
  • 바인딩

6. 최종 결과

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

profile
그냥 하자

0개의 댓글