일정표
| 시간 | 할 일 | 비고 |
| 08:00~10:00 | 코드카타, Atanny, 프로그래머스 | |
| 10:00~13:00 | 언리얼 C++ 3D 게임 만들기(강의 수강) | |
| 14:00~18:00 | 언리얼 C++ 3D 게임 만들기(강의 수강) | |
| 19:00~21:00 | 언리얼 C++ 3D 게임 만들기(강의 수강), 프로그래머스 | |
| 21:00~22:00 | 운동 | |
| 22:00~23:30 | 개인 공부 |
*오늘의 코드카타*
문제. [정렬]K번째 수
두 정수 left와 right가 매개변수로 주어집니다. left부터 right까지의 모든 수들 중에서, 약수의 개수가 짝수인 수는 더하고, 약수의 개수가 홀수인 수는 뺀 수를 return 하도록 solution 함수를 완성해주세요
#include <string>
#include <vector>
#include <cmath>
using namespace std;
int solution(int left, int right) {
int answer = 0;
for (int i = left; i <= right; i++) {
int root = sqrt(i);
if (root * root == i) {
answer -= i;
}
else {
answer += i;
}
}
return answer;
}
cmath 헤더의 sqrt() 함수를 이용해서 제곱근이 있는지 확인했다. 약수는 기본적으로 짝이 존재해서 제곱근을 기준으로 짝수가 된다. 즉, 제곱근이 있다면 약수의 개수가 홀수개이고 제곱근이 없다면 약수의 개수가 짝수이다.
따라서 for문으로 i를 left~right 까지 탐색하며 root * root == i 인 경우 i는 약수의 개수가 홀수이므로 answer에서 i를 빼준다.
*프로그래머스*
1. [스택/큐] 같은 숫자는 싫어
문제 설명
배열 arr가 주어집니다. 배열 arr의 각 원소는 숫자 0부터 9까지로 이루어져 있습니다. 이때, 배열 arr에서 연속적으로 나타나는 숫자는 하나만 남기고 전부 제거하려고 합니다. 단, 제거된 후 남은 수들을 반환할 때는 배열 arr의 원소들의 순서를 유지해야 합니다.
예를 들면,
arr = [1, 1, 3, 3, 0, 1, 1] 이면 [1, 3, 0, 1] 을 return 합니다.
arr = [4, 4, 4, 3, 3] 이면 [4, 3] 을 return 합니다
배열 arr에서 연속적으로 나타나는 숫자는 제거하고 남은 수들을 return 하는 solution 함수를 완성해 주세요
<제한 사항>
배열 arr의 크기 : 1,000,000 이하의 자연수
배열 arr의 원소의 크기 : 0보다 크거나 같고 9보다 작거나 같은 정수
#include <vector>
using namespace std;
vector<int> solution(vector<int> arr)
{
vector<int> answer;
int temp_num = arr[0];
answer.push_back(arr[0]);
for (int i = 1; i < arr.size(); i++) {
if (arr[i] != temp_num)
{
answer.push_back(arr[i]);
temp_num = arr[i];
}
else {
continue;
}
}
return answer;
}
(1) 배열을 for 반복문으로 순회하면서 앞에 숫자가 같지 않은 경우에만 answer 배열에 밀어 넣음.
#include <vector>
using namespace std;
vector<int> solution(vector<int> arr)
{
vector<int> answer;
for (int num : arr) {
if (answer.empty() || answer.back() != num) {
answer.push_back(num);
}
}
return answer;
}
(2) vector 자체가 stack 기능을 제공하므로, 벡터를 스택처럼 활용하는 방법.
굳이 임시 변수를 사용하지 않고 빈 배열이 들어와도 에러 없이 안전하게 작동한다.
answer.empty() || answer.back() != num 조건만으로도 문제가 요구하는 기능을 구현할 수 있다.
#include <vector>
#include <algorithm>
using namespace std;
vector<int> solution(vector<int> arr)
{
arr.erase(unique(arr.begin(), arr.end()), arr.end());
return arr;
}
(3) algorithm 헤더의 unique() 를 사용하는 방법.
unique 함수 자체가 중복된 원소들을 뒤로 밀어내고 그리고 중복이 제거된 구간의 끝 반환자를 반환하는 함수이다.
이 문제는 결국 unique() 함수의 기능 자체를 구현하는 것을 요구했기 때문에, erase와 unique()만 사용하면 굉장히 쉽게 구할 수 있다.
2. [스택/큐] 기능 개발
문제 설명
프로그래머스 팀에서는 기능 개선 작업을 수행 중입니다. 각 기능은 진도가 100%일 때 서비스에 반영할 수 있습니다.
또, 각 기능의 개발속도는 모두 다르기 때문에 뒤에 있는 기능이 앞에 있는 기능보다 먼저 개발될 수 있고, 이때 뒤에 있는 기능은 앞에 있는 기능이 배포될 때 함께 배포됩니다.
먼저 배포되어야 하는 순서대로 작업의 진도가 적힌 정수 배열 progresses와 각 작업의 개발 속도가 적힌 정수 배열 speeds가 주어질 때 각 배포마다 몇 개의 기능이 배포되는지를 return 하도록 solution 함수를 완성하세요.
<제한 사항>
작업의 개수(progresses, speeds배열의 길이)는 100개 이하입니다.
작업 진도는 100 미만의 자연수입니다.
작업 속도는 100 이하의 자연수입니다.
배포는 하루에 한 번만 할 수 있으며, 하루의 끝에 이루어진다고 가정합니다. 예를 들어 진도율이 95%인 작업의 개발 속도가 하루에 4%라면 배포는 2일 뒤에 이루어집니다.
#include <string>
#include <vector>
using namespace std;
vector<int> solution(vector<int> progresses, vector<int> speeds) {
vector<int> answer;
int p_day = 0;
int count = 0;
for (int i = 0; i < progresses.size(); ++i) {
int remain = 100 - progresses[i];
int day = (remain + speeds[i] - 1) / speeds[i];
if (count == 0) {
p_day = day;
count = 1;
} else if (day <= p_day) {
count++;
} else {
answer.push_back(count);
p_day = day;
count = 1;
}
}
if (count > 0) {
answer.push_back(count);
}
return answer;
}
(1) 풀기 전에 생각해보기
며칠 뒤에 각 기능이 100%가 되어야 하는지 먼저 세어보아야 한다. 100 - progresses[i]를 하면 남은 작업량이 나오고, 남은 작업량을 speeds[i]로 나누면 며칠이 걸리는 지 알 수 있다.
progresses 맨 첫번째 원소의 남은 날을 p_day라고 선언했다. p_day보다 뒤의 원소들이 작거나 같으면 뒤의 작업들이 완성되어있는 채로 남아있게 된다. 이후 p_day에 도달하게 되면 완성된 작업들이 함께 빠져나가면서 count를 증가시키게 된다.
만약 첫 번째 원소이거나 현재 기능이 기존 p_day 이내에 끝나는 경우라면 count == 0 일 것이고, 이 때에는 p_day를 day로 초기화 시킨다. 이후 count를 1로 잡아준다. 어차피 함께 완료되는 작업이 없기 때문이다.
완성된 작업이 빠져나가고 나면 남은 작업의 맨 앞 원소는 앞 기능보다 늦게 끝나는 경우가 될 것이다. 따라서 count++로 잘 올려놓은 count를 answer.에 집어 넣는다. 이제 p_day도 day로 초기화해주고, count는 1로 잡는다.
조금 어려운 구조인데, 사실 큐를 활용해서 푸는 방법이 더 직관적이다. 애초에 큐 형식으로 푸는 것이 좋은 문제이기도 하다.
#include <vector>
#include <queue>
using namespace std;
vector<int> solution(vector<int> progresses, vector<int> speeds) {
vector<int> answer;
queue<int> q;
for (int i = 0; i < progresses.size(); i++) {
int remain = 100 - progresses[i];
int day = (remain + speeds[i] - 1) / speeds[i];
q.push(day);
}
while (!q.empty()) {
int front_day = q.front();
q.pop();
int count = 1;
while (!q.empty() && q.front() <= front_day) {
count++;
q.pop();
}
answer.push_back(count);
}
return answer;
}
(2) "뒤에 있는 기능이 먼저 개발되더라도 앞에 있는 기능이 배포될 때 함께 배포된다" 라는 조건은 큐의 특징과 같다.
먼저, 큐에 남은 일 수(day)를 넣는다. 그러면 어차피 배포되어야 하는 순서대로 작업이 배치되어 있기 때문에 큐의 맨 앞이 나가야 뒤의 원소들도 밖으로 나갈 수 있게 된다. 이 때 front_day보다 뒤 기능의 남은 일수(day)가 작거나 같다면 이미 먼저 개발 완료되어 대기중인 상태이므로 함께 배포할 수 있다는 뜻이다. 따라서 count++; 를 하고 q.pop()으로 밖으로 내보낸다. 이것을 조건식이 성립되지 않을 때 까지 반복한다(while 문). 이후, 루프가 마무리 되면 지금까지 모인 count를 정답 배열에 저장한다.
*Atanny*
1. AI 감지 시스템과 자극 처리 이해
언리얼 엔진의 AI Perception 시스템 :
AI Perception 시스템의 주요 감각 컴포넌트에는 AI Sight, AI Hearing, AI Damage 등이 있다. 시각과 청각은 AI가 환경을 인식하는 가장 일반적인 감지 방식이다.
AI Sight Sense :
AI Sight Sense의 감지 범위는 시야 반경(Sight Radius)과 주변 시야 각도(Peripheral Vision Angle)의 조합으로 결정된다. 시야 반경은 AI가 볼 수 있는 최대 거리를, Peripheral Vision Angle은 AI의 시야각을 정의하여 원뿔 형태의 시야 영역을 만든다.
AI Perception 시스템에서 자극(Stimulus)이 AI에게 전달되는 과정 :
Actor가 자극을 발생 -> 등록된 Sense 컴포넌트가 해당 자극을 감지 -> AI Perception Component가 자극을 수집하고 처리 -> On Target Perception Updated등의 이벤트 발생
2. 유한 상태 머신과 스테이트 전이 설계
유한 상태 머신(FSM, Finite State Machine) :
FSM은 유한한 개수의 상태를 가지며, 특정 조건(이벤트나 입력)에 따라 한 상태에서 다른 상태로 전이하는 시스템이다. 게임 개발에서 캐릭터의 행동(Idle, Walk, Run, Jump)등을 관리하는 데 많이 사용된다.
게임 캐릭터의 FSM에서 Jump 상태에서 Fall 상태로 전이하기 위한 조건 :
캐릭터가 상승을 멈추고 하강하기 시작할 때 발생해야 한다. 이때, 수직 속도(Velocity Y)가 양수에서 음수로 변경되는 시점이 이를 나타내는 적절한 조건이 된다.
FSM의 상태 전이 조건 설계 시 고려해야 할 사항 :
전이 조건은 명확하고 모호하지 않아야 하며, 여러 전이 조건이 동시에 만족될 경우 우선순위를 정해야 한다. 또한 전이 조건은 게임 로직과 일관성을 유지해야 한다. FSM 설계에서 모든 상태가 다른 모든 상태로 직접 전이할 수 있어야 하는 것은 아니다.
3. 블루프린트 통신 방식의 이해
특정 액터의 레퍼런스를 직접 저장하여 해당 액터의 함수나 변수에 즉시 접근할 수 있는 가장 간단한 방식 :
Direct Reference 통신 방식이다. Cast는 타입 변환을 통한 접근, Event Dispatcher는 이벤트 기반 통신, Interface는 계약 기반 통신 방식이다.
블루프린트 통신 방식 :
Cast는 특정 클래스 타입으로 변환하여 해당 클래스의 기능에 접근할 수 있게 한다.
Event Dispatcher는 1:N 통신이 가능하여 여러 리스너가 하나의 이벤트를 구독할 수 있다.
Interface는 서로 다른 부모 클래스를 가진 클래스들도 같은 인터페이스를 구현할 수 있어 상속 관계와 무관하게 사용할 수 있다.
Direct Reference는 참조 대상 액터가 명확할 때 가장 효율적이다.
Event Dispatcher :
게임에서 플레이어가 아이템을 획득할 때, UI 매니저, 사운드 매니저, 퀘스트 매니저 등 여러 시스템에 동시에 알림을 보내게 된다. 이 상황에 가장 적합한 블루프린트 통신 방식이 Event Dispatcher이다.
Cast :
Actor 타입의 레퍼런스를 특정 클래스 타입으로 변환하여 해당 클래스의 고유 기능에 접근하기 위해 사용한다. 예를 들어 일반적인 타입(Actor)의 레퍼런스를 특정 클래스 타입(MyCharacter)로 변환해준다.
Interface :
서로 다른 클래스들이 같은 함수를 구현하도록 강제할 수 있고, 각 클래스가 자신만의 방식으로 구현하는 다형성을 보장하고, 특정 타입에 의존하지 않고(레퍼런스 없이도)함수를 호출할 수 있으며, 클래스 간 느슨한 결합을 만들어 유연성을 높일 수 있는 장점이 있다.
블루프린트 노드 사용하기:
[아이템 액터]
아이템 습득 시:
OnItemCollected --> Call
[UI 매니저]
Begin Play:
아이템 레퍼런스 획득
OnItemCollected에 --> Bind Event to (UpdateUI 함수)
Event Dispatcher는 'Call' 노드로 이벤트를 발생시키고, 'Bind Event to' 노드로 이벤트에 함수를 바인딩(구독)한다. 즉, 이 경우 아이템 액터에서는 Call로 이벤트를 발생시키고 UI 매니저에서는 Bind Event to로 자신의 UpdateUI 함수를 해당 이벤트에 연결한다.
*수학 공부*
이번 주제 : 삼각함수(Trigonometry)
벡터가 직선과 좌표를 다룬다면 삼각함수는 각도, 회전, 주기적인 움직임을 다룬다.
빗변의 길이가 1인 직각삼각형을 기준으로 생각하면 삼각함수를 이해하기 쉽다. 각도를 theta라고 할 때 컴퓨터는 이 각도를 가지고 2차원 좌표를 만들어낼 수 있기 때문이다.
코사인 (cos theta) : 직각삼각형의 밑변의 길이. X축 좌표. 게임에서는 주로 정면으로 나아가는 힘을 계산할 때 쓰인다.
사인 (sin theta) : 직각삼각형의 높이. Y축 좌표. 게임에서는 주로 위(또는 옆)으로 나아가는 힘을 계산할 때 쓰인다.
탄젠트 (tan theta) : 밑변 분의 높이, 즉 기울기이다. 다만 게임에서는 탄젠트 자체보다 역함수인 아크탄젠트(atan2)를 더 많이 사용한다. 두 점 사이의 좌표 차이를 집어 넣으면 '바라보는 각도'를 구할 수 있기 때문이다.
1) 삼각함수를 사용해서 각도와 좌표 구하기
C++ 표준 라이브러리 <cmath>의 std::sin(), std::cos() 함수는 우리가 쓰는 degree단위를 이해하지 못하므로, 호의 길이를 이용한 각도 단위인 Radian을 인자로 받아 사용한다. C++ 게임 프로그래밍에서도 당연히 이 변환 공식을 코드에 상수로 박아두고 시작하게 된다. 예를 들어, 180도(degree)는 pi 라디안으로 상수로 쓰고 시작한다.
#include <iostream>
#include <cmath>
// 전역 상수로 정의해두는 수학적 기호들
const float PI = 3.1415926535f;
const float DEG_TO_RAD = PI / 180.0f;
const float RAD_TO_DEG = 180.0f / PI;
struct Vector2 {
float x, y;
Vector2(float _x = 0.0f, float _y = 0.0f) : x(_x), y(_y) {}
};
int main() {
// ---45도 방향으로 초당 5미터씩 이동하는 벡터 구하기 ---
float moveAngleDegrees = 45.0f;
float speed = 5.0f;
// 1. C++ 함수에 넣기 위해 라디안으로 변환
float moveAngleRadians = moveAngleDegrees * DEG_TO_RAD;
// 2. Cos으로 X축, Sin으로 Y축의 단위 방향 벡터를 구함
Vector2 moveDirection;
moveDirection.x = std::cos(moveAngleRadians);
moveDirection.y = std::sin(moveAngleRadians);
// 3. 속도를 곱해 최종 이동 변위 구하기
Vector2 velocity = moveDirection * speed; // (구조체 연산자 생략, 개념적 곱셈)
velocity.x = moveDirection.x * speed;
velocity.y = moveDirection.y * speed;
std::cout << "--- 각도를 이용한 이동 벡터 계산 ---\n";
std::cout << moveAngleDegrees << "도 방향 이동 벡터: (" << velocity.x << ", " << velocity.y << ")\n\n";
// ---두 점 사이의 상대 좌표로 바라보는 각도 구하기 (atan2) ---
Vector2 playerPos(0.0f, 0.0f);
Vector2 mousePos(-3.0f, 3.0f); // 2사분면 위치 (예: 왼쪽 위)
// 도착점 - 출발점
Vector2 relativePos;
relativePos.x = mousePos.x - playerPos.x;
relativePos.y = mousePos.y - playerPos.y;
// std::atan2는 (y, x) 순서로 인자를 받으며, 0으로 나누는 예외 처리를 알아서 해줌(중요)
float lookRadian = std::atan2(relativePos.y, relativePos.x);
float lookDegree = lookRadian * RAD_TO_DEG;
std::cout << "--- 좌표를 이용한 바라보는 각도 계산 ---\n";
std::cout << "플레이어가 마우스를 바라보는 각도: " << lookDegree << "도\n";
return 0;
}
맨 앞에 전역 상수로 수학적 기호들을 정의해두고 각도와 좌표를 구할 때 사용하게 된다.
(1) 각도를 이용한 이동 : 캐릭터가 45도 방향을 바라보고 있을 때, 그 방향으로 이동할 벡터 구하기
'45도'를 C++에서 사용할 수 있도록 라디안으로 변환한다. 이후, 코사인 함수로 X축을, 사인 함수로 Y축의 단위 방향 벡터를 구한다.
이제 속도를 곱하면 최종적으로 해당 방향으로 이동할 벡터를 구할 수 있게 된다.
(2) 좌표를 이용한 각도 구하기 : 적이 내 기준 어디에 있는지 마우스 좌표 등으로 각도 알아내기
두 점 사이의 상대 좌표를 이용하면, atan2(역탄젠트)를 사용해서 바라보는 각도를 구할 수 있다.
std::atan2의 경우 (y, x) 순서로 인자를 받고 0으로 나누는 예외처리를 알아서 해주는 특징이 있으므로 사용하기 편하다.
2) 삼각함수를 사용해서 주기적 변동 구현하기
sin, cos 함수는 결과값이 항상 -1.0~1.0 사이를 무한히 반복하는 특징이 있다. 따라서 게임에서는 이 성질을 이용해서 공중에 둥둥 떠있는 아이템의 부드러운 애니메이션을 만들어 내게 된다. 또한, RPG 게임에서 캐릭터 주변을 도는 방어막, 3D 게임에서 주인공을 중심으로 회전하는 위성 카메라 시스템은 모두 삼각함수를 이용해서 만들 수 있다.
다만 조금 달라진 점은, 이제 삼각함수와 원운동(Circular Motion)의 관계를 이해해야 한다는 것이다. 지금까지는 단순히 X축, Y축으로 얼마나 이동할 지 계산했다면, 원운동의 경우 '기준점으로부터의 거리와 각도'를 구하는 것이 핵심이 된다.
X 좌표 : X = 기준점(x) + (반지름 * cos(theta))
Y 좌표 : Y = 기준점(y) + (반지름 * sin(theta))
이 기본 공식에 델타 타임을 활용해서 매 프레임마다 각도를 조금씩 더해주기만 하면 오브젝트가 기준점을 중심으로 부드럽게 회전하는 원운동을 구현할 수 있게 된다.
(1) 삼각함수로 플레이어를 따라다니는 실드 구현하기
#include <iostream>
#include <cmath>
struct Vector2 {
float x, y;
Vector2(float _x = 0.0f, float _y = 0.0f) : x(_x), y(_y) {}
};
class OrbitingShield {
public:
Vector2 position; // 실드의 현재 위치
float radius; // 플레이어로부터 떨어진 거리 (반지름)
float currentAngle; // 현재 회전 각도 (라디안)
float rotationSpeed; // 회전 속도 (초당 몇 라디안을 돌 것인가)
OrbitingShield(float dist, float speed)
: position(0.0f, 0.0f), radius(dist), currentAngle(0.0f), rotationSpeed(speed) {}
// 매 프레임마다 플레이어의 위치를 기준으로 실드 위치를 업데이트
void Update(Vector2 playerPosition, float deltaTime) {
// 1. 시간에 따라 각도를 증가시킴 (프레임 독립성 유지)
currentAngle += rotationSpeed * deltaTime;
// 각도가 너무 커지는 것을 방지 (2*PI = 360도)
if (currentAngle > 2.0f * 3.14159265f) {
currentAngle -= 2.0f * 3.14159265f;
}
// 2. 삼각함수를 이용해 X, Y 오프셋 계산
float offsetX = radius * std::cos(currentAngle);
float offsetY = radius * std::sin(currentAngle);
// 3. 플레이어의 위치에 오프셋을 더해 최종 위치 결정
position.x = playerPosition.x + offsetX;
position.y = playerPosition.y + offsetY;
}
};
int main() {
// 반경 5.0, 회전 속도 3.14 (1초에 반 바퀴, 즉 180도 회전)인 실드 생성
OrbitingShield shield(5.0f, 3.14159265f);
// 플레이어가 월드 중앙 (10.0, 10.0)에 가만히 서 있다고 가정
Vector2 playerPos(10.0f, 10.0f);
float dt = 0.5f; // 0.5초씩 시뮬레이션
std::cout << "--- 방어막 궤도 회전 시뮬레이션 ---\n";
for (int frame = 1; frame <= 5; ++frame) {
shield.Update(playerPos, dt);
std::cout << "[시간: " << frame * dt << "초 경과]\n";
std::cout << "실드 위치: (" << shield.position.x << ", " << shield.position.y << ")\n\n";
}
return 0;
}
플레이어가 이동하더라도, 실드가 플레이어의 위치, 즉 playerPosition을 매 프레임마다 기준으로 삼기 때문에 플레이어를 따라다니며 원을 그리게 만든다.
(2) 사인 에징(Sine Easing)
유저 인터페이스(UI) 창이 팝업되거나, 컷신에서 카메라가 이동할 때의 부드러운 움직임을 구현하기 위해서도 삼각함수가 사용된다. sin, cos 함수의 부드러운 곡선 형태를 활용해서 시작과 끝이 둥글게 처리되어 조작감을 개선할 수 있는 것이다. 이를 사인 에징이라고 한다.
Ease-In : 서서히 가속 : cos 곡선의 꼬리 부분을 활용해서 처음엔 느리다가 확 빨라지게 만든다.
Ease-Out : 서서히 감속 : sin 곡선의 머리 부분을 활용해서 빠르게 날아오다가 목표점 근처에서 천천히 멈추게 만든다.
Ease-In-Out : 둘 다 적용하기 : 출발과 도착을 모두 부드럽게 만들어주는 화면 전환기법이다. [ f(x) = - (cos(pi * x) - 1) / 2 ]
(3) 카메라 셰이크와 헤드 보빙(Head Bobbing)
FPS 게임에서 캐릭터가 걸어갈 때 화면이 상하좌우로 흔들리게 하는 연출(헤드 보빙)이나 폭발이 일어났을 때 화면이 거칠게 흔들리는 연출(카메라 셰이크)도 삼각함수를 이용해서 구현한다. 주파수와 진폭이 다른 여러개의 sin 파동을 겹쳐서 더하면 완전히 불규칙해보이면서도 끊기지 않는 자연스러운 흔들림을 만들어 낼 수 있는 원리이다.
(4) 카메라의 시야각(FOV) 조절
FOV를 옵션에서 조절하면, 화면에 오브젝트가 얼마나 크게 혹은 작게 보여야 하는지를 결정해야 한다. 이 때 3D 투영 원근법의 핵심 수식에 탄젠트가 사용된다. 탄젠트는 '기울기'이고, 비율을 의미하므로 모니터 평면과 카메라의 눈이 이루는 직각삼각형을 기준으로 원근감을 계산하는 데 활용된다.
(5) 역운동학(IK, Inverse Kinematic)
캐릭터가 울퉁불퉁한 계단을 밟을 때, 발바닥이 허공에 뜨거나 땅에 파묻히지 않게 하기 위해서 목표 지점의 좌표를 먼저 설정하고 무릎과 골반 관절이 각각 몇 도 꺾여야 하는지를 수학적으로 역추산 해야 한다. 이 때 무수히 많은 아크탄젠트 연산이 들어가게 된다.
3) 실무에 적용되는 삼각함수 개념 더 알아보기
(1) 멀티플레이어 네트워크 데드 레코닝(Dead Reckoning)
서버에서 플레이어의 위치를 보내줄 때, 클라이언트가 그 사이의 빈 공간을 수학적으로 예측하여 부드럽게 이동시키는 기법.
수학적 원리: 등가속도 운동 공식
void AMyNetworkCharacter::UpdateDeadReckoning(float DeltaTime)
{
// 서버에서 마지막으로 받은 위치, 속도, 가속도
FVector LastServerPos = GetReplicatedPosition();
FVector ServerVelocity = GetReplicatedVelocity();
FVector ServerAcceleration = GetReplicatedAcceleration();
// 시간 경과에 따른 예측 위치 계산 (물리 기반 예측)
float TimeSinceLastUpdate = GetWorld()->GetTimeSeconds() - LastUpdateTime;
FVector PredictedPos = LastServerPos
+ (ServerVelocity * TimeSinceLastUpdate)
+ (0.5f * ServerAcceleration * FMath::Square(TimeSinceLastUpdate));
// 현재 위치에서 예측 위치로 부드럽게 보간 (지수 감쇠 방식)
float InterpSpeed = 10.0f;
FVector NewPos = FMath::VInterpTo(GetActorLocation(), PredictedPos, DeltaTime, InterpSpeed);
SetActorLocation(NewPos);
}
(2) 사원수(Quaternion) 구면 선형 보간(Slerp)
3D 회전을 다룰 때 오일러 각도(pitch, yaw, roll)를 단순히 선형 보간하면 회전이 일그러지거나 짐벌락이 발생한다. 이를 막기 위해서 삼각함수를 내부적으로 사용하는 사원수 구면 보간을 사용한다.
수학적 원리: 두 사원수 사이의 각도 theta를 구한 뒤, sin 함수의 비율에 따라 회전값을 보간한다.
void AMyCameraActor::SmoothRotateToTarget(FVector TargetLocation, float DeltaTime)
{
// 타겟을 바라보는 방향 벡터
FVector Direction = (TargetLocation - GetActorLocation()).GetSafeNormal();
// 방향 벡터를 회전값(Rotator)으로 변환 후, 사원수(Quat)로 변환
FQuat TargetQuat = direction.Rotation().Quaternion();
FQuat CurrentQuat = GetActorRotation().Quaternion();
// Slerp를 이용해 현재 회전에서 목표 회전으로 구면 선형 보간
// FMath::QInterpTo 내부에서 Slerp와 삼각함수 연산이 안전하게 처리됨
FQuat NewQuat = FMath::QInterpTo(CurrentQuat, TargetQuat, DeltaTime, 5.0f);
SetActorRotation(NewQuat);
}
(3) 포물선 탄도학 조준
곡사포나 수류탄을 던질 때, 타겟의 위치와 발사 속도를 알고 있다면 정확히 몇 도 쏴야 하는지 발사 각도를 역산해 낼 수 있다.
수학적 원리: 역삼각함수(arctan)와 물리 공식을 결합하여 발사 각도 theta를 도출한다.
bool AMyArtillery::CalculateLaunchVelocity(FVector StartPos, FVector TargetPos, float InitialSpeed, FVector& OutVelocity)
{
float Gravity = FMath::Abs(GetWorld()->GetGravityZ());
// 2D 평면 거리(X)와 높이 차이(Y) 계산
FVector Direction = TargetPos - StartPos;
float DistanceY = Direction.Z;
Direction.Z = 0.0f;
float DistanceX = Direction.Size();
// 포물선 방정식 판별식 계산
float SpeedSq = InitialSpeed * InitialSpeed;
float SpeedQuad = SpeedSq * SpeedSq;
float RootVal = SpeedQuad - Gravity * (Gravity * DistanceX * DistanceX + 2.0f * DistanceY * SpeedSq);
if (RootVal < 0.0f) return false; // 타겟이 사거리 밖임
// 낮은 궤적(직사) 기준의 발사 각도(Radian) 도출 (아크탄젠트 활용)
float AngleRad = FMath::Atan((SpeedSq - FMath::Sqrt(RootVal)) / (Gravity * DistanceX));
// 구해진 각도를 바탕으로 최종 발사 3D 벡터 생성
Direction.Normalize();
OutVelocity = Direction * (InitialSpeed * FMath::Cos(AngleRad)); // 평면 속도
OutVelocity.Z = InitialSpeed * FMath::Sin(AngleRad); // 수직 속도
return true;
}
다만, 언리얼에서는 UGameplayStatics::SuggestProjectileVelocity_CustomArc 등을 제공하여 포물선과 탄도학 조준을 위한 최적화한 기능을 제공한다.
(4) 탄막 슈팅 패턴
여러 갈래, 각도로 투사체를 나선형으로 발사할 때 삼각함수의 sin과 cos을 이용해서 각 투사체의 진행 벡터를 생성한다.
수학적 원리: 원의 방정식 좌표 변환 (X = cos, Y = sin)
void AMyBossMonster::FireSpiralPattern(int32 BulletCount, float BaseAngleDegree)
{
FVector SpawnLocation = GetActorLocation();
float AngleStep = 360.0f / BulletCount; // 총알 간의 간격
for (int32 i = 0; i < BulletCount; ++i)
{
// 도(Degree)를 라디안(Radian)으로 변환
float CurrentAngleRad = FMath::DegreesToRadians(BaseAngleDegree + (AngleStep * i));
// 삼각함수를 이용해 발사 방향 벡터 도출
FVector FireDirection;
FireDirection.X = FMath::Cos(CurrentAngleRad);
FireDirection.Y = FMath::Sin(CurrentAngleRad);
FireDirection.Z = 0.0f; // 2D 평면 기준
// 투사체 스폰 및 방향 설정 (SpawnActor 로직 생략)
SpawnBullet(SpawnLocation, FireDirection);
}
}
(5) 파도와 펄럭이는 깃발
물결을 시각적으로 렌더링 하는 것은 주로 셰이더에서 처리하지만, 배가 물결 위에서 출렁이게 하려면 C++에서 배가 위치한 좌표의 파도 높이(Z값)을 삼각함수로 계산해야 한다.
수학적 원리: 거스트너 파도(Gurstner Wave, 실제 바다의 표면과 물방울의 궤적을 모방하여 시각적으로 매우 사실적인 파도를 생성하는 3차원 컴퓨터 그래픽 및 게임 개발 알고리) 또는 사인 파동(가장 기초적이고 규칙적인 주기 파동)

