Post

2026-05-06 TIL (51일차)

2026-05-06 TIL (51일차)

강사님 디지털 트윈 코드 분석2

Wheel 클래스

언리얼 엔진의 ‘차량(Vehicle)’ 템플릿에서 제공하는 UChaosVehicleWheel을 상속받아 앞바퀴와 뒷바퀴의 역할을 물리적으로 분리하여 구현한 클래스입니다.


1. WheelFront (앞바퀴)

앞바퀴의 가장 큰 임무는 ‘방향 전환(조향)’입니다.

  • AxleType = EAxleType::Front;
    • 이 바퀴가 차축 중 ‘앞축’에 장착된다고 물리 엔진에 알려줍니다.
  • bAffectedBySteering = true;
    • AI나 플레이어가 핸들(Steering)을 꺾었을 때 이 바퀴가 좌우로 같이 돌아갈 것인지(조향의 영향을 받을지) 결정하는 스위치를 켭니다.
  • MaxSteerAngle
    • 핸들을 끝까지 돌렸을 때 바퀴가 물리적으로 꺾일 수 있는 최대 각도를 설정합니다.


2. WheelRear (뒷바퀴)

이 차량의 뒷바퀴 임무는 ‘동력 전달’‘강제 제동’입니다.

  • AxleType = EAxleType::Rear;
    • 이 바퀴가 차축 중 ‘뒤축’에 장착된다고 물리 엔진에 알려줍니다.
  • bAffectedByHandbrake = true;
    • DoHandbrakeStart (핸드브레이크) 함수로 사이드 브레이크를 채울 때, 물리적으로 잠겨버리는 바퀴가 뒷바퀴들로 지정된 것입니다.
  • bAffectedByEngine = true;
    • 엔진에서 만들어진 동력(엑셀을 밟았을 때 뿜어져 나오는 힘)이 이 바퀴로 전달된다는 뜻입니다.




SplineFollowerComponent

LandscapeSplinesComponent

핵심 기능

  • 지형 변형 (Deformation): 단순히 길만 그리는 게 아니라, 그 길에 맞춰 산등성이를 깎아 평지로 만들거나 계곡을 메워 도로 높이로 맞추는 작업을 엔진이 자동으로 하게 만듭니다.
  • 메쉬 자동 배치 (Mesh Placement): 도로 바닥 메쉬, 옆의 가드레일, 가로등 같은 것들을 곡선을 따라 자동으로 쭉 깔아줍니다.
  • 레이어 마스크 관리: 도로가 지나가는 자리에만 자동으로 ‘아스팔트’ 텍스처가 칠해지도록 지형의 레이어 정보를 수정할 수 있습니다.

일반 스플라인(USplineComponent)차이점

특징USplineComponentULandscapeSplinesComponent
주목적이동 경로, 단순 배치도로 및 지형 편집
지형 영향지형과 따로 놂지형의 높낮이를 직접 수정함
성능가볍고 범용적임무겁지만 지형 데이터와 긴밀히 연결됨
주요 활용순찰 경로, 레이저 궤적아스팔트 도로, 산길, 강바닥

활용 예시

자동차 프로젝트를 하고 계시니, 이 컴포넌트는 다음과 같은 상황에서 쓰입니다.

  • 레이싱 트랙 제작: 지형을 울퉁불퉁하게 만든 뒤, 이 컴포넌트로 트랙을 깔면 트랙 바닥만 매끈하게 평탄화됩니다.
  • 오프로드 경로: 산길을 만들 때 바닥의 흙 텍스처를 스플라인 경로를 따라 자동으로 입힐 때 사용합니다.


1. 생성자 & BeginPlay)

  • 생성자: 매 프레임(Tick) 연산이 가능하도록 설정하지만, 초기에는 꺼둡니다(bStartWithTickEnabled = false). 경로가 만들어지기 전에 차가 움직이는 것을 막기 위함입니다.
  • BeginPlay: 자신이 부착된 차량(OwnerPawn)을 변수에 저장하고, 자율주행의 첫 단계인 BuildPath()를 호출하여 도로 스캔을 시작합니다.


2. BuildPath (경로 탐색 및 구축)

