TIL
06.19 TIL
2026. 6. 19. 19:10

일정표

시간 할 일 비고
08:00~10:00 코드카타, Atanny, 프로그래머스  
10:00~12:00 TA 수업  
12:00~13:00 TA 수업 복습, 프로그래머스  
14:00~18:00 오늘의 수학, 라이라 스타터 게임 파먹기  
19:00~21:00 팀 프로젝트 준비, TIL  
21:00~22:00 운동**  
22:00~23:30 개인 공부

*오늘의 코드카타*

 

문제. 크기가 작은 부분문자

더보기

숫자로 이루어진 문자열 t와 p가 주어질 때, t에서 p와 길이가 같은 부분문자열 중에서, 이 부분문자열이 나타내는 수가 p가 나타내는 수보다 작거나 같은 것이 나오는 횟수를 return하는 함수 solution을 완성하세요

예를 들어, t="3141592"이고 p="271" 인 경우, t의 길이가 3인 부분 문자열은 314, 141, 415, 159, 592입니다. 이 문자열이 나타내는 수 중 271보다 작거나 같은 수는 141, 159 2개 입니다

#include <string>

using namespace std;

int solution(string t, string p) {
    int answer = 0;
    int t_len = t.length();
    int p_len = p.length();
    
    for (int i = 0; i <= t_len - p_len; ++i)
    {
        string sub = t.substr(i, p_len);
        
        if (stoll(sub) <= stoll(p))
        {
            answer++;
        }
    }
    return answer;
}

(1) substr을 이용해서 풀이한 방법. t.substr(i, p_len) 형식으로 사용한다.

 

#include <string>

using namespace std;

int solution(string t, string p) {
    int answer = 0;
    int t_len = t.length();
    int p_len = p.length();
    
    for (int i = 0; i <= t_len - p_len; ++i) {
        if (t.substr(i, p_len) <= p) {
            answer++;
        }
    }
    
    return answer;
}

(2) 굳이 숫자로 바꿀 필요 없이 문자열 그대로 비교하는 것이 더 좋을 것이다.


*프로그래머스*

1. [기초] 대문자로 바꾸기

더보기

문제 설명

#include <string>

using namespace std;

string solution(string myString) {
    for (char& c : myString)
    {
        c = toupper(c);
    }
    return myString;
}

(1) 익숙한 반복문 사용

 

#include <string>
#include <algorithm>
#include <cctype>

using namespace std;

string solution(string myString) {
    transform(myString.begin(), myString.end(), myString.begin(), ::toupper);
    
    return myString;
}

(2) transform 사용

 

 

2. [기초] 소문자로 바꾸기

더보기

알파벳으로 이루어진 문자열 myString이 주어집니다. 모든 알파벳을 소문자로 변환하여 return 하는 solution 함수를 완성해 주세요

#include <string>
#include <algorithm>

using namespace std;

string solution(string myString) {
    transform(myString.begin(), myString.end(), myString.begin(), ::tolower);
    return myString;
}

(1) transform을 사용하서 푸는 방법.

 

 

3. [기초] 배열에서 문자열 대소문자 변환하기

더보기

문자열 배열 strArr가 주어집니다. 모든 원소가 알파벳으로만 이루어져 있을 때, 배열에서 홀수번째 인덱스의 문자열은 모든 문자를 대문자로, 짝수번째 인덱스의 문자열은 모든 문자를 소문자로 바꿔서 반환하는 solution 함수를 완성해 주세요

#include <string>
#include <vector>

using namespace std;

vector<string> solution(vector<string> strArr) {
    vector<string> answer;
    answer.reserve(strArr.size());
    
    for (int i = 0; i < strArr.size(); ++i)
    {
        string temp = strArr[i];
        
        if (i % 2 == 0)
        {
            for (char& c : temp)
            {
                c = tolower(c);
            }
        }
        else
        {
            for (char& c : temp)
            {
                c = toupper(c);
            }
        }
        answer.emplace_back(temp);
    }
    return answer;
}

(1) 반복문을 사용한 풀이. 1, 2 번 문제와는 다르게 짝수번째 인덱스와 홀수번째 인덱스로 나누는 로직이 추가되었다.

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

using namespace std;

vector<string> solution(vector<string> strArr) {
    for (int i = 0; i < strArr.size(); ++i) {
        if (i % 2 == 0) {
            transform(strArr[i].begin(), strArr[i].end(), strArr[i].begin(), ::tolower);
        } else {
            transform(strArr[i].begin(), strArr[i].end(), strArr[i].begin(), ::toupper);
        }
    }
    
    return strArr;
}

(2)  transform을 이용한 풀이. 마찬가지로 인덱스를 짝수번째와 홀수번째로 나누는 로직을 추가한다.

 

 

