C++과 Unreal Engine으로 3D 게임 개발 4

김여울·2025년 7월 2일

내일배움캠프

목록 보기
36/139

📍 2주차 1강 ~ 2강

🛠️ 게임을 만들자 🛠️

🧍‍♂️ 캐릭터 기능
1. 캐릭터 클래스 구현
2. 입력 매핑 설정 (WASD, 점프, 스프린트, 카메라 회전)
3. 기본 동작 구현 (이동, 점프, 스프린트, 회전 등)
4. 애니메이션 적용 (Idle, Walk, Jump, Sprint)

🎁 아이템 시스템
5. 아이템 종류 설계 (치료, 코인, 체력 회복 아이템 등)
6. 아이템 상호작용 처리 (범위 내 있을 경우 효과 발생 - 체력 감소 / 점수 증가 / 체력 회복 등)
7. 아이템 랜덤 스폰 & 레벨에 따른 수량 및 배치 조정

💥 전투 & 점수
8. 캐릭터 데미지 처리 & 아이템 점수 관리 시스템

🌊 게임 흐름
9. 웨이브 시스템 구현 (적 생성 및 흐름 제어)
10. 실시간 HUD 정보 반영
11. 메뉴 및 UI 흐름 구현

🎨 시각 & 사운드 효과
12. UI 애니메이션 및 3D 위젯 UI 적용
13. 파티클 적용 (지뢰 폭발, 아이템 습득 등)
14. 사운드 효과 적용 (지뢰 폭발음, 아이템 획득음, 발걸음 등)

🚀 마무리
15. 최종 프로젝트 배포

1. 캐릭터 구현

1.1 GameMode 클래스

: 총괄 관리자 역할의 클래스

a. 역할

번호역할예시 (클래스명)
1️⃣플레이어 캐릭터Pawn 클래스 or Character 클래스
2️⃣캐릭터에 빙의PlayerController 클래스
3️⃣게임 규칙 관리로직(함수), 점수의 규칙 등
4️⃣게임 전역 데이터GameState 클래스 (점수 등 전체 공유)
-개별 캐릭터 데이터PlayerState 클래스

b. GameModeBase vs GameMode

항목AGameModeBaseAGameMode
기본 기능아주 최소한의 게임 규칙만 제공점수 처리, 팀, 플레이어 관리 등 더 많은 기능 포함
상속 용도커스텀 룰이 거의 없을 때 적합룰이 많은 게임 (멀티플레이, 점수 등)에 적합
예시 메서드StartPlay()HandleMatchHasStarted(), RestartPlayer()
유연성더 가볍고 유연함기능 많지만 무겁고 복잡함

c. 만들기

1️⃣ C++ 클래스로 GameMode 만든다 → SpartaGameMode

2️⃣ 블루프린트 상속 → BP_SpartaGameMode
C++로 만든 GameMode 그대로 적용하는 것보다 블루프린트로 감싸서 쓰면 에디터 내에서 여러 파라미터 수정 가능

  • SpartaGameMode (C++)
    • 처음에 만든 C++ 클래스
    • ASpartaGameMode : public AGameModeBase
  • BP_SpartaGameMode (블루프린트)
    • SpartaGameMode를 부모로 상속받은 블루프린트 클래스
    • 이건 C++로 만든 기능들을 시각적으로 확장/수정하는 용도

➡ "설계도(C++)"를 먼저 만들었고, 그걸 기반으로 "커스터마이징된 사본(BP)"을 만듦

d. GameMode를 Level에 적용하기

✅ 첫 번째 방법

프로젝트에 전역 GameMode 적용하기
Project Settings > Maps & Modes > Default GameMode
이 프로젝트에 생성된 모든 레벨에 전부 BP_GameMode를 GameMode로 사용

GameMode가 관리하는 클래스도 설정 가능

항목설명
Default Pawn Class플레이어가 조종할 기본 캐릭터 클래스
PlayerController Class입력 처리 담당 클래스
HUD ClassUI 표시를 담당하는 클래스
GameState Class게임 전반의 상태 (점수, 시간 등) 공유
PlayerState Class개별 플레이어 정보 (점수, 팀, 이름 등) 관리
Spectator Class관전자용 Pawn 클래스
플레이어가 죽었을 때 조종하는 Pawn
시점 전환, 카메라 이동, 다른 플레이어 구경 등에 사용

