I. Blueprint - FPS 슈팅 게임 개발 세션 (3회차)
개요
오늘은 FPS 캐릭터의 핵심 기능을 구현했다. 조준(Aim) 기능을 추가하고, 애니메이션 블루프린트를 이용해 이동과 점프 애니메이션을 상태 머신으로 관리했다. 또한 Line Trace를 활용한 히트스캔(Hit Scan) 방식의 사격 시스템을 구현하고, 머즐 플래시, 반동, 사운드, 피격 이펙트 등을 함수로 정리하여 하나의 사격 기능으로 구성하였다.
구현 과정
1) 타임라인을 이용한 조준(Aim) 기능 구현
FPS 게임에서는 조준 시 카메라의 FOV(Field of View)를 줄여 화면을 확대하는 효과를 자주 사용한다. 그런데, FOV를 즉시 변경하면 화면이 갑자기 확대되므로 자연스럽지 않아서 Timeline과 Lerp(Float)를 이용하여 일정 시간 동안 부드럽게 값을 변경하였다.
① 타임라인 생성

Length: 0.25초
Float Track : (0.0초, 0.0), (0.25초, 1.0)
② 플로트 타입 변수 생성
DefaultFOV: 90.0
AimFOV: 50.0
③ Lerp 플로트와 Set Field of View 사용

이전 회차에 만든 Aim기능에 살을 붙였다.


2) 애니메이션 블루프린트 생성
① 캐릭터 BP에 ABP 적용
ABP를 생성한 뒤 Character Mesh의 Animation Mode를 'Use Animation Blueprint'로 변경하였다. 그 후, 애님 클래스를 생성한 ABP로 선택했다.

② State Machine 생성
ABP의 AnimGraph에서 스테이트 머신을 생성하고, 스테이트 머신 내부에 Idle과 Run 스테이트를 만들었다. 그 후, 해당 스테이트에 각각 알맞는 애니메이션 시퀀스를 연결했다.

트랜지션 조건을 만들기 위해서 플로트타입 변수 Speed를 생성했다.
그리고, 트랜지션 조건을 각각
Idle -> Run: Speed > 0
Run -> Idle: Speed <= 0
으로 설정했다.
③ Speed 계산
Speed 값을 계산하기 위해서 Vector Length XY를 사용했다. 점프를 하거나 떨어지는 경우 z값이 변하기 때문에, 정확한 속도 값 계산을 위해서 z축은 Speed 값 계산에 사용하지 않았다.

④ 캐릭터가 공중에 있는 지 여부 검사
캐릭터가 공중에 있는 지 여부를 검사 하기 위해 부울 타입의 IsInAir 변수를 생성했다. 그리고, movement component의 is falling을 사용해 IsInAir를 갱신하도록 했다. 마지막으로, Speed 계산과 IsInAir 여부 검사를 전부 하기 위해서 Flow control의 Sequence를 사용했다.

⑤ State Machine 개선
깔끔한 트랜지션 정리를 위해 Idle/Run 스테이트 에일리어스를 생성했다. Idle과 Run 스테이트를 포함한다. 그리고 Jump, Falling, Land 스테이트를 생성하고, 각각 알맞는 애니메이션 시퀀스를 연결했다.

트랜지션 조건을 각각
Idle/Run -> Jump: IsInAir
Idle/Run -> Falling: IsInAir
Jump -> Falling: 자동 규칙
Falling -> Land: IsInAir에 Not boolean 연결
Land -> Idle: 자동 규칙
으로 설정했다.
또한, 자연스러운 애니메이션 생성을 위해 Idle,Run, Falling 애니메이션 시퀀스는 애니메이션 루프를 체크했다.
3) Line Trace를 이용한 히트스캔(Hit Scan) 사격 구현
FPS 게임에서는 실제 총알을 생성하는 대신 Line Trace를 이용하여 맞았는지를 즉시 판정하는 히트스캔 방식을 많이 사용한다.
① 사격 기능에 쓸 IA 생성 및 IMC 매핑
IA_Fire 생성 -> 값 타입 Digital (bool) 설정
IMC에 IA_Fire 매핑 추가 및 왼쪽 마우스 버튼 키 값 설정
② Line Trace 설정
Camera Location을 Start로 정하고, 시작위치에서 forward 방향으로 6000.0 떨어진 곳을 End로 정했다.

