I. Blueprint - FPS 슈팅 게임 개발 세션 (5회차)
개요
오늘은 FPS 프로젝트에 게임 플레이 요소를 더욱 풍부하게 만들기 위해 회복 아이템과 탄약 아이템을 추가하고, 게임 진행 상태를 관리하는 GameMode와 MainHUD를 구현하였다. 또한 제한 시간과 남은 적 수를 화면에 표시하고, 승리와 패배 조건을 설정하여 게임의 시작부터 종료까지 하나의 흐름으로 완성하였다. 마지막으로 게임 종료 시 플레이어와 적의 동작을 모두 멈추도록 개선하여 보다 자연스러운 게임 종료 화면을 구현하였다.
구현 과정
1) 회복 아이템(Healing Potion) 구현

① Fab에서 빨간색 힐링 포션 메시를 프로젝트에 임포트하였다.
② Actor를 상속받는 포션 블루프린트를 생성하였다.
③ Static Mesh를 추가하여 포션 메시를 적용하였다.
④ Sphere Collision을 추가하고 충돌 범위와 메시 위치를 조정하였다.

⑤ BP_Character에 AddHealth 함수를 생성하였다.

- Heal Amount를 현재 체력에 더한다.
- Min 노드를 이용하여 최대 체력을 넘지 않도록 제한한다.
- Health를 갱신한다.
- Health / MaxHealth 값을 이용하여 HP Progress Bar를 갱신한다.
⑥ 포션 블루프린트에서는 BeginOverlap 발생 시

- 플레이어를 캐스팅한다.
- AddHealth를 호출한다.
- 효과음을 재생한다.
- 포션을 제거한다.
⑦ 결과


2) 탄약 아이템 구현

① Fab에서 가져온 총알 메시를 사용하여 힐링포션과 비슷하게 탄약 아이템을 제작하였다.
② BP_Character에 AddAmmo 함수를 생성하였다.

- 탄약을 증가시킨 뒤 HUD의 탄약 개수를 즉시 갱신하도록 구현하였다.
③ 탄약 아이템 블루프린트에서는

- 플레이어를 캐스팅한다.
- AddAmmo를 호출한다.
- 효과음을 재생한다.
- 총알 아이템을 제거한다.
④ 결과


3) 전투 시스템 구현
① 개미 BP에 체력을 저장하는 Health 변수를 생성하였다.

② 플레이어의 Fire 함수에서 Line Trace가 적을 맞추면 Apply Damage 노드를 호출하도록 구현하였다.

③ 개미 BP에서는 Event AnyDamage를 이용하여 전달받은 데미지만큼 체력을 감소시켰다.

④ Health가 0 이하가 되면 적이 제거(Destroy)되도록 구현하였다.

Beginoverlap(ExplosionSphere) 이벤트 뒤에서 적 제거와 관련된 로직을 하나의 Destroy 함수로 분리하여 재사용할 수 있도록 구성하였다.
4) 개미 Animation Blueprint 구현
① 개미의 Animation Blueprint를 생성하였다.
② 개미 Skeletal Mesh의 Anim Class에 생성한 Animation Blueprint를 적용하였다.
③ Blend Space를 새롭게 생성하여 이동 속도에 따라 애니메이션이 자연스럽게 변경되도록 구성하였다.

Idle, Walk, Run 세 가지 애니메이션 시퀀스를 배치하였다.
④ Blend Space의 가로 축 이름을 Speed로 설정하고 최대값은 600으로 지정하고, Grid Snap을 이용하여 각 애니메이션이 적절한 속도 구간에서 재생되도록 배치하였다.
⑤ Anim Graph에서 BS_Ant를 Output Pose에 연결하였다.

⑥ Event Graph에서는 기존 플레이어 Animation Blueprint와 동일한 방식으로 현재 이동 속도를 계산하였다.

⑦ 계산한 Speed 변수를 Blend Space의 Speed 입력값에 연결하였다.
5) 개미 AI 랜덤 순찰 구현
① 개미가 일정한 위치만 이동하지 않고 맵을 자유롭게 순찰하도록 MoveRandom Custom Event를 구현하였다.

