TIL
06.04 TIL (Ch.3 개인 과제 발제)
2026. 6. 4. 19:44

일정표

시간 할 일 비고
08:30~10:00 코드카타, Atanny, 프로그래머스  
10:00~11:00 CH3 개인과제 발제  
11:00~13:00 CH3 개인과제 구조 설계, 강의 듣기  
14:00~18:00 강의 듣기, 언리얼 사용법 배우기 개인 노션 파기
19:00~21:00 강의 듣기, 프로그래머스  
21:00~22:00 운동  
22:00~23:30 개인과제 진행  

*오늘의 코드카타*

 

문제. 수박수박수

더보기

길이가 n이고, "수박수박수박수...."와 같은 패턴을 유지하는 문자열을 리턴하는 함수, solution을 완성하세요. 예를들어 n이 4이면 "수박수박"을 리턴하고 3이라면 "수박수"를 리턴하면 됩니다.

#include <string>

using namespace std;

string solution(int n) {
    string answer = "";
    
    for (int i = 0; i < n; i++) {
        if (i % 2 == 0 ) {
            answer += "수";
        }
        else {
            answer += "박";
        }
    }
    return answer;
}

문제. 내적

더보기

길이가 같은 두 1차원 정수 배열 a, b가 매개변수로 주어집니다. a와 b의 내적을 return 하도록 solution 함수를 완성해주세요.

이때, a와 b의 내적은 a[0] * b[0] + a[1] * b[1] + ... + a[n-1] * b[n-1] 입니다.(n은 a, b의 길이)

#include <vector>

using namespace std;

int solution(vector<int> a, vector<int> b) {
    int answer = 0;
    
    int len = a.size();
    for (int i = 0; i < len; i++) {
        answer += a[i]*b[i];
    }
    return answer;
}

(1) 일반적인 for문으로 answer 구하기

#include <vector>

using namespace std;

int solution(const vector<int> a, const vector<int> b) {
    int answer = 0;
    int len = a.size();
    for (int i = 0; i < len; i++) {
        answer += a[i]*b[i];
    }
    return answer;
}

(2) 매개변수를 '값'으로 전달받지 않고 상수 참조로 전달하여 성능 개선하기

 

#include <vector>
#include <numeric>

using namespace std;

int solution(const vector<int> a, const vector<int> b) {
    return inner_product(a.begin(), a.end(), b.begin(), 0);
}

(3) <numeric> 헤더에 있는 inner_product(내적을 구하는 함수)를 이용하기.


*프로그래머스*

1. [기초] 문자열의 앞의 n글자

더보기

문제 설명

문자열 my_string과 정수 n이 매개변수로 주어질 때, my_string의 앞의 n글자로 이루어진 문자열을 return 하는 solution 함수를 작성해 주세요.

<제한사항>

my_string은 숫자와 알파벳으로 이루어져 있습니다.

1 ≤ my_string 의 길이 ≤ 1,000

1 ≤ n ≤ my_string의 길이

#include <string>

using namespace std;

string solution(string my_string, int n) {
    string answer = "";
    
    for (int i = 0; i < n; i++) {
        answer += my_string[i];
    }
    return answer;
}

(1) for 반복문을 사용해서 풀기

#include <string>

using namespace std;

string solution(string my_string, int n) {
    string answer = "";
    
    answer = my_string.substr(0, n);
    
    return answer;
}

(2) substr를 이용해서 문자열을 잘라내기

 

#include <string>

using namespace std;

string solution(string my_string, int n) {
    string answer = "";
    
    answer.reserve(n);
    
    for (int i = 0; i < n; i++) {
        answer += my_string[i];
    }
    
    return answer;
}

(3) reserve를 사용해서 메모리 최적화하기

문제 상황과 같이 어차피 자를 문자열의 길이(n)가 정해져 있다면, 미리 해당 공간만큼의 메모리를 할당해서 메모리 재할당이 발생하지 않도록 하는 것이 메모리 최적화의 첫걸음이다.

 

2. [기초] 접두사인지 확인하기

더보기

문제 설명

어떤 문자열에 대해서 접두사는 특정 인덱스까지의 문자열을 의미합니다. 예를 들어, "banana"의 모든 접두사는 "b", "ba", "ban", "bana", "banan", "banana"입니다. 문자열 my_string과 is_prefix가 주어질 때, is_prefix가 my_string의 접두사라면 1을, 아니면 0을 return 하는 solution 함수를 작성해 주세요.

my_string과 is_prefix는 영소문자로만 이루어져 있습니다.

#include <string>

using namespace std;

int solution(string my_string, string is_prefix) {
    int answer = 0;
    int len = is_prefix.length();
    
    if (my_string.substr(0, len) == is_prefix) {
        answer = 1;
    }
    else {
        answer = 0;
    }
    return answer;
}

(1) substr을 이용해서 같은 길이만큼 잘라내고 둘을 비교해서 같으면 1을 반환하게 했다.

 

#include <string>

using namespace std;

int solution(string my_string, string is_prefix) {
    
    int len = is_prefix.length();
    
    if (my_string.length() < len) {
        return 0;
    }
    
    if (my_string.substr(0, len) == is_prefix) {
        return 1;
    }
    return 0;
}

(2) 애초에 is_prefix 문자열의 길이가 더 길다면 접두사가 될 수 없으므로, 먼저 조건문으로 걸러주면 불필요한 연산을 조금이라도 더 줄일 수 있다.

 

#include <string>

using namespace std;

int solution(string my_string, string is_prefix) {
    return my_string.starts_with(is_prefix) ? 1 : 0;
}

(3) 만약 코딩테스트 환경이 아니라 C++ 20 버전의 최신 환경일 경우, 문자열이 특정 문자열로 시작하는지 직관적으로 확인할 수 있는 starts_with이라는 편리한 함수가 추가되어 있으므로 이를 활용해서 짧게 작성할 수도 있다.

 

3. [기초] 문자열 뒤집기

더보기

문제 설명

문자열 my_string과 정수 s,e가 매개변수로 주어질 때, my_string 에서 인덱스 s부터 e까지를 뒤집은 문자열을 return하는 solution 함수를 작성해주세요.

<제한 사항>

my_string은 숫자와 알파벳으로만 이루어져 있습니다.

1 <= my_string의 길이 <= 1,000

0 <= s <= e < my_string의 길이

#include <string>
#include <algorithm>

using namespace std;

string solution(string my_string, int s, int e) {
    reverse(my_string.begin() + s, my_string.begin() + e + 1);
    return my_string;
}

(1) 문자열을 뒤집는 것은 알고리즘 헤더의 reverse로 쉽게 할 수 있다. reverse(시작 반복자, 끝 반복자) 로 사용한다. 시작 위치부터 끝 위치 바로 앞까지의 순서를 뒤집기 때문에(마지막 위치를 포함하지 않음) 인덱스 e에 있는 문자까지 포함해서 뒤집으려면 e + 1을 전달해주어야 한다.

 

