no image
[ UE ] 언리얼의 멀티네트워크
언리얼의 네크워크에 대해 공부를 하다가 내가 이해하기 쉽게 정리해보았다. 멀티플레이 흐름1. 멀티플레이 게임을 실행하면 언리얼 복제 시스템(Replication)이 모든 GameInstance가 동기화되도록 백드라운드에서 실행2. 공유하는 세계를 가져옴(각 PC에서 자체적으로 GetWorld() 함)+서버와 Client는 각각 개별의 UGameEngine을 가짐3. 서버를 부팅하면 NetDriver가 생성되고 원격 프로세스(서버)의 메세지를 수신4. 클라이언트를 부팅하면 클라이언트만의 NetDriver가 생성되고, 서버에 연결 요청을 보냄5. 서버와 클라이언트의 NetDriver가 접촉하면 각 NetDriver 내에 NetConnection이 설정됨+서버에는 연결된 플레이어 각각의 NetConnectio..
2025.03.27
[ UE ] 격투 게임 기획 및 설계
오늘은 앞으로 만들 격투 게임에 대한 기획과 설계를 간단하게 진행해보았다.  인게임 시스템 기능 설계 GameMode(게임 규칙과 전투 흐름 관리할 예정)- 매치 관리 (어떻게 매치할건지 ← 규태님이랑 이야기해보기)- 플레이어 스폰(맵에 움직일 수 있는 랜덤한 위치에 생성할 예정- 게임 시작/종료(하트 3개에 플레이어 1명 남을 때까지, 시간 타이머)- 플레이어 상태 초기화(체력, 스킬 쿨타임 등)- 이벤트 트리거(라운드시작, 종료, 플레이어 사망, 리스폰 등)GameState- GameMode에서 정의한 시작/종료 조건 업데이트- 각 라운드별 결과 및 이벤트 저장- HUD에 필요한 데이터 업데이트(체력, 콤보, 스킬 쿨타임, 남은 시간 등)- GameMode에서 이벤트 트리거 받아와서 UI에 전달- 플..
2025.03.24
no image
[UE, C++] 트러블슈팅, 멀티환경에서 카운트 개별 처리
트러블 슈팅상황호스트와 게스트의 플레이어 입력 카운트가 공유되는 버그 발생 해결GameMode에서 PlayerInputCount를 관리했는데 공부하다보니, 게임모드는 서버만 가지고 있는 인스턴스라 문제가 있었다. 플레이어 컨트롤러에서 카운트를 관리하는 방식으로 변경해주었다.
2025.03.21
no image
[ UE, C++ ] Listen Server를 활용한 숫자 야구 게임 만들기 (1)
오늘은 언리얼의 멀티 네트워크를 깊게 이해해보고자 숫자 야구 게임을 만드는 과제를 진행했다.이렇게 채팅을 "/123" 이런 느낌으로 치면 서버가 생성한 난수에 대한 야구 게임을 시작하는거다. 일단 작업 플로우는 다음과 같이 설계했다.1. 난수 생성 클래스 구현- 여기서는 몇개의 난수를 생성할건지만 매개변수로 받아서 동작하게 설계했다(나중에 난수를 또 만들게 되었을 때 재사용할 수 있으면 좋겠어서)2. 판정 클래스 구현- 플레이어가 입력한 Input에 대해 몇 개의 수를 맞췄는지 알려주는 부분을 구현했다.3. GameMode 클래스 구현- 게임모드는 난수생성클래스와 판정클래스를 가져와서 이 데이터들을 저장한다.- 플레이어의 Input에 대한 예외처리(숫자갯수, 문자가 들어갔는지, 0이 포함되어있는지, 공백..
2025.03.19
[ UE ] 데디케이트 서버 환경 만들기
오늘은 언리얼로 데디케이트 서버를 구축하려고하는데 과정이 너무 어렵게 느껴졌고.. 빌드에러가 너무 많이 발생했다.빌드하는데 걸리는 시간이 길다보니 너무 많은 시간을 잡아먹어 정리를 해보려고 한다.  트러블슈팅 및 데디케이트 서버 환경 구축하기Switch Unreal Engine Version에서 Source로 빌드 선택서버로 된 타겟 파일을 생성깃에서 받은 언리얼 엔진 폴더로 가서 GenerateProjectFiles.bat로 솔루션 업데이트해줌(그래야 우리가 소스 빌드로 변경한 솔루션이 갱신됨) + 이미 git 과 빌드를 완료했다면 Setup.bat 같은 부분은 안해도 됨Development Editor 빌드Development Server 빌드오류 발생 : Serialization Error : Ac..
2025.03.17
no image
[ UE ] 언리얼 멀티플레이어에서 NetMode가 뭘까?
오늘은 언리얼로 멀티플레이를 실습하다가 NetMode가 뭔지 궁금해졌다.  NetMode해당 게임 프로그램이 네트워크 상에서 어떤 역할을 수행하고 있는지를 의미싱글(NM_StandAlone), 서버(NM_ListenServer, NM_DedicatedServer), 클라이언트(NM_Client)NetMode의 필요성멀티플레이 개발할 때, 같은 코드가 여러 pc에서 동작함. 그래서 디버깅할 때, 어떤 pc에서 동작하고 있는지 살펴보는 것이 가장 중요 그래서 이 때 NetMode를 활용하면 데디서버에서 실행 중인지, 클라이언트에서 실행 중인지 분간이 가능함.NetConnection다른 pc와 연결이 발생할 때, 생성됨NetDriver언리얼 네트워크 통신에서 로우레벨 동작들을 관리하는 클래스싱글플레이에서는 U..
2025.03.14
[ C++ ] 프로그래머스 체육복
문제 링크https://school.programmers.co.kr/learn/courses/30/lessons/42862 프로그래머스SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프programmers.co.kr  문제풀이체육복의 갯수를 단서로 잡고 풀이했다.도난당한 학생의 체육복 갯수는 -1, 일반 학생의 갯수는 0, 체육복 여벌이 있는 학생은 1로 잡았다. 1. 학생 리스트를 만들어서 현재 학생들의 체육복 갯수 상태를 채워주었다.2. 현재 인덱스의 값이 -1이라면, 앞 뒤를 체크하여 갯수를 조절해주었다.   내 코드#include #include #include using namespace std;int student[30];int solu..
2025.03.13
[ UE ] FIntPoint가 뭘까?
오늘은 구조체로 코딩을 하다가 FIntPoint라는 자료형?에 대해 접하게 되었다.뭔지 궁금해서 언리얼에서 사용하는 다른 자료형들과 함께 정리를 해보았다. TArraystd::vector와 유사하게 동작하지만 AdddUnique, RemoveAtSwap 등의 부가적인 API를 제공한다고 함.컨테이너의 element들이 가비지 컬렝팅됨. -> UObject에서 파생된 포인터들을 모두 담을 수 있음  TMapstd::map과 유사한 언리얼에서 사용하는 키 벨류 구조의 컨테이너  FIntPoint언리얼에서 사용하는 정수형 좌표(2D 벡터)를 표현하는 구조체기본적인 연산자(+, -, *, /)를 지원함FIntPoint PointA(10, 20); // X=10, Y=20 FIntPoint PointB(30, 4..
2025.03.12
[ UE ] 언리얼의 네트워크 개념
오늘은 언리얼로 네트워크 게임을 설계하기 위한 기본 개념을 공부해보았다. 온라인 게임의 네트워크 방식싱크(Sync)동기, 실시간, 리얼타임PC 게임에서 주로 사용  어싱크(Async)비동기모바일 게임에서 주로 사용  CAP 이론과 게임의 동기화CAP 이론분산 시스템의 동기화 “조건 3개를 모두 만족하는 시스템은 없다” 라는 이론Consistency(일관성) : 전체 시스템은 동일한 값을 가지고 있어야 함Availability(가용성) : 언제든지 시스템에 접근하여 값을 읽고 쓸 수 있어야 함Partition Tolerance(분할 용인) : 시스템을 분할하여 병렬처리 등이 가능해야 함여기서 주로 CP or AP로 구성해서 사용함 (-> 네트워크 게임에선 항상 분할이 가능해야하니까 P는 고정으로 가져감) ..
2025.03.11

언리얼의 네크워크에 대해 공부를 하다가 내가 이해하기 쉽게 정리해보았다.

 

멀티플레이 흐름

1. 멀티플레이 게임을 실행하면 언리얼 복제 시스템(Replication)이 모든 GameInstance가 동기화되도록 백드라운드에서 실행

2. 공유하는 세계를 가져옴(각 PC에서 자체적으로 GetWorld() 함)

+서버와 Client는 각각 개별의 UGameEngine을 가짐

3. 서버를 부팅하면 NetDriver가 생성되고 원격 프로세스(서버)의 메세지를 수신

4. 클라이언트를 부팅하면 클라이언트만의 NetDriver가 생성되고, 서버에 연결 요청을 보냄

5. 서버와 클라이언트의 NetDriver가 접촉하면 각 NetDriver 내에 NetConnection이 설정됨

+서버에는 연결된 플레이어 각각의 NetConnection을 가지고 있음

+각 클라이언트는 서버와의 연결을 나타내는 단일 NetConnection을 가지고 있음

 

6. 각 NetConnection에는 다양한 채널이 연결됨

일반적으로 Control Channel, VoiceChannel, 일련의 Actor Channel을 가지는데 해당 연결을 통해 복제되고 있는 각 액터에 대해 하나씩 있음

 

 

+네트워크 상에서 동기화 상태를 유지해야하는 경우 해당 액터를 복제하고, 이 채널들로 서버와 클라이언트가 해당 액테에 대한 정보를 교환하는 것.

+액터가 클라이언트에 복사될 때

  • LifeTime
    1. 액터의 수명은 서버와 클라이언트 사이에서 동기화됨
    2. 서버에서 복제할 액터를 생성하면 클라이언트에 알림을 줘서 클라이언트에서 자체적으로 사본을 생성할 수 있게 함
    3. 서버에서 액터를 파괴하면 클라이언트에서도 파괴됨
  • Property Replication
    1. 복제를 위한 속성(플래그)이 액터에 포함되어있는 경우에 해당 속성이 서버에서 변경되면, 새 값이 클라이언트로 전송되어서 클라이언트도 동기화됨
    2. 서버에서 클라이언트로 단방향만 복제가능
  • RPC(Remote Procedure Call)
    1. 함수를 멀티캐스트 RPC로 설정하면 서버에서 해당 함수를 호출했을 때, 서버는 현재 액터가 복제되고 있는 모든 클라이언트에 메세지 전송 → 클라이언트는 알림을 받고 해당 함수를 호출해야함.
    2. 서버에서 클라이언트, 클라이언트에서 서버의 함수를 호출할 수 있게 하는 이벤트
      1. Server RPC : 클라이언트에서 서버의 함수를 호출(플레이어의 입력이나 행동을 서버로 전달할 때)
      2. Client RPC : 서버에서 클라이언트의 함수를 호출(서버가 게임 상태나 특정 명령을 클라이언트로 전달할 때)
      3. Multicast RPC : 서버에서 모든 클라이언트의 함수를 호출(전체 플레이어에게 동일한 이벤트나 업데이트를 전파할 때)

 

Pawn 클래스로 만들어진 액터여도 각 플레이어 컨트롤러는 Pawn을 기본적으로 상속받기 때문에 알고 있음. 따라서 서버는 각 PC의 가장 하단에 생성된 액터를 통해서도(클라이언트가 소유한 액터를 거슬러 올라가면서) 소유자가 누구인지 알 수 있음

 

  • 액터 Replication
    • 액터가 복제 대상으로 고려되려면 bReplicates = true여야함 (C++은 일반적으로 생성자에서 이 값을 설정, 블루프린트는 Detail 패널에서 Replicate 체크)
  • 내가 이해한 복제된 액터를 동기화하는 내용 결론
    • 서버는 각 UNetConnection에 연결된 Channel을 통해 ActorChannel에 접근하고, 이 때 Replicate 옵션을 확인. → 복사한다고 되어있으면 해당 액터를 클라이언트에 복제함

 

 

마지막 말

 

내일은 GameMode, GameInstance, PlayerController 들의 Replicate 과정을 공부해볼 예정이다.

오늘은 앞으로 만들 격투 게임에 대한 기획과 설계를 간단하게 진행해보았다.

 

인게임 시스템 기능 설계

GameMode(게임 규칙과 전투 흐름 관리할 예정)

- 매치 관리 (어떻게 매치할건지 ← 규태님이랑 이야기해보기)
- 플레이어 스폰(맵에 움직일 수 있는 랜덤한 위치에 생성할 예정
- 게임 시작/종료(하트 3개에 플레이어 1명 남을 때까지, 시간 타이머)
- 플레이어 상태 초기화(체력, 스킬 쿨타임 등)
- 이벤트 트리거(라운드시작, 종료, 플레이어 사망, 리스폰 등)


GameState

- GameMode에서 정의한 시작/종료 조건 업데이트
- 각 라운드별 결과 및 이벤트 저장
- HUD에 필요한 데이터 업데이트(체력, 콤보, 스킬 쿨타임, 남은 시간 등)
- GameMode에서 이벤트 트리거 받아와서 UI에 전달
- 플레이어 사망했을 때 관전모드
- Rating 값 아웃게임에 넘겨주기?
- 데미지, 준 피해량 저장하고 Rating 계산할 때 쓰기

GameInstance

- 선택한 캐릭터 저장(아웃게임에서 받아오기)
- BattleManager(캐릭터랑 이야기) → 캐릭터에서 데이터 받아와서 판정하고 클라에 다시 뿌려주기
- Attack
- TakeDamage
- DI

 

인게임 UI

- 라운드 시작 UI(전투 시작 전 카운트로 표출)
- 타이머
- 카메라 액션 - 피격당하거나 궁극기같은거 쓸 때 쉐이크 같은 것들(플레이어에서 하면 좋을 것 같음)
- 최종 결과 UI

 

HUD

- 체력바(거리에 따라 UI 동기화할지 말지 추가하면 좋을듯)
- 스킬 쿨타임

 

+PlayerController

 

+(후순위) 튜토리얼 진행

 

이런 식으로 설계해보았다.

전반적인 게임모드와 UI는 인게임, 아웃게임으로 분리해서 진행하였고 나는 인게임을 담당하기로 했다.

언리얼 프로젝트를 진행하면서 게임의 전반적인 흐름을 설계해보고 싶었는데 이번 기회에 잘 녹여내보아야겠다.

 

트러블 슈팅

상황
호스트와 게스트의 플레이어 입력 카운트가 공유되는 버그 발생

 

해결
GameMode에서 PlayerInputCount를 관리했는데 공부하다보니, 게임모드는 서버만 가지고 있는 인스턴스라 문제가 있었다. 플레이어 컨트롤러에서 카운트를 관리하는 방식으로 변경해주었다.

오늘은 언리얼의 멀티 네트워크를 깊게 이해해보고자 숫자 야구 게임을 만드는 과제를 진행했다.

이렇게 채팅을 "/123" 이런 느낌으로 치면 서버가 생성한 난수에 대한 야구 게임을 시작하는거다.

 

일단 작업 플로우는 다음과 같이 설계했다.

1. 난수 생성 클래스 구현
- 여기서는 몇개의 난수를 생성할건지만 매개변수로 받아서 동작하게 설계했다
(나중에 난수를 또 만들게 되었을 때 재사용할 수 있으면 좋겠어서)

2. 판정 클래스 구현
- 플레이어가 입력한 Input에 대해 몇 개의 수를 맞췄는지 알려주는 부분을 구현했다.

3. GameMode 클래스 구현
- 게임모드는 난수생성클래스와 판정클래스를 가져와서 이 데이터들을 저장한다.
- 플레이어의 Input에 대한 예외처리(숫자갯수, 문자가 들어갔는지, 0이 포함되어있는지, 공백이나 특수문자가 있는지 등)을 처리했다.
- 플레이어의 승리, 패배 기능을 구현했다.

4. PlayerContoller (구현 중)
- 멀티캐스트로 RPC 함수 뿌려주는 부분
- 위젯과 연결

+코드짜면서 모르는 부분 공부

 

이 중에서 오늘은 서버 연결 바로 전 플로우인 3번 플로우까지 진행했다.

 

난수 생성 클래스 구현

난수는 0을 제외한 1 ~ 9까지의 수를 통해 생성해야한다.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "GenerateRandomNumber.generated.h"

UCLASS(Blueprintable)
class SIMPLECHAT_API UGenerateRandomNumber : public UObject
{
	GENERATED_BODY()

public:
	UGenerateRandomNumber();

	UFUNCTION(BlueprintCallable, Category = "BaseBallGame")
	TArray<int32> GenerateSecretNumber(int32 NumDigits = 3);  // 추후 3개말고 더 늘릴 수도 있어서 매개변수로 디폴트 지정해줬음
	
private:
	UPROPERTY()
	TArray<int32> InitDigits;
};
#include "GenerateRandomNumber.h"
#include "Math/UnrealMathUtility.h"


UGenerateRandomNumber::UGenerateRandomNumber()
{
}

TArray<int32> UGenerateRandomNumber::GenerateSecretNumber(int32 NumDigits)
{
	TArray<int32> Digits = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	TArray<int32> SecretNumber;

	for (int i = 0; i < NumDigits; i++)
	{
		int32 RandomIndex = FMath::RandRange(0, Digits.Num() - 1);
		int32 SelectedDigit = Digits[RandomIndex];
		SecretNumber.Add(SelectedDigit);
		Digits.RemoveAt(RandomIndex);
	}

	return SecretNumber;
}

TArray<int> 형태에 원하는 숫자들을 모두 넣어주었다.

반목문을 돌면서 랜덤으로 숫자를 뽑아주고, 반환값에 추가해주었다.

그리고 이 뽑힌 랜덤 수는 중복을 없애기 위해 삭제해주었다.

 

 

판정 클래스 구현

판정은 생성된 난수와 플레이어의 입력만을 비교해서 판정하도록 구현했다.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "JudgmentNumber.generated.h"


UCLASS(Blueprintable)
class SIMPLECHAT_API UJudgmentNumber : public UObject
{
	GENERATED_BODY()
	
public:
	UFUNCTION(BlueprintCallable, Category = "BaseBallGame")
	FString Judgment(const TArray<int32>& SecretNumber, const TArray<int32>& PlayerNumber);
};
#include "JudgmentNumber.h"

FString UJudgmentNumber::Judgment(const TArray<int32>& SecretNumber, const TArray<int32>& PlayerNumber)
{
	if (SecretNumber.Num() != PlayerNumber.Num())
	{
		return "배열 초과";
	}

	int32 StrikeCount = 0;
	int32 BallCount = 0;

	for (int i = 0; i < SecretNumber.Num(); i++)
	{
		if (PlayerNumber[i] == SecretNumber[i])
			StrikeCount++;
		else if (SecretNumber.Contains(PlayerNumber[i]))
			BallCount++;
	}

	if (StrikeCount == SecretNumber.Num())
		return TEXT("WIN");
	else if (StrikeCount == 0 && BallCount == 0)
		return TEXT("OUT");
	else
		return FString::Printf(TEXT("%dS%dB"), StrikeCount, BallCount);
}

 

+트러블 슈팅

상황
게임을 실행해서 동작을 확인하던 도중, SecretNumber에서 Index Out Of Range 라는 예외가 떴었다.
알고보니 생성된 난수의 길이와 플레이어가 입력한 인풋의 길이체크를 안해줘서 예외가 나는거였다.

해결방법
생성된 난수의 길이와 플레이어가 입력한 인풋의 길이를 비교해서 같지 않으면 return 하는 방식으로 간단하게 추가해줬다.

 

 

GameMode 클래스 구현
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "SimpleChatGameMode.generated.h"

class UGenerateRandomNumber;
class UJudgmentNumber;

UCLASS()
class SIMPLECHAT_API ASimpleChatGameMode : public AGameModeBase
{
	GENERATED_BODY()

public:
	ASimpleChatGameMode();

	virtual void StartPlay() override;

	UFUNCTION(BlueprintCallable, Category = "BaseBallGame|GameMode")
	void JugmentPlayerInput(const FString& PlayerInput);

protected:
	UFUNCTION(BlueprintCallable, Category = "BaseBallGame|GameMode")
	void InitializeGame();

private:
	UPROPERTY()
	TArray<int32> SecretNumber;

	UPROPERTY()
	UGenerateRandomNumber* SecretNumberGenerate;

	UPROPERTY()
	UJudgmentNumber* Judgment;

	UPROPERTY()
	int32 PlayerInputCount;

	static const int32 MaxInputCount = 3;
};
#include "SimpleChatGameMode.h"  
#include "GenerateRandomNumber.h"  
#include "JudgmentNumber.h"  
#include "Engine/Engine.h"

ASimpleChatGameMode::ASimpleChatGameMode()  
{  
   PlayerInputCount = MaxInputCount;  
   SecretNumberGenerate = CreateDefaultSubobject<UGenerateRandomNumber>(TEXT("SecreteNumberGenerate"));  
   Judgment = CreateDefaultSubobject<UJudgmentNumber>(TEXT("JudgmentPlayerInput"));  
}

void ASimpleChatGameMode::StartPlay()  
{  
   Super::StartPlay();  
   InitializeGame();  
}

void ASimpleChatGameMode::InitializeGame()  
{  
   // 랜덤난수 생성하고 저장  
   if (IsValid(SecretNumberGenerate))  
   {  
       SecretNumber = SecretNumberGenerate->GenerateSecretNumber(3);  
       PlayerInputCount = MaxInputCount;  

       FString SecretStr;  
       for (int32 Num : SecretNumber)  
       {  
           SecretStr += FString::FromInt(Num);  
       }  
       UE_LOG(LogTemp, Log, TEXT("[ServerGameMode] Secret Number: %s"), *SecretStr);  
   }  
}

void ASimpleChatGameMode::JugmentPlayerInput(const FString& PlayerInput)
{
    // 입력 문자열에서 /, 공백 제거
    FString CleanInput = PlayerInput.TrimStartAndEnd();
    if (CleanInput.StartsWith("/"))
    {
        CleanInput = CleanInput.RightChop(1);
    }

    CleanInput = CleanInput.TrimStartAndEnd();

    // 입력 길이 체크
    if (CleanInput.Len() != 3)
    {
        UE_LOG(LogTemp, Warning, TEXT("[ServerGameMode] 유효하지 않은 입력 : %s"), *PlayerInput);
        if (GEngine)
        {
            //GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Invalid input length."));
        }

        return;
    }

    // 문자열을 TArray<int32>로 변환
    TArray<int32> PlayerGuess;
    for (int i = 0; i < CleanInput.Len(); i++)
    {
        TCHAR ch = CleanInput[i];
        if (!FChar::IsDigit(ch))
        {
            UE_LOG(LogTemp, Warning, TEXT("[ServerGameMode] 숫자가 아닌 문자가 포함됨: %s"), *PlayerInput);
            if (GEngine)
            {
                GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Contains non-digit characters."));
            }

            return;
        }
        int32 digit = ch - '0';
        if (digit == 0)
        {
            UE_LOG(LogTemp, Warning, TEXT("[ServerGameMode] 0은 허용되지 않음: %s"), *PlayerInput);
            if (GEngine)
            {
                GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("0 is not allowed."));
            }

            return;
        }
        PlayerGuess.Add(digit);
    }

    // 중복 숫자 체크
    TSet<int32> UniqueDigits;
    for (int32 num : PlayerGuess)
    {
        UniqueDigits.Add(num);
    }
    if (UniqueDigits.Num() != PlayerGuess.Num())
    {
        UE_LOG(LogTemp, Warning, TEXT("[ServerGameMode] 중복 숫자가 있음: %s"), *PlayerInput);
        if (GEngine)
        {
            GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Duplicate digits found."));
        }

        return;
    }

    // UJudgmentNumber 클래스를 통해 판정 결과를 얻음
    if (Judgment)
    {
        FString Result = Judgment->Judgment(SecretNumber, PlayerGuess);

        if (GEngine)
        {
            GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, FString::Printf(TEXT("%s"), *Result));
        }
        UE_LOG(LogTemp, Log, TEXT("[ServerGameMode] Judgment Result: %s"), *Result);

        PlayerInputCount--;


        // 결과에 따른 게임 상태 처리
        if (Result.Equals("WIN"))
        {
            if (GEngine)
            {
                GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("Player WIN!"));
            }
            UE_LOG(LogTemp, Log, TEXT("[ServerGameMode] Player wins! Resetting game."));

            // InitializeGame();
        }
        else if (PlayerInputCount <= 0)
        {
            if (GEngine)
            {
                GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Orange, TEXT("Player LOSE! : No Count!"));
                GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Orange, FString::Printf(TEXT("Secret Number is %s !"), *Result));
            }
            UE_LOG(LogTemp, Log, TEXT("[ServerGameMode] No attempts remaining. Resetting game."));
 
            // InitializeGame();
        }
        else
        {
            if (GEngine)
            {
                GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::White, FString::Printf(TEXT("Reamainning Count : %d"), PlayerInputCount));
            }
            UE_LOG(LogTemp, Log, TEXT("[ServerGameMode] Reamainning Count : %d"), PlayerInputCount);
        }

        
    }
}

