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
[KPT 회고] HelloWorld TPS 슈터게임
HelloWorld 프로젝트 피드백 및 KPT 연계 정리 [ 프로젝트 피드백 정리 ]기획적인 아쉬움: 맵의 트랩과 보스 패턴에 대한 기획이 부족하여 게임의 난이도와 재미 요소가 충분히 반영되지 못했다. 이를 통해 플레이어에게 더 도전적이고 흥미로운 경험을 제공할 수 있는 기회를 놓쳤다.스킬 관리: 스킬이 많아질 경우, 이를 추상 클래스로 묶어서 관리하면 코드의 유지보수성과 확장성이 크게 향상될 것이다. 이는 새로운 스킬을 추가하거나 기존 스킬을 수정할 때 발생할 수 있는 복잡성을 줄여줄 것이다.아이템 시각적 디테일: 현재 아이템이 시각적으로 충분히 매력적이지 않아 플레이어에게 아이템의 중요성을 전달하는 데 어려움이 있다. 시각적 디테일을 추가하여 아이템의 가치를 더 명확히 표현할 필요가 있다.패턴 클래스..
2025.03.07
no image
[ UE ] 적 공격할 때 카메라 깜빡이는 문제
오늘은 작업을 하다가 적이 캐릭터에게 공격을 할 때, 화면이 깜빡거리는 현상을 발견했다. 디버그를 해본 결과 원인은 무기 콜리젼이 스프링암 경로를 지나면서 카메라가 잠깐 적을 확대해서 보여주고 다시 원래대로 돌아가면서 깜빡이는 것처럼 보여지는 것 때문이라 생각했다. 이렇게 무기가 비스듬히 지나가면 괜찮은데 스프링암과 부딪히면서 카메라가 앞으로 확대되었다가 다시 돌아갔다.  뭔가 콜리젼 때문이라 생각하고 검색해봤는데, trace channels이라는 것을 알게되었다.  trace channels라인 트레이스나 스윕(sweep) 등의 충돌 검사 기능을 사용할 때, 어떤 객체와의 충돌을 검사할지 결정하는 기준이다.특정 객체의 충돌 검사를 할지, 말지 설정할 수 있다. 먼저 내가 사용하고 있는 Enemy 캐릭터..
2025.02.28
[ UE, C++ ] 오늘 배운 내용 정리 및 애니메이션에 대해 알게 된 꿀팁
오늘배운내용 - AI Possess할 때는 코드의 옵션과 블루프린트의 옵션이 동일해야 인식된다. - 언리얼 엔진에서는 C++의 TakeDamage가 호출될 때, 블루프린트 쪽에 Event AnyDamage가 자동으로 브로드캐스팅된다.- 적 ai 모션을 구현할 때도 Anim Instance를 만드는게 좋다- 캐릭터가 죽었는지 살았는지 변수 받아서, 적 애니메이션 블루프린트에 캐릭터가 살았는지, 죽었는지 판단해서 처리하는게 좋음 - 공격이나 그런 연속적인 호출? 느낌의 모션은 몽타주가 맞고, idle 사망 같은거는 state를 나눠서 처리하는게 좋음 - additive를 설정해줘야하는 이유 마지막 말적 사망을 구현할 때, 사망 모션이 잘 되다가 갑자기 기본 포즈로 바뀌는 현상을 발견했다.원인은 몽타주로 사망..
2025.02.26

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

이렇게 채팅을 "/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를 고려하지 않지만 게임 서버는 클라이언트 예측과 서버 보정을 해야함

 

 

마무리 말

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

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

HelloWorld 프로젝트 피드백 및 KPT 연계 정리

 

[ 프로젝트 피드백 정리 ]

  • 기획적인 아쉬움: 맵의 트랩과 보스 패턴에 대한 기획이 부족하여 게임의 난이도와 재미 요소가 충분히 반영되지 못했다. 이를 통해 플레이어에게 더 도전적이고 흥미로운 경험을 제공할 수 있는 기회를 놓쳤다.
  • 스킬 관리: 스킬이 많아질 경우, 이를 추상 클래스로 묶어서 관리하면 코드의 유지보수성과 확장성이 크게 향상될 것이다. 이는 새로운 스킬을 추가하거나 기존 스킬을 수정할 때 발생할 수 있는 복잡성을 줄여줄 것이다.
  • 아이템 시각적 디테일: 현재 아이템이 시각적으로 충분히 매력적이지 않아 플레이어에게 아이템의 중요성을 전달하는 데 어려움이 있다. 시각적 디테일을 추가하여 아이템의 가치를 더 명확히 표현할 필요가 있다.
  • 패턴 클래스화: 패턴을 별도의 클래스로 관리한 것은 매우 긍정적이었다. 이는 코드의 가독성을 높이고, 패턴의 재사용성을 증가시켜 개발 효율성을 높이는 데 기여했다.
  • 데이터 관리: 전역적인 데이터의 독립적 관리가 부족하여 데이터의 일관성과 무결성을 유지하는 데 어려움이 있었다. 데이터 관리 방식을 개선하여 이러한 문제를 해결할 필요가 있다.
  • UI 관리: UI를 GameState가 아닌 UI Manager에서 다루는 것이 더 명확하고 깔끔한 구조를 제공할 것이다. 이는 UI 관련 코드의 집중화와 관리 용이성을 높일 수 있다.
  • Tick 함수 사용: Tick 함수를 많이 사용한 부분은 성능에 영향을 미칠 수 있다. 타이머나 캐릭터 위치 계산이 끝난 후 lateUpdate 같은 함수를 사용하여 성능을 최적화할 수 있다.

 

