개발 이야기 안하는 개발자

언리얼5_3_5 : PVP 게임 제작 본문

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

언리얼5_3_5 : PVP 게임 제작

07e 2024. 4. 3. 19:57
반응형

네트워크로 전송할 데이터의 구분

 

플레이어의 세부적인 스탯 정보를 모든 클라이언트에 공유할 필요가 있는가?

스탯 정보는 소유자에게만 공유하고 다른 클라이언트(Simulated Proxy)는 체력 정보만공유

 

무기를 먹었을때 서버에서만 스탯값이 적용되도록 로직을 수정한다.

무기 말고 포션과 스크롤은 맨 위에 HasAuthority()로 묶어 주어 서버에서만 연산하도록 한다.

void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
	UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
	if (WeaponItemData)
	{
		if (WeaponItemData->WeaponMesh.IsPending())
		{
			WeaponItemData->WeaponMesh.LoadSynchronous();
		}
		Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh.Get());		
	}

	if (HasAuthority())
	{
		if (WeaponItemData)
		{
			Stat->SetModifierStat(WeaponItemData->ModifierStat);
		}
	}
}

 

위 로직대로라면 서버와 클라이언트 둘다 클라이언트의 캐릭터가 상자를 먹는걸 체크한다.

이때, 서버만 이를 연산하기 때문에 적용되는건 서버만이다.

즉, 체력이 증가하는 아이템을 먹으면 클라이언트는 변함없고, 서버에서 보는 클라이언트만 체력이 증가할 것이다.

 

변경되는 스탯을 다른 클라이언트에게 보내기 위해 ABCharacterStatComponent.h를 수정한다.