GameMode에서는 앞에서 만들었던 난수 생성 클래스와 판정 클래스를 가져와서 처리해주는 부분을 구현했다.

그리고 위젯에서 입력을 받을 때 먼저 데이터 예외처리를 해줄텐데 안전하게 가기 위해서 한번 더 진행해주었다.

+ 이 부분은 솔직히 중복된 코드들이라 나중에 유틸리티를 만들어서 끌어다쓰면 더 유용할듯하다.

로그는 사용자가 볼 수 있게 뷰포트에 한 번 띄워주고 디버깅하기위해 LogTemp로도 출력해주었다.

 

+트러블슈팅

상황
뷰포트로 로그찍은 부분이 한글이 깨져서 보이는 현상.
블루프린트로 Print String 찍으면 잘 나오는데 C++ 코드로 접근하면 왜 한글이 깨질까?

해결
과제 구현 시간 때문에 따로 서치를 못해보았다.
일단 임시로 다 영어로 출력하는걸로 변경했고, 과제가 끝나고 버그를 잡는 시간에 다시 해결해보고자 한다.

 

마지막 말

난수생성 클래스와 판정 클래스는 블루프린트에서 쓰려고 처음에는 블루프린트 라이브러리 클래스로 구현했다.
하지만 블루프린트 이해가 너무 어려워서 결국 위젯을 제외하고 모두 C++로 구현하려고 방식이 변경되었다.
하다보니 블루프린트 라이브러리 클래스는 C++ 코드를 통해 인스턴스 생성을 못한다고 해서 UObject로 변경해주었다.

