Class GameDev* SheepAdult

[Unreal Engine 4.27] Round Lever / Valve System C++ 본문

Unreal Project

[Unreal Engine 4.27] Round Lever / Valve System C++

SheepAdult 2022. 7. 20. 17:58

 1인칭일 때 사용 가능한 밸브 시스템을 구현했다.  

 방식을 좀 간략하게 설명하자면 밸브에 포커스를 두고 E키를 눌렀을 경우 매 프레임마다 특정값을 더해준다. 이 값을 0에서 100으로 Clamp를 걸어두고 100에 도달하면 인터랙트를 종료되며 다시는 인터랙트할 수 없다. 그리고 포커스가 벗어나거나 E키를 Release하면 밸브는 다시 되돌아가며 모두 되돌아갈 때까지 다시 Interact 할 수 없다. 손으로 돌리고 있는 느낌을 주기 위해 밸브의 회전에 sin을 곱해 등속 회전을 하지 않게 했으며, 상호작용하는 문도 동일하다.

코드는 아래와 같다.

// MainCharacter.cpp
void AMainCharacter::Interact()
{
	FVector Start;
	FVector End;
	FHitResult OutHit;
	float InteractDistance = 200.0f;
	if (bIsFP)	// 1인칭 일 때
	{
		Start = FirstPersonCameraComponent->GetComponentLocation();
		End = Start + FirstPersonCameraComponent->GetForwardVector() * InteractDistance;

		TArray<AActor*> IgnoreActors;
		bool bIsInteracting = UKismetSystemLibrary::LineTraceSingle(
			GetWorld(),
			Start,
			End,
			ETraceTypeQuery::TraceTypeQuery1,
			false,
			IgnoreActors,
			EDrawDebugTrace::None,
			OutHit,
			true
		);

		if (bIsInteracting)
		{
			AMaster_InteractableObject* Obj = Cast<AMaster_InteractableObject>(OutHit.Actor);
			if (Obj)
			{
				if (InteractableObject == nullptr)
				{
					InteractableObject = Obj;
                    InteractableObject->Interact();
				}
			}
            // ...
		}
		// ...
	}
}

void AMainCharacter::StopInteract()
{
	AInteractableObject_Valve* Valve = Cast<AInteractableObject_Valve>(InteractableObject);
	if (Valve)
	{
		Valve->StopInteract();
	}
	InteractableObject = nullptr;
}

Interact()는 E키를 Press한 경우, StopInteract()는 E키를 Release했을 경우이다. 별 내용 없으니.. 아래는 밸브 코드이다.

 코드가 좀 많은디,, 일단 다 써놓고 설명하자면

#pragma once

#include "CoreMinimal.h"
#include "Master_InteractableObject.h"
#include "Interface_GaugeWidget.h"

#include "InteractableObject_Valve.generated.h"

UCLASS()
class CAP2_API AInteractableObject_Valve : public AMaster_InteractableObject
{
	GENERATED_BODY()

// 벨브가 돌아가는 속도, 밸브 회전 주기(sin으로 속도 변화 있으므로), 밸브로 인해 움직이는 문의 속도 등
private:
	float ValveGauge = 0.0f;	// 벨브 게이지
	float ValveRotateValue = 0.0f;		// 벨브를 놓았을 때 다시 원점으로 돌아가기 위해 벨브가 총 돌아간 거리 계산 
	bool bIsInteracting;	// 상호작용 중 게이지가 차고 있는 경우
	bool bCanInteract = true;	// 상호작용 가능한지(벨브를 놓쳐서 다시 돌아갈 때 인터렉트 막아놓으려고)
	bool bIsFinished;	// 끝나면 상호작용 못하게 하려고
	
    //	벨브 게이지 차는 속도
	UPROPERTY(EditAnywhere)
		float ValveGaugeSpeed = 0.1f;
    // 벨브 돌아가는 속도
	float ValveRotationSpeed = 1;
    // 벨브 돌아가는 속도에 영향을 주는 sin함수 주기
	UPROPERTY(EditAnywhere)
		float ValveRotationCycleTime = 1;
    // 벨브 되돌아가는 속도
	float ValveReverseSpeed = 3.5f;

	// 벨브에 연결된 문 올라가는 속도
	UPROPERTY(EditAnywhere)
		float LinkedActorSpeed = 0.9f;

public:
	AInteractableObject_Valve();

	class AMainCharacter* MainCharacter;

	UPROPERTY(EditAnywhere)
		AActor* LinkedActor;

	void LinkedActorAction(float Gauge);