② AI Move To 노드를 사용하여 목적지를 설정하고, Destination에는 Get Random Reachable Point in Radius를 사용하였다.
③ 이동이 끝난 후에는 Delay 노드를 사용하여 0~3초 사이의 랜덤한 시간만큼 대기하도록 구성하였다.
④ 대기가 끝나면 MoveRandom을 다시 호출하여 랜덤 이동을 반복하도록 구현하였다.
⑤ MoveRandom과 StartChase의 AIMoveTo가 겹치지 않도록, Bool타입의 IsChasing 변수를 생성하여 Branch로 관리한다.
-> 추격하지 않을 때만 랜덤으로 이동한다.
6) 적 이동 속도 변경
① 평소 이동 속도와 플레이어를 추적할 때의 속도를 float형 변수로 관리한다.

② 개미가 평소 이동할 때는 Max Walk Speed를 300으로 설정하였다.
Bp_Ant의 MoveRandom 커스텀 이벤트 내부에 Set MaxWalkSpeed를 추가하고, BaseSpeed(float,300.0)을 연결했다.

③ 플레이어를 추격할 때는 Max Walk Speed를 600으로 변경하였다.
StartChase 커스텀 이벤트 내부에 Set MaxWalkSpeed를 추가하고, ChaseSpeed(float,600.0)을 연결했다.

④ AIMoveTo 비동기 오류 해결
AIMoveTo는 비동기로 작동한다. 기존의 StartChase는 적을 공격했을 때와 적의 탐지 범위에 overlap 됐을 때 작동했다. 만약, 개미를 공격하고 Chase 중일 때, 탐지 범위에 overlap 되면 AIMoveTo가 실패로 간주되어 MoveRandom이 작동되어 늘어난 개미의 이동 속도가 다시 300으로 초기화 되는 버그가 일어난다.


그렇기에, Chase와 Update Health 부분에 IsChasing이 false일 때만 StartChase 이벤트가 호출되게 변경하여 AIMoveTo 비동기 오류를 해결했다.
7) MainHUD 제작
① 게임 진행을 표시할 MainHUD를 생성하였다.

② Canvas Panel 위에 남은 시간, 남은 적 수, Result, 재시작 버튼, 종료 버튼을 배치하였다.
③ Result와 버튼들은 처음에는 보이지 않도록 Visibility를 Collapsed로 설정하였다.

④ GameMode BeginPlay에서

Create Widget, MainHUD 변수 저장, Add To Viewport 순으로 HUD를 생성하였다.
⑤ World Settings에서 새 GameMode를 적용하고 Default Pawn을 BP_Character로 변경하였다.

8) 제한 시간 시스템 구현
① GameMode에 float 타입 TimeRemaining 변수를 생성하였다.
② UpdateRemainingTime Custom Event를 생성하였다.

③ Event Tick의 Delta Seconds를 전달하여 시간을 감소시켰다.

④ HUD의 Time Text를 계속 갱신하도록 연결하였다.

⑤ Format Text를 이용하여 시간을 분 : 초 형식으로 출력하였다.
- 분 = RemainingTime / 60
- 초 = RemainingTime % 60
각각 Floor To Integer를 사용하였다.
⑥ 결과


9) 시간 초과 시 게임 패배 구현
① RemainingTime이 0 이하인지 Branch로 검사하였다.
② 시간이 모두 소진되면 시간을 0으로 고정하고, Result를 "게임 패배"로 변경한다.

③ Result와 버튼들의 Visibility를 Visible로 변경한다.

④ 플레이어 입력을 비활성화하고, Input Mode를 UI Only로 변경한다. 그리고, 마우스 커서를 표시한다.

⑤ Tick에서 계속 실행되지 않도록 Do Once를 추가하였다.
⑥ 결과


10) 게임 재시작 및 종료 버튼 구현

① Restart 버튼의 OnClicked 이벤트에서 Open Level을 호출하도록 설정하였다.
재시작 시에는 Input Mode를 다시 UI Only에서 Game Only로 바꾼다.
② Quit 버튼의 OnClicked 이벤트에서 Quit Game을 호출하도록 설정하였다.
11) 남은 적 수 관리

① GameMode BeginPlay에서 Get All Actors Of Class(BP_Ant)를 이용하여 모든 개미를 가져왔다.
② For Each Loop를 이용하여 Ant 배열에 저장하였다.
③ 배열의 Length를 Integer 타입의 RemainingAnts 변수에 저장하였다.
④ HUD의 남은 적 수를 RemainingAnts 값으로 표시하였다.
12) 적 처치 이벤트 구현
① GameMode에 OnAntKilled Custom Event를 생성하였다.

