🔖 [Unreal Engine 5.5 / C++] - CH4 개인 프로젝트 (1) : 숫자 야구 판정 로직 및 입력 유효성 검사 (~20261012 pm12)
: 실제 숫자 야구 게임을 진행하기 위해 필요한 정답 생성, 입력 유효성 검사, Strike / Ball / Out 판정 로직을 중심으로 정리했다.
특히 플레이어가 입력한 숫자를 서버에서 검사하고, 정상적인 입력에 대해서만 시도 횟수를 증가시키는 과정이 중요했다.
숫자 야구 게임에서는 서버가 정답 숫자를 생성하고, 두 플레이어가 같은 정답을 기준으로 게임을 진행해야 한다.
이번 프로젝트의 정답 생성 조건은 다음과 같다.
ex) 386, 527은 정상적인 정답이지만, 112는 숫자가 중복되고 305는 0이 포함되어 있으므로 사용할 수 없다.
중복되지 않는 정답을 생성하기 위해서는 먼저 1~9 사이의 숫자를 준비하고, 그중 3개를 무작위로 선택하는 과정이 필요하다.
언리얼 엔진에서는 TArray를 활용하여 여러 숫자를 저장할 수 있고, FMath::RandRange()를 통해 특정 범위의 난수를 생성할 수 있다.
TArray<int32> Numbers;
for (int32 i = 1; i <= 9; ++i)
{
Numbers.Add(i);
}
위 코드는 1부터 9까지의 숫자를 배열에 저장하는 과정이다.
TArray<int32> : 정수형 데이터를 여러 개 저장하는 동적 배열for : 지정한 조건에 따라 코드를 반복 실행하는 반복문Numbers.Add(i) : 배열에 숫자를 추가하는 함수이후 무작위로 선택한 숫자를 정답 문자열에 추가하고, 이미 선택한 숫자를 배열에서 제거하면 중복을 방지할 수 있다.
int32 Index = FMath::RandRange(0, Numbers.Num() - 1);
Result.Append(FString::FromInt(Numbers[Index]));
Numbers.RemoveAt(Index);
FMath::RandRange()는 지정한 범위에서 무작위 정수를 반환한다.
Numbers.Num()은 배열에 저장된 원소의 개수이며, 배열의 인덱스는 0부터 시작하기 때문에 마지막 인덱스는 Numbers.Num() - 1이 된다.
RemoveAt()은 지정한 인덱스의 원소를 제거한다.
따라서 숫자를 선택한 후 배열에서 제거하면 다음 반복에서 같은 숫자가 다시 선택되지 않는다.
핵심은 무작위 숫자를 생성하는 것뿐 아니라, 선택한 숫자가 중복되지 않도록 처리하는 것이다.
플레이어가 입력한 값이 항상 게임 규칙에 맞는 것은 아니다.
숫자 야구 게임에서는 입력한 숫자가 다음 조건을 모두 만족해야 정상적인 입력으로 인정된다.
| 입력값 | 결과 | 이유 |
|---|---|---|
386 | 정상 | 서로 다른 3자리 숫자 |
12 | 오류 | 3자리가 아님 |
1234 | 오류 | 3자리를 초과 |
112 | 오류 | 숫자 중복 |
305 | 오류 | 0 포함 |
1a3 | 오류 | 숫자가 아닌 문자 포함 |
이번 과제에서는 잘못된 입력을 하더라도 시도 횟수가 차감되지 않도록 처리해야 했다.
따라서 숫자를 판정하기 전에 입력값이 유효한지 먼저 검사하는 과정이 필요했다.
입력값이 정확히 3자리인지 확인하기 위해 FString의 Len() 함수를 활용할 수 있다.
if (InNumberString.Len() != 3)
{
return false;
}
Len() : 문자열의 길이를 반환!= : 두 값이 서로 다른지 비교return false : 조건을 만족하지 않는다는 결과를 반환입력한 문자열의 길이가 3이 아니라면 이후 판정 로직을 진행하지 않는다.
문자열의 길이가 3이라고 해서 모두 정상적인 입력은 아니다.
숫자가 아닌 문자가 포함되어 있거나, 0이 포함되어 있거나, 같은 숫자가 반복되는 경우도 검사해야 한다.
if (!FChar::IsDigit(C) || C == '0')
{
return false;
}
FChar::IsDigit()은 해당 문자가 숫자인지 확인하는 함수다.
!: 조건의 참과 거짓을 반대로 바꾸는 논리 부정 연산자||: 두 조건 중 하나라도 참이면 참이 되는 OR 연산자==: 두 값이 같은지 비교하는 연산자
즉, 숫자가 아니거나 0이 포함된 경우 정상적인 입력이 아니라고 판단한다.
중복 여부는 TSet과 같은 자료구조를 활용하여 검사할 수 있다.
TSet은 중복되지 않는 값을 저장하기 때문에, 이미 등장한 숫자가 다시 입력되었는지 확인하는 데 활용할 수 있다.
이번 과제에서 중요했던 부분은 입력값의 유효성을 확인하기 전에 시도 횟수를 증가시키지 않는 것이었다.
[플레이어 숫자 입력]
|
v
입력값 유효성 검사
|
정상 입력인가?
/ \
아니오 예
| |
v v
오류 안내 시도 횟수 증가
횟수 유지 |
v
숫자 판정 진행
ex) 플레이어가 112를 입력했다면 중복 숫자가 포함되어 있으므로 입력 오류로 처리하고, 기존 시도 횟수를 유지해야 한다.
반면 386처럼 정상적인 숫자를 입력했다면 시도 횟수를 1 증가시키고 Strike / Ball 판정을 진행한다.
이처럼 입력 검사를 통과한 경우에만 게임 진행에 영향을 주도록 처리하는 순서가 중요했다.
서버에서 생성한 정답과 플레이어가 입력한 숫자를 비교하여 결과를 계산한다.
| 판정 | 조건 |
|---|---|
| Strike | 숫자와 위치가 모두 일치 |
| Ball | 숫자는 일치하지만 위치가 다름 |
| Out | 일치하는 숫자가 전혀 없음 |
ex) 정답이 386일 때 다음과 같이 판정된다.
| 플레이어 입력 | 판정 결과 | 이유 |
|---|---|---|
386 | 3S0B | 세 자리 모두 일치 |
396 | 2S0B | 첫 번째와 세 번째 자리 일치 |
638 | 0S3B | 세 숫자가 모두 포함되지만 위치가 다름 |
124 | OUT | 일치하는 숫자가 없음 |
Strike는 숫자와 위치가 모두 일치하는 경우다.
정답과 입력값을 같은 인덱스끼리 비교하여 판정할 수 있다.
if (Secret[i] == Guess[i])
{
StrikeCount++;
}
Secret[i]는 정답 문자열의 i번째 문자이고, Guess[i]는 플레이어가 입력한 문자열의 i번째 문자다.
두 문자가 같다면 해당 위치의 숫자가 일치한다는 의미이므로 StrikeCount를 증가시킨다.
여기서 ++는 변수의 값을 1 증가시키는 연산자다.
Ball은 숫자가 정답에 포함되어 있지만 위치가 다른 경우다.
따라서 같은 위치의 숫자가 일치하지 않았을 때, 해당 숫자가 정답 문자열의 다른 위치에 존재하는지 확인해야 한다.
else
{
if (Secret.Contains(GuessChar))
{
BallCount++;
}
}
Contains()는 문자열에 특정 문자가 포함되어 있는지 확인하는 함수다.
Strike 판정 이후 else에서 Ball을 검사하면 같은 숫자가 Strike와 Ball로 중복 계산되는 상황을 방지할 수 있다.
Strike는 위치까지 일치해야 하고, Ball은 위치가 달라도 숫자가 포함되어 있으면 인정된다는 차이가 있다.
정답과 입력값을 모두 비교한 후 Strike와 Ball이 모두 0이라면 Out으로 처리한다.
if (StrikeCount == 0 && BallCount == 0)
{
return TEXT("OUT");
}
&&는 두 조건이 모두 참일 때만 참이 되는 AND 연산자다.
따라서 Strike가 0이고 Ball도 0인 경우에만 OUT을 반환한다.
그 외에는 Strike와 Ball의 개수를 조합하여 결과를 표시한다.
return FString::Printf(
TEXT("%dS%dB"),
StrikeCount,
BallCount
);
FString::Printf()는 지정한 형식에 따라 문자열을 생성하는 함수다.
%d에는 정수형 값이 들어가므로, 판정 결과를 1S1B, 2S0B와 같은 형태로 반환할 수 있다.
이번 숫자 야구 게임에서는 각 플레이어에게 최대 3번의 유효한 입력 기회가 주어진다.
시도 횟수는 PlayerState를 활용하여 플레이어별로 관리했다.
| 상황 | 시도 횟수 |
|---|---|
| 게임 시작 | 0 / 3 |
| 정상적인 숫자 입력 | 1 증가 |
| 잘못된 숫자 입력 | 변화 없음 |
| 3회 입력 완료 | 추가 입력 제한 |
| 게임 초기화 | 0 / 3으로 복구 |
두 플레이어의 입력 횟수는 서로 독립적으로 관리되어야 한다.
ex) Player A가 2번 입력하고 Player B가 1번 입력했다면 각각 2/3, 1/3으로 표시되어야 한다.
멀티플레이 환경에서는 각 플레이어가 자신의 시도 횟수를 임의로 변경하지 않도록 서버에서 상태를 관리해야 한다.
이번 프로젝트에서는 PlayerState의 시도 횟수 데이터를 Replication을 통해 동기화하고, 각 플레이어의 UI에 반영하도록 구성했다.
[클라이언트 숫자 입력]
|
v
Server RPC
|
v
[서버 입력 검사]
|
유효한 입력인가?
|
v
시도 횟수 증가
|
v
[PlayerState 갱신]
|
Replication
|
v
[클라이언트 UI]
이를 통해 서버에서 처리한 결과를 클라이언트 화면에서도 확인할 수 있도록 했다.
숫자 생성과 판정 로직을 연결한 뒤, Listen Server 환경에서 두 플레이어의 입력을 테스트했다.
특히 잘못된 입력에 대한 처리와 플레이어별 시도 횟수 제한이 정상적으로 동작하는지 확인했다.
실제 시연에서는 정상 입력과 잘못된 입력을 구분하여 처리하는 과정, 3 Strike 승리, 무승부 및 추가 입력 제한을 확인했다.
이를 통해 입력값 검증부터 숫자 판정, 시도 횟수 관리까지 이어지는 숫자 야구의 핵심 게임 진행 로직을 구성할 수 있었다.
Q1. TArray는 무엇인가?
언리얼 엔진에서 제공하는 동적 배열로, 여러 데이터를 순서대로 저장하고 관리할 수 있다.
Q2. 정답 숫자가 중복되지 않도록 하려면?
이미 선택한 숫자를 배열에서 제거하는 등의 방식으로 같은 숫자가 다시 선택되지 않도록 처리한다.
Q3. 입력 유효성 검사를 먼저 진행하는 이유는?
잘못된 입력이 숫자 판정으로 전달되거나 시도 횟수를 소진하지 않도록 하기 위해서다.
Q4. if와 else를 활용한 Strike / Ball 판정의 차이는?
같은 위치의 숫자가 일치하면 Strike로 처리하고, 그렇지 않은 경우 정답에 숫자가 포함되어 있는지 검사하여 Ball을 판정한다.
Q5. return false는 무엇을 의미하는가?
반환형이 bool인 함수에서 조건을 만족하지 않는다는 결과를 호출한 곳으로 전달한다.
Q6. &&와 ||의 차이는?
&&는 모든 조건이 참이어야 참이고, ||는 조건 중 하나라도 참이면 참이다.
Q7. PlayerState를 활용한 이유는?
각 플레이어의 시도 횟수를 별도로 관리하고, 필요한 상태를 네트워크 환경에서 동기화하기 위해서다.
[학습 후 느낀 점과 개선하고 싶은 점]
: 숫자 야구 게임의 규칙은 비교적 단순하지만, 실제로 구현하려면 정답 생성부터 입력 검사, 숫자 판정, 시도 횟수 관리까지 여러 조건을 순서대로 처리해야 했다. 특히 잘못된 입력이 들어왔을 때 시도 횟수가 증가하지 않도록 하는 부분처럼, 작은 조건 하나가 전체 게임 진행에 영향을 줄 수 있다는 점을 확인했다.
조건문과 반복문을 실제 문제에 적용하는 연습이 더 필요하다 생각했고
추후에는 다음과 같은 부분을 개선하고 싶다.
이번에는 필수 게임 기능을 완성하는 데 집중했다면, 이후에는 완성된 코드의 동작을 이해하는 것을 넘어 직접 구현할 수 있는 수준으로 복습하고 싶다.