protected:

	UPROPERTY(ReplicatedUsing = OnRep_CurrentHp, Transient, VisibleInstanceOnly, Category = Stat)
	float CurrentHp;

	UPROPERTY(ReplicatedUsing = OnRep_MaxHp, Transient, VisibleInstanceOnly, Category = Stat)
	float MaxHp;

	...
    
	UPROPERTY(Transient, ReplicatedUsing = OnRep_BaseStat, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
	FABCharacterStat BaseStat;

	UPROPERTY(Transient, ReplicatedUsing = OnRep_ModifierStat, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
	FABCharacterStat ModifierStat;

이렇게 스탯이 변하면 CallBack을 받도록 정의한다.

이후 로직을 GetLifetimeReplicatedProps에 등록한다.

void UABCharacterStatComponent::OnRep_CurrentHp()
{
	AB_SUBLOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
	OnHpChanged.Broadcast(CurrentHp, MaxHp);
	if (CurrentHp <= KINDA_SMALL_NUMBER)
	{
		OnHpZero.Broadcast();
	}
}

void UABCharacterStatComponent::OnRep_MaxHp()
{
	AB_SUBLOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
	OnHpChanged.Broadcast(CurrentHp, MaxHp);
}

void UABCharacterStatComponent::OnRep_BaseStat()
{
	AB_SUBLOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
	OnStatChanged.Broadcast(BaseStat, ModifierStat);
}

void UABCharacterStatComponent::OnRep_ModifierStat()
{
	AB_SUBLOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
	OnStatChanged.Broadcast(BaseStat, ModifierStat);
}

변경 사항을 서버에서 받아 브로드캐스트를 진행한다.

이렇게 하면 변경되는 것을 확인할 수 있다.

 

이러한 식의 데이터를 몰라도 상관없는 제2의 클라이언트에겐 데이터 낭비일 수 있다.

그러므로 이런 OnRep의 메소드를 소유한 캐릭터에게만 전송하도록 로직을 수정한다.

 

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

	DOREPLIFETIME(UABCharacterStatComponent, CurrentHp);
	DOREPLIFETIME(UABCharacterStatComponent, MaxHp);
	DOREPLIFETIME_CONDITION(UABCharacterStatComponent, BaseStat, COND_OwnerOnly);
	DOREPLIFETIME_CONDITION(UABCharacterStatComponent, ModifierStat, COND_OwnerOnly);
}

DOREPLIFETIME 말고 ,조건을 달수 있는데, 마지막 조건으로 COND_OwnerOnly로 소유자만 해당 콜백을 받게 된다.

 

새로 생긴 MaxHP를 모든 곳에서 수정해준다.

 

네트워크 멀티플레이어를 위해 고려할 최적화 요소

[일반적으로 최적화시 고려할 사항]

- 불필요한 액터의 리플리케이션 옵션을 끄기

- 액터에 맞는 최적의 업데이트 빈도(Frequency)와 우선권(Priority) 설정

- 연관성(Relevancy)을 위한 최적의 조건 설정

- 프로퍼티 리플리케이션의 조건 설정

- 필요시 휴면(Dormancy)상태의 설정

- 데이터 크기의 양자화(FVector_NetQuantize)

 

[기타 심화주제]

- 구조체 데이터 전송의 최적화 설계(NetSerialize)

- 프로퍼티 리플리케이션의 푸시 모델 설정

- 필요시 빠른 배열 자료구조를 사용한 데이터 전달(FFastArraySerializer)

- 빠른 리플리케이션 엔진의 교체(ReplicationGraph , lris)

 

NetSerialize 데이터 설계

네트워크로 전송할 구조체 데이터를 직접 설계해 보내고 싶은 경우에 유용

- 데이터 양을 최소화 할 수 있음.

- 데이터가 자주 바뀔 때 유용하게 활용.

- 플래그를 설정해 불필요한 데이터 전송을 건너뛸 수 있음.

- 정수 데이터로 변환해 크기를 줄일 수 있음.

 

 

Max HP를 먹었을때의 데이터 전송량을 확인해 본다.

MaxHp가 변경되었기 때문에 MaxHP 가 전송되었고, 변경된 Stat에 관해 전송되었다.

확인해보면 AttackRange는 0이기 때문에 언리얼 내부에서 자체적으로 최적화해서 데이터를 전송하지 않은것을 확인할 수 있다.

 

NetSerialize 기능을 활용해서 해당 구조체의 내용을 압축해서 전송해본다.

해당 전송할 데이터는 FTableRowBase를 상속받아 정의된 FABChracaterStat이다.

	bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
	{
		uint32 uMaxHp = (uint32)MaxHp;
		Ar.SerializeIntPacked(uMaxHp);
		MaxHp = (float)uMaxHp;

		uint32 uAttack = (uint32)Attack;
		Ar.SerializeIntPacked(uAttack);
		Attack = (float)uAttack;

		uint32 uAttackRange = (uint32)AttackRange;
		Ar.SerializeIntPacked(uAttackRange);
		AttackRange = (float)uAttackRange;

		uint32 uAttackSpeed = (uint32)AttackSpeed;
		Ar.SerializeIntPacked(uAttackSpeed);
		AttackSpeed = (float)uAttackSpeed;

		uint32 uMovementSpeed = (uint32)MovementSpeed;
		Ar.SerializeIntPacked(uMovementSpeed);
		MovementSpeed = (float)uMovementSpeed;

		return true;
	}

메소드의 이름을 그대로 정의하면 된다.

NetSerialize의 메소드는 독특한데, 처음 2줄(uint32 ... ) 는 데이터를 저장할때 사용되고, 저장하는데에 있어서 3번째 줄은 사용되지 않는다.

같은 방식으로 해당 메소드를 통해 데이터를 로드하는데, 이때 처음줄은 로드하는데 사용되않고, 다음 2줄이 로드하는데에 사용이 되도록 메소드가 구성된다.

 

template<>
struct TStructOpsTypeTraits<FABCharacterStat> : public TStructOpsTypeTraitsBase2<FABCharacterStat>
{
	enum
	{
		WithNetSerializer = true
	};
};

클래스 외부에 이러한 구조체를 선언하면 NetSerialize함수를 호출한다.

이렇게 해야지 압축해서 서버로 전송하게 된다.

 

처음과 동일하게 같은 아이템을 먹었을때의 결과이다.

처음처럼 MaxHp가 전송되는것은 동일하지만 데이터를 저렇게 64비트로 압축해서 하나의 덩어리로 전송한 것을 볼 수 있다.

물론 이러한 경우엔 사용하지 않는 데이터도 전송하기 때문에 경우에 따라서 언리얼 최적화가 더 좋을 수 도 있다.

 

 

게임 모드의 특징과 관리하는 정보

오직 서버에만 존재한다. (심판같은 역할)

게임 모드가 관리하는 정보

- 현재 게임에 참여하고 있는 플레이어의 수

- 플레이어가 게임에 입장하는 방법과 스폰, 리스폰 규칙

- 게임에 관련된 중요한 처리 : 대미지 처리, 스코어에 관련된 로직 처리 등

 

게임 스테이트의 특징과 관리하는 정보

서버와 모든 클라이언트에 존재한다.

클라이언트는 게임 스테이트를 사용해 현재 게임의 상태를 파악할 수 있다.

팀전에서의 팀 스코어를 관리하는데 유용하다.

 

게임 스테이트가 관리하는 정보

- 현재 월드 시간

- 플레이어 스테이트의 배열

 

플레이어 스테이트가 관리하는 정보

- 스코어

- 플레이어 이름

- 플레이어 ID

 

 

CharacterPlayer의 Attack 메소드를 수정한다.

void AABCharacterPlayer::Attack()
{
	...
			GetWorldTimerManager().SetTimer(AttackTimerHandle, this, &AABCharacterPlayer::ResetAttack, AttackTime, false);
	...
}

 

공격에서 공격이 종료되면 ResetAttack 메소드를 호출하도록 타이머를 수정한다.

void AABCharacterPlayer::ResetAttack()
{
	bCanAttack = true;
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}

리셋은 원래 있던 내용을 옮겨놓은 것으로 구현했다.

 

void AABCharacterPlayer::ServerRPCAttack_Implementation(float AttackStartTime)
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	bCanAttack = false;
	OnRep_CanAttack();

	AttackTimeDifference = GetWorld()->GetTimeSeconds() - AttackStartTime;
	AB_LOG(LogABNetwork, Log, TEXT("LagTime : %f"), AttackTimeDifference);
	AttackTimeDifference = FMath::Clamp(AttackTimeDifference, 0.0f, AttackTime - 0.01f);

	GetWorldTimerManager().SetTimer(AttackTimerHandle, this, &AABCharacterPlayer::ResetAttack, AttackTime - AttackTimeDifference, false);

	LastAttackStartTime = AttackStartTime;
	PlayAttackAnimation();

	//MulticastRPCAttack();
	for (APlayerController* PlayerController : TActorRange<APlayerController>(GetWorld()))
	{
		if (PlayerController && GetController() != PlayerController)
		{
			if(!PlayerController->IsLocalController())
			{
				AABCharacterPlayer* OtherPlayer = Cast<AABCharacterPlayer>(PlayerController->GetPawn());
				if (OtherPlayer)
				{
					OtherPlayer->ClientRPCPlayAnimation(this);
				}
			}
		}
	}
}