#include <string>

using namespace std;

string solution(string my_string, int s, int e) {
    
    while (s < e) {
        swap(my_string[s], my_string[e]);
        s++;
        e--;
    }
    return my_string;
}

(2) 양 끝에 인덱스를 두고, 서로의 문자를 swap 하며 가운데로 좁혀 들어오는(s++; e--;) 방식을 사용할 수도 있다.

 

4. [기초] 세로 읽기

더보기

문제 설명

문자열 my_string과 두 정수 m, c 가 주어집니다. my_string을 한 줄에 m 글자씩 가로로 적었을 때 왼쪽부터 세로로 c번째 열에 적힌 글자들을 문자열로 return하는 solution 함수를 작성해 주세요.

<제한 사항>

my_string은 영소문자로 이루어져 있습니다.

1 <= m <= my_string의 길이 <= 1,000

m은 my_string 길이의 약수로만 주어집니다.

1 <= c <= m

#include <string>

using namespace std;

string solution(string my_string, int m, int c) {
    string answer = "";
    
    for (int i = 0; i < my_string.length(); i++) {
        if (i % m == (c - 1)) {
            answer += my_string[i];
        }
    }
    return answer;
}

(1) 문제 규칙을 찾아서 코드 작성하기

 

#include <string>

using namespace std;

string solution(string my_string, int m, int c) {
    string answer = "";
    
    answer.reserve(my_string.length() / m + 1);
    
    for (int i = (c - 1); i < my_string.length(); i += m) {
        answer += my_string[i];
    }
    return answer;
}

(2) 메모리 최적화 하기

reserve를 사용해서 answer 문자열의 최대 길이를 미리 잡아둔다. string의 += 연산 시 발생하는 불필요한 메모리 재할당을 막기 위해 자주 사용된다.

for 문에서도 애초에 c번째 열부터 시작하고, m씩 건너뛰며 반복하게 만든다.

 

5. [기초] qr code

더보기

문제 설명

두 정수 q, r 과 문자열 code가 주어질 때, code의 각 인덱스를 q로 나누었을 때 나머지가 r인 위치의 문자를 앞에서부터 순서대로 이어 붙인 문자열을 return 하는 solution 함수를 작성해주세요.

0 <= r < q <= 20

r < code의 길이 <= 1,000

code는 영소문자로만 이루어져 있습니다.

#include <string>

using namespace std;

string solution(int q, int r, string code) {
    string answer = "";
    answer.reserve(code.length() / q + 1);
    
    for (int i = r; i < code.length(); i += q) {
        answer += code[i];
    }
    return answer;
}

(1) string 메모리 재할당 막으면서 작성하기.


 

*Atanny*

1. 동기/비동기 처리와 Delegate 활용

 

동기 처리와 비동기 처리 

동기 처리(Synchronous)는 작업이 완료될 때까지 대기하며 순차적으로 실행된다. 반면 비동기 처리(Asynchronous)는 작업 완료를 기다리지 않고 다음 작업을 진행하며 메인 스레드를 차단하지 않는다.

 

Unreal Engine에서 Delegate의 주요 역할 

Delegate는 언리얼 엔진에서 함수 포인터를 안전하게 관리하고, 이벤트 기반 프로그래밍을 구현할 수 있게 해주는 시스템이다. 특정 이벤트가 발생했을 때 미리 등록된 함수들을 호출할 수 있어, 객체 간의 느슨한 결합(loose coupling)을 가능하게 한다.

 

Unreal Engine에서 Timer를 사용하는 주요 목적 

일정 시간 후에 특정 함수를 자동으로 호출하거나, 반복적으로 함수를 일정 간격으로 실행하거나, 지연된 실행(delayed execution)을 비동기적으로 처리하기 위해 사용한다. Timer는 게임 루프를 멈추지 않고 비동기적으로 작동한다.

 

비동기 처리가 요구되는 상황 

네트워크 통신, 파일 로딩, 데이터베이스 쿼리 등 시간이 오래 걸리는 I/O 작업은 비동기 처리가 필수적이다. 이러한 작업을 동기적으로 처리하면 게임이 멈춰 사용자 경험이 크게 저하된다.

 

Timer를 사용하여 3초 후에 함수를 호출하기 

FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(
    TimerHandle,
    this,
    &AMyActor::MyFunction,
    3.0f,
    false
);

SetTimer 함수의 네번째 인자는 시간을 초(second) 단위의 float 값으로 받는다. 따라서 3초를 표현하려면 3.0f를 사용해야 한다.

 

 

2. 네트워크 지연과 패킷 손실의 영향 분석

 

네트워크 지연(Latency) 

Latency는 데이터 패킷이 송신자로부터 수신자에게 도달하는 데 걸리는 시간을 의미하며, 일반적으로 밀리초(ms)단위로 측정된다.

 

높은 네트워크 지연(200ms 이상)이 발생했을 때 나타나는 현상

높은 네트워크 지연은 게임플레이의 반응성과 동기화에 영향을 미친다. 예를 들어, 적을 조준하고 사격했지만 탄환이 맞지 않는 것처럼 보이거나, 플레이어의 이동이 끊기거나 순간이동하는 것처럼 보이거나, 입력한 명령과 실제 반응 사이에 시간차가 발생한다.

 

패킷 손실(Packet Loss)이 게임 플레이에 미치는 영향 

패킷 손실은 네트워크를 통해 전송되는 데이터 패킷이 목적지에 도달하지 못하는 현상이다. 이로 인해 위치, 상태 등의 중요한 정보가 누락되어 오브젝트가 순간이동하거나 사라지는 등의 시각적 문제가 발생하게 된다.

 

 

3. 네트워크 동기화와 데이터 일관성 관리

 

언리얼 엔진의 네트워크 복제(Replication)에서 Server Authority(서버 권한) 개념 

Server Authority는 언리얼 엔진 네트워크 아키텍처의 핵심 원칙으로, 서버만이 복제된 변수의 값을 변경할 권한을 가진다. 클라이언트는 서버로부터 변경사항을 받아 동기화만 수행하며, 이를 통해 데이터 일관성을 유지한다. 클라이언트가 직접 변수를 수정할 수 있게 하는 것은 Server Authority 원칙에 위배된다.

 

Replicated 변수를 선언하는 방법 

언리얼 엔진에서 네트워크 복제를 위한 변수를 선언할 때는 UPROPERTY(Replicated) 매크로를 사용한다. 또한 GetLifetimeReplicatedProps 함수에서 DOREPLIFETIME 매크로를 통해 복제 설정을 추가해야 한다.

 

RepNotify와 일반 Replicated 변수의 차이점 

RepNotift의 핵심 특징은 '변수 값이 변경될 때마다 지정된 콜백 함수(OnRep_ 함수)가 자동으로 호출된다는 것이다. 이를 통해 변수 변경에 대한 즉각적인 반응(UI 업데이트, 시각 효과 등)을 구현할 수 잇다.

