9. PhysX - Character Controllers - 1
캐릭터 컨트롤러(CCT)
- 캐릭터 컨트롤러 SDK는 PhysXExtensions와 유사한 방식으로 PhysX 위에 구축된 외부 구성 요소이다.
- CCT는 다양한 방식으로 구현될 수 있고, CCT 모듈의 PhysX 구현은 그 중 하나일 뿐이다.
- CCT는 게임마다 매우 특수하며, 게임마다 고유한 기능을 가질 수 있음
- 예를 들면 어떤 게임에선 바운딩 볼륨을 사용하고, 어떤 게임에선 역피라미드를 사용할 수 있다.
- PhysX에서 제공하는 CCT는 캐릭터 제어 및 상호작용과 같은 기본 기능을 제공한다.
- 이는 사용자에게 강력한 기초를 제공할 것이라고 한다.
PhysX CCT는 Kinematic 컨트롤러이다
- 일반적으로 캐릭터 컨트롤러는 kinematic 또는 dynamic일 수 있다.
- kinematic 컨트롤러는 입력 변위 벡터와 직접적으로 작동한다.
- dynamic 컨트롤러는 추가적으로 입력 속도 또는 힘으로 작용하게 된다.
Kinematic 컨트롤러의 주요 장점은 다음과 같은 Dynamic 컨트롤러의 문제점이 없다는 것
1. 연속적인 충돌 감지의 부재 : 터널링 효과를 유발
*터널링 효과란?
2. 캐릭터가 너무 빠른 경우 벽을 통과하는 현상
- 이에 따라 캐릭터의 최대 속도가 제한된다
(이전 포트폴리오 작업에서 이 경험으로 인해 중력으로 인한 추락 속도를 소폭 조정한 바 있음..)
3. 터널링이 발생하지 않더라도 캐릭터가 모서리로 밀려날 때 지글거리는 현상이 발생한다.
캐릭터 컨트롤러 생성
- 캐릭터 컨트롤러를 생성하기 위해서는 어플리케이션 어딘가에 컨트롤러 매니저를 생성해야 한다.
- 이 객체는 모든 컨트롤러를 추적하고, 같은 매니저의 캐릭터들이 서로 상호작용할 수 있게 한다.
PxScene* scene; // Previously created scene
PxControllerManager* manager = PxCreateControllerManager(*scene);
- 게임 내 각 캐릭터마다 하나의 컨트롤러를 만든다.
- 현재 작성 시점에서는 박스 (PxBoxController)와 캡슐 (PxCapsuleController)만 지원된다.
- 아래는 캡슐 컨트롤러는 만드는 방법이다.
PxCapsuleControllerDesc desc;
...
<fill the descriptor here>
...
PxController* c = manager->createController(desc);
- 이 관리자 클래스는 생성된 모든 컨트롤러를 추적합니다.
- 이런 컨트롤러는 다음 함수를 통해 언제든 검색할 수 있다.
PxU32 PxControllerManager::getNbControllers() const = 0;
PxController* PxControllerManager::getController(PxU32 index) = 0;
- 캐릭터 컨트롤러를 해제하려면 아래 해제 함수를 호출하면 된다.
void PxController::release() = 0;
- 생성된 모든 캐릭터 컨트롤러를 한 번에 해제하려면, 관리자 자체를 해제하면 됨
- 관리자를 계속 사용하려면 다음과 같은 코드를 사용하면 된다.
void PxControllerManager::purgeControllers() = 0;
Overlap 복구 모듈
- 캐릭터가 주변의 기하학적 물체와 겹쳐서 생성되어서는 안된다.
- 따라서 캐릭터를 생성하기 전 그 위치가 비어 있는지를 확인하기 위해서 여러 개의 PxScene overlap 함수를 사용할 수 있다.
- 기본적으로 CCT 모듈은 자체적으로 overlap을 확인하지는 않음
- static actor와 캐릭터가 겹쳐서 스폰되면 캐릭터가 지면을 뚫고 나가는 등의 문제가 생길 수 있다.
- 이 경우 overlap 복구 모듈을 사용하면 캐릭터의 위치를 자동으로 보정할 수 있다
- 또한 겹침의 정도가 용인가능한 수준이라면, 복구 모듈은 적절한 충돌 없는 위치로 캐릭터를 이동시킬 수 있다.
- Overlap 복구 모듈은 다음과 같은 경우에 유용하다
1. CCT가 직접 생성되거나 다른 객체로 순간이동될 때
2. CCT 알고리즘이 한정된 FPU 정확성으로 실패할 때
3. CCT의 Shape가 회전해서 주변 객체와 겹치는 경우 (Up Vector가 수정된 경우)
- Overlap 복구 모듈이 활성화되면, CCT가 주변 정적 actor간의 겹침을 해결하며,
캐릭터가 안전하게 이동할 수 있게 보장한다. (동적 actor는 무시된다)
- 다음 함수를 통해 overlap recovery 모듈을 활성화 또는 비활성화할 수 있다.
void PxControllerManager::setOverlapRecoveryModule(bool flag);
- 그러나 기본적으로 캐릭터 컨트롤러는 정밀한 스윕 테스트를 사용해서,
- 접촉 오프셋이 너무 작지 않은 경우 겹침을 방지할 수 있는 충분한 정확성을 가진다.
- 따라서 대부분의 경우 오버랩 복구 모듈이 필요하지 않다.....
Character Volume
AABB(Axis-Aligned Bounding Box)
- 중심 위치와 extents 벡터로 정의되는 AABB는 회전하지 않는다.
- 회전하는 경우 좁은 공간에서 갇히는 문제가 발생할 수 있기 때문이다.
Capsule (권장되는 기본 선택)
- 중심 위치와 수직 높이 및 반지름으로 정의되는 캡슐
- 높이는 캡슐 끝 두 구의 중심 간 거리이다.
- 캡슐은 계단 오르기와 같은 상황에서 더 나은 동작을 보인다.

