일정표
| 시간 | 할 일 | 비고 |
| 08:00~10:00 | 코드카타, Atanny, 프로그래머스 | |
| 10:00~13:00 | 언리얼 C++ 개인과제 | |
| 14:00~18:00 | 언리얼 C++ 강의 수강 | |
| 19:00~21:00 | 오늘의 수학, TIL | |
| 21:00~22:00 | 운동 | |
| 22:00~23:30 | 언리얼 개인 공부 |
*오늘의 코드카타*
문제. 문자열 내림차순으로 배치하
문자열 s에 나타나는 문자를 큰것부터 작은 순으로 정렬해 새로운 문자열을 리턴하는 함수, solution을 완성해주세요.
s는 영문 대소문자로만 구성되어 있으며, 대문자는 소문자보다 작은 것으로 간주합니다.
#include <string>
#include <algorithm>
using namespace std;
string solution(string s) {
string answer = "";
sort(s.begin(), s.end(), greater<char>());
answer = s;
return answer;
}
문제에서 제시하는 게 char 자료형을 내림차순으로 정렬하라는 것과 같다.
#include <string>
#include <algorithm>
using namespace std;
string solution(string s) {
string answer = "";
sort(s.rbegin(), s.rend());
answer = s;
return answer;
}
rbegin(), rend() 역방향 반복자를 사용할 수 있다.
*프로그래머스*
1. [스택/큐] 올바른 괄호
문제 설명
괄호가 바르게 짝지어졌다는 것은 '(' 문자로 열렸으면 반드시 짝지어서 ')' 문자로 닫혀야 한다는 뜻입니다. 예를 들어
"()()" 또는 "(())()" 는 올바른 괄호입니다.
")()(" 또는 "(()(" 는 올바르지 않은 괄호입니다.
'(' 또는 ')' 로만 이루어진 문자열 s가 주어졌을 때, 문자열 s가 올바른 괄호이면 true를 return 하고, 올바르지 않은 괄호이면 false를 return 하는 solution 함수를 완성해 주세요.
<제한사항>
문자열 s의 길이 : 100,000 이하의 자연수
문자열 s는 '(' 또는 ')' 로만 이루어져 있습니다.
#include <string>
#include <stack>
using namespace std;
bool solution(string s)
{
if (s.front() == ')' || s.back() == '(') {
return false;
}
stack<char> st;
for (char c : s) {
if (c == '(') {
st.push(c);
} else if (c == ')') {
if (st.empty()) {
return false;
}
st.pop();
}
}
return st.empty();
}
스택 개념을 사용해서 풀 수 있다.
(가 나오면 스택에 집어넣고 그 다음을 검사한다. 만약 (가 나오면 계속 스택에 (를 집어넣는다. 그리고 )가 나오는 순간 괄호가 닫히게 되니까 스택에서 (를 제거한다(s.pop()), 다만 이때 스택에 아무것도 없다면( (가 나오지 않았다면), 조건을 만족하지 못하는 것이므로 바로 false를 반환한다.
)가 맨 앞에 나왔을 경우와 맨 뒤에 (가 나왔을 경우는 중간에 무엇이 나오든 어차피 false를 반환해야 하므로 미리 조건문을 넣어서 효율성을 조금이라도 더 챙겼다.
2. [스택/큐] 프로세스
문제 설명
운영체제의 역할 중 하나는 컴퓨터 시스템의 자원을 효율적으로 관리하는 것입니다. 이 문제에서는 운영체제가 다음 규칙에 따라 프로세스를 관리할 경우 특정 프로세스가 몇 번째로 실행되는지 알아내면 됩니다.
1. 실행 대기 큐(Queue)에서 대기중인 프로세스 하나를 꺼냅니다.
2. 큐에 대기중인 프로세스 중 우선순위가 더 높은 프로세스가 있다면 방금 꺼낸 프로세스를 다시 큐에 넣습니다.
3. 만약 그런 프로세스가 없다면 방금 꺼낸 프로세스를 실행합니다.
3.1 한 번 실행한 프로세스는 다시 큐에 넣지 않고 그대로 종료됩니다
예를 들어 프로세스 4개 [A, B, C, D]가 순서대로 실행 대기 큐에 들어있고, 우선순위가 [2, 1, 3, 2]라면 [C, D, A, B] 순으로 실행하게 됩니다
현재 실행 대기 큐(Queue)에 있는 프로세스의 중요도가 순서대로 담긴 배열 priorities와, 몇 번째로 실행되는지 알고싶은 프로세스의 위치를 알려주는 location이 매개변수로 주어질 때, 해당 프로세스가 몇 번째로 실행되는지 return 하도록 solution 함수를 작성해주세요
<제한사항>
priorities의 길이는 1 이상 100 이하입니다
priorities의 원소는 1 이상 9 이하의 정수입니다
priorities의 원소는 우선순위를 나타내며 숫자가 클 수록 우선순위가 높습니다
location은 0 이상 (대기 큐에 있는 프로세스 수 - 1) 이하의 값을 가집니다
priorities의 가장 앞에 있으면 0, 두 번째에 있으면 1 … 과 같이 표현합니다
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
int solution(vector<int> priorities, int location) {
queue<pair<int, int>> q;
priority_queue<int> max_heap;
for (int i = 0; i < priorities.size(); ++i) {
q.push({priorities[i], i});
max_heap.push(priorities[i]);
}
int execution_order = 0;
while (!q.empty()) {
int current_priority = q.front().first;
int current_index = q.front().second;
q.pop();
if (current_priority < max_heap.top()) {
q.push({current_priority, current_index});
}
else {
execution_order++;
max_heap.pop();
if (current_index == location) {
return execution_order;
}
}
}
return execution_order;
}
운영체제의 우선순위 스케줄링 방식을 구현하는 문제이다.
핵심은 <프로세스, 우선순위> 쌍을 저장하고 이를 큐에 넣어 사용해야 한다는 것이다.
그리고 새로 꺼낸 프로세스의 우선순위를 기존 프로세스들의 우선순위와 비교하기 위해서 우선순위 정렬용 최대 힙을 사용하면 효율적이다.
3. [입문] 가까운 수
정수 배열 array와 정수 n이 매개변수로 주어질 때, array에 들어있는 정수 중 n과 가장 가까운 수를 return 하도록 solution 함수를 완성해주세요.
<제한사항>
1 ≤ array의 길이 ≤ 100
1 ≤ array의 원소 ≤ 100
1 ≤ n ≤ 100
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
int solution(vector<int> array, int n) {
if (find(array.begin(), array.end(), n) != array.end())
{
return n;
}
array.push_back(n);
sort(array.begin(), array.end());
int idx = find(array.begin(), array.end(), n) - array.begin();
if (idx == 0)
{
return array[1];
}
else if (idx == array.size() - 1)
{
return array[idx - 1];
}
else
{
int left_val = array[idx - 1];
int right_val = array[idx + 1];
int left_diff = abs(left_val - n);
int right_diff = abs(right_val - n);
if (left_diff <= right_diff)
{
return left_val;
}
else
{
return right_val;
}
}
}
(1) sort로 정렬하고 좌우 값을 사용한 절대값을 비교해서 값 반환하기
find를 해서 array에 이미 n이 있다면 어차피 답도 n이므로 맨 처음부터 걸러내도록 조건식을 작성했다. 다만 이렇게 구현하면 결국 array의 원소가 많아질 수록 n을 집어 넣고 sort로 정렬하는 과정에서 많은 메모리가 소모될 수밖에 없다.
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
int solution(const vector<int>& array, int n) {
auto it = min_element(array.begin(), array.end(), [n](int a, int b) {
int diff_a = abs(a - n);
int diff_b = abs(b - n);
if (diff_a == diff_b) {
return a < b;
}
return diff_a < diff_b;
});
return *it;
}
(2) 메모리 최적화, 성능 최적화, 확장성을 모두 개선해보자.
C++ 라이브러리의 std::min_element와 람다식을 사용하고, 참조자 &를 통해서 불필요한 메모리 복사를 방지해보자.
먼저 매개변수로 const vector<int>& array를 사용해서, 값에 의한 호출을 피하고 상수 참조를 사용해서 배열 전체가 메모리에 복사되는 오버헤드를 원천적으로 차단한다.
min_element를 사용해서 배열을 단 한번만 순회하도록 만든다. 굳이 불필요하게 정렬하지 않는다.
반환자(it) 기반으로 작성해서 여러 다른 자료구조로 컨테이너가 변경되더라도 코드 수정 없이 유연하게 대처할 수 있게 했다.
람다 함수 내에서 diff_a == diff_b 일 때, a < b 를 반환하도록 해서 가장 가까운 수가 여러개일 경우 더 작은 수를 반환한다는 요구사항까지 챙긴다.
<람다 함수>
[n] : n은 람다 함수 밖에서 받은 값이므로 해당 n 변수를 가져다 쓸 수 있도록 캡처 블록을 사용한다.
if (diff_a == diff_b) { return a < b; } : 두 절대값이 같으면--> a < b 가 true이면 a가 이겼다고 판정한다(min_element 안에서의 규칙)
두 절대값이 다르면, 더 값이 작은 쪽이 이기면 된다. 따라서, diff_a가 diff_b보다 작으면 true를 반환해서 a가 이겼다고 판정한다. (그 반대이면 false를 반환해서 b가 이겼다고 판정한다)
결과적으로, min_element(array.begin(), array.end(), 람다식) 의 결과로 도출된 it(반환자)를 반환받고, 최종적으로 *it를 통해 그 위치에 있는 진짜 값을 꺼내어 반환하면 된다.
4. [입문] 369게임
머쓱이는 친구들과 369게임을 하고 있습니다. 369게임은 1부터 숫자를 하나씩 대며 3, 6, 9가 들어가는 숫자는 숫자 대신 3, 6, 9의 개수만큼 박수를 치는 게임입니다. 머쓱이가 말해야하는 숫자 order가 매개변수로 주어질 때, 머쓱이가 쳐야할 박수 횟수를 return 하도록 solution 함수를 완성해보세요.
#include <string>
using namespace std;
int solution(int order) {
int answer = 0;
string str_num = to_string(order);
for (int i = 0; i < str_num.length(); i++) {
if (str_num[i] == '3' || str_num[i] == '6' || str_num[i] == '9') {
answer++;
}
}
return answer;
}
(1) for 문과 Or 논리연산자를 사용해서 문제 풀기
int solution(int order) {
int answer = 0;
while (order > 0) {
int digit = order % 10;
if (digit == 3 || digit == 6 || digit == 9) {
answer++;
}
order /= 10;
}
return answer;
}
(2) 성능 면에서 가장 유리한(to_string을 사용하지 않음) 정수 연산만으로 각 자릿수 판별하기
#include <string>
#include <algorithm>
using namespace std;
int solution(int order) {
string str_num = to_string(order);
int answer = count(str_num.begin(), str_num.end(), '3') +
count(str_num.begin(), str_num.end(), '6') +
count(str_num.begin(), str_num.end(), '9');
return answer;
}
(3)C++ <algorithm> 헤더의 std::count를 사용해서 작성하기(간결하고 가독성이 좋음)
5. [입문] 암호 해독
군 전략가 머쓱이는 전쟁 중 적군이 다음과 같은 암호 체계를 사용한다는 것을 알아냈습니다
암호화된 문자열 cipher를 주고받습니다
그 문자열에서 code의 배수 번째 글자만 진짜 암호입니다
문자열 cipher와 정수 code가 매개변수로 주어질 때 해독된 암호 문자열을 return하도록 solution 함수를 완성해주세요.
<제한사항>
cipher는 소문자와 공백으로만 구성되어 있습니다
공백도 하나의 문자로 취급합니다
#include <string>
using namespace std;
string solution(string cipher, int code) {
string answer = "";
for (int i = code - 1; i < cipher.length(); i += code) {
answer += cipher[i];
}
return answer;
}
크게 어려운 문제가 아니었고, 굳이 더 이상 개선할 필요는 없어보였다. 추가하자면 reserve로 미리 메모리를 할당해 놓아서 메모리 재할당을 막는 방법이 있다.
6. [입문] 대문자와 소문자
문자열 my_string이 매개변수로 주어질 때, 대문자는 소문자로 소문자는 대문자로 변환한 문자열을 return하도록 solution 함수를 완성해주세요. my_string은 영어 대문자와 소문자로만 구성되어 있습니다
#include <string>
#include <cctype>
using namespace std;
string solution(string my_string) {
string answer = "";
for (char c : my_string) {
if (isupper(c)) {
answer += tolower(c);
}
else if (islower(c)) {
answer += toupper(c);
}
else {
answer += c;
}
}
return answer;
}
가장 기본적인 형태의 풀이. cctype 헤더를 포함시키고, isupper, islower, toupper, tolower를 사용해서 문제의 조건대로 코드를 작성한다.
*Atanny*
1. XR 입력 구조와 핸드트래킹 원리
XR(eXtended Reality)의 정의 :
현실 세계와 가상 세계를 결합한 모든 몰입형 기술을 포괄하는 용어. VR(가상현실), AR(증강현실), MR(혼합현실) 등을 모두 포함하며, 물리적 현실과 디지털 요소를 다양한 방식으로 혼합하는 모든 기술을 의미한다.
VR 콘텐츠에서 6DOF(Six Degrees of Freedom) 입력 :
6DOF는 3개의 이동 축(X, Y, Z)과 3개의 회전 축(Pitch, Yaw, Roll)을 의미하는 것으로 사용자의 머리나 컨트롤러가 공간 내에서 완전히 자유롭게 움직이고 회전할 수 있음을 뜻하며, 몰입감 있는 VR 경험을 제공하는 핵심 요소이다.
핸드트래킹 기술의 작동 원리 :
Meta Quest나 HoloLens 등의 기기는 적외선 카메라, RGB 카메라, 깊이 센서 등을 활용하여 맨손 상태에서도 손의 위치, 형태, 제스처를 추적할 수 있다. 컴퓨터 비전과 머신러닝 알고리즘이 손가락 관절의 위치를 실시간으로 인식하며, 컨트롤러 없이도 자연스러운 상호작용을 가능하게 한다.
2. 언리얼 엔진 패키징과 빌드 설정
패키징(Packaging) :
패키징은 최종 배포를 위한 프로세스로, 게임을 독립 실행 가능한 형태로 만들고, 최종 사용자에게 배포할 수 있도록 변환하고, 에디터 전용 리소스를 제거하여 최적화한다. 실시간 디버깅 환경은 개발 단계에서 에디터 내에서 제공되는 기능일 뿐 패키징의 목적이 아니다.
Windows용 패키징 :
Windows 플랫폼용 패키징 시에는 .exe 확장자를 가진 실행 파일이 생성된다. .app은 macOS 애플리케이션 번들이며, .apk는 Android 패키지, .dmg는 macOS 디스크 이미지 파일 형식이다.
언리얼 엔진의 빌드 구성 중 Development 빌드:
Development 빌드는 디버깅 기능과 성능 최적화가 적절히 균형을 이루어 개발 및 테스트 단계에 가장 적합한 구성이다. 최대 성능과 디버깅 기능 제거는 Shipping 빌드의 특징이고, Development 빌드도 패키징 및 배포가 가능하다.
3. 게임 메커니즘과 목표 기획 설계
게임 기획 문서에서 '핵심 메커니즘' 정의하기 :
플레이어가 게임 내에서 반복적으로 수행하는 기본적인 상호작용과 규칙. 점프하기, 공격하기, 아이템 수집하기 등이 해당된다.
FPS 게임의 핵심 메커니즘 :
조준 시스템, 사격 메커니즘, 이동 및 점프 메커니즘, 무기 재장전 및 탄약 관리 시스
게임의 목표(Game Objective) 정의하기 :
플레이어가 무엇을 달성해야 하는지 명확하게 제시해야 한다. 승리조건, 실패조건, 구체적인 목적을 포함해야 한다.
*수학 공부*
이번 주제 : 선형대수학의 행렬
행렬은 공간 전체를 늘리고, 돌리고, 이동시키는 데에 사용된다. 게임에서 공간을 다루기 위해서는 행렬을 잘 이해하고 있어야 한다. 게임 프로그래밍에서 행렬이 어떻게 이용되는지 알아보고자 한다.
1. 게임 프로그래밍에서의 행렬 [SRT 변환]
3D 공간의 캐릭터 모델은 수만개의 점으로 이루어져 있다. 그런데 캐릭터가 이동할 때마다 수만개의 점에 일일이 더하기와 삼각함수를 계산해서 이동 위치를 계산하는 것은 불가능에 가깝다. 이때 4 * 4 행렬 하나만 만들어두면 수만 개의 점을 한 번의 행렬 곱셈 연산으로 동시에 변환(Transform)할 수 있다.
행렬은 오브젝트의 3가지 상태를 담아내는데, 이를 SRT(Scale, Rotation, Translation)이라고 부른다.
요소는 3개인데 4*4 행렬을 사용하는 이유는, 가상의 4번째 차원인 동차 좌표계(Homogeneous Coordinates)를 도입해서 행렬의 곱셈 한번으로 크기와 회전에 덧붙여 이동까지 계산하기 위해서이다.
또, 행렬 곱셈은 부모-자식 계층 구조에서 엄청난 위력을 보인다. 행렬끼리 곱하면 변환 내역이 하나로 합쳐진다는 특성을 이용해서, 부모의 행렬과 자식의 행렬을 곱해주기만 하면 캐릭터의 최종 월드 위치가 계산될 수 있게 된다. 이를 언리얼 에디터에서 구현한 것이 FTransform 클래스이다. 사실 이미 엔진이 다 구현해 놓아서 건들 필요는 없지만 내부 원리를 알아 둘 필요가 있다.
#include "Math/Transform.h"
void AMyCharacter::EquipWeapon(AActor* Weapon)
{
// 캐릭터 손(소켓)의 월드 좌표와 회전 상태를 가져옴
FTransform HandTransform = GetMesh()->GetSocketTransform("RightHandSocket", ERelativeTransformSpace::RTS_World);
// 무기가 손에 맞게 쥐어지기 위한 미세 조정 (로컬 변환 행렬)
// 위치는 0, 회전은 90도, 크기는 그대로 1배
FVector LocalLocation(0.0f, 0.0f, 0.0f);
FRotator LocalRotation(0.0f, 90.0f, 0.0f);
FVector LocalScale(1.0f, 1.0f, 1.0f);
// 무기의 로컬 상태를 Transform(내부적으로 행렬)으로 만듦
FTransform WeaponLocalTransform(LocalRotation, LocalLocation, LocalScale);
// 두 변환을 곱셈 (자식 변환 * 부모 변환)
// 언리얼의 FTransform 곱셈 연산자(*)는 내부적으로 행렬(FMatrix) 곱셈을 수행
FTransform FinalWorldTransform = WeaponLocalTransform * HandTransform;
// 무기를 최종 계산된 월드 공간으로 이동시킴
Weapon->SetActorTransform(FinalWorldTransform);
}
2. 좌표계와 행렬
언리얼에서 제일 조심해야 되고 버그를 많이 만들어내는 두 가지 공간인 World Space와 Local Space를 언리얼 엔진 C++ 코드로 제어하기 위해서는 행렬 개념을 잘 이해하고 있어야 한다. 월드 공간은 게임 맵 전체의 절대적인 기준점을 가진 공간이고, 로컬 공간은 특정 오브젝트 자신이 중심이 되는 상대적인 공간이다. 이 두 공간 사이를 오가는 것을 공간 변환(Space Transformation)이라고 부르고, 이 변환 연산의 내부에 4 * 4 행렬 곱셈과 역행렬(Inverse Matrix) 개념이 들어가 있다.
(1) 로컬 공간에서 월드 공간으로
예를 들어, 총구에서 탄약이 나가는 기능을 구현할 때, 캐릭터가 총을 발사하면 탄약이 해당 총구에서 스폰되게 된다. 총구 기준으로 탄약이 스폰되는 것은 로컬 좌표이지만, 탄약을 월드에 생성하기 위해서는 월드 좌표가 필요하게 된다. 이때, 캐릭터의 현재 회전과 위치 행렬을 곱해서 로컬 좌표를 월드 좌표로 변환하게 된다.
#include "GameFramework/Actor.h"
#include "Math/Transform.h"
void AMyCharacter::FireWeapon()
{
// 1. 캐릭터 기준 총구의 로컬 좌표 (앞 +X, 위 +Z)
FVector MuzzleLocalPos(100.0f, 0.0f, 50.0f);
// 2. 캐릭터의 월드 변환 행렬(Transform)을 가져옴
FTransform CharacterTransform = GetActorTransform();
// 3. 로컬 좌표 -> 월드 좌표로 변환 (행렬 곱셈)
// 공식: WorldPos = LocalPos * CharacterTransformMatrix
FVector MuzzleWorldPos = CharacterTransform.TransformPosition(MuzzleLocalPos);
// 4. 변환된 월드 좌표에 총알 생성
// GetWorld()->SpawnActor<ABullet>(BulletClass, MuzzleWorldPos, GetActorRotation());
}
(2) 월드 공간에서 로컬 공간으로
캐릭터가 폭발 데미지를 입은 경우 폭발 지점은 월드 좌표 기준으로 스폰된다. 이때 화면에서 내 캐릭터 기준으로 폭발 데미지 방향을 표시하는 피격 UI를 띄우게 하려면, 월드 공간의 좌표를 로컬 공간의 좌표로 변환하는 계산이 수행되어야 한다. 폭발 지점의 월드 좌표를 캐릭터의 로컬 좌표로 변환하는 역행렬 곱셈을 수행하는 것이다.
#include "GameFramework/Actor.h"
#include "Math/Transform.h"
void AMyCharacter::TakeExplosionDamage(FVector ExplosionWorldPos)
{
// 1. 캐릭터의 월드 변환 행렬을 가져옴
FTransform CharacterTransform = GetActorTransform();
// 2. 월드 좌표 -> 로컬 좌표로 역변환 (역행렬 곱셈 발생)
// 이제 ExplosionLocalPos는 '내 캐릭터'를 (0,0,0)으로 둔 상대 좌표계가 됨.
FVector ExplosionLocalPos = CharacterTransform.InverseTransformPosition(ExplosionWorldPos);
// 3. 로컬 공간에서는 판단이 아주 쉬워짐 (X는 앞뒤, Y는 좌우)
if (ExplosionLocalPos.X > 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("전방에서 폭발"));
}
else
{
UE_LOG(LogTemp, Warning, TEXT("후방에서 폭발"));
}
if (ExplosionLocalPos.Y > 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("오른쪽에서 데미지"));
// 우측 피격 UI 출력 로직 삽입
}
}
물론 벡터 개념에서 배웠던 내적과 외적을 사용해서 전후좌우를 판별할 수도 있지만, 이렇게 InverseTransformPosition을 사용하면 3D 공간에서 모든 좌표축을 한번에 얻을 수 있어 UI 작업에 더 유용하게 사용할 수 있다.
3. MVP 행렬 파이프라인
게임에 등장하는 특정 오브젝트를 플레이어의 2D 모니터 화면의 픽셀 위치에 찍기 위해서 그래픽 카드는 모든 점에 대해 3번의 행렬 곱셈을 수행하게 된다.
(1) M(Model Matrix) : 로컬 공간 -> 월드 공간
3D 모델러가 블렌더와 같은 툴에서 만든 오브젝트는 자기 자신의 발밑(0,0,0)으로 하는 로컬 공간에 존재한다. 이 오브젝트를 월드의 특정 위치에 배치하고, 크기를 키우고, 회전시키기 위해 사용하는 행렬이 모델 행렬이다.
(2) V(View Matrix) : 월드 공간 -> 카메라(뷰) 공간
컴퓨터 그래픽스에서는 '카메라'가 세상의 중심에 고정되어 있고, 카메라가 이동하는 방향의 반대로 월드를 움직이는 방식이다. 따라서 뷰 행렬에서는 카메라의 월드 행렬을 구한 뒤, 그 역행렬을 온 세상 모든 물체에 곱하는 방식으로 뷰 공간을 구현한다.
(3) P(Projection Matrix) : 카메라 공간 -> 화면(클립) 공간
뷰 공간을 구현했으니, 이 3D 공간을 2D 모니터에 원근감을 적용해서 변환시켜야 한다. 여기에서 FOV를 적용하고 2D 픽셀 좌표로 최종 변환하는 단계를 거치게 된다.
결과적으로 3D 공간의 점 하나가 화면에 그려지기 위한 최종 연산식은 다음과 같다.
>> V(screen) = V(local) * M * V * P
다만 이 연산식은 C++ 에서 계산하지 않고 GPU의 버텍스 셰이더로 행렬들을 넘겨서 병렬로 처리한다. 그럼 우리 C++ 프로그래머들은 뭘해야할까.
이 파이프라인에 필요한 M, V, P 행렬을 준비해서 셰이더로 넘겨주는 역할을 하는 것이다.
// 셰이더나 렌더링 파이프라인에 데이터를 넘겨줄 때 자주 볼 수 있는 언리얼 C++ 내부 구조 예시
void AMyCameraManager::UpdateCameraMatrices()
{
// 1. View Matrix 계산 (카메라 위치와 회전의 역행렬)
FVector CameraLocation = GetCameraLocation();
FRotator CameraRotation = GetCameraRotation();
// FTranslationMatrix와 FRotationMatrix를 조합하고 역(Inverse)을 취해 뷰 행렬 생성
FMatrix ViewMatrix = FTranslationMatrix(-CameraLocation) * FInverseRotationMatrix(CameraRotation);
// 2. Projection Matrix 계산 (FOV와 화면 비율 적용)
float FOV = FMath::DegreesToRadians(90.0f);
float AspectRatio = 1920.0f / 1080.0f;
float NearClip = 10.0f;
float FarClip = 10000.0f;
// 언리얼의 원근 투영 행렬 생성 함수 (내부에 탄젠트 수학이 들어있음)
FMatrix ProjectionMatrix = FPerspectiveMatrix(FOV, AspectRatio, NearClip, FarClip);
// 3. ViewProjection 행렬 완성 (M은 각 오브젝트마다 다르므로, V와 P만 먼저 묶어둠)
FMatrix ViewProjectionMatrix = ViewMatrix * ProjectionMatrix;
// 이 ViewProjectionMatrix가 그래픽 카드로 전송, 계산식에 들어가서 화면에 그려지게 됨.
*Unreal C++*
1. Unreal Engine C++으로 회전발판과 움직이는 장애물 만들기
1) 서로 다른 Actor 클래스 2개 이상 구현하기
움직이는 발판을 만들기 위해서 C++ 액터 클래스를 만들고, ArtResource 파일을 Migrate해서 가지고 온 에셋 파일을 적용했다.
USceneComponent, UStaticMeshComponent 와 같은 컴포넌트를 선언하고 에셋을 붙였다.
2) Tick 함수 기반 동적 Transform 변경 + 리플렉션 적용
(1) MoveFloor.h, MoveFloor.cpp
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MoveFloor.generated.h"
UCLASS()
class CH3WORLD_API AMoveFloor : public AActor
{
GENERATED_BODY()
public:
AMoveFloor();
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Platform|Components")
USceneComponent* SceneRoot;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Platform|Components")
UStaticMeshComponent* StaticMeshComp;
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Platform|Movement")
float MoveSpeed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Platform|Movement")
FVector MoveDirection;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Platform|Movement")
float MaxRange;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Platform|Movement")
FVector StartLocation;
float RunningTime;
};
#include "MoveFloor.h"
AMoveFloor::AMoveFloor()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMeshComp->SetupAttachment(SceneRoot);
//'/Game/Resources/Props/Floor_400x400.Floor_400x400'
//'/Game/Resources/Props/Materials/M_Rock.M_Rock'
static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/Floor_400x400.Floor_400x400"));
if (MeshAsset.Succeeded())
{
StaticMeshComp->SetStaticMesh(MeshAsset.Object);
}
static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Props/Materials/M_Rock.M_Rock"));
if (MaterialAsset.Succeeded())
{
StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
}
PrimaryActorTick.bCanEverTick = true;
MoveSpeed = 2.0f;
MoveDirection = FVector(0.0f, 1.0f, 0.0f);
MaxRange = 150.0f;
RunningTime = 0.0f;
}
void AMoveFloor::BeginPlay()
{
Super::BeginPlay();
StartLocation = GetActorLocation();
MoveDirection = MoveDirection.GetSafeNormal();
}
void AMoveFloor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
RunningTime += DeltaTime * MoveSpeed;
float TimeValue = FMath::Sin(RunningTime);
FVector NewLocation = StartLocation + (MoveDirection * MaxRange * TimeValue);
SetActorLocation(NewLocation);
}
주요 변수인 이동 속도, 이동 범위를 설정하고 에디터에서 조정할 수 있도록 UPROPERTY() 매크로를 선언했고, EditAnywhere, BlueprintReadWrite로 선언해서 해당 값을 수정하고 블루프린트에서도 사용할 수 있도록 했다.
특히 FMath::Sin(RunningTime)을 사용해서 발판이 부드럽게 움직이도록 구현했다.
(2)SpinFloor.h, SpinFloor.cpp
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SpinFloor.generated.h"
UCLASS()
class CH3WORLD_API ASpinFloor : public AActor
{
GENERATED_BODY()
public:
ASpinFloor();
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Platform|Components")
USceneComponent* SceneRoot;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Platform|Components")
UStaticMeshComponent* StaticMeshComp;
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Platform|RotationSpeed")
float RotationSpeed;
};
#include "SpinFloor.h"
ASpinFloor::ASpinFloor()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMeshComp->SetupAttachment(SceneRoot);
//'/Game/Resources/Props/Floor_400x400.Floor_400x400'
//'/Game/Resources/Props/Materials/M_Frame.M_Frame'
static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/Floor_400x400.Floor_400x400"));
if (MeshAsset.Succeeded())
{
StaticMeshComp->SetStaticMesh(MeshAsset.Object);
}
static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Props/Materials/M_Frame.M_Frame"));
if (MaterialAsset.Succeeded())
{
StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
}
PrimaryActorTick.bCanEverTick = true;
RotationSpeed = 45.0f;
}
void ASpinFloor::BeginPlay()
{
Super::BeginPlay();
}
void ASpinFloor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (!FMath::IsNearlyZero(RotationSpeed))
{
AddActorLocalRotation(FRotator(0.0f, 0.0f, RotationSpeed * DeltaTime));
}
}
SpinFloor는 컴포넌트의 중앙을 기준으로 회전하는 발판으로, RotationSpeed를 UPROPERTY()로 선언해서 에디터에서 값을 수정하여 회전 속도를 변경할 수 있도록 했다. 다만 BeginPlay(); 는 굳이 필요 없는 기능이라 삭제해도 무방할 것 같다. 기존에는 C++ 코드로 GetActorWorldLocation을 사용해서 게임이 시작되면 월드에 생성되게 작성했는데, 해당 기능을 삭제하면서 현재는 필요 없는 기능이 되었다.
3) Timer 시스템 활용하기
(1) TrapFloor.h, TrapFloor.cpp
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TrapFloor.generated.h"
UCLASS()
class CH3WORLD_API ATrapFloor : public AActor
{
GENERATED_BODY()
public:
ATrapFloor();
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Platform|Components")
USceneComponent* SceneRoot;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Platform|Components")
UStaticMeshComponent* StaticMeshComp;
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Platform|Interval")
float TrapInterval;
bool bIsVisible;
FTimerHandle ToggleTimerHandle;
void ToggleTrapState();
};
#include "TrapFloor.h"
ATrapFloor::ATrapFloor()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMeshComp->SetupAttachment(SceneRoot);
//'/Game/Resources/Props/Floor_400x400.Floor_400x400'
//'/Game/Resources/Props/Materials/M_Bush.M_Bush'
static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/Floor_400x400.Floor_400x400"));
if (MeshAsset.Succeeded())
{
StaticMeshComp->SetStaticMesh(MeshAsset.Object);
}
static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Props/Materials/M_Bush.M_Bush"));
if (MaterialAsset.Succeeded())
{
StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
}
PrimaryActorTick.bCanEverTick = false;
TrapInterval = 3.0f;
bIsVisible = true;
}
void ATrapFloor::BeginPlay()
{
Super::BeginPlay();
GetWorld()->GetTimerManager().SetTimer(ToggleTimerHandle, this, &ATrapFloor::ToggleTrapState, TrapInterval, true);
}
void ATrapFloor::ToggleTrapState()
{
bIsVisible = !bIsVisible;
SetActorHiddenInGame(!bIsVisible);
SetActorEnableCollision(bIsVisible);
}
이름은 TrapFloor로 거창하지만 사실 깜빡거리며 나타났다가 사라지는 발판이다.
FTimeHandle과 BeginPlay() 에서 GetWorld()->GetTimerManager().SetTimer() 를 사용해서 특정 시간 후 또는 주기적으로 함수를 호출할 수 있도록 했다. 이렇게 하면 무거운 Tick() 라이프 사이클 함수를 사용하지 않고도 나타났다 사라지는 발판 기능을 구현할 수 있다. TrapInterval 변수를 UPROPERTY()로 선언해서 에디터에서 수정할 수 있게 했고, bIsVisible 기본값은 true;로 초기화하고 false 값이 되면 SetActorHiddenInGame 값을 true로, true 값일 때 SetActorEnableCollision을 true로 해서 보일때만 해당 발판을 밟을 수 있도록 구현했다. 투명 발판을 만들고 싶다면 SetActorEnableCollision이 항상 true이도록 하면 된다.
*오늘의 총평*
개인과제를 하면서 언리얼 C++ 코드 작성법을 배워가며 학습하고 있다. 원하는 기능을 구현하고 제약 사항도 미리 체크해서 개선안을 내놓을 수 있을 만큼 연습하는 것이 지금의 목표이다. 학습 과제 이외에도 유튜브 공식 채널, 또는 Lyra Starter Game 과 같은 자료를 참고해서 별도로 추가적인 언리얼 에디터 기능을 이용할 수 있도록 연습도 병행하기로 했다.
'TIL' 카테고리의 다른 글
| 06.10 TIL (0) | 2026.06.10 |
|---|---|
| 06.09 TIL (0) | 2026.06.09 |
| 06.05 TIL (0) | 2026.06.05 |
| 06.04 TIL (Ch.3 개인 과제 발제) (0) | 2026.06.04 |
| 06.02 TIL (커리어 데이) (0) | 2026.06.02 |