일반 Replicated 변수는 값만 동기화되며 별도의 콜백이 호출되지 않는다.

두 방식 모두 Server Authority 원칙을 따뜨려, 복제 속도는 동일하다.

 

RepNotify 콜백 함수 사용하기 

UPROPERTY(ReplicatedUsing=OnRep_Score)
int32 Score;

UFUNCTION()
void AMyCharacter::OnRep_Score()
{
    UpdateScoreUI();
}

 

RepNotify 콜백 함수(OnRep_)는 일반 UFUNCTION()으로 선언하면 된다. 이 함수는 변수가 복제될 때 자동으로 클라이언트에서 호출되므로, Server, Client, NetMulticast 같은 RPC(Remote Procedure Call) 지정자가 필요 없다. 

 

클라이언트에서 서버의 복제 변수를 변경하는 방법 

Server Authority 원칙에 따라 복제 변수는 서버에서만 수정할 수 있다. 클라이언트가 변수를 변경하고 싶다면 Server RPC를 통해 서버에 요청을 보내고, 서버가 유효성을 검증한 후 변수를 수정해야 한다. 클라이언트가 직접 복제 변수를 수정하면 로컬에서만 변경되고 다른 클라이언트나 서버에는 반영되지 않으며, 곧 서버 값으로 덮어씌워진다.

 

4. AI 행동 트리 구조 이해

 

언리얼 엔진에서 AI Controller의 주요 역할 

AI Controller는 AI Pawn을 제어하는 주체로, Behavior Tree를 실행하고 AI의 의사결정을 담당한다.

참고로, Blackboard는 데이터 저장, Behavior Tree는 행동 로직 구성, Animation Blueprint는 애니메이션 제어를 담당한다.

 

Blackboard 

Blackboard는 AI의 메모리 역할을 하는 데이터 저장소이다. AI의 상태 정보와 데이터를 키-값 쌍으로 저장하고, Behavior Tree의 노드들이 공유 데이터에 접근할 수 있게 한다. Vector, Object, Bool 등 다양한 타입의 데이터를 저장할 수 있다.

 

Behavior Tree 

Behavior Tree는 트리 구조로 AI 행동을 시각적으로 구성할 수 있어 복잡한 AI 로직을 직관적으로 표현할 수 있다. FSM(Finite State Machine)보다 복잡한 행동 표현에 유리하며, 블루프린트와 C++ 모두 사용 가능하고, AI Controller에 의해 실행되어야 한다.

 

AI Controller, Behavior Tree, Blackboard의 상호 연동 구조 

AI Controller가 Behavior Tree를 소유하고 실행하며, Behavior Tree의 각 노드들이 필요에 따라 Blackboard의 데이터를 읽거나 쓰게 된다.

 

5. NavMesh와 경로탐색 알고리즘 이해

 

NavMesh(Navigation Mesh)

NavMesh는 AI 캐릭터가 이동 가능한 영역을 3D 메시 형태로 표현한 것으로, 이동 가능한 영역을 폴리곤으로 분할하여 표현한다. 또한, 일반적으로 사전에 Baking 과정을 통해 생성한다. 즉, 성능 최적화를 위해 정적인 환경에서는 미리 계산된 NavMesh를 사용하게 된다. NavMesh를 사용하면 캐릭터가 장애물을 피해 목적지까지 경로를 찾을 수 있게 된다.

 

A* 알고리즘에서 사용하는 평가 함수 f(n) = g(n) + h(n) 

A* 알고리즘의 평가 함수에서 g(n)은 시작 노드부터 현재 노드까지의 실제 이동 비용이고, h(n)은 현재 노드부터 목표 노드까지의 휴리스틱(추정) 비용이다. 이 두 값을 합산한 f(n)을 기준으로 가장 유망한 노드를 선택하여 탐색하게 된다. g(n)은 실제 계산된 값이고, h(n)만 휴리스틱 함수로 추정된다.

 

Dijkstra 알고리즘과 A* 알고리즘의 차이점 

Dijkstra 알고리즘은 시작 노드부터의 실제 비용만을 고려하여 모든 방향으로 균등하게 탐색하는 반면, A* 알고리즘은 휴리스틱 함수 h(n)을 사용하여 목표 지점까지의 예상 거리를 고려함으로써 목표 방향으로 우선적으로 탐색한다. 이로 인해 A*가 일반적으로 더 효율적이다. 두 알고리즘 모두 적절한 조건에서 최단 경로를 보장하고, 가중치 그래프에 사용 가능하다.

 

NavMesh 기반 경로 탐색 

NavMesh 기반 경로 탐색에서 'NavMesh Agent' 는 NavMesh 위에서 이동 가능한 경로를 계산하고, 설정된 속도와 가속도에 따라 캐릭터를 이동시키며, 장애물 회피(Obstacle Avoidance) 기능을 수행한다. 즉, 이미 생성된 NavMesh 위에서 경로를 탐색하고 이동하는 컴포넌트이다.

NavMesh 데이터 자체의 생성과 업데이트는 별도의 NavMesh 베이킹 프로세스나 동적 NavMesh 시스템에서 담당한다.

 

6. Behavior Tree 노드와 AI 행동 분기

 

Behavior Tree에서 Task 노드의 역할 

Task 노드는 AI가 실제로 수행할 구체적인 행동을 실행하는 리프 노드이다. 실행 후 Success 또는 Failure를 반환한다.

 

Behavior Tree에서 Decorator 노드의 역할

Decorator 노드는 특정 조건을 평가하여 하위 노드의 실행 여부를 결정하는 노드이다. 즉, 조건 평가를 통해 하위 노드의 실행을 제어하는 노드이다. 하위 노드가 실행되는 동안 지속적으로 조건을 체크하고, 조건이 만족될 때만 하위 노드를 실행하도록 제어한다. 또한, 실행 중인 하위 노드를 중단시킬 수 있다.

 

Behavior Tree에서 Service 노드의 역할 

Service 노드는 일정 주기마다 데이터를 갱신하거나 환경을 감지하는 노드이다. 특정 브랜치가 활성화되어 있는 동안 설정된 주기(Interval)마다 반복 실행되며, 주로 Blackboard 값 갱신이나 환경 감지에 사용된다.

 

Behavior Tree에서 Composite 노드의 역할 

Composite 노드는 여러 개의 자식 노드를 가지고 어떤 순서로 자식 노드를 실행할지, 그리고 자식 노드의 결과에 따라 자신의 Success/Failure 여부를 결정한다. 대표적인 Composite 노드로 SequenceSelector가 있다.

Sequence : 자식 노드를 왼쪽에서 오른쪽으로 순차적으로 실행하고, 자식 노드 중 하나라도 실패(Failure)하면 즉시 실행을 멈추고 Sequence 노드 자체도 Fail을 반환한다. (And 역할)

