TIL
06.09 TIL
2026. 6. 9. 19:44

일정표

시간 할 일 비고
08:00~10:00 코드카타, Atanny, 코드프로그래머스  
10:00~13:00 언리얼 4번과제  
14:00~18:00 C++ 수업, 언리얼 강의  
19:00~21:00 언리얼 강의 + 오늘의 수학 + TIL  
21:00~22:00 운동  
22:00~23:30 개인 공부(디자인 패턴)  

*오늘의 코드카타*

 

문제. 부족한 금액 계산하기

더보기

새로 생긴 놀이기구는 인기가 매우 많아 줄이 끊이질 않습니다. 이 놀이기구의 원래 이용료는 price원 인데, 놀이기구를 N 번 째 이용한다면 원래 이용료의 N배를 받기로 하였습니다. 즉, 처음 이용료가 100이었다면 2번째에는 200, 3번째에는 300으로 요금이 인상됩니다.
놀이기구를 count번 타게 되면 현재 자신이 가지고 있는 금액에서 얼마가 모자라는지를 return 하도록 solution 함수를 완성하세요.
단, 금액이 부족하지 않으면 0을 return 하세요

money, price, count는 자연수입니다.

using namespace std;

long long solution(int price, int money, int count)
{
    long long answer = 0;
    
    long long standard = 1LL * count * (count + 1) / 2 * price;
    
    if ( money < standard ) {
        answer = standard - money;
    }
    else {
        answer = 0;
    }
    return answer;
}

오버플로우를 조심해야 하는 문제. standard를 계산하는 과정에서 int 자료형으로 변수들을 받기 때문에 우항에서 오버플로우가 발생할 위험이 있다. 따라서, 1LL 을 곱해주어 long long 자료형으로 만든다

 

using namespace std;

long long solution(int price, int money, int count)
{
    long long standard = 1LL * count * (count + 1) / 2 * price;
    return money < standard ? standard - money : 0;
}

삼항연산자를 사용해서 조건문을 한 줄로 작성해보는 연습도 해 보았다. 코드가 간결하고 짧아져서 좋긴했다.


*프로그래머스*

1. [스택/큐] 다리를 지나는 트럭

더보기

문제 설명

트럭 여러 대가 강을 가로지르는 일차선 다리를 정해진 순으로 건너려 합니다. 모든 트럭이 다리를 건너려면 최소 몇 초가 걸리는지 알아내야 합니다. 다리에는 트럭이 최대 bridge_length대 올라갈 수 있으며, 다리는 weight 이하까지의 무게를 견딜 수 있습니다. 단, 다리에 완전히 오르지 않은 트럭의 무게는 무시합니다.

solution 함수의 매개변수로 다리에 올라갈 수 있는 트럭 수 bridge_length, 다리가 견딜 수 있는 무게 weight, 트럭 별 무게 truck_weights가 주어집니다. 이때 모든 트럭이 다리를 건너려면 최소 몇 초가 걸리는지 return 하도록 solution 함수를 완성하세요.

<제한사항>

bridge_length는 1 이상 10,000 이하입니다

weight는 1 이상 10,000 이하입니다

truck_weights의 길이는 1 이상 10,000 이하입니다

모든 트럭의 무게는 1 이상 weight 이하입니다

#include <string>
#include <vector>
#include <queue>

using namespace std;

int solution(int bridge_length, int weight, vector<int> truck_weights) {
    int time = 0;
    int current_weight = 0;
    queue<int> bridge;
    
    for (int i = 0; i < bridge_length; i++) {
        bridge.push(0);
    }
    
    int wait_truck_idx = 0;
    
    while (wait_truck_idx < truck_weights.size()) {
        time++;
        current_weight == bridge.front();
        bridge.pop();
        
        if (current_weight + truck_weights[wait_truck_idx] <= weight) {
            bridge.push(truck_weights[wait_truck_idx]);
            current_weight += truck_weights[wait_truck_idx];
            wait_truck_idx++;
        }
        else 
        {
            bridge.push(0);    
        }
    }
    return time + bridge_length;
}

