
이번에는 액터를 이동시키는 방법에 대해서 알아볼 것이다
그걸 위해서는 먼저 언리얼엔진에서 게임을 플레이 할 때 게임이 어떻게 진행되는지를 알아야 한다
https://docs.unrealengine.com/4.27/ko/InteractiveExperiences/Framework/GameFlow/
게임 흐름 개요
엔진 시작 및 게임이나 에디터에서 플레이 세션을 실행시키는 프로세스입니다.
docs.unrealengine.com
언리얼엔진에서 게임의 흐름은 초기화 단계, 게임 루프 단계, 종료 단계로 크게 세 단계로 나누어 설명할 수 있다.
1. 초기화 단계(Initialization Phase): 이 단계는 게임이 시작될 때, 즉 프로그램을 실행했을 때 발생한다
- 엔진 및 서브 시스템 시작(Engine and Subsystem Startup): 언리얼 엔진의 핵심 구성 요소와 서브 시스템이 초기화되며, 렌더러, 오디오 시스템, 입력 관리자, 메모리 관리자 등이 설정된다
- 게임 데이터 로딩(Game Data Loading): 게임에 필요한 데이터, 애셋, 설정을 로드한다. 이는 텍스쳐, 메시, 애니메이션, 레벨 데이터 등이 포함될 수 있다
- 레벨 및 월드 설정(Level and World Setup): 게임에 포함된 레벨이 로드되며, 게임 월드가 구성된다. 이 과정에서 스태틱 메시, 액터, 조명 등이 배치된다
- 액터 초기화(Actors Initialization): 레벨에 배치된 각 액터의 BeginPlay 함수가 호출되어, 초기 위치 설정, 이벤트 구독, 초기 상태 설정 등의 작업이 수행된다
2. 게임 루프 단계(Game Loop Phase): 이 단계는 게임이 실행되는 동안 지속적으로 반복되는 단계이다
- 이벤트 처리(Event Processing): 게임루프의 시작 부분에는 플레이어의 입력(키보드, 마우스, 게임패드 등)과 시스템 이벤트(창 크기 변경, 포커스 변경 등)를 감지하고 처리한다.
- 월드 상태 업데이트(World State Update): 게임의 각 프레임마다 모든 게임 액터의 Tick( )함수가 호출되어, 게임의 상태를 업데이트한다. AI 동작, 플레이어 상태 변화, 게임 오브젝트의 움직임 등이 이 단계에서 처리된다.
- 물리 및 충돌 처리(Physics and Collision Processing): 물리 엔진이 활성화된 객체들의 물리적 상호작용과 충돌을 계산한다. 이는 물체의 운동, 충돌 처리 등을 포함한다.
- 게임 로직 실행(Game Logic Execution): 스크립트, 이벤트 트리거 등 게임 특정 로직이 실행되고, 이에 따라 게임 상태가 변할 수 있다. 예를 들어, 플레이어의 생명 값이 0이 되면 게임 오버 상태로 전환된다
- AI 업데이트(AI Update): 게임 월드 내의 AI 액터들이 판단하고, 행동을 결정하며, 경로를 탐색한다
- 렌더링(Rendering): 게임의 현재 상태를 바탕으로 그래픽을 렌더링하며, 화면에 출력될 이미지를 생성한다. 그리고 모든 시각적 요소가 프레임 버퍼에 그려지며, 이후 화면에 표시된다. 이 과정에는 실시간 조명 계산(루멘 등), 쉐이딩, 포스트 프로세싱 효과 등을 포함한다
- 오디오 처리(Audio Processing): 사운드 효과와 음악이 재생된다
- 네트워크 처리(Network Processing): 네트워크 게임의 경우, 다른 플레이어와의 데이터 동기화와 통신이 이루어진다
- 프레임 종료(End of Frame): 모든 렌더링 작업이 완료되고, 화면에 그려진 내용이 표시된 후, 게임 루프는 한 프레임을 마무리한다. 현재 프레임의 모든 처리가 완료되면, 다음 프레임을 위한 준비가 이루어진다
실제 게임을 플레이 하면 굉장히 부드럽게 진행되는 것처럼 보이는데, 사실 수많은 프레임(이미지)들이 한 프레임씩 처리가 되면서 우리가 연속적인 프레임으로 게임을 할 수 있는 것이다.
이 과정이 바로 '게임 루프(Game Loop)'이고, 게임 루프가 한 번 완전히 돌 때마다 한 프레임(Frame)의 처리가 완료된다. 이 루프는 게임의 동적인 요소들이 실시간으로 업데이트 되고, 사용자의 입력을 처리하며, 게임의 논리와 렌더링을 담당하는 핵심적인 메커니즘이다
3. 종료 단계(Termination Phase): 이 단계는 게임이 종료될 때 발생한다. 게임 및 엔진이 사용한 리소스를 해제하고 종료한다
- 게임 상태 저장(Game State Saving): 필요한 경우, 게임의 진행 상태나 플레이어의 성과를 저장한다.
- 리소스 정리(Resource Cleanup): 메모리에서 로드된 모든 리소스(애셋, 텍스쳐, 사운드 등)를 해제한다.
- 액터 정리(Actors Cleanup): 게임 월드의 모든 액터들이 파괴되며, 각 액터의 EndPlay( )함수가 호출된다.
- 엔진 종료(Engine Shutdown): 엔진과 관련된 모든 서브시스템이 종료되며, 마지막 정리 작업이 수행된다.
우리가 여기서 주의깊게 봐야할 부분이 게임 루프 단계의 '월드 상태 업데이트(World State Update)' 부분이다.
언리얼5-8 C++ <Cpp 클래스 생성/기본 이벤트>에서 봤듯이 Tick( )함수는 모든 프레임에 한번씩 호출되는데, 이 함수를 이용해서 매 프레임 마다 게임이 업데이트 되도록 작업을 할 수 있다
그렇다면 액터를 움직이려면 어떻게 해야할까?
사실 정답은 이미 알려져 있다. 바로 Tick( )함수를 이용해서 매초 프레임 마다 액터의 위치만 변경시키면 되는 것이다
먼저 헤더파일에서 FVector 구조체를 이용하여 MyVector 변수를 만들어주고, 변수에 원하는 위치값을 할당시켜 준 다음, UPROPERTY 매크로를 넣고 이 프로퍼티를 에디터에서 수정할 수 있도록 만들어준다 [본인은 편하게 (0,0,0)을 할당시켜주었다]
이제 위치를 변경하면 되는데, 액터의 위치를 변경시키는 함수는 언리얼5-13 C++ <함수와 UFUNCTION>에서 사용했던 SetActorLocation( ) 함수를 이용하면 된다
소스파일로 가서, Tick( )함수 구현부에 SetActorLocation(MyVector); 코드를 작성해준다. 이렇게 하면 MyVector에 할당되어있는 위치인 (0,0,0)으로 MovingObject C++ 클래스가 한 프레임마다 위치해 있을 것이다.
하지만 이것으로 끝내면 매 프레임마다 MovingObject는 계속해서 같은 장소에 위치해 있으므로 제자리에 있는 것처럼 보일 것이다
그래서 MyVector에 값을 더해줘서 매 프레임 마다 위치를 이동 시켜 주어야 한다
MyVector의 X요소에 1을 더해주고 그 값을 다시 MyVector.X에 넣도록 해준다 [프로그래밍을 했던 사람들은 MyVector.X += 1;도 동일하게 동작한다는 것을 알 것이다]
코드를 모두 작성하면 저장하고 컴파일해준다
MovingObject C++ 클래스를 월드에 배치하고 나서 형태를 주기 위해 [+Add] 버튼을 선택하고 Cube를 선택해준다
그리고 게임을 플레이 시켰을 때 (0,0,0) 위치에서 X값이 1씩 증가하여 액터가 움직이는 모습을 볼 수 있다
물론 X방향 뿐만 아니라 Y 방향으로 이동이 가능하고,
Z축 방향으로도 이동이 가능하고 XY축, YZ축, ZX축, 특정 요소 없이 X,Y,Z 축 동시에 이동하는 것도 가능하다
이 액터가 시작하는 위치(MyVector값)를 UPROPERTY(EditAnywhere)로 수정할 수 있게 만들어놓았기 때문에 여기서 위치를 지정하고 플레이 해도 되지만, 만약 이 MovingObject C++ 클래스를 여러개 생성해서 사용한다고 가정하면, 일일이 다 값을 넣어주어야 한다
이 문제를 해결하려면 개발자가 액터를 어디에 두던지 그 위치에서부터 액터를 움직이게 하면 될 것이다
그래서 사용되는 함수가 바로 GetActorLocation( ) 함수이다.
먼저 헤더파일과 소스파일에서 액터의 위치를 정해주었던 변수인 MyVector 부분을 다 지워주고,
GetActorLocation( ) 함수로 현재 위치를 가져와서 CurrentLocation 변수에 할당한 다음, SetActorLocation( )함수에 CurrentLocation을 넣어준다. 일단 이렇게 하면 액터의 현재 위치를 가져와서 다시 설정하므로 아무런 움직임이 없을 것이다.
이때 FVector CurrentLocation = GetActorLocation( ); 문장을 헤더파일에서 선언하고 사용해도 되지 않을까? 라고 생각할 수 있는데,
만약 FVector CurrentLocation = GetActorLocation( ); 문장이 헤더파일에서 선언되었다고 가정해보자.
이것은 게임 흐름에서 '초기화' 단계에서 발생하고, 게임의 객체의 생성과 설정과 관련된 작업을 처리하는 과정에서 아직 게임 월드에 액터가 배치되지 않았는 것을 의미한다.
그래서 GetActorLocation( )은 액터의 위치가 아직 설정되지 않았으므로 기본값인 (0,0,0)을 반환하게 되는 것이다. 이후 게임 루프가 '월드 상태 업데이트' 단계로 넘어가면서 Tick 함수가 호출되지만, CurrentLocation은 이미 초기화 단계에서 값을 할당 받았고, 이후에 업데이트 되지 않은 것이다.
따라서 Tick( )함수 내에서 CurrentLocation값을 변화시켜도, 이는 항상 (0,0,0) 위치에서부터 값을 변경한 결과를 반영하게 되므로, 이로 인해 액터는 월드의 (0,0,0) 위치에서 시작하게 된다
그렇기 때문에 액터의 현재 위치를 정확하게 반영하기 위해서는 GetActorLocation( )을 '월드 상태 업데이트' 단계, 즉 Tick( )함수 내애서 호출해야 한다. 이렇게 하면 Tick( )함수가 호출될 때마다 액터의 실시간 위치를 가져와서, 그 위치에서 값을 더한 내용을 적용할 수 있다.
이제 액터를 움직이기 위해, 구조체의 멤버를 이용해서 편한대로 액터를 움직이는 코드를 작성해보자
이렇게 하면 먼저 배치된 액터의 위치를 가져와서 CurrentLocation에 저장하고, 그 값의 멤버를 이용하여 위치를 옮겨준 뒤, SetActorLocation( )로 변경된 위치를 Set(설정)하게 된다
코드 작성이 끝나면 모두 저장하고 컴파일을 진행해준다
그리고 게임을 원하는 위치에 배치 후 플레이 해보면, 그 위치에서부터 액터가 움직이기 시작하는 것을 확인할 수 있다
지금까지 SetActorLocation( )함수와 GetActorLocation( )함수를 이용하여 벡터의 위치를 중심적으로 다루었다.
이제 액터가 움직이는 속도를 스크립트 에디터에서 따로 지정하지 않고 디테일 패널에서 한번에 수정할 수 있도록 만들어보자
소스파일에서 CurrentLocation의 멤버 변수에 값을 더하는 코드들을 지워주고,
헤더파일에서 액터의 속도를 담당할 ObjectVelocity 변수를 만들어준 다음, 위치는 임의대로 (50, 0, 0)을 넣어준다.
또한 에디터에서 수정할 수 있도록 UPROPERTY 매크로와 EditAnywhere, Category = "ActorVelocity" 지정자를 넣어주어 ActorVelocity 카테고리에 생성해준다
그리고 다시 소스파일로 가서 앞서 했던 방법처럼 CurrentLocation에 ObjectVelocity(50, 0, 0)가 더해질 수 있도록 코드를 작성한다
이렇게 하면 액터가 배치된 위치를 가져와서 X에는 50이 더해지게 되고, Y는 0, Z도 0 값이 각각 더해지게 될 것이다.
코드를 모두 저장후 컴파일 한 뒤,
디테일 패널을 보면 이렇게 Actor Velocity 카테고리가 생성되고 그 밑에 액터의 속도를 결정할 FVector 타입이 있는 것을 볼 수 있다
이제 액터를 원하는 위치에 배치하고 게임을 플레이 해 보면 액터가 굉장히 빠르게 움직이는 모습을 볼 수 있다.
이것은 CurrentLocation의 특정 요소를 사용해서 움직일 때도 마찬가지지만, 오브젝트가 프레임마다 50유닛[1cm = 1유닛]마다 더해지고 있기 때문이다.
이것은 컴퓨터마다 다를 수 있는데,
만약 FPS가 100인(초당 100프레임) 컴퓨터로 실행하면 100프레임 * 50유닛 = 5000cm 즉, 초당 50m를 이동하고
FPS가 10인(초당 10프레임) 컴퓨터로 실행한다면 10프레임 * 50유닛 = 500cm 즉, 초당 5m를 이동하게 되는 것이다
이렇게 컴퓨터마다 달리 실행이 된다면 게임을 플레이 하는데 있어서 굉장히 큰 문제가 되기 때문에, 프레임마다 움직이는 것이 아니라 매 초마다 움직이게 만들어야 한다.
그래서 사용되는 것이 바로 Tick( )함수의 DeltaTime 매개변수이다.
이때까지는 그저 Tick( )함수가 매 프레임 호출된다는 것 말고 다른 것들을 무시했었는데, 이 DeltaTime을 사용하면 게임 프레임률(FPS)을 각 컴퓨터 마다 다르게 할 수 있다.
DeltaTime은 각 프레임이 지속되는 시간을 구할 수 있는데, FPS(초당 프레임)가 100인 컴퓨터의 DeltaTime(한 프레임 지속시간)은 0.01초, FPS(초당 프레임)가 10인 컴퓨터의 DeltaTime(한 프레임 지속시간)은 0.1초가 된다.
그래서 프레임 당 N유닛 만큼 움직인다고 하면 N * FPS * DeltaTime = N * 1이 되므로, 초당 N 만큼의 이동을 할 수 있는 것이다
그래서 ObjectVelocity(50 * FPS)값에 DeltaTime을 곱하게 되면 초당 50유닛 만큼 이동하게 된다
[연산자 우선순위로 인해 *가 먼저 계산되므로, 괄호는 안넣어도 상관이 없다]
코드 작성이 완료되면 모두 저장하고 컴파일 한다
게임을 실행시키고 F8 키를 눌러서 위치를 확인해보면, 프레임당 50씩 이동하는 이전의 모습과는 달리, 초당 50씩 이동하는 모습을 확인할 수 있다. DeltaTime을 곱하여 독립적인 프레임 레이트를 완성한 것이다. [단축키 Alt + 'S'를 이용하여 바로 시뮬레이션이 가능하다]
이제 마지막으로 플레이가 시작된 후, 시작 지점으로부터 MovingObject가 얼마나 이동했는지를 계산하는 방법을 알아보자
그걸 위해서는 우선 시작 위치를 알아야 한다
헤더 파일로 가서 시작 위치가 될 FVector 타입의 StartLocation 변수와, 이동거리를 저장할 float 타입의 MovedDistance 변수를 선언해준다
MovedDistance 변수에 값을 볼 수는 있지만 수정할 수 없도록(읽기 전용), UPROPERTY 매크로와 VisibleAnywhere 지정자를 넣어준다. StartLocation 변수는 현재 위치만 저장할 것이기 때문에 굳이 매크로를 넣을 필요가 없다.
[이때 MovedDistance에 -1을 넣는 이유는 일반적으로 프로그래밍에서 "아직 유효한 값으로 설정되지 않음"을 나타내기 위해서 이다. 만약 프로그램을 실행하고 변수가 여전히 -1이라면, 프로그램이 변수를 적절히 초기화 하지 않았음을 알 수 있다]
그리고 소스파일의 BeginPlay( )함수로 가서, StartLocation 변수에 현재 위치를 가져와서 할당해준다. 이렇게 하면 게임을 플레이 시 StartLocation 변수에 액터의 시작점을 저장할 수 있을 것이다
그 밑의 Tick( ) 함수에서는 MovedDistance 변수에 FVector::Dist(StartLocation, CurrentLocation) 값을 넣어준다
여기서 범위 지정 연산자(::)를 사용하여 구조체 FVector안에 선언된 정적 메소드인 Dist( )함수를 호출했는데,
범위 지정 연산자(::)은 특정 네임스페이스, 클래스, 구조체, 열거형 등의 범위에 있는 변수, 함수, 데이터 타입 등을 지정하는 데 사용되고, 이미 MovingObject 클래스에 속하는 생성자 함수나 BeginPlay함수, Tick 함수에도 사용되어 있는 연산자이다
이렇게 가져온 Dist( )함수는 두 개의 FVector 타입을 인수로 가져와서 두 벡터 사이의 직선거리를 float 타입으로 반환하는 역할을 한다
코드를 모두 작성하면 저장한 뒤 컴파일을 진행한다
그리고 디테일 패널을 보면 Distance 카테고리로 Moved Distance가 생성되어있고 기본 값이 -1로 되어있는 것을 볼 수 있다
이제 각자가 원하는 방향으로 움직이게 할 수 있도록 Object Velocity 값을 조정해준다
그리고 액터를 배치하고 플레이 버튼을 누른 다음, F8키를 이용하여 확인해보면 최초로 위치한 장소로부터 움직인 거리를 구할 수 있다
VR게임 개발을 위한 언리얼엔진/C++ 공부한 내용 끄적이기...
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!