Selector : 자식 노드들을 왼쪽에서 오른쪽으로 순차적으로 실행하고, 자식 노드 중 하나라도 성공(Success)하면 즉시 실행을 멈추고 Selector 노드 자체도 Success를 반환한다. (Or 역할)

 

Behavior Tree에서 Parallel 노드의 역할 

Parallel 노드의 경우 언리얼 엔진에서는 Simple Parallel 노드만을 제공한다. 즉 정확히 두 개의 자식 연결만 허용하게 된다.

두 개의 자식은 Main Task와 Background Tree로 구분된다. 메인 태스크가 실행되는 동안 동시에 실행될 서브트리가 Background Tree라고 할 수 있다. 예를 들어, 메인 태스크가 '이동하기'라면, 백그라운드 트리는 '이동하면서 총 쏘기'가 된다.

이후 메인 태스크가 종료되었을 때 백그라운드 트리를 Immediate(즉각 종료)하거나, Delayed(지연 종료)하도록 설정해야 한다.

 

AI 행동 분기:

Behavior Tree는 AI가 무엇을 해야 하는지 행동 분기를 시각적으로 처리해 놓은 흐름도이다.

 

1) 행동이 분기되려면 가장 먼저 조건을 제시해야 한다. Decorator 노드가 이를 담당하며, Decorator 노드는 Blackboard를 확인하고 해당 분기(Branch)로 진입할 자격을 검사하게 된다. [Composite 노드 위에 붙음]

 

2) Service노드해당 분기(Branch)가 실행 되는 동안, 백그라운드에서 주기적으로 C++ 함수를 호출하여 Blackboard의 값을 계속 업데이트 한다.

 

3) Decorator 노드가 검사를 한 후 해당 분기에 진입할 자격이 주어졌다면, Composite 노드에 따라(Sequence 혹은 Selector) 순서대로 Task를 진행하게 된다. 이 때 Task를 Parallel 로 진행할 수 있고, 또는 단일로도 진행할 수 있다.


*수학 공부*

1. 가속도와 마찰력의 수학적 개념

 

가속도(Acceleration)와 마찰력(Friction) 및 감쇠(Damping)개념은 게임 물리 엔진을 이해하는 핵심 개념이고, 이를 '오일러 적분' 방식을 이용해서 컴퓨터가 계산한다.

 

가속도 : 1초당 속도가 변화하는 양으로, 키보드 방향키를 누르고 있으면 가속도가 붙어 캐릭터가 점점 빨라지거나 하는 식으로 구현된다.

마찰력/감쇠 : 운동을 방해하는 힘으로, 속도의 반대 방향으로 작용한다. 유저가 입력을 멈췄을 때 캐릭터가 뚝 멈추지 않고 미끄러지듯 멈추게 하거나 부드러운 점프와 착지에 사용된다.

 

2. 오일러 적분

컴퓨터는 현실 세계처럼 시시각각 변하는 연속적인 시간을 계산할 수 없다. 대신 델타 타임(delta t)이라는 매우 짧은 시간 조각 단위로 끊어서 근사치를 계산해서 사용하게 된다. 이 방식을 오일러 적분 이라고 부른다.

매 프레임마다 물리 엔진은 아래의 순서로 캐릭터의 상태를 갱신한다.

1) 가속도 계산 : 현재 캐릭터가 받는 힘과 마찰력을 조합해 최종 가속도를 구한다.

2) 속도 갱신 : 현재 속도에 (가속도 * delta t)를 더해 새로운 속도를 구한다.

   Velocity = Velocity + Acceleration * delta t

3) 위치 갱신 : 현재 위치에 (속도 * delta t)를 더해서 새로운 위치를 구한다.

   Position = Position + Velocity * delta t

 

3. C++에 적용하기

구조체에 가속도, 속도, 마찰력 개념을 도입해서 오일러 적분 기반의 물리 동역학을 구현한 코드를 만들 수 있다.

#include <iostream>
#include <cmath>

struct Vector2 {
    float x, y;
    Vector2(float _x = 0.0f, float _y = 0.0f) : x(_x), y(_y) {}
    Vector2 operator+(const Vector2& rhs) const { return Vector2(x + rhs.x, y + rhs.y); }
    Vector2 operator-(const Vector2& rhs) const { return Vector2(x - rhs.x, y - rhs.y); }
    Vector2 operator*(float scalar) const { return Vector2(x * scalar, y * scalar); }
};

struct PhysicsCharacter {
    Vector2 position;     // 현재 위치
    Vector2 velocity;     // 현재 속도 (프레임마다 가속도에 의해 변함)
    float accelerationScalar; // 키보드 입력 시 붙는 가속도의 세기
    float friction;       // 마찰 계수 (0에 가까울수록 빙판길, 클수록 뻑뻑함)

    PhysicsCharacter(Vector2 startPos, float acc, float fric) 
        : position(startPos), velocity(0.0f, 0.0f), accelerationScalar(acc), friction(fric) {}

    // 매 프레임 호출되는 물리 업데이트 (오일러 적분)
    void Update(Vector2 inputDirection, float deltaTime) {
        // 1. 입력에 따른 가속도 설정
        Vector2 acceleration = inputDirection * accelerationScalar;

        // 2. 오일러 적분 1단계: 속도 갱신 (Velocity = Velocity + Acceleration * dt)
        velocity = velocity + (acceleration * deltaTime);

        // 3. 마찰력 적용 (속도가 있을 때, 속도의 반대 방향으로 속력을 줄임)
        // 단순화된 감쇠(Damping) 공식을 사용: 매 프레임 속도를 일정 비율로 깎음
        velocity = velocity * (1.0f - friction * deltaTime);

        // 4. 오일러 적분 2단계: 위치 갱신 (Position = Position + Velocity * dt)
        position = position + (velocity * deltaTime);
    }
};

int main() {
    // 가속도 세기 10.0, 마찰력 3.0의 캐릭터 생성
    PhysicsCharacter player(Vector2(0.0f, 0.0f), 10.0f, 3.0f);
    float dt = 0.016f; // 60fps 가정

    // 1. 유저가 오른쪽(1.0, 0.0)으로 가속하는 상황 (5프레임)
    std::cout << "--- 키보드를 누르고 있는 상태 (점점 가속됨) ---\n";
    for (int i = 1; i <= 5; ++i) {
        player.Update(Vector2(1.0f, 0.0f), dt);
        std::cout << "[프레임 " << i << "] 속도X: " << player.velocity.x << ", 위치X: " << player.position.x << "\n";
    }

    // 2. 유저가 키보드에서 손을 뗀 상황 (입력 0.0, 마찰력에 의해 감속)
    std::cout << "\n--- 키보드에서 손을 뗀 상태 (마찰력으로 미끄러지며 감속) ---\n";
    for (int i = 6; i <= 10; ++i) {
        player.Update(Vector2(0.0f, 0.0f), dt);
        std::cout << "[프레임 " << i << "] 속도X: " << player.velocity.x << ", 위치X: " << player.position.x << "\n";
    }

    return 0;
}

