본문 바로가기

공부/이득우의 언리얼 프로그래밍

[Study] Part 3 - 캐릭터 공격 구현 (9/15)

728x90
반응형

 

 

  • 정리
    • 캐릭터 공격 구현
      1. 네트웍 멀티플레이어 구현의 기본 원칙의 이해
      2. 네트웍 멀티플레이어에서 동작하는 캐릭터 공격 구현
      3. 액터 컴포넌트 리플리케이션의 설정과 관련 이벤트 함수의 학습
      4. 캐릭터의 체력 프로퍼티 동기화 구현

 

 

  • 기존의 콤보어택으로 구성되어 있던 공격방식을 단일공격방식으로 변경해준다.
  • 네트웍 멀티플레이의 구현을 위한 4원칙
    1. 클라이언트의 명령은 Server RPC를 사용한다.
    2. 중요한 게임 플레이 판정서버에서 처리한다.
    3. 게임 플레이에 영향을 주는 중요한 정보프로퍼티 리플리케이션을 사용한다.
    4. 클라이언트의 시각적인 효과(Cosmetic)는 Client RPC와 Multicast RPC를 사용한다. (중요하지 않은 정보)
  • 네트웍 멀티플레이를 위한 공격 기능 구현 기획
    • 입력 명령을 전달한 이후에는 모두 서버에서 처리하도록 설계

 

네트웍 멀티플레이를 위한 공격 기능 구현 기획

 

  • 액터 컴포넌트 리플리케이션
    • 언리얼에서 리플리케이션의 주체는 액터
    • 액터가 소유하는 언리얼 오브젝트에 대해 리플리케이션 진행이 가능
      • 이를 통틀어 서브오브젝트(Subobject)라고도 함
    • 스탯을 관리하는 액터 컴포넌트의 리플리케이션 설정
      • 리플리케이션을 지정 : SetIsReplicated(true)
      • 리플리케이션이 준비되면 호출되는 이벤트 함수 : ReadyForReplication

 

protected:
	virtual void GetLifetimeReplicatedProps(TArray< FLifetimeProperty >& OutLifetimeProps) const override;
	void Attack();

	virtual void AttackHitCheck() override;

	UFUNCTION(Server, Reliable, WithValidation)
	void ServerRPCAttack();

	UFUNCTION(NetMulticast, Reliable)
	void MulticastRPCAttack();

	// 프로퍼티로 승격
	UPROPERTY(ReplicatedUsing = OnRep_CanAttack)
	uint8 bCanAttack : 1;

	UFUNCTION()
	void OnRep_CanAttack();
  • ABCharacterPlayer.h
// 캐릭터가 가지고 있는 프로퍼티들을 모두 상속받아 구현해줘야 한다.
void AABCharacterPlayer::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(AABCharacterPlayer, bCanAttack);
}

void AABCharacterPlayer::Attack()
{
	//ProcessComboCommand();

	if (bCanAttack)
	{
		// 공격을 하게 되면 Server로부터 보내도록
		// 신호를 받은 서버는 ServerRPCAttack_Implementation를 실행하게 된다.
		ServerRPCAttack();
	}
}

void AABCharacterPlayer::AttackHitCheck()
{
	// 서버에서만 동작하도록
	if (HasAuthority())
	{
		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

		FHitResult OutHitResult;
		FCollisionQueryParams Params(SCENE_QUERY_STAT(Attack), false, this);

		const float AttackRange = Stat->GetTotalStat().AttackRange;
		const float AttackRadius = Stat->GetAttackRadius();
		const float AttackDamage = Stat->GetTotalStat().Attack;
		const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
		const FVector End = Start + GetActorForwardVector() * AttackRange;

		bool HitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
		if (HitDetected)
		{
			FDamageEvent DamageEvent;
			OutHitResult.GetActor()->TakeDamage(AttackDamage, DamageEvent, GetController(), this);
		}

#if ENABLE_DRAW_DEBUG

		FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
		float CapsuleHalfHeight = AttackRange * 0.5f;
		FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;

		DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(GetActorForwardVector()).ToQuat(), DrawColor, false, 5.0f);

#endif
	}
}

bool AABCharacterPlayer::ServerRPCAttack_Validate()
{
	return true;
}

// 신호를 받은 서버는 ServerRPCAttack_Implementation를 실행하게 된다.
void AABCharacterPlayer::ServerRPCAttack_Implementation()
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	// 서버는 서버와 모든 클라이언트에게 명령을 보내도록
	MulticastRPCAttack();
}

