Post

[토이프로젝트] 액터를 상호작용 가능하게 만드는 컴포넌트와 인터페이스

오브젝트들과 플레이어 간의 상호작용은 게임의 몰입감과 사용자 경험을 크게 향상시키는 중요한 요소입니다.

[토이프로젝트] 액터를 상호작용 가능하게 만드는 컴포넌트와 인터페이스

게임 내에는 수많은 오브젝트들이 존재합니다.

이 오브젝트들과 플레이어 간의 상호작용은 게임의 몰입감과 사용자 경험을 크게 향상시키는 중요한 요소입니다. 하지만 오브젝트마다 상호작용을 개별적으로 코딩하게 된다면 매우 비효율적이고 번거로운 일이 될 수 있습니다.

따라서 효율적인 개발을 위해서는 공통적으로 사용할 수 있는 상호작용 가능한 컴포넌트(Interactable Component) 를 만드는 것이 좋습니다. 이 컴포넌트를 통해 오브젝트가 상호작용을 요청받으면 지정된 동작을 수행하도록 일반화할 수 있으며, 코드를 중복하지 않고 재사용할 수 있습니다.

예를 들어, 게임 속에 문, 장비 아이템, 레버 등이 있다고 가정해 봅니다. 문은 플레이어가 접근하여 ‘상호작용 키(E)’를 누르면 열리고 닫힙니다. 장비 아이템은 같은 키를 눌러 장착할 수 있도록 합니다. 레버는 키를 눌러 특정 이벤트를 발생시킵니다. 이때 모든 오브젝트가 각자 개별적인 코드를 가진다면, 유지보수가 어렵고 코드가 중복될 것입니다.

이럴 때, 상호작용 가능한 컴포넌트와 인터페이스를 만들어 사용하면 플레이어가 오브젝트와 상호작용할 때의 기본적인 로직은 한 번만 구현하고, 각 오브젝트에서 이 컴포넌트를 상속받아 구체적인 동작만 정의하면 됩니다. 예를 들어 문은 열리고 닫히는 로직을 추가하고, 아이템 상자는 아이템 지급 로직만 추가하면 됩니다. 이러한 방식으로 개발의 효율성을 높이고 유지보수가 쉬운 코드 구조로 만들 수 있습니다.

인터페이스

IFPInteractable

1
2
3
4
5
6
7
8
class FPSPROJECT_API IFPInteractable
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
	void Interact(class AFPCharacterBase* Interactor);
};

상호작용 기능을 효과적으로 관리하기 위해 공통 인터페이스인 ‘IFPInteractable’을 정의했습니다. 이 인터페이스는 상호작용 가능한 모든 오브젝트가 반드시 구현해야 하는 Interact() 함수를 선언하여, 다양한 오브젝트들이 일관된 방식으로 상호작용을 처리할 수 있도록 합니다. 오브젝트들은 이 인터페이스를 상속받아 자신만의 구체적인 상호작용 로직을 구현하게 됩니다.

IFPHasMesh

1
2
3
4
5
6
7
class FPSPROJECT_API IFPHasMesh
{
	GENERATED_BODY()

public:
	virtual UMeshComponent* GetMainMesh() = 0;
};

상호작용을 수행하는 액터의 Mesh를 캐싱하기 위한 인터페이스인 ‘IFPHasMesh’도 정의했습니다. 각 인터페이스는 하나의 역할만 수행하도록 두개의 인터페이스로 나누어 정의하였습니다. IFPHasMesh는 GetMainMesh()를 선언하여, Mesh의 레퍼런스를 가져와야 하는 클래스에서 Getter를 정의하도록 합니다. 캐싱한 Mesh는 이후 상호작용이 가능하다는 시각적 효과를 위한 포스트 프로세싱에 활용합니다.

컴포넌트

FPInteractableComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void UFPInteractableComponent::BeginPlay()
{
	Super::BeginPlay();
	if (AActor* OwnerActor = GetOwner())
	{
		if (IFPHasMesh* HasMeshInterface = Cast<IFPHasMesh>(OwnerActor))
		{
			CachedMesh = HasMeshInterface->GetMainMesh();
			UE_LOG(LogTemp, Log, TEXT("Mesh cashed"));
		}
		else
		{
			// 폴백: 메시 직접 찾기
			CachedMesh = OwnerActor->FindComponentByClass<UMeshComponent>();
		}
	}
}
void UFPInteractableComponent::Highlight(bool bEnable)
{
	if (!CachedMesh) return;

	CachedMesh->SetRenderCustomDepth(bEnable);

	// 필요 시 색상 지정 (Material에서 이 StencilValue를 기준으로 색상 분기)
	CachedMesh->SetCustomDepthStencilValue(bEnable ? 1 : 0);
}

