다양한 기록

[ChronoSpace] 키 아이템과 상호작용 with Subsystem Singleton (ClockworkLabyrinth) 본문

언리얼 엔진/ChronoSpace

[ChronoSpace] 키 아이템과 상호작용 with Subsystem Singleton (ClockworkLabyrinth)

라구넹 2025. 2. 13. 03:39

- 맵 배치용 액터(키 아이템) : CSLabyrinthKey

F키로 상호작용하여 획득

라비린스 키를 맵에 20개 뿌려둠 : 1층에 10개 2층에 7개 3층에 3개

실제 활성화는 그 중에서 10개

 

- 모아야 하는 건 5개키 아이템을 바칠 곳 : CSLabyrinthKeyAltar (제단)

F키로 상호작용하여 키 제출

5개 이상 제출 시 맵 클리어 (처음 나선 맵으로 이동시킴)

 

일단 상호작용 해야 하니 상호작용을 추가

CharacterPlayer에 F에 인풋 액션 만들고 오브젝트가 오버랩되면 델리게이트를 구독하는 방식

 

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FInteractionDelegate);

// Interaction Section
public:
	FInteractionDelegate OnInteract;
	void Interact();
    
void ACSCharacterPlayer::Interact()
{
	OnInteract.Broadcast();
}
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CSLabyrinthKey.generated.h"

UCLASS()
class CHRONOSPACE_API ACSLabyrinthKey : public AActor
{
	GENERATED_BODY()
	
public:	
	ACSLabyrinthKey();

	UFUNCTION()
	void OnTriggerBeginOverlapCallback(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult);

	UFUNCTION()
	void OnTriggerEndOverlapCallback(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

	UFUNCTION()
	void Interact();

protected:
	UPROPERTY(VisibleAnywhere, Category = "Trigger", Meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class USphereComponent> SphereTrigger;

	UPROPERTY(VisibleAnywhere, Category = "Mesh", Meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class UStaticMeshComponent> StaticMeshComp;

	float TriggerRange = 100.0f;
};

// Fill out your copyright notice in the Description page of Project Settings.


#include "Actor/CSLabyrinthKey.h"
#include "Character/CSCharacterPlayer.h"
#include "Components/StaticMeshComponent.h"
#include "Components/SphereComponent.h"
#include "Physics/CSCollision.h"
#include "ChronoSpace.h"

// Sets default values
ACSLabyrinthKey::ACSLabyrinthKey()
{
	bReplicates = true;

	// SphereTrigger
	SphereTrigger = CreateDefaultSubobject<USphereComponent>(TEXT("GravitySphereTrigger"));
	SphereTrigger->SetSphereRadius(TriggerRange, true);
	SphereTrigger->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
	RootComponent = SphereTrigger;
	SphereTrigger->SetCollisionProfileName(CPROFILE_CSTRIGGER);
	SphereTrigger->SetIsReplicated(true);

	// Static Mesh
	StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
	StaticMeshComp->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
	StaticMeshComp->SetupAttachment(SphereTrigger);
	StaticMeshComp->SetCollisionProfileName(CPROFILE_CSCAPSULE);
	StaticMeshComp->SetIsReplicated(true);

	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/Mesh/StaticMesh/BlockSphere.BlockSphere'"));
	if (StaticMeshRef.Object)
	{
		StaticMeshComp->SetStaticMesh(StaticMeshRef.Object);
	}

	float MeshRadius = 50.0f;
	float MeshScale = (TriggerRange / MeshRadius) * 0.75f;
	StaticMeshComp->SetRelativeScale3D(FVector(MeshScale, MeshScale, MeshScale));

	SphereTrigger->OnComponentBeginOverlap.AddDynamic(this, &ACSLabyrinthKey::OnTriggerBeginOverlapCallback);
	SphereTrigger->OnComponentEndOverlap.AddDynamic(this, &ACSLabyrinthKey::OnTriggerEndOverlapCallback);
}

void ACSLabyrinthKey::OnTriggerBeginOverlapCallback(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	ACSCharacterPlayer* Player = Cast<ACSCharacterPlayer>(OtherActor);

	if ( Player )
	{
		Player->OnInteract.Clear();
		Player->OnInteract.AddDynamic(this, &ACSLabyrinthKey::Interact);
	}
}

void ACSLabyrinthKey::OnTriggerEndOverlapCallback(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	ACSCharacterPlayer* Player = Cast<ACSCharacterPlayer>(OtherActor);

	if (Player)
	{
		Player->OnInteract.Clear();
	}
}

void ACSLabyrinthKey::Interact()
{
	UE_LOG(LogCS, Log, TEXT("ACSLabyrinthKey - Interact"));
}

일단 테스트용으로 메시는 적당히 넣어두고

 

F누르니까 로그 나오는 걸 보니 정상 작동

 

그래서 고민 점 : 싱글턴 만들어서 키 아이템 관리하고 싶음

그런데 프로젝트 세팅에 있는 게임 싱글턴 클래스는 하나만 만들 수 있음

저 안에서 싱글턴 여러 개 관리할 수도 있겠지만, SubSystem으로 싱글톤 만드는게 더 깔끔해보임\

 

정확히는 UGameInstanceSubsystem을 상속받아서 만들 거나, UWorldSubsystem 상속 받아서 만들면 되는데

전자는 레벨(맵)이 바뀌어도 유지되고, 후자는 레벨이 바뀌면 리셋됨

 

라비린스의 키는 레벨이 바뀌었을 때 유지할 필요는 없음

월드 서브시스템 상속받아서 만들 것임

 

이거 상속

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "CSLabyrinthKeyWorldSubsystem.generated.h"

/**
 * 
 */
UCLASS()
class CHRONOSPACE_API UCSLabyrinthKeyWorldSubsystem : public UWorldSubsystem
{
	GENERATED_BODY()
	
public:
	UCSLabyrinthKeyWorldSubsystem();
	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	virtual void Deinitialize() override;

	FORCEINLINE void SetLabyrinthKeyCount(int8 InCount) { LabyrinthKeyCount = InCount; }
	FORCEINLINE const int8 GetLabyrinthKeyCount() const { return LabyrinthKeyCount; }
private:
	int8 LabyrinthKeyCount;
};

// Fill out your copyright notice in the Description page of Project Settings.


#include "Subsystem/CSLabyrinthKeyWorldSubsystem.h"

UCSLabyrinthKeyWorldSubsystem::UCSLabyrinthKeyWorldSubsystem()
{
	LabyrinthKeyCount = 0;
}

void UCSLabyrinthKeyWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);
	LabyrinthKeyCount = 0;
}

void UCSLabyrinthKeyWorldSubsystem::Deinitialize()
{
	Super::Deinitialize();
}

싱글톤 이렇게 만들어주고

 

void ACSLabyrinthKey::Interact()
{
	UCSLabyrinthKeyWorldSubsystem* LabyrinthKeySubsystem = GetWorld()->GetSubsystem<UCSLabyrinthKeyWorldSubsystem>();

	if (LabyrinthKeySubsystem)
	{
		LabyrinthKeySubsystem->SetLabyrinthKeyCount((LabyrinthKeySubsystem->GetLabyrinthKeyCount()) + 1);

		UE_LOG(LogCS, Log, TEXT("ACSLabyrinthKey - Interact : %d"), LabyrinthKeySubsystem->GetLabyrinthKeyCount());
	}

	Destroy();
}

Interact 구현

 

근데 이러니까 클라이언트가 Destroy해도 권한 없어서 삭제 안됨

RPC 처리까지 해줘야 함

당연히 플레이어 Interaction을 RPC 해야 함 저 오브젝트에서가 아니라

해도 작동이야 하겠다만 확장성 챙기자

 

사실 오브젝트에다 RPC 쓰고 있다가 뭔가 이상해서 지우고 플레이어로 했다

 

public:
	FInteractionDelegate OnInteract;

