2026-04-07(화) std::distance, DFS, BFS, 인접 리스트, 인접 행렬(2차원 배열),

조범근·2026년 4월 7일

TIL

목록 보기
38/87

C++ Week 7

Study

언리얼C++ 기초, 7번 과제, 알고리즘 1문제


Today I Learned


std::distance

Overview

    auto it = std::find(seoul.begin(), seoul.end(), "Kim");
    
    int num = std::distance(seoul.begin(), it);

std::distance를이용해 seoul.begin()에서 it까지의 거리를 구할 수 있다.
그런데 궁금한게 하나가 생겼다.

it - v.begin()일때 it은 주소값인데 어떻게 index가 나올까?

C++이 단순한 바이트 단위로 뺴는 게 아니라는 것을 알고 있어야한다.
만약 string하나가 메모리에서 32바이트를 차지한다고 가정했을때

begin의 주소가 1000이라면?
-> 그다음 칸은 1032, 그다음 칸은 1064가 된다.
-> it - begin을 하면 컴퓨터는 이렇게 계산한다.
-> (현재 주소 - 시작주소) / 자료형의 크기
EX: (1064 - 1000) / 32 = 2

즉, 주소 차이가 64바이트니까, 32바이트짜리 데이터가 2개 들어있다는걸 파악하고 번호를 2번이라고 결론을 내리는 것 이다.



깊이 우선 탐색 (DFS, Depth-First Search)과 너비 우선 탐색(BFS, Breadth-First Search)

Overview

최대한 깊이 내려간 뒤, 더이상 깊이 갈 곳이 없을 경우 옆으로 이동.

  1. 모든 노드를 방문하고자 하는 경우에 이 방법을 선택한다.
  2. 깊이 우선탐색(DFS)이 너비 우선 탐색(BFS)보다 좀 더 간단하다.
  3. 검색 속도 자체는 너비 우선 탐색(BFS)에 비해서 느리다

스택 또는 재귀함수로 구현한다.


최대한 넓게 이동한 다음, 더 이상 갈 수 없을 때 아래로 이동
두 노드 사이의 최단 경로를 찾고 싶을때 이 방법을 선택한다.

큐를 이용해서 구현한다.


문제 유형

1) 그래프의 모든 정점을 방문하는 것이 중요한 문제
DFS, BFS 두 가지 방법 중 어느 것을 사용해도 상관이없다

2) 경로의 특징을 저장해둬야 하는 문제
각 정점에 숫자가 적혀있고 a -> b까지 가는 경로를 구하는데 경로에 같은 숫자가 있으면 안 된다는 문제
등, 각각의 경로마다 특징을 저장해둬야 할 때는 DFS를 사용(BFS는 경로의 특징을 가지지 못한다)

3) 최단거리 구해야 하는 문제
미로 찾기 등 최단거리를 구해야 하는 경우, BFS가 유리하다.
DFS로 경로를 검색할 경우 처음으로 발견되는 해답이 최단거리가 아닐 수 있지만,
BFS로 현재 노드에서 가까운 곳부터 찾기 때문에 경로탐색 시 먼저 찾아지는게 최단거리기 때문

이거 외에 검색 대상 그래프가 정말 크다면 DFS를 고려,
검색대상 규모가 크지 않고, 검색 시작 지점으로부터 원하는 대상이 별로 멀지 않다면 BFS


코드

bool visited[9]; 
vector<int> graph[9];

void dfs(int x)
{
	visited[x] = true;
	cout << x << " ";
	for (int i = 0; i < graph[x].size(); i++) // 인접한 노드 사이즈만큼 탐색
	{
		int y = graph[x][i];
		if (!visited[y]) // 방문하지 않았으면 즉 visited가 False일 때 not을 해주면 True가 되므로 아래 dfs 실행
            		dfs(y); // 재귀적으로 방문
	}
}

