결과물

코드 재구성

한 동안 노는게 너무 좋아서 작업을 별로 안했는데 오늘 작업을 다시 시작하기로 마음 먹고 프로젝트를 새로 파서 다시 만들어보니 작업이 술술 잘 되었다.

인터렉션 구조 개선

기존의 코드에서는 Weapon단위의 엑터를 받아올 때 생성에 대한 문제가 있었다. 생성자에서 불러와야만 가능했던 코드가 있는데 그 부분은 엔진에서 지원하지 않는 것 같았다. 그래서 코드를 갈아엎고 새로 써냈다.

오늘 한 작업의 정리를 해보겠다.

플레이어 엑터 컴포넌트

캐릭터의 HP나 상태같은 부분들을 책임지는 PlayerStatus 엑터 컴포넌트를 만들었다.

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class HACKED_API UPlayerStatus : public UActorComponent
{
	GENERATED_BODY()

private:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PlayerStatus", meta = (AllowPrivateAccess = "true"))
	float hp;
	
public:	
	// Sets default values for this component's properties
	UPlayerStatus();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
	FORCEINLINE float GetHP() { return hp; }
	FORCEINLINE void SetHP(int32 _hp) { hp = _hp; }
		
};

아직은 구성이 완벽하지 않아 HP 정도만 저장해 둔 뒤 생성자에 적당한 HP를 대입해 주는 것으로 일단 마무리한다.

픽업 인터렉션 구성

아주 간단하게 설명하면 이렇다.

ItemStatus에서 TArray<AWeapon*>로 만든 weaponSlots 배열을 하나 두고 거기서 아이템들을 관리한다. 아이템들을 관리함에 있어 Slot만 바꿔 아이템을 껐다 켰다 하는 방식을 선택했다.

ChangeWeapon과 SwitchWeapon이 주축을 이루어 아이템을 등록, 변경하고 DropWeapon에서 PickupItem 엑터를 내뱉는 방식으로 만들었다.

ItemStatus.h

// Copyright 2019 블랙말랑카우 All rights reserved.

#pragma once

#include "HACKED.h"
#include "Components/ActorComponent.h"
#include "Weapon.h"
#include "ObjectPool.h"
#include "PickupItem.h"
#include "ItemStatus.generated.h"


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class HACKED_API UItemStatus : public UActorComponent
{
	GENERATED_BODY()

private:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "ItemStatus", meta = (AllowPrivateAccess = "true"))
	TArray<AWeapon*> weaponSlots;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ItemStatus", meta = (AllowPrivateAccess = "true"))
	int32 curSlot;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ItemStatus", meta = (AllowPrivateAccess = "true"))
	class AWeapon* defaultWeapon;

	ObjectPool objPool;

public:	
	// Sets default values for this component's properties
	UItemStatus();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

	void ChangeWeapon(AWeapon* _weapon);
	void SwitchWeapon(int32 _slot);
	void DropWeapon();

	FORCEINLINE int32 GetCurSlot() { return curSlot; }
	FORCEINLINE void SetCurSlot(int32 _curSlot) { curSlot = _curSlot; }
};

defaultWeapon은 캐릭터가 아무것 도 들고있지 않을 때의 AWeapon상태를 만들기 위해 만들었다. 아직까지는 테스트용이니 무시해주길 바란다.

ItemStatus.cpp

// Copyright 2019 블랙말랑카우 All rights reserved.

#include "ItemStatus.h"
#include "HACKEDCharacter.h"

// Sets default values for this component's properties
UItemStatus::UItemStatus()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = true;
	curSlot = 0;

}


// Called when the game starts
void UItemStatus::BeginPlay()
{
	Super::BeginPlay();

	defaultWeapon = GetWorld()->SpawnActor<AWeapon>(AWeapon::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator);
	defaultWeapon->SetItemType(EItemType::DEFAULT);
	defaultWeapon->SetCurClip(10);
	defaultWeapon->GetSkeletalMeshComp()->SetSkeletalMesh(objPool.skeletalMeshes[(int)defaultWeapon->GetItemType()]);
	defaultWeapon->SetActorHiddenInGame(true);

	AHACKEDCharacter* player = Cast<AHACKEDCharacter>(GetOwner());

	for (int i = 0; i < 4; i++)
	{
		AWeapon* temporary = GetWorld()->SpawnActor<AWeapon>(AWeapon::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator);
		temporary->SetActorHiddenInGame(true);
		weaponSlots.Add(temporary);
	}
	
	FName weaponSocket(TEXT("WeaponSocket"));
	player->SetCurrentWeapon(weaponSlots[curSlot]);
	weaponSlots[curSlot]->AttachToComponent(player->GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, weaponSocket);
	player->GetCurrentWeapon()->SetActorHiddenInGame(false);
}


// Called every frame
void UItemStatus::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	// ...
}