멀티캐스트로 RPC 함수를 뿌려준다고하는데 서버가 가지고 있는 PlayerController에다가 뿌려야할지 클라이언트가 가지고 있는 PlayerController에 뿌려야할지? 아직 PlayController 소유에 대한 이해가 잡히지 않아 이 부분을 더 공부해야겠다.

 

 

오늘은 언리얼로 데디케이트 서버를 구축하려고하는데 과정이 너무 어렵게 느껴졌고.. 빌드에러가 너무 많이 발생했다.

빌드하는데 걸리는 시간이 길다보니 너무 많은 시간을 잡아먹어 정리를 해보려고 한다.

 

 

트러블슈팅 및 데디케이트 서버 환경 구축하기
  1. Switch Unreal Engine Version에서 Source로 빌드 선택
  2. 서버로 된 타겟 파일을 생성
  3. 깃에서 받은 언리얼 엔진 폴더로 가서 GenerateProjectFiles.bat로 솔루션 업데이트해줌(그래야 우리가 소스 빌드로 변경한 솔루션이 갱신됨) + 이미 git 과 빌드를 완료했다면 Setup.bat 같은 부분은 안해도 됨
  4. Development Editor 빌드
  5. Development Server 빌드
    1. 오류 발생 : Serialization Error : Action Needed Corrupt data found, please verify your installation.
    2. 해결 방법 : 아래의 git으로 받은 언리얼 경로에가서 해당 명령어 입력해줌
      1. cd /d D:\\UE5_project\\Git_UnrealEngine\\Engine\\Build\\BatchFiles
        (본인의 경로로 설정해주기)
    3. RunUAT.bat BuildCookRun -project="D:\\UE5_project\\SimpleChat\\SimpleChat.uproject" -noP4 -platform=Win64 -server -clientconfig=Development -serverconfig=Development -cook -build -stage -pak -archive -archivedirectory="D:\\UE5_project\\SimpleChatBuild"
  6. 언리얼 에디어로 들어가서 Server 모드로 패키징
  7. 서버.exe -log로 실행하면 데디케이트 서버가 열린 것
  8. 언리얼 에디터에서는 Netmode Standlone으로 설정 후 테스트 진행