서버가 받은 공격 메소드도 마찬가지 이다. 공격이 서버 딜레이 시간을 제외하고 시간이 되면 ResetAttack을 호출하도록 한다.

 

public:
	virtual FTransform GetRandomStartTransform() const = 0;
	virtual void OnPlayerKilled(AController* Killer, AController* KilledPlayer, APawn* KilledPawn) = 0;
};

위 코드는 정의했던 GameInterface를 수정한 내용이다.

스폰할때 랜덤한 위치에 스폰할 수 있도록 메소드를 제작했다.

누가 어떻게 죽었는지 표시하기 위해서 제작되었다.

 

float AABCharacterPlayer::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	const float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	if (Stat->GetCurrentHp() <= 0.0f)
	{
		IABGameInterface* ABGameMode = GetWorld()->GetAuthGameMode<IABGameInterface>();
		if (ABGameMode)
		{
			ABGameMode->OnPlayerKilled(EventInstigator, GetController(), this);
		}
	}

	return ActualDamage;
}

서버에서 현재 내가 누구에게 데미지를 받았는지 추가로 보기 위해 기능을 확장해 본다

 

GameMode.h

...

	virtual FTransform GetRandomStartTransform() const;
	virtual void OnPlayerKilled(AController* Killer, AController* KilledPlayer, APawn* KilledPawn);

	virtual void StartPlay() override;

