시작용 콘텐츠를 포함한 3인칭 프로젝트에서 진행됨
모든 클라이언트가 각 플레이어의 체력에 대해 동기환된 정보를 갖도록 리플리케이트 되어야한다.
또한, 대미지를 입는 플레이어에게 피드백을 제공해야 한다.
RepNotify를 사용하여 RPC에 의존하지 않고 변수에 대한 모든 필수 업데이트를 동기화해보자
protected:
/** 플레이어의 최대 체력. 체력의 상한선이자 스폰될 때 가지고 시작하는 체력 값입니다.*/
UPROPERTY(EditDefaultsOnly, Category = "Health")
float MaxHealth;
/** 플레이어의 현재 체력. 0이 되면 죽은 것으로 간주됩니다.*/
UPROPERTY(ReplicatedUsing=OnRep_CurrentHealth)
float CurrentHealth;
/** 현재 체력에 가해진 변경에 대한 RepNotify*/
UFUNCTION()
void OnRep_CurrentHealth();
#include "Net/UnrealNetwork.h"
#include "Engine/Engine.h"
//플레이어 체력 초기화
MaxHealth = 100.0f;
CurrentHealth = MaxHealth;
/** 프로퍼티 리플리케이션 */
void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
//현재 체력 리플리케이트
DOREPLIFETIME(AThirdPersonMPCharacter, CurrentHealth);
protected:
/** 업데이트되는 체력에 반응. 서버에서는 수정 즉시 호출, 클라이언트에서는 RepNotify에 반응하여 호출*/
void 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 [프로젝트명]Character::OnRep_CurrentHealth()
{
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;
void A[프로젝트명]Character::SetCurrentHealth(float healthValue)
{
if (GetLocalRole() == ROLE_Authority)
{
CurrentHealth = FMath::Clamp(healthValue, 0.f, MaxHealth);
OnHealthUpdate();
}
}
float A[프로젝트명]Character::TakeDamage(float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
float damageApplied = CurrentHealth - DamageTaken;
SetCurrentHealth(damageApplied);
return damageApplied;
}
// 콜리전 테스트에 사용되는 스피어 컴포넌트
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;
#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;
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;
virtual void Destroyed() override;
void AProjectile::Destroyed()
{
FVector spawnLocation = GetActorLocation();
UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, spawnLocation, FRotator::ZeroRotator, true, EPSCPoolMethod::AutoRelease);
}
protected:
UFUNCTION(Category="Projectile")
void OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
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();
}
if (GetLocalRole() == ROLE_Authority)
{
SphereComponent->OnComponentHit.AddDynamic(this, &AProjectile::OnProjectileImpact);
}
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;
#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);
}
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &A[프로젝트명]Character::StartFire);