오늘은 언리얼로 멀티플레이를 실습하다가 NetMode가 뭔지 궁금해졌다.

 

  •  NetMode
    • 해당 게임 프로그램이 네트워크 상에서 어떤 역할을 수행하고 있는지를 의미
    • 싱글(NM_StandAlone), 서버(NM_ListenServer, NM_DedicatedServer), 클라이언트(NM_Client)
  • NetMode의 필요성
    • 멀티플레이 개발할 때, 같은 코드가 여러 pc에서 동작함. 그래서 디버깅할 때, 어떤 pc에서 동작하고 있는지 살펴보는 것이 가장 중요 그래서 이 때 NetMode를 활용하면 데디서버에서 실행 중인지, 클라이언트에서 실행 중인지 분간이 가능함.

  • NetConnection
    • 다른 pc와 연결이 발생할 때, 생성됨

  • NetDriver
    • 언리얼 네트워크 통신에서 로우레벨 동작들을 관리하는 클래스
    • 싱글플레이에서는 UNetDriver 개체가 생성되지 않음
    • 멀티플레이에서만 생성되고, UWorld::Listen() 함수를 통해 UNetDriver 개체 생성됨 (멀티플레이에 참여하는 각 PC마다 UNetDruver 개체 생성됨)
    • UNetDriver는 생성된 UNetConnection 개체를 소유하고 관리함
    • 서버 pc에 생성된 UNetDriver는 접속하는 클라이언트 수만큼 UConnection 관리
    • 클라 pc에 생성된 UNetDriver는 ServerConnection 하나만 관리

 

 