- 캐릭터의 볼륨 주위에 작은 스킨이 유지되어, 캐릭터가 다른 형상에 닿을 때 발생할 수 있는 수치적 문제를 방지한다.
- 디버그 목적으로 캐릭터의 볼륨을 렌더링할 때, 이 스킨의 크기를 더하여 정확한 디버그 결과를 얻을 수 있다.
- 이 스킨은 PxControllerDesc::contactOffset에서 정의되며, 나중에 PxController::getContantOffset() 함수를 통해 사용 가능하다.
Volume Update
- 때로는 런타임 중 캐릭터의 볼륨 크기를 변경하는 것이 유용하다.
- 예를 들어 캐릭터가 앉는 경우, 볼륨의 높이를 줄이는 경우가 있다.
- 그 경우 아래 함수들을 이용하면 된다.
- 박스 컨트롤러의 경우
bool PxBoxController::setHalfHeight(PxF32 halfHeight) = 0;
bool PxBoxController::setHalfSideExtent(PxF32 halfSideExtent) = 0;
bool PxBoxController::setHalfForwardExtent(PxF32 halfForwardExtent) = 0;
- 캡슐 컨트롤러의 경우
bool PxCapsuleController::setRadius(PxF32 radius) = 0;
bool PxCapsuleController::setHeight(PxF32 height) = 0;
- 위 함수들을 사용하여 컨트롤러의 크기를 변경하면 실제로 위치가 변경되지는 않는다.
- 따라서 캐릭터가 땅에 서 있는 상태에서 캐릭터의 높이를 갑자기 줄인 후 위치를 업데이트하지 않으면,
캐릭터는 지면 위에 떠 있는 상태가 되어 중력에 의해 바닥에 떨어지게 될 것이다.
- 따라서 컨트롤러의 높이를 수정하고 바닥에 놓이게 하려면 컨트롤러의 높이와 위치를 모두 변경해야 하는데
아래 함수를 이용하면 이 작업을 자동으로 수행한다.
void PxController::resize(PxF32 height) = 0;

- Volume은 추가적인 테스트 없이 직접 수정되므로, 그 결과가 반영되었을 때, 근처 일부 지오메트리와 겹칠 수 있음
(캐릭터가 앉았다가 일어설 때 천장에 머리가 닿는 경우 일어날 수 없어야 함)
- 이런 목적으로 다양한 PxScene 오버랩 쿼리를 사용하는 것이 좋다.
bool PxScene::overlap(...) = 0;
Moving a Character Controller
- 캐릭터 컨트롤러를 이동시키는 CCT 알고리즘의 핵심은 실제로 캐릭터를 이동시키는 함수이다.
PxControllerCollisionFlags collisionFlags =
PxController::move(const PxVec3& disp, PxF32 minDist, PxF32 elapsedTime,
const PxControllerFilters& filters, const PxObstacleContext* obstacles=NULL);
disp
- disp는 현재 프레임의 이동 벡터(displacement vector) 이다.
- 일반적으로 중력으로 인한 수직 이동과, 캐릭터가 이동할 때의 측면 이동이 결합된 값
- 사용자는 캐릭터에 중력을 적용해야 한다.
minDist
- minDist는 재귀적 이동 알고리즘을 일찍 중지시키기 위한 최소 길이
- 남아 있는 이동 거리가 이 한계치 이하로 떨어질 때 조기에 이동 알고리즘을 중지시킨다.
elapsedTime
- 마지막 move 함수 호출 후 지난 시간
filters
- 캐릭터가 충돌해야 할 대상을 제어할 수 있다 (레이어개념인듯)
obstacles
- 선택적으로 추가되는 캐릭터가 충돌해야 할 추가적인 장애물 객체
- 닿은 장애물은 캐시되며, 장애물 집합이 변경되면 캐시를 무효화해야 한다.
collisionFlags
- move동안 발생한 충돌 이벤트를 정의하는 비트마스크
- 이는 PxControllerCollisionFlag 플래그의 조합이다.
- 이를 사용해서 캐릭터 애니메이션을 트리거할 수 있다 (!!)
- 예를 들어 캐릭터가 추락 중이고, 추락 idle 애니메이션이 실행중일 때 PxControllerCollisionFlag::eCOLLISION_DOWN이 반환되는 즉시 착지하는 애니메이션을 실행시킬 수 있다.
*PxController::move와 PxController::setPosition의 차이점을 이해하는 것이 중요하다.
- PxController::move 함수는 CCT 모듈의 핵심이다.
- 여기서 위에서 언급한 충돌 및 슬라이드 알고리즘이 실행되기 때문
- 따라서 함수는 CCT의 현재 위치에서 시작해서 필요한 방향으로 이동을 시도하고, 스위프트 테스트를 사용해서 이동한다.
- 장애물이 발견되면 CCT를 부드럽게 이동시켜 장애물에 따라 슬라이드할 수 있다.
- CCT가 벽에 막히는 경우 move 호출의 결과는 주변 지오메트리에 따라 다르다.
- PxContoller::setPosition은 간단히 말해서 텔레포트 함수다.
- CCT가 어디에서 시작하든 순간이동한다. (그 위치가 다른 객체의 중간 위치라고 하더라도)
Graphic Update
- 각 프레임에서 PxController::move 호출 후, 그래픽 객체는 새 CCT 위치와 일치하도록 유지해야 한다.
- 컨트롤러 위치는 다음 함수를 통해 가져올 수 있다.
const PxExtendedVec3& PxController::getPosition() const;
- 이 함수는 충돌 모양의 중심으로부터의 위치를 반환한다.
- 컨트롤러는 회전하지 않으므로 위치만 액세스할 수 있다.
- 발 위치를 가져오는 헬퍼 함수도 제공된다.(!!)
const PxExtendedVec3& PxController::getFootPosition() const;
bool PxController::setFootPosition(const PxExtendedVec3& position);