4. [기초] A 강조하기

더보기

문자열 myString이 주어집니다. myString에서 알파벳 "a"가 등장하면 전부 "A"로 변환하고, "A"가 아닌 모든 대문자 알파벳은 소문자 알파벳으로 변환하여 return 하는 solution 함수를 완성하세요

#include <string>

using namespace std;

string solution(string myString) {
    for (char& c : myString)
    {
        if (c == 'a' || c == 'A')
        {
            c = 'A';
        }
        else
        {
            c = tolower(c);
        }
    }
    return myString;
}

(1) 조건을 단순화해서 참조 연산자를 활용하고 for 문을 사용하기

#include <string>
#include <algorithm>
#include <cctype>

using namespace std;

string solution(string myString) {
    transform(myString.begin(), myString.end(), myString.begin(), [](char c) {
        if (c == 'a' || c == 'A') return 'A';
        return (char)tolower(c);
    });
    return myString;
}

(2) transform과 [] 람다 함수식을 사용하기. 

if 문을 쓴다고 해서 꼭 {} 하고 else {} 형식으로 쓸 필요는 없다. C++ 모든 함수에는 빠른 반환(Early Return)이 항상 적용된다. 함수 안에서 return을 만나는 순간 함수는 그 즉시 실행을 종료하고 결과값을 반환한다.

만약 들어온 문자 c가 'a' 거나 'A' 이면 return 'A' ; 를 실행하고 함수가 종료된다. 아래에 있는 tolower(c) 부분은 실행되지 않게 된다.

만약 들어온 문자가 조건문에 해당하지 않으면, return 'A'는 실행되지 않고 다음 줄인 return (char)tolower(c); 로 넘어가서 소문자로 바꾼 값을 반환하고 람다 함수가 종료된다.

 

변수나 함수 앞에 (자료형) 을 붙이는 것을 명시적 형변환이라고 부른다. 이는 컴퓨터에게 데이터의 형태를 강제로 바꿔달라고 명령하는 것과 같다.

tolower 함수는 사실 정수(int)를 반환한다. 즉, 아스키코드로 계산해서 숫자로 값을 뱉어내기 때문에, (char)를 tolower 앞에 붙여주면서 컴퓨터에게 에러 띄우지 말고 char로 바꿔서 값을 넣으라는 것을 명령하는 것이다.

 


 

5. [기초] 특정한 문자를 대문자로 바꾸기

더보기

영소문자로 이루어진 문자열 my_string과 영소문자 1글자로 이루어진 문자열 alp가 매개변수로 주어질 때, my_string에서 alp에 해당하는 모든 글자를 대문자로 바꾼 문자열을 return 하는 solution 함수를 작성해 주세요

#include <string>

using namespace std;

string solution(string my_string, string alp) {
    char char_alp = alp[0];    
    for (char& c : my_string)
    {
        if (c == char_alp)
        {
            c = toupper(c);
        }
    }
    return my_string;
}

(1) & 참조자와 for 반복문을 사용한 풀이.

 

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

using namespace std;

string solution(string my_string, string alp) {
    char char_alp = alp[0];    
    transform(my_string.begin(), my_string.end(), my_string.begin(), [char_alp](char c) {
        if (c == char_alp) return (char)toupper(c);
        return c;
    });
    return my_string;
}

(2) C++ transform과 [] 람다 함수를 사용한 풀이

 

 

6. [입문] 7의 개수

더보기

머쓱이는 행운의 숫자 7을 가장 좋아합니다. 정수 배열 array가 매개변수로 주어질 때, 7이 총 몇 개 있는지 return 하도록 solution 함수를 완성해보세요.

#include <string>
#include <vector>

using namespace std;

int solution(vector<int> array) {
    int answer = 0;
    
    for (const auto num : array)
    {
        string temp = to_string(num);
        for (char c : temp)
        {
            if (c == '7')
            {
                answer++;
            }
        }
    }
    return answer;
}

(1) 기존의 방식대로 숫자를 문자열로 변환한 후, 7이면 answer++; 하는 방법.

 

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

int solution(vector<int> array) {
    int answer = 0;
    
    for (const auto num : array)
    {
        string temp = to_string(num);
        answer += count(temp.begin(), temp.end(), '7');
    }
    return answer;
}

(2) count를 사용해서, temp 문자열을 처음부터 끝까지 스캔하고 '7' 이 있으면  count를 1 늘리는 방법. 알고리즘 헤더를 사용하면 된다.

 

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

int solution(vector<int> array) {
    int answer = 0;
    
    for (int num : array)
    {
        while (num > 0)
        {
            if (num % 10 == 7)
            {
                answer++;
            }
            num /= 10;
        }
    }
    return answer;
}