// Multicast RPC
// 서버는 로컬로 실행이 되기 때문에 바로 호출, 다른 모든 클라이언트는 네트워크를 통해서 패킷이 전송되면 함수가 호출
void AABCharacterPlayer::MulticastRPCAttack_Implementation()
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	// 서버에서 실행
	// HasAuthority를 통해 서버로직으로 분리, 서버에서는 중요한 변수들을 설정하도록
	if (HasAuthority())
	{
		// bCanAttack을 변수에서 프로퍼티로 승격시켜 서버에서 해당 값을 바꿀 때 자동으로 모든 클라이언트에 바뀐 값이 전송되도록
		bCanAttack = false;
		
		// OnRep 이벤트 함수의 경우 서버에서는 실행되지 않고, 클라이언트에서만 실행됨.
		// 그렇기 때문에 서버에서는 자동으로 호출되지않기 때문에 명시적으로 호출해줘야 함
		OnRep_CanAttack();

		FTimerHandle Handle;
		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
			{
				bCanAttack = true;
				OnRep_CanAttack();
			}
		), AttackTime, false, -1.0f);
	}

	// 서버와 클라이언트 모두 같이 재생
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	AnimInstance->Montage_Play(ComboActionMontage);
}

// bCanAttack이 프로퍼티이니, 해당 값이 바뀌면 움직임이 바뀔 수 있도록
void AABCharacterPlayer::OnRep_CanAttack()
{
	// 공격할 수 없다면 움직임 x
	if (!bCanAttack)
	{
		GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
	}
	// 공격할 수 있다면 움직임 ok
	else
	{
		GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
	}
}
  • ABCharacterPlayer.cpp

 

  • bCanAttack이라는 공격에 대한 중요한 변수를 프로퍼티로 승격시켜주고, 해당 값이 변경될 때마다 OnRep_CanAttack함수가 호출되도록 설정해준다.
  • Attack 함수에서 공격을 하게 되면 Server로부터 보내도록 신호를 받은 서버는 ServerRPCAttack_Implementation를 실행하게 된다. 신호를 받은 서버는 서버와 모든 클라이언트에게 명령을 보내도록 MulticastRPCAttack를 호출해 모두에게 명령을 보낸다.
  • 또한, 해당 코드는 서버와 클라이언트 둘 다 실행이 되기 때문에 서버에서만 호출될 수 있는 로직을 분리해주기 위해 HasAuthority를 통해 분리시켜준다.

 

protected:
	void SetHp(float NewHp);

	// 프로퍼티로 승격. 프로퍼티가 변경될 때마다 OnRep_CurrentHp함수가 호출되도록
	UPROPERTY(ReplicatedUsing = OnRep_CurrentHp, Transient, VisibleInstanceOnly, Category = Stat)
	float CurrentHp;

	// ...

protected:
	virtual void BeginPlay() override;
	virtual void ReadyForReplication() override;
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

	UFUNCTION()
	void OnRep_CurrentHp();
  • ABCharacterStatComponent.h
UABCharacterStatComponent::UABCharacterStatComponent()
{
	CurrentLevel = 1;
	AttackRadius = 50.0f;

	bWantsInitializeComponent = true;

	SetIsReplicated(true);
}

void UABCharacterStatComponent::BeginPlay()
{
	Super::BeginPlay();

	AB_SUBLOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
}

void UABCharacterStatComponent::ReadyForReplication()
{
	AB_SUBLOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	Super::ReadyForReplication();
}

void UABCharacterStatComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	// Replication 매크로
	DOREPLIFETIME(UABCharacterStatComponent, CurrentHp);
}

// 리플리케이션
void UABCharacterStatComponent::OnRep_CurrentHp()
{
	OnHpChanged.Broadcast(CurrentHp);

	if (CurrentHp <= KINDA_SMALL_NUMBER)
	{
		OnHpZero.Broadcast();
	}
}
  • ABCharacterStatComponent.cpp

 

  • 생성자에 SetIsReplicatedtrue로 설정해준다. 그래야 해당 액터 컴포넌트는 네트워크로 리플리케이션될 준비가 되어 있다고 볼 수 있다.
  • GetLifetimeReplicatedProps를 통해 프로퍼티를 등록해준다.
  • 리플리케이션이 되면 호출할 OnRep 함수를 구성해주고, 값이 변경될 때 브로드캐스트 되도록 설정해준다.

 

 

 

 

클라이언트, 서버에 정상적으로 HP가 적용되는 모습

 

  • 로그를 확인해보면 BeginPlay가 호출되기전에 ReadyForReplication이 호출되는 것을 확인할 수 있다.
  • ReadyForReplication
    • BeginPlay 이전에 호출이 되며, 이때 네트워크로 통신할 모든 준비를 마쳤다라고 이해하면 된다.

 

 

 

 

 

해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이어 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
 

이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해 강의 | 이득우 - 인프런

이득우 | 또 하나의 언리얼 엔진이라고도 불리는 네트웍 멀티플레이어 프레임웍을 학습합니다. 네트웍 멀티플레이어 게임을 제작할 때 반드시 알아야 하는 주요 개념, 내부 동작 원리, 최적화

www.inflearn.com

 

 

728x90