언리얼 엔진의 '소켓 시스템'을 활용해
캐릭터 에셋에 액새서리를 부착하는 방법을 학습한다.
물리 엔진 기능을 활용해 플레이어 캐릭터만 감지하는 아이템 상자를 제작하고
상자를 먹으면 아이템을 생성한 후 캐릭터에 부착시키는 기능을 구현해 본다.
<1>. 캐릭터 소켓 설정
캐릭터 오른손에 무기를 장착해 보자.
무기는 캐릭터에 트랜스폼으로 배치하는 것이 아니라 메시에 착용해야 한다.
언리얼 엔진은 캐릭터에 무기, 액세서리를 부착하기 위한 용도로 '소켓'이라는 시스템을 제공한다.
※예제에는 'hand_rSocket' 이름의 소켓이 오른쪽 손에 있다.
무기는 스켈레탈 메시이기 때문에 스켈레탈 메시 컴포넌트를 캐릭터 메시에 부착한다.
//ABCharacter.h
UPROPERTY(VisibleAnywhere, Category = Weapon)
USkeletalMeshComponent* Weapon;
//ABCharacter.cpp
FName WeaponSocket(TEXT("hand_rSocket"));
if(GetMesh()->DoesSocketExist(WeaponSocket))
{
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WEAPON"));
static ConstructorHelpers::FObjectFinder<USkeletalMesh>SK_WEAPON(TEXT("(경로)"));
if(SK_WEAPON.Succeeded())
{
Weapon->SetSkeletalMesh(SK_WEAPON.Object);
}
Weapon->SetupAttachment(GetMesh(), WeaponSocket);
}
<2>. 무기 액터의 제작
필요에 따라 무기를 바꿀 수 있게 하려면, 무기를 액터로 분리해 만드는 것이 좋다.
→ "ABWeapon"이라는 이름의 액터 생성
예제에서 무기는 실제로 충돌을 발생시키지 않는다. 그러므로 액터의 루트 컴포넌트인 스켈레탈 메시 컴포넌트의 충돌 설정을 NoCollision으로 지정한다.
//ABWeapon.h
public:
UPROPERTY(VisibleAnywhere, Category=Weapon)
USkeletalMeshComponent* Weapon;
//ABWeapon.cpp
AABWeapon::AABWeapon()
{
PrimaryActorTick.bCanEverTick = false;
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WEAPON"));
RootComponent = Weapon;
static ConstructorHelpers::FObjectFinder<USkeletalMesh>SK_WEAPON(TEXT("(경로)"));
if(SK_WEAPON.Succeeded())
{
Weapon->SetSkeletalMesh(SK_WEAPON.Object);
}
Weapon->SetCollisionProfileName(TEXT("NoCollision"));
}
이제 무기 액터를 따로 만들었으니, 캐릭터의 BeginPlay에서 무기 액터를 생성하고, 이를 캐릭터에 부착시켜 보자.
월드에서 새롭게 액터를 생성하는 명령은 'SpawnActor'이다. SpawnActor의 인자에는 '생성할 액터의 클래스'와 액터가
'앞으로 생성할 위치 및 회전'을 지정한다.
→ 액터는 월드에 존재하므로 이는 월드의 명령어다. GetWorld 함수로 월드의 포인터를 가져와서 함수를 실행한다.
//ABCharacter.cpp
#include "ABWeapon.h"
void AABCharacter::BeginPlay()
{
FName WeaponSocket(TEXT("hand_rSocket"));
auto CurWeapon = GetWorld()->SpawnActor<AABWeapon>(FVector::ZeroVector,FRotator::ZeroRotator);
if(nullptr != CurWeapon)
{
CurWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale,
WeaponSocket);
}
}
<3>. 아이템 상자의 제작
플레이어에게 무기를 공급해 줄 아이템 상자를 제작해 보자.
→ Actor를 부모로 하는 "ABItemBox" 클래스를 생성한다.
아이템 상자는 플레이어를 감지하는 '콜리전 박스'와 아이템 상자를 시각화해 주는 '스태틱메시'로 나뉜다.
→ 루트 컴포넌트에는 박스 콜리전 컴포넌트를, 자식에는 스태틱메시 컴포넌트를 추가한다.
※박스 콜리전 컴포넌트의 Extent 값은 전체 박스 영역 크기의 절반 값을 의미한다.
//ABItemBox.h
public:
UPROPERTY(VisibleAnywhere, Category=Box)
UBoxComponent* Trigger;
UPROPERTY(VisibleAnywhere, Category=Box)
UStaticMeshComponent* Box;
//ABItemBox.cpp
AABItemBox::AABItemBox()
{
PrimaryActorTick.bCanEverTick = false;
Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TRIGGER"));
Box = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BOX"));
RootComponent = Trigger;
Box->SetupAttachment(RootComponent);
Trigger->SetBoxExtent(FVector(40.0f, 42.0f, 30.0f)); //박스 메시 크기가 80cmx82cmx41cm
static ConstructorHelpers::FObjectFinder<UStaticMesh>SM_BOX(TEXT("(경로)"));
if(SM_BOX.Succeeded())
{
Box->SetStaticMesh(SM_BOX.Object);
}
Box->SetRelativeLocation(FVector(0.0f, -3.5f, -30.0f));
}
이제 폰이 아이템을 획득하도록 아이템 상자에 '오브젝트 채널'을 추가한다.→ 콜리전 메뉴에서 ItemBox라는 오브젝트 채널을 생성한다(기본반응 = 무시).→ ABCharacter 오브젝트 채널에만 반응하도록 '겹침' 세팅을 지정한다. (양방향 둘 다)
→ ItemBox라는 프리셋을 추가한다.
새로운 프리셋을 박스 컴포넌트에 설정하고, 박스 컴포넌트에서 캐릭터를 감지할 때 관련된 행동을 구현한다.
박스 컴포넌트에는 Overlap 이벤트를 처리할 수 있게 'OnComponentBeginOverlap'라는 델리게이트가 선언돼 있다.
→ 해당 델리게이트 선언 : FComponentBeginOverlapSignature OnComponentBeginOverlap;
→ FComponentBeginOverlapSignature는 델리게이트 유형이고, 다음과 같이 선언돼 있다.
DECLARE_DYNAMIC_MULTICAST_SixParams(FComponentBeginOverlapSignature, UPrimitiveComponent*,
OverlappedComponent, AActor*, OtherActor, UPrimitiveComponent*, OtherComp, int32, OtherBodyIndex,
bool bFromSweep, const FHitResult &, SweepResult);
→ 이 매크로를 통해, 'OnComponentBeginOverlap'는 멀티캐스트 델리게이트임을 확인할 수 있다.
→ 유형, 인자를 모두 복사해 매크로 설정과 동일한 멤버 함수를 선언하고 이를 해당 델리게이트에 바인딩하면
Overlap 이벤트가 발생할 때마다 멤버 함수가 호출된다.
//ABItemBox.h
class ARENABATTLE_API AABItemBox : public AActor
{
protected:
virtual void PostInitializeComponents() override;
private:
UFUNCTION()
void OnCharacterOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult);
//ABItemBox.cpp
AABItemBox::AABItemBox()
{
Trigger->SetCollisionProfileName(TEXT("ItemBox"));
Box->SetCollisionProfileName(TEXT("NoCollision"));
}
void AABItemBox::PostInitializeComponents()
{
Super::PostInitializeComponents(); //자식 클래스에서 추가된 작업 전에 부모 클래스에서 정의된 초기화 작업.
//override 했기 때문에 해주는 초기화
Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABItemBox::OnCharacterOverlap);
//자식 클래스의 추가 초기화 작업
}
void AABItemBox::OnCharacterOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult);
{
ABLOG_S(Warning);
}
<4>. 아이템의 습득
아이템 상자를 통과하면 빈손의 플레이어에게 아이템을 쥐어주는 기능을 구현해 보자
→ 아이템 상자에 클래스 정보를 저장할 속성을 추가하고, 이를 기반으로 아이템을 생성하도록 기능을 구현한다.
→ 클래스 정보를 저장하는 변수를 선언할 때, UClass 포인터를 사용할 수 있지만, 이를 사용하면 현재 프로젝트에 사용하는 모든 언리얼 오브젝트의 선언이 보이게 됨.
→ 특정/상속받은 클래스들로 목록을 한정하는 'TSubclassof'라는 키워드를 사용하면 목록에서 아이템 상자, 이를 선언한 클래스 목록만 볼 수 있다.
//ABItemBox.h
class ARENABATTLE_API AABItemBox : public AActor
{
public:
UPROPERTY(EditInstanceOnly, Category=Box)
TSubclassOf<class AABWeapon> WeaponItemClass; //클래스 정보를 저장하는 변수
}
//ABItemBox.cpp
#include "ABWeapon.h"
AABItemBox::AABItemBox()
{
WeaponItemClass = AABWeapon::StaticClass(); //WeaponItemClass는 AABWeapon 클래스의 타입 정보를 가진다.
}
캐릭터에 무기를 장착시키는 SetWeapon이라는 멤버 함수를 선언한다.
→ 현재 무기가 없으면, hand_rSocket에 무기를 장착시키고 → 무기 액터의 소유자를 캐릭터로 변경하는 로직을 넣는다.
//ABCharacter.h
class ARENABATTLE_API AABCharacter:public ACharacter
{
public:
bool CanSetWeapon(); //캐릭터에게 무기가 있는지 없는지
void SetWeapon(class AABWeapon* NewWeapon); //무기 장착
UPROPERTY(VisibleAnywhere, Category = Weapon)
class AABWeapon* CurrentWeapon;
}
//ABCharacter.cpp
bool AABCharacter::CanSetWeapon() //현재무기가 없을 때(null일 때), true를 반환
{
return (nullptr == CurrentWeapon);
}
void AABCharacter::SetWeapon(AABWeapon* NewWeapon)
{
FName WeaponSocket(TEXT("hand_rSocket")); //FName타입의 WeaponSocket을 "hand_rSocket"으로 초기화
if(nullptr != NewWeapon)
{
NewWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, WeaponSocket);
//부착할 때 사용하는 변환 규칙. 부모의 위치,회전에 맞춰 부착하지만, 스케일은 무시하는 규칙
NewWeapon->SetOwner(this); //무기 액터의 소유자를 캐릭터로 변경
CurrentWeapon = NewWeapon;
}
}
이제 배치한 상자에 Overlap 이벤트가 발생할 때, 아이템 상자에 설정된 클래스 정보로부터 무기를 생성하고 이를 캐릭터에 장착시키는 기능을 구현한다.
//ABItemBox.cpp
#include "ABCharacter.h"
void AABItemBox::OnCharacterOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult);
{
ABLOG_S(Warning);
auto ABCharacter = Cast<AABCharacter>(OtherActor); //OtherActor를 AABCharacter타입으로 변환
if(nullptr != ABCharacter && nullptr != WeaponItemClass)
{
if(ABCharacter->CanSetWeapon())
{
auto NewWeapon = GetWorld()->SpawnActor<AABWeapon>(WeaponItemClass, FVector::ZeroVector,
FRotator::ZeroRotator);
ABCharacter->SetWeapon(NewWeapon);
}
else
{
ABLOG(Warning, TEXT("%s can't equip weapon currently."), *ABCharacter->GetName());
}
}
}
이번엔 상자에 이펙트를 부착해 아이템을 습득하면 이펙트를 재생하고, 재생이 끝나면 상자가 사라지는 기능을 구현한다.
→ 상자 액터에 파티클 컴포넌트를 추가한 후, 이펙트 에셋을 파티클 컴포넌트의 템플릿으로 지정한다.
→ 멤버 함수를 하나 추가하고 OnSystemFinished(파티클 컴포넌트 시스템에서 제공하는 델리게이트)에 이를 연결해 이펙트 재생이 종료되면 상자가 제거되는 로직을 구성하자.
→ 이펙트가 재생될 때는 액터의 충돌 기능을 제거해 아이템을 두 번 습득하지 못하도록 방지 / 박스 스태틱메시도 액터가 제거될 때까지 모습을 숨기게끔 하자.
※OnSystemFinished 델리게이트는 '다이내믹 형식'이므로 바인딩할 대상 멤버 함수에 UFUNCTION 매크로를 선언해 준다.
- 정적 바인딩 : 성능이 중요하고 호출 대상이 고정적일 때 사용. 컴파일 시간에 결정되어 있으므로 실행 시점에서 추가적인 비용 없이 빠르게 동작함. 블루프린트에서 사용 불가능
- 동적 바인딩 : 객체의 실제 동작이 런타임에 결정되어야 하거나, 상속 관계에서 다양한 타입의 객체를 일관된 인터페이스로 다룰 때 유용함. 블루프린트에서 사용 가능.
//ABItemBox.h
public:
UPROPERTY(VisibleAnywhere, Category=Effect)
UParticleSystemComponent* Effect;
private:
UFUNCTION()
void OnEffectFinished(class UParticleSystemComponent* PSystem);
//ABItemBox.cpp
AABItemBox::AABItemBox()
{
Effect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("EFFECT"));
Effect->SetupAttachment(RootComponent);
static ConstructorHelpers::FObjectFinder<UParticleSystem>P_CHESTOPEN(TEXT("/Game/InfinityBladeGrassLands/Effects/FX_Treasure/Chest/P_TreasureChest_Open_Mesh.P_TreasureChest_Open_Mesh"));
if (P_CHESTOPEN.Succeeded())
{
Effect->SetTemplate(P_CHESTOPEN.Object); //Effect에 사용할 파티클 시스템 템플릿 설정
Effect->bAutoActivate = false; //이 컴포넌트가 생성되었을 때, 자동으로 활성화될지 여부 -> 명시적으로 활성화될 때까지 비활성 상태로 유지
}
}
...
if (ABCharacter->CanSetWeapon())
{
auto NewWeapon = GetWorld()->SpawnActor<AABWeapon>(WeaponItemClass, FVector::ZeroVector, FRotator::ZeroRotator);
ABCharacter->SetWeapon(NewWeapon);
Effect->Activate(true); //파티클 효과 발생시키기
Box->SetHiddenInGame(true, true); //Box 컴포넌트를 게임에서 숨길지, 두번째 인수는 하위 컴포넌트도 함께 숨길지
SetActorEnableCollision(false); //두번 습득 방지
Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
}
void AABItemBox::OnEffectFinished(UParticleSystemComponent* PSystem)
{
Destroy();
}
이번에는 아이템 상자에서 다른 무기가 나오게끔 새로운 무기를 추가해 본다.
→ 이번엔 C++가 아닌, 블루프린트를 사용해 ABWeapon을 상속받은 객체를 다수 생성해 본다.
→ 블루프린트 클래스 생성 → ABWeapon을 부모 클래스로 설정 → 해당 블루프린트에서 스켈레탈 메시 컴포넌트의 스켈레탈 메시에서 새로운 무기로 바꿔줌 → 상자의 WeaponClass 설정을 방금 생성한 블루프린트로 변경 → 상자 먹으면 새로운 무기 획득
'게임 개발 > 이득우의 언리얼 C++' 카테고리의 다른 글
[이득우의 언리얼 C++ 게임개발의 정석] #12. AI 컨트롤러와 비헤이비어 트리 (0) | 2024.06.28 |
---|---|
[이득우의 언리얼 C++ 게임개발의 정석] #11. 게임 데이터와 UI 위젯 (0) | 2024.06.26 |
[이득우의 언리얼 C++ 게임개발의 정석] #9. 충돌 설정과 대미지 전달 (0) | 2024.06.22 |
[이득우의 언리얼 C++ 게임개발의 정석] #8. 애니메이션 시스템 활용 (0) | 2024.06.20 |
[이득우의 언리얼 C++ 게임개발의 정석] #7. 애니메이션 시스템의 설계 (0) | 2024.06.17 |