(3) 사실 to_string()을 호출할 때마다 새로운 문자열을 위한 메모리를 할당해야 하므로 배열의 크기가 매우 커지면 속도가 느려질 수 있다. 따라서, 순수 정수 연산만으로 해결하는 방법이 가장 메모리 낭비 없이 빠르게 동작하는 방법이다.

 

 

 

7. [입문] 잘라서 배열로 저장하기

더보기

문자열 my_str과 n이 매개변수로 주어질 때, my_str을 길이 n씩 잘라서 저장한 배열을 return하도록 solution 함수를 완성해주세요

#include <string>
#include <vector>

using namespace std;

vector<string> solution(string my_str, int n) {
    vector<string> answer;
    
    int start_idx = 0;
    string temp = "";
    
    while (start_idx < my_str.length()) {
        for (int i = start_idx; i < start_idx + n && i < my_str.length(); ++i) {
            temp += my_str[i];
        }
        answer.emplace_back(temp);
        start_idx += n;
        temp = "";
    }   
    return answer;
}

(1) 문자열을 순회하면서 임시 변수에 글자를 모아 배열에 넣는 방ㅇ식.

반복문 진입 시 while 문 조건을 주고, while 문 안의 for문에서 인덱스가 my_str.length() 를 초과하지 않도록 && 논리 연산자를 활용하여 한번 더 조건을 걸어줘야 한다. 만약 해당 조건이 없다면 문자열의 실제 길이를 벗어날 경우 쓰레기값을 읽어버리게 된다.

#include <string>
#include <vector>

using namespace std;

vector<string> solution(string my_str, int n) {
    vector<string> answer;
 
    int reserve_size = (my_str.length() + n - 1) / n;
    answer.reserve(reserve_size);
    
    for (int i = 0; i < my_str.length(); i += n)
    {
        answer.emplace_back(my_str.substr(i, n));
    }
    return answer;
}

(2) substr을 사용하는 방법. 문자열.substr(시작 인덱스, 잘라낼 길이) 로 사용하면 된다. 

컴퓨터에서는 정수 / 정수를 하면 무조건 나머지를 버리고 내려버린다. 그래서 나눌 값을 미리 한번 더해준 뒤에 - 1 을 해주면, 나눗셈 이후에 내가 원하는 값이 무조건 도출되게 할 수 있다.

 

 

8. [입문] 중복된 숫자 개수

더보기

정수가 담긴 배열 array와 정수 n이 매개변수로 주어질 때, array에 n이 몇 개 있는 지를 return 하도록 solution 함수를 완성해보세요

#include <vector>

using namespace std;

int solution(vector<int> array, int n) {
    int answer = 0;
    
    for (const auto num : array)
    {
        if (num == n)
        {
            answer++;
        }
    }
    return answer;
}

(1) for문을 사용한 기본적인 풀이 방법

#include <vector>
#include <algorithm>

using namespace std;

int solution(vector<int> array, int n) {
    return count(array.begin(), array.end(), n);
}

(2) 알고리즘 헤더의 count를 사용한 풀이

 

 

9. [입문] 머쓱이보다 키 큰 사람

더보기

머쓱이는 학교에서 키 순으로 줄을 설 때 몇 번째로 서야 하는지 궁금해졌습니다. 머쓱이네 반 친구들의 키가 담긴 정수 배열 array와 머쓱이의 키 height가 매개변수로 주어질 때, 머쓱이보다 키 큰 사람 수를 return 하도록 solution 함수를 완성해보세요

#include <vector>

using namespace std;

int solution(vector<int> array, int height) {
    
    int answer = 0;
    
    for (const auto num : array)
    {
        if (num > height)
        {
            answer++;
        }
    }
    return answer;
}

(1) 기본적인 풀이 방법

 

#include <vector>
#include <algorithm>

using namespace std;

int solution(vector<int> array, int height) {
    return count_if(array.begin(), array.end(), 
                    [height](int h) {
                        return h > height;
                    });
}

(2) 고급지게 람다 함수를 사용해서 조건문을 달아 count_if 를 사용하여 풀 수도 있다.


*Atanny*

1. Data Table 구조와 게임 데이터 관리

 

핵심 내용 

Data Table은 게임에서 많은 양의 데이터를 체계적으로 관리하기 위한 에셋. 쉽게 말해 엑셀 스프레드시트처럼 행과 열 형태로 정보를 저장한다.

구조체(Structure)가 열(Column)을 정의하고, 각 행(Row)에 실제 데이터가 들어간다.

구조체는 틀이고 Data Table은 그 틀을 써서 게임 데이터를 표 형식으로 관리하는 도구이다.

 


2. UMG 위젯 구조와 데이터 바인딩

 

핵심 내용 