단순 등속도 운동과 달리 가속도와 마찰력을 추가해서 캐릭터가 움직일 때 무게감과 관성을 느껴지도록 만들 수 있다.

 

언리얼 엔진에서도 기본적으로 물리 엔진을 제공하므로 가속도, 관성같은 요소들을 구현하는 것은 그리 어렵지 않다. 다만, 재미있는 게임을 만들기 위해서는 직접 사용할 수 있어야 하는 이유가 있다. 기본 물리 엔진은 굴러다니는 돌멩이나 상자 등에 사용되고, 플레이어의 경험과 직결되는 모든 핵심적인 움직임은 프로그래머가 직접 수학을 바탕으로 통제할 수 있어야 한다.

 

1) 조작감 개선을 위한 캐릭터 컨트롤러

기본 물리 엔진을 캐릭터에 그대로 적용하면 내가 원하는 게임 방식에 맞지 않는 형태로 다르게 움직일 수 있다. 따라서 내가 의도한 조작감과 플레이어의 최상의 경험을 위해서는 엔진의 물리를 끄고(Kinematic 상태), 프로그래머가 직접 속도와 가속도를 통제해야 한다.

예를 들어, 캐릭터가 대시(Dash)하는 스킬을 사용할 수 있다고 했을 때, 엔진의 힘을 주는 대신 순간적으로 속도를 덮어씌우고 마찰력을 커스텀해 놓은 상태에서 정확히 원하는 거리만 미끄러지게 만드는 로직을 C++로 직접 구현해야 한다.

 

2) AI의 궤적 예측과 리드 샷(Trajectory Prediction)

물리 엔진은 현재 상태를 다음 프레임으로 넘겨줄 뿐이다. 포물선으로 날아가는 수류탄의 궤적을 화면에 점선으로 미리 그려주거나, 적 AI가 달리고 있는 플레이어의 앞을 향해 예측 사격을 하도록 만드려면 결국 가속도와 속도 공식을 활용할 수 있어야 한다.

예를 들어, 등가속도 운동 공식(P = P0 + Vt + 1/2 * a * t^2)을 C++ 코드에 적용해서 타겟이 t초 뒤에 어디에 있을지 수학적으로 미리 계산하고, 그 위치를 향해 발사하도록 AI를 프로그래밍할 수 있다.

 

3) 멀티플레이어 게임의 네트워크 동기화

온라인 게임에서는 서버와 클라이언트 사이에 반드시 지연시간(Ping)이 존재한다. 서버에서 보내주는 캐릭터의 위치만 그대로 화면에 그리면 캐릭터가 끊기면서 순간이동을 하게된다. 따라서 Client Prediction을 사용해야 한다.

예를 들어, 클라이언트는 서버의 응답을 기다리지 않고 유저의 입력(가속도)과 현재 속도를 바탕으로 오일러 적분을 사용해 스스로 위치를 예측해서 먼저 움직인다. 이후 서버의 진짜 위치가 도착하면 자연스럽게 보정을 해주는 네트워크 프로그래밍에 필수적으로 수학적 개념이 사용된다.

 

4) 절차적 애니메이션(Procedual Animation)과 카메라 연출

