1. 개발 환경 설정 및 C++ 프로젝트 컴파일
1.1 C++ 클래스 생성 및 명명 규칙
- 툴 메뉴에서 새 C++ 클래스를 생성할 수 있으며 월드에 배치할 움직이는 플랫폼을 만들기 위해 부모 클래스로 액터를 선택한다
- 언리얼 엔진의 명명 규칙에 따라 액터 클래스 이름 앞에는 대문자 A가 자동으로 붙는다
- 클래스 이름을 지을 때는 공백 없이 각 단어의 첫 글자를 대문자로 적는 파스칼 케이스 방식을 반드시 준수해야 한다
2. 변수 시스템 및 UPROPERTY 활용
2.1 헤더 파일과 소스 파일의 역할
- 프로젝트의 Source 폴더를 확인하면 C++ 클래스가 헤더 파일과 소스 파일로 나뉘어 있는 것을 알 수 있다
- 헤더 파일은 변수와 함수를 선언하는 설계도 역할을 하며 블루프린트의 변수 패널과 비슷한 기능을 한다
- 소스 파일은 실제 로직을 구현하는 실행부 역할을 담당하며 블루프린트의 이벤트 그래프에 해당한다
2.2 UPROPERTY 매크로와 에디터 노출
- C++에서 선언한 변수를 언리얼 에디터 인스펙터 창에서 확인하고 수정하려면 변수 윗줄에 UPROPERTY 매크로를 입력해야 한다
- 괄호 안에 EditAnywhere를 넣으면 에디터에서 값을 자유롭게 변경할 수 있고 VisibleAnywhere를 넣으면 읽기 전용으로 설정된다
- Category 속성을 추가하여 문자열을 지정하면 디테일 패널에서 변수들을 직관적인 그룹으로 묶어서 관리할 수 있다
2.3 핵심 데이터 타입 정의
- 언리얼 C++에서 숫자를 다룰 때 소수점이 없는 정수는 int32를 사용하고 소수점이 있는 실수는 float 타입을 사용한다
- 참과 거짓을 판별하는 논리형 데이터는 bool 타입을 사용한다
- 3차원 공간의 위치나 크기를 나타낼 때는 내부적으로 X Y Z 값을 가지는 FVector 구조체를 활용한다
3. 액터 이동 로직 및 시간 제어
3.1 BeginPlay 및 Tick 함수의 이해
- BeginPlay 함수는 게임이 시작될 때 단 한 번만 실행되므로 변수의 기본값을 초기화하거나 초기 위치를 설정할 때 유용하다
- Tick 함수는 게임 루프에 따라 매 프레임마다 호출되므로 플랫폼의 실시간 이동이나 회전 상태를 지속적으로 업데이트할 때 사용한다
- 함수 내부에서 선언한 지역 변수는 해당 함수가 종료되면 메모리에서 사라지므로 상태를 유지하려면 헤더 파일에 멤버 변수로 선언해야 한다
3.2 프레임 독립적 이동과 DeltaTime
- 컴퓨터 사양에 따라 프레임률이 달라지기 때문에 플랫폼의 이동 속도에 반드시 DeltaTime을 곱해주어야 한다
- DeltaTime은 이전 프레임에서 현재 프레임까지 걸린 시간을 의미하며 이를 통해 모든 기기에서 동일한 속도로 게임이 작동하게 만든다
- 이동 공식은 현재 위치에 속도와 델타타임을 곱한 값을 더하는 방식이며 연산자 우선순위 원칙에 따라 곱셈이 먼저 수행된다
3.3 벡터 연산 및 함수 호출
- GetActorLocation 함수를 호출하여 액터의 현재 월드 좌표를 FVector 형태로 가져온다
- 구조체 내부의 특정 축 값에 접근할 때는 점 연산자를 사용한다
- 계산이 끝난 새로운 위치 값은 SetActorLocation 함수의 인수로 전달하여 액터의 실제 위치를 업데이트한다
4. 제어문과 거리 계산 및 최적화
4.1 조건문 활용 및 왕복 로직
- 플랫폼이 지정한 범위를 벗어나지 않고 왕복하도록 if 문을 활용하여 이동 거리를 제어한다
- 이동한 거리가 설정한 최대 이동 거리보다 커지는 순간 조건문이 참이 되어 내부 코드를 실행한다
- 플랫폼의 속도 변수에 마이너스 기호를 붙여 반대 방향으로 속도를 반전시키면 앞뒤로 움직이는 메커니즘이 완성된다
4.2 범위 지정 연산자와 FVector의 Dist 함수
- 시작 지점과 현재 위치 사이의 정확한 거리를 계산하기 위해 FVector 클래스의 Dist 함수를 사용한다
- 클래스 자체에 포함된 정적 함수를 호출할 때는 더블 콜론 형태의 범위 지정 연산자를 사용한다
- 플랫폼이 방향을 바꿀 때 시작 위치를 현재 위치로 새롭게 갱신해주어야 이동 거리 계산 시 오차가 누적되지 않는다
4.3 오버슈트 방지와 getSafeNormal
- 빠른 속도로 이동하는 플랫폼은 프레임 간격 탓에 목표 지점을 훌쩍 지나치는 오버슈트 현상을 겪게 된다
- 이를 방지하기 위해 벡터의 멤버 함수인 getSafeNormal을 호출하여 크기가 1인 순수한 이동 방향 벡터를 얻어낸다
- 이 방향에 정확한 이동 거리를 곱해 목표 지점을 산출하고 액터의 위치를 해당 지점에 강제로 고정시켜 오차를 없앤다
5. 함수 구조화 및 객체 지향 설계
5.1 멤버 함수 분리와 캡슐화
- Tick 함수 내부에 코드가 너무 길어지면 MovePlatform이나 RotatePlatform 같은 별도의 커스텀 함수를 만들어 로직을 분리한다
- 헤더 파일에서 키워드 private을 선언하고 그 아래에 함수를 배치하면 클래스 외부에서의 접근을 완벽히 차단할 수 있다
- 이러한 방식을 통해 데이터 변형을 막고 객체 지향 프로그래밍의 핵심인 캡슐화를 실천한다
5.2 Const 키워드와 반환문 적용
- GetDistanceMoved처럼 클래스의 내부 변수를 수정하지 않고 단순히 계산된 값만 읽어오는 함수는 괄호 뒤에 const 키워드를 붙인다
- 상수화된 함수 내에서는 변수 수정이 불가능해지므로 코드의 안정성이 크게 향상된다
- 함수 실행 결과를 외부로 전달할 때는 반환 타입을 명시하고 코드 마지막에 return 키워드를 사용하여 값을 반환한다
6. 디버깅 및 출력 로그 시스템
6.1 UE_LOG 매크로 사용법
- 에디터 화면 하단의 출력 로그 창에 프로그램 내부 상태를 기록하려면 UE_LOG 매크로를 사용한다
- 첫 번째 인수로는 LogTemp 카테고리를 넣고 두 번째 인수로는 시급성에 따라 Display Warning Error 중 하나의 로그 수준을 선택한다
- 출력할 문자열 내부에 부동 소수점 변수 값을 포함하고 싶다면 C언어에서 유래한 서식 지정자인 %f를 텍스트에 삽입한다
6.2 FString과 동적 문자열 출력
- 언리얼 C++에서 문자열 데이터를 다룰 때는 FString 타입을 사용하며 로그에 출력할 때는 서식 지정자 %s를 사용한다
- UE_LOG 함수는 FString 변수를 그대로 인식하지 못하므로 변수 이름 앞에 반드시 별표 연산자를 붙여 변환 연산자로 처리해야 한다
- GetName 함수를 호출하면 액터의 고유 이름을 FString으로 가져올 수 있어 수많은 플랫폼 중 어떤 객체에서 문제가 발생했는지 쉽게 식별할 수 있다
7. 블루프린트 연동 및 물리 충돌 해결
7.1 하위 블루프린트 클래스 생성
- C++로 작성한 코드를 시각적으로 꾸미기 위해 C++ 클래스를 우클릭하고 블루프린트 하위 클래스를 생성한다
- 생성된 블루프린트 에디터에서 다양한 스태틱 메시 컴포넌트를 추가하여 돌기둥이나 회전판 같은 멋진 외형을 입힌다
- C++에서 UPROPERTY로 노출시킨 변수들을 디테일 패널에서 다르게 설정하여 하나의 코드로 여러 변형 플랫폼을 만들어 낸다
7.2 캐릭터 충돌 보정 기능
- 움직이는 플랫폼이나 푸셔가 캐릭터를 밀어낼 때 캐릭터가 부드럽게 밀리지 않고 겹치거나 튕기는 글리치 현상이 발생할 수 있다
- 캐릭터 코드가 가만히 서 있을 때는 물리 충돌을 능동적으로 계산하지 않기 때문이다
- 캐릭터 블루프린트의 틱 이벤트에서 MoveUpdatedComponent 함수를 호출하여 캐릭터를 미세하게 이동시켰다가 복구하는 스윕을 강제하면 충돌이 정상적으로 감지된다
8. 게임모드 및 회전 메커니즘
8.1 게임모드 클래스와 스폰 규칙
- 게임모드 클래스는 레벨에 어떤 플레이어를 스폰하고 어떤 게임 규칙을 적용할지 관리하는 최상위 관리자이다
- 새로운 게임 모드 베이스 블루프린트를 생성한 뒤 클래스 디폴트의 Default Pawn Class를 우리가 만든 3인칭 캐릭터로 변경한다
- 이후 프로젝트 세팅에서 기본 게임 모드를 방금 만든 클래스로 설정하면 뷰포트 아무 곳에서나 우클릭하여 여기서 플레이 기능으로 쉽게 테스트할 수 있다
8.2 FRotator와 회전 플랫폼 구현
- 플랫폼을 회전시키려면 FVector 대신 회전 정보를 담는 특수 구조체인 FRotator 타입을 사용해야 한다
- SetActorRotation 함수로 회전값을 직접 덮어씌우면 특정 각도에서 회전축이 꼬이는 예외 상황이 발생할 수 있다
- 대신 AddActorLocalRotation 함수를 호출하고 델타타임이 곱해진 회전 속도를 더해주면 엔진이 알아서 복잡한 수학적 문제를 해결하며 부드럽게 회전시킨다