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

김여울·2025년 7월 4일

내일배움캠프

목록 보기
37/139

코드카타

두 정수 사이의 합 구하기

#include <string>
#include <vector>
#include <algorithm> // 여기에 std::min, std::max 있음

using namespace std;

long long solution(int a, int b) 
{   // static_cast<long long>을 사용하는 이유는 int 범위를 넘는 경우를 방지
    long long MinValue = static_cast<long long>(min(a, b));     // min(A, B) : 두 값 중 작은 값을 반환하는 함수 - 소문자 사용
    long long MaxValue = static_cast<long long>(max(a, b));
    
    long long Count = MaxValue - MinValue + 1;  // 항의 개수 구하기
    long long answer = (MinValue + MaxValue) * Count / 2;   // 등차수열의 합 공식
  
    return answer;
}

// 다른 방법

long long solution(int a, int b) 
{
    long long answer = 0;
    
    if (a > b)
    {
        int temp = a;
        a = b;
        b = temp;
    }
    
    for (int i = a; i <= b; ++i)
    {
        answer += i;
    }
    
    return answer;
}

long long을 사용하는 이유

  • a = 5,000,000, b = 10,000,000 같은 경우엔 합이 30조 이상 될 수 있음
  • int는 범위가 약 21억까지라서 overflow 발생
  • 따라서 long long(64비트 정수) 써야 안전함

std::min(a, b) / std::max(a, b) 사용법

  • 두 수 중 작은/큰 값을 구하는 C++ 표준 함수
  • 반드시 #include <algorithm> 필요
  • 함수 이름은 소문자! Min(a, b) 또는 Max(a, b) 쓰면 에러 남
  • 변수 이름도 Min, Max로 지으면 충돌하니 MinValue, MaxValue 등으로 구분
long long MinValue = static_cast<long long>(std::min(a, b));
long long MaxValue = static_cast<long long>(std::max(a, b));

(Min + Max) * Count / 2

  • 등차수열의 합 공식
  • Count = Max - Min + 1 → 항의 개수
  • (Min + Max) * Count / 2 → 첫 수 + 끝 수 × 항 개수 ÷ 2
  • 반복문 없이 O(1) 계산 가능해서 훨씬 효율적임

실수했던 부분

실수한 코드이유
Min(a, b)❌ 함수 아님. 대문자 함수는 정의되어 있지 않음
long long Min = ... + Min(a, b)❌ Min은 이미 변수라 함수처럼 쓸 수 없음
#include <algorithm> 빠짐std::minstd::max 못 찾음

📍 2주차 3강 ~ 4강

🛠️ 언리얼 입력 시스템 흐름
구성 요소 역할
1️⃣ GameMode
: 게임 전체 흐름과 Character 생성 및 관리
2️⃣ Character
: 실제 게임 속 플레이어 캐릭터 (GameMode에서 스폰됨)
3️⃣ PlayerController
: 입력 담당. Character와 플레이어를 연결하는 뇌/영혼 역할
4️⃣ Input Mapping Context (IMC)
: 어떤 키에 어떤 동작이 연결됐는지 정의
5️⃣ Local Player Subsystem
: IMC를 관리하고, 입력의 의미를 해석해 PlayerController에 전달

Local Player - Local Player Subsystem - IMC 활성화

🧩 입력 흐름 예시
1️⃣ 사용자가 A키 누름
2️⃣ PlayerController가 "A" 입력 감지
3️⃣ "A가 뭔데?" → Local Player Subsystem에 물어봄
4️⃣ Subsystem: "A = 공격 동작!"
5️⃣ Controller: "오케이, 공격 실행!" → 캐릭터에게 명령
6️⃣ Character: 실제 공격 함수 실행

셋은 유기적으로 상호작용하면서 입력 → 해석 → 실행 과정을 반복
PlayerController = 중간 관리자
Local Player Subsystem = 통역사
Character = 실행자

3. 캐릭터 기본 동작 구현

3.1 SpartaCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaCharacter.generated.h"

// 헤더에서 include를 사용하지 않고 미리 선언을 사용
class USpringArmComponent;
class UCameraComponent;
struct FInputActionValue;	// 구조체

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;	// IA에다 동작 함수들 연결하는 장소

	// 함수 선언
	// IA 만들 때 설정한 valuetype
	// 구조체 같은 건 크기 때문에 참조 안 하고 갖고 오면 객체의 모든 데이터를 복사해서 갖고 옴 -> 성능, 복사 비용 크다
	// 수정 못하게 const로 선언
	// 리플렉션 시스템에 등록만
	UFUNCTION()
	void Move(const FInputActionValue& value);	
	UFUNCTION()
	// boolean 타입은 on/off 상태를 나눠주는게 좋음
	void StartJump(const FInputActionValue& value);
	UFUNCTION()
	void StopJump(const FInputActionValue& value);
	UFUNCTION()
	void Look(const FInputActionValue& value);
	UFUNCTION()
	void StartSprint(const FInputActionValue& value); 
	UFUNCTION()
	void StopSprint(const FInputActionValue& value);