✅ 두 번째 방법

특정 레벨에만 GameMode 적용하기

Window > World Settings
레벨마다 따로 GameMode를 지정할 수 있음 → Level 단위 설정
→ 레벨에서 따로 지정하면 프로젝트 기본값은 무시되고 그 레벨만의 GameMode가 적용됨

구분설정 위치우선순위
프로젝트 기본값Project Settings > Maps & Modes낮음
레벨 전용 설정레벨에서 World Settings > Selected GameMode높음


1.2 캐릭터 클래스 만들기

a. 상속 구조

Actor		            모든 게임 오브젝트의 기본 클래스
  └── Pawn	            플레이어나 AI가 조종 가능한 오브젝트
        └── Character   보행, 점프 등 기본 이동 기능이 내장된 Pawn 클래스
구분AActorAPawnACharacter
조종 가능
이동 컴포넌트❌ 수동 구현❌ (수동)CharacterMovementComponent 내장
점프 / 중력✅ 자동 포함
메시 컴포넌트SkeletalMeshComponent 내장
예시문, 상자, 아이템자동차, 비행기, 드론사람형 캐릭터, 몬스터

❓ Pawn 클래스 = "조종 가능한 오브젝트"
마리오 카트는 플레이어가 자동차를 조종함

b. 만들기

1️⃣ C++ 클래스로 캐릭터 만든다 → SpartaCharacter

2️⃣ 블루프린트 상속 → BP_SpartaCharacter

컴포넌트 이름설명
CapsuleComponent충돌 영역. 캐릭터의 물리적인 범위를 나타냄 (기본 충돌 바운딩)
ArrowComponent캐릭터의 앞 방향을 시각적으로 보여줌 (방향 확인용)
Mesh (CharacterMesh0)캐릭터의 스켈레탈 메시(3D 모델) 표시 컴포넌트, 뼈가 있음
CharacterMovement걷기, 점프, 중력 등 이동 처리 전담 컴포넌트

3️⃣ Skeletal Mesh Asset 설정하기
Character 시선 방향은 x축

4️⃣ Capsule Component에 맞추기

  • 캐릭터 위치 맞추기
  • 캡슐 크기 키우기

5️⃣ 3인칭 게임이라면 캐릭터 뒤통수에 카메라 붙여줘야 함

  • SpringArm
  • Camera Component

✅ C++로 만들어보기

  • 미리 선언 (Forward Declaration)
    • 클래스를 실제로 정의하지 않고, 이름만 먼저 알려주는 줌
    • 헤더 파일 간의 의존성 줄여 컴파일 속도 빨라지고, 순환 참조도 막음
    • 미리 선언한 클래스는 포인터, 참조로만 사용 가능
    • 객체 자체를 만들거나 멤버에 접근하려면 전체 헤더 include 해야 함
상황설명
미리 선언만 할 때.h 파일에서 class AMyClass;처럼 이름만 알려줌
전체 정의가 필요할 때.h 또는 .cpp에서 #include "MyClass.h" 해야 함
  • 상속 구조
RootComponent
└── SpringArmComp
    └── CameraComp
요소설명
CreateDefaultSubobject<T>()컴포넌트를 엔진에 등록하면서 생성하는 함수
TEXT("SpringArmComp")에디터/엔진에서 이 컴포넌트를 식별할 이름
SpringArmComp해당 컴포넌트를 변수에 저장해서 나중에 설정하거나 사용
//SpartaCharacter.h
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaCharacter.generated.h"

// 헤더에서 include를 사용하지 않고 미리 선언을 사용
class USpringArmComnponent;
class UCameraComponent;

UCLASS()
class SPARTAPROJECT_API ASpartaCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ASpartaCharacter();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")	// 객체 변경은 불가능, 내부 속성은 에디터에서 조정 가능
	USpringArmComponent* SpringArmComp;	// 컴포넌트 선언
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	UCameraComponent* CameraComp;

protected:
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};


// SpartaCharacter.cpp
#include "SpartaCharacter.h"
// 헤더 파일에서 include를 사용하지 않고 미리 선언을 사용
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"