③ 피격 이펙트 생성
피격 이펙트는 Break Hit Result의 Impact Point를 이용하여 충돌 위치에 생성하였다.

4) Fire 함수 생성
기능이 많아질수록 Event Graph가 복잡해지므로, 사격 기능을 라인 트레이스를 포함하여 함수로 분리했다.
Fire 함수는 Muzzle Flash 생성 -> 반동 생성 -> 발사 사운드 재생 -> Line Trace -> 피격 이펙트 생성 순서로 실행되도록 구현했다.

① Muzzle Flash 생성
Fab에서 무료로 제공 중인 Muzzle Flash를 프로젝트에 추가하여 적용했다.
이전 회차에서 생성한 Muzzle Point에서 flash가 생성되도록 Spawn System Attached를 사용했다.

② 반동 생성 및 발사 사운드 재생
Random Float in Range를 pitch와 yaw에 적용하여 사격할 때마다 조금씩 시점이 흔들리도록 구현했다.

5) Niagara Muzzle Flash 반복 문제 해결
Spawn System Attached는 Niagara System을 한 번 생성할 뿐이며, 실제 반복 여부는 Niagara 내부 Emitter의 설정에 따라 결정된다.
① Loop Behavior 설정
Muzzle Flash가 계속 반복 생성되는 문제가 발생했다. Niagara System 내부의 모든 Emitter의 Life Cycle 속 Loop Behavior가 Infinite로 설정되어 있었다. 이를 전부 Once로 변경해주었다.

6) 자동 연사 기능 구현
입력 자체를 반복하는 것이 아니라, 현재 발사 중인지 여부를 변수로 관리하여 일정 시간마다 Fire 함수를 반복 호출하는 방식으로 구현했다. FireInterval 변수만 변경하면 무기마다 다른 연사 속도를 쉽게 설정할 수 있는 구조가 되었다.

① 자동 연사
Started 이벤트에서 IsFiring = true로 설정하고, Fire 함수를 실행하였다. 그 후, Fire 함수가 종료되면, Delay(Fire Interval = 0.15)을 거친 뒤 Branch로 IsFiring을 다시 확인했다. 이때, true라면 다시 Fire를 호출하여 반복 발사가 이루어지도록 구현했다.
② 발사 종료
좌클릭을 멈추면, Completed 이벤트에서 IsFiring = false로 설정하여 반복이 종료되도록 구현했다.


