언리얼엔진 학습 및 과제

팀 프로젝트 슈터 게임 (4)

park94128 2025. 10. 31. 21:54

● 데칼 액터를 대충 배치하지 말자

버그를 발견했다. 데칼 액터에 닿으면 캐릭터의 몸에 데칼이 묻는 현상이다. 그래서 캐릭터가 핏자국에 올라서기만 하면 바로 피에 젖는 모습을 보여준다.

원인은 간단했다. 내가 데칼 액터를 대충 배치했기 때문이다. 데칼 액터는 영향을 주는 범위가 있는데 이 범위를 너무 크게 잡은 상태였다. 나는 이게 스태틱 메시에만 영향을 받을거라는 안일한 생각을 했지만 틀렸다. 스켈레탈 메시에도 영향을 준다.

 

최대한 얇게 만들고 바닥과 동일한 위치로 옮겨주는 것으로 해결했다. 핏자국 뿐만이 아니라 안내 표지 또한 저런식으로 대충 배치한 바람에 몽땅 수정해야만 했다.

 

● 이제 기믹을 만들어볼까

맵의 기본적인 형태는 다 만들었으니 원래 만들기로 했던 기믹을 구현할 차례다. 지금까지 맵을 만드느라 코드를 사용하지 않았지만 이제부터 코드에 손을 댈 차례다.

UCLASS(Abstract)
class TEAM14CH3PROJECT_API AInteractionWithPlayer : public AActor
{
	GENERATED_BODY()

public:
	//플레이어가 이 함수를 호출하여 구현된 상호작용을 실행한다.
	UFUNCTION(BlueprintCallable)
	virtual void Interaction() override PURE_VIRTUAL();
}

- UCLASS에 Abstract라고 적은 것이 있다. 이렇게 하면 이 클래스는 직접 사용할 수 없고 오직 상속용으로만 사용가능하다.

- 언리얼 엔진용 클래스는 일반적인 C++형태의 순수 가상 함수를 허용하지 않으며 PURE_VIRTUAL 매크로를 사용한다.

- Interaction 함수는 다른 블루프린트에서 호출할 것을 염두하여 함수 위에 UFUNCTION(BlueprintCallable)이라고 적었다.

 

나의 원래 생각은 이렇다. 상호작용이 가능한 모든 액터는 위 클래스를 상속받아서 내용을 구현한다. 그리고 캐릭터를 담당하는 팀원은 이 클래스를 찾아 상호작용 함수를 실행하는 것 1개만 추가하는 것으로 구현할 생각이었다.

 

하지만 여기서 팀원의 의견이 있었다. 아이템을 위한 인터페이스 클래스를 만들었으니 이 인터페이스에 합쳐서 1개만 쓰는 것이 더 좋다는 것이다.

 

class TEAM14CH3PROJECT_API IItemInterface
{
	GENERATED_BODY()

public:
	virtual void OnItemOverlap(
		UPrimitiveComponent* OverlappedComp,
		AActor* OtherActor,
		UPrimitiveComponent* OtherComp,
		int32 OtherBodyIndex,
		bool bFromSweep,
		const FHitResult& SweepResult) = 0;
	virtual void OnItemEndOverlap(
		UPrimitiveComponent* OverlappedComp,
		AActor* OtherActor,
		UPrimitiveComponent* OtherComp,
		int32 OtherBodyIndex) = 0;
	// 아이템이 사용되었을 때 호출
	virtual void ActivateItem(AActor* Activator) = 0;
	// 아이템의 유형(타입)을 반환
	virtual FName GetItemType() const = 0;
};

※ 내가 만든 코드가 아닌 팀원이 만든 코드다.

이 인터페이스용 클래스는 UCLASS 선언이 없으니 C++식 순수 가상 함수로 선언하는 것이 가능하다는걸 알았다.

 

여기에 내가 만든 함수명을 가져와 아랫줄에 새 함수를 추가하여 아래와 같은 코드가 됐다.

class TEAM14CH3PROJECT_API IItemInterface
{
	GENERATED_BODY()

public:
	virtual void OnItemOverlap(
		UPrimitiveComponent* OverlappedComp,
		AActor* OtherActor,
		UPrimitiveComponent* OtherComp,
		int32 OtherBodyIndex,
		bool bFromSweep,
		const FHitResult& SweepResult) = 0;
	virtual void OnItemEndOverlap(
		UPrimitiveComponent* OverlappedComp,
		AActor* OtherActor,
		UPrimitiveComponent* OtherComp,
		int32 OtherBodyIndex) = 0;
	// 아이템이 사용되었을 때 호출
	virtual void ActivateItem(AActor* Activator) = 0;
	// 아이템의 유형(타입)을 반환
	virtual FName GetItemType() const = 0;
	// 플레이어가 상호작용을 하여 작동하게 하려면 이 함수를 구현
	virtual void Interaction() = 0;
};