UMG 위젯은 언리얼 엔진에서 UI를 만드는 시스템이고, 데이터 바인딩은 게임 데이터와 UI를 자동으로 연결하는 방식이다.

데이터 바인딩의 핵심은 "함수를 연결해서 실시간으로 값을 가져오게 한다"는 원리이다. Tick이나 이벤트 리스너처럼 계속 확인하는 게 아니라, 바인딩 함수가 필요할 때마다 호출되어 최신 데이터를 반환하는 방식으로 작동한다.

바인딩과 직접 업데이트는 다르다. Tick 이벤트에서 매 프레임마다 Set Text나 Set Progress 같은 노드를 직접 실행하는 것과, 처음부터 바인딩 함수를 연결해 놓는 것은 완전히 다른 방식이다.

바인딩은 선언적(한 번 연결하면 끝)이고, 직접 업데이트는 명령적(계속 반복)이다. 따라서 같은 결과처럼 보이지만, 코드 구조와 효율성이 다르다.

또 하나 놓치기 쉬운 부분은 바인딩 함수의 반환 타입이다. 텍스트 위젯에 연결하면 String을 반환해야 하고, Progress Bar처럼 비율을 표현하는 위젯에는 0.0~1.0 범위의 float를 반환해야 한다. 위젯 종류에 맞게 데이터를 가공해서 반환하는 게 중요하다.


3. 애니메이션 블루프린트와 캐릭터 애니메이션 처리

 

핵심 내용 

애니메이션 블루프린트는 언리얼 엔진에서 캐릭터의 애니메이션을 관리하고 제어하는 시스템이다. 쉽게 말해서 캐릭터가 "어떤 상태일 때 어떤 애니메이션을 재생할지"를 결정하는 스크립트라고 보면 된다.

애니메이션 블루프린트는 크게 두 가지 부분으로 나뉜다. EventGraph는 데이터를 계산하고 정보를 수집하는 공간이고, AnimGraph는 그 정보를 바탕으로 실제로 어떤 애니메이션을 재생할지 결정하는 공간이다.. EventGraph에서 캐릭터의 상태 정보(속도, 방향 등)를 계산하면, 그 정보가 AnimGraph로 전달되어 적절한 애니메이션이 선택된다.

캐릭터의 속도(Try Get Character Owner)는 Get Velocity 노드직접 가져와야 한다. Get Velocity는 캐릭터가 현재 얼마나 빠르게 움직이는지를 벡터 형태로 반환하는 노드이다. 이 벡터는 x, y, z 방향의 속도 성분을 가지고 있다. 

애니메이션 블루프린트에서는 보통 "캐릭터가 얼마나 빠르게 움직이는가"라는 **속력(크기)**을 알아야하는데, 이를 위해 Vector Length 노드를 사용한다. Vector Length는 속도 벡터의 크기를 계산해서 스칼라값(단일 숫자)으로 변환해준다. 이 값이 바로 캐릭터의 속력이고, 이를 변수에 저장하면 AnimGraph에서 "느리게 걷기", "빠르게 뛰기" 같은 애니메이션을 선택할 때 사용할 수 있게 된다.


4. 파티클 시스템과 사운드 컴포넌트 이해

 

핵심 내용 

파티클 시스템은 수많은 작은 입자들을 한 번에 움직이고 제어해서 연기, 불, 먼지 같은 동적이고 복잡한 시각 효과를 만드는 방식이다. 한 개의 파티클이 아니라 여러 개가 동시에 생성·이동·소멸하는 흐름 자체가 핵심이다.

사운드 컴포넌트는 액터에 붙여서 오디오 파일을 재생하고 관리하는 도구라고 볼 수 있다. 게임의 배경음악, 효과음, 상황별 사운드를 언제, 어디서, 어떻게 울릴지 제어하는 역할을 한다.

 


*수학 공부*

주제 : 고급 곡선 (Spline)

 

Spline은 카메라의 영화 같은 연출, 부드러운 UI 팝업, 롤러코스터 레일, 유도 미사일의 매끄러운 궤적 등 아름다운 곡선이 필요한 게임을 지배하는 핵심 수학이다.

 

1. 단순한 Lerp의 한계와 베지에 곡선(Bézier Curve)

선형 보간(Lerp)는 출발점에서 도착점까지 직선으로만 이동한다. 만약 중간에 장애물을 부드럽게 피해가게 하고 싶으면 베지에 곡선을 사용해야 한다.

베지에 곡선의 원리는 간단하다. 바로 Lerp를 여러 번 중첩하는 것이다. 

 

 2차 베지에 곡선

기준점을 3개 잡고, A와 B 사이를 Lerp하고, 동시에 B와 C 사이를 Lerp 한다. 그리고 그렇게 나온 두 개의 이동하는 점 사이를 한 번 더 Lerp 하면 완벽한 포물선이 그려지게 된다.

 

 3차 베지에 곡선