ASpartaCharacter::ASpartaCharacter()
{ 
	PrimaryActorTick.bCanEverTick = false;	// 이 캐릭터는 Tick을 사용하지 않음

	SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArmComp")); // 컴포넌트 생성
	SpringArmComp->SetupAttachment(RootComponent);	// 루트 컴포넌트에 연결
	SpringArmComp->TargetArmLength = 300.0f; // 스프링 암(삼각대)의 길이 설정
	SpringArmComp->bUsePawnControlRotation = true; // 캐릭터의 회전을 스프링 암이 따르도록 설정

	CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComp"));
	CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName); // 스프링 암 끝 부분에 연결
	CameraComp->bUsePawnControlRotation = false; // 카메라가 캐릭터의 회전을 따르지 않도록 설정
}

void ASpartaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}


코드에서 에러는 없어서 솔루션 정리 후 다시 빌드했더니 찾았다!!

VisibleAnywhere, BlueprintReadOnly → 디테일 창에서 조정 가능
→ SprintArm 높이 변경

6️⃣ GameMode에서 Character 연결
✅ Editor
이 GameMode가 적용된 레벨들은 방금 만든 캐릭터를 사용함

✅ C++

// GameMode.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "SpartaGameMode.generated.h"

UCLASS()
class SPARTAPROJECT_API ASpartaGameMode : public AGameMode
{
	GENERATED_BODY()

public:
	ASpartaGameMode();	// 생성자 선언
	
};


// GameMode.cpp
#include "SpartaGameMode.h"
#include "SpartaCharacter.h"	// Character 헤더파일 포함

ASpartaGameMode::ASpartaGameMode()	// 생성자 정의
{
	DefaultPawnClass = ASpartaCharacter::StaticClass();	// 기본 폰 클래스를 SpartaCharacter로 설정
	// StaticClass()는 클래스의 정적 메서드로, 
	// 객체를 생성하지 않았지만 해당 클래스의 UClass 객체를 반환함
	// C++ 클래스에서 그 클래스의 UClass 정보를 꺼내주는 역할
}
  • DefaultPawnClass = ASpartaCharacter::StaticClass();
    기본 플레이어 캐릭터는 SpartaCharacter라는 클래스를 기반으로 만들어~

  • StaticClass()

    • 언리얼은 내부적으로 객체를 생성할 때 UClass라는 클래스 정보가 필요
    • StaticClass()는 C++ 클래스에서 그 클래스의 UClass 정보를 꺼내주는 역할
    • 이걸로 언리얼이 어떤 클래스를 Spawn(생성)해야 할지 알 수 있음
  • 비교

    코드의미
    new ASpartaCharacter()객체를 "지금" 메모리에 직접 생성
    ASpartaCharacter::StaticClass()"클래스 정보"만 가져옴 (객체는 아직 X)

아래 PlayerStart 위치에 Character spawn

💡 PlayerStart
게임 시작 시 PlayerStart 위치에 Character spawn (GameMode 역할)

여기까지 하면 캐릭터 등장 짠 ~ 🌟


2. 캐릭터 입력 매핑 구현

2.1 Player Controller

  • 플레이어와 캐릭터 사이의 중간 관리자
  • 입력(Input)을 받아서 캐릭터에게 전달
  • 카메라를 조종할 수 있음
  • UI랑 상호작용할 때도 컨트롤러가 중간에 있음
[🧑‍💻 플레이어] 
       ↓ 입력 
[🎮 PlayerController]
   ├─→ [🧍‍♂️ 캐릭터 (Pawn)]   ← 이동 / 점프 등
   ├─→ [📷 카메라]          ← 시야 회전
   └─→ [🖱️ UI]              ← 클릭 / 버튼 / 메뉴

PlayerController 뇌 (명령 전달)
Character 몸뚱이 (처리 담당)

a. 역할

  1. 입력 처리
  • Enhanced Input System 사용 (언리얼 5 표준)
  • 키보드, 마우스, 패드 등 다양한 입력 감지
  1. 카메라 제어
  • 캐릭터 회전 또는 독립적인 시점 회전 처리
  1. UI와 상호작용
  • 버튼 클릭, 메뉴 네비게이션 등 UI 입력 중계
  1. Possess / UnPossess
  • Possess() : Pawn에 '빙의'해서 조작 가능하게 만듦
  • UnPossess() : 조작에서 분리됨 (예: 캐릭터 사망 시 관전 전환)

