본문 바로가기
언리얼엔진

언리얼 엔진 5 C++ (무기 장착 및 교체)

by 대니스 2023. 1. 31.

무기 장착

1. 무기 헤더 파일 및 cpp 파일 생성

1. Tools -> New C++ Class 선택

2. Actor -> Class type은 Public -> Create Class 선택

2. 무기 장착 코드

// 무기(액터) 클래스의 헤더파일
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Weapon.generated.h"

UCLASS(Abstract)
class TRUEFPSTUTORIAL_API AWeapon : public AActor
{
	GENERATED_BODY()

public:
	AWeapon();

protected:
	virtual void BeginPlay() override;

public:
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Components")
		class USceneComponent* Root; 

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Components")
		class USkeletalMeshComponent* Mesh; //Root와 Mesh는 무기의 Mesh를 이용하기 때문에 필요하다.

	UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "State")
		class ATrueFPSCharacter* CurrentOwner; //무기를 들고 있는 캐릭터. 즉, 플레이어를 지칭한다.

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Configurations")
		FTransform PlacementTransform; //무기의 위치를 표시하는 것이다.
};
// 무기(액터) 클래스의 cpp 파일
#include "Weapons/Weapon.h"

AWeapon::AWeapon()
{
	PrimaryActorTick.bCanEverTick = false;

	SetReplicates(true);

	Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
	RootComponent = Root;

	Mesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
	Mesh->SetupAttachment(Root); // 서버에서 무기를 스폰하고 클라이언트에 복제하기 위해서 이와 같이 만든다.
}

void AWeapon::BeginPlay()
{
	Super::BeginPlay();

	if(!CurrentOwner)
		Mesh->SetVisibility(false); //장착하지 않으면 보이지 않게 만든다.
}
// 캐릭터 클래스의 헤더 파일

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "TrueFPSCharacter.generated.h"

UCLASS()
class TRUEFPSTUTORIAL_API ATrueFPSCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ATrueFPSCharacter();

protected:
	virtual void BeginPlay() override;
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override; // 복제를 위한 함수이다.

public:	

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Components")
		class UCameraComponent* Camera;

protected:
	UPROPERTY(EditDefaultsOnly, Category = "Configurations")
		TArray<TSubclassOf<class AWeapon>> DefaultWeapons; //디폴트의 무기 클래스이다.

public:
	UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Replicated, Category = "State")
		TArray<class AWeapon*> Weapons; //무기 호출을 위한 배열이다.

	UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, ReplicatedUsing = OnRep_CurrentWeapon, Category = "State")
		class AWeapon* CurrentWeapon; //현재 들고 있는 무기를 나타낸다.

	UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "State")
		int32 CurrentIndex = 0; //현재 무기의 번호를 나타낸다.

protected:
	UFUNCTION()
		virtual void OnRep_CurrentWeapon(const class AWeapon* OldWeapon); //무기를 바꿀 때 필요한 함수이다.

protected:
	void MoveForward(const float Value);
	void MoveRight(const float Value);
	void LookUp(const float Value);
	void LookRight(const float Value);
};
// 캐릭터 클래스의 cpp 파일


#include "Character/TrueFPSCharacter.h"
#include "..\..\Public\Character\TrueFPSCharacter.h"
#include <Runtime/Engine/Public/Net/UnrealNetwork.h> //이 라이브러리를 포함해야 'DOREPLIFETIME_CONDITION'를 사용할 수 있다.
#include "Weapons/Weapon.h"
#include "Camera/CAmeraComponent.h"

// Sets default values
ATrueFPSCharacter::ATrueFPSCharacter()
{
	PrimaryActorTick.bCanEverTick = false;

	Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
	Camera->bUsePawnControlRotation = true; // 폰의 움직임
	Camera->SetupAttachment(GetMesh(), FName("head")); // 머리쪽으로 카메라 설치 위함
}