protected:
	virtual void PostInitializeComponents() override;
	virtual void DefaultGameTimer();
	void FinishMatch();

	FTimerHandle GameTimerHandle;

protected:
	TArray<TObjectPtr<class APlayerStart>> PlayerStartArray;
};

인터페이스를 수정했으니 정의해본다.

 

FTransform AABGameMode::GetRandomStartTransform() const
{
	if (PlayerStartArray.Num() == 0)
	{
		return FTransform(FVector(0.0f, 0.0f, 230.0f));
	}

	int32 RandIndex = FMath::RandRange(0, PlayerStartArray.Num() - 1);
	return PlayerStartArray[RandIndex]->GetActorTransform();
}

void AABGameMode::OnPlayerKilled(AController* Killer, AController* KilledPlayer, APawn* KilledPawn)
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	APlayerState* KillerPlayerState = Killer->PlayerState;
	if (KillerPlayerState)
	{
		KillerPlayerState->SetScore(KillerPlayerState->GetScore() + 1);

		if (KillerPlayerState->GetScore() > 2)
		{
			FinishMatch();
		}
	}
}

가지고 있는 Start 포인트중 랜덤한 곳에 생성된다.

누군가를 죽였을때 스코어와 같이 로그가 뜬다.

점수를 3점내면 게임이 종료된다.

 

void AABGameMode::StartPlay()
{
	Super::StartPlay();

	for (APlayerStart* PlayerStart : TActorRange<APlayerStart>(GetWorld()))
	{
		PlayerStartArray.Add(PlayerStart);
	}
}

월드에 잇는 PlayerStart를 모두 가져온다.

 

다시 CharacterPlayer로 돌아간다.

죽었을 경우를 고려해 플레이어를 초기화 한다.

void AABCharacterPlayer::ResetPlayer()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance)
	{
		AnimInstance->StopAllMontages(0.0f);
	}

	Stat->SetLevelStat(1);
	Stat->ResetStat();
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
	SetActorEnableCollision(true);
	HpBar->SetHiddenInGame(false);

	if (HasAuthority())
	{
		IABGameInterface* ABGameMode = GetWorld()->GetAuthGameMode<IABGameInterface>();
		if (ABGameMode)
		{
			FTransform NewTransform = ABGameMode->GetRandomStartTransform();
			TeleportTo(NewTransform.GetLocation(), NewTransform.GetRotation().Rotator());
		}
	}
}

리셋을 호출하면 애니메이션을 초기화하고, 현재 상태를 모두 초기화 한다.

서버라면 캐릭터를 랜덤한 위치로 가져간다.

레벨은 1로 초기화 된다.

 

CharacterStatComponent의 메소드 이다.

자신의 스텟 정보를 불러오고, 이를 맞춰 체력이 전부 찬다.

void UABCharacterStatComponent::ResetStat()
{
	SetLevelStat(CurrentLevel);
	MaxHp = BaseStat.MaxHp;
	SetHp(MaxHp);
}

 

초기화를 진행할때 스탯의 값도 초기화 한다.

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

	SetLevelStat(CurrentLevel);
	ResetStat();

	OnStatChanged.AddUObject(this, &UABCharacterStatComponent::SetNewMaxHp);

	SetIsReplicated(true);
}

 

CharacterPlayer.cpp에서 죽었을때 입력을 멈추는 부분을 주석처리한다.

다시 라운드를 시작하기 때문이다.

void AABCharacterPlayer::SetDead()
{
	Super::SetDead();

	GetWorldTimerManager().SetTimer(DeadTimerHandle, this, &AABCharacterPlayer::ResetPlayer, 5.0f, false);

	//APlayerController* PlayerController = Cast<APlayerController>(GetController());
	//if (PlayerController)
	//{
	//	DisableInput(PlayerController);
	//}
}

타이머를 실행하고 끝나면 다시 플레이어를 리셋하도록 로직을 변경한다.

 

CharacterBase에서 메쉬를 로드하는 로직을 수정한다.

