1. 팀 프로젝트 진행
1) 오전
프로젝트 마무리 작업, 각자 프로젝트 진행 애로사항과 느낀점 공유.
2) 오후
디버그, 플레이 테스트(오류 수정), 플레이 영상 녹화(발표 자료), 녹화 영상 편집, 설명의 편의성을 위한 자막 삽입, 타임라인 별 자막 삽입.
최종 완성본 게임 시연 영상 : 돈조의 모험 (https://www.youtube.com/watch?v=gbZjm35-xMc)
*오늘의 코드카타*
전화번호가 문자열 phone_number로 주어졌을 때, 전화번호의 뒷 4자리를 제외한 나머지 숫자들을 전부 *으로 가린 문자열을 리턴하는 함수, solution을 완성해주세요.
--> 문자열이 주어지므로, 문자열의 뒤 4자리만 남기고 전부 '*'로 바꿔주기만 하면 된다.
#include <string>
#include <vector>
using namespace std;
string solution(string phone_number) {
string answer = phone_number;
for (int i = 0; i < answer.length() - 4; i++) {
answer[i] = '*';
}
return answer;
}
또는, 이렇게 문자열을 '바꿔줄' 수도 있다.(replace). 이 경우 i에서 1개, 즉 해당하는 문자를 "*"로 바꿔준다.
#include <string>
#include <vector>
using namespace std;
string solution(string phone_number) {
string answer = phone_number;
for (int i = 0; i < answer.size() - 4; i++) {
answer.replace(i, 1, "*");
}
return answer;
}
2. 그 외
[프로그래머스]
개미군단
#include <string>
#include <vector>
using namespace std;
int solution(int hp) {
int answer = 0;
int GA = hp / 5;
int BA = (hp-GA*5) / 3;
int ZA = hp - GA*5 - BA*3;
answer = GA + BA + ZA;
return answer;
}
이렇게 해도 되지만, 다르게 풀어보자.
#include <string>
#include <vector>
using namespace std;
int solution(int hp) {
int answer = 0;
answer += hp / 5;
hp = hp % 5;
answer += hp / 3;
hp = hp % 3;
answer += hp;
return answer;
}
나머지 개념을 이용해서 푸는 방법도 있다. 어차피 최대한 적은 숫자의 개미를 데려가려면 5로 나눈 몫이 최대한 커야 하기 때문이다.
모스부호(1)
머쓱이는 친구에게 모스부호를 이용한 편지를 받았습니다. 그냥은 읽을 수 없어 이를 해독하는 프로그램을 만들려고 합니다. 문자열 letter가 매개변수로 주어질 때, letter를 영어 소문자로 바꾼 문자열을 return 하도록 solution 함수를 완성해보세요.
모스부호는 다음과 같습니다.
morse = {
'.-':'a','-...':'b','-.-.':'c','-..':'d','.':'e','..-.':'f',
'--.':'g','....':'h','..':'i','.---':'j','-.-':'k','.-..':'l',
'--':'m','-.':'n','---':'o','.--.':'p','--.-':'q','.-.':'r',
'...':'s','-':'t','..-':'u','...-':'v','.--':'w','-..-':'x',
'-.--':'y','--..':'z'
}
제한사항
- 1 ≤ letter의 길이 ≤ 1,000
- return값은 소문자입니다.
- letter의 모스부호는 공백으로 나누어져 있습니다.
- letter에 공백은 연속으로 두 개 이상 존재하지 않습니다.
- 해독할 수 없는 편지는 주어지지 않습니다.
- 편지의 시작과 끝에는 공백이 없습니다.
처음에 감을 못잡았다. 우선 띄어쓰기를 기준으로 모스부호를 구분해야 하는데, 모스부호를 배열에 넣어두고 띄어쓰기가 나오기 전까지 모스부호를 가져온 다음 배열에서 같은 값을 찾아서 반환하는 방식을 생각했다.
#include <string>
#include <vector>
#include <sstream>
using namespace std;
string solution(string letter) {
string answer = "";
string morse_array[26] = {
".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---",
"-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-",
"..-", "...-", ".--", "-..-", "-.--", "--.."
};
stringstream ss(letter);
string word;
while (ss >> word) {
for (int i = 0; i < 26; i++) {
if (morse_array[i] == word) {
answer += (char)('a' + i);
break;
}
}
}
return answer;
}
더 효율적인 방법은 맵을 이용하는 것이다. 어차피 모스부호와 대응되는 알파벳 값은 1대 1로 정해져 있는 상태다. map 자료구조를 활용해서 띄어쓰기를 기준으로 키값만 찾아서 값을 반환하면 된다. 띄어쓰기는 <sstream>의 stringstream ss(); 로 띄어쓰기 기준 구분이 가능하다(엑셀 파일로 데이터 관리할 때 많이 쓰임)
#include <string>
#include <vector>
#include <map>
#include <sstream>
using namespace std;
map<string,char> m = {
{".-",'a'},{"-...",'b'},{"-.-.",'c'},{"-..",'d'},
{".",'e'},{"..-.",'f'},{"--.",'g'},{"....",'h'},
{"..",'i'},{".---",'j'},{"-.-",'k'},{".-..",'l'},
{"--",'m'},{"-.",'n'},{"---",'o'},{".--.",'p'},
{"--.-",'q'},{".-.",'r'},{"...",'s'},{"-",'t'},
{"..-",'u'},{"...-",'v'},{".--",'w'},{"-..-",'x'},
{"-.--",'y'},{"--..",'z'}
};
string solution(string letter) {
string answer = "";
stringstream ss(letter);
string s;
while (!ss.eof()) {
ss >> s;
auto t = m.find(s);
if(t != m.end()) {
answer += t->second;
}
}
return answer;
}
가위바위보
#include <string>
#include <vector>
using namespace std;
string solution(string rsp) {
string answer = "";
for (int i = 0; i < rsp.length(); i++) {
if (rsp[i] == '0') {
answer += '5';
}
else if (rsp[i] == '2') {
answer += '0';
}
else {
answer += '2';
}
}
return answer;
}
자주 사용하는 switch case 문으로도 구현할 수 있다.
#include <string>
#include <vector>
using namespace std;
string solution(string rsp) {
string answer = "";
for(const auto v : rsp)
{
switch(v)
{
case '2':
{
answer += '0';
break;
}
case '0':
{
answer += '5';
break;
}
case '5':
{
answer += '2';
break;
}
}
}
return answer;
}
구슬을 나누는 경우의 수
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int solution(int balls, int share) {
long long answer = 1;
share = min(share, balls - share);
for (int i = 0; i < share; i++) {
answer *= (balls - i);
answer /= (i + 1);
}
return answer;
}
서로 다른 n개 중 m개를 뽑는 경우의 수 공식은 다음과 같습니다.

for 반복문과, 서로 다른 n개 중 m개를 뽑는 경우의 수 공식을 활용하면 코드를 작성할 수 있다. 다만, 여기에는 재귀적 사고방식인 파스칼의 삼각형을 이용해서 풀 수 있다(동적 계획법 -DP). 이를 활용하면 곱셈조차 사용하지 않고 오직 덧셈만으로 조합을 구할 수 있다.
#include <vector>
using namespace std;
long long solution(int balls, int share) {
// 31 x 31 크기의 2차원 배열 선언 및 0으로 초기화
long long dp[31][31] = {0, };
for (int i = 1; i <= balls; i++) {
dp[i][0] = 1; // i개 중 0개를 고르는 경우는 1가지
dp[i][i] = 1; // i개 중 i개를 전부 고르는 경우도 1가지
for (int j = 1; j < i; j++) {
// 바로 윗줄의 두 값을 더해 현재 값을 구함
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
}
}
return dp[balls][share];
}
2차원 배열 dp를 만들어서 값을 쌓아 올린다. dp[i][j]는 i개의 구슬 중 j개를 고르는 경우의 수를 의미한다.
구슬이 30개 이므로 0~30개까지 고르는 경우의 수를 다루기 때문에 31*31의 배열을 미리 초기화시켜둔다. 이렇게 하면 메모리 안전성을 확보할 수 있고 문법적인 편리함도 챙길 수 있다.
C++에서는 배열을 초기화할 때 괄호 {} 안에 값을 지정한다. 이 때, 배열의 크기보다 적은 개수의 값을 지정하면 명시된 값들은 순서대로 들어가고 나머지 빈공간은 모두 0으로 자동 초기화 된다. 즉, {0, } 이렇게 작성하면, "첫번째 원소는 0으로 채우고, 나머지 960(31*31-1)의 원소도 전부 기본값인 0으로 채우라는 명령을 한 것과 같다.
파스칼의 삼각형은 바로 윗줄의 값을 더해서 현재 값을 구하는 것이다. 이를 dp[i][j] = dp[i-1][j-1] + dp[i-1][j]; 로 나타낼 수 있다. 이 때, 만약 배열의 빈 공간이 0으로 비워져 있지 않다면 범위를 벗어난 값을 더할 때 쓰레기 값이 더해져 오버플로우가 발생할 수도 있다.
점의 위치 구하기
#include <string>
#include <vector>
using namespace std;
int solution(vector<int> dot) {
int answer = 0;
if (dot[0] > 0 && dot[1] > 0) {
answer = 1;
}
else if (dot[0] < 0 && dot[1] > 0) {
answer = 2;
}
else if (dot[0] < 0 && dot[1] < 0) {
answer = 3;
}
else {
answer = 4;
}
return answer;
}
if문이면 삼항 연산자로도 할 수 있다. 삼항 연산자를 두번 겹쳐 쓰는 것이다.
#include <string>
#include <vector>
using namespace std;
int solution(vector<int> dot) {
return dot[0] > 0 ? (dot[1] > 0 ? 1 : 4) : (dot[1] < 0 ? 3 : 2);
}
2차원으로 만들기
처음에는 <queue>를 이용해서 push_back과 pop을 조합해서 구현할 생각을 했다.
#include <string>
#include <vector>
#include <queue>
using namespace std;
vector<vector<int>> solution(vector<int> num_list, int n) {
vector<vector<int>> answer;
queue<int> q;
for (int num : num_list) {
q.push(num);
}
while (!q.empty()) {
vector<int> row;
for (int i = 0; i < n; ++i) {
row.push_back(q.front());
q.pop();
}
answer.push_back(row);
}
return answer;
}
queue의 pop()은 앞에서부터 원소가 하나씩 삭제되므로, for 반복문과 조합해서 맨 앞에서 n번 반복 호출해서 밀어넣고 기존의 맨 앞 원소는 삭제하는 방식을 이용했다. 문제는 반복문이 많다보니까 원소가 많아질 수록 메모리를 너무 많이 잡아먹는다.
#include <vector>
using namespace std;
vector<vector<int>> solution(vector<int> num_list, int n) {
vector<vector<int>> answer;
for (int i = 0; i < num_list.size(); i += n) {
vector<int> row;
for (int j = 0; j < n; ++j) {
row.push_back(num_list[i+j]);
}
answer.push_back(row);
}
return answer;
}
아이디어를 좀 외워두고 싶은데, i를 n씩 증가시키며 각 행의 시작 인덱스를 잡는 것이다. 이렇게 되면 예를 들어 n = 3 일 때 i = 0, i = 3, i = 6 ...이런 식으로 각 배열의 시작 인덱스가 잡히게 된다. 그리고, 그 아래 for문에서 j = 0 ~ n까지 row 벡터에 차례대로 밀어넣는다.
핵심은 n에 따라서 시작 인덱스도 n씩 증가한다는 것이고, 각 배열의 원소 개수도 n개라는 것이다.
공 던지기
queue 문제로 유명한 요세푸스 문제를 응용한 형태의 문제였다. 따라서 처음에는 queue 구조를 활용해서 문제를 풀었다.
핵심은 K번째 던지는 사람의 번호를 구하는 것이므로 k-1번 공을 던진 후 도착하는 인덱스의 번호를 구해야 하고, 공을 던진 후에 그 다음 사람도 다시 뒤로 보내야 한다는 것을 잊으면 안된다.
#include <string>
#include <vector>
#include <queue>
using namespace std;
int solution(vector<int> numbers, int k) {
queue<int> q;
for (int num : numbers) {
q.push(num);
}
for (int i = 0; i < k - 1; ++i) {
q.push(q.front());
q.pop();
q.push(q.front());
q.pop();
}
return q.front();
}
사실 문제 유형을 외워서 푼 케이스이긴 하다. 비슷한 유형이 나오면 대처가 가능하겠으나 조금이라도 응용되면 풀이가 어려울 수도 있겠다는 생각을 했다. 조금 수학적인 사고방식을 곁들이기 위해 조금 더 나은 풀이를 찾아보고, 다음과 같이 작성하는 연습도 해보았다.
#include <string>
#include <vector>
using namespace std;
int solution(vector<int> numbers, int k) {
int index = (2 * (k - 1)) % numbers.size();
return numbers[index];
}
수포자가 번호를 패턴화해서 찍는 것과 비슷한 원리이다. 배열의 끝에 도달했을 때 다시 처음으로 돌아와야 하므로 수학의 나머지 연산을 활용하면 간단하게 풀 수 있다. 즉, 나머지 값이 곧 다시 처음으로 돌아온 후 도달하는 인덱스 값이 된다.
k-1번 던지고 난 후의 인덱스 값은 2 * (k - 1) 이다. 이를 numbers.size()로 나눈 나머지 값은 곧 처음으로 돌아온 후 도달하는 인덱스 값이 된다. 해당 인덱스 값이 k번째 던지는 사람이 된다.
*배열 회전시키기*
#include <string>
#include <vector>
using namespace std;
vector<int> solution(vector<int> numbers, string direction) {
int n = numbers.size();
if (direction == "right") {
int temp = numbers[n-1];
for (int i = n-2; i >= 0; i--) {
numbers[i+1] = numbers[i];
}
numbers[0] = temp;
}
else if (direction == "left") {
int temp = numbers[0];
for (int i = 0; i < n-1; i++) {
numbers[i] = numbers[i+1];
}
numbers[n-1] = temp;
}
return numbers;
}
배열의 원소를 하나씩 옮겨서 붙여넣고 맨 마지막, 맨 앞 원소만 다시 갖다 붙이는 방식으로 구현해 보았다.
이렇게 배열안의 원소를 하나씩 옮길 수도 있지만...
C++ 라이브러리에서 제공하는 기능을 사용하면 훨씬 성능이 뛰어나게 구현할 수 있다. 원리는 간단하다. 맨 앞의 원소를 맨 끝으로 옮기면 왼쪽으로 회전하는 것이고, 맨 뒤의 원소를 맨 앞으로 옮기면 오른쪽으로 회전하는 것이다.
이때 pop_back, erase, insert, push_back 을 사용한다.
참고로 배열의 맨 마지막 원소를 뽑아오는 것은 numbers.back(), 맨 앞의 원소를 뽑아오는 것은 numbers.front() 로 가능하다.
#include <string>
#include <vector>
using namespace std;
vector<int> solution(vector<int> numbers, string direction) {
if (direction == "right") {
int last = numbers.back();
numbers.pop_back();
numbers.insert(numbers.begin(), last);
}
else if (direction == "left") {
int first = numbers.front();
numbers.erase(numbers.begin());
numbers.push_back(first);
}
return numbers;
}
세번째 풀이 방법으로, 논리적 인덱스를 이용한 방법으로도 풀 수 있다. 배열의 원소를 실제로 뺐다 넣었다 하는 대신에, 어디서부터 읽을지만 결정해줘서 새로운 배열을 만들어내는 방식이다.
#include <string>
#include <vector>
using namespace std;
vector<int> solution(vector<int> numbers, string direction) {
vector<int> answer;
int n = numbers.size();
int start_idx = 0;
if (direction == "right") {
start_idx = n - 1;
} else if (direction == "left") {
start_idx = 1;
}
for (int i = 0; i < n; i++) {
answer.push_back(numbers[(start_idx + i) % n]);
}
return answer;
}
오른쪽으로 회전하면 시작 인덱스를 n-1로 설정한다. 즉, 맨 마지막 원소가 맨 앞이 된다. 왼쪽으로 회전하면 시작 인덱스를 1로 설정한다. 즉, 두 번째 원소가 맨 앞이 된다.
이후에 for 문 안에서 반복문과 %(나머지) 연산자를 활용해서 인덱스가 배열 끝을 넘어가면 다시 0으로 돌아오게한다. n번 반복하면 반복문이 종료 되므로 n개의 원소가 answer 배열 안에 들어가게 된다.
논리적 인덱스 방식은 데이터를 옮기는 물리적인 연산이 아예 발생하지 않기 때문에 성능면에서 가장 뛰어나다.
네번째 풀이 방법으로, 덱(deque)을 이용한 방법으로도 풀 수 있다.
<deque>을 사용하게 되면 pop_front() 와 push_front()를 자유자재로 쓸 수 있다.
#include <string>
#include <vector>
#include <deque>
using namespace std;
vector<int> solution(vector<int> numbers, string direction) {
deque<int> dq(numbers.begin(), numbers.end());
if (direction == "right") {
dq.push_front(dq.back());
dq.pop_back();
}
else if (direction == "left") {
dq.push_back(dq.front());
dq.pop_front();
}
return vector<int>(dq.begin(), dq.end());
}
numbers 배열의 처음부터 끝까지 원소들을 모두 dq 덱에 넣고, 조건문에 따라서 push_front + pop_back, push_back + pop_front를 사용해서 같은 기능을 구현할 수있다. 마지막에 dq 덱을 다시 vector<int> 형태의 배열로 돌려놓는 것도 잊지 말자.
덱을 사용한 방식은 코드를 읽고 이해하기에 굉장히 직관적이라는 장점이 있다. 다만 deque로 변환하고 다시 vector로 변환하는 과정에서 미세한 성능차이가 발생할 수 있다.
[atanny]
1. UMG 위젯 구조와 데이터 바인딩
UMG(Unreal Motion Graphics)란? :
언리얼 엔진에서 제공하는 기본 사용자 인터페이스 및 HUD(Head-Up Display) 제작 시스템으로 게임 플레이 화면에 띄워지는 체력바, 크로스헤어, 인벤토리 창부터 메인 화면의 시작 메뉴나 환경 설정 창, 상호작용하는 거의 모든 시각적 요소를 UMG를 통해 만든다. UMG의 핵심은 위젯 블루프린트이고, 타임라인 기반의 애니메이션 툴이 내장되어있어 동적인 효과를 쉽게 연출할 수 있다.
UMG 위젯의 계층 구조에서 최상위 루트 요소 :
Canvas Panel은 가장 많이 최상위 루트 요소로 사용되는 패널 위젯으로 자식 위젯들을 자유롭게 배치할 수 있고 절대 좌표와 앵커를 통해 위치를 지정할 수 있다.
데이터 바인딩을 사용하여 HUD에 플레이어의 체력을 실시간으로 표시하기:
Text Block의 Text속성에 바인딩 함수를 연결하여 체력 값을 반환하거나, Progress Bar의 Percent 속성에 바인딩 함수를 연결하여 0.0~1.0 사이의 체력 비율을 반환하거나, 위젯 블루프린트에서 Get Player Character 노드를 통해 캐릭터의 체력 변수에 접근한다.
UMG 위젯 블루프린트에서 바인딩 함수를 생성할 때 지켜야 할 규칙:
바인딩 함수의 반환 타입은 바인딩할 속성의 데이터 타입과 반드시 일치해야 한다. 예를 들어 Text 속성에 바인딩하면 Text타입을 반환하고, percent 속성에 바인딩한다면 Float 타입을 반환해야 한다.
2. 애니메이션 블루프린트와 캐릭터 애니메이션 처리
언리얼 엔진에서의 Animation Blueprint의 주요 역할 :
애니메이션 상태 머신을 구성하여 애니메이션 전환을 관리하고, 블렌드 스페이스를 활용하여 여러 애니메이션을 혼합하고, 캐릭터의 현재 상태에 따라 적절한 애니메이션을 선택하고 재생한다.
Animation Blueprint의 EventGraphh에서 캐릭터의 이동 속도를 계산하여 저장하는 과정 :
캐릭터의 이동속도는 Try Get Pawn Owner로 캐릭터를 가져온 후, Get Velocity(속도) 노드로 속도 벡터를 얻고, Vector Length 노드로 벡터의 크기(스칼라 값)을 계산하여 Speed 같은 변수에 저장하는 것이 일반적인 방법이다. Speed 변수의 경우 이후에 AnimGraph에서 애니메이션 블렌딩이나 상태 전환 조건으로 사용된다.
Animation Blueprint의 두 가지 주요 그래프 :
EventGraph는 이벤트에서 캐릭터의 상태를 읽어와 Speed, IsInAir 같은 변수를 업데이트 한다. AnimGraph는 이 변수들을 사용하여 State Machine, Blend Space 등을 통해 최종 애니메이션 포즈를 계산하고 출력한다.
State Machine에서 상태 전환(Transition)의 조건 설정하기 :
State Machine의 Transition Rule 에서는 EventGraph에서 업데이트된 변수들을 조건식으로 사용하여 상태 전환 여부를 결정한다. 예를 들어, Boolean, Float 변수를 비교하는 조건식을 사용하는 형태이다.
3. 파티클 시스템과 사운드 컴포넌트 이해
언리얼 엔진의 파티클 시스템(Particle System) :
파티클 시스템은 이미터(Emitter)를 통해 파티클을 생성하고 관리하며 불, 연기, 비, 눈과 같은 시각 효과를 구현하는 데 사용된다. Cascade또는 Niagara에디터를 사용하여 제작할 수 있으며, 런타임 중에도 블루프린트나 C++코드를 통해 속성을 동적으로 변경할 수 있다. 예를 들어 파티클의 색상, 크기, 속도, 발생률 등을 게임 진행 상황에 따라 실시간으로 조정할 수 있다.
언리얼 엔진에서 사운드를 재생하기 위해 액터에 추가하는 컴포넌트 :
Audio Component이다. 언리얼 엔진에서는 Audio Component를 액터에 추가하여 사운드를 재생한다. Audio Component는 Sound Wave나 Sound Cue 같은 사운드 애셋을 참조하여 3D 공간 상에서 사운드를 재생할 수 있다. 볼륨, 피치, 감쇠(Attenuation) 등의 속성을 제어할 수도 있다.
파티클 시스템의 이미터(Emitter)에서 설정할 수 있는 모듈의 역할 :
파티클 이미터의 모듈은 파티클의 생명 주기 전반에 걸쳐 다양한 속성을 제어한다.
Spawn 모듈은 파티클 생성률을 제어하고, Lifetime 모듈은 수명을 제어하고, Velocity 모듈은 속도를 제어하고, Color Over Life 모듈은 색상 변화를 제어하는 등 파티클의 동작, 외형, 물리적 특성을 세밀하게 조정할 수 있다.
[C++ 공부]
코드를 작성하다보면 여러 조건식을 같이 써야하는 경우가 빈번하다. 특히 && 와 || 를 쓰다보면 가독성도 점점 떨어지고 나중에 버그가 발생했을 때 디버깅하기에도 골치아프다. 지금이야 크게 어려운 조건식을 사용하지 않겠지만 프로젝트 규모가 커질 수록 이러한 문제가 발생하는 것을 미리 대비하는 것이 좋다.
&& && || 어쩌구저쩌구 쓰지 말고 이렇게 가독성 있게 정리해보자.
1. &= (And 대입 연산자) : A &= B; 는 A = A & B; 와 완벽하게 같다. A와 B가 둘다 참일때 A가 True로 유지된다.
2. |= (Or 대입 연산자) : A |= B;는 A = A | B; 와 완벽하게 같다. A와 B 중 하나라도 참이면 A가 True가 된다.
예를 들어,
달리기 15초 이하, 팔굽혀펴기 30개 이상, 턱걸이 10개 이상의 조건을 만족해야 체력장을 통과할 수 있다고 해보자.
추가로 이미 국가대표라면 필요 없고 최종 합격 처리가 된다.
이제 if문을 나열하는 것과 연산자를 사용하는 것을 비교해보자.
if문을 연달아서 발사해보면 왜 이러한 연산자들을 사용하는 것이 좋은지를 알 수 있다.
#include <iostream>
int main() {
int runTime = 14;
int pushUps = 25;
int pullUps = 12;
bool isNationalAthlete = true;
bool isPassed = true;
if (runTime > 15) {
isPassed = false;
}
if (pushUps < 30) {
isPassed = false;
}
if (pullUps < 10) {
isPassed = false;
}
if (isNationalAthlete) {
isPassed = true;
}
if (isPassed) {
std::cout << "최종 합격.\n";
} else {
std::cout << "불합격.\n";
}
return 0;
}
직관적이기는 한데, 코드가 세로로 많이 길어진다.
그래서 보통 !(Not) 연산자를 활용해서 간결하게 만들곤 한다.
#include <iostream>
int main() {
int runTime = 14;
int pushUps = 25;
int pullUps = 12;
bool isNationalAthlete = true;
bool isPassed = true;
if (!(runTime <= 15)) isPassed = false;
if (!(pushUps >= 30)) isPassed = false;
if (!(pullUps >= 10)) isPassed = false;
if (isNationalAthlete) isPassed = true;
if (isPassed) {
std::cout << "최종 합격.\n";
} else {
std::cout << "불합격.\n";
}
return 0;
}
!를 사용하기 보다 코드가 더 읽기 쉽고 직관적인 &=, |= 연산자를 사용하는 방식을 구현해보자.
#include <iostream>
int main() {
int runTime = 14;
int pushUps = 25;
int pullUps = 12;
bool isNationalAthlete = true;
bool isPassed = true;
isPassed &= (runTime <= 15);
isPassed &= (pushUps >= 30);
isPassed &= (pullUps >= 10);
isPassed |= isNationalAthlete;
if (isPassed) {
std::cout << "최종 합격.\n";
} else {
std::cout << "불합격.\n";
}
return 0;
}
코드길이도 그렇고, 가독성도 그렇고, 유지보수 측면에서도 더 효율적이라고 할 수 있다.
뭐가 다른건지, 사실 잘 체감이 안된다. 그래서 더 어려운 조건을 찾아보기로 했다.
예를 들어서, 스킬 사용 조건 검사를 해본다고 하자.
다음과 같은 조건들이 있다.
1. 마나가 충분한 상태
2. 스킬 쿨타임이 다 돌아야 함
3. 플레이어가 기절 혹은 침묵 상태가 아니어야 함
4. 타겟이 존재하고, 타겟이 죽은 상태가 아니어야 함
5. 타겟이 적이거나 중립 몬스터여야 함
6. 타겟과의 거리가 스킬 사거리 이내여야 함.
if문과 !를 활용해서 최대한 조건문을 때려 넣으면 다음과 같이 작성된다.
if (currentMana >= skillManaCost &&
cooldownTimer <= 0.0f &&
!(isStunned || isSilenced) &&
target != nullptr &&
target->state != STATE_DEAD &&
(target->type == TYPE_ENEMY || target->type == TYPE_NEUTRAL) &&
distance <= skillRange)
{
Fireball->Cast(target);
currentMana -= skillManaCost;
cooldownTimer = defaultCooldown;
} else {
PlayErrorSound();
}
로직이 잘 동작한다고 쳐도, 나중에 만약 에러가 나는 경우 어디에서 에러가 나는지 찾기가 어렵다. 어떤 조건에 문제가 생겨서 스킬이 나가지 않게 되는지 알기가 어려운 것이다. 게다가 스킬 시전에 실패했을 경우 출력되는 UI메시지도 알맞게 띄워주기가 어렵게 된다.
그래서 실무에서는 이렇게 복잡한 조건을 한번에 검사하지 않고 조건을 통과하지 못하면 함수를 즉시 종료(return)시켜버리는 패턴(Guard Clause)를 주로 사용한다.
void CastFireball(Character* target, float distance) {
// 1. 자원 및 쿨타임 검사
if (currentMana < skillManaCost) {
ShowUI_Message("마나가 부족합니다.");
return; // 실패했으니 여기서 함수 즉시 종료
}
if (cooldownTimer > 0.0f) {
ShowUI_Message("아직 스킬을 사용할 수 없습니다.");
return;
}
// 2. 플레이어 상태 검사 (기절이거나 침묵이면 불가)
if (isStunned || isSilenced) {
ShowUI_Message("상태 이상 중에는 스킬을 쓸 수 없습니다.");
return;
}
// 3. 타겟 유효성 검사
if (target == nullptr || target->state == STATE_DEAD) {
ShowUI_Message("잘못된 타겟입니다.");
return;
}
// 4. 타겟 적대성 검사 (!= 를 써서 아군이면 거름)
if (target->type != TYPE_ENEMY && target->type != TYPE_NEUTRAL) {
ShowUI_Message("공격할 수 없는 대상입니다.");
return;
}
// 5. 거리 검사
if (distance > skillRange) {
ShowUI_Message("대상이 너무 멉니다.");
return;
}
// 파이어볼 발사 로직 실행
Instantiate(FireballPrefab, this->position);
currentMana -= skillManaCost;
cooldownTimer = defaultCooldown;
}
조건이 많아질수록 혹시 발생할 수 있는 에러와 여러 기능들을 위해서 하나하나의 기능마다 미리 실패 시 return을 시켜버리는 것이다.
&=, |= 연산자의 경우 다음과 같이 활용해볼 수 있다.
if (isStunned || isSilenced || isFrozen || isKnockedBack) {
ShowUI_Message("상태 이상 중에는 스킬을 쓸 수 없습니다.");
return;
}
여러 상태 이상이 겹칠 때, 이렇게 쓰기 보다는
bool isCrowdControlled = false;
isCrowdControlled |= isStunned;
isCrowdControlled |= isSilenced;
isCrowdControlled |= isFrozen;
isCrowdControlled |= isKnockedBack;
// 상태 이상 중 하나라도 걸렸다면 빠른 종료
if (isCrowdControlled) {
ShowUI_Message("상태 이상 중에는 스킬을 쓸 수 없습니다.");
return;
}
이런식으로 &=, |= 연산자와 Early Return을 결합해서 가독성을 높이는 방법도 있다.
다만 &=, |= 연산자를 사용할 때 Crash가 발생할 수 있으므로, 사용법을잘 알아두어야 한다.
예를 들어, if (target == nullptr || target->hp <= 0) { return; } 의 경우 앞 조건에서 이미 false가 확정되므로 뒤의 조건은 검사하지 않는다. 그런데,
bool isValidTarget = true;
isValidTarget &= (target != nullptr);
isValidTarget &= (target->hp > 0);
이렇게 &= 연산자를 사용해서 작성하면, 앞의 조건과 뒤의 조건을 전부다 검사하게 된다. 따라서, nullptr인 상황에서 hp 검사를 수행하게 되면서 크래시가 발생한다.
이처럼 객체의 생존 여부(Null 을 사용)에 의존하는 연속적인 조건 검사에서는 사용할 수 없다.
결론은, Early Return과 &=, |= 연산자를 잘 섞어써야 한다.
상태값이나 플래그(Flag)를 묶을 때, |=, &=을 사용하여 코드를 수직으로 정리한다. 이후 포인터 안전성 체크나 개별 UI 피드백이 필요할 때 if(!조건) return; 방식을 사용한다.
**오늘의 총평**
팀플을 무사히 마무리했고, 발표 준비를 돕고 전반적으로 무난한 하루였다. 마무리작업으로 게임을 시연하는 영상을 찍고 간단하게 편집했다. 남는 시간에는 곧 있을 C++ 테스트를 대비해서 프로그래머스의 기초 문제, 입문 문제를 고민하면서 풀었고, 추가적으로 튜터님이 말씀하신 연산자를 활용한 조건식 사용 방법을 조금 더 깊게 알아보았다.
Atanny를 활용한 학습으로는 언리얼 엔진 에디터에서 사용할 개념들을 일부 배웠다. 위젯과 데이터 바인딩, 애니메이션 블루프린트, 파티클 시스템과 사운드 컴포넌트의 개념과 일부 코드 문법을 학습했다.
'TIL' 카테고리의 다른 글
| 06.01 TIL (이력서 세션 및 커리어 데이) (0) | 2026.06.01 |
|---|---|
| 05.29 TIL (ch.2 팀프로젝트 day6) (0) | 2026.05.29 |
| 05.27 TIL (ch.2 팀프로젝트 day4) (0) | 2026.05.27 |
| 05.26 TIL (ch.2 팀프로젝트 day3) (0) | 2026.05.26 |
| 05.22 TIL (ch.2 팀프로젝트 day2) (0) | 2026.05.22 |