기준점을 4개 사용해서 Lerp를 3번 중첩한 것이 3차 베지에 곡선이다.

 

2. 베지에 곡선의 한계와 캣멀-롬 스플라인(Catmull-Rom Spline)

베지에 곡선은 포물선을 그리는 훌륭한 방법이지만 치명적인 단점이 있다. 중간에 있는 제어점(Control Points)들을 직접 통과하지 않고 곡선만 그린다는 것이다. 예를 들어, 정찰병 AI 가 웨이포인트 5개를 정확히 밟고 지나가게 만들고 싶을 때 베지에 곡선을 사용하면 웨이포인트를 비껴가면서 둥글게 돌아버리게 된다.

이때 사용하는 것이 캣멀-롬 스플라인이다.

캣멀-롬 스플라인은 주어진 모든 웨이포인트를 반드시 부드럽게 통과하도록 각 점에서의 접선(Tangent)을 양옆의 점들을 이용해서 수학적으로 자동 계산해준다. 레이싱 게임의 트랙을 깎거나, AI의 순찰 경로를 만들 때 실무에서 가장 많이 쓰이는 방법이다.

 

3. 큐빅 에르미트 스플라인(Cubic Hermit Spline)

캣멀-롬 스플라인에 대해서 이해하기 위해서는 먼저 에르미트 스플라인을 이해해야 한다. 앞서 살펴보았던 Lerp나 베지에 곡선은 점의 위치들을 섞어서 곡선을 만들었다. 하지만 에르미트 스플라인은 점의 위치뿐만 아니라, 그 점을 지날 때의 '속도(방향과 크기 == 접선 tangent)를 직접 수학적으로 제어한다는 특징이 있다.

에르미트 스플라인은 수학적으로 4개의 데이터를 요구한다.

P(0) : 출발 위치

P(1) : 도착 위치

V(0) : 시작 접선. 출발할 때의 방향과 세기

V(1) : 도착 접선. 도착할 때의 방향과 세기

 

C++에서 시간 t(0.0~1.0)에 따른 위치를 구하는 에르미트 Basic Function 조합식을 살펴보자.

// 에르미트 스플라인 곡선 상의 특정 시간(t)의 위치를 반환하는 함수
FVector GetHermitePoint(FVector P0, FVector V0, FVector P1, FVector V1, float t)
{
    float t2 = t * t;
    float t3 = t2 * t;

    // 에르미트 기저 다항식 (수학자들이 증명해둔 완벽한 블렌딩 비율)
    float h00 = 2*t3 - 3*t2 + 1;
    float h10 = t3 - 2*t2 + t;
    float h01 = -2*t3 + 3*t2;
    float h11 = t3 - t2;

    // 위치와 접선을 기저 다항식 비율로 섞음
    return (h00 * P0) + (h10 * V0) + (h01 * P1) + (h11 * V1);
}

 

4. 에르미트 스플라인의 한계와 캣멀-롬 스플라인

에르미트 스플라인은 속도 벡터를 다루기 때문에 완벽한 제어력을 주지만, 실무에서 사용하기에는 귀찮다. AI의 순찰 경로 점을 10개 찍으면 결국 각 점마다 진입 각도와 진출 각도 벡터를 일일이 계산해서 넣어줘야 하기 때문이다.

 

그래서 만들어낸 스플라인이 바로 캣멀-롬 스플라인이다. 에르미트 스플라인 공식을 그대로 사용하되, 접선 V(0)와 V(1)은 앞뒤의 점들을 보고 수학적으로 자동 계산하도록 만든 것이다.

 

수학적 원리 : 점 P1에서 다음 점 P2로 가는 곡선을 그린다고 하자.

P1에서의 접선 V1은 이전 점 P0에서 다음 점 P2를 바라보는 벡터를 절반으로 나눠서 구한다.

P2에서의 접선 V2는 이전 점 P1에서 다음 점 P3를 바라보는 벡터를 절반으로 나눠서 구한다.

 

이렇게 계산 된 V1과 V2를 에르미트 함수에 집어넣으면 캣멀-롬 스플라인이 된다. 즉, 점 4개만 던져주면 가운데에 있는 P1 과 P2 사이를 완벽하게 부드러운 곡선으로 이을 수 있게 된다.

 

5. 언리얼 엔진에서 사용하기

 

언리얼 엔진의 USplineComponent가 바로 이 방식을 사용해서 부드러운 곡선을 만들어낸다. 

언리얼 에디터에서 spline 점을 클릭하면 빨간색 핸들이 나오게 되는데, 그걸 마우스로 조작하는 행위가 에르미트 스플라인의 V 벡터(접선)를 수동으로 조작하는 것이다.