처음엔 이런식으로 bridge라는 큐를 만들어서, '0' 이라는 빈 값과 트럭이 큐 안에서 앞으로 이동하면서, 조건에 맞다면 새로운 트럭이 큐의 맨 뒤에 들어오고 맨 앞의 트럭이 나가게 되는 구조를 만들었다. 이 때 'time'을 사용해서 트럭이 앞으로 움직이는 것을 구현했는데, 문제는 실행 시간이 너무 길어져서 문제를 풀 수가 없게 되어버렸다. 따라서, 1초 단위로 모든 순간을 시뮬레이션 하지 않고 트럭이 빠져나가는 시간으로 단숨에 건너뛰는 방식으로 최적화를 해야 했다.

#include <vector>
#include <queue>
#include <algorithm>

using namespace std;

int solution(int bridge_length, int weight, vector<int> truck_weights) {
    int time = 0;
    int current_weight = 0;
    queue<pair<int, int>> bridge;
    
    for (int truck : truck_weights) {
        time++;
        
        while (bridge.size() == bridge_length || current_weight + truck > weight) {
            time = max(time, bridge.front().second);
            current_weight -= bridge.front().first;
            bridge.pop();
        }
        bridge.push({truck, time + bridge_length});
        current_weight += truck;
    }
    return bridge.back().second;
}

큐에 '현재 트럭의 무게, 다리를 빠져나갈 시간' 을 짝지어서 넣고, 다리가 꽉 찼거나 새 트럭이 진입하기에는 무게가 초과될 때, 맨 앞의 트럭이 빠져나가는 시간으로 현재 시간을 바로 넘기는 것이다. 이러면 (현재 시간 + 다리 길이)가 새로 진입한 트럭이 빠져나갈 시간이 되고, 최종적으로는 큐의 맨 마지막에 들어간 가장 마지막 트럭이 빠져나가는 시간이 곧 전체 소요 시간이 된다.

 

 

2. [스택/큐] 주식가격

더보기

문제 설명

초 단위로 기록된 주식가격이 담긴 배열 prices가 매개변수로 주어질 때, 가격이 떨어지지 않은 기간은 몇 초인지를 return 하도록 solution 함수를 완성하세요.

<제한사항>

prices의 각 가격은 1 이상 10,000 이하인 자연수입니다.

prices의 길이는 2 이상 100,000 이하입니다.

#include <string>
#include <vector>
#include <stack>

using namespace std;

vector<int> solution(vector<int> prices) {
    int n = prices.size();
    vector<int> answer(n);
    stack<int> s;
    
    for (int i = 0; i < n; i++) {
        while (!s.empty() && prices[s.top()] > prices[i]) {
            answer[s.top()] = i - s.top();
            s.pop();
        }
        s.push(i);
    }
    while (!s.empty()) {
        answer[s.top()] = n - 1 - s.top();
        s.pop();
    }    
    return answer;
}

answer 배열에는 미리 n(prices.size()) 만큼 메모리 공간을 확보해놓고, 스택에는 주식의 가격이 아니라 시간을 저장한다. 여기서 시간은 배열의 인덱스와 같다. 즉, 배열의 인덱스 5번은 '시간 5' 일 때의 주식의 가격과 같다는 소리이다.

이후 가격 하락 여부를 확인한다. 현재 주식 가격이 스택의 맨 위의 가격보다 낮다면 가격이 떨어진 것으로 판단한다.

가격이 떨어졌다면, 떨어진 가격을 계산한다. 스택에서 해당 시간을 빼내고, 현재 시간에서 빼낸 시간을 빼서 결과 배열에 기록하게 된다. 이 과정을 스택이 비거나 가격이 떨어지지 않을 때 까지 반복하면 결과 배열에 최종적으로 떨어진 기간이 남게 된다.

마지막까지 남은 주식들을 처리한다. 모든 순회가 끝난 후에도 스택에 남아있는 시간들은 끝까지 가격이 한 번도 떨어지지 않은 경우이므로, 전체 시간 - 스택에 저장된 시간 - 1 으로 기간을 계산해 준다.

 

3. [기초] 문자 개수 세기

더보기

문제 설명

알파벳 대소문자로만 이루어진 문자열 my_string이 주어질 때, my_string에서 'A'의 개수, my_string에서 'B'의 개수,..., my_string에서 'Z'의 개수, my_string에서 'a'의 개수, my_string에서 'b'의 개수,..., my_string에서 'z'의 개수를 순서대로 담은 길이 52의 정수 배열을 return 하는 solution 함수를 작성해 주세요