② 이벤트 실행 시 RemainingAnts를 1 감소시키고, HUD를 갱신한다.
③ RemainingAnts가 0 이하가 되면 IsWin을 True로 변경하고, 게임 승리 HUD가 표시된다.
④ BP_Ant의 BeginPlay에서 GameMode를 캐스팅하여 변수로 저장하였다.

⑤ 개미가 제거될 때마다 OnAntKilled를 호출하도록 Destroy 함수 내부에서 호출하고 연결하였다.

⑥ 결과


13) 게임 종료 시 플레이어와 적 제어
① 게임 종료 시 플레이어의 IsFiring을 False로 변경하여 사격을 중지하였다.

② BP_Ant에 CanMove 변수를 추가하고, Move Random과 Start Chase에서 CanMove를 검사하도록 수정하였다.

③ 게임에서 승리하면

Ant 배열을 For Each Loop로 순회하고, 모든 개미의 CanMove를 False로 변경한다. 그 후, 개미의 Movement Mode를 None으로 변경하여 이동을 완전히 중지시켰다.
Completed 핀을 사용하여 게임 종료 UI가 한 번만 실행되도록 수정하였다.
14) 체력이 모두 소진되었을 때 게임 패배 구현
① Update Health and HUD에서 Health가 0 이하인지 검사하였다.