트러블 슈팅

문제상황 1. 뭔가 코드 intellisence가 다 깨져있고, 빌드를 했을 때 파일이 손상되었다는 에러창이 떴다.

이렇게 다 깨져있고

데디케이트 서버 실험을 하기 위해 내 비주얼스튜디오의 타겟파일을 복사해서 Server로 변경해주고, Development Editor에서 빌드한 후, Development Server로 바꾸고 빌드를 해줬다.

빌드는 정상적으로 잘 된 것 같았는데, 실행해보니 파일이 손상되었다는 오류가 떴고, 실행이 되지않았다..

 

예상 문제 1 : git으로 언리얼을 받아놓고 소스빌드를 한 상태인데, 습관적으로 언리얼 런처로 프로젝트를 켰다가 이 난리가 난 것 같다고 예상한다.(왜냐면 같은 팀의 다른 팀원도 이 상황에서 겪었다고 하셨다..)

예상 문제 2: 깃으로 받은 언리얼 빌드를 하다가 중간에 꼬여서 그냥 내 프로젝트 서버 빌드를 했는데 더더 꼬여버린 경우..

 

해결한 방법:

아직 해결 못했다.. 문제가 될 상황들이 너무 많은지라 오류를 자세히 들여다보고 다시 해결해야겠다..

 

 

 

 