📌 하지만 직접 처리는 거의 안 하고, 대부분 Pawn/캐릭터가 처리함!

b. 만들기

1️⃣ C++ 클래스로 Player Controller 만든다 → SpartaPlayerController
GameMode에서 PlayerController 관리함

// GameMode.cpp
#include "SpartaGameMode.h"
#include "SpartaCharacter.h"	
#include "SpartaPlayerController.h"	 // GameMode에서 PlayerController 관리함



ASpartaGameMode::ASpartaGameMode()	// 생성자 정의
{
	DefaultPawnClass = ASpartaCharacter::StaticClass();	
	PlayerControllerClass = ASpartaPlayerController::StaticClass();	// PlayerController 설정
	
}

2️⃣ 블루프린트 상속 → BP_SpartaPlayerController
BP로 만들고 GameMode에서 Player Controller Class 설정해줌


2.2 Enhanced Input System

a. 개념

✅ IMC (Input Mapping Context)

  • 입력 묶음 관리 (사람 전용 / 자동차 전용 등)
  • 다양한 Input Action(IA)을 포함하고 있음 → 스위치 역할

✅ IA (Input Action)

  • 추상적인 행동 정의 (ex. 점프, 이동 등)
  • 실제 함수와 연결됨 (전선처럼 연결된 느낌)

🧍 캐릭터 행동

  • 이동 → WASD
  • 회전 → 마우스
  • 점프 → Space
  • 스프린트 → Shift

b. Input Action (IA)

Value Type

ValueType설명예시
Boolean참/거짓 입력 (토글/버튼 누름 등)점프, 공격, 인터랙션, 스프린트
Axis1D1차원 축 입력, 한 방향만 입력 가능전진/후진, 가속 페달 → 자동차에서 많이 사용
Axis2D2차원 축 입력 (X, Y)이동 (WASD), 마우스 시점 회전, 조이스틱
Axis3D3차원 축 입력 (X, Y, Z)카메라 회전(3축), VR 컨트롤러, 비행 시뮬레이션

Triggers

입력이 언제 활성화되게 할 것인가
근데 보통은 따로 지정 안해도 됨

Trigger설명
None트리거 없음. 기본값.
Chorded Action특정 키 조합(Chorded)이 눌릴 때만 작동. (ex. Ctrl + C)
Combo (Beta)키 조합 연속으로 눌렀을 때 작동하는 실험적 트리거.
Down키가 막 눌렸을 때 1회 작동.
Hold키를 누르고 있는 동안 계속 작동.
Hold And Release키를 누르고 있다가 뗄 때 작동.
Pressed키가 한 번 눌렸을 때 작동. (Down과 비슷)
Pulse누르고 있는 동안 주기적으로 반복 작동.
Released키를 뗐을 때 1회 작동.
Tap짧게 누르고 뗐을 때 작동. 길게 누르면 무시됨.

Modifiers

🛠 Modifier 이름📝 설명
Dead Zone조이스틱처럼 민감한 입력 장치에서 작은 입력값을 무시하고 0으로 처리함
FOV Scaling시야(Field of View)에 따라 입력을 조정함 (줌인/아웃 시 민감도 자동 조절)
Negate입력값의 부호를 반전시킴 (예: 1 → -1)
Response Curve – User Defined커스텀 곡선에 따라 입력값을 보정 (입력 곡선 편집기로 직접 설정 가능)
Scalar입력값에 특정 수치를 곱함 (예: X축 입력만 2배 빠르게 만들기)
Scale By Delta Time프레임율에 관계없이 일정한 속도로 입력을 적용하도록 보정
Smooth입력값을 부드럽게 만들어 움직임을 자연스럽게 만듦
Smooth Delta변화량(Δ값)을 기준으로 부드럽게 보정
Swizzle Input Axis Values입력 축을 서로 바꾸거나 조합함 (예: X ↔ Y)
To World Space입력 벡터를 월드 좌표계 기준으로 변환

