일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- deltaTime
- Unreal Engine 4
- 아이작 맵 생성
- tick 사용하지 않고
- tick처럼 사용
- multithreard chat
- bluezone
- 채팅 서버
- 언리얼
- Ai
- unity
- ue4 Crash
- Fresh cooked tweens
- Unreal Engine 5
- 13iew
- LittleNightMare
- UE
- UE4
- 언리얼 자기장
- UE5
- Random Map Generator
- unreal engine
- Chat Server
- Unreal Engine Error
- ue4 error
- game ability system
- UnrealEngine
- unreal engine 5 tween
- 랜덤 맵 생성
- Unity Engine
- Today
- Total
Class GameDev* SheepAdult
[Unreal Engine 4.27] Round Lever / Valve System C++ 본문
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
결과:
좀 급하게 적은 느낌이 있지만 사실 어렵지 않은 방식의 집합이라 더 설명하면 난잡해질 것 같아 이런 방식으로 작성했다.
'Unreal Project' 카테고리의 다른 글
[Unreal Engine 4.27 C++] Change Direction (방향 전환) Animation (0) | 2022.08.09 |
---|---|
[Unreal Engine C++] CheckPoint & Load Different Animation (0) | 2022.07.12 |
[Unreal Engine 4.27] Widget Animation Delegate C++ (0) | 2022.05.14 |
[Unreal Engine 4.27] Climbing Pole/Rope System C++ (0) | 2022.05.04 |
[Unreal Engine 4.27] Multi Slot Save & Load Game C++ (0) | 2022.05.03 |