폭발이 일어났을 때 화면이 흔들리거나, 총구가 반동으로 미렸다가 제자리로 돌아오는 연출은 물리 엔진의 몫이 아니다. 목표 위치와 현재 위치 사이의 거리를 계산하고, 탄성력(가속도)과 감쇠(마찰력)를 수학적으로 섞어서 매 프레임 위치를 업데이트하는 훅의 법칙(Hooke's Law) 기반 스프링 수학을 직접 C++로 구현해서 부드러운 연출을 만들어낸다.

 

4. (종합) 유도 미사일 알고리즘을 C++코드로 구현해보기

벡터의 뺄셈, 정규화, 내적, 외족, 델타 타임 개념을 활용해서 유도미사일 알고리즘을 C++ 코드로 구현해보자.

'유도' 기능은 매 프레임마다 즉시 방향을 타겟 쪽으로 고정하는 '즉시 조준 방식'이 있고, 미사일에 선회력 제한을 두어 타겟을 향해 현실적인 곡선을 그리며 날아가고 미사일이 빗나갔다가 다시 돌아오는 연출이 가능한 '선회력 제한 방식'이 있다. 이번에는 실무에서 주로  쓰이는 선회력 제한 방식을 구현해보자.

 

#include <iostream>
#include <cmath>

// [기본 뼈대] 2D 벡터 구조체
struct Vector2 {
    float x, y;
    Vector2(float _x = 0.0f, float _y = 0.0f) : x(_x), y(_y) {}

    Vector2 operator+(const Vector2& rhs) const { return Vector2(x + rhs.x, y + rhs.y); }
    Vector2 operator-(const Vector2& rhs) const { return Vector2(x - rhs.x, y - rhs.y); }
    Vector2 operator*(float scalar) const { return Vector2(x * scalar, y * scalar); }

    float Magnitude() const { return std::sqrt(x * x + y * y); }
    
    void Normalize() {
        float mag = Magnitude();
        if (mag > 0.0001f) { x /= mag; y /= mag; }
    }

    // 좌우 판별을 위한 2D 외적
    float Cross(const Vector2& rhs) const {
        return (x * rhs.y) - (y * rhs.x);
    }
};

// [유도 미사일 클래스]
class HomingMissile {
public:
    Vector2 position;      // 현재 위치
    Vector2 forward;       // 현재 미사일이 바라보는 정면 방향 (단위 벡터)
    float speed;           // 비행 속력 (초당 이동 거리)
    float rotationSpeed;   // 선회력 (라디안/초, 1초에 회전할 수 있는 최대 각도)

    HomingMissile(Vector2 startPos, Vector2 startDir, float flySpeed, float turnSpeed)
        : position(startPos), forward(startDir), speed(flySpeed), rotationSpeed(turnSpeed) {
        forward.Normalize();
    }

    // 매 프레임 미사일의 위치와 방향을 업데이트하는 함수
    void Update(Vector2 targetPos, float deltaTime) {
        // Step 1: 타겟을 향하는 정규화된 방향 벡터 구하기 (벡터의 뺄셈 + 정규화)
        Vector2 dirToTarget = targetPos - position;
        dirToTarget.Normalize();

        // Step 2: 외적을 통해 타겟이 미사일의 왼쪽에 있는지 오른쪽에 있는지 판별
        float crossResult = forward.Cross(dirToTarget);

        // Step 3: 현재 정면(forward) 벡터를 외적 부호에 따라 회전시킴
        // 복잡한 회전 행렬 대신, 선회력 제한을 표현하기 위해 각도를 델타 타임만큼 변형
        float currentAngle = std::atan2(forward.y, forward.x);
        
        // 외적 값이 0보다 크면 타겟이 왼쪽에 있으므로 반시계 방향(+) 회전
        // 0보다 작으면 타겟이 오른쪽에 있으므로 시계 방향(-) 회전
        if (crossResult > 0.01f) {
            currentAngle += rotationSpeed * deltaTime;
        }
        else if (crossResult < -0.01f) {
            currentAngle -= rotationSpeed * deltaTime;
        }

        // Step 4: 변경된 각도를 바탕으로 새로운 정면(forward) 벡터 갱신
        forward.x = std::cos(currentAngle);
        forward.y = std::sin(currentAngle);
        forward.Normalize();

        // Step 5: 위치 이동 (위치 = 위치 + 방향 * 속력 * 시간)
        position = position + (forward * speed * deltaTime);
    }
};

int main() {
    // (0,0) 위치에서 위쪽(0,1)을 바라보며 발사된 미사일
    // 속력: 초당 10미터, 선회력: 초당 약 1.57 라디안 (90도)
    HomingMissile missile(Vector2(0.0f, 0.0f), Vector2(0.0f, 1.0f), 10.0f, 1.57f);

    // 미사일 오른쪽에 가만히 서 있는 적 (5, 5)
    Vector2 enemyTarget(5.0f, 5.0f);
    
    float dt = 0.2f; // 시뮬레이션을 위해 델타 타임을 크게 설정 (0.2초씩 경과)

    std::cout << "타겟 위치: (" << enemyTarget.x << ", " << enemyTarget.y << ")\n\n";

    for (int frame = 1; frame <= 6; ++frame) {
        missile.Update(enemyTarget, dt);
        
        std::cout << "[프레임 " << frame << "]\n";
        std::cout << "미사일 위치: (" << missile.position.x << ", " << missile.position.y << ")\n";
        std::cout << "미사일 방향: (" << missile.forward.x << ", " << missile.forward.y << ")\n\n";
    }

    return 0;
}

(1) forward.Cross(dirToTarget) : 외적을 사용해서 결과가 양수인지 음수인지에 따라 currentAngle에 +, - , 즉 회전각을 더해줄 지 빼줄지 결정한다.

(2) rotationSpeed * deltaTime : 미사일의 회전이 프레임 레이트에 영향을 받으면 안되므로 초당 회전 각도에 deltaTime을 곱해서 프레임에 독립적인 회전을 구현한다.

 

이제 기본적으로 구현한 유도 미사일 로직에, 오일러 적분을 이용한 가속도와 관성을 합쳐서 '드리프트' 현상을 추가해보자.

관성의 법칙을 적용받는 미사일에는 어떤 변화가 있을까?

기존 미사일은 Position = Position + Velocity * delta t 의 단순한 계산으로 위치를 계산했다. 즉, 방향을 바꾸면 그 즉시 새로운 방향으로 날아갔다.

관성이 적용된 미사일에는 추진력(가속도), 관성(Velocity), 공기 저항(Friction) 개념이 들어가게 된다.

#include <iostream>
#include <cmath>

struct Vector2 {
    float x, y;
    Vector2(float _x = 0.0f, float _y = 0.0f) : x(_x), y(_y) {}
    Vector2 operator+(const Vector2& rhs) const { return Vector2(x + rhs.x, y + rhs.y); }
    Vector2 operator-(const Vector2& rhs) const { return Vector2(x - rhs.x, y - rhs.y); }
    Vector2 operator*(float scalar) const { return Vector2(x * scalar, y * scalar); }
    float Magnitude() const { return std::sqrt(x * x + y * y); }
    void Normalize() {
        float mag = Magnitude();
        if (mag > 0.0001f) { x /= mag; y /= mag; }
    }
    float Cross(const Vector2& rhs) const { return (x * rhs.y) - (y * rhs.x); }
};

class PhysicsHomingMissile {
public:
    Vector2 position;      // 현재 위치
    Vector2 velocity;      // 현재 속도 (관성을 유지하는 핵심 데이터)
    Vector2 forward;       // 미사일의 머리가 향하는 방향 (단위 벡터)

    float thrust;          // 엔진 추진력 (가속도의 세기)
    float drag;            // 공기 저항 (마찰 계수)
    float rotationSpeed;   // 선회력 (라디안/초)

    PhysicsHomingMissile(Vector2 startPos, Vector2 startDir, float engineThrust, float airDrag, float turnSpeed)
        : position(startPos), velocity(0.0f, 0.0f), forward(startDir), 
          thrust(engineThrust), drag(airDrag), rotationSpeed(turnSpeed) {
        forward.Normalize();
    }

    void Update(Vector2 targetPos, float deltaTime) {
        // [1단계: 조향 (Steering) - 기존과 동일]
        Vector2 dirToTarget = targetPos - position;
        dirToTarget.Normalize();

        float crossResult = forward.Cross(dirToTarget);
        float currentAngle = std::atan2(forward.y, forward.x);
        
        if (crossResult > 0.01f) {
            currentAngle += rotationSpeed * deltaTime; // 좌회전
        } else if (crossResult < -0.01f) {
            currentAngle -= rotationSpeed * deltaTime; // 우회전
        }

        forward.x = std::cos(currentAngle);
        forward.y = std::sin(currentAngle);
        forward.Normalize();

        // [2단계: 물리 적용 (오일러 적분)]
        // 1. 가속도 생성 (엔진은 항상 미사일의 정면으로 힘을 가함)
        Vector2 acceleration = forward * thrust;

        // 2. 속도에 가속도 누적 (v = v + a*dt) -> 관성 발생
        velocity = velocity + (acceleration * deltaTime);

        // 3. 공기 저항 적용 (속도를 서서히 깎아냄. 없으면 제어 불능 상태가 됨)
        velocity = velocity * (1.0f - drag * deltaTime);

        // 4. 최종 위치 이동 (p = p + v*dt)
        position = position + (velocity * deltaTime);
    }
};

int main() {
    // 추진력 50, 공기 저항 2.0 (1초에 속도의 일정량을 잃음), 선회력 약 90도/초
    PhysicsHomingMissile missile(Vector2(0.0f, 0.0f), Vector2(0.0f, 1.0f), 50.0f, 2.0f, 1.57f);
    Vector2 target(10.0f, 10.0f);
    float dt = 0.1f; 

    std::cout << "관성 유도 미사일 시뮬레이션";
    for (int frame = 1; frame <= 5; ++frame) {
        missile.Update(target, dt);
        std::cout << "[프레임 " << frame << "]\n";
        std::cout << "방향(Forward): (" << missile.forward.x << ", " << missile.forward.y << ")\n";
        // 방향과 속도가 일치하지 않음 (머리는 돌렸지만 몸은 원래 가던 방향으로 밀림)
        std::cout << "속도(Velocity): (" << missile.velocity.x << ", " << missile.velocity.y << ")\n\n";
    }

    return 0;
}

(3) 미사일에는 추진기가 있을 것이고, 항상 자신이 바라보는 정면으로 가속도를 만들어낸다. 가속도는 '속도 벡터'에 누적되며, 속도 벡터는 원래 날아가던 방향의 힘을 기억하고 있어서 방향이 바뀌어도 즉시 방향이 바뀌지 않는다. 즉, 누적되어있는 속도 벡터의 방향, 크기에 영향을 받는다. 다만, 우리가 가속도 개념을 적용하게 되면 '마찰력'도 반드시 포함되어야 한다. 마찰력이 없다면 미사일은 속도가 무한히 빨라져서 결국 타겟을 맞추지 못할 것이므로 기존 방향의 관성을 서서히 줄이는 마찰력(drag, 공기 저항)을 velocity = velocity * (1.0f - drag * deltaTime); 처럼 적용한다.


*Unreal C++*

이번 주제 : 언리얼 엔진 인터페이스(Interface)

 

언리얼 C++ 인터페이스는 다중 상속의 안전성 문제를 해결하고, C++ 코드와 블루프린트 간의 완벽한 상호 호환을 지원하기 위해 두 개의 클래스가 한 쌍을 이루는 고유한 구조를 가진다. 두 개의 클래스는 U 접두사 클래스, I 접두사 클래스가 있다.

 

(1) U 접두사 클래스(UInteractableInterface) : 엔진의 리플렉션 및 블루프린트 시스템이 인터페이스를 인식할 수 있도록 돕는 메타데이터 클래스이다. 개발자가 직접 내부 코드를 수정하지 않는다.

(2) I 접두사 클래스(IInteractableInterface) : 실제 C++ 개발자가 가상 함수를 선언하고, 다른 액터 클래스에서 다중 상속받아 기능을 구현하는 실제 인터페이스 클래스이다.

 

1) 사용방법

 