c. Input Mapping Context (IMC)

  • Swizzle Input Axis Values
    • 원래: Y = 좌우, X = 앞뒤
    • 근데 입력 장치나 블루프린트에서 Y축으로 “앞으로” 값이 들어옴
      (프로콘 생각하면 앞으로 가게 스틱 움직이면 Y축임)
      그래서 Swizzle로 YX 축 바꿔줌 → 캐릭터가 앞으로 움직임

❓ 언리얼 InputAction 값이 왜 반대로 나올까

  • 키보드 입력 값 (WASD) 기준

    • W → InputValue: (X=0, Y=1)

    • D → InputValue: (X=1, Y=0)

      방향
      앞뒤X
      좌우Y
      위아래Z

입력값의 Y가 "앞"인데, 언리얼은 X가 앞이니까 좌표축이 안 맞음!
➡ 축 스왑해서 매핑해줘야 함 ➡ Swizzle Input Axis Values

  • Negate 부정 : 반대 방향
    W ←→ S
     A ←→ D

▶ Move의 IMC
▶ Jump의 IMC
▶ Sprint의 IMC
▶ Look의 IMC

❓ 왜 Y축만 부정(Negate)하는가

  • 월드 기준 Y축
    • 위쪽 이동: 양수
    • 아래쪽 이동: 음수
  • 카메라 기준 회전(Y축 입력)
    • 위로 마우스 이동: 음수
    • 아래로 마우스 이동: 양수

➡ 그래서 카메라 회전 방향이 직관적으로 맞게 하려면 Y축 입력을 부정(Negate) 해줘야 함

d. IMC 활성화

1️⃣ 활성화 해야 Player Controller가 IMC의 영향 아래 있게 됨

// SpartaPlayerController.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SpartaPlayerController.generated.h"

// 미리 선언
class UInputMappingContext;
class UInputAction;

UCLASS()
class SPARTAPROJECT_API ASpartaPlayerController : public APlayerController
{
	GENERATED_BODY()

public:
	ASpartaPlayerController();	// 생성자

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")	//에디터에서 수정 가능하도록 설정
	UInputMappingContext* InputMappingContext;	// IMC를 할당해줄 ptr 변수
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputAction* MoveAction;	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputAction* JumpAction;	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputAction* LookAction;	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputAction* SprintAction;
	
protected:	// IMC 활성화 및 입력 액션 바인딩을 위한 함수들
	virtual void BeginPlay() override;	// 플레이 시작 시 호출되는 함수
	// PlayerController가 생성된 직후에 IMC를 활성화
};



// SpartaPlayerController.cpp
#include "SpartaPlayerController.h"
#include "EnhancedInputSubsystems.h"	// Enhanced Input Subsystem을 사용하기 위한 헤더

ASpartaPlayerController::ASpartaPlayerController()	// 생성자 정의
	: InputMappingContext(nullptr), 
	  MoveAction(nullptr), 
	  JumpAction(nullptr), 
	  LookAction(nullptr), 
	  SprintAction(nullptr)	// BP에서 할당 할 거니까 여기선 nullptr로 초기화
{	
}

// IMC 활성화
void ASpartaPlayerController::BeginPlay()	// 플레이 시작 시 호출되는 함수
{
	Super::BeginPlay();	// 부모 클래스의 BeginPlay 호출
	if (ULocalPlayer* LocalPlayer = GetLocalPlayer())	// 로컬 플레이어 정보 가져오기
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
			LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())	// EnhancedInputSubsystem을 관리하는 Subsystem 획득하기
		{
			if (InputMappingContext)	// IMC가 할당되어 있는지 확인
			{
				Subsystem->AddMappingContext(InputMappingContext, 0);	// 획득한 Subsystem에 IMC를 추가, 0은 최우선순위로 두고 활성화 시킴
			}
		}
	}
}

2️⃣ BP_SpartaPlayerController에서 할당하기

3️⃣ 실행되는지 확인하기


실행하니까 플레이어 컨트롤러를 스폰하지 못했다..

World Settings에서 Player Controller Class BP로 만든 거 넣어주기!

그러면 IA랑 IMC로 만들어 놓은게 작동한다!


💭

분명 예전에 배웠던 내용인데, 구현 순서를 정리하면서 다시 공부하니까 이해가 더 연결돼서 머리에 잘 들어오는 느낌이다.
그래도 아직은 헷갈리고 어려운 부분이 많아서 계속 해봐야겠다...

0개의 댓글