Curve Auto가 바로 캣멀-롬 스플라인을 활용한 기능이다. 스플라인 점을 우클릭하고 곡선 유형을 auto로 설정하면 알아서 부드러워지는데, 언리얼 엔진 내부 C++에서 앞뒤 점의 위치를 참조해서 캣멀-롬 수학을 돌려서 자동으로 덮어씌우는 것이다.

 

(적용 사례 : 스플라인을 따라가는 시네마틱 카메라)

#include "Components/SplineComponent.h"
#include "Math/UnrealMathUtility.h"

void AMyCameraRail::UpdateCameraPosition(float DeltaTime)
{
    // 1. 레일을 따라 이동할 거리 누적 (속도 * 델타 타임)
    CurrentDistance += MoveSpeed * DeltaTime;

    // 스플라인의 총 길이를 넘어가면 끝에서 멈추도록 예외 처리
    float TotalLength = CameraSpline->GetSplineLength();
    CurrentDistance = FMath::Clamp(CurrentDistance, 0.0f, TotalLength);

    // 2. 현재 거리에 해당하는 3D 공간의 완벽한 '위치(Position)'를 곡선 수학으로 추출
    FVector NewLocation = CameraSpline->GetLocationAtDistanceAlongSpline(
        CurrentDistance, ESplineCoordinateSpace::World);

    // 3. 미분학의 승리: 해당 지점에서의 접선(곡선의 기울기)을 구해, 
    // 카메라가 나아갈 '회전값'으로 변환
    FRotator NewRotation = CameraSpline->GetRotationAtDistanceAlongSpline(
        CurrentDistance, ESplineCoordinateSpace::World);

    // 4. 계산된 완벽한 곡선 좌표와 회전을 카메라에 적용
    CameraActor->SetActorLocationAndRotation(NewLocation, NewRotation);
}

 

조금 더 응용해보자면, 단순히 똑같은 속도로만 움직이지 않고 내리막길에서 빨라지고 오르막길에서 느려지게 만들고 싶다면,

스플라인의 특정 지점에서 접선 벡터(Forward Vector)월드의 중력 벡터(Up)내적(Dot Product)해서, 내적 값에 따라 경사도를 정확히 알아내고 해당 비율을 오일러 적분을 활용하여 가속도에 더해주면 사실적인 물리 엔진을 완성할 수 있다.

 

*심화*

구심적(Centripetal) 캣멀-롬 스플라인

일반적인 캣멀-롬을 쓰다보면 카메라가 갑자기 루프하거나 궤적이 바깥으로 심하게 튕겨 나가는 버그를 마주하게 된다. 이를 보완하고자 구심적 캣멀-롬 스플라인을 사용하게 된다.

 

1) 왜 버그가 발생할까?

일반적인 캣멀-롬은 곡선을 그릴 때, '점과 점 사이가 뭐든 간에 도착하는 데 걸리는 시간(t)는 무조건 1초다' 라는 가정을 하고 출발한다. 따라서, 점들이 일정한 간격으로 찍혀있으면 크게 문제가 될 게 없지만 실제로 점과 점 사이의 간격이 크게 달라진다면 수학적 계산 방식에서 치명적인 문제가 발생하게 된다.

예를 들어, P1과 P2는 가까운 상태에서 P3의 위치가 매우 멀리 찍혀있다면, P1에서 출발할 때의 속도(접선 벡터)가 두 점 사이의 실제 거리에 비해서 비정상적으로 거대한 값으로 도출되게 된다. 결과적으로 자동차 또는 카메라가 좁은 점 사이를 지날 때(P1에서 출발한 이후) 속도를 주체하지 못하고 튕겨 나갔다가 다시 돌아오면서 곡선이 8자로 꼬이는(돼지꼬리 버그) 오버슈팅이 발생하게 된다.

 

그래서 이 문제를 해결하기 위해 시간(t)을 점과 점 사이의 물리적 거리를 계산해서 시간을 유동적으로 할당하는 방식(매개변수화, Parameterization)을 도입했다. 이 때, 거리를 반영하는 비율(알파값)에 따라 3가지로 나뉘게 된다.

 - 1. Uniform (알파값 = 0.0) : 거리를 무시함(돼지꼬리 버그 발생)

 - 2. Chordal (알파값 = 1.0) : 실제 거리를 그대로 시간에 100% 반영한다. 꼬임은 발생하지 않지만 곡선이 팽팽해져서 코너에서 뾰족하게 각이 지는(Cusp) 문제가 발생한다.

 - 3. Centripetal (알파값 = 0.5) : 실제 거리의 제곱근을 시간에 반영한다.

 