void UItemStatus::ChangeWeapon(AWeapon * _weapon)
{
	AHACKEDCharacter* player = Cast<AHACKEDCharacter>(GetOwner());

	if (weaponSlots[curSlot]->GetItemType() == _weapon->GetItemType())
	{
		weaponSlots[curSlot]->SetCurClip(_weapon->GetCurClip());
	}
	
	if (weaponSlots[curSlot]->GetItemType() == EItemType::DEFAULT)
	{
		weaponSlots[curSlot] = _weapon;
		weaponSlots[curSlot]->AttachToComponent(player->GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, weaponSlots[curSlot]->GetWeaponSocketName());
	}
	else
	{
		//if another weapon is attach
	}

}

void UItemStatus::SwitchWeapon(int32 _slot)
{
	LOG(Warning, TEXT("Current Slot : %d"), curSlot);
	LOG(Warning, TEXT("Incoming Slot : %d"), _slot);
	/*if (curSlot == _slot)
		return;*/
	//if (weaponSlots[curSlot]->GetItemType() == EItemType::DEFAULT&&weaponSlots[_slot]->GetItemType() == EItemType::DEFAULT)
	//	return;
	AHACKEDCharacter* player = Cast<AHACKEDCharacter>(GetOwner());
	if (player == nullptr)
		return;

	curSlot = _slot;
	player->GetCurrentWeapon()->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
	player->GetCurrentWeapon()->SetActorHiddenInGame(true);
	
	weaponSlots[curSlot]->SetActorHiddenInGame(false);
	player->SetCurrentWeapon(weaponSlots[curSlot]);
	weaponSlots[curSlot]->AttachToComponent(player->GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, weaponSlots[curSlot]->GetWeaponSocketName());

}

void UItemStatus::DropWeapon()
{
	if (weaponSlots[curSlot]->GetItemType() == EItemType::DEFAULT)
		return;

	AWeapon* dropwep = weaponSlots[curSlot];
	int32 dropclip= dropwep->GetCurClip();
	EItemType droptype = dropwep->GetItemType();

	APickupItem *dropitem = GetWorld()->SpawnActor<APickupItem>(APickupItem::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator);
	dropitem->SetItemType(droptype);
	dropitem->SetCurClip(dropclip);
	dropitem->GetStaticMeshComp()->SetStaticMesh(objPool.staticMeshes[(int)droptype]);
	
	weaponSlots[curSlot]=defaultWeapon;
}

ChangeWeapon에서는 받아온 아이템을 탈부착 하는 기능을 한다.

else문을 아직 구현하지 않았는데 저 부분에는 아이템을 먹고난 뒤 APickupItem을 뱉어내는 부분이다.

SwitchWeapon에서는 문제가 조금 있다. 슬롯은 4개의 슬롯이 있는데 처음 게임을 시작하고 1,2,3,4 순서대로 슬롯을 누르지 않으면 처음엔 매쉬들이 바뀌지 않는다. 이 부분은 계속 더 보아야겠다.

일단 슬롯을 받아 플레이어의 CurrentWeapon에 탈부착 시켜주는 부분이다.

DropWeapon은 아직 테스트를 진행하지 못했다. 기능을 확장한 뒤 테스트 해봐야겠다.

APickupItem

픽업 아이템의 경우 계속 만들던 것이다 보니 쉬웠다.

APickupItem.h

// Copyright 2019 블랙말랑카우 All rights reserved.

#pragma once

#include "HACKED.h"
#include "GameFramework/Actor.h"
#include "ItemData.h"
#include "ObjectPool.h"
#include "PickupItem.generated.h"

UCLASS()
class HACKED_API APickupItem : public AActor
{
	GENERATED_BODY()

private:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PickupItem", meta = (AllowPrivateAccess = "true"))
	EItemType itemType;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PickupItem", meta = (AllowPrivateAccess = "true"))
	UStaticMeshComponent* staticMeshComp;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PickupItem", meta = (AllowPrivateAccess = "true"))
	int32 curClip;
	
	ObjectPool objPool;

public:	
	APickupItem();

	void Interaction(class AHACKEDCharacter* player);

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;
	virtual void OnConstruction(const FTransform& Transform) override;
	FORCEINLINE EItemType GetItemType() { return itemType; }
	FORCEINLINE void SetItemType(EItemType _itemType) { itemType = _itemType; }
	FORCEINLINE UStaticMeshComponent* GetStaticMeshComp() { return staticMeshComp; }
	FORCEINLINE int32 GetCurClip() { return curClip; }
	FORCEINLINE void SetCurClip(int32 _curClip) { curClip = _curClip; }
};

ItemStatus에서 AHACKEDCharacter 헤더를 좀 불러와야했었는데 그 부분에서 순환참조 오류가 발생해 전방선언하는 방식으로 전환했다.

APickupItem.cpp

