필요한 개념
<무기 작업>
구조 설명(그림 10-1 참고)
Player에서 무기를 장착하라는 명령이 내려오면 OneHand StateType을 Equip로 교체함
또 명령 ActionComponent에다가 어떤 전투모드인지 설정(OneHand)
ActionComponent안에는 CEquipment라는 클래스를 소유(장비 장착을 담당) -> 필요에 따라 BP로 만들어서 동작을 재정의 가능(같은 한손 검이지만, 옆구리나 등에서도 칼 빼고 장착가능하고... 장착 방법에 따른 오버라이딩 가능)
CAttachment 화면에 나타날 객체(불이나..)
CAttachment를 CEquipment에 연결해줘서 어떤 장비가 장착되면 CAttachment의 특정 함수가 콜됨(이벤트 연결)
CAction->직접적인 공격이나 행동들을 만듬(melee, warp, Lighting, fireball)
DataAsset으로 관리 DataAsset으로 부터 객체들을 관리하고, 불러오고, 플레이하고 그런식으로 진행
<ActionComponent 생성>
ActionComponent : 액션을 관리할 컴포넌트(액션 상태에 따른 행동들을 처리할 수 있도록 함)
DataAsset들을 ActionComponent가 가지고 있게 됨(EActionType::Max 만큼)
DataAsset에 CEquipment, CAttachment, CAction을 세팅할 수 있음(실무에서 작업할때도 DataTable은 간단할 때, DataAsset은 조합할때(아이템 하나가 DataAsset 하나)
우리는 DataAsset을 만들기 위해 언리얼에서 DataAsset을 만들어주는 것을 제공해주는 방법을 사용할 것임
// CActionComponent.h
// 어떤 무기를 장착한 상태인지
UENUM(BlueprintType)
enum class EActionType: uint8
{
Unarmed, OneHand, TwoHand,
};
// 장착이 바뀌었을때 행동처리를 위해
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FActionTypeChanged, FActionType, InPrevType, FActionType, InNewType);
public:
// 어떤 무기를 낀 상태인지 확인하기 위해
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsUnarmedMode() { return Type == EActionType::Unarmed; }
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsOneHandMode() { return Type == EActionType::OneHand; }
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsTwoHandMode() { return Type == EActionType::TwoHand; }
public:
void SetUnarmedMode();
void SetOneHandMode();
void SetTwoHandMode();
private:
// SetMode 함수는 외부에서 액션 상태를 바꾸는 함수를 콜하게 되면 이 함수를 호출하게 해서
// Mode를 바꿔주는 공통코드를 실행하게 됨
void SetMode(EActionType InType);
void ChangeType(EActionType InNewType);
private:
// 블루프린트에서 할당가능하도록(즉, BP함수 바인딩 가능하도록)
UPROPERTY(BlueprintAssignable)
FActionTypeChanged OnActionTypeChanged;
private:
EActionType Type;
// CActionComponent.cpp
void UCActionComponent::SetMode(EActionType InType)
{
// 수행할꺼 수행하고 ChangeType을 통해 바꿔주고 델리게이션 콜 됨
// 이전 타입과 현재 타입이 같다면 칼을 넣어주는 동작
if (Type == InType) // 같은 무기 해제
{
SetUnarmedMode();
return;
}
// 다른 무기 장착 부분
else if (IsUnarmedMode() == false)
{
}
// TODO : 무기 장착
ChangeType(InType);
}
void UCActionComponent::ChangeType(EActionType InNewType)
{
EActionType prevType = InNewType;
Type = InNewType;
if (OnActionTypeChanged.IsBound())
OnActionTypeChanged.Broadcast(prevType, InNewType);
}
<DataAsset 생성>
C++ 클래스 추가 -> 모든 클래스 표시 -> DataAsset 기반 클래스로 생성
DataAsset : 타입이나 이런것을 모아서 관리할 수 있다.
UDataAsset을 상속 받게 됨
* 기반 BP 생성
기타->데이터 에셋
<CEquipment 클래스 생성>
Actor를 상속 받는 CEquipment 클래스 생성
CEquipment : 무기 장착을 관리할 클래스
Equip() 함수는 C++에서 기본으로 내용을 정의해놓고 BP에서 재정을 할 수 있도록 작업함(BlueprintNativeEvent)
// CEquipment.h
public:
// Equip() 함수는 C++에서 기본으로 내용을 정의해놓고 BP에서 재정을 할 수 있도록 작업함(BlueprintNativeEvent)
UFUNCTION(BlueprintNativeEvent)
void Equip();
// C에서 일단 기본으로 정의해놓음
void Equip_Implementation();
// 장착 개시(노티파이에 의해 콜 됨)
UFUNCTION(BlueprintNativeEvent)
void Begin_Equip();
void Begin_Equip_Implementation();
// 장착 완료
UFUNCTION(BlueprintNativeEvent)
void End_Equip();
void End_Equip_Implementation();
// 해제 명령(따로 장착해제 애니메이션 안할거여서 명령만 넣음)
UFUNCTION(BlueprintNativeEvent)
void Unequip();
void Unequip_Implementation();
CHelpers::GetComponent() 추가
* CStateComponent에 Equip, UnEquip Mode 추가
// CStateComponent.h
UENUM(BlueprintType)
enum class EStateType : uint8
{
Idle, Roll, Backstep, Equip, Unequip, Max
};
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsEquipMode() { return Type == EStateType::Equip; }
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsUnEquipMode() { return Type == EStateType::Unequip; }
// CStateComponent.cpp
void UCStateComponent::SetEquipMode()
{
ChangeType(EStateType::Equip);
}
void UCStateComponent::SetUnequipMode()
{
ChangeType(EStateType::Unequip);
}
* DrawMontage 생성해서 분할된 Draw 애니메이션과 합치기 & UpperBody
몽타주 생성한거에다가 추가할 애니메이션 끌어다 놓으면 됨
칼뽑는거 뛰어가면서 바꿀 거여서 UpperBody로 몽타주를 세팅
* BP_CPlayer에서 ActionComponent에서 Datas에 해당 DataAsset을 각각 할당
// ActionComponent.h
private:
UPROPERTY(EditDefaultsOnly, Category = "Weapons")
class UCActionData* Datas[(int32)EActionType::Max];
만들어서
// ActionComponent.cpp
ACharacter* character = Cast<ACharacter>(GetOwner());
for (int32 i = 0; i < (int32)EActionType::Max; i++)
{
if (!!Datas[i])
Datas[i]->BeginPlay(character);
}
해서 UCActionData의 BeginPlay()가 호출되고
Equipment에 데이터가 세팅됨
// UCActionData.cpp
void UCActionData::BeginPlay(class ACharacter* InOwnerCharacter)
{
// 기존에는 SpawnActor를 사용했는데 SpawnActor는 등장과 동시에
// BeginPlay()가 호출됨
// 지금은 객체만 생성한 후 값을 세팅하고 등장시키는 방법을 사용하는데
// 등장은 우리가 시키라고 할때 등장됨(즉, 객체 생성만 일어나고, 등장은 아직 하지 않음)
// 이렇게 처리해주는 것이 SpawnActorDeffered
FTransform transform;
//Equipment
{
// 객체 생성만 완료됨, 등장은 안 시킴
// 여기서 transform은 확정시키지 않으면 BeginPlay()가 끝나면 확정이 됨, 그때 등장할 위치
Equipment = InOwnerCharacter->GetWorld()->SpawnActorDeferred<ACEquipment>(EquipmentClass, transform, InOwnerCharacter);
// mesh에다가 붙여넣음
Equipment->AttachToComponent(InOwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true));
// 할당 받은 데이터를 Equipment로 넘겨줌
Equipment->SetData(EquipmentData);
// FinishSpawningActor() : 확정해 등장시킬 액터와 Transform을 입력
// transform : 확정되서 등장시킬 위치
UGameplayStatics::FinishSpawningActor(Equipment, transform);
}
}
* 이번에는 AnimNotifyState로 생성
UCAnimNotifyState_Equip 클래스 생성
CActionComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CActionComponent.generated.h"
UENUM(BlueprintType)
enum class EActionType: uint8
{
Unarmed, OneHand, TwoHand, Max,
};
// 장착이 바뀌었을때 행동처리를 위해
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FActionTypeChanged, EActionType, InPrevType, EActionType, InNewType);
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class UONLINE_04_ACTION_API UCActionComponent : public UActorComponent
{
GENERATED_BODY()
private:
UPROPERTY(EditDefaultsOnly, Category = "Weapons")
class UCActionData* Datas[(int32)EActionType::Max];
public:
// 현재 타입에 맞는 ActionData 가져옴
UFUNCTION(BlueprintPure)
FORCEINLINE class UCActionData* GetCurrent() { return Datas[(int32)Type]; }
public:
// 어떤 무기를 낀 상태인지 확인하기 위해
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsUnarmedMode() { return Type == EActionType::Unarmed; }
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsOneHandMode() { return Type == EActionType::OneHand; }
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsTwoHandMode() { return Type == EActionType::TwoHand; }
public:
UCActionComponent();
void SetUnarmedMode();
void SetOneHandMode();
void SetTwoHandMode();
public:
// 현재 타입의 DoAction 실행
void DoAction();
protected:
virtual void BeginPlay() override;
private:
// SetMode 함수는 외부에서 액션 상태를 바꾸는 함수를 콜하게 되면 이 함수를 호출하게 해서
// Mode를 바꿔주는 공통코드를 실행하게 됨
void SetMode(EActionType InType);
void ChangeType(EActionType InNewType);
public:
// 블루프린트에서 할당가능하도록(즉, BP함수 바인딩 가능하도록)
UPROPERTY(BlueprintAssignable)
FActionTypeChanged OnActionTypeChanged;
private:
EActionType Type;
};
CActionComponent.cpp
#include "CActionComponent.h"
#include "Global.h"
#include "Actions/CActionData.h"
#include "Actions/CEquipment.h"
#include "Actions/CDoAction.h"
#include "GameFramework/Character.h"
UCActionComponent::UCActionComponent()
{
}
void UCActionComponent::BeginPlay()
{
Super::BeginPlay();
ACharacter* character = Cast<ACharacter>(GetOwner());
for (int32 i = 0; i < (int32)EActionType::Max; i++)
{
if (!!Datas[i])
Datas[i]->BeginPlay(character);
}
}
void UCActionComponent::SetUnarmedMode()
{
// 이미 무기가 장착되어 있다면 해제하고
if (!!Datas[(int32)Type])
{
ACEquipment* equipment = Datas[(int32)Type]->GetEquipment();
if (!!equipment)
equipment->Unequip();
}
ChangeType(EActionType::Unarmed);
}
void UCActionComponent::SetOneHandMode()
{
SetMode(EActionType::OneHand);
}
void UCActionComponent::SetTwoHandMode()
{
SetMode(EActionType::TwoHand);
}
void UCActionComponent::SetMode(EActionType InType)
{
// 수행할꺼 수행하고 ChangeType을 통해 바꿔주고 델리게이션 콜 됨
// 이전 타입과 현재 타입이 같다면 칼을 넣어주는 동작
if (Type == InType) // 같은 무기 해제
{
SetUnarmedMode();
return;
}
// 다른 무기 장착 부분
else if (IsUnarmedMode() == false)
{
ACEquipment* equipment = Datas[(int32)Type]->GetEquipment();
CheckNull(equipment);
// 기존 무기를 해제시킴
equipment->Unequip();
}
// 새로운 타입 무기 장착
ACEquipment* equipment = Datas[(int32)InType]->GetEquipment();
CheckNull(equipment);
equipment->Equip();
// 상태 바꿔줌
ChangeType(InType);
}
void UCActionComponent::ChangeType(EActionType InNewType)
{
EActionType prevType = InNewType;
Type = InNewType;
if (OnActionTypeChanged.IsBound())
OnActionTypeChanged.Broadcast(prevType, InNewType);
}
void UCActionComponent::DoAction()
{
CheckTrue(IsUnarmedMode());
if (!!Datas[(int32)Type])
{
ACDoAction* action = Datas[(int32)Type]->GetDoAction();
if (!!action)
action->DoAction();
}
}
CActionData.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "CActionData.generated.h"
// 데이터 에셋에서 데이터를 묶음으로 불러오기 위한 구조체를 정의
// 블루프린트랑 연동해서 불러올 것 이어서
// 장착 애니메이션 데이터 담당
USTRUCT(BlueprintType)
struct FEquipmentData
{
GENERATED_BODY()
public:
// 얘들은 따로 타입을 안써도 됨
UPROPERTY(EditAnywhere)
class UAnimMontage* AnimMontage;
UPROPERTY(EditAnywhere)
float PlayRatio = 1.0f;
UPROPERTY(EditAnywhere)
FName StartSection;
UPROPERTY(EditAnywhere)
bool bCanMove = true;
};
UCLASS()
class UONLINE_04_ACTION_API UCActionData : public UDataAsset
{
GENERATED_BODY()
public:
FORCEINLINE class ACEquipment* GetEquipment() { return Equipment; }
public:
// 등장 시킬 클래스 타입
// 즉, 이 클래스를 BP화 해도 사용할 수 있다는 얘기
UPROPERTY(BlueprintReadOnly, EditAnywhere)
TSubclassOf<class ACEquipment> EquipmentClass;
// DataAsset에서 할당할 수 있게 되는 애들
UPROPERTY(BlueprintReadOnly, EditAnywhere)
FEquipmentData EquipmentData;
public:
void BeginPlay(class ACharacter* InOwnerCharacter);
private:
class ACEquipment* Equipment;
};
CActionData.cpp
#include "CActionData.h"
#include "Global.h"
#include "CEquipment.h"
#include "GameFramework/Character.h"
#include "Components/SkeletalMeshComponent.h"
void UCActionData::BeginPlay(class ACharacter* InOwnerCharacter)
{
// 기존에는 SpawnActor를 사용했는데 SpawnActor는 등장과 동시에
// BeginPlay()가 호출됨
// 지금은 객체만 생성한 후 값을 세팅하고 등장시키는 방법을 사용하는데
// 등장은 우리가 시키라고 할때 등장됨(즉, 객체 생성만 일어나고, 등장은 아직 하지 않음)
// 이렇게 처리해주는 것이 SpawnActorDeffered
FTransform transform;
//Equipment
{
// 객체 생성만 완료됨, 등장은 안 시킴
// 여기서 transform은 확정시키지 않으면 BeginPlay()가 끝나면 확정이 됨, 그때 등장할 위치
Equipment = InOwnerCharacter->GetWorld()->SpawnActorDeferred<ACEquipment>(EquipmentClass, transform, InOwnerCharacter);
// mesh에다가 붙여넣음
Equipment->AttachToComponent(InOwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true));
Equipment->SetData(EquipmentData);
// FinishSpawningActor() : 확정해 등장시킬 액터와 Transform을 입력
// transform : 확정되서 등장시킬 위치
UGameplayStatics::FinishSpawningActor(Equipment, transform);
// 연결해줌
Equipment->OnEquipmentDelegate.AddDynamic(Attachment, &ACAttachment::OnEquip);
Equipment->OnUnequipmentDelegate.AddDynamic(Attachment, &ACAttachment::OnUnequip);
}
}
CEquipment.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Actions/CActionData.h"
#include "CEquipment.generated.h"
// CAttachment에 연결될 델리게이트
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FEquipmentDelegate);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FUnequipmentDelegate);
UCLASS()
class UONLINE_04_ACTION_API ACEquipment : public AActor
{
GENERATED_BODY()
public:
// CActionData로 부터 값을 세팅함
FORCEINLINE void SetData(FEquipmentData InData) { Data = InData; }
public:
ACEquipment();
public:
// Equip() 함수는 C++에서 기본으로 내용을 정의해놓고 BP에서 재정을 할 수 있도록 작업함(BlueprintNativeEvent)
UFUNCTION(BlueprintNativeEvent)
void Equip();
// C에서 일단 기본으로 정의해놓음
void Equip_Implementation();
// 장착 개시(노티파이에 의해 콜 됨)
UFUNCTION(BlueprintNativeEvent)
void Begin_Equip();
void Begin_Equip_Implementation();
// 장착 완료
UFUNCTION(BlueprintNativeEvent)
void End_Equip();
void End_Equip_Implementation();
// 해제 명령(따로 장착해제 애니메이션 안할거여서 명령만 넣음)
UFUNCTION(BlueprintNativeEvent)
void Unequip();
void Unequip_Implementation();
protected:
virtual void BeginPlay() override;
public:
// BlueprintAssignable : BP에도 연결될 수 있어서(이벤트 바인딩, 이벤트 언바인딩 가능)
UPROPERTY(BlueprintAssignable)
FEquipmentDelegate OnEquipmentDelegate;
UPROPERTY(BlueprintAssignable)
FUnequipmentDelegate OnUnequipmentDelegate;
// BP 접근위해
protected:
UPROPERTY(BlueprintReadOnly)
class ACharacter* OwnerCharacter;
UPROPERTY(BlueprintReadOnly)
class UCStateComponent* State;
UPROPERTY(BlueprintReadOnly)
class UCStatusComponent* Status;
private:
FEquipmentData Data;
};
CEquipment.cpp
#include "CEquipment.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/CStateComponent.h"
#include "Components/CStatusComponent.h"
ACEquipment::ACEquipment()
{
}
void ACEquipment::BeginPlay()
{
// 찾아놓음
OwnerCharacter = Cast<ACharacter>(GetOwner());
State = CHelpers::GetComponent<UCStateComponent>(OwnerCharacter);
Status = CHelpers::GetComponent<UCStatusComponent>(OwnerCharacter);
// 이 코드가 BP에 있는 BeginPlay()를 호출하는 함수도 있어서
// 필요한 변수들을 세팅후 부모 함수를 호출함
// Actor에서는 항상 Super::BeginPlay()를 밑에다 넣을것임
Super::BeginPlay();
}
void ACEquipment::Equip_Implementation()
{
State->SetEquipMode();
// 칼이 있는애들은 칼을 뽑고
// 마법같은 애는 그런게 없으니까. Equip를 종료시킴
if (Data.AnimMontage != NULL)
OwnerCharacter->PlayAnimMontage(Data.AnimMontage, Data.PlayRatio, Data.StartSection);
else
End_Equip();
// 칼을 장착하면 정면을 바라보게 됨
OwnerCharacter->bUseControllerRotationYaw = true;
// 회전하지 못하도록 함
OwnerCharacter->GetCharacterMovement()->bOrientRotationToMovement = false;
}
void ACEquipment::Begin_Equip_Implementation()
{
if (OnEquipmentDelegate.IsBound())
OnEquipmentDelegate.Broadcast();
}
void ACEquipment::End_Equip_Implementation()
{
State->SetIdleMode();
}
void ACEquipment::Unequip_Implementation()
{
// 따로 호출할 필요가 없음
OwnerCharacter->bUseControllerRotationYaw = false;
OwnerCharacter->GetCharacterMovement()->bOrientRotationToMovement = true;
if (OnUnequipmentDelegate.IsBound())
OnUnequipmentDelegate.Broadcast();
}
CHelpers.h 추가된 내용
#pragma once
#include "CoreMinimal.h"
#include "UObject/ConstructorHelpers.h"
class UONLINE_04_ACTION_API CHelpers
{
public:
...
// 컴포넌트 하나 가져옴(어느 액터에 있는)
template<typename T>
static T* GetComponent(AActor* InActor)
{
// 액터에 해당 클래스로 된 컴포넌트를 하나 찾아줌
return Cast<T>(InActor->GetComponentByClass(T::StaticClass()));
}
};
CStateComponent.h 추가된 내용
UENUM(BlueprintType)
enum class EStateType : uint8
{
Idle, Roll, Backstep, Equip, Max
};
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UONLINE_04_ACTION_API UCStateComponent : public UActorComponent
{
GENERATED_BODY()
public:
...
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsEquipMode() { return Type == EStateType::Equip; }
public:
...
void SetEquipMode();
}
CStateComponent.cpp 추가된 내용
...
void UCStateComponent::SetEquipMode()
{
ChangeType(EStateType::Equip);
}
CPlayer.h 추가된 내용
.,..
UCLASS()
class UONLINE_04_ACTION_API ACPlayer : public ACharacter
{
GENERATED_BODY()
private:
UPROPERTY(VisibleDefaultsOnly)
class UCActionComponent* Action;
}
CPlayer.cpp 추가된 내용
...
#include "Components/CActionComponent.h"
ACPlayer::ACPlayer()
{
...
CHelpers::CreateActorComponent<UCActionComponent>(this, &Action, "Action");
}
CAnimNotifyState_Equip.h
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "CAnimNotifyState_Equip.generated.h"
UCLASS()
class UONLINE_04_ACTION_API UCAnimNotifyState_Equip : public UAnimNotifyState
{
GENERATED_BODY()
public:
FString GetNotifyName_Implementation() const override;
virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration) override;
virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override;
};
CAnimNotifyState_Equip.cpp
#include "CAnimNotifyState_Equip.h"
#include "Global.h"
#include "Actions/CEquipment.h"
#include "Components/CActionComponent.h"
FString UCAnimNotifyState_Equip::GetNotifyName_Implementation() const
{
return "Equip";
}
void UCAnimNotifyState_Equip::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration)
{
// 현재 몽타주가 플레이되려면 무기는 이미 장착되어 있는 상황
Super::NotifyBegin(MeshComp, Animation, TotalDuration);
CheckNull(MeshComp);
CheckNull(MeshComp->GetOwner());
// 해당 액터에 할당되어 있는 컴포넌트를 가져와서 사용하는 방식이
// BP에서 인터페이스로 사용하는 방식보다 훨씬 편함
UCActionComponent* action = CHelpers::GetComponent<UCActionComponent>(MeshComp->GetOwner());
CheckNull(action);
// 여기까지 왔다는것은 Equipment가 있다는 뜻임
action->GetCurrent()->GetEquipment()->Begin_Equip();
}
void UCAnimNotifyState_Equip::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
Super::NotifyEnd(MeshComp, Animation);
CheckNull(MeshComp);
CheckNull(MeshComp->GetOwner());
UCActionComponent* action = CHelpers::GetComponent<UCActionComponent>(MeshComp->GetOwner());
CheckNull(action);
action->GetCurrent()->GetEquipment()->End_Equip();
}
결과
'Unreal Engine 4 > C++' 카테고리의 다른 글
<Unreal C++> 44 - Action RPG(CAction & Melee Attack) (0) | 2022.05.09 |
---|---|
<Unreal C++> 43 - Action RPG(Weapon Attachment) (0) | 2022.05.09 |
<Unreal C++> 37 - Action RPG (Avoiding(Montages)) (0) | 2022.05.03 |
<Unreal C++> 33 - Action RPG(Player & Animation Setting & Avoiding(State)) (0) | 2022.05.02 |
<Unreal C++> 31 - Gun Shooting Mode (Fire Effect) (0) | 2022.05.02 |