결론은 뭐, Centripetal 알파값을 적용하겠다는 방식이 수학적으로 밸런스가 잡힌 방식이다. 거리가 너무 멀면 시간을 적당히 늘려주고, 거리가 너무 짧으면 시간을 줄여서 곡선이 스스로 교차하는 꼬임 현상이 일어나지 않게 하는 것이다.(수학적으로 증명되어있음)

 

따라서 사실 언리얼 엔진의 USplineComponent도 그렇고, 유니티 엔진의 Cinemachine 도 기본적으로 이 구심적 캣멀-롬을 내부 표준으로 사용하고 있다. 덕분에 개발자들이 아무렇게나 점을 찍어 놓아도 치명적인 버그가 발생하지 않도록 물리 엔진이 알아서 차단해주는 것이다.


*TA 수업*

주제 : 게임 수학(1) : 벡터

 

1. 위치

오브젝트가 공간 안에서 어디에 있는지를 나타낸다. (X, Y, Z). 위치 자체는 방향이 아니다.

 

 

2. 벡터

벡터는 X, Y, Z 값을 가진 자료형이다. 게임에서는 같은 Vector3 라도 상황에 따라 다르게 해석한다.

더보기
사용 방식 의미
위치 오브젝트가 어디에 있는가
방향 어느 방향을 향하는가
이동량 한 번에 얼마나 이동하는가
속도 어느 방향으로 얼마나 빠르게 움직이는가
거리 두 지점 사이의 길이

 

 

3. 방향

두 위치의 차이로 구한다. (Taget Position - Start Position)

이 벡터는 플레이어에서 적을 향하는 방향 벡터이다.

 

 

4. 거리

거리는 벡터의 길이로 구한다. Length(Target - Start)

게임에서는 거리를 여러 상황에 따라 다르게 사용한다.

더보기
사용처 설명
공격 사거리 적이 공격 가능한 거리 안에 있는지 확인
감지 범위 AI가 플레이어를 발견할 수 있는지 확인
상호작용 거리 아이템이나 문과 상호작용 가능한지 확인
카메라 거리 캐릭터와 카메라 사이 거리 계산
스킬 범위 범위 공격 안에 대상이 있는지 확인

 

 

5. 정규화

벡터의 길이를 1로 만드는 과정. 대각선으로 움직일 때 의도한 값보다 더 크게 움직이는 것을 막아준다.

ToEnemy = (300, 400, 0)
Length = 500

Normalized = (300 / 500, 400 / 500, 0 / 500)
Normalized = (0.6, 0.8, 0)

원래 벡터 = 방향 + 거리
정규화된 벡터 = 방향만 남은 벡터

결국 정규화할 값 =  방향 값에 크기가 남게 하지 않고 '1' 로 방향만 가리켜주기 위해서 길이로 나누는 과정이라고 생각하면 된다.

 

6. 이동량

캐릭터 이동은 보통 다음 공식으로 만든다!

Movement = Direction * Speed * DeltaTime

이를 활용해서 다음 위치를 계산한다.

NextPosition = CurrentPosition + Movement

 

이동 흐름을 다음과 같이 정리할 수 있다.

Target - Start
-> 방향 벡터 구하기
-> Length로 거리 구하기
-> Normalize로 방향만 남기기
-> Direction * Speed * DeltaTime
-> 한 프레임 이동량 구하기
-> CurrentPosition + Movement
-> 다음 위치 구하기

 

 

7. Vector3 클래스 만들기 + 게임 상황에 적용하기

C++로 간단한 Vector3 클래스를 직접 만들어보자.

더보기
기능 설명
X, Y, Z 3D 공간 값 저장
생성자 기본값 또는 직접 값 입력
operator+ 위치 + 이동량
operator- 두 위치의 차이
operator* 방향 * 속도 * 시간
Length() 벡터 길이 계산
Normalize() 방향만 남기기
Print() 결과 출력
class Vector3
{
public:
    float X;
    float Y;
    float Z;

    Vector3();
    Vector3(float x, float y, float z);

    Vector3 operator+(const Vector3& other) const;
    Vector3 operator-(const Vector3& other) const;
    Vector3 operator*(float scalar) const;