void ATrueFPSCharacter::BeginPlay()
{
	Super::BeginPlay();

	if (HasAuthority())
	{
		for (const TSubclassOf<AWeapon>& WeaponClass : DefaultWeapons)
		{
			if (!WeaponClass) continue;
			FActorSpawnParameters Params;
			Params.Owner = this; 
			AWeapon* SpawnedWeapon = GetWorld()->SpawnActor<AWeapon>(WeaponClass, Params); //서버와 클라이언트에 무기 스폰을 해야하기 때문에 반복문을 이용해준다.
			const int32 Index = Weapons.Add(SpawnedWeapon); //무기의 개수를 나타내주는 변수이다.
			if (Index == CurrentIndex)
			{
				CurrentWeapon = SpawnedWeapon;
				OnRep_CurrentWeapon(nullptr);
			} //무기가 하나인 경우를 나타낸다.
		}
	}
}

void ATrueFPSCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME_CONDITION(ATrueFPSCharacter, Weapons, COND_None); 
	DOREPLIFETIME_CONDITION(ATrueFPSCharacter, CurrentWeapon, COND_None); //서버와 클라이언트에서 복제해주는 문장이다.
}

void ATrueFPSCharacter::OnRep_CurrentWeapon(const AWeapon* OldWeapon)
{
	if (CurrentWeapon)
	{
		if (!CurrentWeapon->CurrentOwner)
		{
			const FTransform PlacementTransform = CurrentWeapon->PlacementTransform * GetMesh()->GetSocketTransform(FName("hand_r"));
			CurrentWeapon->SetActorTransform(PlacementTransform, false, nullptr, ETeleportType::TeleportPhysics);
			CurrentWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepWorldTransform, FName("hand_r")); //큰 따옴표 안에는 본인이 장착하고 싶은 부위를 적으면된다. 이 부위는 스켈레탈 메시에 Skeleton Tree에서 확인할 수 있다.

			CurrentWeapon->CurrentOwner = this; //플레이어에게 장착하게 만들어주는 코드이다.
		} //무기가 장착되지 않은 경우에 장착하게 만들어준다.

		CurrentWeapon->Mesh->SetVisibility(true); //무기를 보여주게 만든다.
	}

}

// Called to bind functionality to input
void ATrueFPSCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis(FName("MoveForward"), this, &ATrueFPSCharacter::MoveForward);
	PlayerInputComponent->BindAxis(FName("MoveRight"), this, &ATrueFPSCharacter::MoveRight);
	PlayerInputComponent->BindAxis(FName("LookUp"), this, &ATrueFPSCharacter::LookUp);
	PlayerInputComponent->BindAxis(FName("LookRight"), this, &ATrueFPSCharacter::LookRight);
}

void ATrueFPSCharacter::MoveForward(const float Value)
{
	const FVector& Direction = FRotationMatrix(FRotator(0.f, GetControlRotation().Yaw, 0.f)).GetUnitAxis(EAxis::X);
	AddMovementInput(Direction, Value);
}

void ATrueFPSCharacter::MoveRight(const float Value)
{
	const FVector& Direction = FRotationMatrix(FRotator(0.f, GetControlRotation().Yaw, 0.f)).GetUnitAxis(EAxis::Y);
	AddMovementInput(Direction, Value);
}

void ATrueFPSCharacter::LookUp(const float Value)
{
	AddControllerPitchInput(Value);
}

void ATrueFPSCharacter::LookRight(const float Value)
{
	AddControllerYawInput(Value);
}

3. 무기 블루프린트 생성 및 위치 선정

1. 콘텐츠 브라우저에서 생성하고 싶은 파일 내에서 오른쪽 마우스 클릭

2. Blueprint Class -> ALL CLASSES에서 무기 클래스를 선택 -> Select

3. Mesh 내에 있는 Skeletal Mesh에서 본인이 장착하고 싶은 무기를 선택

4. 캐릭터 블루프린트에서 Configuration내에 있는 Default Weapons의 '+' 버튼을 눌러 무기 블루프린트를 적용

5. 왼쪽 Components에 '+Add'를 눌러 Skeletal Mesh를 Mesh 하위에 생성

6. Skeletal Mesh에 Mesh를 무기 블루프린트에서 적용한 Mesh로 적용 및 Sockets내에 있는 Parent Sockets은 위 코드의 큰 따옴표에 있는 소켓을 적용 