월드에 깔린 도로를 스캔하여 차량이 밟고 지나갈 ‘점(PathPoints)’들의 배열로 변환합니다.

도로 데이터 변환: ALandscapeSplineActor를 찾아 로컬 좌표계를 차량이 이동할 월드 좌표계로 변환합니다.

  • 스플라인 액터 검색: TActorIterator를 사용하여 현재 월드에 배치된 ALandscapeSplineActor를 찾고, 그 내부의 도로 컴포넌트(ULandscapeSplinesComponent)를 변수에 할당하여 준비합니다.
  • 월드 좌표계 변환 준비: 도로 데이터는 자신만의 고유한 ‘로컬 좌표계’를 사용합니다. 이를 그대로 스폰이나 이동에 사용하면 기준점이 달라져 차량이 완전히 엉뚱한 곳으로 이동하는 버그가 발생합니다. 따라서 차량이 실제 주행할 ‘월드 좌표계’로 정확히 변환해주기 위한 트랜스폼(Transform) 값을 미리 할당하여 준비해 둡니다.

기준점(Nearest CP) 찾기: 내 차량에서 반경 내에 있는 가장 가까운 도로 제어점을 찾습니다.

  • 1. 제어점 순회 (반복 검사 시작)
    • “스플라인을 구성하는 굵직한 제어점들을 하나씩 꺼내서 확인합니다.”
    • ➡️ for (ULandscapeSplineControlPoint* CP : SplinesComp->GetControlPoints())
    • 실행 원리: 도로(스플라인) 데이터 안에 들어있는 수많은 점(CP)들의 목록을 처음부터 끝까지 하나씩 순서대로 꺼내어 반복문(for)을 돌립니다.
  • 2. 3D 공간 거리 계산
    • “꺼낸 제어점과 현재 차량 위치(VehicleLoc) 사이의 실제 거리를 잽니다.”
    • ➡️ const float D = FVector::Dist(ToWorld.TransformPosition(CP->Location), VehicleLoc);
    • 실행 원리: 제어점의 로컬 좌표를 실제 월드 좌표로 변환(TransformPosition)한 뒤, 내 차량의 위치와의 3차원 직선 거리(D)를 계산합니다.
  • 3. 최단 거리 갱신 알고리즘
    • “지정된 반경(SearchRadius) 내에 있으면서 ‘가장 가까운’ 점을 찾아냅니다.”
    • ➡️ float BestDist = SearchRadius;if (D < BestDist) { BestDist = D; NearestCP = CP; }
    • 실행 원리:
      1. 가장 짧은 거리 기록(BestDist)의 초기값을 허용 최대치인 SearchRadius로 설정해 둡니다. (이 반경 밖의 점은 아예 취급하지 않기 위함입니다.)
      2. 방금 측정한 거리(D)가 현재 1등 기록(BestDist)보다 더 짧을 경우에만!
      3. 1등 기록을 갈아치우고(BestDist = D), 최종 우승자 자리(NearestCP)에 현재 점(CP)을 앉히는 과정을 반복합니다.
  • 4. 예외 처리 (안전장치)
    • “주변에 도로가 아예 없을 경우를 대비합니다.”
    • ➡️ if (!NearestCP) { return; }
    • 실행 원리: 모든 점을 다 뒤졌는데도 내 주변(반경 내)에 점이 하나도 없어서 우승자(NearestCP)가 선발되지 않았다면, 무리해서 코드를 돌리지 않고 경고 로그를 남긴 채 함수를 즉시 종료(return)하여 게임이 튕기는 것을 막습니다.

시작점 및 경로 추출: 역방향으로 탐색해 도로의 진짜 시작점을 찾고(순환로 여부 판별), 다시 정방향으로 진행하며 차량이 지나갈 경로점들을 수집합니다.