문제 링크

https://school.programmers.co.kr/learn/courses/30/lessons/42862

 

프로그래머스

SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

 

 

문제풀이

체육복의 갯수를 단서로 잡고 풀이했다.

도난당한 학생의 체육복 갯수는 -1, 일반 학생의 갯수는 0, 체육복 여벌이 있는 학생은 1로 잡았다.

 

1. 학생 리스트를 만들어서 현재 학생들의 체육복 갯수 상태를 채워주었다.

2. 현재 인덱스의 값이 -1이라면, 앞 뒤를 체크하여 갯수를 조절해주었다.

 

 

 

내 코드
#include <string>
#include <vector>
#include <iostream>

using namespace std;

int student[30];

int solution(int n, vector<int> lost, vector<int> reserve) {
    int answer = 0;
    
    // lost의 체육복 갯수
    for (int i = 0; i < lost.size(); i++)
    {
        student[lost[i]]--;
    }
    
    // reserve 체육복 갯수
    for (int i = 0; i < reserve.size(); i++)
    {
        student[reserve[i]]++;
    }
    
    for (int i = 1; i <= n; i++)
    {
        if (student[i] == -1)
        {
            if (student[i - 1] == 1)
            {
                student[i - 1] = 0;
                student[i] = 0;
            }
            else if (student[i + 1] == 1)
            {
                student[i + 1] = 0;
                student[i] = 0;
            }
        }
        
        if (student[i] >= 0)
        {
            answer++;
        }
    }
    
    
    return answer;
}

 

 

 

 