#include <string>
#include <vector>

using namespace std;

vector<int> solution(string my_string) {
    vector<int> answer(52, 0);
    for (char c : my_string) {
        if (c >= 'A' && c <= 'Z')
        {
            answer[c-'A']++;
        }
        else if (c >= 'a' && c <= 'z') {
            answer[c - 'a' + 26]++;
        }
    }
    return answer;
}

(1)아스키 코드 특징을 사용해서 그대로 센 경우

 

#include <string>
#include <vector>
#include <cctype>

using namespace std;

vector<int> solution(string my_string) {
    vector<int> answer(52, 0);
    for (char c : my_string) {
        if (isupper(c)) {    
            answer[c - 'A']++;
        }
        else if (islower(c)) {
            answer[c - 'a' + 26]++;
        }
    }
    return answer;
}

(2)<cctype> 헤더의 isupper, islower() 함수를 사용해서 가독성 챙기기 

 

4. [기초] 배열 만들기 1

더보기

문제 설명

정수 n과 k가 주어졌을 때, 1 이상 n이하의 정수 중에서 k의 배수를 오름차순으로 저장한 배열을 return 하는 solution 함수를 완성해 주세요

#include <vector>

using namespace std;

vector<int> solution(int n, int k) {
    vector<int> answer;
    for (int i = k; i <= n; i += k) {
        answer.push_back(i);
    }
    return answer;
}

(1) 기본적인 방법으로 문제 풀기

다음에는 emplace_back(i) 로 개선하자.

 

5. [기초] 글자 지우기

더보기

문제 설명

문자열 my_string과 정수 배열 indices가 주어질 때, my_string에서 indices의 원소에 해당하는 인덱스의 글자를 지우고 이어 붙인 문자열을 return 하는 solution 함수를 작성해 주세요

#include <string>
#include <vector>

using namespace std;

string solution(string my_string, vector<int> indices) {
    string answer = "";
    vector<bool> vec_delete(my_string.length(), false);
    
    for (int idx : indices) {
        vec_delete[idx] = true;
    }
    
    for (int i = 0; i < my_string.length(); i++) {
        if (vec_delete[i] == false) {
            answer += my_string[i];
        }
    }
    return answer;
}

(1) find, erase를 사용하면 시간 복잡도가 너무나 커질 우려가 있어서 indices 배열을 이용, 해당 배열의 숫자 인덱스에 해당하는 문자만 제외하고 answer에 붙이는 방식으로 코드를 작성했다.

 

#include <string>
#include <vector>

using namespace std;

string solution(string my_string, vector<int> indices) {
    string answer = "";
    answer.reserve(my_string.length() - indices.size());
    
    vector<bool> to_delete(my_string.length(), false);
    

    for (int idx : indices) {
        to_delete[idx] = true;
    }
    
    for (int i = 0; i < my_string.length(); i++) {
        if (!to_delete[i]) {
            answer += my_string[i];
        }
    }
    
    return answer;
}

(2) 최적화를 더 할 수 있는 방법은 reserve를 사용해서 메모리를 미리 할당해놓는 것이다. 문자열의 길이에서 내가 지울 문자 개수를 빼기만 해도 미리 할당해 놓을 메모리를 확보해놓을 수 있다.

 

#include <string>
#include <vector>

using namespace std;

string solution(string my_string, vector<int> indices) {
    string answer = "";
    for(const auto v : indices)
    {
        my_string[v] = ' ';
    }

    for(const auto c : my_string)
    {
        if(c != ' ')
        {
            answer += c;
        }
    }

    return answer;
}

(3) 실무에서 '툼스톤 패턴' 이라고 부르는 기법. 삭제할 대상(문자열 내부의 삭제할 문자)에 ' ' 로 마킹을 해놓는다. 마킹을 한 후 my_string을 순회하면서 ' '가 마킹되어있지 않은 문자만 answer에 붙인다.

다만 이 경우는 my_string이 알파벳 대소문자로만 이루어져 있어서 가능한 방법이다. 만약 my_string에 공백이 포함되어있다면 정상적인 공백 문자들까지 if (c != ' ') 조건에 걸려 모두 삭제되어버린다. 

