[UE5](C++) 아이템, 무기 픽업
언리얼 엔진 C++ 공부중이며 건설적인 비판이나 수정사항 요청은 언제든지 환영합니다!
RPG 게임에서의 핵심적인 요소 중 하나인 아이템 픽업을 구현해보았다.
아이템 픽업을 구현하는 방법은 여러가지가 있겠지만,
내가 구현한 방식은 델리게이트, 콜리전 프로파일, 소켓, 오버라이딩에 대한 이해가 필요하다.
델리게이트를 활용한 충돌 이벤트
우선, 아이템 클래스를 만든다.
필자는 cpp 클래스의 이름을 MyItem로 정했다.
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
32
33
34
35
36
37
38
39
40
41
42
43
44
|
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyItem.generated.h"
UCLASS()
class PRACTICEJ_API AMyItem : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMyItem();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
protected:
UPROPERTY(EditAnywhere)
USceneComponent* PickupRoot;
UPROPERTY(EditAnywhere)
UStaticMeshComponent* PickupMesh;
UPROPERTY(EditAnywhere)
UShapeComponent* PickupBox;
UFUNCTION()
virtual void OnPlayerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
virtual void OnPlayerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex);
};
|
cs |
헤더파일에 PickupRoot, PickupMesh, PickupBox 를 추가적으로 각각 선언해준다.
PickupRoot는 USceneComponent를 사용해서 간단한 transform 기능을 활용하도록 하고,
PickupMesh는 스태틱 메쉬, 즉 아이템 에셋을 넣기 위해 UStaticMeshComponent를 사용했다.
PickupBox는 픽업 이벤트를 처리하기 위한 UShapeComponent를 사용했는데,
UBoxComponent라고 명시하지 않은 이유는 나중에 수정해야하는 상황이 생길경우 전체를 바꿔야할 수 있기 때문이다.
그리고 OnPlayerBeginOverlap은 OnComponentBeginOverlap이라는 객체끼리의 충돌을 감지하는 델리게이트를 활용하기 위해 선언한 함수이며, OnPlayerEndOverlap도 마찬가지이다.
virtual 키워드는 이후에 아이템을 상속받은 무기를 픽업했을 경우엔 이벤트를 다르게 적용해주기 위해서 넣었다. 무기는 소켓에 장착될 수 있게 하기 위해서이다.
보면 파라미터가 상당히 많은데, 여기서 가장 중요한건 AActor* OtherActor 부분으로 충돌하는 다른 객체를 의미한다.
https://dev.epicgames.com/community/learning/tutorials/zw7m/hits-and-overlaps-bp-c-multiplayer
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
|
#include "MyItem.h"
#include <Components/BoxComponent.h>
// Sets default values
AMyItem::AMyItem()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
PickupRoot = CreateDefaultSubobject<USceneComponent>(TEXT("PickupRoot"));
RootComponent = PickupRoot;
PickupMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("PickupMesh"));
PickupMesh->AttachToComponent(PickupRoot, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
PickupBox = CreateDefaultSubobject<UBoxComponent>(TEXT("PickupBox"));
PickupBox->SetWorldScale3D(FVector(1.0f, 1.0f, 1.0f));
PickupBox->SetGenerateOverlapEvents(true);
PickupBox->SetCollisionProfileName(TEXT("MyCollectible"));
PickupBox->AttachToComponent(PickupRoot, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
PickupBox->OnComponentBeginOverlap.AddDynamic(this, &AMyItem::OnPlayerBeginOverlap);
PickupBox->OnComponentEndOverlap.AddDynamic(this, &AMyItem::OnPlayerEndOverlap);
}
|
cs |
CPP에서는 헤더파일에 선언된 변수와 함수를 생성자에서 구현했다.
PickupMesh와 PickupBox를 PickupRoot에 붙이고,
PickupBox를 보면 MyCollectible이라는 콜리전 프로파일로 설정한 것을 볼 수 있는데,
이는 어떤 것은 충돌을 하게끔 하고, 어떤 것은 충돌을 하지 않게끔 정해놓은 것이라 보면 된다.
그리고 마지막 두줄은 델리게이트를 활용한 것으로, 델리게이트에 대한 설명은 언리얼 엔진 공식 문서에 자세히 나와있다.
요약하면 델리게이트는 C++오브젝트의 멤버함수를 안전하게 호출하기 위해서 사용되는 것으로,
C#에서는 지원하지만 C++에서는 기본적으로 지원하지 않기 때문에 좀 더 안전한 함수포인터를 만들었다라고 생각한다.
따라서 PickupBox에서 다이나믹 델리게이트(런타임 바인딩 가능)에 함수를 추가하기 위해 AddDynamic을 사용했다.
1
2
3
4
|
void AMyItem::OnPlayerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
Destroy();
}
|
cs |
그리고 당장은 OnPlayerBeginOverlap에는 간단하게 충돌했을 경우 아이템을 사라지도록 만들었다.
이후에는 언제든지 아이템을 먹으면 인벤토리에 저장이 되는 등의 구현 부분이 들어갈 수 있다.
콜리전 프로파일
다음 글을 읽기 전에 언리얼 문서를 보면 더 쉽게 이해가 가능하다.
https://www.unrealengine.com/ko/blog/collision-filtering
프로젝트 세팅->콜리전을 들어가면 다음과 같이 두개의 채널과 프리셋이 존재한다.
오브젝트 채널에 MyCharacter와 MyCollectible을 추가한다.
각각 기본반응은 블록, 무시로 설정한다.
그리고 맨 아래 프리셋 항목에서 새 프로파일을 만들고 다음과 같이 체크박스를 표시했다.
MyCollectible과 MyCharacter가 충돌했을시 서로 이벤트를 발생하기 위해서 오버랩으로 표시하고,
MyCollectible과 MyCollectible은 무시, MyCharacter와 MyCharacter가 만나면 충돌하게끔 표시했다.
이제 MyItem기반 블루프린트를 만들고,
메시->콜리전에서 콜리전 프리셋을 아까 수정했던 프로파일로 대체한다. (메시는 빨간 물약으로 했다)
MyCharacter도 콜리전 프리셋을 수정하자.
맵에 배치한뒤 가까이 다가가보면,
잘 없어지는 것을 확인 할 수 있다.
소켓
일반 아이템이 아닌 무기를 획득한다고 가정하자.
캐릭터 메시 에셋을 클릭하면 캐릭터 스켈레톤이 나온다.
소켓은 원하는 곳 어디에나 붙일 수 있으나 필자는 RightHand에 소켓을 붙였다.
소켓 이름은 나중에 코드를 작성할때 틀리지 않도록 복사해두자.
프리뷰 아이템을 넣어서 손에 자연스럽게 잡히도록 해준다.
MyCharacter 클래스에 bHasWeapon 이라는 bool 함수를 만들고 기본값을 false 라고 설정한다.
이후 MyItem 파생 C++ 클래스를 생성하고, 이름은 필자는 MyWeapon이라고 이름을 지었다. (자식 클래스)
이제 MyItem의 OnPlayerBeginOverlap을 오버라이딩해서 MyWeapon만의 새로운 기능을 추가한다.
새로운 기능이란? 충돌 이벤트가 발생하면 무기를 캐릭터 소켓에 붙이는 것!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include "MyWeapon.h"
#include "MyCharacter.h"
void AMyWeapon::OnPlayerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (GEngine) GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("Child override"));
auto player = Cast<AMyCharacter>(OtherActor);
if (player && !(player->bHasWeapon))
{
FName WeaponSocket(TEXT("RightHandWeaponSocket"));
this->AttachToComponent(player->GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, WeaponSocket);
player->bHasWeapon = true;
}
}
|
cs |
GEngine은 함수가 잘 오버라이딩 됐는지 확인하려고 넣은 것이고 (GEngine은 항상 참이 보장되지 않아서 조심),
중요한건 player 객체를 불러와서 잘 불러와졌다면 무기를 가지고 있는지 확인하고, 무기를 갖고 있지 않다면
플레이어의 소켓에 무기를 붙여주면 된다. 무기를 붙인 이후에는 bHasWeapon = true라고 설정한다.
MyWeapon 기반 블루프린트도 만들어주고, 똑같이 콜리전도 MyCollectible로 설정한다.
이제 맵에 배치해서 다가가보면,
무기가 소켓에 잘 붙는 것을 확인할 수 있다!
그리고 다른 무기도 넣어봤는데 bHasWeapon = true로 설정되므로 줍지 않는 것을 확인할 수 있다.
다음에는 무기를 주울때 특정키를 눌러서 줍게 하거나, Drop 기능을 구현해서 언제든지 무기를 바꿀 수 있도록 해야겠다.