7. 자연스럽게 장착한 모습으로 위치 조정

8. 그 위치를 복사하고 무기 블루프린트에서 붙임

9. 캐릭터 클래스에서 만든 Skeletal Mesh는 삭제

 

무기 교체

1. 입력 설정

1. 메인 화면 상위의 탭 중에서 Edit -> Project Settings 선택

2. Engine -> Input 에서 아래의 그림과 같이 설정 

2. 무기 교체 코드

// 캐릭터 클래스의 헤더 파일

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "TrueFPSCharacter.generated.h"

UCLASS()
class TRUEFPSTUTORIAL_API ATrueFPSCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ATrueFPSCharacter();

protected:
	virtual void BeginPlay() override;
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

public:	

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Components")
		class UCameraComponent* Camera;

protected:
	UPROPERTY(EditDefaultsOnly, Category = "Configurations")
		TArray<TSubclassOf<class AWeapon>> DefaultWeapons;

public:
	UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Replicated, Category = "State")
		TArray<class AWeapon*> Weapons;

	UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, ReplicatedUsing = OnRep_CurrentWeapon, Category = "State")
		class AWeapon* CurrentWeapon;

	UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, Category = "State")
		int32 CurrentIndex = 0;

	UFUNCTION(BlueprintCallable, Category = "Character")
		virtual void EquipWeapon(const int32 Index); //무기를 바꾸기 위해 필요함

protected:
	UFUNCTION()
		virtual void OnRep_CurrentWeapon(const class AWeapon* OldWeapon);

	UFUNCTION(Server, Reliable)
		void Server_SetCurrentWeapon(class AWeapon* Weapon); //서버에 무기를 호출하기 위한 함수
		virtual void Server_SetCurrentWeapon_Implementation(class AWeapon* NewWeapon); //위를 실행하는 함수

protected:
	virtual void NextWeapon(); //다음 무기로 넘어가기 위한 함수
	virtual void LastWeapon(); //이전 무기로 넘어가기 위한 함수 

	void MoveForward(const float Value);
	void MoveRight(const float Value);
	void LookUp(const float Value);
	void LookRight(const float Value);
};
// 캐릭터 클래스의 cpp 파일


#include "Character/TrueFPSCharacter.h"
#include "..\..\Public\Character\TrueFPSCharacter.h"
#include <Runtime/Engine/Public/Net/UnrealNetwork.h>
#include "Weapons/Weapon.h"
#include "Camera/CAmeraComponent.h"

ATrueFPSCharacter::ATrueFPSCharacter()
{
	PrimaryActorTick.bCanEverTick = false;

	Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
	Camera->bUsePawnControlRotation = true; // 폰의 움직임
	Camera->SetupAttachment(GetMesh(), FName("head")); // 머리쪽으로 카메라 설치 위함
}

void ATrueFPSCharacter::BeginPlay()
{
	Super::BeginPlay();

	if (HasAuthority())
	{
		for (const TSubclassOf<AWeapon>& WeaponClass : DefaultWeapons)
		{
			if (!WeaponClass) continue;
			FActorSpawnParameters Params;
			Params.Owner = this;
			AWeapon* SpawnedWeapon = GetWorld()->SpawnActor<AWeapon>(WeaponClass, Params);
			const int32 Index = Weapons.Add(SpawnedWeapon);
			if (Index == CurrentIndex)
			{
				CurrentWeapon = SpawnedWeapon;
				OnRep_CurrentWeapon(nullptr);
			}
		}
	}
}

void ATrueFPSCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME_CONDITION(ATrueFPSCharacter, Weapons, COND_None);
	DOREPLIFETIME_CONDITION(ATrueFPSCharacter, CurrentWeapon, COND_None);
}