    float Length() const;
    Vector3 Normalize() const;
    void Print() const;
};
```

 

기본적으로 이해해야 하고 다룰 줄 알아야 하는 내용을 정리하면 다음과 같다.

더보기
코드 의미
EnemyPosition - PlayerPosition 플레이어에서 적을 향하는 벡터
ToEnemy.Length() 플레이어와 적 사이의 거리
ToEnemy.Normalize() 적을 향하는 방향만 남긴 벡터
Direction * Speed * DeltaTime 한 프레임 동안 이동할 양
PlayerPosition + Movement 다음 위치

 

이를 활용해서, 게임 상황에서의 예시를 살펴보자.

Vector3PlayerPosition(0.0f,0.0f,0.0f);
Vector3EnemyPosition(300.0f,400.0f,0.0f);

플레이어의 위치 vector 값과, 적의 위치 vector값이 주어진 상황이다.

 

더보기
계산 항목 결과
플레이어에서 적을 향하는 벡터 (300, 400, 0)
플레이어와 적 사이 거리 500
정규화된 방향 벡터 (0.6, 0.8, 0)
한 프레임 이동량 Direction * Speed * DeltaTime
다음 위치 PlayerPosition + Movement

 

사거리를 판정하는 예시도 함께 살펴보자. 기본적으로 floatAttackRange = 600.0f 처럼 주어지고, EnemyPosition - PlayerPosition으로 Distance를 구할 수 있다.

floatAttackRange =600.0f;

if (Distance<=AttackRange)
{
    std::cout<<"Enemy is in attack range."<< std::endl;
}
else
{
    std::cout<<"Enemy is too far."<< std::endl;
}

 

 

8. 언리얼에서의 FVector

일반적인 C++에서의 Vector3와 언리얼에서의 FVector를 비교해보자.

더보기
일반적인 Vector3 Unreal FVector
Vector3 FVector
X, Y, Z X, Y, Z
Length() Size()
Normalize() GetSafeNormal()
Target - Start TargetLocation - CurrentLocation
Position + Movement AddActorWorldOffset() 또는 SetActorLocation()

 

 

9. 언리얼에서의 C++ 핵심 코드

if (TargetActor==nullptr)
{
	return;
}

FVectorCurrentLocation =GetActorLocation();
FVectorTargetLocation =TargetActor->GetActorLocation();

FVectorToTarget =TargetLocation-CurrentLocation;
floatDistance =ToTarget.Size();
FVectorDirection =ToTarget.GetSafeNormal();

FVectorMovement =Direction*MoveSpeed*DeltaTime;
AddActorWorldOffset(Movement);

 

 

10. 과제

Vector3를 이용해서 거리 판정과 이동량을 계산해보자.

Player Position = (100, 50, 0)
Enemy Position  = (500, 350, 0)
Speed = 600
DeltaTime = 1 / 60
AttackRange = 450

이렇게 주어진 상황에서, 다음을 계산해보자

1. Player에서 Enemy를 향하는 벡터를 구한다.
2. Player와 Enemy 사이의 거리를 구한다.
3. Enemy가 AttackRange 안에 있는지 판정한다.
4. Player가 Enemy 방향으로 한 프레임 동안 이동할 양을 구한다.
5. Player의 다음 위치를 구한다.
6. Unreal의 FVector로 작성한다면 어떤 함수와 연결되는지 정리한다.

 

<해답>

1. 방향 : (400, 300, 0)
2. 거리 : 500
3. 결과값 : false
4. 단위 벡터 : (0.8, 0.6, 0) / 이동 스칼라 값 : 10, 프레임당 이동 벡터 : (0.8 * 10, 0.6 * 10, 0 * 10)
   결과 : (8, 6, 0)
5. Player 다음 위치 : (108, 56, 0)
6.

struct Vector3 {
    float X, Y, Z;
    
    Vector3 operator-(const Vector3& V) const { return {X - V.X, Y - V.Y, Z - V.Z}; }
    Vector3 operator+(const Vector3& V) const { return {X + V.X, Y + V.Y, Z + V.Z}; }
    Vector3 operator*(float Scale) const { return {X * Scale, Y * Scale, Z * Scale}; }
};

void CalculatePlayerMovement()
{
    FVector PlayerPos = FVector(100.f, 50.f, 0.f);
    FVector EnemyPos = FVector(500.f, 350.f, 0.f);
    float Speed = 600.f;
    float DeltaTime = 1.0f / 60.0f;
    float AttackRange = 450.f;

    FVector Direction = EnemyPos - PlayerPos;

    float Distance = FVector::Dist(PlayerPos, EnemyPos);

    bool bCanAttack = Distance <= AttackRange;

    FVector MovementDelta = Direction.GetSafeNormal() * (Speed * DeltaTime);

    FVector NextPlayerPos = PlayerPos + MovementDelta;
}


*오늘의 총평*

 

언리얼 에디터를 다루는 방법을 열심히 따라하면서 연습하고 있다. 무엇보다도 많이 써보고 익숙해지는 것이 제일 중요한 것 같다. 그 외에는 C++ 기본기를 갖추는 데에 힘썼다.

 

'TIL' 카테고리의 다른 글

06.23 TIL  (0) 2026.06.23
06.22 TIL  (0) 2026.06.22
06.18 TIL  (0) 2026.06.18
06.17 TIL  (0) 2026.06.17
06.12 TIL  (0) 2026.06.12