일정표
| 시간 | 할 일 | 비고 |
| 08:30~10:00 | 코드카타, Atanny | |
| 10:00~12:00 | 커리어 데이 + 프로그래머스 | |
| 13:00~18:00 | 커리어 데이 + 프로그래머스 | (13:00~15:30) 이력서 작성 및 상담 (15:30~16:00) 채용 공고 분석 탬플릿 작성 (16:00~17:00) ch.2 프로젝트 개인 회고 |
| 19:00~21:00 | 오늘의 게임 수학 + 언리얼 C++ | |
| 21:00~22:00 | 운동 | |
| 22:30~23:30 | TIL, 이력서 내용 추가, 모자란 부분 |
커리어 데이
AI를 잘 활용하기 위해 알아야 할 기반 지식, 제너럴리스트 게임 개발자로 경쟁력 갖추기, 비전공자의 게임 개발자 역량 성장기
이력서 보완 자율 실습 + 담당 튜터 1:1 상담, 채용공고 분석 워크샵, 실습 결과 상담 + CH2 팀프로젝트 회고를 진행했다.
노션에 별도로 작성하였다.
*오늘의 코드카타*
문제. 가운데 글자 가져오기
단어 s의 가운데 글자를 반환하는 함수, solution을 만들어 보세요. 단어의 길이가 짝수라면 가운데 두글자를 반환하면 됩니다. s는 길이가 1 이상, 100이하인 스트링입니다.
#include <string>
#include <vector>
using namespace std;
string solution(string s) {
string answer = "";
int len = s.length();
int mid = len / 2;
if (len % 2 == 0) {
answer += s[mid-1];
answer += s[mid];
}
else {
answer += s[mid];
}
return answer;
}
s[i] 형식으로 단일 문자를 뽑아낼 수 있으므로 뽑아낸 문자를 += 연산자를 사용해 빈 문자열에 더해주는 방식을 사용했다.
#include <string>
using namespace std;
string solution(string s) {
string answer = "";
int mid = s.length() / 2;
answer = s.length() % 2 == 0 ? s.substr(mid-1, 2) : s.substr(mid, 1);
return answer;
}
string 헤더의 substr 메서드와 삼항연산자를 사용해서 작성할 수도 있다.
*프로그래머스*
1. [정렬] K번째 수
배열 array의 i번째 숫자부터 j번째 숫자까지 자르고 정렬했을 때, k번째에 있는 수를 구하려 합니다.
배열 array, [i, k, j]를 원소로 가진 2차원 배열 commands가 매개변수로 주어질 때, commands의 모든 원소에 대해 앞서 설명한 연산을 적용했을 때 나온 결과를 배열에 담아 return 하도록 solution 함수를 작성해주세요.
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> solution(vector<int> array, vector<vector<int>> commands) {
vector<int> answer;
for (int idx = 0; idx < commands.size(); idx++) {
int i = commands[idx][0] - 1;
int j = commands[idx][1];
int k = commands[idx][2] - 1;
vector<int> sub(array.begin() + i, array.begin() + j);
sort(sub.begin(), sub.end());
answer.push_back(sub[k]);
}
return answer;
}
commands에서 주어지는 i와 k에서 1을 빼주어 0부터 시작하는 인덱스에 맞춘다. 이후 vector의 생성자를 이용해서 array.begin() + i 부터 array.begin() + j 직전까지의 원소를 복사하여 새로운 부분 배열(sub)을 만든다.
해당 sub 부분 배열을 sort를 사용해서 오름차순으로 정렬한 후, sub 배열에서 k번째 인덱스 값을 찾아 answer 벡터에 추가한다.
2. [정렬] 가장 큰 수
0 또는 양의 정수가 주어졌을 때, 정수를 이어 붙여 만들 수 있는 가장 큰 수를 알아내 주세요. 예를 들어, 주어진 정수가 [6, 10, 2]라면 [6102, 6210, 1062, 1026, 2610, 2106]를 만들 수 있고, 이중 가장 큰 수는 6210입니다. 0 또는 양의 정수가 담긴 배열 numbers가 매개변수로 주어질 때, 순서를 재배치하여 만들 수 있는 가장 큰 수를 문자열로 바꾸어 return 하도록 solution 함수를 작성해주세요.
numbers의 길이는 1 이상 100,000 이하입니다.
numbers의 원소는 0 이상 1,000 이하입니다.
정답이 너무 클 수 있으니 문자열로 바꾸어 return 합니다.
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
bool compare(const string& a, const string& b) {
return a + b > b + a;
}
string solution(vector<int> numbers) {
string answer = "";
vector<string> str_numbers;
for (int num : numbers) {
str_numbers.push_back(to_string(num));
}
sort(str_numbers.begin(), str_numbers.end(), compare);
if (str_numbers[0] == "0") {
return "0";
}
for (const string& str : str_numbers) {
answer += str;
}
return answer;
}
(1) 로직 : numbers 배열에 있는 정수들을 모두 문자열로 변환한다. 두 문자열 a와 b 가 있을 때 bool compare(const string& a, const string& b) { return a + b > b + a; 커스텀 정렬을 사용해서, 더 큰 값을 만드는 숫자 순서대로 배열에 정렬한다.
결과적으로 조건에 맞도록 잘 정렬된 문자를 순서대로 이어붙여서 str을 만들고 answer를 반환하면 된다.
(2) compare 함수
sort와 compare(커스텀 규칙)이 결합하면 매우 훌륭한 알고리즘을 사용할 수 있게 되는데, 우리가 만들어 낸 비교 기준(compare)에 의해서 sort 함수 내부적으로 퀵 정렬 기반으로 알아서 전체 배열을 완성하기 때문이다.
이 때 compare를 이해하는 것이 중요하다. compare(a, b)는 true일 경우 무조건 a를 b보다 앞에 두게 된다. 반대로 false일 경우 무조건 a를 b 뒤로 보낸다. 따라서, compare(a, b) return a+b > b+a 하게 되면, a+b가 진짜로 b+a보다 크면(문자열을 이어붙였을 때) true 가 되고 a를 b보다 앞에 위치하게 한다. 반대로 a+b > b+a 가 false값이면 a를 b뒤로 보내게 된다. 이렇게 반복하면서 sort 내부에서 알아서 compare에 정한 정렬 규칙에 따라 배열을 정렬하는 것이다.
추가적으로 주의할 사항은, 배열에 0밖에 없을 경우를 대비해서 예외 사항을 고려하여 컴파일 에러가 나지 않도록 방어 장치를 마련하는 습관도 만들자.
3. [정렬] H-Index
문제 설명
H-Index는 과학자의 생산성과 영향력을 나타내는 지표입니다. 어느 과학자의 H-Index를 나타내는 값인 h를 구하려고 합니다. 위키백과1에 따르면, H-Index는 다음과 같이 구합니다.
어떤 과학자가 발표한 논문 n편 중, h번 이상 인용된 논문이 h편 이상이고 나머지 논문이 h번 이하 인용되었 다면 h의 최댓값이 과학자의 H-Index입니다.
어떤 과학자가 발표한 논문의 인용 횟수를 담은 배열 citations가 매개변수로 주어질 때, 이 과학자의 H-Index를 return 하도록 solution 함수를 작성해주세요.
<제한사항>
과학자가 발표한 논문의 수는 1편 이상 1,000편 이하입니다.
논문별 인용 횟수는 0회 이상 10,000회 이하입니다.
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int solution(vector<int> citations) {
int h_index = 0;
sort(citations.begin(), citations.end(), greater<int>());
for (int i = 0; i < citations.size(); i++) {
if (citations[i] >= i + 1) {
h_index = i + 1;
}
else {
break;
}
}
return h_index;
(1) sort를 활용해서 내림차순 정렬을하고, 주어진 조건에 따라 조건식을 작성해서 h_index를 설정한다.
처음 H_index 문제를 보고 뭔 소리야? 했는데, 곰곰이 생각해 보니 다음과 같이 정리할 수 있었다.
인용된 횟수에 따라 내림차순 정렬하게 되면, 예를 들어 {6, 5, 5, 4, 3, 3, 3, 2, 1, 1, 1} 이런 식으로 쌓이게 될 것이다.
내림차순 정렬했기 때문에, 내가 지금 들고 있는 논문의 인용 횟수가 내 책상 위에 있는 논문들 중 가장 낮은 인용 횟수이다. 예를 들어, 3등 논문이 5번 인용되었다면 내 앞의 1등, 2등은 무조건 5번보다 많이 인용되었음이 확실한 것이다.
즉 ,정렬된 배열을 첫번째 논문(1등)부터 차례대로 스캔하면서, "이 논문의 인용 횟수가 현재 순위보다 크거나 같은가?"를 확인한다.
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
int solution(vector<int> citations) {
map<int, int, greater<int>> count_map;
for (int c : citations) {
count_map[c]++;
}
int max_h = 0;
int papers = 0;
for (auto const& [cite, count] : count_map) {
papers += count;
int current_h = min(papers, cite);
max_h = max(max_h, current_h);
}
return max_h;
}
(2) map을 이용해서 푸는 방법
배열 정렬에서는 몇 등인지를 기준으로 판단했다. 그러나 map을 사용하면 "인용 횟수 이상을 달성한 논문이 누적 몇 편인가"를 기준으로 판단하게 된다.
우선 내림차순 Map을 생성해서 Key에는 인용횟수, Value에는 해당 인용 횟수를 가진 논문의 개수를 저장한다.
Map은 자동으로 키값에 따라 정렬되니까 인용 횟수가 가장 높은 것부터 순회하면서 누적 논문수를 더할 수 있다.
특정 인용 횟수(Key)에 도달했을 때, 달성할 수 있는 최대 H-index는 현재 인ㄴ용 횟수와 누적 논문 수 중 더 작은 값이 된다.
map에 삽입하는 과정 O(N log K)이며, 중복 데이터가 많을 때의 처리 방식으로 더 성능이 뛰어난 코드라고 할 수 있다.
4. [입문] 컨트롤 제트
문제 설명
숫자와 "Z"가 공백으로 구분되어 담긴 문자열이 주어집니다. 문자열에 있는 숫자를 차례대로 더하려고 합니다. 이 때 "Z"가 나오면 바로 전에 더했던 숫자를 뺀다는 뜻입니다. 숫자와 "Z"로 이루어진 문자열 s가 주어질 때, 머쓱이가 구한 값을 return 하도록 solution 함수를 완성해보세요.
<제한 사항>
1 ≤ s 의 길이 ≤ 200
-1,000 < s의 원소 중 숫자 < 1,000
s는 숫자, "Z", 공백으로 이루어져 있습니다.
s에 있는 숫자와 "Z"는 서로 공백으로 구분됩니다.
연속된 공백은 주어지지 않습니다.
0을 제외하고는 0으로 시작하는 숫자는 없습니다.
s는 "Z"로 시작하지 않습니다.
s의 시작과 끝에 공백이 없습니다.
"Z"가 연속으로 나오는 경우는 없습니다.
#include <string>
#include <vector>
using namespace std;
int solution(string s) {
int answer = 0;
int prev = 0;
string temp = "";
for (int i = 0; i <= s.length(); i++) {
if (i == s.length() || s[i] == ' ') {
if (temp == "Z") {
answer -= prev;
}
else if (temp != "") {
prev = stoi(temp);
answer += prev;
}
temp = "";
} else {
temp += s[i];
}
}
return answer;
}
(1) 임시 문자열 변수(temp)를 만들어서 공백이 아닐 때는 문자를 계속 이어 붙이고 공백을 만났을 때 연산을 수행하도록 한다.
#include <string>
#include <vector>
#include <sstream>
using namespace std;
int solution(string s) {
int answer = 0;
int prev = 0;
string temp;
stringstream ss(s);
while (ss >> temp) {
if (temp == "Z") {
answer -= prev;
}
else {
prev = stoi(temp);
answer += prev;
}
}
return answer;
}
(2) stringstream을 이용해서 공백을 기준으로 문자열을 분리하고 계산을 수행하게 할 수 있다. 문자열을 공백 기준으로 분리하거나 쉼표 기준으로 분리하는 건 많이 사용되는 방식이니 숙지하자.
ss >> temp 라고 하면 문자열을 처음부터 살펴보면서 temp(여기서는 해당 문자)가 "Z"이면 prev값을 빼고, "Z" 가 아니면 temp(해당 문자)를 숫자로 바꾼뒤에 prev 값으로 저장하고 더한다. 이렇게 하면 굳이 공백인 것을 확인하기 위해 조건식을 만들지 않아도 된다.
#include <string>
#include <vector>
#include <sstream>
#include <stack>
using namespace std;
int solution(string s) {
stack<int> st;
string token;
stringstream ss(s);
while (ss >> token) {
if (token == "Z") {
if (!st.empty()) {
st.pop();
}
} else {
st.push(stoi(token));
}
}
int answer = 0;
while (!st.empty()) {
answer += st.top();
st.pop();
}
return answer;
}
(3) (나의 아이디어)스택 사용하기 : 제약 사항이 없고, 실무 관점으로 접근하기
<stack> 라이브러리를 사용해서 stringstream과 결합해서 뒤로가기 기능을 실제로 구현할 수 있다.
먼저 stack<int> st;로 정수 자료형의 숫자를 담을 스택을 생성한다.
그리고, 공백을 기준으로 토큰을 하나씩 분리하면서, 만난 토큰이 Z 라면 스택이 비어있지 않을 때만 가장 최근 값을 제거하도록 한다. 이렇게 최근 값을 제거하는 것이 뒤로가기 기능이다. 결과적으로 st.top() 은 이전 값이 남게 된다. 만약 만난 토큰이 숫자라면 st.push(stoi(token)); 으로 스택의 맨 위에 쌓는다.
그리고 스택의 가장 위에있는 값을 가져와서 더한 값은 스택에서 제거하는 방식으로 동일한 덧셈 로직을 구현한다.
이렇게 스택으로 구현하게 되면 데이터를 쌓는 과정과 최종 계산하는 과정이 명확히 분리된다는 장점이 있다. 또한, Z가 연속으로 두번 나오는 등 복잡하게 변형되어도 코드 수정 없이 완벽하게 대응할 수 있다.
사실 순수하게 메모리와 성능만 놓고 보면 stringstream과 단일 변수(prev)를 사용한 코드가 더 우수하지만, 알고리즘의 안정성과 확장성, 즉 예외 처리까지 고려하면 Stack 구조를 사용하는 것이 훨씬 더 좋은 선택이 될 것이다.
*추가 공부*
프로그램 코드는 위에서 아래로 순차적으로 실행된다. 따라서 스택 구조를 작성할 때처럼 while 문을 두개로 분리해 놓는다 하더라도 두 루프는 동시에 실행되지 않고 위에서 아래로 순차적으로 실행되며 반복한다.
뒤로가기의 원리를 이해해보면, 어차피 뒤로가기 한번 누르면 그냥 계산을 안하는 것과 같다. 그래서 뒤로가기까지 고려해서 더할 숫자들만 스택에 쌓아놓고 나중에 아래 While 문으로 가서 계산을 시작하는 것이다.
즉 첫번째 루프에서는 정말로 더할 숫자들만 스택에 남겨두고, 두번째 루프에서는 스택에 남아있는 숫자를 더하는 역할만 수행한다. 이렇게 역할을 분리할 수 있는 것이 Stack 구조의 장점이고, 결과적으로 코드를 더 안전하고 수정하기 쉽게 만들 수 있다.
5. [입문] 배열 원소의 길이
문제 설명
문자열 배열 strlist가 매개변수로 주어집니다. strlist 각 원소의 길이를 담은 배열을 return 하도록 solution 함수를 완성해주세요.
#include <string>
#include <vector>
using namespace std;
vector<int> solution(vector<string> strlist) {
vector<int> answer;
int len = strlist.size();
for (int i = 0; i < len; i++) {
string temp_str = strlist[i];
int len_temp_str = temp_str.length();
answer.push_back(len_temp_str);
}
return answer;
}
(1) 순수하게 for 반복문을 사용해서 각각의 문자열을 뽑아내어 길이를 구했고, 해당 길이를 정수값으로 answer 배열에 밀어 넣었다.
#include <string>
#include <vector>
using namespace std;
vector<int> solution(vector<string> strlist) {
vector<int> answer;
answer.reserve(strlist.size());
for (const string& str : strlist) {
answer.push_back(str.length());
}
return answer;
}
(2) 범위 기반 for 문(Range-based for loop)과 메모리 사전 할당(reserve)를 적용하는 방법
push_back을 할 때마다 메모리를 재할당하고 복사하는 비용이 발생하게 된다. 이 문제의 경우 반환할 배열의 길이를 이미 알고 있으므로, reserve()로 공간을 미리 확보하면 성능이 향상된다.
또한, const string& str : strlist 처럼 매번 문자열의 복사본을 생성하지 말고 참조자를 사용해서 원본을 복사 없이 가리키기만 하면서 시간과 메모리를 아끼게 하는 것이 좋다.
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> solution(vector<string> strlist) {
vector<int> answer(strlist.size());
transform(strlist.begin(), strlist.end(), answer.begin(), [] (const string& str) {return str.length(); });
return answer;
}
(3) <algorithm> 헤더의 transform 과 람다 함수 사용하기
우리가 요새 자주 보는 람다함수를 사용할 수도 있다.
먼저 answer 벡터의 크기를 미리 지정해서 생성하고, strlist의 처음부터 끝까지 순회하면서 람다 함수를 통해 길이를 구하고 answer에 바로 넣는다.
이 패턴은 코딩 테스트 뿐만 아니라 실무에서도 데이터 매핑 시 자주 쓰이는 패턴이므로 숙지해두자.
*transform(원본의 시작, 원본의 끝, 목적지의 시작, 적용할 함수); 이렇게 4개의 인자를 가진다. 또는, 두 개의 컨테이너에 있는 원소들을 짝지어서 연산해야 할 때는 인자를 5개 받기도 한다.
vector<int> A = {1, 2, 3};
vector<int> B = {10, 20, 30};
vector<int> result(3); // 크기가 3인 결과 벡터 미리 준비
// A의 원소와 B의 원소를 더해서 result에 저장
transform(A.begin(), A.end(), B.begin(), result.begin(),
[](int a, int b) { return a + b; });
// 결과: {11, 22, 33}
다만, 사용할 때 주의할 점이 있다. transform 함수 자체는 결과를 저장할 공간을 미리 만들어주지 않으므로 위에서 본 것처럼 벡터의 크기를 미리 지정해서 생성해야 한다.
*그런데 공부하다보니 문득 이런 생각이 들었다.
함수가 나오면 다 몇개의 인자를 가지고 있는지 외워야 하고 내가 거기에다가 적어 넣을 위치까지 외워야하는 거야?
다행히 그럴 필욘 없다고 한다.
(1) 기본적으로 IDE의 자동 완성 기능을 활용할 수도 있고,
(2) 더 기본적인 것은 C++ 라이브러리의 패턴을 이해하기만 하면 된다. C++ 표준 라이브러리 설계의 일관된 규칙이 존재하기 때문이다. 예를 들어 <algorithm> 헤더의 대부분 함수는 "어디서 어디서부터 작업할지"를 먼저 묻도록 설계되어 있으므로 거의 대부분 첫 번째 인자와 두 번째 인자는 (시작점, 끝점)이 들어가게 된다.
(3) 모르겠으면 망설이지 말고 cppreference.com 의 공식문서를 찾아 들어가서 함수 사용방법을 찾아보자.
제일 중요한 것은 자주 써보면서 손에 익어서 자연스럽게 외워지는 것들을 머리에 남겨두고, 좋은 방법이면 잘 외워놨다가 실제로 기능을 구현할 때 써먹으면 된다.
6. [입문] 중복된 문자 제거
문제 설명
문자열 my_string이 매개변수로 주어집니다. my_string 에서 중복된 문자를 제거하고 하나의 문자만 남긴 문자열을 return하도록 solution 함수를 완성해주세요.
<제한사항>
1 ≤ my_string ≤ 110
my_string은 대문자, 소문자, 공백으로 구성되어 있습니다.
대문자와 소문자를 구분합니다.
공백(" ")도 하나의 문자로 구분합니다.
중복된 문자 중 가장 앞에 있는 문자를 남깁니다.
#include <string>
#include <vector>
using namespace std;
string solution(string my_string) {
string answer = "";
for (char c : my_string) {
if (answer.find(c) == string::npos) {
answer += c;
}
}
return answer;
}
(1) find와 npos 사용하기
answer 문자열 안에 현재 탐색중인 c가 없다면(string::npos) c를 answer 문자열에 추가하도록 할 수 있다.
다만 이경우 문자열의 길이가 길어질수록 탐색(find)하는 데 시간이 더 많이 걸릴 수도 있다.
#include <string>
#include <vector>
using namespace std;
string solution(string my_string) {
string answer = "";
bool seen[128] = {false};
for (char c : my_string) {
if (!seen[c]) {
answer += c;
seen[c] = true;
}
}
return answer;
}
(2) 문자의 인덱스를 활용한 방문 체크
영문 대소문자와 공백은 0~127 사이의 아스키코드 값을 가지므로 활용할 수 있는 방법이다. 크기가 128인 bool 배열을 만들어서 아직 발견 못한 문자 인덱스에는 기본 값인 false를 넣어두고, 발견하면 해당 문자를 answer 에 붙이고 해당 인덱스로 찾아가 값을 true로 바꿔놓는다.
기본적으로 해당 문자에 해당하는 인덱스로 바로 접근이 가능해서 O(N)으로 빠르다.
#include <string>
#include <unordered_set>
using namespace std;
string solution(string my_string) {
string answer = "";
unordered_set<char> seen;
for (char c : my_string) {
if (seen.find(c) == seen.end()) {
answer += c;
seen.insert(c);
}
}
return answer;
}
(3)해시 자료구조 활용(std::unordered_set)
set 자료구조는 중복을 허용하지 않는다는 특성을 이용한 것. 기본 오름차순 정렬이 필요 없으므로 unordered_set으로 사용한다.
seen.find(c)로 seen 집합에 c가 없다면 ( == seen.end()), c를 answer에 더해주고 seen 집합에 삽입( insert(c) ) 한다.
탐색시간이 평균적으로 O(1) 이기 때문에 전체 시간 복잡도는 O(N)이다. 자료구조의 특성을 명확히 활용할 수 있다는 점에서 코딩 테스트할 때 활용도도 높고 좋은 점수를 줄 수 있다.
7. [입문] 삼각형의 완성조건 (1)
문제 설명
선분 세 개로 삼각형을 만들기 위해서는 다음과 같은 조건을 만족해야 합니다.
"가장 긴 변의 길이는 다른 두 변의 길이의 합보다 작아야 한다."
삼각형의 세 변의 길이가 담긴 배열 sides 이 매개변수로 주어집니다. 세 변으로 삼각형을 만들 수 있다면 1, 만들 수 없다면 2를 return하도록 solution 함수를 완성해주세요
<제한사항>
sides의 원소는 자연수
sides의 길이는 3
1 ≤ sides 의 원소 ≤ 1,000
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int solution(vector<int> sides) {
sort(sides.begin(), sides.end());
int A = sides[0];
int B = sides[1];
int C = sides[2];
if (C < (A + B)) {
return 1;
} else return 2;
}
(1) sort의 오름차순 정렬을 이용한 풀이
#include <vector>
#include <algorithm>
using namespace std;
int solution(vector<int> sides) {
sort(sides.begin(), sides.end());
return sides[2] < (sides[0] + sides[1]) ? 1 : 2;
}
(2) 삼항 연산자로 짧게 써보기
*Atanny*
1. 언리얼 Replication과 Authority 시스템 이해
언리얼 엔진의 Replication 시스템 :
변수가 Replication 되려면 UPROPERTY 매크로에 Replicated 또는 ReplicatedUsing 지정자를 명시적으로 추가해야 한다. 모든 변수가 자동으로 복제되는 것은 아니며, 개발자가 필요한 변수만 선택적으로 복제하도록 설정해야 한다.
Replication은 서버의 데이터를 클라이언트에 자동으로 동기화하는 시스템으로서, Replication을 사용하려면 Actor의 bReplicates 속성을 true로 설정해야 한다. Replication은 멀티플레이어 게임에서 네트워크 동기화를 위해 사용된다.
언리얼 엔진의 Authority 시스템 :
Authority를 가진 머신(일반적으로 서버)만이 Replication을 통해 데이터를 다른 머신에 전송할 수 있다. Authority는 보통 서버에 있으며 임의로 전달되지 않고, Authority 없이 변경한 값은 로컬에만 반영된다.
코드 :
UPROPERTY(________)
int32 Health;
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyCharacter, Health);
}
변수를 Replication하려면 UPROPERTY 매크로에 Replicated 지정자를 추가해야 하고, GetLifetimeReplicatedProps 함수에서 DOREPLIFETIME 매크로를 사용하여 해당 변수를 등록해야 한다.
2. RPC와 통신 방식의 이해
언리얼 엔진에서의 RPC(Remote Procedure Call) :
RPC는 네트워크 멀티플레이어 환경에서 원격 머신의 함수를 호출할 수 있게 해주는 메커니즘이다. 언리얼 엔진에서는 Server, Client, Multicast RPC를 통해 네트워크로 연결된 다른 머신에서 특정 함수를 실행할 수 있다.
Server RPC :
Server RPC는 클라이언트에서 호출하면 서버에서만 실행되는 RPC이다. 모든 클라이언트에게 동시에 함수를 실행시키는 것은 Multicast RPC이다.
Server RPC는 주로 치트 방지를 위해 서버에서 권한 검증이 필요한 로직(아이템 습득, 공격 처리 등)을 처리하는 데 사용된다.
Multicast RPC :
Multicast RPC는 서버에서 호출하면 서버 자신과 모든 연결된 클라이언트에서 동시에 실행되는 RPC이다. 이는 모든 플레이어에게 동일한 이벤트(예: 폭발 이펙트, 사운드 재생)를 동기화할 때 유용하다.
3. 네트워크 오너십과 권한 부여
언리얼 엔진의 네트워크 오너십(Ownership) :
오너십은 게임 도중 동적으로 변경할 수 있다. 예를 들어, 플레이어가 아이템을 주웠을 때나 차량에 탑승할 때 오너십이 변경될 수 있다. 오너십은 특정 액터가 어느 클라이언트에 소속되어 있는지를 나타내며, 서버는 모든 액터에 대한 오너십을 가질 수 있다. 또한 오너십은 RPC 호출 권한과 관련이 있어 호출 시 권한 검증에 사용된다.
플레이어 캐릭터(Pawn)와 기본 오너(Owner) :
플레이어가 조종하는 Pawn의 오너는 해당 Pawn을 제어하는 PlayerController를 소유한 클라이언트이다. 이를 통해 클라이언트는 자신의 캐릭터에 대해 특정 권한을 가지며 Client RPC를 받을 수 있다. 서버가 소유하는 것이 아니라 각 클라이언트가 자신의 캐릭터를 소유한다.
네트워크 오너십 :
네트워크 오너십의 주요 목적은 RPC 호출 권한을 제어하고 네트워크 보안을 강화하는 것이다. 클라이언트가 소유하지 않은 액터에 대해 함부로 RPC를 호출하지 못하도록 제한함으로써 치팅을 방지하고 네트워크 권한을 적절히 관리할 수 있다.
*수학 공부*
이번 주제 : 벡터(Vectors)
기본적으로 게임 공간에서 하나의 벡터 데이터는 "위치" 와 "방향과 크기"를 나타낸다. 수학적으로 살펴보자면, (도착점 - 출발점) 을 계산하면 출발점에서 도착점을 향하는 벡터 값이 나오게 된다. 벡터에는 방향도 있지만 크기도 있는데, 크기는 화살표의 길이를 의미한다. 이는 피타고라스 정리를 이용해서 계산한다.
벡터의 방향은 그대로 유지한 채, 크기를 1로 압축하는 과정을 '정규화' 라고 한다.
키보드 방향키를 위, 오른쪽을 동시에 누르게 되면 대각선으로 이동하게 되는데, 이때 그냥 더하게 되면 길이가 2 제곱근으로 약 1.414가 되어서 캐릭터가 대각선으로 더 빨리 이동하는 버그가 발생하게 된다. 따라서, 이동 방향을 정규화 한 후에 원하는 속도 변수를 곱해줘야 항상 일정한 속도로 이동할 수 있게 된다.
벡터 덧셈은 위치 이동에 사용되고, 벡터 뺄셈은 방향 탐색에 사용되고, 스칼라 곱셈은 속도 조절에 사용된다. 기초 Vector2 구조체 구현을 통해 알아보자.
#include <cmath>
struct Vector2 {
float x;
float y;
// 생성자
Vector2(float _x = 0.0f, float _y = 0.0f) : x(_x), y(_y) {}
// 벡터 덧셈: 위치 이동에 사용 (Position + Velocity)
Vector2 operator+(const Vector2& rhs) const {
return Vector2(x + rhs.x, y + rhs.y);
}
// 벡터 뺄셈: 방향 탐색에 사용 (Target - Player)
Vector2 operator-(const Vector2& rhs) const {
return Vector2(x - rhs.x, y - rhs.y);
}
// 스칼라 곱셈: 속도 조절에 사용 (Direction * Speed)
Vector2 operator*(float scalar) const {
return Vector2(x * scalar, y * scalar);
}
// 벡터의 크기(길이) 반환
float Magnitude() const {
return std::sqrt(x * x + y * y);
}
// 정규화 (크기가 1인 단위 벡터로 변환)
void Normalize() {
float mag = Magnitude();
if (mag > 0.0001f) { // 0으로 나누는 오류 방지
x /= mag;
y /= mag;
}
}
};
이제 이 Vector2 구조체에 내적(Dot Product)를 결합해서 어떻게 활용하는 지 알아보자.
내적이란, 두 벡터를 곱해서 하나의 스칼라(실수)값을 만들어내는 연산이다. 기호로는 점(·)을 사용한다.
수학적으로 내적을 구하는 방법은 두 가지가 있는데, 하나는 좌표를 이용한 계산이고, 또 하나는 각도를 이용한 계산이다.
1) 좌표를 이용한 계산 : A · B = (A(x) * B(x)) + (A(y) * B(y))
2) 각도를 이용한 계산 : A · B = ||A|| * ||B|| * cos(theta)
게임 개발에서 내적은 방향 판별에 사용한다. 즉, 각도를 이용한 계산에 특화되어있다고 볼 수 있다.
두 벡터가 모드 정규화(길이가 1인 단위 벡터)되어 있을 때, ||A||와 ||B||가 모두 1이므로 A · B = cos(theta) 가 된다.
즉, 정규화된 두 벡터를 내적하면 두 벡터가 이루는 각도의 코사인 값만 도출할 수 있게 된다. 코사인 값은 -1~1의 값을 가지므로, 결국 내적의 결과값만 보고 타겟이 어디있는지 알 수 있게 되는 것이다.
예를 들어,
코사인 값이 0보다 클 때, 두 벡터의 각도는 90도 미만으로 타겟이 내 앞에 있다는 것을 도출할 수 있고,
코사인 값이 0보다 작을 때, 두 벡터의 각도는 90도 초과로 타겟이 내 뒤에 있다는 것을 도출할 수 있고,
코사인 값이 0일 때, 두 벡터의 각도는 90도로 타겟이 정확히 내 좌우 옆에 있다는 것을 알 수 있다(방향은 알지 못함).
코사인 값이 0일 때 알수 있듯이, 단순 벡터 각도가 90도라는 정보로는 타겟이 내 왼쪽에 위치하고 있는지, 오른쪽에 위치하고 있는지 알 수는 없다. 따라서 게임 프로그래밍에서는 내적과 항상 외적(Cross Product)를 같이 사용한다.
외적은 3D 공간에서 두 벡터에 모두 수직인 3번째 벡터를 구하는 연산이지만, 2D 게임에서는 두 벡터가 평면(X, Y)에만 존재한다고 가정하고 계산한다. 따라서, 외적을 하고 나서 결과값으로 나오는 가상의 Z축 값의 부호(+, -)를 확인하면 좌우를 판별할 수 있게 되는 것이다.
외적의 공식은 다음과 같다.
[외적의 공식] A * B = (A(x) * B(y)) - (A(y) * B(x))
따라서, 기본적으로 오른쪽이 +X, 위쪽이 +Y라고 기준을 세워보면, 캐릭터가 바라보는 방향 벡터(A)와 타겟을 향하는 방향 벡터(B)를 외적했을 때 결과값으로 다음을 판별할 수 있게 된다.
외적 결과가 0보다 클 때, 타겟은 기준 벡터의 왼쪽(반시계 방향)에 있다.
외적 결과가 0보다 작을 때, 타겟은 기준 벡터의 오른쪽(시계 방향)에 있다.
외적 결과가 0일 때, 타겟이 정확히 앞이나 뒤에 있다(평형 상태임을 의미함).
결론적으로, Vector2 구조체에 내적과 2D외적을 계산하는 함수를 추가하고, 이를 활용해 캐릭터가 타겟을 향해 어느 방향으로 회전해야 하는지 판별하는 코드를 구현할 수 있게 된다.
#include <iostream>
struct Vector2 {
float x;
float 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);
}
float Magnitude() const {
return std::sqrt(x * x + y * y);
}
void Normalize() {
float mag = Magnitude();
if (mag > 0.0001f) {
x /= mag;
y /= mag;
}
}
//벡터의 내적 (Dot Product)
float Dot(const Vector2& rhs) const {
return (x * rhs.x) + (y * rhs.y);
}
// 2D 외적 (Z축 성분의 스칼라 값 반환)
float Cross(const Vector2& rhs) const {
return (x * rhs.y) - (y * rhs.x);
}
};
// 몬스터의 시야에 플레이어가 들어왔는지 판별하는 함수
bool IsPlayerInSight(Vector2 monsterPos, Vector2 monsterForward, Vector2 playerPos, float fieldOfViewCos) {
// 1. 몬스터에서 플레이어를 향하는 방향 벡터를 구함
Vector2 dirToPlayer = playerPos - monsterPos;
// 2. 방향 벡터를 정규화 (길이를 1로 만듦)
dirToPlayer.Normalize();
// 3. 몬스터가 바라보는 방향(Forward)과 플레이어를 향하는 방향을 내적
// (monsterForward도 이미 정규화되어 있다고 가정)
float dotResult = monsterForward.Dot(dirToPlayer);
// 4. 내적 결과(cos 값)
if (dotResult >= fieldOfViewCos) {
return true;
}
return false;
}
// 타겟이 왼쪽에 있는지 오른쪽에 있는지 판별하는 함수
void CheckTargetDirection(Vector2 playerPos, Vector2 playerForward, Vector2 targetPos) {
// 플레이어에서 타겟을 향하는 방향 벡터 계산
Vector2 dirToTarget = targetPos - playerPos;
// 플레이어의 정면 방향과 타겟 방향을 외적
float crossResult = playerForward.Cross(dirToTarget);
if (crossResult > 0.0f) {
std::cout << "타겟이 왼쪽에 있습니다\n";
} else if (crossResult < 0.0f) {
std::cout << "타겟이 오른쪽에 있습니다\n";
} else {
std::cout << "타겟이 정확히 정면(또는 후면)에 있습니다\n";
}
}
이처럼, 내적으로는 사거리나 시야각 안에 들어왔는지 판단하고, 외적으로는 좌우를 판별해 캐릭터를 회전시키는 것이 유도 기능이나 AI 추적 로직의 핵심이다.
벡터의 개념과 연산자 오버로딩이 적용된 구조체를 바탕으로, 실제 Unreal engine 등에서 매 프레임마다 캐릭터를 어떻게 이동시키는지 그 구조를 알아보자.
이때 가장 중요한 게임 프로그래밍 개념인 "프레임 독립성(Frame-rate Independence)"과 "델타 타임(Delta Time)" 에 대해서 알아야 한다.
프레임 독립성이란, 유저의 컴퓨터 사양에 관계 없이 게임 속 시간과 움직임은 현실 시간과 같이 흘러가야 한다는 원칙이다.
게임은 끊임없이 화면을 새로 그리는 while 루프 안에서 돌아가게 된다. 이것이 게임 루프인데, 사양이 좋은 컴퓨터는 1초에 144번(144fps) 루프를 돌고, 일반 컴퓨터는 60(60fps)루프를 돈다고 생각해보자. 만약 단순히 매 프레임마다 캐릭터를 +1 이동시킨다면, 사양이 좋은 컴퓨터에서는 캐릭터가 2배 빠르게 이동하는 사고가 발생하게 된다.
따라서, 이를 막기 위해 실제 흐른 시간(초 단위)인 델타 타임을 이동 거리에 곱해주는 방식으로 해결하는 것이다.
수학적 공식으로 표현해보자면,
새로운 위치 = 현재 위치 + (방향 * 속력 * 델타 타임) 으로 표현할 수 있다.
이를 종합해서, 게임 루프 안에서 사용자 입력을 받아 캐릭터를 움직이는 전체적인 구조를 만들 수 있다.
#include <iostream>
#include <cmath>
// 1. 지금까지 빌드업한 Vector2 구조체
struct Vector2 {
float x;
float 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*(float scalar) const { return Vector2(x * scalar, y * scalar); }
// 대각선 이동 속도 보정을 위한 정규화 함수
void Normalize() {
float mag = std::sqrt(x * x + y * y);
if (mag > 0.0001f) {
x /= mag;
y /= mag;
}
}
};
// 2. 게임 월드의 오브젝트를 표현하는 캐릭터 구조체
struct Character {
Vector2 position; // 현재 위치 (X, Y)
Vector2 moveDirection; // 입력받은 이동 방향 벡터
float speed; // 초당 이동할 속력 (units/second)
Character(Vector2 startPos, float moveSpeed)
: position(startPos), moveDirection(0.0f, 0.0f), speed(moveSpeed) {}
// 매 프레임마다 호출되는 업데이트 함수
void Update(float deltaTime) {
// 입력받은 방향 벡터의 길이를 1로 만듦 (대각선 이동 시 빨라지는 버그 방지)
moveDirection.Normalize();
// 변위(Displacement) = 방향 * 속력 * 시간
Vector2 displacement = moveDirection * speed * deltaTime;
// 새로운 위치 반영
position = position + displacement;
}
};
// 3. 가상의 메인 게임 루프 시뮬레이션
int main() {
// (0, 0) 위치에 있고, 1초에 5미터(단위)를 움직이는 캐릭터 생성
Character player(Vector2(0.0f, 0.0f), 5.0f);
// 가상의 프레임별 델타 타임 (예: 60fps 레이트라면 약 0.016초)
float mockDeltaTime = 0.016f;
std::cout << "--- 캐릭터 이동 시작 (초기 위치: 0.0, 0.0) ---\n";
// 유저가 키보드에서 [오른쪽]과 [위쪽] 화살표를 동시에 누른 상황을 가정 (대각선 이동)
player.moveDirection = Vector2(1.0f, 1.0f);
// 5프레임 동안 게임 루프가 도는 상황 시뮬레이션
for (int frame = 1; frame <= 5; ++frame) {
player.Update(mockDeltaTime);
std::cout << "[Frame " << frame << "] "
<< "Player Position: (" << player.position.x << ", " << player.position.y << ")\n";
}
return 0;
}
지금까지 배웠던 개념으로 캐릭터를 '등속도'로 움직이는 가장 기본적인 물리 법칙을 코드로 구현할 수 있게 된 것이다.
(06.02)
*Unreal C++*
이번 주제 : 객체 지향 프로그래밍(OOP)
언리얼 엔진에서의 OOP는 일반적인 C++와 매우 유사하지만 언리얼 헤더 툴(UHT) 매크로라는 고유한 리플렉션 시스템이 결합되어 있다는 것이 가장 큰 특징이다. 이를 반영해서, 언리얼 C++에서 구조체(struct)와 캡슐화, 상속, 다형성의 핵심 객체 지향 원칙이 어떻게 표현되는지 코드와 함께 살펴보자.
1) 헤더 파일 : 구조체와 클래스를 설계한다. 언리얼 엔진에서는 클래스와 구조체를 정의할 때 엔진이 인식할 수 있도록 UCLASS, USTRUCT, UPROPERTY, UFUNCTION 등의 매크로를 붙인다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyBaseActor.generated.h" // 항상 마지막에 include
// 언리얼에서 구조체는 가비지 컬렉션(GC)의 대상이 되는 클래스와 달리
// 가벼운 '값 타입' 데이터를 묶을 때 주로 사용한다. (접두사 'F')
USTRUCT(BlueprintType)
struct FCharacterStats
{
GENERATED_BODY()
public:
// 블루프린트에서 읽고 쓸 수 있도록 한다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
int32 Health;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float AttackPower;
// 기본 생성자
FCharacterStats()
{
Health = 100;
AttackPower = 15.0f;
}
};
// 2. 클래스 (Class): 상속, 캡슐화, 다형성
// 월드에 배치될 수 있는 AActor를 상속(Inheritance)받는다. (접두사 'A')
UCLASS()
class MYPROJECT_API AMyBaseActor : public AActor
{
GENERATED_BODY()
public:
AMyBaseActor();
protected:
// 다형성(Polymorphism): 부모 클래스(AActor)의 가상 함수를 재정의(Override)한다.
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
// 외부에서 직접 데이터를 수정하지 못하게 하고(캡슐화), 함수를 통해 상호작용한다.
UFUNCTION(BlueprintCallable, Category = "Action")
virtual void TakeDamage(int32 DamageAmount);
private:
// 캡슐화(Encapsulation): 멤버 변수는 private으로 보호한다.
// AllowPrivateAccess를 통해 C++에서는 private이지만
// 블루프린트/에디터에서는 접근할 수 있게 한다.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
class UStaticMeshComponent* MeshComp;
// 위에서 정의한 구조체를 멤버 변수로 활용한다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats", meta = (AllowPrivateAccess = "true"))
FCharacterStats ActorStats;
};
2) 소스 파일 : 로직을 구현한다. C++ 소스 파일에서는 부모 클래스의 기능을 호출(Super::)하고, 객체를 초기화하며, 실제 동작을 구현한다.
#include "MyBaseActor.h"
#include "Components/StaticMeshComponent.h"
// 생성자: 객체 초기화
AMyBaseActor::AMyBaseActor()
{
// 매 프레임마다 Tick 함수를 호출할지 여부 설정.
PrimaryActorTick.bCanEverTick = true;
// 컴포넌트를 생성하고 루트 컴포넌트로 설정.
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
RootComponent = MeshComp;
}
// 게임이 시작되거나 액터가 스폰될 때 호출.
void AMyBaseActor::BeginPlay()
{
// 다형성: 부모(AActor)가 가진 기본 로직을 먼저 실행하도록 보장
Super::BeginPlay();
}
// 매 프레임마다 호출.
void AMyBaseActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// 캡슐화된 상태(구조체 데이터)를 변경하는 퍼블릭 메서드.
void AMyBaseActor::TakeDamage(int32 DamageAmount)
{
// 구조체 내부의 데이터 수정
ActorStats.Health -= DamageAmount;
// 체력이 0 이하가 되면 파괴하는 간단한 로직
if (ActorStats.Health <= 0)
{
Destroy(); // AActor에서 상속받은 함수
}
}
3) 언리얼 C++ 구조체 사용 시 핵심 포인트 :
언리얼에서 USTRUCT는 주로 체력, 인벤토리 아이템 정보, 스킬 데미지 공식 등 연관된 데이터를 깔끔하게 묶어 클래스 내부를 정리하는 용도로 사용한다. 구조체 이름 앞에는 접두사 "F"를 붙이는 접두사 규칙이 있고, 메모리 관리의 경우 구조체 자체는 언리얼 가비지 컬렉터에 의해 자동으로 메모리가 관리되지 않으므로, 클래스 내부의 UPROPERTY로 선언하여 관리 생명주기를 클래스에 의존하게 만드는 것이 일반적이다.
4) 구조체(USTRUCT)와 클래스(UCLASS)의 메모리 차이
언리얼 엔진 개발에서 메모리 관리 방식을 이해하는 것은 성능 최적화와 크래시 방지의 핵심이다.
먼저, USTRUCT를 살펴보자.
(1) 메모리 할당 위치 : 주로 스택(Stack)영역에 할당되거나, 다른 클래스의 멤버 변수로 포함되어 해당 클래스가 위치한 힙메모리 내부에 직접 포함된다.
(2) 수명 관리: 표준 C++ 수명 주기 규칙을 엄격하게 따른다. 함수 내부에서 지역 변수로 선언된 구조체는 함수 블록을 벗어나는 순간 스택에서 즉시 소 멸한다.
(3) 가비지 컬렉션: 언리얼 가비지 컬렉터의 직접적인 관리 대상이 아니다. 구조체 내부에 UPROPERTY() 참조 포인터가 없다면 엔진은 구조체의 생성과 소멸을 추적하지 않는다.
(4) 메모리 메커니즘 : 구조체는 값(Value) 타입으로 동작한다. UCLASS와 달리 내부에 선언된 변수들의 크기(및 바이트) 외에는 추가적인 메모리 오버헤드가 거의 없다. 따라서 많은 양의 데이터를 다뤄야 하는 인벤토리 아이템이나 레이더 데이터 등에 구조체가 필수적인 이유이다. 또한, 구조체는 함수 인자로 넘기거나 다른 변수에 대입할 때 기본적으로 전체 데이터가 복사되게 된다. 따라서, 내부에 큰 배열이나 많은 변수가 포함된 구조체를 복사값으로 넘기면 심각한 성능 저하가 발생할 수 있다. 그러니 언리얼 C++에서는 항상 참조 전달을 사용하는 것이 원칙이다.
참조 전달은 const FMyStruct& MyStruct 처럼 사용하면 된다.
다음으로, UCLASS를 살펴보자.
(1) 메모리 할당 위치 : 언제나 힙(Heap)영역에 할당된다. 언리얼 C++에서는 표준 new 연산자를 사용하지 않고, 엔진의 NewObject<T>() 또는 SpawnActor<T>() 함수를 통해 시스템 전용 힙 메모리에 객체를 생성한다.
(2) 수명 관리와 가비지 컬렉션: 언리얼 엔진의 가비지 컬렉션 시스템이 수명을 전적으로 제어한다. 객체가 생성되면 포인터(주소값)를 통해 참조되며, 더 이상 이 객체를 참조하는 UPROPERTY() 변수가 없거나 Destroy() 함수가 호출되면, 엔진이 주기적으로 실행하는 가비지 컬렉션 사이클에 의해 메모리에서 완전히 스윕된다.
(3) 메모리 메커니즘 : UCLASS 인스턴스는 언리얼 엔진의 내부 관리를 위해 객체당 수십 바이트 이상의 내부 메타데이터 오버헤드를 가진다.
모든 UCLASS 객체는 UObject를 부모로 두고, 엔진이 관리하는 전역 객체 배열(GUObjectArray)에 등록되어 라이프사이클이 관리된다. 보통 포인터 소유권과 댕글링 포인터가 C++ 개발 시 가장 문제가 되는 요소 중 하나(메모리 누수, 크래시 발생)인데, 이를 방지하기 위해서 UPROPERTY() 매크로를 붙여주면 해당 객체가 소멸할 때 엔진이 이 포인터를 자동으로 nullptr로 만들어준다. 그런데, 두 객체가 서로를 UPROPERTY() 포인터로 강하게 참조하고 있다면, GC는 둘 다 사용중인 것으로 판단해서 영원히 메모리에서 해제하지 않는 순환 참조 문제가 발생할 수 있다. 이 경우에는 '약한 참조'를 위해서 약한 참조 포인터인 TWeakObjectPtr을 사용해서 GC의 수거를 방지하지 않으면서도 안전하게 객체의 생존 여부를 확인하게 할 수 있다.
UCLASS와 USTRUCT에 대해서 간단하게 알아보았으니, "데이터를 묶고(USTRUCT), 객체를 만들고(UCLASS), 행동을 규격화(Interface)하는 일련의 흐름을 이해하기 위해서 다음에는 언리얼 C++ 인터페이스의 구현 방법을 소스 코드와 함께 알아보기로 했다.
[구조체 : 데이터를 가볍게 묶는다]
-> 구조체를 클래스의 멤버 변수로 포함시킨다.
[클래스 : 데이터 + 행동 + 생명 주기. 상속을 통한 확장이 일어나나 강한 결합이 발생한다.]
-> 클래스 간의 의존성(강한 결합)을 끊기 위해 인터페이스를 적용한다.
[인터페이스 : 행동을 규격화 한다. 다형성을 확보하는 단계]
*오늘의 총평*
이력서와 자기소개서를 쓰면서 생활 루틴을 잘 지키고, 공부 밀도를 높이기 위해서 고민해보기로 했다. 추가적으로 게임 개발 직무 관련해서 전반적인 지식을 습득하기 위해서 수학 공부를 시작하고 애니메이션 리깅, 그 외 여러가지 AI 활용법에 대해서도 알아보았다.
'TIL' 카테고리의 다른 글
| 06.05 TIL (0) | 2026.06.05 |
|---|---|
| 06.04 TIL (Ch.3 개인 과제 발제) (0) | 2026.06.04 |
| 06.01 TIL (이력서 세션 및 커리어 데이) (0) | 2026.06.01 |
| 05.29 TIL (ch.2 팀프로젝트 day6) (0) | 2026.05.29 |
| 05.28 TIL (ch.2 팀프로젝트 day5) (0) | 2026.05.28 |