#include <string>
#include <vector>

using namespace std;

string solution(string my_string, vector<int> indices) {
    string answer = "";

    answer.reserve(my_string.length() - indices.size());
    
    for(const auto v : indices) {
        my_string[v] = '\0'; 
    }

    for(const auto c : my_string) {
        if(c != '\0') {
            answer += c;
        }
    }

    return answer;
}

(4) 원본 문자열에 공백이 포함될 수 있다는 조건이 있으면, 절대 입력으로 들어올 일이 없는 특수 문자. 여기에서는 예를 들어 널 문자 '\0' 을 사용해서 안전하게 코드를 작성할 수 있다. 공백 ( ' ' ) 대신에 널 문자로 마킹해서 안전성을 확보하고 마킹되지 않은 문자만 answer에 추가한다.

 

6. [기초] 카운트 다운

더보기

문제 설명

정수 start_num와 end_num가 주어질 때, start_num에서 end_num까지 1씩 감소하는 수들을 차례로 담은 리스트를 return하도록 solution 함수를 완성해주세요.

#include <vector>

using namespace std;

vector<int> solution(int start_num, int end_num) {
    vector<int> answer;
    
    for (int i = start_num; i >= end_num; i--) {
        answer.emplace_back(i);
    }
    return answer;
}

(1) 일반적인 반복문을 사용해서 간단하게 문제를 풀어냈다.

 

#include <vector>
#include <numeric>
#include <algorithm>

using namespace std;

vector<int> solution(int start_num, int end_num) {
    vector<int> answer(start_num - end_num + 1);
    
    iota(answer.begin(), answer.end(), end_num);
    reverse(answer.begin(), answer.end());
    
    return answer;
}

(2) 좀 더 특이하게 풀어보고 싶어서 numeric 헤더의 iota 함수를 사용했다. iota는 연속된 숫자를 채워넣는 함수이다. iota함수를 사용하고 reverse로 배열을 뒤집어버리는 것이다.

 

7. [기초] 가까운 1 찾기

더보기

문제 설명

정수 배열 arr가 주어집니다. 이때 arr의 원소는 1 또는 0입니다. 정수 idx가 주어졌을 때, idx보다 크거나 같으면서 배열의 값이 1인 가장 작은 인덱스를 찾아서 반환하는 solution 함수를 완성해 주세요

단, 만약 그러한 인덱스가 없다면 -1을 반환합니다

arr의 원소는 전부 1 또는 0입니다

#include <vector>

using namespace std;

int solution(vector<int> arr, int idx) {
    for (int i = idx; i < arr.size(); i++) {
        if (arr[i] == 1) {
            return i;
        }
    }
    return -1;
}

#include <vector>

using namespace std;

int solution(vector<int> arr, int idx) {
    int i = idx;
    
    while (i < arr.size()) {
        if (arr[i] == 1) {
            return i;
        }
        i++;
    }
    return -1;
}

(1) 가장 일반적인 for문을 활용한 풀이. 아래에는 while을 써서 풀어보았다.

 

#include <vector>
#include <algorithm>

using namespace std;

int solution(vector<int> arr, int idx) {
    auto it = find(arr.begin() + idx, arr.end(), 1);
    
    if (it != arr.end()) {
        return distance(arr.begin(), it);
    }
    return -1;
}

(2) 다만 조금 더 C++ 스럽게 풀기 위해서 find를 써보기로 했다. idx가 주어지므로 idx에서부터 1을 찾고 반환자 it의 위치(인덱스)를 distance를 사용해서 구한다.


*Atanny*

1. 현실적 범위 설정과 일정 수립

 

게임 개발 프로젝트에서 구현 범위를 현실적으로 설정하기 위해 가장 먼저 고려해야 할 요소 

"팀원들의 기술 수준과 경험"

 

현실적인 개발 일정(개발 단계별 시간 배분) 

핵심 개발에 가장 많은 시간을 할애하고, 프로토타입과 테스트에도 충분한 시간을 확보해야 한다. 기획에 너무 많은 시간을 쓰는 것은 비현실적이다.

 

프로젝트 범위 축소가 필요하지 않은 상황. 

핵심 시스템 구현 지연, 팀원 이탈, 기술적 제약은 모두 가용 리소스나 시간이 줄어든 상황으로 범위 축소를 고려해야 한다.

 