void AABCharacterBase::MeshLoadCompleted()
{
	if (MeshHandle.IsValid())
	{
		USkeletalMesh* NPCMesh = Cast<USkeletalMesh>(MeshHandle->GetLoadedAsset());
		if (NPCMesh)
		{
			GetMesh()->SetSkeletalMesh(NPCMesh);
			GetMesh()->SetHiddenInGame(false);
		}
	}

	MeshHandle->ReleaseHandle();
}

 

기존에 있던 NPC Load 메소드를 수정한 것이다.

 

캐릭터마다 메쉬를 다르게 할 것이다.

Config 파일에 들어가서 메쉬 경로를 지정해준다.

UCLASS(config = ArenaBattle)
[/Script/ArenaBattle.ABCharacterNonPlayer]
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Barbarous.SK_CharM_Barbarous 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/sk_CharM_Base.sk_CharM_Base 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Bladed.SK_CharM_Bladed 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Cardboard.SK_CharM_Cardboard 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Forge.SK_CharM_Forge 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_FrostGiant.SK_CharM_FrostGiant 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Golden.SK_CharM_Golden 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Natural.SK_CharM_Natural 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Pit.SK_CharM_Pit 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Ragged0.SK_CharM_Ragged0 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_RaggedElite.SK_CharM_RaggedElite 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Ram.SK_CharM_Ram 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Robo.SK_CharM_Robo 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Shell.SK_CharM_Shell 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_solid.SK_CharM_solid 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Standard.SK_CharM_Standard 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Tusk.SK_CharM_Tusk 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Warrior.SK_CharM_Warrior

[/Script/ArenaBattle.ABCharacterPlayer]
+PlayerMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Barbarous.SK_CharM_Barbarous 
+PlayerMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Bladed.SK_CharM_Bladed 
+PlayerMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Cardboard.SK_CharM_Cardboard 
+PlayerMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Forge.SK_CharM_Forge 
+PlayerMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_FrostGiant.SK_CharM_FrostGiant 
+PlayerMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Golden.SK_CharM_Golden

 

랜덤하게 하나 가져올 것이다.

기존에 있던 Mesh를 가져오는 부분은 제거한다.

void AABCharacterPlayer::UpdateMeshFromPlayerState()
{
	int32 MeshIndex = FMath::Clamp(GetPlayerState()->PlayerId % PlayerMeshes.Num(), 0, PlayerMeshes.Num() - 1);
	MeshHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(PlayerMeshes[MeshIndex], FStreamableDelegate::CreateUObject(this, &AABCharacterBase::MeshLoadCompleted));

}

 

이제 이 메소드를 호출해줘야 하는 타이밍을 찾아야한다.

OnRep_PlayerState는 서버에선 호출하지 않는다.

위 메소드는 PlayerState가 생성이 완료되었을때 클라이언트에서 호출하는 메소드 이다.

 

그리고 PossessedBy는 서버에서만 호출한다.

서버가 클라이언트 Controller를 제작하고 빙의할때 호출하는 메소드이다.

void AABCharacterPlayer::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();

	UpdateMeshFromPlayerState();
}
void AABCharacterPlayer::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

	UpdateMeshFromPlayerState();
}

 

PlayerState 를 보면 이미 여기엔 서버와 통신이 가능한 다양한 변수가 포함되어 있다

(Life, Ping 등등)

서버 통신으로 Replication으로 이미 되어있기 때문에 확장해서 사용할 수 있다.

단, 누군가가 들어오거나, 다른 레벨로 이동하게 되면 PlayerState가 계속 바뀌기 때문에 고유한 데이터로 사용하면 안된다.

 

 

게임 방 찾기 기능은 GameMode와 GameState를 통해 제작할 수 있다.

UCLASS()
class ARENABATTLE_API AABGameState : public AGameState
{
	GENERATED_BODY()
	
public:
	AABGameState();

	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const;


	//virtual void HandleBeginPlay() override;
	//virtual void OnRep_ReplicatedHasBegunPlay() override;

	UPROPERTY(Transient, Replicated)
	int32 RemainingTime;