역방향 탐색
  • 1. 연결된 길(Segment) 찾기
    • “현재 제어점과 연결된 길 중, 아직 안 가본 길을 찾습니다.”
    • ➡️ for (const auto& Conn : StartCP->ConnectedSegments)if (!Visited.Contains(Conn.Segment))
    • 실행 원리: 현재 제어점(StartCP)에 연결된 길(Segment)들을 검사하여, 한 번도 방문하지 않은 길(Seg)을 찾아 방문 처리(Visited.Add)를 합니다. 무한 루프에 빠지는 것을 막기 위한 필수 과정입니다.
  • 2. 반대쪽 끝점 찾기 (뒤로 한 칸 이동)
    • “선택한 길의 반대편 끝에 있는 제어점으로 이동합니다.”
    • ➡️ ULandscapeSplineControlPoint* Other = (Seg->Connections[0].ControlPoint == StartCP) ? ...
    • 실행 원리: 하나의 길(Segment)에는 양끝에 두 개의 제어점이 있습니다. 내가 방금 서 있던 점이 0번이라면 1번 점으로, 1번이었다면 0번 점(Other)으로 이동하여 뒤로 한 칸 거슬러 올라갑니다. 그리고 StartCP = Other;를 통해 내 위치를 갱신합니다.
  • 3. 역방향 탐색 종료 조건 (루프 탈출)
    • 종료 조건 A (막다른 길): if (!Seg) break;
      • 뒤로 가려고 찾아봤지만 연결된 안 가본 길이 없을 때입니다. 즉, 이곳이 도로의 맨 처음(진짜 시작점)이므로 탐색을 종료합니다.
    • 종료 조건 B (순환로 감지): if (Other == NearestCP) { StartCP = NearestCP; break; }
      • 뒤로 계속 갔는데 처음 출발했던 기준점(NearestCP)이 다시 나왔을 때입니다. 도로가 뺑글뺑글 도는 트랙(서킷)이라는 뜻이므로, 출발했던 점을 시작점으로 고정하고 탐색을 종료합니다.
정방향 탐색
  • 1. 초기화 및 전진
    • “방문 기록을 지우고, 진짜 시작점부터 앞으로 나아갑니다.”
    • ➡️ Visited.Reset(); ULandscapeSplineControlPoint* CurCP = StartCP;
    • 실행 원리: 앞서 뒤로 가면서 남긴 방문 기록을 싹 지우고, 방금 찾은 시작점부터 다시 연결된 길(Seg)을 찾아 앞으로 전진합니다.
  • 2. 상세 경로점 추출 및 월드 좌표 변환
    • “길(Segment) 안에 있는 촘촘한 곡선 점들을 월드 좌표로 꺼내옵니다.”
    • ➡️ const TArray<FLandscapeSplineInterpPoint>& Pts = Seg->GetPoints();PathPoints.Add(ToWorld.TransformPosition(Pts[i].Center));
    • 실행 원리: 큼직한 제어점(ControlPoint) 사이에는 곡선을 부드럽게 만드는 수많은 보간 점(InterpPoint)들이 들어있습니다. 이 점들을 하나씩 꺼내어 차량이 밟을 수 있는 실제 월드 좌표로 변환(TransformPosition)한 뒤 배열에 차곡차곡 담습니다.
  • 3. 중복 방지 및 역방향 연결 보정
    • “점이 겹치거나 에디터에서 도로를 거꾸로 이은 경우를 바로잡습니다.”
    • ➡️ const bool bReversed = ... / const bool bSkipFirst = PathPoints.Num() > 0;
    • 실행 원리:
      1. 에디터에서 개발자가 길을 역방향으로 그렸을 경우(bReversed == true), 점들을 배열의 뒤에서부터 거꾸로 읽어와 올바른 주행 방향을 맞춰줍니다.
      2. 두 길이 만나는 이음새 부분은 점이 겹쳐서 차가 덜컹거릴 수 있으므로, 두 번째 길부터는 맨 첫 번째 점을 무시(bSkipFirst)하고 연결합니다.
  • 4. 정방향 탐색 종료 조건 (루프 탈출)
    • 종료 조건 A (도로의 끝): if (!Seg) break;
      • 앞으로 계속 가다가 더 이상 안 가본 연결된 길이 없다면, 도로의 맨 끝(도착 지점)에 도달한 것이므로 수집을 무사히 마칩니다.
    • 종료 조건 B (한 바퀴 완주): if (NextCP == StartCP) { bClosedLoop = true; break; }
      • 앞으로 계속 갔는데 맨 처음 출발했던 진짜 시작점(StartCP)에 도달했을 때입니다. 순환로를 완벽하게 한 바퀴 돌았다는 뜻이므로, bClosedLooptrue로 켜서 자율주행 시스템에 “이 길은 무한 루프 트랙이야!”라고 알려주고 수집을 종료합니다.