float AMyWaterManager::GetWaveHeightAtPosition(FVector WorldPos, float TimeSeconds)
{
// 파도의 진폭, 파장, 속도 설정
float Amplitude = 50.0f;
float WaveLength = 1000.0f;
float Speed = 2.0f;
// 파도의 주파수(K)와 위상(Phase) 계산
float K = (2.0f * PI) / WaveLength;
float Phase = TimeSeconds * Speed;
// X, Y 좌표를 결합하여 사인 함수를 통과시킴
// 실제 실무에서는 여러 개의 사인파를 합성(Sum)하여 불규칙한 파도를 만듦
float WaveZ = Amplitude * FMath::Sin(K * (WorldPos.X + WorldPos.Y) + Phase);
return GetActorLocation().Z + WaveZ; // 월드 기준 수면 높이 반환
}
(6) 헥사곤과 아이소메트릭 타일
사용자의 마우스 2D 화면 좌표를 3차원처럼 보이는 비스듬한 타일(30도, 60도와 같이) 좌표로 정확하게 변환할 때 삼각함수 비율이 고정된 상수로 행렬 연산에 포함시킨다.
수학적 원리 : 화면 좌표를 cos, sin을 이용해서 역변환
FIntPoint AMyGridManager::ScreenToIsometricGrid(FVector2D ScreenPos)
{
float TileWidthHalf = 64.0f;
float TileHeightHalf = 32.0f;
// 화면 좌표를 아이소메트릭 수학 공식을 이용해 역산
// (기울어진 각도의 빗변 비율을 풀어낸 공식)
float GridX = (ScreenPos.X / TileWidthHalf + ScreenPos.Y / TileHeightHalf) / 2.0f;
float GridY = (ScreenPos.Y / TileHeightHalf - ScreenPos.X / TileWidthHalf) / 2.0f;
// 소수점을 버리고 정확한 타일의 인덱스 반환
return FIntPoint(FMath::FloorToInt(GridX), FMath::FloorToInt(GridY));
}
4) 삼각함수를 사용하는 핵심 키워드
(1) [회전, 지향] 각도와 좌표를 서로 변환해야 할 때
>> 특정 각도 방향으로 나아가는 힘이나 위치를 구할 때(sin, cos), 목표물의 위치를 향해 몇 도 회전해야 하는지 구해야 할 때(atan2)
(2) [주기성, 진동] 끊임없이 부드럽게 왕복하거나 반복해야 할 때
>> 델타 타임을 적용한 값을 무한히 계속 더해서 sin(시간)안에 집어 넣으면 부드러운 무한 반복 연출을 만들 수 있음.
(3) [투영, 기울기] 공간의 비율이나 원근감을 다룰 때
>> 기울기를 구하거나 조작해야 할 때, 투영 행렬(Projection Matrix. FOV 정할 때 사용)을 건드리거나 빗면(Normal Vector. 경사진 바닥의 기울기를 구할 때 사용)의 법선 벡터를 구할 때.
*Unreal C++*
1. 게임 프레임워크 : 게임 모드와 상태 관리, C++ 로 게임모드 만들기
2. 액터 : 캐릭터 클래스, 컴포넌트, C++ 로 카메라 컴포넌트와 스프링 암 컴포넌트 부착하기.
3. 입력 제어 : Enhanced Input System, C++로 IA, IMC 설정 만들기
4. 애니메이션 : ABP, 블렌드 스페이스, 컨트롤 리그, IK 노 등 직접 이벤트 그래프 / 애니메이션 그래프 그리고 애니메이션 적용하기
*오늘의 총평*
몸이 좀 안좋아서 진척이 잘 안됐지만 C++ 언리얼 게임 만들기 강의를 수강하며 따라하다보니 많이 배운 것 같다. 이게 아직까지는 배우는 내용들이 좀 공중에 떠있는 느낌인데 계속 반복해서 접하고 실제로 조용히 따라서 만들다보니까 슬슬 먼 나라 이야기가 아니게 되는 느낌을 받고 있다.
'TIL' 카테고리의 다른 글
| 06.09 TIL (0) | 2026.06.09 |
|---|---|
| 06.08 TIL (0) | 2026.06.08 |
| 06.04 TIL (Ch.3 개인 과제 발제) (0) | 2026.06.04 |
| 06.02 TIL (커리어 데이) (0) | 2026.06.02 |
| 06.01 TIL (이력서 세션 및 커리어 데이) (0) | 2026.06.01 |


