4 minute read

‘이득우의 언리얼 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