	int32 MatchPlayTime = 2000;
	int32 ShowResultWaitingTime = 5;
};
AABGameState::AABGameState()
{
	RemainingTime = MatchPlayTime;
}

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

	DOREPLIFETIME(AABGameState, RemainingTime);
}

매칭 시간을 두고

 

게임모드에서 타이머를 가동할 것이다.

protected:
	virtual void PostInitializeComponents() override;
	virtual void DefaultGameTimer();
	void FinishMatch();

	FTimerHandle GameTimerHandle;
void AABGameMode::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	GetWorldTimerManager().SetTimer(GameTimerHandle, this, &AABGameMode::DefaultGameTimer, GetWorldSettings()->GetEffectiveTimeDilation(), true);
}

void AABGameMode::DefaultGameTimer()
{
	AABGameState* const ABGameState = Cast<AABGameState>(GameState);

	if (ABGameState && ABGameState->RemainingTime > 0)
	{
		ABGameState->RemainingTime--;
		AB_LOG(LogABNetwork, Log, TEXT("Remaining Time : %d"), ABGameState->RemainingTime);
		if (ABGameState->RemainingTime <= 0)
		{
			if (GetMatchState() == MatchState::InProgress)
			{
				FinishMatch();
			}
			else if (GetMatchState() == MatchState::WaitingPostMatch)
			{
				GetWorld()->ServerTravel(TEXT("/Game/ArenaBattle/Maps/Part3Step2?listen"));
			}
		}
	}

}

void AABGameMode::FinishMatch()
{
	AABGameState* const ABGameState = Cast<AABGameState>(GameState);
	if (ABGameState && IsMatchInProgress())
	{
		EndMatch();
		ABGameState->RemainingTime = ABGameState->ShowResultWaitingTime;
	}
}

PostInit에서 타이머를 실행한다.

매 시간마다 DefaultGameTimer가 실행된다.

 

매치가 실행중이라면 매치를 종료한다.

매치가 끝났다면 다른 레벨로 이동을 한다.

ServerTravel은 서버에서 호출하면 클라이언트들과의 접속을 유지한 채로 레벨을 이동하는 것이다.

 

게임 스테이트를 따로 수정하지 않았기 때문에 기본으로 InProgress상태일 것이다.

이후, FinishMatch()에서 EndMatch()를 호출하면 스테이트가 WaitingPostMatch로 바뀔 것이다.

 

 

게임 패키징

강의에 없어서 찾아서 제작해 보았다.

(ref : https://www.youtube.com/watch?v=wvbFdSIPCHA)

 

영상에서 버튼을 여러개 만들었고, 나는 IP 만 필요했기 때문에 IP 주소로 접속하는 방법만 구현했다.

ThirdPersonExampleMap 이라는 맵을 제작했다.

이후, Host Session이란 메소드를 제작했다.

서버는 ThridPersonExampleMap 을 Listen 서버형식으로 열게 될 것이다.

 

Join By IP Address 메소드를 제작했다.

이는 클라이언트가 이용하게 될 것이다.

IP 주소를 통해 Open을 하게 되면 해당 IP의 서버에 접속을 하게 되는 방식이다.

127.0.0.1로 내 IP에 접속했지만, 다른 유저의 IP를 통해 접속도 가능하다 (단, 이때 해당 IP로 서버가 켜져있어야 함)

 

버튼을 2개 만든 위젯이다.

버튼은 2개로, 하나는 호스트, 하나는 클라이언트로 이동될 것이다.

 

게임이 최초 실행되는 레벨의 블루프린트이다.

해당 레벨은 디폴트 맵으로 Escape나 M을 누르면 위젯이 생성고, Input GameMode가 변경된다.

이후 호스트가 서버를 열면 클라이언트가 진입할 수 있다.

 

이제 해당 프로젝트를 패키징할것이다.

최초시작 맵과 이동후 맵을 패키징에 포함한다.

빌드 타겟으론 Shipping을 고른다

 

이후 빌드를 진행하고 실행해본다.

(해당 프로젝트가 백그라운드에서 돌때 버벅임이 있다. 실제로 다른 컴퓨터에선 문제 없다.)

반응형