*주의할 점은 발 위치가 접촉 오프셋(Contact Offset)을 고려한다는 것
Auto Stepping
- 이 기능을 사용하지 않으면 캐릭터가 지형의 작은 고도차이에 막히게 된다.
- 다음 사진의 작은 고도 차이는 캐릭터가 지나갈 수 없게 한다.
- 실제 세계에서는 캐릭터가 이 작은 장애물을 건너뛰는 것이 당연한 일이지만 자연스럽게 느껴지지 않음

- Auto Stepping은 이런 기능을 수행한다.
- 아래 사진처럼 플레이어가 개입하지 않고 박스는 작은 장애물 위로 올라가게 된다.

- 하지만 장애물이 너무 커서 높이가 stepOffset 매개변수보다 큰 경우, 막히게 된다.

- 이러한 StepOffset은 PxControllerDesc::stepOffset에서 정의되며,
- PxController::getStepOffset() 함수를 통해 가져올 수 있다.
(일반적으로 stepOffset은 작게 유지하는게 좋다)
Climbing Mode
- 이런 Auto-Step 기능은 원래 박스 컨트롤러를 위해 만들어졌다,
- 박스 컨트롤러는 지면의 작은 장애물에 의해 막힐 수 있기 때문이다.
- 캡슐 컨트롤러는 둥근 모양 때문에 이 기능이 필요하지 않을 수 있다.
- Step Offset이 0.0인 경우에도, 캡슐은 작은 장애물을 넘어갈 수 있다.
왜냐하면 둥근 바닥은 작은 장애물과 충돌해서 상승 운동을 유발하기 때문
- Step Offset이 0이 아닌 경우, 캡슐은 Auto-Step 기능과 둥근 모양의 결합으로 더 높은 장애물을 넘을 수 있다.
- 이 경우 예측하기 어렵기 때문에, 캡슐에는 두 가지 다른 클라이밍 모드가 존재한다.
1. PxCapsuleClimbingMode:eEASY
- 이 모드에선 캡슐이 Step Offset 값에 제한을 받지 않는다.
- 따라서 이 모드에서는 이 값보다 높은 장애물을 넘어갈 가능성이 있다.
2.PxCalsuleClimbingMode::eCONSTRAINED
- 이 모드에선 캡슐이 Step Offset보다 높은 장애물을 넘을 수 없다
Up Vector
- Up Vector는 PxControllerDesc::upDirection을 통해 정의되며, PxController::getUpDirection을 통해 가져올 수 있다.
- Up Vector는 축에 정렬할 필요가 없다.
- PxController::setUpDirection() 함수를 사용해서 매 프레임마다 변경할 수 있으므로 캐릭터가 구 형태의 월드에서 이동할 수 있게 된다. (SampleCustomGravity 예제 참고)
- Up Vector를 수정하면 CCT 라이브러리가 캐릭터 볼륨을 변경할 때 방향이 변경된다.
- 예를 들어 캡슐은 PxCapsuleControllerDesc::height로 정의되며, 이는 Up Vector를 따라 수직 높이를 나타낸다.
- 따라서 이 UpVector를 변경하면 라이브러리 관점에서는 캡슐이 회전하게 된다.
- 이 수정 작업은 인접한 지오메트리 중첩을 방지하는 검사 없이 즉시 적용되기 때문에 오버랩 복구 모듈을 사용해야 한다.

- 왼쪽의 캡슐은 수직 방향의 Up Vector를 사용하고, 오른쪽 캡슐은 45도로 회전한 Up Vector를 사용한다