I. Blueprint - FPS 슈팅 게임 개발 세션 (4회차)
개요
오늘은 FPS 프로젝트에 필요한 HUD(UI) 와 적 AI, 그리고 플레이어 피격 시스템을 구현했다.
Widget Blueprint를 이용해 체력과 탄약을 표시하는 HUD를 제작하고, Collision과 Overlap 이벤트를 활용하여 플레이어를 추적하고 폭발 공격을 수행하는 적 AI를 만들었다. 마지막으로 Apply Damage와 Event Any Damage를 이용해 플레이어의 체력을 감소시키고 UI가 함께 갱신되도록 구현하면서 게임의 기본 전투 흐름을 완성했다.
구현 과정
1) 위젯 블루프린트(HUD)
① HUD 제작
Widget Blueprint(WB_HUD) 생성
-> Canvas Panel 배치
-> Progress Bar를 이용하여 HP 바 제작 및 변수로 지정
-> Text를 이용하여 탄약 수 표시
-> 탄약 수를 표시하는 Text는 AmmoAmount 변수로 지정
-> 중앙에 Crosshair(Image) 추가
-> Engine Content의 Anchor 이미지를 사용하여 조준점 구현
-> 알파 값을 낮춰 화면을 가리지 않도록 설정

② HUD 생성 및 화면 출력
Create Widget의 Return Value를 PlayerHUD 변수로 저장하여 이후 체력 및 탄약 UI를 쉽게 갱신할 수 있도록 구성하였다. 또한, Set Text를 사용해 현재 총기의 시작 시 탄약 UI를 초기화했다.

BeginPlay에서 CurrentAmmo를 MaxAmmo로 초기화한 후 HUD의 탄약 텍스트에도 초기값을 표시하였다.

③ 탄약 UI 업데이트
Fire 함수에서 발사 가능한 경우

CurrentAmmo 감소와 AmmoAmount Text 갱신이 함께 이루어지도록 구현하였다.
실제 FPS에서는 맞지 않아도 총알은 소모되므로 총기 반동, 탄약 감소, UI 갱신을 Line Trace와 Branch 사이에 배치했다.

④ 결과



2) Collision과 Overlap을 이용한 적 AI 구현
① 적 생성
BP_Ant를 생성
-> Skeletal Mesh 적용
-> Arrow Component 방향으로 메시 정렬
-> Collision Preset을 Custom으로 변경
-> Visibility 항목을 Ignore에서 Block으로 변경

Visibility를 Block으로 변경하여 플레이어의 Line Trace가 적과 충돌하도록 설정하였다.
② 플레이어 탐지 (Chase)
플레이어 감지를 위해 Sphere Collision 컴포넌트를 추가하고 TraceSphere로 이름을 지정했다.
Sphere 반경을 600으로 설정하고 TraceSphere의 BeginOverlap 이벤트 노드를 아래처럼 연결했다.

플레이어가 감지 범위 안으로 들어오면 AI가 플레이어를 목표로 추적하도록 구현하였다.

또한 레벨 전체를 감싸는 NavMeshBoundsVolume을 배치하여 AI Move To가 정상적으로 경로를 탐색할 수 있도록 설정하였다.
③ 폭발 공격 (Attack)
근접 공격 범위를 담당하는 새 Sphere Collision 컴포넌트를 추가하고 이름을 ExplosionSphere로 지정했다.
Sphere 반경을 100으로 설정하고 ExplosionSphere의 BeginOverlap 이벤트 노드를 아래처럼 연결했다.

플레이어가 공격 범위 안으로 들어오면 플레이어에게 데미지를 전달하고, 폭발 파티클과 사운드를 재생한 뒤, 적이 사라지도록 구현하였다.
추적(TraceSphere)과 공격(ExplosionSphere)을 서로 다른 Collision으로 분리하여 역할을 명확하게 구분하였다.
3) 플레이어 피격 처리 및 HUD 갱신
플레이어가 적의 공격을 받으면 HP가 감소하도록 Event AnyDamage를 사용하여 피격 처리를 구현하였다.

현재 체력에서 전달받은 Damage 값을 차감한 후
Health / MaxHealth
를 계산하여 Progress Bar의 Percent를 갱신하였다.
이를 통해 체력이 감소하면 HUD의 HP 바도 함께 줄어들도록 구현하였다.
마지막으로 Delay 이후 피격 사운드를 재생하여 피격 연출을 추가하였다.