MoSCoW 방법으로 게임 기능의 우선순위 정하기 

Mush have : 반드시 필요한 핵심 기능

Should have : 중요하지만 당장 없어도 출시 가능한 기능

Could have : 여유가 있으면 추가하는 기능

Won't have : 있으면 좋지만 이번에는 추가하지 않는 기능

 

프로젝트를 스프린트 방식으로 진행하기 

애자일 개발 방식으로, 2주 단위의 스프린트가 가장 일반적이며 효과적이다. 각 스프린트마다 플레이 가능한 빌드를 만들면 진행 상황을 지속적으로 확인하고 피드백을 받을 수 있다. 즉, 문제 발견을 제 시간에 해야하고, 의미있는 기능을 구현할 충분한 시간을 확보하는 것이 좋다.

 

프로젝트 일정 수립 시 버퍼 시간을 포함해야 하는 이유 

예상치 못한 문제나 위험 요소에 대비하기 위한 것이다. 기술적 문제, 인력 공백, 학습 시간 등 현실적인 상황에 대비하여 프로젝트 일정을 안정적으로 관리하기 위해 필요하다. 일반적으로 전체 일정의 15~20% 정도를 버퍼로 설정한다.

 

 

2. 게임 기획과 구현의 논리적 설명

 

게임 기획 의도 정의하기

플레이어에게 어떤 경험을 제공하고 무엇을 느끼게 하려는지에 대한 핵심 목표를 명확히 하는 것이 곧 게임 기획 의도이다. 모든 게임 개발의 출발점이 되며, 핵심 기능과 구현 방식을 결정하는 기준이 된다.

 

FPS 게임의 핵심 기능 설계하기

장르의 핵심이 되는 1인칭 시점에서의 이동, 조준, 사격 메커니즘이 가장 먼저 구현되어야 한다. 게임의 기본적인 플레이 경험을 결정하는 핵심 기능이기 때문이다.

 

게임 프로토타입 단계에서 핵심 기능 검증하기 

프로토타입 단계에서는 게임의 기획 의도를 검증하기 위해 가장 핵심이 되는 메커니즘을 먼저 구현하고 빠르게 테스트하는 것이 중요하다. 완벽한 구현이나 그래픽, 스토리는 핵심 기능이 검증된 후에 추가하는 것이 효율적이다.

 

 

3. 문제 해결과 회고 문서화

 

포스트모템(Post-Mortem) 문서 작성 

포스트모템은 프로젝트나 개발 과정에서 발생한 문제의 원인, 시도한 해결 방법, 최종 결과를 분석하고 문서화하여 팀의 학습과 성장을 도모하는 것이 주요 목적이다. 향후 유사한 문제를 예방하고 더 나은 의사결정을 할 수 있게 한다.

 

포스트모템 문서의 필수 구성 요소 

문제의 원인 분석, 시도한 해결 방법, 최종 결과 및 교훈을 포함하는 회고 문서이다.

 

'문제의 원인' 기술하기 

포스트모템에서는 문제의 근본 원인을 구체적이고 명확하게 파악하여 기록해야 한다. 발생 조건, 환경, 근본 원인을 상세히 기록해야 향후 유사한 문제를 예방할 수 있다.


*수학 공부*

이번 주제 : 회전과 사원수(Quaternion)

 

1. 3D 공간에서 회전을 표현하는 방법

3D 공간에는 축이 X, Y, Z 세 개가 있기 때문에 물체를 회전시키려면 추가적인 도구가 필요하다. 크게 행렬, 오일러 각, 사원수 3가지 도구를 사용할 수 있다.

행렬은 직관적이지 않고 메모리를 많이 차지해서 저장용으로는 잘 사용하지 않는다.

오일러 각의 경우 인간에게 가장 직관적인 3개의 각도(Pitch, Yaw, Roll)로 회전을 표현하는 것이다.

사원수(쿼터니언)는 컴퓨터를 위한 완벽한 4차원 복소수 회전이라고 생각하면 된다.

 

2. 오일러 각