void UFPInteractableComponent::Interact(AFPCharacterBase* Interactor) const
{
	if (AActor* OwnerActor = GetOwner())
	{
		UE_LOG(LogTemp, Log, TEXT("%s 와 상호작용 실행"), *OwnerActor->GetName());

		// 캐릭터인지 확인
		if (OwnerActor->Implements<UFPInteractable>())
		{
			IFPInteractable::Execute_Interact(OwnerActor, Interactor);
		}
	}
}

‘FPInteractableComponent’는 UBoxComponent를 상속받아 플레이어가 액터와의 상호작용을 시도할 때 감지할 수 있도록 충돌 박스를 생성합니다. 내부적으로는 메시를 캐싱하여 상호작용 시 시각적 효과(예: 외곽선 강조)를 쉽게 적용할 수 있도록 지원합니다. 또한 이 컴포넌트는 액터에 부착되어 액터의 기본적인 상호작용 감지와 로직 실행을 관리함으로써, 각 액터마다 상호작용이 가능한 액터인지 구분할 필요가 없도록 인터페이스를 통해 구조화되어 있습니다.

적용하기

AFPWeaponBase.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class FPSPROJECT_API AFPWeaponBase : public AActor, public IFPInteractable, public IFPHasMesh
{
	/* 무기 상호작용 관련 섹션 */
	// 키보드를 이용한 상호작용 컴포넌트
    virtual void Interact_Implementation(AFPCharacterBase* Interactor) override;

protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	TObjectPtr<UFPInteractableComponent> InteractableComponent;
  
	// HasMesh Interface
protected:
	FORCEINLINE virtual UMeshComponent* GetMainMesh() override { return ThirdPersonMesh; }
}

해당 헤더는 캐릭터가 장착가능한 무기를 정의하는 상위 부모 클래스 중 일부입니다.

AFPInteractableLevelObject.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class FPSPROJECT_API AFPInteractableLevelObject : public AActor, public IFPInteractable, public IFPHasMesh
{
	GENERATED_BODY()
	
public:	
	AFPInteractableLevelObject();

protected:
	virtual void BeginPlay() override;
	
	/* Mesh */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mesh")
	TObjectPtr<UStaticMeshComponent> VisualMesh;

	/* 상호작용 섹션 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TObjectPtr<class UFPInteractableComponent> InteractableComponent;

	// Interface
	virtual void Interact_Implementation(AFPCharacterBase* Interactor) override;
	FORCEINLINE virtual UMeshComponent* GetMainMesh() override { return VisualMesh; }
};

해당 헤더는 상호작용이 가능한 액터들 중 레벨에 배치되는 오브젝트들을 정의하는 상위 부모 클래스입니다.

AFPInteractableDoor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class FPSPROJECT_API AFPInteractableDoor : public AFPInteractableLevelObject
{
	GENERATED_BODY()

public:
	AFPInteractableDoor();
	
protected:
	virtual void BeginPlay() override;
	virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;

	/* Mesh */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mesh")
	TObjectPtr<UStaticMeshComponent> DoorPivot;
	
	/* 문 상호작용 (토글) */
public:
	virtual void Interact_Implementation(AFPCharacterBase* Interactor) override;
	
	// 문 열기 / 닫기 (서버에서 호출)// 문 열림 상태 (Replicated)
	UPROPERTY(ReplicatedUsing = OnRep_DoorState, BlueprintReadWrite, EditAnywhere)
	bool bIsOpen;
	
	UFUNCTION(BlueprintCallable)
	void ToggleDoor();

	UFUNCTION()
	void OnRep_DoorState();

	// 서버에서 상태를 전환
	UFUNCTION(Server, Reliable)
	void ServerToggleDoor();

	UFUNCTION(BlueprintImplementableEvent)
	void PlayDoorAnimation(bool bOpen);
};

해당 헤더는 AFPInteractableLevelObject를 상속받아 상호작용 시 개폐되는 문을 정의하는 클래스입니다. 멀티플레이에서 사용할 수 있도록 설계되어 있습니다. 또한, 문이 개폐되는 애니메이션이나 기타 로직이 실행되야 하는 경우, 이를 구현하는 것은 프로그래머가 아닌 다른 팀에서 구체적으로 구현해야할 가능성이 클 것입니다. 따라서, 해당 함수의 경우에는 블루프린트에서 구현할 수 있도록 설계하였습니다. 문이 개폐되는 블루프린트는 아래와 같이 구현하였습니다.

마지막으로 실제 적용 영상을 보여드리면서 글을 마무리하도록 하겠습니다.

https://youtu.be/zWDw64PImNo

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