(1) 인터페이스를 정의하는 헤더 파일을 만든다. 예를 들어, MyInterface.h 파일을 살펴보자.

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "MyInterface.generated.h"

// 엔진 시스템용 U 클래스 (수정 금지)
UINTERFACE(MinimalAPI, Blueprintable)
class UMyInterface : public UInterface
{
    GENERATED_BODY()
};

// 실제 구현을 위한 I 클래스
class MYPROJECT_API IMyInterface
{
    GENERATED_BODY()

public:
    // 1. 순수 C++ 전용 인터페이스 함수 (상속받는 곳에서 반드시 구현해야 함)
    virtual void ReactToTrigger() = 0;

    // 2. C++와 블루프린트 공용 인터페이스 함수 (BlueprintNativeEvent)
    // C++에서 기본 로직을 만들고, 블루프린트에서 이를 덮어쓰거나 확장할 수 있음.
    UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Interaction")
    void ExecuteAction(AActor* Instigator);
};

 

(2) 인터페이스를 상속받는 액터의 헤더 파일을 만든다. 예를 들어, MyInterfaceActor.h 파일을 살펴보자.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyInterface.h" // 인터페이스 헤더 인클루드
#include "MyInterfaceActor.generated.h"

UCLASS()
class MYPROJECT_API AMyInterfaceActor : public AActor, public IMyInterface // 인터페이스 다중 상속
{
    GENERATED_BODY()
	
public:	
    AMyInterfaceActor();

    // 1. 순수 C++ 전용 인터페이스 구현
    virtual void ReactToTrigger() override;

    // 2. BlueprintNativeEvent 구현 시에는 함수 이름 뒤에 '_Implementation'을 붙여 오버라이드.
    virtual void ExecuteAction_Implementation(AActor* Instigator) override;
};

 

(3) 이후, 소스 파일을 만들어 실제 상호작용을 구현한다. 예를 들어, MyInterfaceActor.cpp 파일을 살펴보자.

#include "MyInterfaceActor.h"

AMyInterfaceActor::AMyInterfaceActor()
{
    PrimaryActorTick.bCanEverTick = false;
}

// 순수 C++ 인터페이스의 구현부
void AMyInterfaceActor::ReactToTrigger()
{
    // C++ 내부에서만 동작할 트리거 반응 로직을 작성
    UE_LOG(LogTemp, Log, TEXT("C++ Interface: ReactToTrigger 호출"));
}

// BlueprintNativeEvent의 구현부 (실제 실행 로직)
void AMyInterfaceActor::ExecuteAction_Implementation(AActor* Instigator)
{
    if (Instigator)
    {
        // 상호작용을 유발한 대상(Instigator)의 이름을 로그로 출력
        UE_LOG(LogTemp, Log, TEXT("Interface Action Executed by: %s"), *Instigator->GetName());
    }
}

 

2) 인터페이스와 다형성을 활용한 디커플링(Decoupling) 과정

 

언리얼 엔진에서 인터페이스를 사용하는 가장 흔한 예시는 상호작용 시스템이다. 플레이어가 다양한 객체와 상호작용할 때, 각각의 클래스로 형변환을 하지 않고 공통된 인터페이스 함수 하나만 호출하는 것이 핵심이다.

 

(1) 인터페이스 설계하기

// Interactable.h
#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Interactable.generated.h"

// 엔진 리플렉션용 더미 클래스
UINTERFACE(MinimalAPI)
class UInteractable : public UInterface
{
    GENERATED_BODY()
};

// 실제 구현부가 들어가는 클래스
class MYPROJECT_API IInteractable
{
    GENERATED_BODY()

public:
    // BlueprintNativeEvent: C++와 블루프린트 모두에서 구현/호출 가능하도록 설정.
    // 주의: 이 함수는 C++에서 '직접' 가상 함수(virtual)로 선언하지 않음. 
    // 언리얼 헤더 툴이 자동으로 생성해 준다.
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
    void Interact(); 
};

언리얼 C++ 에서 인터페이스를 정의할 때 가장 강력한 기능은 BlueprintNativeEvent입니다. 이 매크로를 사용하면 C++ 에서 기본 로직을 구현할 수 있고, 블루프린트에서 이를 오버라이드할 수도 있어서 확장성이 매우 뛰어나다.

 

(2) 인터페이스 상속 및 구현

// DoorActor.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interactable.h" // 인터페이스 헤더 포함
#include "DoorActor.generated.h"

UCLASS()
class MYPROJECT_API ADoorActor : public AActor, public IInteractable // 다중 상속
{
    GENERATED_BODY()
    
public:	
    ADoorActor();

    // BlueprintNativeEvent로 선언된 함수를 C++에서 구현할 때는 
    // 반드시 함수 이름 뒤에 '_Implementation'을 붙여야 함.
    virtual void Interact_Implementation() override;
};
// DoorActor.cpp
#include "DoorActor.h"

ADoorActor::ADoorActor()
{
    PrimaryActorTick.bCanEverTick = false;
}

void ADoorActor::Interact_Implementation()
{
    // 문이 열리는 C++ 로직을 여기에 작성
    UE_LOG(LogTemp, Warning, TEXT("문이 열렸습니다"));
}

상호작용이 가능한 객체(ex) Door)를 만들고 인터페이스를 다중 상속받아 기능을 구현한다.

 