void ATrueFPSCharacter::OnRep_CurrentWeapon(const AWeapon* OldWeapon)
{
	if (CurrentWeapon)
	{
		if (!CurrentWeapon->CurrentOwner)
		{
			const FTransform PlacementTransform = CurrentWeapon->PlacementTransform * GetMesh()->GetSocketTransform(FName("hand_r"));
			CurrentWeapon->SetActorTransform(PlacementTransform, false, nullptr, ETeleportType::TeleportPhysics);
			CurrentWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepWorldTransform, FName("hand_r"));

			CurrentWeapon->CurrentOwner = this;
		}

		CurrentWeapon->Mesh->SetVisibility(true);
	}

	if (OldWeapon)
	{
		OldWeapon->Mesh->SetVisibility(false); //무기를 바꿀 때 이전에 쓰였던 무기는 보이지 않게 하기 위해서 false로 둔다
	}
}

void ATrueFPSCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAction(FName("NextWeapon"), EInputEvent::IE_Pressed, this, &ATrueFPSCharacter::NextWeapon); 
	PlayerInputComponent->BindAction(FName("LastWeapon"), EInputEvent::IE_Pressed, this, &ATrueFPSCharacter::LastWeapon); //바인딩을 위한 코드

	PlayerInputComponent->BindAxis(FName("MoveForward"), this, &ATrueFPSCharacter::MoveForward);
	PlayerInputComponent->BindAxis(FName("MoveRight"), this, &ATrueFPSCharacter::MoveRight);
	PlayerInputComponent->BindAxis(FName("LookUp"), this, &ATrueFPSCharacter::LookUp);
	PlayerInputComponent->BindAxis(FName("LookRight"), this, &ATrueFPSCharacter::LookRight);
}

void ATrueFPSCharacter::EquipWeapon(const int32 Index)
{
	if (!Weapons.IsValidIndex(Index) || CurrentWeapon==Weapons[Index]) return; //Index에 무기를 사용할 수 없거나 교체할 무기가 없는 경우

	if (IsLocallyControlled())
	{
		CurrentIndex = Index;

		const AWeapon* OldWeapon = CurrentWeapon;
		CurrentWeapon = Weapons[Index];
		OnRep_CurrentWeapon(OldWeapon);
	} // 무기를 바꿀 수 있는 환경인 경우
	else if (!HasAuthority())
	{
		Server_SetCurrentWeapon(Weapons[Index]);
	} // 클라이언트로 승인 호출을 못 할 경우
}

void ATrueFPSCharacter::Server_SetCurrentWeapon_Implementation(AWeapon* NewWeapon)
{
	const AWeapon* OldWeapon = CurrentWeapon;
	CurrentWeapon = NewWeapon; // 무기 교체
	OnRep_CurrentWeapon(OldWeapon);
} //서버에 무기를 호출해줌

void ATrueFPSCharacter::NextWeapon()
{
	const int32 Index = Weapons.IsValidIndex(CurrentIndex + 1) ? CurrentIndex + 1 : 0;
	EquipWeapon(Index);
} // 휠로 다음 무기로 교체

void ATrueFPSCharacter::LastWeapon()
{
	const int32 Index = Weapons.IsValidIndex(CurrentIndex - 1) ? CurrentIndex - 1 : Weapons.Num()-1;
	EquipWeapon(Index);
} // 휠로 이전 무기로 교체

void ATrueFPSCharacter::MoveForward(const float Value)
{
	const FVector& Direction = FRotationMatrix(FRotator(0.f, GetControlRotation().Yaw, 0.f)).GetUnitAxis(EAxis::X);
	AddMovementInput(Direction, Value);
}

void ATrueFPSCharacter::MoveRight(const float Value)
{
	const FVector& Direction = FRotationMatrix(FRotator(0.f, GetControlRotation().Yaw, 0.f)).GetUnitAxis(EAxis::Y);
	AddMovementInput(Direction, Value);
}

void ATrueFPSCharacter::LookUp(const float Value)
{
	AddControllerPitchInput(Value);
}

void ATrueFPSCharacter::LookRight(const float Value)
{
	AddControllerYawInput(Value);
}

3. 무기 블루프린트 생성 및 위치 선정

※ 위에 있는 설명과 동일하므로 생략

 

● 참고 동영상 : https://www.youtube.com/watch?v=QA-3466E6LU&t=2835s

● 무기 에셋들은 마켓플레이스 Military Weapons Silver에서 받았습니다.