역방향으로 갔다가 다시 정방향을 하는 이유

차량과 가장 가까운 점(NearestCP)은 보통 도로의 ‘중간’ 어딘가에 있습니다. 도로의 전체적인 모양을 파악하고 올바른 주행 순서를 잡으려면 무조건 도로의 처음부터 순서대로 점을 모아야 합니다.
따라서 역방향으로 계속 거슬러 올라가 도로의 ‘시작점’을 찾은 뒤, 거기서부터 다시 정방향으로 탐색하면 차량이 밟고 갈 ‘경로점들을 수집하는 것입니다.

주행 가동: 경로 보정이 끝나면 가장 가까운 점에서부터 주행을 시작하도록 인덱스를 맞추고, Tick을 켜서(SetComponentTickEnabled) 자율주행 시스템을 가동합니다.


3. ResampleCatmullRom (경로 곡선화 및 스무딩)

  • 의미: 에디터에서 대충 찍어 각져 있는 도로의 점들을 수학 공식(Catmull-Rom)을 이용해 아주 부드러운 곡선으로 만들어주는 기능입니다.
  • 기능: ResampleSpacing 간격마다 점을 촘촘하게 새로 찍어냅니다. 이를 통해 자율주행 차량이 로봇처럼 뚝뚝 끊기며 조향하지 않고, 사람처럼 부드럽게 핸들을 꺾을 수 있도록 주행 경로의 품질을 높입니다.


4. GetPointAhead (전방 목표 지점 탐색 / 시선 처리)

  • 기능: 현재 차의 위치에서 Distance만큼 앞으로 더 나아갔을 때, 도로상의 어느 지점에 도달할지(미래 목표 좌표)와 그곳의 방향(OutDir)을 계산합니다.
  • 역할 (Look-ahead): 실제 운전자가 바로 앞 범퍼만 보고 운전하지 않고 멀리 시선을 두는 것과 같습니다. 차가 현재 위치만 보고 핸들을 꺾으면 늦기 때문에, 미래의 목표점을 미리 내다보고 조향하기 위한 ‘자율주행의 눈’ 역할을 합니다.


5. 곡률 및 안전 속도 계산 함수

  • EstimateCurvature (곡률 계산):
    • 앞쪽의 방향 벡터(D1)와 조금 더 앞쪽의 방향 벡터(D2)를 내적(Dot Product)합니다.
    • 두 벡터의 각도 차이를 통해 “앞으로 다가올 도로가 얼마나 급격하게 꺾여 있는지(곡률)”를 수치화합니다.
  • ComputeCurveSpeedLimit (커브길 감속 한계 계산):
    • 방금 구한 곡률과 타이어의 측면 마찰력(LateralFriction), 중력 가속도를 조합합니다.
    • “이 커브를 돌 때 원심력에 의해 차가 밖으로 튕겨 나가지 않는 최대 안전 속도”를 물리 공식($V = \sqrt{\mu g R}$)으로 계산해 냅니다.


6. TickComponent (실시간 조향 및 속도 제어)

매 프레임마다 실행되며, 차량의 핸들(DoSteering)과 페달(DoThrottle/DoBrake)을 직접 조작하는 최종 관문입니다.

주행 상태 업데이트

  • 차량이 현재 목표점을 지나쳤는지 확인(DotProduct 사용)하고, 지나쳤다면 다음 점을 향하도록 CurrentPointIndex를 증가시킵니다.
  • 순환로(루프)가 아닌데 도로의 끝에 도달했다면, 엑셀을 떼고 브레이크를 밟아 차를 세웁니다.

조향 제어 (Steering 로직)