[ KPT 연계 ]

K (Keep)

  • 활발한 진척도 공유: 팀원 간의 지속적인 진척도 공유는 프로젝트의 투명성을 높이고, 협업을 강화하는 데 큰 도움이 됨
  • 깔끔한 깃 플로우 전략: 체계적인 깃 플로우 전략을 통해 코드의 버전 관리가 효율적으로 이루어짐
  • 소프트 스킬: 팀원 간의 원활한 의사소통과 협력은 프로젝트의 성공적인 진행에 중요한 역할을 함
  • 잘 지켜진 코드 컨벤션: 코드 컨벤션을 철저히 준수함으로써 코드의 일관성과 가독성을 유지할 수 있었음
  • 문제 해결을 위한 팀의 협력: 하나의 문제를 해결하기 위해 팀원들이 함께 노력한 점은 매우 긍정적이었음
  • 명확한 역할 분리: 각 팀원의 역할을 명확히 분리하여 업무의 효율성을 높임
  • 승현님의 깃 해결 능력: 승현님의 뛰어난 깃 문제 해결 능력은 프로젝트 진행에 큰 도움이 됨

P (Problem)

  • 언리얼 기반 클래스의 추상화 부족: 언리얼 엔진의 기반 클래스를 추상화하지 못한 점은 코드의 재사용성과 유지보수성을 저해
  • 세부적인 기획 부족으로 인한 개발 중 변경: 초기 기획의 세부적인 부분이 부족하여 개발 중에 많은 변경이 발생했다. 이는 개발 효율성을 떨어뜨리는 요인이 됨
  • 인터페이스 간 통신 구조의 미흡: 인터페이스 간의 통신 구조가 미흡하여 모듈 간의 의존성을 줄이는 데 어려움이 있었음
  • 게임 수학을 활용한 로직 설계의 어려움: 게임 수학을 활용한 로직 설계에 어려움을 겪어 복잡한 로직 구현에 시간이 소요됨
  • 블루프린트와 C++의 역할 분리: 블루프린트와 C++의 역할을 명확히 분리하지 못해 코드의 복잡성이 증가함
  • 인벤토리 데이터 관리의 어려움: 인벤토리 데이터 관리가 비효율적으로 이루어져 데이터의 일관성을 유지하는 데 어려움이 있었음
  • GameState, GameMode, GameInstance의 역할 이해 부족: 이들 클래스의 역할을 명확히 이해하지 못해 코드 설계에 혼란이 있었음
  • 프로젝트 메모리 관리의 어려움: 메모리 관리가 비효율적으로 이루어져 성능 저하의 원인이 됨