private:
	float NormalSpeed;	// 기본 속도
	float SprintSpeedMultiplier;	// 기본 속도에 몇 개를 곱해줄건데
	float SprintSpeed;	// 위에 두 개를 곱해서 나오는 속도 = 얼마나 빨라졌는지
};

3.2 SpartaCharacter.cpp

#include "SpartaCharacter.h"
#include "EnhancedInputComponent.h"	// Enhanced Input 시스템을 사용하기 위한 include
#include "SpartaPlayerController.h"	// 플레이어 컨트롤러를 사용하기 위한 include
// 헤더 파일에서 include를 사용하지 않고 미리 선언을 사용
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/CharacterMovementComponent.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; // 카메라가 캐릭터의 회전을 따르지 않도록 설정

	NormalSpeed = 600.0f;	// 기본 속도 초기화
	SprintSpeedMultiplier = 1.7f;	// 스프린트 속도 배율 초기화
	SprintSpeed = NormalSpeed * SprintSpeedMultiplier;	

	// CharacterMovement 컴포넌트가 여러 이동 함수들을 가지고 있음
	GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;	// 캐릭터의 최대 걷기 속도를 기본 속도로 설정

}

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

	if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))	// PlayerInputComponent 여러 갠데 우리가 사용하는 EnhancedInput 기능으로 정해주기
	{
		ASpartaPlayerController* PlayerController = Cast<ASpartaPlayerController>(GetController());	// 현재 캐릭터가 조작하는 Controller get -> SpartaPlayerController로 한 번 캐스팅

		if (PlayerController == nullptr)	// null 체크
		{
			return;
		}

		if (PlayerController->MoveAction)	// IMC가 할당되어 있다면
		{
			EnhancedInput->BindAction(	// BindAction : 이벤트랑 함수를 연결하는 핵심 코드
				PlayerController->MoveAction,	// IA 가져오기
				ETriggerEvent::Triggered,	// IMC에서 설정한 트리거 이벤트
				this,	// 이 캐릭터에 바인딩
				&ASpartaCharacter::Move	// Move 함수에 바인딩, 포인터 - 호출된 함수 주소 가져옴
			);
		}

		if (PlayerController->JumpAction)
		{
			EnhancedInput->BindAction(
				PlayerController->JumpAction,
				ETriggerEvent::Triggered,
				this,
				&ASpartaCharacter::StartJump
			);

			EnhancedInput->BindAction(
				PlayerController->JumpAction,
				ETriggerEvent::Triggered,
				this,
				&ASpartaCharacter::StopJump
			);
		}

		if (PlayerController->LookAction)
		{
			EnhancedInput->BindAction(
				PlayerController->LookAction,
				ETriggerEvent::Triggered,
				this,
				&ASpartaCharacter::Look
			);
		}

		if (PlayerController->SprintAction)
		{
			EnhancedInput->BindAction(
				PlayerController->SprintAction,
				ETriggerEvent::Started,
				this,
				&ASpartaCharacter::StartSprint
			);

			EnhancedInput->BindAction(
				PlayerController->SprintAction,
				ETriggerEvent::Completed,
				this,
				&ASpartaCharacter::StopSprint
			);
		}
	}
}

	
void ASpartaCharacter::Move(const FInputActionValue& value)
{
	// 컨트롤러가 있어야 방향 계산이 가능
	if (!Controller) return;

	// Value는 Axis2D로 설정된 IA_Move의 입력값 (WASD)을 담고 있음
// 예) (X=1, Y=0) → 전진 / (X=-1, Y=0) → 후진 / (X=0, Y=1) → 오른쪽 / (X=0, Y=-1) → 왼쪽
	const FVector2D MoveInput = value.Get<FVector2D>();

	if (!FMath::IsNearlyZero(MoveInput.X))
	{
		// 캐릭터가 바라보는 방향(정면)으로 X축 이동
		AddMovementInput(GetActorForwardVector(), MoveInput.X);
	}

	if (!FMath::IsNearlyZero(MoveInput.Y))
	{
		// 캐릭터의 오른쪽 방향으로 Y축 이동
		AddMovementInput(GetActorRightVector(), MoveInput.Y);
	}
}