복합적인 오차 값을 계산하여 완벽한 핸들링 값을 도출합니다.

  • PosDelta: 차의 정면 방향과 ‘시선을 둔 목표점’ 사이의 각도 오차.
  • HdgDelta: 커브길에 진입하기 전, 미리 도로가 휘어지는 방향으로 핸들을 꺾기 위한 예측 각도 오차.
  • CrossErr (Crosstrack Error): 차가 도로의 중앙선(Centerline)에서 좌우로 얼마나 빗겨나 있는지(차선 이탈)를 계산한 거리 값.
  • 결과 적용: 위 세 가지 오차를 혼합(Blend)하여 최종 핸들 꺾기 비율(Steer값, -1.0 ~ 1.0)을 계산하고 OwnerPawn->DoSteering(Steer)로 명령을 내립니다.

속도 제어 (Speed 로직)

  • 현재 위치의 곡률과 다가올 전방의 곡률을 계산하여 더 낮은 쪽의 목표 안전 속도(SpeedLimit)를 채택합니다.
  • 현재 내 차의 속도(Velocity)와 목표 안전 속도를 비교합니다.
    • 속도를 높여야 하면 엑셀(DoThrottle)을 밟습니다.
    • 커브길이라 감속해야 하면 브레이크(DoBrake)를 밟아 부드럽게 속도를 줄입니다.




깃포크 사용법

Stash (스태시)

Stash는 Git을 사용하면서 유용하게 쓰이는 ‘임시 보관함’ 기능입니다. 작업 중인 코드를 Commit하기 애매할 때 사용할 수 있습니다.


Stash가 필요한 상황

  • 상황: 열심히 코드를 수정하고 있었지만, 아직 미완성이라 Commit(저장)을 하기엔 애매한 상태입니다.
  • 문제 발생: 갑자기 팀원이 “급하게 고쳐야 할 치명적인 버그가 있어요! 당장 다른 브랜치(Branch)로 넘어가서 확인해 주세요!”라고 요청합니다.
  • 딜레마: 다른 브랜치로 넘어가려면 현재 수정 중인 코드를 어떻게든 처리해야 합니다.
    1. 미완성 상태로 억지로 Commit 한다. ➡️ 나중에 Git 기록이 지저분해져서 싫음.
    2. 수정한 걸 다 지우고 넘어간다. ➡️ 내 피 같은 코드가 날아감.
  • 해결책: 이때 바로 Stash를 사용합니다!

Stash의 핵심 역할 및 흐름

  1. 임시 보관 (Stash) Stash 버튼을 누르면, 현재 수정 중이던 미완성 코드들이 ‘임시 보관함’에 안전하게 쏙 들어갑니다. 내 작업 폴더는 수정을 시작하기 전의 아주 깨끗한 상태로 돌아갑니다.
  2. 다른 작업 수행 이제 홀가분하고 마음 편하게 다른 브랜치로 넘어가서 급한 버그를 고치고 작업을 완료합니다.
  3. 다시 꺼내오기 (Apply / Pop) 급한 일을 다 처리하고 원래 하던 작업 브랜치로 돌아온 뒤, 임시 보관함에 넣어뒀던 코드를 다시 꺼내옵니다. 아까 작업하던 상태 그대로 코드가 완벽하게 복구되어 바로 이어서 작업할 수 있습니다.

Git Fork에서 Stash 사용 및 확인하기

Fork 프로그램 내에서 Stash 기능은 크게 두 곳에서 제어할 수 있습니다.

  • 상단 툴바 (보관할 때): 상단 Fetch, Pull, Push 버튼 바로 옆에 있는 상자 모양의 Stash 버튼을 누릅니다. 현재 작업 중인 내용을 보관할 때 사용합니다.
  • 좌측 패널 하단 (꺼낼 때/확인할 때): Branches, Remotes 등이 있는 목록 맨 아래에 Stashes 메뉴가 있습니다. 방금 임시 보관한 내역들이 이곳에 쌓이게 되며, 나중에 이 목록에서 우클릭하여 원하는 내역을 다시 꺼내올 수 있습니다.

새로 만든 파일까지 남김없이 전부 임시 보관함으로 저장하고 작업 폴더를 전으로 되돌리고 싶다면 이 체크박스를 체크(✔️)

This post is licensed under CC BY 4.0 by the author.