타겟을 지정하는 스킬을 시전할 때, 또는 상호작용을 할 때 등등 많은 상황에서 마우스로 액터를 지정하는 일이 많이 생긴다.
이 때 이 액터와 상호작용할 수 있음을 알려줄 수 있도록, 마우스 오버에 대응해 액터의 테두리 색깔을 적용하는 기능을 구현해보자.
가장 먼저 해야 할 것은 인터페이스를 생성하는 것이다.
특정한 캐릭터 클래스에 기능을 구현할 수도 있지만, 마우스 오버 시 테두리 표시 등의 기능은 한 클래스의 기능으로 보기보다는 상호작용할 수 있는 요소라고 보는 것이 좋고, 또는 다양한 종류의 클래스 (예 : 몬스터, 아이템, NPC 등등)에 공통적으로 사용할 수 있는 기능이기 때문에, 특정한 클래스 내부에 종속시키기보다는 인터페이스에 구현해 놓고, 기능을 지원하게 할 액터에게 이 인터페이스를 붙여줌으로써 구현하는 것이 좋을 것이다.
C++ 클래스에서 가장 아래에 있는 Unreal Interface
를 클릭하여 인터페이스를 만든다. 이름은 HighlightInterface
로 지정했다.
포스트 프로세스 볼륨은 레벨이 렌더링 된 후에 효과를 적용할 수 있는 볼륨이다. 시각적인 효과를 즉시 적용할 수 있어 유용하게 사용된다. 이 볼륨을 레벨에 포함시키고, custom depth를 이용해서 외곽선 효과를 적용할 것이다.
자세한건 렌더링 관련하여 깊이 다룰 때 다루기로 하고, 우선은 이것들을 사용한다고 보면 될 것 같음
먼저 레벨에 Post Process Volume이 존재하지 않는다면 하나를 추가해 주고,
Infinite Extent를 활성화해 줌으로써, 이 볼륨의 영향을 받는 범위를 레벨 전역으로 확장시킨다.
그리고 Project Settings에서 Custom Depth-Stencil Pass
를 Enabled with Stencil
로 설정.
그리고 디테일 패널에 Rendering Features
탭의 Post Process Materials를 추가한다. 여기에 사용할 머티리얼이 필요하다.
새 머티리얼을 하나 만들고 Material Domain에서 Post Process
를 설정해 주면 이 머티리얼은 post process에 사용된다.
머티리얼 세팅에 대한 자세한 내용
https://dev.epicgames.com/documentation/ko-kr/unreal-engine/post-process-materials-in-unreal-engine
이제 에디터에서 Render Custom Depth Pass
를 체크 해주면 위와 같이 외곽선 효과를 볼 수 있다.
마우스를 오버함에 따라서 각 클래스에서 서로 다른 기능을 사용할 수도 있으므로, Interface 자체에 구현할 함수는 순수 가상 함수로 만들고 이를 implement하여 사용하는 클래스에서 함수를 정의하도록 한다.
외곽선 테두리를 적용하는 HighlightActor
그리고 적용 해제하는 UnHighlightActor
두 가지 함수를 만든다.
class AURA_API IHighlightInterface
{
GENERATED_BODY()
public:
virtual void HighlightActor() = 0;
virtual void UnHighlightActor() = 0;
};
그리고 아래와 같이 구현할 클래스에 IHighlightInterface
인터페이스를 추가해 준다.
class AURA_API AAuraEnemy : public AAuraCharacterBase, public IHighlightInterface
{
GENERATED_BODY()
public:
AAuraEnemy();
virtual void HighlightActor() override;
virtual void UnHighlightActor() override;
};
어떤 기능이 필요할지 생각 해보면 앞서 보았던 Render Custom Depth Pass
를 true, false로 토글, 및 활성화할 때 stencil value를 설정하는 것이 전부이다.
AAuraEnemy::AAuraEnemy()
{
GetMesh()->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
}
void AAuraEnemy::HighlightActor()
{
GetMesh()->SetRenderCustomDepth(true);
GetMesh()->SetCustomDepthStencilValue(250);
Weapon->SetRenderCustomDepth(true);
Weapon->SetCustomDepthStencilValue(250);
}
void AAuraEnemy::UnHighlightActor()
{
GetMesh()->SetRenderCustomDepth(false);
Weapon->SetRenderCustomDepth(false);
}
생성자에서는 마우스 커서로부터 트레이스 결과를 얻기 위해 Visibility 채널에 대한 collision response를 block으로 설정한다. (마우스 커서는 visibility 채널로부터 트레이스 수행)
그리고 RenderCustomDepth()
, SetCustomDepthStencilValue()
함수를 호출해 주는 간단한 구현을 추가한다.
플레이어 컨트롤러에는 마우스 커서로부터 라인 트레이스를 실행할 수 있는 기능이 존재한다. Tick
함수에서 이 트레이스 함수를 수행함으로써 마우스에 오버되어 있는 액터를 매 틱마다 감지하고, 그 액터에게 함수를 호출해 줄 수 있다.
어떤 상황에 HighlightActor
그리고 UnHighlightActor
를 호출할까를 생각해 보면 쉽게 알 수 있다.
먼저 하이라이트 함수를 호출하는 때는 바로 마우스 오버를 처음 시작했을 때이다. 마우스 오버를 처음 했다는 것은 두 가지 포인터를 가지고 알 수 있는데, 이번 프레임에 트레이스를 하여 얻은 액터 포인터와, 직전 프레임에 얻은 액터 포인터를 사용하면 된다. 지난 프레임의 액터를 LastActor
라고 하고, 이번 프레임의 액터를 ThisActor
라고 했을 때 LastActor는 nullptr이면서 ThisActor가 처음으로 nullptr이 아닐 때의 그 프레임이 바로 마우스 오버가 시작된 프레임이다.
하이라이트를 끝내는 부분 역시 마찬가지로 마우스 오버가 처음으로 끝난 부분일 것이며 LastActor와 ThisActor를 반대로 사용해 주면 된다. LastActor가 nullptr이 아닌데 ThisActor가 nullptr이면 처음으로 마우스 커서 트레이스 결과가 nullptr가 됐으므로 그 프레임에서 UnHighlight
를 실행하면 된다.
또한 액터에서 액터로 바로 마우스가 이동하는 경우도 있을 수 있으므로, 각 액터 포인터가 nullptr인지만 확인할 것이 아니고 둘이 서로 같은 액터를 가리키고 있는지의 여부도 판단하여 서로 다른 두 액터 간의 커서 이동에 대한 엣지 케이스도 처리한다.
void AAuraPlayerController::CursorTrace()
{
FHitResult CursorHit;
GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
if (!CursorHit.bBlockingHit) return;
LastActor = ThisActor;
ThisActor = CursorHit.GetActor();
/*
* 커서로부터 트레이스
* 1) LastActor == null && ThisActor == null
* -> 아무것도 하지 않음
* 2) LastActor == null && ThisActor != null
* -> 처음으로 액터에 호버된 것이므로 Highlight Actor
* 3) LastActor != null && ThisActor == null
* -> 더이상 호버하지 않으므로 UnHighlight Actor
* 4) LastActor != null && ThisActor != null && LastActor != ThisActor
* -> 호버 대상이 바뀌었으므로 UnHighlight LastActor, Highlight ThisActor
* 5) LastActor != null && ThisActor != null && LastActor == ThisActor
* -> 이미 호버한 대상을 계속 호버하고 있으므로 아무것도 하지 않음
*/
if (LastActor == nullptr)
{
if (ThisActor != nullptr)
{
// case 2
ThisActor->HighlightActor();
}
else
{
// case 1
}
}
else
{
if (ThisActor == nullptr)
{
// case 3
LastActor->UnHighlightActor();
}
else if (ThisActor != nullptr && LastActor != ThisActor)
{
// case 4
LastActor->UnHighlightActor();
ThisActor->HighlightActor();
}
else
{
// case 5
}
}
}