void ASpartaCharacter::StartJump(const FInputActionValue & value)
{
	if (value.Get<bool>())	// bool 값이 true라면
	{

		Jump();	// 점프 함수 호출
	}
}

void ASpartaCharacter::StopJump(const FInputActionValue & value)
{
	if (!value.Get<bool>())	// bool 값이 false라면
	{
		StopJumping();	// 점프 중지 함수 호출
	}
}

void ASpartaCharacter::Look(const FInputActionValue & value)
{
	FVector2D LookInput = value.Get<FVector2D>();	// 입력값을 2D 벡터로 가져옴

	AddControllerYawInput(LookInput.X);	// X축 입력값을 yaw 회전에 적용
	AddControllerPitchInput(LookInput.Y);	// Y축 입력값을 pitch 회전에 적용
}

void ASpartaCharacter::StartSprint(const FInputActionValue & value)
{
	if (GetCharacterMovement())
	{
		GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;	// 캐릭터의 최대 걷기 속도를 스프린트 속도로 설정
	}
}

void ASpartaCharacter::StopSprint(const FInputActionValue & value)
{
	if (GetCharacterMovement())
	{
		GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;	// 캐릭터의 최대 걷기 속도를 기본 속도로 설정
	}
}

4. 캐릭터 애니메이션 적용

캐릭터의 상태에 따라 애니메이션 적용해야 함 → 애니메이션 블루프린트를 만들어야 한다

4.1 애니메이션 블루프린트 (ABP)

a. ABP 만들기

b. Preview Mesh 설정하기

c. Loop Animation 체크하기

→ 애니메이션 계속 반복됨

d. Animation > Animation Mode > Animclass 설정하기

e. ABP EventGraph

❓ 어느 시점에 애니메이션 변경할지

  • Character Movement Component
    이동에 관련된 다양한 데이터 & 함수 가지고 있음
    → 이 정보 가져와서 그때그때 애니메이션 변경
  • Event BlueprintInitializeAnimation = BeginPlay
    ABP가 초기화 될 때 딱 한 번 호출됨
  • Event BlueprintUpdateAnimation = Tick
    매 프레임마다 호출 → 매 번 여기를 통해 데이터 가져오기

4.2 Locomotion

f. StateMachine (Locomotion)

상태에 따라 적절한 애니메이션으로 전환

  • Locomotion
    Idle, Walk, Spring 등을 상황에 따라서 전환하는 스테이트를 모아놓은 스테이트 머신
    ▶ 각 스테이트들 Loop Animation ☑
    ▶ Walk/Run은 블랜드 스페이스로 애니메이션 자연스럽게 섞어주기
    ▶ 트랜지션 룰 : Idle <-> Walk/Run 조건 설정하기
    ▶ 최종 형태

4.3 Main States

g. Main States & Control Rig

Control Rig

  • 본(Bone)을 제어하거나 애니메이션을 절차적으로 조작할 수 있게 해주는 리깅 도구
  • 애니메이션 중에 발이 공중에 뜨지 않도록 조절할 때 (예: 발이 바닥에 붙어야 함)
  • 무기 들고 있을 때 손 위치를 정확히 유지
  • 캐릭터가 경사면에 서있을 때 발 위치 보정
 애니메이션 재생 중 
 → 발이 공중에 뜸 (not IsFalling) 
 → Control Rig 사용
→ 발 위치를 땅 높이에 맞게 보정 
→ 자연스러운 움직임 완성

▶ Set Initial Transforms from Mesh ☑
▶ Control Rig Class 설정 후 ShouldDoIKUse Pin ☑ + 조건 연결

IK vs Control Rig

항목🦿 IK (Inverse Kinematics)🎮 Control Rig
뭐냐?관절 뼈 계산 (관절 자동 제어)IK 계산법을 따라 실제로 캐릭터를 움직임 (툴/시스템)
목적손·발 같은 끝 위치 고정하고 나머지 자동 계산본을 직접 스크립트처럼 조정하거나 제어
작동 방식목표 위치 주면 → 나머지 관절 알아서 계산IK 포함해서 회전, 위치, 조건 등 전부 조작 가능
대표 예시Two Bone IK, Full Body IKControl Rig 블루프린트에서 IK 노드, 조건, 계산 등
쓰는 곳애님 블루프린트, Skeletal ControlControl Rig 에디터, 애님 BP, 시퀀서 등
단독 사용?O (IK 노드만 써도 작동함)X (Control Rig은 IK를 포함해서 씀)
  • IK만 사용 → 발 위치는 고정되지만 상황 따라 유연한 제어는 어려움
  • Control Rig → "경사면이면 무릎도 꺾고, 발 각도도 조절해!" 같은 조건부 로직까지 가능