이걸 단순한 숫자로 생각하지말고 하나의 노드로 생각해서 맵을 탐색한다는 느낌으로 봐야한다.
그렇지 않으면 목적을 이해 할 수가 없다.

  • dfs(y); -> 함수가 자기 자신을 호출하면 현재 상태를 메모리(Stack)에 쌓아두고 새로운 함수를 실행한다.
    갈 길이 막히면 가장 최근에 쌓인 함수부터 종료하며 되돌아오는 방식(Backtracking)이 DFS의 본질임

인접 리스트(Adjacency List) vs 2차원 배열

vector<int> graph[9]를 봤을때 2차원 배열이라고 생각했는데 개념이 다른 인접 리스트(Adjacency List)였다.

  • 인접 행렬(2차원 배열) matrix[4][4]
    이 방식은 모든 경우의 수를 표로 만드는 것이다.
    특정 두 노드가 연결되었는지 확인하는 속도가 가장 빠르다.
    그렇지만 길이 거의 없는 경우에도 빈칸을 모두 저장해야 하므로 메모리가 낭비된다.

  • 인접 리스트 graph[4]
    이 방식은 각 노드마다 연결된 노드들 목록을 주머니에 넣어두는 방식이다.
    메모리 효율이 매우 좋다. 실제 연결된 친구들의 번호만 저장하므로 낭비가 없다.
    하지만 특정 두 노드가 연결되었는지 확인하려면 바구니를 열어서 노드 목록을 일일이 찾아봐야 한다.





언리얼 C++

1. SKM(Skeletal Mesh)

몸 전체가 하나로 묶여 있는 게 아니라, 부모-자식 관계로 뼈와 뼈사이가 이어져있다.


2. SpringArm과 Camera

SpringArm (셀카봉)
-> 카메라와 캐릭터 사이의 거리를 조절하고, 벽에 부딪히면 카메라를 앞으로 당겨주는 완충 역할을 한다.

Camera (눈)
-> 실제로 화면을 보여주는 역할

SocketName

CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);

  • SocketName : SpringArm의 맨 끝부분에 해당하는 이름표이다.
    카메라를 맨 끝에 달린 소켓에 붙여주는 것이다.



3. Enhanced Input System(강화된 입력 시스템)

언리얼 엔진 5의 표준 입력 방식으로, 무엇을 누르는가(입력)과 무엇을 하는가(행동)을 완전히 분리하여 관리하는 것이 핵심이다.

IMC와 IA가 편리한 이유

  • 유연성 -> '스페이스바 = 점프' 라고 코드에 박아넣지 않고 나중에 점프 키를 'A버튼'으로 바꾸고 싶을 때, 코드를 건드리지 않고 IMC 설정만 바꾸면 끝이다.
  • 상태별 관리 -> 평소에는 IMC_Character를 쓰다가, 차에타면 IMC_Car로 Mapping만 갈아 끼우면 된다.

Input Action(IA) "무엇을 할 것인가?"

추상적인 '행동' 그 자체를 정의한다. (EX: 점프, 이동, 공격)
Value Type

  • Digital (bool) -> 눌렀다(True), 뗐다(False) (점프, 스프린트)
  • Axis2D (Vector2D) -> 마우스 이동이나 WASD처럼 X, Y축의 변화량이 동시에 필요할때 사용
  • Axis3D -> 드론 조종처럼 X,Y,Z축 값이 모두 필요할때 사용

Triggers "어떤 방식으로 사용할까?"

입력이 언제 실행될지 조건을 거는것

  • Hold -> 일정 시간 이상 꾹 누르고 있어야 발동 (기 모으기 공격)
  • Released -> 키를 누를 때가 아니라, 손을 뗄 때 발동
  • 특수한 조작(EX 차지샷)이 아니면 기본값으로 두는 것이 가장 반응 속도가 좋고 자연스럽다.

Modifiers "입력된 값을 어떻게 가공할까?"

들어온 가공되지 않은 값을 요리하는 단계이다.

  • Dead Zone -> 마우스나 스틱이 아주 미세하게 떨리는 것을 무시한다. (민감도 조절)
  • Negate -> 값을 반전시킨다 보통 S(뒤로 가기)나 A(왼쪽 가기)에 걸어준다 왜냐면 앞으로가거나 오른쪽으로가는게 정배이기 때문
  • Scalar -> 입력값에 곱하기를 해서 감도를 키우거나 줄인다.
  • Swizzle Input Axis values -> 입력 축을 바꾼다.