void APickupItem::Interaction(class AHACKEDCharacter* player)
{
	AWeapon* newWeapon= GetWorld()->SpawnActor<AWeapon>(AWeapon::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator);
	newWeapon->SetCurClip(curClip);
	newWeapon->SetItemType(itemType);
	newWeapon->GetSkeletalMeshComp()->SetSkeletalMesh(objPool.skeletalMeshes[1]);
	player->GetItemStatus()->ChangeWeapon(newWeapon);
	this->SetActorHiddenInGame(true);
	this->Destroy(true);
}

많이 볼필요 없이 인터렉션먼 보면 될 것 같다. 여기서 Weapon을 하나 생성해 설정을 마친다음 ChangeItem에 넣어주었다.

나중에 무기를 버릴 때 새로 또 생길 것이니 게임에서 Destory시켜주는 것으로 끝내두었다.

ItemData.h

// Copyright 2019 블랙말랑카우 All rights reserved.

#pragma once

#include "HACKED.h"
#include "Engine/DataTable.h"
#include "ItemData.generated.h"

UENUM(BlueprintType)
enum class EItemType : uint8
{
	//ID MUST BE SAME AS DATA TABLE
	DEFAULT = 0,
	TEST,
	COUNT
};

static FString GetItemEnumTypeAsString(EItemType EnumValue)
{
	const UEnum* EnumPtr = FindObject<UEnum>(ANY_PACKAGE, TEXT("EItemType"), true);
	if (!EnumPtr) return FString("Invalid");

	return EnumPtr->GetNameByValue((int64)EnumValue).ToString();
}

static FName GetItemTypeName(EItemType EnumValue)
{
	const UEnum* EnumPtr = FindObject<UEnum>(ANY_PACKAGE, TEXT("EItemType"), true);
	if (!EnumPtr) return FName("Invalid");

	FString concated = EnumPtr->GetNameByValue((int64)EnumValue).ToString();
	concated.RemoveFromStart(TEXT("EItemType::"));
	return FName(*concated);
}

USTRUCT(BlueprintType)
struct FItemData : public FTableRowBase
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	int32 ID;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	FString DisplayName;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	int32 MaxClip;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	FString SMPath;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	FString SKPath;
};

아이템 관련해 Enum이나 Struct를 선언해둔 ItemData.h가 있다.

현재 내 코드는 Enum에 상당히 많이 의존하고 있는 코드이나 어떻게 보면 관리가 편해질 수 있다는 장점이 있을 것 같다.

Enum을 효율적으로 쓰기 위해 FName이나 String으로 Enum을 뽑아내는 함수도 구현해 두었다.

Struct의 경우엔 추후 애니메이션이나 무기의 공격력등의 정보가 추가될 것 같다.

Character

캐릭터에서는 인터렉션을 위한 Weapon이나 PlayerStatus, ItemStatus를 구현해 두고 사용중이다.

// Copyright 2019 블랙말랑카우 All rights reserved.

#pragma once

#include "HACKED.h"
#include "GameFramework/Character.h"
#include "ObjectPool.h"
#include "PickupItem.h"
#include "PlayerStatus.h"
#include "ItemStatus.h"
#include "Weapon.h"
#include "HACKEDCharacter.generated.h"

UCLASS(config=Game)
class AHACKEDCharacter : public ACharacter
{
	GENERATED_BODY()
private:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class USpringArmComponent* CameraBoom;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class UCameraComponent* FollowCamera;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction", meta = (AllowPrivateAccess = "true"))
	class UBoxComponent* InteractionBox;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction", meta = (AllowPrivateAccess = "true"))
	FVector BoxSize;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Status", meta = (AllowPrivateAccess = "true"))
	class UPlayerStatus* playerStat;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Status", meta = (AllowPrivateAccess = "true"))
	class UItemStatus* itemStat;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon", meta = (AllowPrivateAccess = "true"))
	class AWeapon* currentWeapon;

	ObjectPool objPool;

public:
	AHACKEDCharacter();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
	float BaseTurnRate;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
	float BaseLookUpRate;

	virtual void Tick(float DeltaSeconds) override;

protected:

	void MoveForward(float Value);
	void MoveRight(float Value);
	void TurnAtRate(float Rate);
	void LookUpAtRate(float Rate);

protected:

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

public:
	void Interaction();
	APickupItem* SearchClosestItem();

	void Slot1();
	void Slot2();
	void Slot3();
	void Slot4();
	void Attack();

	FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
	FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
	FORCEINLINE class UBoxComponent* GetInteractionBox() const { return InteractionBox; }
	FORCEINLINE class UPlayerStatus* GetPlayerStatus() const { return playerStat; }
	FORCEINLINE class UItemStatus* GetItemStatus() const { return itemStat; }
	FORCEINLINE class AWeapon* GetCurrentWeapon() const { return currentWeapon; }
	FORCEINLINE void SetCurrentWeapon(AWeapon* _weapon) { currentWeapon = _weapon; }
}; 