(3) 인터페이스 호출

언리얼 C++ 인터페이스의 가장 중요한 부분. 순수 C++ 포인터 캐스팅(dynamic_cast)을 사용하면 블루프린트 전용으로 구현된 인터페이스를 호출할 수 없는 문제가 발생한다. 따라서 언리얼 리플렉션 시스템을 이용한 안전한 호출 방식을 사용해야 한다.

// MyCharacter.cpp (플레이어 캐릭터)
#include "MyCharacter.h"
#include "Interactable.h"

void AMyCharacter::TryInteract(AActor* TargetActor)
{
    if (TargetActor == nullptr) return;

    // 방법 1: 언리얼 리플렉션을 활용한 검사 및 호출 (가장 권장됨)
    // 객체가 해당 인터페이스를 구현하고 있는지 확인. (U 접두사 사용)
    if (TargetActor->Implements<UInteractable>())
    {
        // 인터페이스 함수 호출 (I 접두사 클래스의 static 함수인 Execute_ 함수명 사용)
        // 이 방식을 사용하면 타겟이 C++ 클래스이든, 
        // 순수 블루프린트 클래스이든 안전하게 호출할 수 있음.
        IInteractable::Execute_Interact(TargetActor);
    }

    /* // 방법 2: Cast를 이용한 전통적인 방식 (C++로만 구현된 경우에 유효)
    IInteractable* InteractableObj = Cast<IInteractable>(TargetActor);
    if (InteractableObj)
    {
        InteractableObj->Execute_Interact(TargetActor);
    }
    */
}

인터페이스의 사용은 객체 지향의 다형성의존성 역전 원칙이 적용될 수 있도록 하므로 사용하는 방법을 잘 숙달하자.

 

언리얼 리플렉션 시스템이란 프로그램이 실행 중(런타임)에 작기 자신의 구조를 스스로 파악하고 조작할 수 있는 기능이다. C++언어 자체에는 이 리플렉션 기능이 없으나 언리얼 엔진에서는 언리얼 리플렉션 시스템을 제공한다. 우리가 코드에 UCLASS(), USTRUCT(), UPROPERTY(), UFUNCTION() 같은 매크로를 달아두면, 컴파일 직전에 언리얼 헤더 툴(UHT)이 코드를 확인하고 매크로가 붙은 요소들의 정보를 모아 메타데이터로 별도 저장하여 .generated.h 파일을 생성한다. 이 정보를 바탕으로 가비지 컬렉션, 블루프린트 연동, 네트워크 리플리케이션, 직렬화와 같은 기능을 엔진이 수행할 수 있다.

 

만약 게임 개발자가 블루프린트 에디터에서 "나무 상자" 클래스를 만들고, 거기에 C++로 만든 IInteractable 인터페이스를 추가했다고 가정해보면, 이 나무상자는 순수 C++ 코드로 작성된 객체가 아니고 실행 중에 엔진이 동적으로 만들어낸 블루프린트 객체가 된다. 따라서, 메모리 상의 C++ 부모는 AActor일 뿐 IInteractable을 C++ 에서 상속받은 적이 없다. 그렇기 때문에 언리얼 엔진에서는 리플렉션을 통한 동적 호출 방법을 사용한다. 리플렉션 시스템에게 인터페이스가 있는지 확인 받고, 인터페이스가 있다면 리플렉션 시스템을 통해 함수를 실행하게 되는 것이다. 이 때 객체가 C++ 클래스면 C++ 함수를, 블루프린트 클래스면 블루프린트 노드를 알아서 찾아 실행해준다.

 

인터페이스를 쓸 때 대상이 C++ 클래스든 블루프린트 클래스든 상관없이 안전하게 상호작용 함수를 호출할 수 있는 이유는, 리플렉션 시스템이 중간에서 메타데이터를 확인하고 올바른 함수 연결을 대신 처리해주는 것이라고 생각하면 된다.

 


*Unreal 프로젝트*

1) 강의 수강

강의를 들으며 전반적인 구조와 흐름을 이해하고 이후에 개인 과제와 함께 실습.

 

(1) 언리얼 엔진 폴더 구조 및 프로젝트 이해 :

콘텐츠 폴더, 소스 폴더, 바이너리 폴더, 컨피그 폴더, 캐시 파일과 임시 빌드 파일 및 세이브 폴더, 엔진 폴더 등

 

(2) 빌드 시스템과 오류 복구 :

빌드와 DLL 파일, 디버그 모드, 디벨롭먼트 모드, Shipping 모드, 전체 빌드와 부분 빌드, 아웃풋 및 에러 리스트, 언리얼 라이브 코딩, 빌드 오류 발생 시 복구 절차

 

(3) 에셋 관리 및 레벨 설정 :

프로젝트 폴더 구조와 마이그레이션, 디폴트 레벨 설정

 

(4) 오브젝트, 액터 및 C++ 클래스 관리

오브젝트 클래스, 액터 클래스, C++ 클래스 생성시 public 타입과 private 타입, C++ 클래스 완전 삭제 방법

 

(5) 액터 생명 주기 및 로깅

라이프사이클, 각 시점의 콜백 함수(생성자, BeginPlay, Tick, PostInitializeComponents, Destroyed, EndPlay 등), UE_LOG 활용법, 커스텀 로그

 

(6) 트랜스폼과 델타 타임

루트 컴포넌트, x축(Roll), y축(Pitch), z축(Yaw), 월드 좌표계와 로컬 좌표계, FVector / FRotator / Transform 구조체, Tick 함수와 AddActorLocalRotation으로 객체 회전시키기, 델타 타임 사용법, 부동소수점 연산 조건문(!FMath::IsNearlyZero), 생성자의 PrimaryActorTick.bCanEverTick = true; 설정

 

(7) 리플렉션 시스템과 블루프린트 연동

C++ 코드와 리플렉션 시스템 사용 이유, 리플렉션 시스템의 인자 활용법, EditAnywhere / VisibleAnywhere / Category /... 등을 지정하여 언리얼 에디터에서 값 혹은 메시 수정하기, UFUNCTION 매크로의 BlueprintCallable(호출 가능), BlueprintPure(반환 전용), BlueprintImplementableEvent(블루프린트에서 내부 로직 구현 가능) 등의 인자 넣기, FVector::ZeroVector 사용하기


*오늘의 총평*

기본기를 열심히 쌓는 날이었다고 생각. 언리얼 에디터를 C++로 켜서 아무것도 없는 레벨에서부터 시작해서 과제를 만들며 열심히 배웠다.

'TIL' 카테고리의 다른 글

06.08 TIL  (0) 2026.06.08
06.05 TIL  (0) 2026.06.05
06.02 TIL (커리어 데이)  (0) 2026.06.02
06.01 TIL (이력서 세션 및 커리어 데이)  (0) 2026.06.01
05.29 TIL (ch.2 팀프로젝트 day6)  (0) 2026.05.29