	void StopInteract();
	void ProgressingGauge();
	void ReverseGauge();
	void ReverseGaugeEvent(float FSpeed);
	void ValveGaugeComplete();

	// 게이지 차는 위젯
	UPROPERTY(EditAnywhere)
		TSubclassOf<UUserWidget> _GaugeWidget;
	UPROPERTY()
		class UGaugeWidget* GaugeWidget;

protected:
	virtual void Interact() override;
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaTime) override;
};

아래는 cpp이다.

// Valve.cpp

#include "InteractableObject_Valve.h"
#include "MainCharacter.h"
#include "Kismet/GameplayStatics.h"
#include "InteractableObject_Valve.h"
#include "Kismet/KismetMathLibrary.h"
#include "GaugeWidget.h"
#include "HighlightableComponent.h"
#include "SoundManager.h"
#include "Components/AudioComponent.h"

AInteractableObject_Valve::AInteractableObject_Valve()
{
	PrimaryActorTick.bCanEverTick = true;

	StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
	StaticMesh->SetupAttachment(RootComponent);
}

void AInteractableObject_Valve::BeginPlay()
{
	Super::BeginPlay();

	MainCharacter = Cast<AMainCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
}

void AInteractableObject_Valve::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	if (GaugeWidget)
	{
		if (bIsInteracting)
		{
        	// 게이지가 0에서 100으로 Clamp되는데 100에 아직 미치지 못한경우
			if (!UKismetMathLibrary::NearlyEqual_FloatFloat(ValveGauge, 100.0f, 0.1) && MainCharacter && MainCharacter->bIsOnFocusHighlightable)
			{
				ProgressingGauge();
				LinkedActorAction(ValveGauge);
			}
            // 게이지가 100에 도달하기 전에 포커스를 다른 곳으로 둘 경우
			else if(!MainCharacter->bIsOnFocusHighlightable)
			{
				if (!bIsFinished)
				{
					ReverseGauge();
					if (GaugeWidget->IsInViewport())
					{
						GaugeWidget->RemoveFromParent();
					}
				}	
			}
		}
        // E키를 Release 했을 경우
		else
		{
			if (!bIsFinished)
			{
				ReverseGauge();
			}
		}			
	}
}

void AInteractableObject_Valve::Interact()
{
	if (bCanInteract)
	{
		bCanInteract = false;
        // 이건 연타를 하니까 방정맞아 보여서 누르고 일정시간 동안 인터랙트를 막아놓기 위해 작성했는데 Delay 기능은 언제 사용해도 찝찝하다,,
		FTimerHandle WaitHandle;
		float WaitTime = 0.5f;
		GetWorld()->GetTimerManager().SetTimer(WaitHandle, FTimerDelegate::CreateLambda([&]()
			{
				bCanInteract = true;
			}), WaitTime, false);
            
        // Tick 필요없을 땐 꺼주고 필요할 땐 켜주는 용도
		SetActorTickEnabled(true);
		if (_GaugeWidget)
		{
			GaugeWidget = Cast<UGaugeWidget>(CreateWidget(GetWorld(), _GaugeWidget));
			if (GaugeWidget)
			{
				GaugeWidget->AddToViewport();
				bIsInteracting = true;
			}
		}
	}
}

void AInteractableObject_Valve::StopInteract()
{
	MainCharacter->bIsActivatingValve = false;
	bIsInteracting = false;

	// 게이지 위젯을 0으로 둔다. 원래는 리버스되면 게이지를 점점줄이고 다시 e키를 누르면 줄어들던 시점부터 다시
    // 상호작용되게 했지만 돌아갈때 못잡게 하면서 수정함
	GaugeWidget->SetValveGauge(0, false);
	if (GaugeWidget->IsInViewport())
	{
		GaugeWidget->RemoveFromParent();
	}
}

void AInteractableObject_Valve::LinkedActorAction(float Gauge)
{
	if (LinkedActor)
	{
    	// 벨브와 같이 sin함수로 등속회전이 아니라 비잉글 비잉글 돌아가게함
		LinkedActor->AddActorWorldOffset(FVector(0, 0, LinkedActorSpeed * abs(sin(Gauge / 3 / ValveRotationCycleTime))));
	}
}