언리얼 엔진에서는 FRotator라는 구조체로 다루게 되는 회전 표현법. Pitch는 Y축을 기준으로 고개를 끄덕이고, Yaw는 Z축을 기준으로 고개를 좌우로 도리도리하고, Roll은 X축을 기준으로 고개를 좌우로 갸우뚱한다. 프로그래머가 직관적으로 이해하기도 쉽고 에디터에서 숫자를 입력하면서 실제로 사용하기에도 좋다. 다만 치명적인 약점이 있는데...

 

3. 짐벌 락

오일러 각은 작업할 때에는 좋지만 내부 계산용으로는 사용할 수 없다. 바로 짐벌 락(Gimber Lock)현상 때문이다. 무슨 현상이길래 그러는 걸까?

오일러 각은 X, Y, Z 를 동시에 돌리는 것이 아니라 부모-자식관계를 가진 계층구조로 순서대로 돌리게 된다. 이때, 중간에 있는 축이 정확히 90도로 꺾이게 되면 첫번째 축과 세번째 축이 완전히 겹쳐버리면서 오브젝트가 특정 방향으로 절대 회전하지 못하고 팽이처럼 도는 버그가 발생하게 된다. 축이 두개가 되어버리면서 결국 2D 회전밖에 나타낼 수밖에 없는 상황이 되어버리는 것이다.

 

4. 사원수(Quaternion)

짐벌 락 문제를 해결하기 위해서 등장한 것이 4차원 복소수, 사원수(쿼터니언, q)이다.

수학적 형태 : q = w + xi + yj + zk (실수부 w 1개와 허수부 x, y, z 3개로 이루어짐)

공간적 의미 : 오일러 각 처럼 축을 3번 나누어 돌리지 않고, 어떤 임의의 회전 축(vector x, y, z)을 기준으로 한번에 scalar도 만큼 돌리겠다라는 개념.

즉, 축이 겹치는 과정을 없애서 짐벌 락이 절대 발생하지 않고, 행렬보다 메모리도 적게 차지하고, 회전과 회전 사이를 보간(Slerp)할 때 경로가 찌그러지지 않고 가장 짧고 부드러운 구면을 따라 이동한다는 장점을 가지게 된다. 다만, 사람이 사용하기에는 좀 어렵다.

 

5. 언리얼 C++ 적용하기

핵심은 다음과 같다.

입력받을 때 : 오일러 각(FRotator)

내부 연산 및 보간 : 쿼터니언(FQuat)

 

방향을 부드럽게 추적하는 미사일이나 적을 향해 서서히 고개를 돌리는 포탑을 구현 할 때 짐벌 락 없이 안전하게 코드를 구현해보자.

#include "Math/Quat.h"
#include "Math/Rotator.h"

void AMyTurret::SmoothLookAtTarget(FVector TargetPos, float DeltaTime)
{
    // 1. 현재 포탑의 회전 상태를 쿼터니언으로 가져옴 (연산을 위해)
    FQuat CurrentQuat = GetActorRotation().Quaternion();

    // 2. 타겟을 바라보기 위한 목표 방향 벡터 계산
    FVector DirectionToTarget = (TargetPos - GetActorLocation()).GetSafeNormal();

    // 3. 목표 방향 벡터를 쿼터니언 회전값으로 변환
    // (FRotator를 거치지 않고 바로 방향 벡터에서 회전 행렬을 거쳐 쿼터니언을 뽑아냄)
    FQuat TargetQuat = DirectionToTarget.Rotation().Quaternion();

    // 4. Slerp(구면 선형 보간)를 이용해 현재 회전에서 목표 회전으로 부드럽게 이동
    // 짐벌 락의 위험이 없고, 어느 각도에서든 일정한 속도로 부드럽게 회전함
    float RotationSpeed = 5.0f;
    FQuat NewQuat = FMath::QInterpTo(CurrentQuat, TargetQuat, DeltaTime, RotationSpeed);

    // 5. 최종 계산된 쿼터니언을 다시 액터에 적용
    SetActorRotation(NewQuat);
}

 

만약 두 개의 회전을 더하고 싶을 때는 쿼터니언끼리 곱셈을 한다. * 연산자를 이용하고, 행렬을 곱해서 공간을 합치는 것과 같은 원리이다.

// 캐릭터가 현재 회전 상태에서 Z축으로 45도 더 회전하게 만들고 싶을 때
FQuat AddRotation = FRotator(0.0f, 45.0f, 0.0f).Quaternion();
FQuat FinalRotation = GetActorRotation().Quaternion() * AddRotation;