보통 기본 입력은 X축으로 들어오기때문에 이걸 Swizzle해서 Y축이나 Z축 값으로 변환해 사용

Input Mapping Context (IMC) "어떤 키에 연결할까?"

IA와 실제 키보드/마우스 버튼을 Mapping 해주는 매개체이다.
하나의 IA에 여러 키를 등록 가능



4. LocalPlayer Mapping

void ASprataPlayerController::BeginPlay(){
	Super::BeginPlay();
	
	if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
	{
		if (UEnhancedInputLocalPlayerSubsystem* SubSystem =
			LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
		{
			if (InputMappingContext)
			{
				SubSystem->AddMappingContext(InputMappingContext, 0 );
			}
		}
	}
}

GetLocalPlayer()

지금 플레이 하고있는 플레이어를 데려온다.


GetSubsystem< UEnhancedInputLocalPlayerSubsystem >()

플레이어 안에 있는 입력 전담 매니저(Subsystem)을 불러온다.

if (InputMappingContext)

헤더파일에서 선언했던 IMC 파일이 실제로 할당되어 있는지 체크
에디터(blueprint)의 디테일 창에서 만든 IMC_Character파일을 넣어줬는지 확인하는 체크박스


SubSystem->AddMappingContext(InputMappingContext, 0)

유저한테 입력 규칙(IMC)를 적용해주고 우선순위는 0번으로 설정한다.
이제부터 W를 누르면 앞으로 가기 라는 규칙이 이 플레이어에게 활성화된다.

우선순위가 뭘까? 만약 인벤토리를 열었다면 IMC_Inventory를 우선순위 1번으로 추가한다. 그럼 같은 W를 눌러도 캐릭터가 걷지 않고 인벤토리 커서가 움직이게 할 수 있다.

이 시스템의 장점은 탈부착이 가능하다.
SubSystem->RemoveMappingContext(IMC_Character)를 하고
AddMappingContext(IMC_Car)를 하면 순식간에 조작법이 바뀐다.



5. 컨트롤러 작업

#include "EnhancedInputComponent.h"
void ASprataCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	
	if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{
		if (ASprataPlayerController* PlayerController = Cast<ASprataPlayerController>(GetController()))
		{
			if (PlayerController->MoveAction)
			{
				EnhancedInput->BindAction(
					PlayerController->MoveAction,
					ETriggerEvent::Triggered,  // 입력이 눌렀을때
					this,
					&ASprataCharacter::Move  // MoveAction이 있을때 Move를 연결 시키겠다
					);
			}

if(PlayerController->MoveAction)일때 행동을 아래와 같이 하겠다라는 뜻임
JumpAction, SprintAction, LookAction 내가 만들어둔 설정을 가져오는 것,

Cast< T >는 진짜 T타입이 맞는지 확인하고 맞으면 그 기능을 쓸 수 있게 해달라는 것

BindAction은 어떤상황에 어떤 행동을 할지를 결정하는 4개의 필수 정보를 받는다.

  1. Action(무엇을) : PlayerController->MoveAction
    에디터에서 만든 Input Action 데이터 에셋이다. "어떤 키를 눌렀을 때"에 해당하는 정의를 담고 있다.

  2. TriggerEvent (언제) : ETriggerEvent::Triggered
    입력이 발생하는 타이밍
    - Started -> 버튼을 누르는 순간 딱 한번
    - Triggered -> 버튼을 누르고 있는 동안 매 프레임(지속적)
    - Completed -> 버튼에서 손을 떼는 순간

  3. Object (누가) : this
    실행할 함수가 들어있는 객체의 주소. 보통 현재 캐릭터인 this를 넣는다. 엔진에게 이 함수는 this 안에 있어 라고 알려주는 것

  1. Function (어떤 함수를) : &ASprataCharacter::Move
    실행될 함수의 주소. 클래스 이름과 함수 이름을 명시하여 정확히 어떤 로직을 실행할지 지정한다.

void ASprataCharacter::Move(const FInputActionValue& value){
	// value가 2D Vector로 들어옴
	if (!Controller) return; // 컨트롤러가 있는지 확인
	
	const FVector2D MoveInput = value.Get<FVector2D>();
	
	if (!FMath::IsNearlyZero(MoveInput.X))
	{
		AddMovementInput(GetActorForwardVector(), MoveInput.X);
	}
	if (!FMath::IsNearlyZero(MoveInput.Y))
	{
		AddMovementInput(GetActorRightVector(), MoveInput.Y);
	}
}

IA에서 Swizzle로 W를 눌면 X Z Y 좌표값으로 바꿔서 전환되게 해둔 것이다.

그래서 (1.0, 0.0, 0.0) 이렇게 데이터가 들어온다 치면 NearlyZero가 아니니까 내가 원한 만큼 움직이는거고 반대로 양옆으로 움직이는 것도 그렇다.
Negate걸어둔것들은 반대로 움직이는거니까 WASD랑 좌표값이랑 Mapping한것은 IA한다고 생각하면 될듯
인벤토리 같은거 열때도 I누르면 데이터 1값이 들어오면 열리게 이런식으로 하게 되는것


6. 왜 Look은 컨트롤러가 있는지 확인을 안할까?

Look함수에서 사용한 AddControllerYawInput()이나 AddControllerPitchInput() 함수는 안에 이미 다 체크를 하는게 들어있다.


7. Animation State Machine

To Falling -> jump
점프 키를 눌러서 강한 상승 기류를 탔을 때 Jump 애니메이션을 작동해라
velocity > 100 일떄 작동

To Falling -> Fall Loop
그냥 걷다가 낭떠러지로 떨어졌을 때, 점프 모션 없이 허우적거리는 루프 모션으로 가야한다.
Jump를 velocity > 100 일떄 작동으로 해뒀음으로 velocity < 100 일때 작동으로 해놔야 나중에 겹치지않는다.

왜 1만 변해도 하는게아니라 100을 기준으로 잡았을까?

캐릭터가 평지를 뛰어가도 발바닥이 돌부리에 걸리거나 경사로를 만날때마다 Z 속도가 1, 5, 10정도는 수시로 발생 할 수있음.


8. 짐벌 락

90도에 도달하는 순간 수치가 90 -> -179.9 -> 89.9 이런 식으로 미친 듯이 튀는데, 이때 화면이 뒤집히거나 떨리는 게 전형적인 짐벌 락 증상.

void ASprataCharacter::Look(const FInputActionValue& Value)
{
    FVector2D LookInput = Value.Get<FVector2D>();

    float CurrentPitch = SpringArmComp->GetRelativeRotation().Pitch;
    float TargetPitch = CurrentPitch + LookInput.Y;

    TargetPitch = FMath::Clamp(TargetPitch, -80.0f, 80.0f);

    SpringArmComp->SetRelativeRotation(FRotator(TargetPitch, 0.0f, 0.0f));

    AddActorLocalRotation(FRotator(0.0f, LookInput.X, 0.0f));
}

FMath::Clamp로 제한을 걸어두는 것 까지는 OK였는데 CurrentPitch를 정해두지않고 그냥 LookInput.Y에 Clamp를 건게 패인이였다.

이렇게 하게되면 LookInput.Y는 1.0f이런식으로 값이 나오는데 거기에 -80 ~ 80까지 Clamp를 걸어봤자 전이랑 다를게 없는 것이다.

그래서 CurrentPitch로 현재 Rotation을 받아서 거기에다가 LookInput.Y를 더했을때 -80 ~ 80 안에서 Clamp를 걸 수 있어야 제대로 된 설정이 락이 걸린다.
여기서 80으로 한 이유는 원래는 90도 부터인데 순간적으로 90을 걸어두게 되면 내부 연산중에 90.00001이 될 수 있기때문에 안전하게 80

0개의 댓글