후기
오늘은 HUD와 적 AI, 플레이어 피격 시스템을 서로 연결하면서 FPS 게임의 기본적인 전투 흐름을 완성했다. 단순히 기능을 하나씩 구현하는 것이 아니라 UI와 게임 로직이 서로 연동되는 과정을 이해할 수 있었고, Apply Damage와 Event Any Damage를 이용해 공격하는 객체와 피해를 받는 객체의 역할을 분리하는 구조도 익힐 수 있었다.
또한 강의를 따라 구현하는 과정에서 총알이 적에게 맞았을 때만 탄약이 감소하는 구조가 실제 FPS의 동작과 다르다고 판단하여, 탄약 감소와 UI 갱신을 Branch 이전으로 옮겨 수정했다. 단순히 따라 만드는 것에 그치지 않고 게임의 동작을 기준으로 로직을 검토하고 개선해 본 점이 오늘 가장 의미 있었던 부분이었다.
II. C언어 세션 (4회차)
개요
오늘 C언어 라이브 세션에서는 함수(Function) 와 포인터(Pointer) 의 개념을 학습했다. 함수를 사용하는 이유와 선언·정의·호출의 흐름, 매개변수와 반환값의 개념을 익혔고, 재귀 함수의 특징과 장단점도 살펴보았다. 또한, 포인터의 기본 개념인 메모리 주소, 주소 연산자(&), 역참조 연산자(*), 포인터 초기화의 중요성 등을 배우며 앞으로 배열, 문자열, 동적 메모리 등을 이해하기 위한 기초를 다졌다.
학습 내용
1) 함수 (Function)
함수는 특정 작업을 수행하는 코드 블록으로, 한 번 작성하면 여러 번 호출하여 재사용할 수 있다.
동일한 코드를 반복해서 작성하지 않아도 되므로 코드의 중복을 줄이고 유지보수를 쉽게 만들어 준다.
① 함수의 구성
함수는 크게 선언(Prototype), 정의(Definition), 호출(Call) 의 과정으로 사용된다.
int add(int a, int b);
선언 : 함수의 이름, 반환형, 매개변수를 컴파일러에게 미리 알려준다.
int add(int a, int b) {
return a + b;
}
정의 : 함수 내부에서 실제 수행할 기능을 구현한다.
#include <stdio.h>
int add(int a, int b);
int main(void) {
printf("%d\n", add(5, 7));
return 0;
}
int add(int a, int b) {
return a + b;
}
호출 : 정의된 함수를 실행한다.
C 컴파일러는 코드를 위에서 아래로 순차적으로 읽기 때문에, 함수를 정의하기 전에 호출하려면 반드시 함수 선언이 필요하다.
실제 프로젝트에서는 함수 선언을 헤더 파일(.h) 에, 함수 구현을 소스 파일(.c) 에 작성하여 역할을 분리한다. 이를 통해 여러 소스 파일에서 같은 함수를 사용할 수 있고, 코드의 재사용성과 유지보수성이 향상된다.
② 매개변수와 인자
매개변수(Parameter) : 함수를 정의할 때 선언하는 변수
인자(Argument) : 함수를 호출할 때 전달하는 실제 값
함수는 전달받은 인자를 매개변수에 저장한 뒤 해당 값을 이용하여 작업을 수행한다.
③ 반환형과 return
함수는 작업 결과를 호출한 곳으로 돌려줄 수 있으며, 이를 반환값(Return Value) 이라고 한다.
int subtract(int a, int b) {
return a - b;
}
return은 함수의 실행을 종료하고 값을 반환하는 명령어이다.
반환형이 void가 아닌 함수는 반드시 return으로 적절한 값을 반환해야 한다.
return을 생략하면 컴파일러에서 경고나 오류가 발생할 수 있으며, 프로그램 실행 시 의도하지 않은(쓰레기) 값이 반환될 수 있다.
void printHello() {
printf("Hello!");
}
int main(void) {
}
C99 이후의 C 표준에서는 main 함수 끝에 도달하면 컴파일러가 자동으로 return 0;을 추가한 것처럼 작동하기에 예외이다.
매개변수가 없는 함수는 () 대신 (void)를 사용하는 것이 C언어에서는 권장된다.
이렇게 작성하면 "이 함수는 어떤 인자도 받지 않는다." 는 의미를 컴파일러와 다른 개발자에게 명확하게 전달할 수 있다.
④ 변수의 종류
함수에서 사용하는 변수는 선언 위치에 따라 종류가 나뉜다.
지역 변수(Local Variable) : 함수 내부에서 선언하며, 함수가 종료되면 메모리에서 제거된다.
전역 변수(Global Variable) : 함수 밖에서 선언하며, 프로그램 종료 시까지 메모리에 유지된다.
static 지역 변수 : 함수 내부에서 선언하지만 프로그램이 종료될 때까지 값을 유지하며, 이전 호출의 값을 기억한다.
#include <stdio.h>
int global = 99; // 전역 변수: 어디서든 접근 가능, 프로그램 종료까지 유지
void demo(void) {
int local = 10; // 지역 변수: 이 함수 안에서만
static int count = 0; // 정적 변수: 이 함수 안에서만 보이지만 값은 유지됨
count++;
printf("count = %d\n", count);
}
int main(void) {
demo(); // count = 1
demo(); // count = 2
demo(); // count = 3
// printf("%d", local); // ❌ 에러. local은 demo 안에서만 존재
return 0;
}
지역 변수와 static 지역 변수는 모두 함수 내부에서만 사용할 수 있지만, 생존 시간이 다르다.
⑤ 재귀 함수
재귀 함수는 함수 내부에서 자기 자신을 다시 호출하는 함수이다.
복잡한 문제를 작은 문제로 나누어 해결할 때 유용하며, 수학적인 정의를 코드로 표현하기 쉽다는 장점이 있다.
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
하지만 함수가 호출될 때마다 스택(Stack) 에 함수 정보가 저장되므로 메모리 사용량이 증가하며, 호출 깊이가 너무 깊어지면 Stack Overflow가 발생할 수 있다.
실무에서는 성능이 중요하거나 호출 횟수가 많은 기능은 반복문으로 변경하는 경우가 많지만, 트리 탐색이나 깊이우선탐색(DFS)처럼 재귀가 더 적합한 문제에서는 여전히 많이 사용된다.
2) 포인터 (Pointer)
포인터는 메모리 주소를 저장하는 변수이다.
일반 변수는 데이터를 저장하지만, 포인터는 데이터가 저장된 위치(주소)를 저장하여 해당 메모리에 직접 접근할 수 있다.
① 주소 연산자(&)와 역참조 연산자(*)
주소 연산자(&) : 변수의 메모리 주소를 구한다.
역참조 연산자(*) : 포인터가 저장하고 있는 주소의 실제 값에 접근한다.
두 연산자는 서로 반대 역할을 수행한다.
*(&a) = a
&(*p) = p
#include <stdio.h>
int main(void)
{
int num = 10;
int* ptr = #
*ptr = 20;
printf("%d", num);
return 0;
}
② 포인터 초기화
초기화하지 않은 포인터는 쓰레기 주소(Garbage Address) 를 저장하고 있으므로 매우 위험하다.
이러한 포인터를 역참조하면 예기치 않은 메모리에 접근하게 되어 프로그램이 비정상적으로 종료되거나 오류가 발생할 수 있다.
따라서 포인터는 사용하기 전에 반드시 유효한 주소를 저장하거나 NULL로 초기화하는 습관이 중요하다.
// 잘못된 예: 초기화하지 않은 포인터
int *p1;
*p1 = 24; // 쓰레기 주소 접근 (위험)
// 잘못된 예: NULL 포인터
int *p2 = NULL;
*p2 = 12; // NULL 역참조 (위험)
// 올바른 예
int num = 12;
int *p3 = #
*p3 = 24; // 정상
③ 포인터의 특징
포인터는 주소를 저장하는 변수이기 때문에 int*, char*, float* 모두 주소를 저장한다는 점은 동일하다.
포인터의 타입은 가리키는 데이터의 타입을 의미하며, 컴파일러가 해당 메모리를 어떤 자료형으로 해석할지를 결정한다.
#include <stdio.h>
int main(void)
{
int a1 = 3;
char b1 = 'd';
float c1 = 3.14159f;
int *a2 = &a1;
char *b2 = &b1;
float *c2 = &c1;
printf("int* : %zu\n", sizeof(a2));
printf("char* : %zu\n", sizeof(b2));
printf("float* : %zu\n", sizeof(c2));
if (sizeof(a2) == sizeof(b2) &&
sizeof(b2) == sizeof(c2))
{
printf("모든 포인터의 크기가 같습니다.\n");
}
return 0;
}
④ 포인터의 활용
포인터는 앞으로 배우게 될 배열, 문자열, 함수의 인자 전달, 동적 메모리 할당, 연결 리스트 및 트리와 같은 자료구조 등의 핵심 개념에서 계속 사용된다.
후기
오늘은 함수와 포인터라는 C언어의 핵심 개념을 학습했다. 함수 선언과 헤더 파일의 역할, 반환값과 매개변수의 개념을 더 명확하게 이해할 수 있었다. 특히 헤더 파일과 소스 파일을 분리하는 구조가 앞으로 언리얼 C++의 .h와 .cpp 구조로 이어진다는 점이 인상적이었다.
포인터는 처음 접하면 어렵게 느껴질 수 있는 개념이지만, '값이 아니라 메모리 주소를 저장하는 변수' 라는 관점으로 이해하니 전체적인 흐름이 보인다. 앞으로 배열과 문자열, 동적 메모리까지 배우면서 포인터를 더 많이 활용하게 될 텐데, 이번에 배운 기본 개념을 확실히 이해해 두는 것이 중요하다.
'언리얼 엔진 > TIL' 카테고리의 다른 글
| TIL - 본캠프 6일차 (0) | 2026.07.03 |
|---|---|
| TIL - 본캠프 5일차 (1) | 2026.07.02 |
| TIL - 본캠프 3일차 (0) | 2026.06.30 |
| TIL - 본캠프 2일차 (0) | 2026.06.29 |
| TIL - 본캠프 1일차 (0) | 2026.06.26 |