이렇게 하는 이유는 상술하듯 1개의 클래스로 통합하기 위함도 있지만, 팀원의 의견을 빌리자면 단순히 닿는 것으로 얻는 아이템 뿐만이 아니라 상호 작용을 하여 얻는 아이템도 있기 때문에 내가 만든 방식이 필요하다고 했다.

함수 하나만 추가하면 가능한 기능이라 해결은 간단했다. 캐릭터가 상호작용을 통해 Interacion함수를 호출하기만 한다면 그 아이템은 Interaction함수에 내용을 구현하면 된다.

 

UCLASS(Abstract)
class TEAM14CH3PROJECT_API AInteractionWithPlayer : public AActor, public IItemInterface
{
    GENERATED_BODY()

public:
    //플레이어가 이 함수를 호출하여 구현된 상호작용을 실행한다.
    UFUNCTION(BlueprintCallable)
    virtual void Interaction() override PURE_VIRTUAL();

private:
#pragma region 구현할 필요가 없는 abstract 함수 모음
    virtual void OnItemOverlap(
       UPrimitiveComponent* OverlappedComp,
       AActor* OtherActor,
       UPrimitiveComponent* OtherComp,
       int32 OtherBodyIndex,
       bool bFromSweep,
       const FHitResult& SweepResult) override;

    virtual void OnItemEndOverlap(
       UPrimitiveComponent* OverlappedComp,
       AActor* OtherActor,
       UPrimitiveComponent* OtherComp,
       int32 OtherBodyIndex) override;

    virtual void ActivateItem(AActor* Activator) override;
    virtual FName GetItemType() const override;
#pragma endregion
};

그리고 나는 원래 만든 클래스를 팀원이 만든 인터페이스 클래스를 상속받는 형태로 만들었다. 필요 없는 함수는 그냥 private에 구현하고 region으로 묶어놨다. 이러면 나는 원래 하던거 하면 된다.

 

● 우선 간단한거 하나

지금까지 맵을 만들때 빈 곳이 아직 남았는데 상호 작용이 가능한 액터를 배치할 공간이기 때문이다. 위 사진처럼 열 수 있는 문이 그 중 하나다. 원래는 문 같은것도 없이 뻥 뚫렸다.

UCLASS()
class AHospitalDoor : public AInteractionWithPlayer
{
	GENERATED_BODY()
	
public:
	AHospitalDoor();

protected:
	virtual void BeginPlay() override;

public:
	virtual void Interaction() override;
    
private:
	//문열림 연출용 함수
	UFUNCTION()
	void DoorOpenTimeLineFunc(float Output);

	//문 메시
	UPROPERTY(EditDefaultsOnly, meta = (AllowPrivateAccess = "true"))
	UStaticMeshComponent* DoorMesh;

	//문열림 연출 실행
	FOnTimelineFloat UpdateFunctionFloat;

	//문열림 연출용 CurveFloat
	UPROPERTY(EditDefaultsOnly, meta = (AllowPrivateAccess = "true"))
	UCurveFloat* DoorTimelineCurveFloat;

	//문열림 연출용 Timeline
	UPROPERTY(EditDefaultsOnly, Category = "Door Action", meta = (AllowPrivateAccess = "true"))
	UTimelineComponent* DoorTimeline;

	//문열림 여부
	UPROPERTY(EditAnywhere, Category = "Door Action", meta = (AllowPrivateAccess = "true"))
	bool bDoorOpen;
};

열리는 문을 구현할 것이니 문에 해당되는 스태틱 메시 컴포넌트, 타임라인 실행을 위한 각종 클래스를 사용한다.

 

타임라인을 사용한 연출 구현은 이전에 블루프린트로 만든 것을 토대로 코드로 구현했다.

 

void AHospitalDoor::Interaction()
{
	if (bDoorOpen)
		return;
	
	//문을 열어라
	DoorTimeline->Play();
	bDoorOpen = true;
}

마지막으로 타임라인 실행을 Interaction 함수에 넣어주면 캐릭터가 인터페이스의 Interaction 함수를 실행함으로 위 내용을 실행할 수 있다.

 

?????

구현 자체는 성공적인데... 수치를 잘못 넣었다.

 

어쨌든 이를 기반으로 모든 맵에 만들기로 한 기믹을 구현하면 되겠다.