T (Try)

  • 코드 리뷰 세션 정례화: 주 1회 코드 리뷰 세션을 통해 각자 구현한 부분을 공유하고, 피드백을 주고받는 시간을 가질 것임. 이를 통해 다른 업무의 세부적인 로직을 파악하고 전체 시스템에 대한 이해도를 높일 것임.
  • 설계 문서 작성 및 관리 시스템 도입: 클래스 다이어그램, 시퀀스 다이어그램 등을 작성하고 관리할 수 있는 도구(예: PlantUML, Draw.io)를 도입하여 문서화를 자동화하고 지속적으로 갱신할 것임.
  • 언리얼 엔진 아키텍처 스터디: GameState, GameMode, GameInstance의 역할과 관계를 명확히 이해하기 위한 스터디 그룹을 운영하고, 실제 프로젝트에 적용할 수 있는 아키텍처 패턴을 연구할 것임.
  • 추상화 리팩토링 계획 수립: 기존 코드에서 공통 기능을 식별하고 추상 클래스로 리팩토링하는 계획을 수립할 것임. 특히 스킬 시스템과 같이 확장 가능성이 높은 부분부터 우선적으로 적용할 것임.
  • 인터페이스 기반 설계 도입: 모듈 간 의존성을 줄이기 위해 인터페이스 기반 설계를 도입하고, 의존성 주입 패턴을 활용하여 컴포넌트 간 결합도를 낮출 것임.
  • 게임 수학 스터디 및 라이브러리 활용: 게임 수학에 대한 이해를 높이기 위한 스터디를 진행하고, 언리얼 엔진에서 제공하는 수학 라이브러리를 효과적으로 활용하는 방법을 익힐 것임.
  • 블루프린트-C++ 가이드라인 수립: 블루프린트와 C++의 역할 분담에 대한 명확한 가이드라인을 수립할 것임. 일반적으로 성능이 중요한 코어 시스템은 C++로, UI 및 프로토타이핑은 블루프린트로 구현하는 원칙을 세울 것임.
  • 데이터 관리 시스템 개선: 인벤토리 및 게임 데이터 관리를 위한 통합 시스템을 설계하고, 데이터 일관성을 보장하기 위한 검증 메커니즘을 구현할 것임. 필요시 데이터베이스 패턴을 도입할 것임.
  • 메모리 프로파일링 도구 활용: 언리얼 엔진의 메모리 프로파일링 도구를 활용하여 메모리 누수와 비효율적인 메모리 사용을 식별하고 최적화할 것임. 주기적인 프로파일링을 통해 성능 이슈를 조기에 발견할 것임.
  • 접근 지정자 가이드라인 수립: 클래스 멤버 변수와 함수에 대한 접근 지정자 사용 가이드라인을 수립하고, 코드 리뷰 시 이를 준수하는지 확인할 것임. 특히 캡슐화 원칙을 강조할 것임.
  • 애니메이션 워크숍 진행: 언리얼 엔진의 애니메이션 시스템에 대한 워크숍을 진행하고, 애니메이션 블루프린트와 스테이트 머신의 효과적인 활용법을 익힐 것임.
  • 기획 단계 강화: 개발 시작 전 기획 단계를 강화하여 세부적인 게임 메커니즘, 레벨 디자인, 보스 패턴 등을 문서화하고, 변경 사항이 발생할 경우 체계적인 변경 관리 프로세스를 따를 것임.

오늘은 작업을 하다가 적이 캐릭터에게 공격을 할 때, 화면이 깜빡거리는 현상을 발견했다.

 

디버그를 해본 결과 원인은 무기 콜리젼이 스프링암 경로를 지나면서 카메라가 잠깐 적을 확대해서 보여주고 다시 원래대로 돌아가면서 깜빡이는 것처럼 보여지는 것 때문이라 생각했다.

 

이렇게 무기가 비스듬히 지나가면 괜찮은데 스프링암과 부딪히면서 카메라가 앞으로 확대되었다가 다시 돌아갔다.

 

 

뭔가 콜리젼 때문이라 생각하고 검색해봤는데, trace channels이라는 것을 알게되었다.

 

trace channels

라인 트레이스나 스윕(sweep) 등의 충돌 검사 기능을 사용할 때, 어떤 객체와의 충돌을 검사할지 결정하는 기준이다.

특정 객체의 충돌 검사를 할지, 말지 설정할 수 있다.

 

먼저 내가 사용하고 있는 Enemy 캐릭터 클래스에서 충돌체를 확인했고, collsion 타입을 Capsule Component는 Pawn으로 설정되어있었고, Mesh는 CharacterMesh인 것을 확인했다.

 

Edit -> Project Settings에 들어가서 Engine에 Collsion 카테고리에 들어가면 Trace Channels를 설정할 수 있는 메뉴가 나온다.

 

여기에서 내가 사용하고 있는 Pawn과 CharacterMesh는 카메라에 대한 충돌을 ignore로 설정해주었다.

 

그랬더니 화면이 깜빡거리는 현상없이 잘 나오는 것을 확인했다!

오늘배운내용


- AI Possess할 때는 코드의 옵션과 블루프린트의 옵션이 동일해야 인식된다.
- 언리얼 엔진에서는 C++의 TakeDamage가 호출될 때, 블루프린트 쪽에 Event AnyDamage가 자동으로 브로드캐스팅된다.
- 적 ai 모션을 구현할 때도 Anim Instance를 만드는게 좋다
- 캐릭터가 죽었는지 살았는지 변수 받아서, 적 애니메이션 블루프린트에 캐릭터가 살았는지, 죽었는지 판단해서 처리하는게 좋음
- 공격이나 그런 연속적인 호출? 느낌의 모션은 몽타주가 맞고, idle 사망 같은거는 state를 나눠서 처리하는게 좋음
- additive를 설정해줘야하는 이유

 

마지막 말

적 사망을 구현할 때, 사망 모션이 잘 되다가 갑자기 기본 포즈로 바뀌는 현상을 발견했다.

원인은 몽타주로 사망을 구현했기 때문인데, 콤보 공격이나 피격 같은 부분은 연속적으로 특정 이벤트가 호출될 때마다 발생할 수 있기 때문에 몽타주로 구현하는 것이 유리하다.

하지만 Idle이나 사망 모션 같은 경우에는 하나의 애니메이션이 지속되는 형태이기 때문에 이런 경우에는 State Machine을 사용하는 것이 더 효율적이라는 것을 알게되었다.