아이템 상자와 무기 제작
‘이득우의 언리얼 c++ 게임 개발의 정석’ 책을 참고하여 작성한 포스트입니다.
소켓 시스템을 이용해 캐릭터 에셋에 무기를 부착해 본다.
캐릭터에만 반응하는 상자를 생성하고 캐릭터와 상호작용 할 수 있게 만든다.
캐릭터 소켓 설정
- 소켓을 우클릭하고 프리뷰 에셋 추가 메뉴 클릭 후 무기를 선택하면 착용 상태를 미리 확인 가능하다.
- 툴바의 프리뷰 애니메이션에 들어가 애니메이션에 따른 무기의 적절한 위치를 파악하자. 이후 코드에서 해당 위치를 세팅해준다.
- 무기 장착은 무기의 메시를 캐릭터 메시에 부착하면 되는데, SetupAttachment 함수에 소켓 이름을 파라미터로 넘기면 소켓 위치를 기준으로 트랜스폼이 자동 설정된다.
sample code
// h
UPROPERTY(VisibleAnywhere, Category = Weapon)
USkeletalMeshComponent* Weapon;
//cpp
FName WeaponSocket(TEXT("hand_rSocket"));
if (GetMesh()->DoesSocketExist(WeaponSocket))
{
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WEAPON"));
static ConstructorHelpers::FObjectFinder<USkeletalMesh> SK_WEAPON(TEXT("/Game/InfinityBladeWeapons/Weapons/Blade/Swords/Blade_BlackKnight/SK_Blade_BlackKnight.SK_Blade_BlackKnight"));
if(SK_WEAPON.Succeeded())
{
Weapon->SetSkeletalMesh(SK_WEAPON.Object);
}
}
Weapon->SetupAttachment(GetMesh(), WeaponSocket);
무기 액터의 제작
- 필요에 따라 무기를 바꾸게 하려면 무기를 액터로 분리 해주는 것이 좋다.
- 액터를 부모 클래스로 하는 C++ 클래스를 생성해 준다.
- 무기로 실제 충돌을 일으키지 않을 것이므로 충돌 설정은 NoCollision으로 해준다. tick 도 false
- 무기 메시 컴포넌트 하나만 있으니까 이 메시 컴포넌트를 루트 컴포넌트로 해준다.
// Weapon : 무기 메시 컴포넌트
Weapon->SetCollisionProfillName(TEXT("NoCollision"));
- 무기 습득 시 캐릭터에 장착해주는 코드를 작성해 본다.
- 월드에 새롭게 액터를 생성하는 명령은 UWorld::SpawnActor 이다.
// location, rotation 오버로드 버전
template<class T>
T * SpawnActor
(
FVector const & Location,
FRotator const & Rotation,
const FActorSpawnParameters & SpawnParameters
)
auto CurWeapon = GetWorld()->SpawnActor<AABWeapon>(
FVector::ZeroVector,
FRotator::ZeroRotator);
CurWeapon->AttachToComponent(
GetMesh(),
FAttachmentTransformRules::SnapToTargetNotIncludingScale,
WeaponSocket)
- Attaches the RootComponent of this Actor to the supplied component, optionally at a named socket. It is not valid to call this on components that are not Registered.
void AttachToComponent
(
USceneComponent * Parent, // parent to attach to
// how to handle transforms and welding when attaching
const FAttachmentTransformRules & AttachmentRules,
// optional socket to attach on the parent
FName SocketName
)
- SnapToTargetIncludingScale
- Snap the actor component to the new parent. This calculates the relative scale of the component that is being attached; so that it keeps the same scale. Essentially this is taking in the scale given and using that. (This one is usually used to attach a weapon to the character mesh.)
- SnapToTargetNotIncludingScale
- This Does the same as the above, but ignores the scale param of the given relative or world transform.
아이템 상자의 제작
- 액터를 부모로 하는 상자 클래스를 하나 생성한다.
- 캐릭터와 상호작용 해야 하므로 겹침을 감지할 콜리전 오브젝트를 추가한다.
- 물체의 크기는 스태틱메시 에디터의 LOD0 섹션에 있는 빌드 스케일 옵션으로 설정 가능하다. 코딩으로 번거롭게 안해도 됨
- 이때 박스 콜리전 컴포넌트의 extend 값은 전체 박스 영역 크기의 반으로 해주면 된다.
- tick 도 false 로 해주면 된다.
- 프로젝트 세팅의 콜리전 메뉴에서 ItemBox 이름의 오브젝트 채널을 하나 생성하고 기본 반응은 무시로 설정한다.
- ItemBox 이름의 프리셋을 하나 추가 한 후,
- 감지만 할 예정이므로 콜리전 켜짐 메뉴 값을 Query Only로 지정하고, 캐릭터 오브젝트 채널에만 겹침으로 세팅해준다.
- 캐릭터 프리셋에 가서 ItemBox 콜리전 채널과의 반응 설정을 겹침으로 해준다.
- 코드에서 이 프리셋을 박스 컴포넌트에 설정하고 캐릭터를 감지할 수 있게 한다.
- 박스 컴포넌트에는 Overlap 이벤트를 처리할 수 있게 OnComponentBeginOverlap 델리게이트가 선언돼 있다.
- 멀티캐스트 다이내믹 델리게이트 이며, Overlap 이벤트 발생 시마다 바인딩한 멤버 함수를 호출한다.
아이템의 습득
- 아이템 상자에 클래스 정보를 저장하고 캐릭터가 영역에 들어왔을 때 아이템을 생성하도록 해보자.
- 클래스 정보 저장하는 변수 선언시 UClass의 포인터를 사용할 수도 있지만 그러면 현재 프로젝트에 사용하는 모든 언리얼 프로젝트의 선언이 보이게 된다.
- 특정 클래스와 상속받은 클래스들로 목록을 한정하도록 TSubclassOf 키워드를 사용하자. 그러면 아이템 상자와 이를 선언한 클래스 목록만 볼 수 있다.
- 생성자 코드에서 해당 속성에 대한 기본 클래스 값을 지정하고 나면 에디터에서 상자를 선택시 디테일 윈도우에 클래스를 지정하는 메뉴가 나타날 것이다.(Weapon Item Class)
전체 코드
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "GameFramework/Actor.h"
#include "ABItemBox.generated.h"
UCLASS()
class ARENABATTLE_API AABItemBox : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AABItemBox();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
virtual void PostInitializeComponents() override;
public:
UPROPERTY(VisibleAnywhere, Category = Box)
UBoxComponent* Trigger;
UPROPERTY(VisibleAnywhere, Category = Box)
UStaticMeshComponent* Box;
UPROPERTY(EditInstanceOnly, Category = Box)
TSubclassOf<class AABWeapon> WeaponItemClass;
UPROPERTY(VisibleAnywhere, Category = Effect)
UParticleSystemComponent* Effect;
private:
UFUNCTION()
void OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
void OnEffectFinished(class UParticleSystemComponent* PSystem);
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "ABItemBox.h"
#include "ABWeapon.h"
#include "ABCharacter.h"
// Sets default values
AABItemBox::AABItemBox()
{
// 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;
Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TRIGGER"));
Box = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BOX"));
Effect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("EFFECT"));
RootComponent = Trigger;
Box->SetupAttachment(RootComponent);
Trigger->SetBoxExtent(FVector(40.0f, 42.0f, 30.0f));
static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_BOX(TEXT("/Script/Engine.StaticMesh'/Game/InfinityBladeGrassLands/Environments/Breakables/StaticMesh/Box/SM_Env_Breakables_Box1.SM_Env_Breakables_Box1'"));
if(SM_BOX.Succeeded())
{
Box->SetStaticMesh(SM_BOX.Object);
}
Box->SetRelativeLocation(FVector(0.0f, -3.5f, -30.0f));
Trigger->SetCollisionProfileName(TEXT("ItemBox"));
Box->SetCollisionProfileName(TEXT("NoCollision"));
static ConstructorHelpers::FObjectFinder<UParticleSystem> P_CHESTOPEN(TEXT("/Script/Engine.ParticleSystem'/Game/InfinityBladeGrassLands/Effects/FX_Treasure/Chest/P_TreasureChest_Open_Mesh.P_TreasureChest_Open_Mesh'"));
if (P_CHESTOPEN.Succeeded())
{
Effect->SetTemplate(P_CHESTOPEN.Object);
Effect->bAutoActivate = false;
}
Effect->SetupAttachment(RootComponent);
WeaponItemClass = AABWeapon::StaticClass();
}
// Called when the game starts or when spawned
void AABItemBox::BeginPlay()
{
Super::BeginPlay();
}
void AABItemBox::PostInitializeComponents()
{
Super::PostInitializeComponents();
Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABItemBox::OnCharacterOverlap);
}
void AABItemBox::OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
ABLOG_S(Warning);
auto ABCharacter = Cast<AABCharacter>(OtherActor);
ABCHECK(nullptr != ABCharacter);
if (nullptr != ABCharacter && nullptr != WeaponItemClass)
{
if (ABCharacter->CanSetWeapon())
{
auto NewWeapon = GetWorld()->SpawnActor<AABWeapon>(WeaponItemClass, FVector::ZeroVector, FRotator::ZeroRotator);
ABCharacter->SetWeapon(NewWeapon);
Effect->Activate(true);
Box->SetHiddenInGame(true, true);
SetActorEnableCollision(false);
Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
}
else
{
ABLOG(Warning, TEXT("%s can't equip weapon currently."), *ABCharacter->GetName());
}
}
}
void AABItemBox::OnEffectFinished(UParticleSystemComponent* PSystem)
{
Destroy();
}
Leave a comment