[ UE ] FIntPoint가 뭘까?

닿메_dahme
|2025. 3. 12. 21:26

오늘은 구조체로 코딩을 하다가 FIntPoint라는 자료형?에 대해 접하게 되었다.

뭔지 궁금해서 언리얼에서 사용하는 다른 자료형들과 함께 정리를 해보았다.

 

TArray
  • std::vector와 유사하게 동작하지만 AdddUnique, RemoveAtSwap 등의 부가적인 API를 제공한다고 함.
  • 컨테이너의 element들이 가비지 컬렝팅됨. -> UObject에서 파생된 포인터들을 모두 담을 수 있음

 

 

TMap
  • std::map과 유사한 언리얼에서 사용하는 키 벨류 구조의 컨테이너

 

 

FIntPoint
  • 언리얼에서 사용하는 정수형 좌표(2D 벡터)를 표현하는 구조체
  • 기본적인 연산자(+, -, *, /)를 지원함
FIntPoint PointA(10, 20); // X=10, Y=20 FIntPoint PointB(30, 40);

 

연산방법 FIntPoint A(10, 20); FIntPoint B(5, 5);

// (A의 X인 10 + B의 X인 5 = 15, A의 Y인 20 + B의 Y인 5 = 25)
FIntPoint Sum = A + B; 