후기
오늘은 FPS 게임에서 가장 중요한 기능들을 하나씩 구현해 보았다. 단순히 입력을 받아 기능을 실행하는 수준이 아니라, Timeline을 이용한 자연스러운 조준 전환, Animation Blueprint의 State Machine을 활용한 상태 관리, Line Trace를 이용한 히트스캔 사격 시스템까지 구현하면서 FPS 게임의 기본 구조를 이해할 수 있었다.
특히, 이번에는 단순히 기능을 추가하는 데서 끝나지 않고, Fire 함수를 만들어 사격 기능을 하나의 단위로 관리하고, IsFiring 변수를 이용해 입력과 실제 발사 로직을 분리한 점이 인상 깊었다. 또한 Niagara의 Loop Behavior 설정 때문에 Muzzle Flash가 반복 재생되는 문제를 직접 원인을 찾아 해결하면서, 시스템을 사용하는 것뿐만 아니라 내부 설정이 결과에 어떤 영향을 주는지도 경험할 수 있었다.
II. C언어 세션 (3회차)
개요
오늘은 지난 시간에 배운 C언어 기초 문법을 전체적으로 복습한 뒤, 여러 개의 데이터를 효율적으로 관리하기 위한 배열(Array)에 대해 학습하였다. 특히 초보자가 자주 실수하는 문법과 배열 사용 시 주의해야 할 사항들을 함께 정리하면서 기초를 다지는 시간을 가졌다.
학습 내용
1) C언어 기초 문법 복습
① 변수와 자료형
변수는 데이터를 저장하는 메모리 공간이며, 자료형은 해당 메모리에 어떤 형태의 데이터를 저장할 것인지를 결정한다.
int score = 90; // 정수
double pi = 3.14159; // 실수 (소수점)
char grade = 'A'; // 문자 하나 (작은따옴표)
unsigned 자료형은 음수를 표현하지 않는 대신 양수의 표현 범위를 넓힌다.
예) int의 범위는 -2^31 ~ 2^31 - 1이고, unsigned int의 범위는 0~2^32 - 1이다.
변수 이름 규칙
영문자, 숫자, _만 사용가능
숫자로 이름이 시작할 수 없
대소문자를 구분함
int, return과 같은 예약어는 사용 불가
상수(Constant)는 프로그램 실행 중 값이 변경되지 않는 데이터
두 가지 방법으로 선언 가능
const double PI = 3.14159;
#define MAX 100
② 입출력 함수
printf()는 서식 지정자(%d, %f, %c, %s 등)를 이용하여 다양한 자료형의 데이터를 출력할 수 있다.
int Hp = 90;
double grade = 4.0;
printf("Hp = %d, grade = %f\n", Hp, grade);
scanf()는 사용자에게 입력을 받아 변수에 저장하는 함수이다. 특히 scanf()는 변수 자체가 아니라 변수의 메모리 주소를 전달해야 하므로 대부분의 경우 & 연산자를 함께 사용해야 한다.
int num;
printf("숫자를 입력하세요.\n");
scanf("%d", &num);
printf("숫자: %d\n", num);
③ 연산자
산술 연산자(+, -, *, /, %), 논리 연산자(&&, ||, !), 비교 연산자(==, >, <, >=, <=, !=), 복합 대입 연산자(+=, -=, *=, /=), 증감 연산자(x++, x--) 등이 있다.
연산자 우선 순위
int a = 1 + 4 * 9; // 곱셈 먼저 계산
int b = (2 + 6) * 7; // 괄호 먼저 계산
- 정수끼리 나누면 소수점 이하가 버려진다.
- 실수 결과가 필요하면 형변환(Casting)을 해야 한다.
- 전위(++i)와 후위(i++) 증감 연산자는 실행 시점이 다르다.
- 복합 대입 연산자를 사용하면 코드를 간결하게 작성할 수 있다.
④ 조건문
if, else, else if
조건식의 결과가 참(True)일 때만 해당 블록이 실행되며, 여러 조건을 검사할 때는 else if를 이용하여 순차적으로 비교할 수 있다.
int score = 92;
if (score >= 90) {
printf("A\n");
} else if (score >= 80) {
printf("B\n");
} else {
printf("C 이하\n");
}
switch - case
int item = 2;
switch (item) {
case 1:
printf("한손검\n");
break; // break를 빼면 아래로 계속 흘러내림(fall-through)
case 2:
printf("양손검\n");
break;
default:
printf("선택 없음\n");
}
⑤ 반복문
for문은 반복 횟수가 정해져 있을 때 자주 사용한다.
for (int i = 0; i < 10; i++) {
printf("%d ", i);
}
while문은 조건이 참인 동안 반복한다.
int i = 0;
while (i < 10) {
printf("%d ", i);
i++;
}
do while문은 조건과 관계없이 최소 한 번은 실행된다.
int n;
do {
printf("짝수 입력: ");
scanf("%d", &n);
} while (n % 2 == 0); // 짝수가 들어올 때까지 반복
break와 continue
for (int i = 0; i < 10; i++) {
if (i == 5) break; // 반복문을 즉시 종료
if (i % 2 == 0) continue; // 현재 반복만 건너뛰고 다음 반복을 수행
printf("%d ", i);
}
중첩 반복문은 2차원 형태의 데이터를 처리할 수 있다.
for (int i = 1; i <= 9; i++){
for (int j = 1; j <= 9; j++) {
printf("%d * %d = %d\n", i, j, i * j);
}
}
⑥ 주의사항
| unsigned 음수 변환 | unsigned int i = -1; 에러 아님, 큰 양수로 해석됨 | |
| scanf의 & 누락 | scanf("%d", &age); 값 받을 주소를 넘겨야 함 | |
| 정수 나눗셈 | 7/2 == 3 실수 원하면 캐스팅 | |
| 연쇄 비교 | 5 < x < 10은 항상 참이 나옴 | |
| =과 == 차이 | =은 대입, ==이 비교 연산 | |
| if에서 = 사용 | if (x = 5) 대입이라 항상 실행됨 | |
| switch의 break 누락 | 다음 case로 흘러내림 | |
| 무한 루프 | i++, i-- 등 증감 누락 주의 | |
| unsigned로 거꾸로 세기 | i >= 0이 항상 참 → 무한 루프 | |
2) 배열 (Array)
① 정의
같은 자료형의 여러 데이터를 하나의 이름으로 연속된 메모리에 저장하는 자료구조
예를 들어 학생 30명의 점수를 각각 변수로 선언하면
score1
score2
score3
...
score30
처럼 관리가 매우 불편하다.
배열을 사용하면
score[30]
하나만으로 동일한 데이터를 효율적으로 관리할 수 있다.
② 배열 선언 및 초기화
배열은 다음과 같이 선언한다.
int numbers[5];
이는 정수형 변수 5개를 연속된 메모리에 생성하는 의미이다.
배열은 선언과 동시에 초기화할 수 있다.
int numbers[5] = {10, 20, 30, 40, 50};
또는 크기를 생략할 수도 있다.
int numbers[] = {10, 20, 30, 40, 50};
컴파일러가 원소 개수를 자동으로 계산한다.
그 외의 초기화 방법
int b[3] = {1}; // {1, 0, 0} — 나머지는 0으로 채워짐
int d[3] = {0}; // 전체를 0으로 초기화하는 관용구 {0, 0, 0}
③ 배열의 크기
배열 전체 크기는 sizeof를 이용하여 확인할 수 있다.
sizeof(array)
원소 하나의 크기로 나누면 배열의 원소 개수를 구할 수 있다.
sizeof(array) / sizeof(array[0])
이 방법은 반복문에서 배열 전체를 순회할 때 자주 사용된다.
④ 2차원 배열
배열 안에 배열을 저장하는 형태로 2차원 배열도 만들 수 있다.
예를 들어
int map[5][5];
는 5×5 형태의 데이터를 저장할 수 있다.
게임에서는
- 타일맵
- 체스판
- 미로
등을 표현할 때 자주 사용된다.
⑤ 주의사항
배열의 인덱스는 항상 0부터 시작한다.
int numbers[5];
이라면
사용 가능한 인덱스는
0
1
2
3
4
이다.
범위를 넘어
numbers[5]
처럼 접근하면 프로그램이 비정상적으로 동작할 수 있으므로 항상 범위를 확인해야 한다.
후기
오늘은 새로운 문법을 배우기보다는 지금까지 학습한 내용을 다시 정리하면서 자주 실수하는 부분들을 점검할 수 있었다. 특히 unsigned, scanf()의 주소 전달, 정수 나눗셈, =와 ==의 차이처럼 사소해 보이지만 실제 코드를 작성할 때 자주 발생하는 실수들을 다시 확인할 수 있었다.
배열은 여러 개의 변수를 하나의 이름으로 관리할 수 있다는 점이 가장 좋았다. 특히 게임 개발에서는 플레이어 정보, 인벤토리, 타일맵 등 동일한 형태의 데이터를 많이 다루기 때문에 앞으로 배열을 자주 활용하게 될 것이다.
'언리얼 엔진 > TIL' 카테고리의 다른 글
| TIL - 본캠프 5일차 (1) | 2026.07.02 |
|---|---|
| TIL - 본캠프 4일차 (0) | 2026.07.01 |
| TIL - 본캠프 2일차 (0) | 2026.06.29 |
| TIL - 본캠프 1일차 (0) | 2026.06.26 |
| TIL - 베이스캠프 12일차 (0) | 2026.06.24 |