언리얼엔진 학습 및 과제

액터 리플리케이션을 이용한 게임 제작 (9)

park94128 2026. 1. 6. 22:00

● 경찰과 도둑다운 모습을 만들기 위한 밑준비

A. 내가 만들고자 하는 게임의 룰

- 1명은 경찰이고 나머지는 도둑이다.

- 경찰은 공격할 수 있지만 도둑은 공격할 수 없다.

- 도둑은 경찰의 공격을 받지 않고 맵 내의 모든 아이템을 얻으면 승리한다.

- 경찰은 도둑이 모든 아이템을 얻기 전에 전부 쓰러뜨리면 승리한다.

 

B. 경찰, 도둑 구분 방법

플레이어 스테이트를 생성한다.

protected:
	virtual void BeginPlay() override;

public:
	virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;

	//경찰 여부
	UPROPERTY(Replicated)
	bool bPolice;

경찰, 도둑 구분을 위한 bool 변수를 선언하되 Replicated 키워드를 사용한다. 리플리케이션을 해야 하니 GetLifetimeReplicatedProps함수가 필요하다.

 

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

	bPolice = false;
}

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

	DOREPLIFETIME(ThisClass, bPolice);
}

프로퍼티 리플리케이션에 대한 기본적인 초기화는 이젠 설명을 안해도 되겠지

 

public:
	//플레이어 스테이트
	FORCEINLINE AMainGamePlayerState* GetMainGamePlayerState() const { return MainGamePlayerState; }

private:
	//플레이어 스테이트
	UPROPERTY()
	AMainGamePlayerState* MainGamePlayerState;

플레이어 캐릭터에서는 생성한 클래스인 플레이어 스테이트를 저장할 변수 및 외부에서 쉽게 호출하기 위한 함수를 선언한다.

 

void AMainGameCharacter::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	MainCharAnimation = Cast<UMainCharAnimation>(GetMesh()->GetAnimInstance());
	if (!IsValid(MainCharAnimation))
	{
		UE_LOG(LogHiddenThief, Error, TEXT("'%s' : 애님 인스턴스를 찾을 수 없습니다."), *GetNameSafe(this));
	}

	MainGamePlayerState = Cast<AMainGamePlayerState>(GetPlayerState());
	if (!IsValid(MainGamePlayerState))
	{
		UE_LOG(LogHiddenThief, Error, TEXT("'%s' : 플레이어 스테이트를 찾을 수 없습니다."), *GetNameSafe(this));
	}
}

PostInitializeComponents함수에서 플레이어 스테이트를 가져온다.

 

월드 세팅에서 플레이어 스테이트를 지정하는 것을 잊지 말자.

 

이제 지정한 플레이어가 경찰인지, 혹은 도둑인지 구분하려면 게임 스테이트에 있는 이 bool값을 확인하면 된다.

 

C. 문제 발생

그런데 여기까지 해보고 실행해보니 플레이어 스테이트를 찾을 수 없다고 한다.

 

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

	CurrentAttackDelay = 0.0f;
	bIsDeath = false;

	MainGamePlayerState = Cast<AMainGamePlayerState>(GetPlayerState());
	if (!IsValid(MainGamePlayerState))
	{
		UE_LOG(LogHiddenThief, Error, TEXT("'%s' : 플레이어 스테이트를 찾을 수 없습니다."), *GetNameSafe(this));
	}
}

원인을 확인해보니 PostInitializeComponents함수를 실행한 시점에서는 아직 캐릭터에 플레이어 스테이트를 붙이지 않았기 때문이다. 그래서 BeginPlay로 옮겨보니 정상적으로 잘 작동한다.

 

● 공격은 경찰만의 권리

void AMainGameCharacter::Attack()
{
	//사망하거나 이미 공격 상태라면 공격 불가
	if (bIsDeath || CurrentAttackDelay > 0)
	{
		return;
	}

	//공격은 경찰만 가능
	if (!MainGamePlayerState->bPolice)
	{
		return;
	}

	CurrentAttackDelay = AttackDelayTime;

	//서버에 공격 요청하기
	AttackOrder();
}

공격시 실행하는 함수중 클라이언트가 실행하는 구간에 게임 스테이트를 통한 경찰 구분을 이용한다. 이때 경찰이 아닐 경우 서버에 공격 요청을 못보내게 해야 한다.

 

void AMainGameCharacter::AttackOrder_Implementation()
{
	//공격은 경찰만 가능
	if (!MainGamePlayerState->bPolice)
	{
		return;
	}
	
	//공격 실행
	AttackActionByServer();

	//공격 모션
	AttackMotion();
}

그리고 서버가 실행하는 구간에서도 클라이언트가 잘못 보냈을 수도 있으니 한 번 더 검사한다.

 

저 bool값은 false를 기본값으로 했으니 당분간은 공격 버튼을 눌러도 공격 모션이 실행되지 않을 것이다.

 

● 서버에서 경찰 정하기

제일 처음 게임 모드 코드를 복사하기만 하고 블루 프린트에서 객체 지정만 했는데 이제 게임 모드에서 서버라는 권한을 보여줄 차례다.

//게임 진행 상태
enum class EGameState
{
    NotPlaying,
    ReadyToPlay,
    Playing,
    EndOfGame
};

일단 enum을 선언한다. 이 enum은 게임 모드 안에서만 사용할 것이니 클래스 안쪽에 선언했다.

 