// (5, 15) FIntPoint Mult = A * 2; // (20, 40)
FIntPoint Diff = A - B;

 

Size도 반환가능하다!!

// 결과: 5.0 (3^2 + 4^2 = 5) -> X^2 + Y^2 = 루트결과인건가?
FIntPoint P(3, 4); float Size = P.Size();

 

Clamp로 범위 제한도 가능

FIntPoint P(120, 50);
// (100, 50)
FIntPoint Clamped = P.ClampAxes(0, 100);

 

 

마지막 말

언리얼의 자료형에 더 익숙해진 것 같아서 뿌듯하다.

항상 클래스만 쓰다가 구조체를 쓰니까 뭔가 낯설긴한데 자주 예제를 찾아서 적용해봐야겠다.

오늘은 언리얼로 네트워크 게임을 설계하기 위한 기본 개념을 공부해보았다.

 

온라인 게임의 네트워크 방식


싱크(Sync)

  • 동기, 실시간, 리얼타임
  • PC 게임에서 주로 사용

 

 어싱크(Async)

  • 비동기
  • 모바일 게임에서 주로 사용

 

 

CAP 이론과 게임의 동기화

CAP 이론

  • 분산 시스템의 동기화 “조건 3개를 모두 만족하는 시스템은 없다” 라는 이론
  • Consistency(일관성) : 전체 시스템은 동일한 값을 가지고 있어야 함
  • Availability(가용성) : 언제든지 시스템에 접근하여 값을 읽고 쓸 수 있어야 함
  • Partition Tolerance(분할 용인) : 시스템을 분할하여 병렬처리 등이 가능해야 함
  • 여기서 주로 CP or AP로 구성해서 사용함 (-> 네트워크 게임에선 항상 분할이 가능해야하니까 P는 고정으로 가져감)

 

 

클라이언트 보간
  • 네트워크는 불안정한 전기 신호로 수신됨
  • 따라서 클라이언트에서 보간 작업을 수행해줘야함(언리얼은 자동으로 된다고 함?)

 

CP 방식(가용성 보정)

  • 일정 시간동안 이벤트를 모았다가 브로드캐스팅
  • 플레이어의 입력을 받음 → 플레이어에게 피드백(송수신간의 Gap을 위장)하면서 서버에 이벤트 전송 → 서버가 브로드캐스팅 → 클라이언트는 서버가 브로드캐스팅한 데이터로 상태 업데이트

 

AP 방식(일관성 보정)

  • 게임 클라이언트를 우선적으로 업데이트하고 후에 일관성을 보정
  • 예측과 서버 값이 맞으면 그대로 사용, 틀리면 서버값으로 반영(그래서 응답속도가 낮을 경우 게임 하다가 롤백이나 상대방의 순간이동하는 것처럼 보일 수 있음)
  • 플레이어 입력 → RPC 호출 → 서버 브로드캐스팅 → 클라이언트 상태 업데이트

 

 비동기

  • 네트워크에서 세션을 유지하기 어려운 모바일 환경 등에서 사용하는 방법으로 “보장된 데이터를 사용”하여 게임 환경을 구축함
  • 느리지만 손실 허용이 안되는 TCP 프로토콜 사용
  • 이벤트만 서버로 전송하고, 서버는 이벤트 검증하고 결과를 DB에 저장

 

 

채팅 서버와 온라인 게임 서버의 유사점과 차이점
  • 유사점
    • 사용자의 입력을 전체 사용자에게 브로드캐스팅함
    • 사용자의 세션을 관리함
    • 채널(서버)별로 다른 상태를 가짐
    • 채널을 선택하거나 내 아바타를 커스텀하거나 친구 목록을 보는 등의 로비가 있음
  • 차이점
    • 채팅에서는 사용자를 그래픽으로 나타내지않음
    • 게임은 승과 패의 요소가 있지만 채팅은 그렇지 않음
    • 채팅은 낮은 Latency를 고려하지 않지만 게임 서버는 클라이언트 예측과 서버 보정을 해야함

 

 

마무리 말

서버 개념에 대해 처음 접하는 부분이 많아 아직 제대로 이해하지 못했다.

지금은 개념부터 익숙해지도록 공부하고, 이후에 실습을 통해 자세한 이해를 해야겠다.