② Health가 0 이하가 되면 OnHealthDepleted를 호출하도록 연결하였다.
③ OnHealthDepleted는 다른 게임 종료 방식과 구성이 같다.
④ 게임 종료 구성
게임 종료 -> 승리/혹은 패배 문구 출력 -> For Each Loop로 적 움직임 제어 -> HUD Visible -> IsFiring false로 변경, Disable Input -> Set Input Mode UI Only -> 마우스 커서 보임
후기
오늘은 아이템 시스템부터 GameMode를 이용한 게임 진행 관리, 승리 및 패배 조건, UI 제어까지 게임의 전체 흐름을 구현하는 방법을 학습하였다. 특히, GameMode가 게임 전체의 상태를 관리하는 중심 역할을 한다는 점을 이해할 수 있었으며, 플레이어와 적, HUD가 서로 연결되어 하나의 게임 시스템을 구성하는 과정을 경험할 수 있었다.
이전까지는 총 발사, AI, HUD 등 각각의 기능을 개별적으로 구현하는 데 집중했다면, 오늘은 이러한 기능들을 하나로 연결하여 실제 게임처럼 동작하는 구조를 만드는 과정을 경험할 수 있었다. 기능 간의 연결 관계를 이해하면서 게임을 구성하는 전체적인 흐름을 조금 더 넓은 시각에서 바라볼 수 있게 되었다.
또한, 게임 종료 시 Do Once를 사용하여 이벤트의 중복 실행을 방지하고, IsFiring, CanMove, Movement Mode를 활용해 플레이어와 적의 행동을 모두 멈추도록 구현하면서 게임의 완성도를 높일 수 있었다. 이번 실습을 통해 단순히 기능을 구현하는 것을 넘어 게임의 시작부터 종료까지 전체 흐름을 설계하는 방법을 익힐 수 있었고, 이후 C++에서도 이러한 구조를 구현하는 데 큰 도움이 될 것이라 생각한다.
II. C언어 세션 (5회차)
개요
오늘 학습은 C언어의 두 핵심 축인 함수 호출 방식과 포인터의 동작 원리를 중심으로 진행되었다.
단순히 “포인터 문법”이나 “함수 사용법”을 익히는 단계가 아니라,
- 왜 함수 안에서 값이 안 바뀌는지
- 왜 포인터를 써야 원본이 바뀌는지
- 배열과 포인터가 왜 비슷해 보이는지
- *, & 같은 기호가 왜 헷갈리는지
이러한 문제들의 근본 원인(메모리 구조 + 값 전달 방식)을 기준으로 다시 정리했다.
학습 내용
1) 함수에서 값이 안 바뀌는 이유
함수에 넣으면 그냥 원본이 들어간다고 착각하기 쉽지만, 실제로는 그냥 복사본이다.
void func(int x) {
x = 100;
}
이 코드는 겉으로 보면 x를 바꾸는 것 같지만 실제로는 main의 변수와는 다른 “복사된 x”를 수정한다.
즉, C는 기본적으로 값 전달(call by value) 구조다. 그래서 원본을 바꾸려면 *를 써야 한다.
void func(int *x) {
*x = 100;
}
2) return의 진짜 역할 (값 반환 + 함수 종료)
return은 단순히 값을 돌려주는 게 아니라 함수 즉시 종료, 이후 코드 실행 차단 두 가지 역할을 한다.
if (x < 0) return -1;
이 구조가 가능한 이유는 return이 흐름 종료 장치이기 때문이다.
3) 포인터
int x = 10;
int *p = &x;
x → 값
&x → 주소
p → 주소 저장 변수
*p → 그 주소에 있는 값
4) & 와 *의 역할이 헷갈리는 이유
이게 제일 헷갈리는 부분이다.
| & | 변수 앞 | 주소 꺼내기 |
| * | 선언 | 포인터 선언 |
| * | 사용 | 주소 따라가기 |
int *p = &x;
*p = &x (잘못된 해석)
p = &x (정확한 구조)
5) 배열과 포인터의 관계
int arr[5];
int *p = arr;
| 배열 | 포인터 | |
| 크기 | 전체 크기 존재 | 주소 크기만 존재 |
| 대입 | 불가능 | 가능 |
| 이동 | 불가능 | 가능 |
sizeof(arr); // 전체 크기, 20
sizeof(p); // 포인터 크기, 8
둘은 확실히 차이난다. 배열은 고정된 메모리 공간이고, 포인터는 이동 가능한 주소 변수이다.
6) 함수에 배열을 넘기면 크기가 사라진다
void func(int arr[])
이건 사실 내부적으로 아래처럼 바뀐다.
void func(int *arr)
즉, 함수 안에서는 배열의 크기를 알 수 없고, 항상 size를 따로 넘겨야 한다.
7) 포인터 연산은 바이트 이동이 아니다
p + 1
이건 단순 +1이 아니라 타입 크기만큼 이동한다.
int → 4바이트
double → 8바이트
char → 1바이트
포인터는 타입 기반 주소 이동 시스템이다.
8) NULL로 초기화
int *p;
*p = 10; // 위험
초기화 안 된 포인터는 쓰레기 주소이고, 접근하면 undefined behavior를 일으킨다.
안전하게 쓸려 이렇게 NULL로 초기화해줘야 한다.
int *p = NULL;
포인터는 반드시 어디를 가리키는지 확실해야 사용 가능하다.
9) 지역 변수와 생명주기 문제
int* func() {
int x = 10;
return &x;
}
x는 함수 끝나면 사라진다.
주소만 남음 (댕글링 포인터)
주소는 남아도 값은 사라질 수 있다
10) 포인터의 포인터 (**)를 쓰는 이유
int **pp;
이건 단순 확장이 아니라 포인터 자체를 바꾸고 싶을 때 쓴다.
* → 값 변경
** → 주소 자체 변경
후기
오늘 내용은 새로운 개념이라기보다는, 이미 알고 있던 것들을 다시 한 번 “왜 그렇게 동작하는지” 기준으로 정리한 느낌이었다. 특히, 함수 호출과 포인터 전달 구조는 이미 알고 있었지만, 값 전달 vs 주소 전달이 실제로 코드에서 어떻게 구분되는지 다시 보면서 정리가 됐다.
그리고 포인터 자체보다는 *가 선언과 역참조에서 의미가 달라지는 지점, 배열이 포인터로 decay되는 순간의 차이, 함수 매개변수에서 타입 정보가 사라지는 구조 등 이런 언어가 설계된 방식 쪽이 다시 정리된 게 핵심이었다. 결국, 오늘은 새로 배웠다기보다는
흩어져 있던 C의 메모리 모델을 다시 한 번 구조로 묶어본 날이었다.
'언리얼 엔진 > TIL' 카테고리의 다른 글
| TIL - 본캠프 6일차 (0) | 2026.07.03 |
|---|---|
| TIL - 본캠프 4일차 (0) | 2026.07.01 |
| TIL - 본캠프 3일차 (0) | 2026.06.30 |
| TIL - 본캠프 2일차 (0) | 2026.06.29 |
| TIL - 본캠프 1일차 (0) | 2026.06.26 |