필요한 개념
<Player Setting>
Player 기본 구조가 밑의 구조다.
그런데 Character에는 Mesh는 기본적으로 있으니까, 즉, SpringArm과 Camera 컴포넌트를 넣어줘야 함
Mesh의 회전값이 -90이므로 자식인 SpringArm도 -90도 회전되어있어서, SpringArm을 다시 90도 돌려서 0으로 만든다.
// 사방을 뛰어다닐 수 있게(카메라의 회전에 따라 회전하지 않음(Yaw))
bUseControllerRotationYaw = false;
// bDoCollisionTest : SpringArm 사이에 뭔가 충돌하면 카메라 회전 방지
SpringArm->bDoCollisionTest = false;
// RotationRate : OrientRotationToMovement 이용할때 회전 속도 올려줌
// 원래 기본값 360이어서 720으로 2배 빠르게 해줌
// bOrientRotationToMovement : 현재 캐릭터가 가속값을 가지고 있다면
// 현재 가속되고있는값 방향으로 캐릭터 메쉬를 회전시켜줍니다.
GetCharacterMovement()->RotationRate = FRotator(0, 720, 0);
GetCharacterMovement()->bOrientRotationToMovement = true;
<Animation Setting>
<게임모드에서 플레이어 세팅>
CGameMode.cpp
CHelpers::GetClass(&DefaultPawnClass, "Blueprint'/Game/Player/BP_CPlayer.BP_CPlayer_C'");
* 프로젝트 세팅 -> 맵&모드 -> CGameMode 세팅
<플레이어 키 입력 이동 처리>
* PlayerController에서 해줘도 되지만, 복잡해서 Player에서 해주는게 좋다.
void ACPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAxis("MoveForward", this, &ACPlayer::OnMoveFoward);
PlayerInputComponent->BindAxis("MoveRight", this, &ACPlayer::OnMoveRight);
PlayerInputComponent->BindAxis("HorizontalLook", this, &ACPlayer::OnHorizontalLook);
PlayerInputComponent->BindAxis("VerticalLook", this, &ACPlayer::OnVerticalLook);
}
void ACPlayer::OnMoveFoward(float InAxis)
{
FRotator rotator = FRotator(0, GetControlRotation().Yaw, 0);
FVector direction = FQuat(rotator).GetForwardVector();
AddMovementInput(direction, InAxis);
}
void ACPlayer::OnMoveRight(float InAxis)
{
FRotator rotator = FRotator(0, GetControlRotation().Yaw, 0);
FVector direction = FQuat(rotator).GetRightVector();
AddMovementInput(direction, InAxis);
}
* HorizontalLook, VerticalLook은 따로 처리
액터 컴포넌트로부터 상속받아 회전 속도를 조정하는 값들을 관리하는 컴포넌트를 하나 제작해서 처리함(옵션해서 관리함)
ActorComponent : 데이터를 관리하기 위해 사용
카메라 속도를 조정하기 위한 컴포넌트 -> COptionComponent class 생성
나중에 Timeline ZoomIn, ZoomOut 속도도 여기다 넣으면 된다.
* COptionComponent를 들여다 보면
// ClassGroup=(GameProject) -> 클래스 그룹을 지정가능
// meta=(BlueprintSpawnableComponent) -> 블루프린트에서 생성 가능하다는 문구
UCLASS(ClassGroup=(GameProject), meta=(BlueprintSpawnableComponent))
class UONLINE_04_ACTION_API UCOptionComponent : public UActorComponent
{
...
}
* COptionComponent에서 값 가져와서 회전 값 세팅
void ACPlayer::OnHorizontalLook(float InAxis)
{
float rate = Option->GetHorizontalLookRate();
// deltaTime도 넣어줘야 정확한 값이 됨
AddControllerYawInput(InAxis * rate * GetWorld()->GetDeltaSeconds());
}
void ACPlayer::OnVerticalLook(float InAxis)
{
float rate = Option->GetVerticalLookRate();
AddControllerPitchInput(InAxis * rate* GetWorld()->GetDeltaSeconds());
}
<캐릭터 애니메이션 처리>
AnimInstance를 상속받는 클래스를 생성한다.
여기서 기본적으로 필요한건 캐릭터의 속도, 방향이다.
UPROPERTY(BlueprintReadOnly, EditAnywhere)
float Speed;
BlueprintReadOnly : 블루프린트에서 읽을 수 있도록
NativeBeginPlay : 게임이 시작되고, 애니메이션이 최초의 콜이 될때 호출(BeginPlay)
NativeUpdateAnimation : 게임뿐만이 아니라 에디터 상황에서도 애니메이션이 플레이 될 때 호출(Tick)
Speed = character->GetVelocity().Size2D();
Direction = CalculateDirection(character->GetVelocity(), character->GetControlRotation());
얘를 기반으로 AnimBP를 만든다.
* CPlayer에서 AnimBP를 불러옴
TSubclassOf<UAnimInstance> animInstance;
CHelpers::GetClass<UAnimInstance>(&animInstance, "AnimBlueprint'/Game/Player/ABP_CPlayer.ABP_CPlayer_C'");
GetMesh()->SetAnimInstanceClass(animInstance);
<상태 관리 컴포넌트 생성(StatusComponent)>
상태관리 ex) HP라던가 캐릭터의 상태를 관리할 액터 컴포넌트 클래스(CStatusComponent)를 생성한다.
이동 속도나, HP등을 관리하게 됨
* CStatusComponent를 활용하여 상태 체크
// CPlayer.cpp
void ACPlayer::OnMoveFoward(float InAxis)
{
// 이동 금지 상황일때 빠져나옴
CheckFalse(Status->CanMove());
FRotator rotator = FRotator(0, GetControlRotation().Yaw, 0);
FVector direction = FQuat(rotator).GetForwardVector();
AddMovementInput(direction, InAxis);
}
void ACPlayer::OnMoveRight(float InAxis)
{
CheckFalse(Status->CanMove());
FRotator rotator = FRotator(0, GetControlRotation().Yaw, 0);
FVector direction = FQuat(rotator).GetRightVector();
AddMovementInput(direction, InAxis);
}
*Tip)
대부분 컴포넌트가 있다면 값을 가져다 쓰는 방식을 많이씀
<Player Avoiding(StateComponent 생성)>
애니메이션 상태 관리
(애니메이션 상태에 따라 몽타주 플레이 가능)
Avoid 몽타주 동작 관리를 위해 사용
*Tip) 상태를 정의할 enum 자료형을 만듬
실무에서는 간단한 상태를 관리하기 위해서는
enum을 사용하기는 하지만 캐릭터의 상태처럼 복잡한 상태를 관리하는 것은
enum을 사용하지 않음(StatePattern을 사용)
// StateComponent.h
UENUM
- BlueprintType : 블루프린트와 통신할 것인지
// 언리얼에서는 기본적으로 ENUM은 uint8(unsigned int8)을 사용
UENUM(BlueprintType)
enum class EStateType : uint8
{
// 애니메이션 상태 정의
Idle, Roll, BackStep,
};
// 상태를 바뀌기 위해
// 이벤트로 해줘도 되지만, BP콜이 이게 편함
// 기존타입과 새로 바꾸려는 타입을 넣어줌
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FStateTypeChanged, EStateType, InPrevType, EStateType, InNewType);
public:
// Get은 괜찮지만, Set은 이 방식은 관리가 짜증남(어디서 set되는지 알수가 없다.)
//FORCEINLINE void SetType(EStateType InType) { InType = Type; }
//FORCEINLINE EStateType GetType() {return Type;}
// 그래서 함수를 하나하나 만들어줌(처음에는 귀찮지만, 관리는 편함)
// 이렇게 하면 일일히 위처럼 리턴받아 비교하지 않아도
// IsIdleMode()를 콜하면 받아서 바로 쓸 수 있다.
// BlueprintPure : (const) getter의 성격에 해당하는 함수를 블루프린트 그래프에 노출시킬 때 사용하면 된다.
// Get은 BlueprintPure하고 Set은 BlueprintCallable하는 게 좋다.
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsIdleMode() { return Type == EStateType::Idle; }
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsRollMode() { return Type == EStateType::Roll; }
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsBackstepMode() { return Type == EStateType::Backstep; }
public:
// 얘의 경우에도 따로 Type을 받지 않아도 Idle 상태로 안에서 Type을 바꿔주면 됨
void SetIdleMode();
void SetRollMode();
void SetBackstepMode();
private:
void ChangeType(EStateType InType);
public:
// BlueprintAssignable : 블루프린트에서도 할당 가능하도록
// 즉, 다이나믹 멀티캐스트 델리게이트에 UPROPERTY(BlueprintAssignable)을 지정해 주어야만 블루프린트에서 해당 델리게이트를 검색할 수 있게 된다.
UPROPERTY(BlueprintAssignable)
FStateTypeChanged OnStateTypeChanged;
// UCStateComponent.cpp
void UCStateComponent::SetIdleMode()
{
// 디버깅 모드로 누구에 의해 콜되었는지 확인가능한 장점이 있음
ChangeType(EStateType::Idle);
}
void UCStateComponent::SetRollMode()
{
ChangeType(EStateType::Roll);
}
void UCStateComponent::SetBackstepMode()
{
ChangeType(EStateType::Backstep);
}
void UCStateComponent::ChangeType(EStateType InType)
{
// 기존 타입
EStateType type = Type;
// 바뀌려는 새타입
Type = InType;
if (OnStateTypeChanged.IsBound())
OnStateTypeChanged.Broadcast(type, InType);
}
UOnline_04_Action.Build.CS
using UnrealBuildTool;
public class UOnline_04_Action : ModuleRules
{
public UOnline_04_Action(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
PrivateDependencyModuleNames.AddRange(new string[] { });
// ModuleDirectiory 디렉터리는 Sources 폴더 밑 프로젝트 폴더
// 이 폴더를 기준으로 헤더를 불러들일 수 있도록 세팅
// ex) 그래야 폴더 내에 있는 파일들이 밖에 있는 Global.h를 경로를 생략하고 불러들일 수가 있음
PublicIncludePaths.Add(ModuleDirectory);
}
}
CPlayer.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "CPlayer.generated.h"
UCLASS()
class UONLINE_04_ACTION_API ACPlayer : public ACharacter
{
GENERATED_BODY()
private:
UPROPERTY(VisibleDefaultsOnly)
class USpringArmComponent* SpringArm;
UPROPERTY(VisibleDefaultsOnly)
class UCameraComponent* Camera;
private:
UPROPERTY(VisibleDefaultsOnly)
class UCOptionComponent* Option;
UPROPERTY(VisibleDefaultsOnly)
class UCStatusComponent* Status;
public:
ACPlayer();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
private:
void OnMoveFoward(float InAxis);
void OnMoveRight(float InAxis);
void OnHorizontalLook(float InAxis);
void OnVerticalLook(float InAxis);
};
CPlayer.cpp
#include "CPlayer.h"
#include "Global.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/InputComponent.h"
#include "Animation/AnimInstance.h"
#include "Components/COptionComponent.h"
#include "Components/CStatusComponent.h"
ACPlayer::ACPlayer()
{
PrimaryActorTick.bCanEverTick = true;
// 메시에 붙음
CHelpers::CreateComponent<USpringArmComponent>(this, &SpringArm, "SpringArm", GetMesh());
// SprintArm에 붙음
CHelpers::CreateComponent<UCameraComponent>(this, &Camera, "Camera", SpringArm);
CHelpers::CreateActorComponent<UCOptionComponent>(this, &Option, "Option");
CHelpers::CreateActorComponent<UCStatusComponent>(this, &Status, "Status");
// 사방을 뛰어다닐 수 있게(카메라의 회전에 따라 회전하지 않음(Yaw))
bUseControllerRotationYaw = false;
GetMesh()->SetRelativeLocation(FVector(0, 0, -90));
GetMesh()->SetRelativeRotation(FRotator(0, -90, 0));
USkeletalMesh* mesh;
CHelpers::GetAsset<USkeletalMesh>(&mesh, "SkeletalMesh'/Game/Character/Mesh/SK_Mannequin.SK_Mannequin'");
GetMesh()->SetSkeletalMesh(mesh);
SpringArm->SetRelativeLocation(FVector(9, 0, 140));
// 원래 회전으로 돌려놈
SpringArm->SetRelativeRotation(FRotator(0, 90, 0));
SpringArm->TargetArmLength = 200.0f;
// bDoCollisionTest : SpringArm 사이에 뭔가 충돌하면 카메라 회전 방지
SpringArm->bDoCollisionTest = false;
SpringArm->bUsePawnControlRotation = true;
SpringArm->bEnableCameraLag = true;
// GetCharacterMovement()->MaxWalkSpeed =
// RotationRate : OrientRotationToMovement 이용할때 회전 속도 올려줌
// 원래 기본값 360이어서 720으로 2배 빠르게 해줌
// bOrientRotationToMovement : 현재 캐릭터가 가속값을 가지고 있다면
// 현재 가속되고있는값 방향으로 캐릭터 메쉬를 회전시켜줍니다.
GetCharacterMovement()->RotationRate = FRotator(0, 720, 0);
GetCharacterMovement()->bOrientRotationToMovement = true;
TSubclassOf<UAnimInstance> animInstance;
CHelpers::GetClass<UAnimInstance>(&animInstance, "AnimBlueprint'/Game/Player/ABP_CPlayer.ABP_CPlayer_C'");
GetMesh()->SetAnimInstanceClass(animInstance);
}
void ACPlayer::BeginPlay()
{
Super::BeginPlay();
}
void ACPlayer::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void ACPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAxis("MoveForward", this, &ACPlayer::OnMoveFoward);
PlayerInputComponent->BindAxis("MoveRight", this, &ACPlayer::OnMoveRight);
PlayerInputComponent->BindAxis("HorizontalLook", this, &ACPlayer::OnHorizontalLook);
PlayerInputComponent->BindAxis("VerticalLook", this, &ACPlayer::OnVerticalLook);
}
void ACPlayer::OnMoveFoward(float InAxis)
{
// 이동 금지 상황일때 빠져나옴
CheckFalse(Status->CanMove());
FRotator rotator = FRotator(0, GetControlRotation().Yaw, 0);
FVector direction = FQuat(rotator).GetForwardVector();
AddMovementInput(direction, InAxis);
}
void ACPlayer::OnMoveRight(float InAxis)
{
CheckFalse(Status->CanMove());
FRotator rotator = FRotator(0, GetControlRotation().Yaw, 0);
FVector direction = FQuat(rotator).GetRightVector();
AddMovementInput(direction, InAxis);
}
void ACPlayer::OnHorizontalLook(float InAxis)
{
float rate = Option->GetHorizontalLookRate();
// deltaTime도 넣어줘야 정확한 값이 됨
AddControllerYawInput(InAxis * rate * GetWorld()->GetDeltaSeconds());
}
void ACPlayer::OnVerticalLook(float InAxis)
{
float rate = Option->GetVerticalLookRate();
AddControllerPitchInput(InAxis * rate* GetWorld()->GetDeltaSeconds());
}
CAnimInstance.h
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "CAnimInstance.generated.h"
UCLASS()
class UONLINE_04_ACTION_API UCAnimInstance : public UAnimInstance
{
GENERATED_BODY()
protected:
UPROPERTY(BlueprintReadOnly, EditAnywhere)
float Speed;
UPROPERTY(BlueprintReadOnly, EditAnywhere)
float Direction;
public:
virtual void NativeBeginPlay() override;
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
};
CAnimInstance.cpp
#include "CAnimInstance.h"
#include "Global.h"
#include "GameFramework/Character.h"
void UCAnimInstance::NativeBeginPlay()
{
Super::NativeBeginPlay();
}
void UCAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
ACharacter* character = Cast<ACharacter>(TryGetPawnOwner());
CheckNull(character);
Speed = character->GetVelocity().Size2D();
Direction = CalculateDirection(character->GetVelocity(), character->GetControlRotation());
}
CGameMode.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "CGameMode.generated.h"
UCLASS()
class UONLINE_04_ACTION_API ACGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
ACGameMode();
};
CGameMode.cpp
#include "CGameMode.h"
#include "Global.h"
ACGameMode::ACGameMode()
{
CHelpers::GetClass<APawn>(&DefaultPawnClass, "Blueprint'/Game/Player/BP_CPlayer.BP_CPlayer_C'");
}
COptionComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "COptionComponent.generated.h"
UCLASS( ClassGroup=(GameProject), meta=(BlueprintSpawnableComponent) )
class UONLINE_04_ACTION_API UCOptionComponent : public UActorComponent
{
GENERATED_BODY()
private:
UPROPERTY(EditDefaultsOnly)
float HorizontalLookRate = 45;
UPROPERTY(EditDefaultsOnly)
float VerticalLookRate = 45;
public:
FORCEINLINE float GetHorizontalLookRate() { return HorizontalLookRate; }
FORCEINLINE float GetVerticalLookRate() { return VerticalLookRate; }
public:
UCOptionComponent();
protected:
virtual void BeginPlay() override;
};
COptionComponent.cpp
#include "COptionComponent.h"
#include "Global.h"
UCOptionComponent::UCOptionComponent()
{
}
void UCOptionComponent::BeginPlay()
{
Super::BeginPlay();
}
CStatusComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CStatusComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UONLINE_04_ACTION_API UCStatusComponent : public UActorComponent
{
GENERATED_BODY()
private:
// 캐릭터의 이동 속도 관리
UPROPERTY(EditDefaultsOnly, Category = "Speed")
float WalkSpeed = 200.0f;
UPROPERTY(EditDefaultsOnly, Category = "Speed")
float RunSpeed = 400.0f;
UPROPERTY(EditDefaultsOnly, Category = "Speed")
float SprintSpeed = 400.0f;
public:
FORCEINLINE float GetWalkSpeed() { return WalkSpeed; }
FORCEINLINE float GetRunSpeed() { return RunSpeed; }
FORCEINLINE float GetSprintSpeed() { return SprintSpeed; }
FORCEINLINE bool CanMove() { return bCanMove; }
public:
UCStatusComponent();
// 이동 가능하도록, 하지 못하도록 외부에서 세팅가능
void SetMove();
void SetStop();
protected:
virtual void BeginPlay() override;
private:
// 이동 가능한지
bool bCanMove = true;
};
CStatusComponent.cpp
#include "CStatusComponent.h"
#include "Global.h"
UCStatusComponent::UCStatusComponent()
{
}
void UCStatusComponent::SetMove()
{
bCanMove = true;
}
void UCStatusComponent::SetStop()
{
bCanMove = false;
}
void UCStatusComponent::BeginPlay()
{
Super::BeginPlay();
}
CStateComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CStateComponent.generated.h"
// 상태를 정의할 enum 자료형을 만듬
// 실무에서는 간단한 상태를 관리하기 위해서는
// enum을 사용하기는 하지만 캐릭터의 상태처럼 복잡한 상태를 관리하는 것은
// enum을 사용하지 않음(StatePattern을 사용)
// 언리얼에서는 기본적으로 ENUM은 uint8(unsigned int8)을 사용
UENUM(BlueprintType)
enum class EStateType : uint8
{
Idle, Roll, Backstep,
};
// 상태를 바뀌기 위해
// 이벤트로 해줘도 되지만, BP콜이 이게 편함
// 기존타입과 새로 바꾸려는 타입을 넣어줌
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FStateTypeChanged, EStateType, InPrevType, EStateType, InNewType);
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UONLINE_04_ACTION_API UCStateComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Get은 괜찮지만, Set은 이 방식은 관리가 짜증남(어디서 set되는지 알수가 없다.)
//FORCEINLINE void SetType(EStateType InType) { InType = Type; }
//FORCEINLINE EStateType GetType() {return Type;}
// 그래서 함수를 하나하나 만들어줌(처음에는 귀찮지만, 관리는 편함)
// 이렇게 하면 일일히 위처럼 리턴받아 비교하지 않아도
// IsIdleMode()를 콜하면 받아서 바로 쓸 수 있다.
// BlueprintPure : (const) getter의 성격에 해당하는 함수를 블루프린트 그래프에 노출시킬 때 사용하면 된다.
// Get은 BlueprintPure하고 Set은 BlueprintCallable하는 게 좋다.
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsIdleMode() { return Type == EStateType::Idle; }
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsRollMode() { return Type == EStateType::Roll; }
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsBackstepMode() { return Type == EStateType::Backstep; }
public:
// 얘의 경우에도 따로 Type을 받지 않아도 Idle 상태로 안에서 Type을 바꿔주면 됨
void SetIdleMode();
void SetRollMode();
void SetBackstepMode();
private:
void ChangeType(EStateType InType);
public:
UCStateComponent();
protected:
virtual void BeginPlay() override;
public:
// BlueprintAssignable : 블루프린트에서도 할당 가능하도록
UPROPERTY(BlueprintAssignable)
FStateTypeChanged OnStateTypeChanged;
private:
EStateType Type;
};
CStateComponent.cpp
#include "CStateComponent.h"
#include "Global.h"
UCStateComponent::UCStateComponent()
{
}
void UCStateComponent::BeginPlay()
{
Super::BeginPlay();
}
void UCStateComponent::SetIdleMode()
{
// 디버깅 모드로 누구에 의해 콜되었는지 확인가능한 장점이 있음
ChangeType(EStateType::Idle);
}
void UCStateComponent::SetRollMode()
{
ChangeType(EStateType::Roll);
}
void UCStateComponent::SetBackstepMode()
{
ChangeType(EStateType::Backstep);
}
void UCStateComponent::ChangeType(EStateType InType)
{
// 기존 타입
EStateType type = Type;
// 바뀌려는 새타입
Type = InType;
if (OnStateTypeChanged.IsBound())
OnStateTypeChanged.Broadcast(type, InType);
}
결과
'Unreal Engine 4 > C++' 카테고리의 다른 글
<Unreal C++> 40 - Action RPG (Action Component & Equip) (0) | 2022.05.09 |
---|---|
<Unreal C++> 37 - Action RPG (Avoiding(Montages)) (0) | 2022.05.03 |
<Unreal C++> 31 - Gun Shooting Mode (Fire Effect) (0) | 2022.05.02 |
<Unreal C++> 27 - Gun Shooting Mode(Aiming), Fire & CrossHair (0) | 2022.04.25 |
<Unreal C++> 24 - Gun Shooting Mode(Rifle Equip) (0) | 2022.04.24 |