	UFUNCTION(Server, Reliable)
	void ServerInteract();

	void Interact();
    
void ACSCharacterPlayer::ServerInteract_Implementation()
{
	OnInteract.Broadcast();
}

void ACSCharacterPlayer::Interact()
{
	if ( HasAuthority() )
	{
		OnInteract.Broadcast();
	}
	else
	{
		ServerInteract();
	}
}

이렇게 세팅해주면 된다

 

 

로그 찍히는 걸 보니 싱글턴 적용도 잘 되고 동기화도 잘 됐다

 

이제 다음으로는 'F 키를 눌러 상호작용' 멘트를 띄우고 싶다

 

위젯 만들고 (그냥 글씨만 띄우는 거라 GAS 안쓸 거라 기본 사용)

 

UPROPERTY(VisibleAnywhere)
TObjectPtr<class UWidgetComponent> InteractionPromptComponent;


///////////////////////

// Widget
InteractionPromptComponent = CreateDefaultSubobject<UWidgetComponent>(TEXT("InteractionPromptComponent"));
InteractionPromptComponent->SetupAttachment(SphereTrigger);
InteractionPromptComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 100.0f));

static ConstructorHelpers::FClassFinder<UUserWidget> InteractionPromptWidgetRef(TEXT("/Game/Blueprint/UI/BP_InteractionPrompt.BP_InteractionPrompt_C"));
if (InteractionPromptWidgetRef.Class)
{
	InteractionPromptComponent->SetWidgetClass(InteractionPromptWidgetRef.Class);
	InteractionPromptComponent->SetWidgetSpace(EWidgetSpace::Screen);
	InteractionPromptComponent->SetDrawSize(FVector2D(500.0f, 30.f));
	InteractionPromptComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}

InteractionPromptComponent->SetVisibility(false);

/////

void ACSLabyrinthKey::OnTriggerBeginOverlapCallback(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	ACSCharacterPlayer* Player = Cast<ACSCharacterPlayer>(OtherActor);

	if ( Player )
	{
		Player->OnInteract.Clear();
		InteractionPromptComponent->SetVisibility(true);
		Player->OnInteract.AddDynamic(this, &ACSLabyrinthKey::Interact);
	}
}

void ACSLabyrinthKey::OnTriggerEndOverlapCallback(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	ACSCharacterPlayer* Player = Cast<ACSCharacterPlayer>(OtherActor);

	if (Player)
	{
		InteractionPromptComponent->SetVisibility(false);
		Player->OnInteract.Clear();
	}
}

이러면 가까이 갔을 때만 위젯이 보인다