public:
	AMainGameMode();

	virtual void OnPostLogin(AController* NewPlayer) override;

protected:
	virtual void BeginPlay() override;

생성자, 그리고 여기서 사용할 언리얼 엔진 함수의 재정의

 

private:
	//게임 시작
	void GameStart();
	
	//게임 준비를 위한 타이머 핸들
	FTimerHandle ReadyToPlayTimer;
	
	//현재 게임 진행 상태
	EGameState CurrentGameState;

	//게임 준비 시간(초)
	UPROPERTY(EditDefaultsOnly, Category = "Game Rule", meta = (AllowPrivateAccess = true))
	float ReadyTime;
	
	//현재 존재하는 플레이어 수
	int32 PlayerCount;

	//남은 도둑 수
	int32 ThiefCount;

추가로 선언한 게임 시작 함수, 그리고 게임 준비를 위한 변수 여러 개를 선언했다.

 

void AMainGameMode::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);

	//플레이어 수 증가, 2명 이상이면 게임 시작 준비
	if (++PlayerCount >= 2 && CurrentGameState == EGameState::NotPlaying)
	{
		CurrentGameState = EGameState::ReadyToPlay;
		GetWorld()->GetTimerManager().SetTimer(ReadyToPlayTimer, this, &AMainGameMode::GameStart, ReadyTime, false);
	}
}

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

	CurrentGameState = EGameState::NotPlaying;
	PlayerCount = 0;
}

OnPostLogin을 통한 플레이어 입장 감지와 게임 시작 준비를 구현했다. BeginPlay는 기본적인 초기화를 담당한다.

 

플레이어 입장을 감지하여, 2명이 입장하면 잠시 후 게임을 시작하게 만들었다.

 

void AMainGameMode::GameStart()
{
	//경찰로 지목될 플레이어 순번
	int32 PoliceIndex = FMath::RandRange(1, PlayerCount);

	//경찰 1명, 나머지 도둑
	int32 PlayerIndex = 1;
	for (TActorIterator<AMainGameCharacter> Iter(GetWorld()); Iter; ++Iter)
	{
		if (const AMainGameCharacter* Player = *Iter)
		{
			Player->GetMainGamePlayerState()->bPolice = PoliceIndex == PlayerIndex;
		}

		++PlayerIndex;
	}

	//게임 시작
	CurrentGameState = EGameState::Playing;
	ThiefCount = PlayerCount - 1;
}

준비 시간이 지나면 맵 내의 모든 플레이어 캐릭터를 가져와서 그 중 1명을 경찰로 지정한다.

 

● 2명이 접속했는데 게임 시작을 안한다.

여기서 문제가 발생했다. 분명 2명이 접속했는데 아무리 기다려도 게임 시작을 안한다. 심지어는 나중에 접속한 플레이어에게 플레이어 스테이트를 찾을 수 없는 문제가 또 생겼다.

 

void AMainGameMode::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);

	//플레이어 수 증가, 2명 이상이면 게임 시작 준비
	if (++PlayerCount >= 2 && CurrentGameState == EGameState::NotPlaying)
	{
		UE_LOG(LogTemp, Warning, TEXT("게임 준비"));
		CurrentGameState = EGameState::ReadyToPlay;
		GetWorld()->GetTimerManager().SetTimer(ReadyToPlayTimer, this, &AMainGameMode::GameStart, ReadyTime, false);
	}
	UE_LOG(LogTemp, Warning, TEXT("접속 인원 : %d"), PlayerCount);
}

뭔가 이상하다는 생각에 게임 모드에 로그를 심어봤다.

 

그리고 다시 해봤는데 접속 인원이 여전히 1명으로 뜬다. 왜 그런가 싶더네, 넷 모드를 'Play As Listen Server'로 실행한 것이 문제였다. 서로 연결은 되지만 각자 자신이 서버라고 생각하고 각자 게임 모드를 가지고 있었던 것이었다.

 

결국 처음에 언급한 내용중, 서버를 따로 실행하도록 하고 넷 모드를 'Play As Server'로 하니 계속 안된다고 했던 것을 해결하는 상황이 됐다.

 

● 왜 데디케이티드 서버가 안되나 했는데

문제가 처음 발생했을 당시 내가 조금이라도 많이 정보 검색을 했어야 했는데 그러지 않았다. 제대로 조사했다면 진작에 고쳤을 지도 모른다. 원인은 알고나니 간단한 문제였다.

프로젝트 세팅에서 맵을 지정할 때 바로 아래쪽에 고급을 눌러보면 서버 기본 맵이라는 것이 있다. 이것이 지정되지 않았으니 생긴 문제였다. 앞서 온라인 채팅, 야구 게임을 만들었을 때는 맵에 배치한 액터가 따로 없었기 때문에 에러가 생기지 않았던 것인데 지금은 단순 맵 구조물로 배치한 액터만 해도 여러개 된다. 여기에 캐릭터마저 사용해야 하니 맵 정보가 아예 없는 서버 입장에서는 그냥 나몰라라 하고 에러를 일으킨 것이었다.

 

서버 기본 맵을 지정해주니 서버 에러가 발생하지 않았다. 그런데 다른 문제가 생겼는데...

 

서버에서 또다시 플레이어 스테이트 문제가 발생했으며

AI 캐릭터 배치가 맛이 갔다. 처음에 발생한 문제를 방치한게 후회되는 순간이었다.