h. Locomotion을 Main States의 entry state에 연결하기

  • Locomotion을 Cached Pose로 저장
    • 계산된 포즈를 임시로 저장해두고 나중에 또 써먹는 기능

State

  • Land
  • Fall_Loop
  • Jump Jump는 Loop Animation 체크 해제해도 됨

State Alias

그룹 형태로 만듦

  • To Land

    • 공중에 있는 = 지면으로 향하는 그룹
    • Jump, Fall Loop 두 상태를 To Land State 그룹으로 묶음
  • To Falling

    • 지면에 있는 = 공중으로 향하는 그룹
    • Locomotion, Land 두 상태를 To Falling State 그룹으로 묶음

Transition Rule

Automatic Rule Based on Sequence Player in State ☑ 해서 자연스럽게 연결하기

  • Locomotion ← Land
    조건 2개 필요

    • 착륙했고 걷기 시작했다 (bShouleMove) → Locomotion 모드
    • Automatic Rule Based on Sequence Player in State ☑
      • 현재 상태(State)에서 재생 중인 Sequence(애니메이션)가 끝남 → 자동으로 다음 상태로 전이되게 한다
      • 이 조건 안 넣으면 계속 Land Animation만 진행
  • To Land → Land

    • 공중에서 착지로 상태 전환을 해야하는 상황들
    • bIsFalling == false가 되면 Land로 전환
  • To Falling → Jump

    • 걷고 있거나 착륙을 했거나 = 바닥에 있는 상황
    • Z속도값이 100 이상(위로 가는 방향)이고 떨어지고 있는 중(bIsFalling == true)일 때
  • To Falling → Fall Loop

    • 바닥에 있다가 공중으로 전환되는 상황
    • 캐릭터가 땅을 벗어나 떨어지기 시작
      → To Falling 상태로 들어가고
      → 떨어지는 중(Falling) 동안엔 Fall Loop 상태로 유지
    Idle / Run / Jump
         ↓ (is falling?)
     [To Falling]
         ↓ (looping fall anim)
     [Fall Loop]
         ↓ (is on ground?)
     [Land]

💭

영훈 튜터님께서 어제 이해 못했던 IMC swizzle을 알려주셨다.
swizzle을 들어오는 값 자체로 생각하지 말고 바뀐 축 순서로 생각하기

IMC > Modifiers > Order

swizzle은 Swizzle은 벡터의 축 순서를 바꾸는 것

W / S - 전진 / 후진

  1. 키보드 w를 누른다 → x가 들어온다 → x +1
  2. swizzle 섞는다
  3. (1, 0, 0) - X, Z, Y | X, Y, Z
<X, Y, Z Swizzle>
Swizzle 순서: (X, Y, Z)
Vector: (1, 0, 0) → 그대로 유지됨
→ X값은 그대로 첫 번째 자리에 있음 → 정상 전진 ✅

<X, Z, Y Swizzle>
Swizzle 순서: (X, Z, Y)
Vector: (1, 0, 0) → X값이 그대로 첫 번째 자리에 있음
→ X가 여전히 첫 번째 자리에 있음 → 정상 전진 ✅
Swizzle첫 번째 축벡터 해석전진/후진 작동?
X, Y, ZX(1, 0, 0) → 전진
X, Z, YX(1, 0, 0) → 전진
Y, X, ZY(0, 1, 0) → 좌우❌ 전진 실패
Y, Z, XY(0, 0, 1) → 엉뚱한 방향❌ 전진 실패

A / D - 좌우 이동

  1. 키보드 a를 누른다 → x가 들어온다 → x +1
  2. swizzle 섞는다
  3. (1, 0, 0)
  4. Negate
  5. (-1, 0, 0) - Y, X, Z
Y값이 Swizzle에서 첫 번째 자리에 있어야 좌우 이동이 제대로 동작

Original Vector: (-1, 0, 0) // A키 입력 → 왼쪽으로 가고 싶음

<Y, X, Z Swizzle>   
Swizzle 순서: (Y, X, Z) 
Vector: (0, -1, 0) 
→ 첫 번째 자리에 Y가 들어옴  
→ “왼쪽으로 이동한다”는 의미가 정상 반영됨 ✅

<Y, Z, X  Swizzle>  
Swizzle 순서: (Y, Z, X) 
Vector: (0, 0, -1)
최종 출력 벡터는 (0, 0, 1) → Z축(위 방향)으로 이동함 ❌

0개의 댓글