cpp에서는 맨날 보던 것이니 그냥 넘어가도록 하겠다.

ObjectPool System

아이템의 데이터 테이블이나 Skeletal, Static Mesh등의 오브젝트를 싱글톤느낌 비슷하게 저장해 관리할 수있는 ObjectPool 시스템을 제작했다.

// Copyright 2019 블랙말랑카우 All rights reserved.

#pragma once

#include "HACKED.h"
#include "ItemData.h"

class HACKED_API ObjectPool
{
public:
	ObjectPool();
	~ObjectPool();

	static FString itemDataTablePath;
	static UDataTable* itemTable;
	static TArray<UStaticMesh*> staticMeshes;
	static TArray<USkeletalMesh*> skeletalMeshes;
};

모두 static으로 선언해두어 메모리가 더 생성되는 것을 방지했다.

// Copyright 2019 블랙말랑카우 All rights reserved.

#include "ObjectPool.h"

FString ObjectPool::itemDataTablePath = "DataTable'/Game/System/ItemDataTable.ItemDataTable'";
UDataTable* ObjectPool::itemTable;
TArray<UStaticMesh*> ObjectPool::staticMeshes;
TArray<USkeletalMesh*> ObjectPool::skeletalMeshes;

ObjectPool::ObjectPool()
{
	LOG(Warning, TEXT("Item Table Initialize Started!"));

	static ConstructorHelpers::FObjectFinder<UDataTable>DT_ITEM(*itemDataTablePath);
	if (DT_ITEM.Succeeded())
	{
		itemTable = DT_ITEM.Object;
		LOG(Warning, TEXT("Item Data Table Load Successed!"));
	}

	if (itemTable) 
	{
		static TArray<FName> names;
		names = itemTable->GetRowNames();
		static FString context("");
		for (auto& name : names)
		{
			//Static Mesh Initialize
			ConstructorHelpers::FObjectFinder<UStaticMesh>SM(*itemTable->FindRow<FItemData>(name, context)->SMPath);
			if (SM.Succeeded())
			{
				staticMeshes.Add(SM.Object);
				LOG(Warning, TEXT("[SM Pool] \"%s\" LOADED!"), *name.ToString());
			}

			//Skeletal Mesh Initialize
			ConstructorHelpers::FObjectFinder<USkeletalMesh>SK(*itemTable->FindRow<FItemData>(name, context)->SKPath);
			if (SK.Succeeded())
			{
				skeletalMeshes.Add(SK.Object);
				LOG(Warning, TEXT("[SK Pool] \"%s\" LOADED!"), *name.ToString());
			}
		}

		LOG(Warning, TEXT("ObjectPool Initialize DONE!"));
	}
}

DataTable의 정보를 갖고와서 StaticMesh,SkeletalMesh를 생성해 배열에 저장해서 사용한다. 이때 DataTable의 Name Row를 읽기 때문에 EItemType과 같은 순서로 저장되어 사용이 간편해지게 만들었다.

Weapon

무기의 경우 부모 클래스 정도로 생각하며 만들어 두었다 추후 무기에 관해서는 각각의 클래스를 만들어 두어 여러부분에 있어 설정을 해두며 사용해야 할 것 같다. 그렇기 때문에 모두 protected로 생성해두었다.

// Copyright 2019 블랙말랑카우 All rights reserved.

#pragma once

#include "HACKED.h"
#include "GameFramework/Actor.h"
#include "ItemData.h"
#include "ObjectPool.h"
#include "Weapon.generated.h"

UCLASS()
class HACKED_API AWeapon : public AActor
{
	GENERATED_BODY()
	
protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon", meta = (AllowPrivateAccess = "true"))
	EItemType itemType;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon", meta = (AllowPrivateAccess = "true"))
	USkeletalMeshComponent* skeletalMeshComp;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon", meta = (AllowPrivateAccess = "true"))
	int32 curClip;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon", meta = (AllowPrivateAccess = "true"))
	FName weaponSocketName;

	ObjectPool objPool;

public:	

	AWeapon();

	virtual void Attack();

protected:

	virtual void BeginPlay() override;
	
public:	

	virtual void Tick(float DeltaTime) override;
	FORCEINLINE EItemType GetItemType() { return itemType; }
	FORCEINLINE void SetItemType(EItemType _type) { itemType = _type; }
	FORCEINLINE USkeletalMeshComponent* GetSkeletalMeshComp() { return skeletalMeshComp; }
	FORCEINLINE int32 GetCurClip() { return curClip; }
	FORCEINLINE void SetCurClip(int32 _clip) { curClip = _clip; }
	FORCEINLINE FName GetWeaponSocketName() { return weaponSocketName; }
};