void AInteractableObject_Valve::ProgressingGauge()
{
	if (UKismetMathLibrary::NearlyEqual_FloatFloat(ValveGauge, 100.0f, 0.5f))
	{
    	// 게이지 100까지 차면
		ValveGaugeComplete();
	}
	else
	{
    	// 게이지 100까지 안차면
		MainCharacter->bIsActivatingValve = true;
		ValveGauge += ValveGaugeSpeed;
		ValveGauge = FMath::Clamp(ValveGauge, 0.0f, 100.0f);
		GaugeWidget->SetValveGauge(ValveGauge, true);

		// 벨브 비잉글 비잉글 회전. sin함수크기만큼 더하고 나누기 2를 해도 되지만 절대값 사용해서 더해줌
		float ValveRotationYawValue = abs(sin((ValveGauge / 3) * (1 / ValveRotationCycleTime))) * ValveRotationSpeed;
		StaticMesh->AddRelativeRotation(FRotator(0, ValveRotationYawValue, 0));
		ValveRotateValue += ValveRotationYawValue;
		LinkedActorAction(ValveGauge);
	}
}

void AInteractableObject_Valve::ReverseGauge()
{
	MainCharacter->bIsActivatingValve = false;
	if (!UKismetMathLibrary::NearlyEqual_FloatFloat(ValveRotateValue, 0.0f, 0.1) && ValveRotateValue > 0.0f)
	{
    	// 이거 때문에 시간을 좀 썼다. 아래에서 설명
		if (ValveRotateValue - ValveReverseSpeed >= 0)
		{
			ReverseGaugeEvent(1);
		}
		else
		{
			ReverseGaugeEvent(2);
		}
		bCanInteract = false;
	}
	else
	{
		SetActorTickEnabled(false);
		bCanInteract = true;
	}
}

void AInteractableObject_Valve::ReverseGaugeEvent(float FSpeed)
{
	ValveRotateValue -= ValveReverseSpeed / FSpeed;
	ValveGauge -= ValveReverseSpeed / FSpeed;
	StaticMesh->AddRelativeRotation(FRotator(0, -ValveReverseSpeed / FSpeed, 0));
	if (LinkedActor)
	{
		LinkedActor->AddActorWorldOffset(FVector(0, 0, -7 * LinkedActorSpeed / FSpeed));
	}
}

void AInteractableObject_Valve::ValveGaugeComplete()
{
	bCanInteract = false;
	HightlightableComponent->DestroyComponent();
	StaticMesh->SetRenderCustomDepth(false);
	bIsFinished = true;
	MainCharacter->bIsActivatingValve = false;
	if (GaugeWidget->IsInViewport())
	{
		GaugeWidget->RemoveFromParent();
	}
	if (HorrorAudio)
	{
		HorrorAudio->FadeOut(2, 0);
	}
}

 위의 코드 중 ReversGauge()에서 중간에 ReverseGaugeEvent(n)을 넣어둔 곳이 있다. 저 부분에서 시간을 좀 잡아먹었는데 이유는 밸브를 돌릴 때는 매 프레임마다 0.1f씩 게이지가 차는데 되돌아올 땐 3.5f씩 마이너스가 돼서 E키를 누르자마자 손을 떼면 게이지가 음수 값을 갖게 되어 밸브 처음 Rotation 값보다 더 돌아가버리고 인터랙트 하는 문 액터도 바닥으로 들어가 버리게 된다.(밸브 돌릴 때 위로 올라가기 때문에) 그래서 음수 값이 나오는 경우라면 리버스를 좀 덜 시키게 조정을 한 것이다. 약간 하드코딩적인 방법 같긴 하지만 아직 내 수준에선 최선인 것 같다,,

 아래는 위젯 코드이고 위젯 만드는 법은 링크 첨부하겠다.

// ValveGaugeWidget.cpp
#include "GaugeWidget.h"
#include "Kismet/KismetMathLibrary.h"

void UValveGaugeWidget::NativeTick(const FGeometry& MyGeometry, float DeltaTime)
{
	Super::NativeTick(MyGeometry, DeltaTime);
	if (bIsInteracting)
	{
    	// 메테리얼의 파라미터 값을 변경해 circle gauge를 채운다.
		GaugeImage->GetDynamicMaterial()->SetScalarParameterValue(FName("Decimal"), LerpValveGauge);
	}
}

// 벨브 게이지를 실시간 셋해준다.
void UValveGaugeWidget::SetValveGauge(float Gauge, bool b)
{
	LerpValveGauge = Gauge / 100;
	bIsInteracting = b;
}

- Circle Gauge Widget

https://www.youtube.com/watch?v=7e2LIOdG9NU 

결과:

https://youtu.be/E8aADQn2aGk

 

 좀 급하게 적은 느낌이 있지만 사실 어렵지 않은 방식의 집합이라 더 설명하면 난잡해질 것 같아 이런 방식으로 작성했다.