*언리얼 C++*

1. C++로 TPS 개발하기

개발환경 확인(Rider), Blank C++ 프로젝트 생성, 에디터 화면 구성 익히기, 에디터 사용법(뷰포트, 아웃라이너, 월드 세팅, 콘텐츠 브라우저, Fab), GameMap, Level, 기본맵 등록, C++로 APlayerCharacter 클래스 생성, BP_PlayerCharacter로 덮고 메쉬 입히기

 

2. 코드 품질 관리와 리팩토링:

 

1) 기이한 이름X. "코드 컨벤션"

 

2) 중복 코드X. 비슷하지만 조금씩 다른 코드는 공통 부분을 먼저 정리하고 나서 분리하자.

 

3) 긴함수X. 짧은 함수는 무엇을 하는지 명확하게 보여준다. 주석이 필요하다고 느껴질 정도로 길어졌으면 따로 함수로 빼고 이름을 붙이자.

 

4) 긴 매개변수 목록X. 매개변수가 많으면 함수를 이해하기 힘들어진다. 간결하게 묶거나 축소하자. 중복된 정보는 제거해라.

 

5) 전역데이터의 남용X. 어디서든 접근하게 하는 건 좋은 게 아니다. 데이터 범위를 최소화하고, 꼭 필요한 곳에서만 사용하자.

 

6) 가변데이터 사용 주의점: 값이 자주 바뀌면 예기치 못한 오류나 복잡도가 증가한다. 변경 가능한 범위를 최소화, 가급적이면 불변 데이터 사용할 것!

 

7) 하나의 클래스/모듈이 서로 다른 이유로 자주 수정된다면? 문제가 생긴거니 다른 모듈로 분리해 단일 책임 원칙을 지키도록 해야 한다. "이 클래스가 변하는 이유가 몇 개인가?"를 세어보고 두 개 이상이면 쪼갤 것을 고려하자. (Split Phase -> Extract Class -> Move Function/Field)

 

8) Shotgun Surgery를 피하자. 작은 변경을 위해 여러 곳을 동시에 수정하는 상황을 의미한다. 버그/에러가 발생하기 쉬워지니까 관련된 것을 꼭 한 군데로 모아서 수정 범위를 좁히자.

 

9) Feature Envy(기능 편애)를 줄이자. 어떤 함수가 자신의 객체보다 남의 객체 기능이나 데이터를 더 많이 사용한다면 그냥 그 함수를 데이터가 있는 곳으로 옮겨서 의존성을 줄이는 것이 낫다. 서로 가까운 기능끼리 모여야 코드가 자연스럽고 관리도 수월해진다.

 

10) Data Clumps(데이터 뭉치)를 적극 활용하자. 자주 함께 쓰이는 데이터는 하나로 묶으면 의미가 명확해진다. 중복되는 필드나 매개변수 그룹은 별도 구조로 분리하자. 즉, 구조체를 잘 사용하자.

 

11) Primitive Obsession(기본형 집착)에 주의하자. 복잡한 데이터를 단순한 기본형에 과도하게 의존해서는 안된다. 단순한 기본형 대신 클래스나 구조체를 사용해서 복잡한 데이터를 표현하자.

 

12) Repeated Switches(반복되는 스위치문)에 주의하자. 같은 조건(보통 같은 enum)으로 분기하는 switch가 여러 파일 또는 여러 함수에서 반복해서 등장해서 새로운 case를 하나 추가할 때마다 흩어져 있는 switch를 전부 찾아서 수정해야 한다면 버그/에러가 발생하기 쉽다. 이럴 때에는 분기 대상을 클래스 계층으로 바꾸면 새 타입을 추가할 때 기존 코드를 건드리지 않고 클래스 하나만 추가하면 된다.

 

13) Loops(반복문) 사용할  때 주의할 점. 루프 본문이 길어질 수록 반복문 가독성이 떨어진다. 이럴 때에는 필터링, 집계, 변환 같은 각 단계의 의도를 작은 함수 또는 파이프라인(TArray::FilterByPredicate, Algo::Accumulate 같은)으로 쪼개서 드러내자. 가독성 문제는 항상 중요하다. 읽기 쉬운 코드일수록 병목을 제대로 찾아 최적화하기 더 수월하기 때문이다. 성능 걱정보다 일단 기본을 갖춘 가독성을 더 우선시해보자.

 

