PickupWidget Blueprint
무기를 주울 때 사용할 Widget을 만들 것입니다.
Item이 이를 가질 수 있게 UWidgetComponent 포인터 변수를 생성해 줍니다.
Item.h
// Popup widget for when the player looks at the item
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item Properties", meta = (AllowPrivateAccess))
class UWidgetComponent* PickupWidget;
UMG를 다루기 위해서 Shooter.Build.cs 파일을 찾아 PublicDependencyModuleNames.AddRange에 "UMG"를 추가해 줍니다.
컴파일하고 HUD 폴더에서 BP_PickupWidget을 만들어 줍니다.
이후 BP_PickupWidget에 들어와 좌상단의 Fill Screen을 Custom으로 바꿔 줍니다.
아래와 같이 Overlay와 Image를 이용해서 만들어 줍니다.
Horizontal Box를 설치합니다.
Vertical Box를 두 개 생성하고 25 : 75 비율로 크기를 만들어줍니다.
왼쪽 상자에 Vertical Box를 두개 넣어줍니다.( 7 : 3)
Left Top Box에 여러 개를 겹쳐 넣을 것임으로 Overlay를 설치하고 Fill과 Aliment로 범위를 전체로 설정해 줍니다.
Canvas Panel도 마찬가지입니다.
DefaultMap으로 돌아와서 _Game > Assets > Texture > Icons 폴더를 만들어 줍니다. 이후 Texture를 다운로드합니다.
다운로드한 Texture(IconCircleOutlined2)의 Compression Settings를 UserInterface2D로 바꾸어 줍니다.
이후 아래와 같이 만들어 줍니다.
Finishing the Pickup Widget
반복되는 내용이 많아 만드는 방법은 생략하겠습니다.
Hierachy를 참고하여 만들어 줍시다.
폰트를 다운로드하고 Unreal에 import 하면 UMG에서 사용이 가능합니다.
이후 UI에 맞는 폰트를 적용합니다.
Add Widget to Weapon
지금까지 만든 위젯을 Weapon(Item)에 붙이는 작업을 하겠습니다.
미리 선언해 놓은 PickupWidget을 생성자에서 초기화해줍니다.
Item.cpp
#include "Components/WidgetComponent.h"
AItem::AItem()
{
...
PickupWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("PickupWidget"));
PickupWidget->SetupAttachment(ItemMesh);
}
컴파일하면 BP_BaseWeapon에 Pickup Widget이 생성된 것을 알 수 있습니다.
HUD에서 보여줄 것이기 때문에, User Interface > Space를 Screen으로 바꿔줍니다.
Widget Class, Draw size도 변경해 줍니다.
+궁금해서 Space가 Screen일 때, World일 때를 비교해 보았다.
World의 경우 신기하게 위젯이 뒤에서는 보이지 않는 것을 알 수 있었습니다.
Trace for Widget
LineTrace를 통해서 Item을 쳐다볼때만 Widget이 생성되도록 할것입니다.
우선 Z : 70 정도로 위젯을 올려줍니다.
제일 먼저 할 일은 처음 시작할때 위젯의 visibility를 false로 만드는 것입니다.
Item.cpp
// Called when the game starts or when spawned
void AItem::BeginPlay()
{
Super::BeginPlay();
PickupWidget->SetVisibility(false);
}
ShooterCharacter에서 총을 쏘았을 때처럼 LineTrace를 통해서 물체를 확인하는 함수를 만들어 줍니다.
ShooterCharacter.h
// Line trace for items under the crosshairs
bool TraceUnderCrosshairs(FHitResult &OutHitResult);
ShooterCharacter.cpp
bool AShooterCharacter::TraceUnderCrosshairs(FHitResult &OutHitResult)
{
// Get Viewport Size
FVector2D ViewportSize;
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->GetViewportSize(ViewportSize);
}
// Get screen space location of crosshirs
FVector2D CrosshairLocation(ViewportSize.X / 2.f, ViewportSize.Y / 2.f);
FVector CrosshairWorldPosition;
FVector CrosshairWorldDirection;
// Get world position and direction of crosshairs
bool bScreenToWorld = UGameplayStatics::DeprojectScreenToWorld(
UGameplayStatics::GetPlayerController(this, 0),
CrosshairLocation,
CrosshairWorldPosition,
CrosshairWorldDirection);
if (bScreenToWorld)
{
// Trace from Crosshair world location outward
const FVector Start{CrosshairWorldPosition};
const FVector End{Start + CrosshairWorldDirection * 500'000};
GetWorld()->LineTraceSingleByChannel(
OutHitResult,
Start,
End,
ECollisionChannel::ECC_Visibility);
return true;
}
return false;
}
Tick에서 TraceUnderCorsshairs()를 사용한 로직을 작성해 줍니다.
#include "AItem.h"
...
void AShooterCharacter::Tick(float DeltaTime)
{
...
// #include "AItem.h"
FHitResult ItemTraceResult;
TraceUnderCrosshairs(ItemTraceResult);
if (ItemTraceResult.bBlockingHit)
{
AItem *HitItem = Cast<AItem>(ItemTraceResult.GetActor());
if(HitItem){
}
}
}
AItem의 PickupWidget의 visibility 속성을 바꾸면 되는데, 이때 private인 멤버 변수에 접근할 수 없으니 Public Setter 함수를 만들어 줍시다.
Item.h
public:
FORCEINLINE UWidgetComponent *GetPickupWidget() const { return PickupWidget; }
※ 헤더에서 구현 했으므로, PickupWidget 보다 함수가 아래 있어야 합니다. (실수했음...)
ShooterCharacter.cpp
#include "Components/WidgetComponent.h"
...
void AShooterCharacter::Tick(float DeltaTime)
{
...
FHitResult ItemTraceResult;
TraceUnderCrosshairs(ItemTraceResult);
if (ItemTraceResult.bBlockingHit)
{
AItem *HitItem = Cast<AItem>(ItemTraceResult.GetActor());
if (HitItem && HitItem->GetPickupWidget())
{
// Show Item's Pckup Widget
HitItem->GetPickupWidget()->SetVisibility(true);
}
}
}
Item의 CollisionBox의 Response를 생성자에서 설정해 줍니다.(Visibility만 block 당하게)
Item.cpp
// Sets default values
AItem::AItem()
{
...
CollisionBox = CreateDefaultSubobject<UBoxComponent>(TEXT("CollisionBox"));
CollisionBox->SetupAttachment(ItemMesh);
CollisionBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
CollisionBox->SetCollisionResponseToChannel(
ECollisionChannel::ECC_Visibility,
ECollisionResponse::ECR_Block);
...
}
컴파일 이후 BP_WeaponBase에서 CollisionResponse 셋팅이 잘 됬는지 확인하고 안됬다면, 직접 바꿔주어야 합니다.
실행 사진
Refactor Trace Under Crosshairs
저번 시간에 TraceUnderCrosshairs()를 통해 LineTrace를 이용하여 Widget을 띄웠는데, 중복되는 LineTrace 코드를 리펙토링해 보겠습니다.
GetBeamEndLocation()은 Screen에서 World까지 검사를 하고 총구에서 World까지 두번의 계산을 함을 기억합시다. MuzzleSocketLocation과 OutBeamLocation을 반환하는 GetBeamEndLocation() 함수를 피펙토링 해봅시다.
ShooterCharacter.cpp
bool AShooterCharacter::GetBeamEndLocation(
const FVector &MuzzleSocketLocation,
FVector &OutBeamLocation)
{
FHitResult CrosshairHitResult;
bool bCrosshairHit = TraceUnderCrosshairs(CrosshairHitResult);
if(bCrosshairHit){
// Tentative beam location - still need to trace from gun
OutBeamLocation = CrosshairHitResult.Location;
}else
{
}
...
}
이때, else 구문은 LineTrace의 사정거리에 닫지 않았을때 실행 되는데, 이때의 OutBeamLocation을 직접 계산할 수도 있지만, TraceUnderCrosshairs의 매개변수를 수정하여 같이 반환되게 만들겠습니다.
bool TraceUnderCrosshairs(FHitResult &OutHitResult, FVector &OutHitLocation);
bool AShooterCharacter::TraceUnderCrosshairs(FHitResult &OutHitResult, FVector &OutHitLocation)
{
...
if (bScreenToWorld)
{
const FVector Start{CrosshairWorldPosition};
const FVector End{Start + CrosshairWorldDirection * 500'000};
OutHitLocation = End;
...
if (OutHitResult.bBlockingHit)
{
OutHitLocation = OutHitResult.Location;
return true;
}
}
...
}
그 다음 아래와 같이 코드를 변경해 줄 수 있습니다. else 부분이 빈 이유는 TraceUnderCrosshairs 함수에서 자체적으로 OutBeamLocation을 상황에 맞게 설정해주기 때문입니다. (가시성을 위한 else 구문)
bool AShooterCharacter::GetBeamEndLocation(
const FVector &MuzzleSocketLocation,
FVector &OutBeamLocation)
{
FHitResult CrosshairHitResult;
bool bCrosshairHit = TraceUnderCrosshairs(CrosshairHitResult, OutBeamLocation);
if (bCrosshairHit)
{
// Tentative beam location - still need to trace from gun
OutBeamLocation = CrosshairHitResult.Location;
}
else // no crosshair trace hit
{
// OutBeamLocation is the End location for the line trace
}
아래에 생성되있는 이 코드를 끌어올려줍니다.
AShooterCharacter::GetBeamEndLocation
// Perform a second trace, this time from the gun barrel
FHitResult WeaponTraceHit;
const FVector WeaponTraceStart{MuzzleSocketLocation};
const FVector WeaponTraceEnd{OutBeamLocation};
GetWorld()->LineTraceSingleByChannel(
WeaponTraceHit,
WeaponTraceStart,
WeaponTraceEnd,
ECollisionChannel::ECC_Visibility);
if (WeaponTraceHit.bBlockingHit) // object between barrel and BeamEndPoint?
{
OutBeamLocation = WeaponTraceHit.Location;
return true;
}
return false;
이후 이 밑의 코드들은 삭제합니다. (이제부터 TraceUnderCrosshairs()가 화면 중앙과 World를 검사하고 GetBeamEndLocatoin이 총구와 World를 검사합니다.)(Single Response?)
완성된 GetBeamEndLocation입니다.
bool AShooterCharacter::GetBeamEndLocation(
const FVector &MuzzleSocketLocation,
FVector &OutBeamLocation)
{
FHitResult CrosshairHitResult;
bool bCrosshairHit = TraceUnderCrosshairs(CrosshairHitResult, OutBeamLocation);
if (bCrosshairHit)
{
// Tentative beam location - still need to trace from gun
OutBeamLocation = CrosshairHitResult.Location;
}
else // no crosshair trace hit
{
// OutBeamLocation is the End location for the line trace
}
// Perform a second trace, this time from the gun barrel
FHitResult WeaponTraceHit;
const FVector WeaponTraceStart{MuzzleSocketLocation};
const FVector WeaponTraceEnd{OutBeamLocation};
GetWorld()->LineTraceSingleByChannel(
WeaponTraceHit,
WeaponTraceStart,
WeaponTraceEnd,
ECollisionChannel::ECC_Visibility);
if (WeaponTraceHit.bBlockingHit) // object between barrel and BeamEndPoint?
{
OutBeamLocation = WeaponTraceHit.Location;
return true;
}
return false;
}
※ 한가지 문제가 발생합니다. Crosshair에서 Rotation * 500'000 거리의 Vector와 총구에서 Rotation * 500'000 거리의 Vector는 다릅니다. 따라서 아래와 같이 됩니다.
GetBeamEndLocation의 총구에서 계산하는 코드를 수정해서 이를 보완해줍니다.
bool AShooterCharacter::GetBeamEndLocation(
const FVector &MuzzleSocketLocation,
FVector &OutBeamLocation)
{
...
const FVector WeaponTraceStart{MuzzleSocketLocation};
const FVector StartToEnd{OutBeamLocation - MuzzleSocketLocation};
const FVector WeaponTraceEnd{MuzzleSocketLocation + StartToEnd * 1.25f};
...
}
- const FVector WeaponTraceStart{MuzzleSocketLocation} : 총구의 위치
- const FVector StartToEnd{OutBeamLocation - MuzzleSocketLocation} : 총구에서 OutBeamLocation 까지의 거리
- const FVector WeaponTraceEnd{MuzzleSocketLocation + StartToEnd * 1.25f} : 거리를 1.25배해서 보정(Crosshair보다 길게)
Widget Trace When Close
하나의 Sphere 컴포넌트를 만들어서 이것과 캐릭터가 겹쳐있을 때만 Widget이 나오도록 해보겠습니다.
우선 Item에서 USphereComponent를 만들고 초기화 해줍니다.
Item.h
// Enables item tracing when overlapped
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item Properties", meta =(AllowPrivateAccess))
class USphereComponent * AreaSphere;
Item.cpp
// Sets default values
AItem::AItem()
{
...
AreaSphere = CreateDefaultSubobject<USphereComponent>(TEXT("AreaSphere"));
AreaSphere->SetupAttachment(GetRootComponent());
}
BeginPlay에서 겹쳐졌을 때 이벤트를 발생 시키기 위한 구문을 작성합니다. ※ 두번째 인자가 함수여서 지금은 pass
Item.cpp
void AItem::BeginPlay()
{
...
// Setup overlap for AreaSphere
AreaSphere->OnComponentBeginOverlap.AddDynamic(this, );
}
- OnComponentBeginOverlap() : USphereComponent에 있는 이벤트로, 다른 컴포넌트가 이 스피어 컴포넌트와 겹치기 시작할 때 발생합니다.
- AddDynamic: 이것은 이벤트에 대한 리스너(listener)를 동적으로 추가하는 함수입니다. AddDynamic 함수는 런타임에 이벤트와 연결될 함수를 지정합니다.
- this: 첫 번째 인자로, 현재 클래스의 인스턴스를 나타냅니다. 즉, 이벤트가 발생했을 때 호출될 함수가 이 클래스 내에 있다는 것을 의미합니다.
- 두 번째 인자 : AddDynamic에는 두 번째 인자로 실제 이벤트가 발생했을 때 호출될 함수를 지정해야 합니다. 이 함수는 특정 형식을 따라야 하며, 오버랩 이벤트가 발생했을 때 실행될 로직을 포함합니다.
위의 설명했듯이 AddDynamic에는 두 번째 인자로 함수를 넣어주여야 하기 때문에, 겹치기 시작할때 실행시킬 함수를 만들어 줍니다.
언리얼 엔진에서 AddDynamic을 사용하여 이벤트에 함수를 바인딩할 때
해당 함수는 특정 조건을 만족해야 합니다. 이는 특히 OnComponentBeginOverlap 이벤트와 같은 충돌 관련 이벤트에 해당됩니다. AddDynamic의 두 번째 인자로 사용되는 함수는 다음과 같은 조건을 갖추어야 합니다:
매개변수(Parameter):
- OnComponentBeginOverlap 이벤트의 경우, 함수는 특정 매개변수를 가져야 합니다. 일반적으로 이들 매개변수는 다음과 같습니다:
- UPrimitiveComponent* OverlappedComponent : 이벤트가 발생한 컴포넌트입니다.
- AActor* OtherActor : 컴포넌트와 겹친 다른 액터입니다.
- UPrimitiveComponent* OtherComp : 컴포넌트와 겹친 다른 컴포넌트입니다.
- int32 OtherBodyIndex : 겹친 다른 컴포넌트의 바디 인덱스입니다.
- bool bFromSweep : 이 이벤트가 스윕(sweep)에 의해 발생했는지 여부입니다.
- const FHitResult & SweepResult : 스윕에 대한 세부 정보를 담고 있는 FHitResult 구조체입니다.
※ 주의! 함수를 "이벤트 핸들러를 등록" 또는 "델리게이트에 바인딩" 할때는 UFUNCTION을 사용해 주어야 합니다!
Item.h
protected:
UFUNCTION()
void OnSphereOverlap(
UPrimitiveComponent *OverlappedComponent,
AActor *OtherActor,
UPrimitiveComponent *OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult &SweepResult);
Item.cpp
// Called when the game starts or when spawned
void AItem::BeginPlay()
{
...
// Setup overlap for AreaSphere
AreaSphere->OnComponentBeginOverlap.AddDynamic(this, &AItem::OnSphereOverlap);
}
위젯을 비활성화를 하는 기능을 위해 Overlap이 끝나면 위젯이 비활성화 되는 코드도 작성해 줍니다.
Item.h
UFUNCTION()
void OnSphereEndOverlap(
UPrimitiveComponent *OverlappedComponent,
AActor *OtherActor,
UPrimitiveComponent *OtherComp,
int32 OtherBodyIndex);
Item.cpp
void AItem::BeginPlay()
{
...
// Setup overlap for AreaSphere
AreaSphere->OnComponentBeginOverlap.AddDynamic(this, &AItem::OnSphereOverlap);
AreaSphere->OnComponentEndOverlap.AddDynamic(this, &AItem::OnSphereEndOverlap);
}
ShooterCharacter에서 bShouldTraceItems와
ShooterCharacter.h
// True if we should trace every frame for iterms
bool bShouldTraceForItems;
// Number of overlapped AItems
int8 OverlappedItemCount;
ShooterCharacter.cpp
// Sets default values
AShooterCharacter::AShooterCharacter() : ...
// Item trace vairable
bShouldTraceForItems(false),
OverlappedItemCount(0)
ShooterCharacter.h
public:
FORCEINLINE int8 GetOverlappedItemCount() const { return OverlappedItemCount; }
// Adds/subtracts to/from OverlappedItemCount and updates bShouldTraceForItems
void IncrementOverlappedItemCount(int8 Amount);
ShooterCharacter.cpp
void AShooterCharacter::IncrementOverlappedItemCount(int8 Amount)
{
if (OverlappedItemCount + Amount <= 0)
{
OverlappedItemCount = 0;
bShouldTraceForItems = false;
}
else
{
OverlappedItemCount += Amount;
bShouldTraceForItems = true;
}
}
Item으로 돌아와서 Ovelap이 일어날 때마다 IncrementOverlappedItemCount 함수를 이용해서 ShooterCharacter의 상태를 정해줍니다.
Item.cpp
void AItem::OnSphereOverlap(UPrimitiveComponent *OverlappedComponent,
AActor *OtherActor,
UPrimitiveComponent *OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult &SweepResult)
{
if (OtherActor)
{
AShooterCharacter *ShooterCharacter = Cast<AShooterCharacter>(OtherActor);
if (ShooterCharacter)
{
ShooterCharacter->IncrementOverlappedItemCount(1);
}
}
}
void AItem::OnSphereEndOverlap(UPrimitiveComponent *OverlappedComponent,
AActor *OtherActor,
UPrimitiveComponent *OtherComp,
int32 OtherBodyIndex)
{
if (OtherActor)
{
AShooterCharacter *ShooterCharacter = Cast<AShooterCharacter>(OtherActor);
if (ShooterCharacter)
{
ShooterCharacter->IncrementOverlappedItemCount(-1);
}
}
}
이후 ShooterCharacter.cpp > Tick 에서 Item LineTrace 부분을 bShouldTraceForItems가 True일때만 사용하게 하여 효율성을 높혀줍니다.
// Called every frame
void AShooterCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
CameraInterpZoom(DeltaTime);
// Change look sensitivity based on aiming
SetLookRates();
// Calculate crosshair spread multiplier
CalculateCrosshairSpread(DeltaTime);
if (bShouldTraceForItems)
{
FHitResult ItemTraceResult;
FVector ItemTraceLocation;
TraceUnderCrosshairs(ItemTraceResult, ItemTraceLocation);
if (ItemTraceResult.bBlockingHit)
{
AItem *HitItem = Cast<AItem>(ItemTraceResult.GetActor());
if (HitItem && HitItem->GetPickupWidget())
{
// Show Item's Pckup Widget
HitItem->GetPickupWidget()->SetVisibility(true);
}
}
}
}
수정한 if절을 함수로 만들어서 사용하겠습니다.
ShooterCharacter.h
// Trace for items if OverlappedItemCount > 0
void TraceForItems();
ShooterCharacter.cpp
void AShooterCharacter::TraceForItems()
{
if (bShouldTraceForItems)
{
FHitResult ItemTraceResult;
FVector ItemTraceLocation;
TraceUnderCrosshairs(ItemTraceResult, ItemTraceLocation);
if (ItemTraceResult.bBlockingHit)
{
AItem *HitItem = Cast<AItem>(ItemTraceResult.GetActor());
if (HitItem && HitItem->GetPickupWidget())
{
// Show Item's Pckup Widget
HitItem->GetPickupWidget()->SetVisibility(true);
}
}
}
}
// Called every frame
void AShooterCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
CameraInterpZoom(DeltaTime);
// Change look sensitivity based on aiming
SetLookRates();
// Calculate crosshair spread multiplier
CalculateCrosshairSpread(DeltaTime);
// Check OverlappedItemCount, then trace for items
TraceForItems();
}
'Unreal 공부 > Unreal Engine 4 C++ The Ultimate Shooter' 카테고리의 다른 글
The Weapon - 2 (0) | 2024.02.02 |
---|---|
The Weapon - 1 (0) | 2024.01.05 |
Aiming and Crosshairs - 3 (0) | 2024.01.04 |
Aiming and Crosshairs - 2 (1) | 2024.01.04 |
Aiming and Crosshairs - 1 (0) | 2023.12.31 |