14) Lazy Element(게으른 요소)는 과감히 삭제하자. 하는 일 없이 존재만 하는 메서드나 클래스는 오히려 혼동을 준다. '단순화'가 핵심이다.

 

15) Speculative Generality(추측성 일반화)를 조심하자. 현재 필요한 기능에 집중해서 불필요한 추상화를 걷어내자. 미래에 써야지~! 하고 여러개 만들지 말고 현재 문제 해결에만 우선하자는 것이다. 

 

16) Temporary Field(임시 필드)는 다른 구조로 옮기거나 클래스로 분리하자. 목적이 분명치 않은 필드는 코드 복잡도를 높이기만 할 뿐이다. 

 

17) Message Chains(메시지 체인). 객체를 줄줄이 호출하면 내부 구조가 노출되어 결합도가 커진다. 이런 구조가 필요하다면 최종 로직을 호출부 가까이로 옮겨 의존을 최대한 줄이자.

 

18) Middle Man(중재자)를 제거하자. 실질적 로직 없이 위임만 하는 클래스는 사실 필요가 없다. 직접 연결하고 직관적 구조를 유지하자.

 

19) Insider Trading(내부자 거래). 모듈 간에 비공개 데이터가 과하게 오가면 결합도가 높아진다. 필요한 정보만 교환할 수 있게 인터페이스 범위를 명확히 정의해야 한다. 모듈 간 벽을 두껍게 유지해서 각자 책임을 분리하자.

 

20) Large Class(거대한 클래스)에 주의. 너무 많은 책임을 지는 클래스는 필드와 메서드가 폭발적으로 늘어난다. 관리가 어려워지는 주범이므로 역할이나 기능별로 클래스를 분리하자. 사용 패턴을 분석해 클래스를 쪼개면 유지보수가 훨씬 수월해진다.

 

21) Alternative Classes with Different Interfaces(서로 다른 인터페이스의 대안 클래스들). 클래스를 교체하려면 인터페이스가 호환되어야 한다. 유사 기능 클래스끼리 일관된 형식을 갖추는 것이 좋다. 메서드 시그니처를 통일해서 교체 가능성을 높이자.

 

22) Data Class(데이터 클래스). 필드와 게터/세터만 있는 클래스는 다른 곳에서 함부로 조작되기 쉽다. 변경될 필요가 없는 필드는 세터를 제거해서 그냥 안전성을 높여버리자. 필요 기능이 있다면 이 클래스 안에 직접 구현해서 응집도를 높이는 것이 낫다.

 

23) Refused Bequest(상속 포기)를 고려해보자. 서브클래스가 부모의 기능 중 일부만 필요하거나 인터페이스가 맞지 않다면 굳이 상속하지 않더라도 필요한 부분만 다른 방식으로 얻을 수 있다. 구조를 단순화해보자.

 

24) Comments(주석)작성시 유의 사항. 올바른 주석은 아주 좋은데, 결론적으로는 코드만으로 명확하게 이해가 되게 하는 것이 훨씬 더 중요하다. 주석은 코드를 변명하기 위한 장치다. 주석이 필요없는 좋은 코드가 되도록 노력하자.


*오늘의 총평*

언리얼 3D 게임 만들기 강의를 수강하고, 언리얼 TPS 슈팅 게임 강의도 줌 강의로 들으면서 기본부터 다시 다졌다. 

다시 마음을 다지면, 공부는 동기부여 + 계획과 밀도. 왜 게임 개발 공부를 시작했는지 잊지말고 되새기고, 매일 아침, 매주 세운 계획을 잘 지키면서 남는 시간에는 추가 공부를 하고, 매 순간 집중도를 잃지 말고 쉬는 시간에 스트레칭 하면서 계속 집중력을 환기하자.

 

'TIL' 카테고리의 다른 글

06.11 TIL  (0) 2026.06.11
06.10 TIL  (0) 2026.06.10
06.08 TIL  (0) 2026.06.08
06.05 TIL  (0) 2026.06.05
06.04 TIL (Ch.3 개인 과제 발제)  (0) 2026.06.04