본문 바로가기

Unreal Engine 4/C++

<Unreal C++> 24 - Gun Shooting Mode(Rifle Equip)

필요한 개념


전체적인 구조 : IRifle을 CPlayer가 상속받아 Rifle을 다룰 수 있도록 하고, 즉, IRifle을 상속받는 애들은 Rifle을 다룰 수 있게 되는 애들이다. CRifle에서는 몽타주 재생효과 및 무기탈착에 대한 전반적인 정보를 가진다. OwnerCharacter를 구해서 처리한다. CAnimInstance에서는 bool 타입으로 탈착여부를 가지고 있고, 이것으로 어떤 포즈를 가질지 결정한다. 그리고 Montage를 재생하다보면 우리가 NotifyState를 세팅한 부분이 호출될 것이다. CAnimNotifyState_Equip, CAnimNotifyState_UnEquip에서는 각각 장착, 탈착에 관한 것을 CRifle에서 호출해준다.

 

 

* 액션 매핑 추가

 

*액션 매핑 추가

 




* 소켓 추가
SK_Mannequin 스켈레톤 트리에 총기를 붙일 소켓 추가(소켓에 프리뷰 메시 추가하고 무기에 맞게 Transform 세팅)

 

*&nbsp;등,&nbsp;손&nbsp;무기&nbsp;소켓&nbsp;추가

 



원래 Spawn시키는 함수를 Factory라 해서 만드는 함수를 내부에 심어 놓음(그냥 Spawn시키는 식으로 Factory처럼 내부에 Static으로 놔서 어떻게 처리하는지 알려줌)

public:
// 1. 스폰시킬 위치 2 : 소유할 캐릭터
static ACRifle* Spawn(class UWorld* InWorld, class ACharacter* InOwner);

// 뭘 스폰이나 생성할때 내부적으로 생성해서 리턴시켜주는 Factory구조라고 함
// 총을 스폰 시키고 싶으면 이 함수를 사용해 총 객체를 리턴받으면 된다.
ACRifle* ACRifle::Spawn(UWorld* InWorld, ACharacter* InOwner)
{
FActorSpawnParameters params;
params.Owner = InOwner;

// spawn 시키면서 ACRifle 형태로 Spawn됨
return InWorld->SpawnActor<ACRifle>(params);
}

// 해당 클래스 내부에 정의해서 해당 클래스 내부에서 해당 객체를 생성하여 리턴하는 방식을 팩토리라고 부름
// Q) Spawn을 외부에서 만들어서 콜 하면 안되나?
// 외부에서 만들면 Spawn되는 코드를 다 찾아서 수정해야하기 때문에, 이렇게 내부에 모아 놓으면 공통으로 사용하니까 여기만 수정하면됨(관리 편하도록)
// factory라는게 찍어내는 것(모두 다 동일하게 찍어내는 것)



총에 대한 추가적인 설정이 있다면 BP로 만들고 그걸로 스폰시키면 됨

Spawn, Attach는 BeginPlay에서 하면됨

BlendSpace로 Aim, Grab해서 움직이는 것도 가지고 있어야함
Grab 애니메이션으로 Grab, UnGrab -> 몽타주 생성

FORCEINLINE 이란?

public:
FORCEINLINE bool GetEquipped() { return bEquipped; }

사실

public:
bool GetEquipped() { return bEquipped; } -> 이렇게만 해도 inline이 이뤄진다.(클래스 선언부에서 함수의 선언과 정의를 동시에 한다면 인라인으로 취급됨) 

인라인 함수 : 함수 호출부로 점프하지 않고 컴파일 시에 해당 함수의 정의 부분을 호출부분으로 복사되는 함수(간단한거는 점프하지 않아서 빠름, 복잡하는거는 inline이 문제가 될 소지가 있음)
언리얼에는 FORCEINLINE을 붙여서 항상 인라인 할것을 권고함(!정의와 선언부가 같이 있다면 FORCEINLINE을 붙여라)

* 인라인 함수인지 확인하는 방법은
public:
bool GetEquipped() { return bEquipped; } // Ctrl + . 해서 정의 위치 이동하면 밑에 처럼 결과가 바뀜
=======================================
.h
=======================================
public:
bool GetEquipped();
=======================================
.cpp
=======================================
inline bool ACRifle::GetEquipped() { return bEquipped; }

인라인화된 함수여서 그럼
이거는 Visual C의 기준인거고
게임기마다 규칙은 지키는데 인라인 규칙은 다다름(XBox다르고 플스 다르고)
FORCEINLINE : 어떤 플랫폼에 상관없이 이 키워드가 붙은 함수는 무조건 인라인 함수로 사용되도록 강제하는 키워드

*Tip) 함수 내에서 if, else 보단 if에서 return 빠져나오고 else 안쓰고 코드를 넣는 것이 보기가 편함

void ACPlayer::OnRifle()
{
	if (Rifle->GetEquipped())
	{
		Rifle->UnEquip();
	
		return;
	}

	Rifle->Equip();
}




// 어떤 함수에서 몽타주를 호출해서 Begin과 End를 호출한다면 이름 관리를
// 편하게 하기 위해 해당 함수의 이름앞에 Begin_, End_를 붙여 함수이름관리를
// 용이하게 함

// 장착명령
void Equip();
void Begin_Equip();
void End_Equip();

// 장착해제
void UnEquip();
void Begin_UnEquip();
void End_UnEquip();

 

* 몽타주 속도 세팅
몽타주 -> 에셋 디테일 -> RateScale을 수정하면 속도가 달라짐

* 몽타주 속도 세팅

 

 


* 몽타주 디폴트 그룹 3가지
몽타주->슬롯 ->슬롯이름 -> 3가지 나옴(DefaultGroup.DefaultSlot, DefaultGroup.UpperBody, DefaultGroup.FullBody)
DefaultGroup.UpperBody로 세팅

* 몽타주 디폴트 그룹 3가지



애니메이션 섞음
애니메이션 블루프린트에서 Rifle Grab Idle의 상체와 기본 동작의 하체를 섞어서 상체에는 총을 쥐고 있는 동작을 플레이시킴
(섞으면 발은 뛰고 상체는 무기를 든 Idle이 됨)
(섞을 때(본마다 레이어로 블랜딩) Spine01, 02, 03중 제일 그럴싸한 것을 선택해서 섞으면 된다.) -> 우리는 spine_01 기준으로 섞음
디테일을 보면
LayerSetup 추가해서
BoneName 입력해주고, BlendDepth : 1, MeshSpaceRotationBlend(메쉬공간 회전) : true
Runtime -> BlendWeight도 꺼줌(완전히 섞을 꺼라)

 

*AnimBP 노드 설명



* 총을 장착하는지 여부로 애니메이션 판단
bool로 포즈를 블렌딩(True면 위에 포즈, false면 밑에 포즈)
결국 bool값이 필요해서 AnimInstance C에서 작업하는 것에서 bool 타입을 넣어줌
(총은 플레이어(적, 플레이어) 사용) -> 인터페이스 사용해야함(C에서 만듬)


* 새 C++만들기 맨 밑에 UnrealInterface만들기가 있음
(UObject 인터페이스 클래스로, 다른 UObject 기반 클래스에서 구현됨)이라고 설명 나오는데
다른 UObject 클래스에서 구현해달라는 의미임

*UnrealInterface 만들기

 

 

 

// UINTERFACE가 붙은 위의 클래스는 이 부분은 BP와 통신을 위해
// 자동으로 정의되어 있는 클래스이며 따로 수정하지 않습니다.
UINTERFACE(MinimalAPI)
class UIRifle : public UInterface
{
GENERATED_BODY()
};




* AnimNotifyState로 노티파이 클래스 생성해준다.(무기 장착위해)

*NotifyState 클래스 추가

 


TIP)
4.25에서는 접근지정자를 정의하지 않으면 public이 아니라 private이다.

 

* NotifyState 추가

노티파이 클래스 컴파일 하면
몽타주->노티파이 우클릭 -> 노티파이 스테이트 추가에 해당 클래스 명이 뜨게 된다.
추가 해주면 된다.

 

*AddNotifyState



노티파이를 쭉 늘여뜨려 주면 시작하면 함수가 호출되고 끝날때 함수가 호출됨

손이 붙었다 떨어질쯤에... 노티파이 넣어주면됨(무기 탈착이니깐 자연스럽게)

 

 

 

 

 

IRifle.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "IRifle.generated.h"

// UINTERFACE가 붙은 위의 클래스는 이 부분은 BP와 통신을 위해
// 자동으로 정의되어 있는 클래스이며 따로 수정하지 않습니다.

UINTERFACE(MinimalAPI)
class UIRifle : public UInterface
{
	GENERATED_BODY()
};

class UONLINE_03_CPP_API IIRifle
{
	GENERATED_BODY()

public:
	// 순수 가상함수로 상속 받은 클래스는 반드시 재정의해줘야 상속 받은 클래스는 순수가상함수가 되지 않는다.
	virtual class ACRifle* GetRifle() = 0;
};

 

 

 

 

CPlayer.h 추가된 내용(IIRifle을 상속 받음)


더보기
...

UCLASS()
class UONLINE_03_CPP_API ACPlayer : public ACharacter, public IIRifle
{
	GENERATED_BODY()
    
public:
	// 선언과 동시에니까 FORCEINLINE
	FORCEINLINE class ACRifle* GetRifle() override { return Rifle; }
    
private:
	class ACRifle* Rifle;
};

 

 

 

 

 

 

 

CPlayer.cpp


더보기
...

void ACPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	...
	PlayerInputComponent->BindAction("Rifle", EInputEvent::IE_Pressed, this, &ACPlayer::OnRifle); 
}

void ACPlayer::OnRifle()
{
	if (Rifle->GetEquipped())
	{
		Rifle->UnEquip();

		return;
	}

	Rifle->Equip();
}

 

 

 

CRifle.h


더보기
#pragma once

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

UCLASS()
class UONLINE_03_CPP_API ACRifle : public AActor
{
	GENERATED_BODY()

private:
	UPROPERTY(VisibleDefaultsOnly, Category = "Rifle")
		class USkeletalMeshComponent* Mesh;

	// 총기가 장착될 소켓 지정
	UPROPERTY(VisibleDefaultsOnly, Category = "Rifle")
		FName HandSocket = "Hand_Rifle";

	// 총기가 장착될 소켓 지정
	UPROPERTY(VisibleDefaultsOnly, Category = "Rifle")
		FName HolsterSocket = "Holster_Rifle";

	UPROPERTY(VisibleDefaultsOnly, Category = "Rifle")
		class UAnimMontage* GrabMontage;

	UPROPERTY(VisibleDefaultsOnly, Category = "Rifle")
		class UAnimMontage* UngrabMontage;

public:
	// 1. 스폰시킬 위치 2 : 소유할 캐릭터
	static ACRifle* Spawn(class UWorld* InWorld, class ACharacter* InOwner);

public:
	bool GetEquipped() { return bEquipped; };
	bool GetEquipping() { return bEquipping; };
	bool GetAiming() { return bAiming; };

public:	
	ACRifle();

	// 어떤 함수에서 몽타주를 호출해서 Begin과 End를 호출한다면 이름 관리를
	// 편하게 하기 위해 해당 함수의 이름앞에 Begin_, End_를 붙여 함수이름관리를
	// 용이하게 함

	// 장착명령
	void Equip();
	void Begin_Equip();
	void End_Equip();

	// 장착해제
	void UnEquip();
	void Begin_UnEquip();
	void End_UnEquip();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

private:
	// 적에도 붙을 수 있어서, 공통의 변수이름 짓기
	class ACharacter* OwnerCharacter;

	// 장착했는지
	bool bEquipped;

	// 장착 중인지
	bool bEquipping;
};

 

 

 

 

 

 

 

CRifle.cpp


더보기
#include "CRifle.h"
#include "Global.h"
#include "IRifle.h"
#include "Animation/AnimMontage.h"
#include "GameFramework/Character.h"
#include "Engine/StaticMeshActor.h" // 과녁들을 우리가 staticMesh로 배치해놨다.
#include "Components/SkeletalMeshComponent.h"

// 뭘 스폰이나 생성할때 내부적으로 생성해서 리턴시켜주는 Factory구조라고 함
// 총을 스폰 시키고 싶으면 이 함수를 사용해 총 객체를 리턴받으면 된다.
ACRifle* ACRifle::Spawn(UWorld* InWorld, ACharacter* InOwner)
{
	FActorSpawnParameters params;
	params.Owner = InOwner;

	// spawn 시키면서 ACRifle 형태로 Spawn됨
	return InWorld->SpawnActor<ACRifle>(params);
}

ACRifle::ACRifle()
{
 	PrimaryActorTick.bCanEverTick = true;
	// 이렇게 뒤에 parent 안넣으면 루트가 됨
	CHelpers::CreateComponent<USkeletalMeshComponent>(this, &Mesh, "Mesh");

	USkeletalMesh* mesh;
	// SkeletalMesh 불러옴
	CHelpers::GetAsset<USkeletalMesh>(&mesh, "SkeletalMesh'/Game/Weapons/Meshes/AR4/SK_AR4.SK_AR4'");
	Mesh->SetSkeletalMesh(mesh);

	CHelpers::GetAsset<UAnimMontage>(&GrabMontage, "AnimMontage'/Game/Character/Montages/Rifle_Grab_Montage.Rifle_Grab_Montage'");
	CHelpers::GetAsset<UAnimMontage>(&UngrabMontage, "AnimMontage'/Game/Character/Montages/Rifle_UnGrab_Montage.Rifle_UnGrab_Montage'");
}

void ACRifle::Equip()
{
	// 장착되어 있다면 하지마라
	CheckTrue(bEquipped);
	// 장착 중이라면 하지 마라
	CheckTrue(bEquipping);

	bEquipping = true;
	OwnerCharacter->PlayAnimMontage(GrabMontage);
}

void ACRifle::Begin_Equip()
{
	bEquipped = true;

	// Hand 소켓에 붙음
	AttachToComponent(OwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true), HandSocket);
}

void ACRifle::End_Equip()
{
	bEquipping = false;
}

void ACRifle::UnEquip()
{
	CheckFalse(bEquipped);
	CheckTrue(bEquipping);
	
	bEquipping = true;
	OwnerCharacter->PlayAnimMontage(UngrabMontage);
}

void ACRifle::Begin_UnEquip()
{
	bEquipped = false;

	// HolsterSocket으로 다시 위치 돌려놈
	AttachToComponent(OwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true), HolsterSocket);
}

void ACRifle::End_UnEquip()
{
	bEquipping = false;
}


void ACRifle::BeginPlay()
{
	Super::BeginPlay();
	
	OwnerCharacter = Cast<ACharacter>(GetOwner());

	// BP와 다르게 C에서는 Rule을 하나로 묶어서 정의함
	// 물론 C++에서도 BP처럼 하나씩 정의는 가능
	// BP와 같은 함수를 콜
	// weld simulated bodies : 시뮬레이티드 피직스 바디를 결합
	AttachToComponent(OwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true), HolsterSocket);
}

void ACRifle::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

 

 

 

 

CAnimInstance.h 추가된 내용


더보기
#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "CAnimInstance.generated.h"

/**
 * 
 */
UCLASS()
class UONLINE_03_CPP_API UCAnimInstance : public UAnimInstance
{
	GENERATED_BODY()

	...
	
protected:
	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Animation")
		bool bEquipped;
};

 

 

 

 

 

 

 

CAnimInstance.cpp 추가된 내용


더보기
...

void UCAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
	...
	IIRifle* rifle = Cast<IIRifle>(OwnerCharacter);
	if (!!rifle) // NotNull
	{
		bEquipped = rifle->GetRifle()->GetEquipped();
	}
}

 

 

CAnimNotifyState_Equip.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "CAnimNotifyState_Equip.generated.h"

UCLASS()
class UONLINE_03_CPP_API UCAnimNotifyState_Equip : public UAnimNotifyState
{
	GENERATED_BODY()


public:
	// BlueprintNativeEvent : 필요시 재정의해서 사용하는 키워드, 자식 클래스에서 재정의할 때는
	// Implementation을 붙여서 사용한다.
	FString GetNotifyName_Implementation() const;


public:
	virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration);
	virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation);
	
};

 

 

 

 

 

 

 

CAnimNotifyState_Equip.cpp


더보기
#include "CAnimNotifyState_Equip.h"
#include "Global.h"
#include "IRifle.h"
#include "CRifle.h"


FString UCAnimNotifyState_Equip::GetNotifyName_Implementation() const
{
	return "Equip";
}

void UCAnimNotifyState_Equip::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration)
{
	Super::NotifyBegin(MeshComp, Animation, TotalDuration);
	// 게임모드가 아니라면 MeshComp가 NULL일 수 있음
	CheckNull(MeshComp);

	// Character가 IIRifle로 casting됨
	IIRifle* rifle = Cast<IIRifle>(MeshComp->GetOwner());
	CheckNull(rifle);

	rifle->GetRifle()->Begin_Equip();
}

void UCAnimNotifyState_Equip::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
	Super::NotifyEnd(MeshComp, Animation);
	CheckNull(MeshComp);

	IIRifle* rifle = Cast<IIRifle>(MeshComp->GetOwner());
	CheckNull(rifle);

	rifle->GetRifle()->End_Equip();
}

 

 

 

CAnimNotifyState_UnEquip.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "CAnimNotifyState_UnEquip.generated.h"

UCLASS()
class UONLINE_03_CPP_API UCAnimNotifyState_UnEquip : public UAnimNotifyState
{
	GENERATED_BODY()

public:
	FString GetNotifyName_Implementation() const;


public:
	virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration);
	virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation);

	
};

 

 

 

 

 

 

 

CAnimNotifyState_UnEquip.cpp


더보기
#include "CAnimNotifyState_UnEquip.h"
#include "Global.h"
#include "IRifle.h"
#include "CRifle.h"


FString UCAnimNotifyState_UnEquip::GetNotifyName_Implementation() const
{
	return "UnEquip";
}

void UCAnimNotifyState_UnEquip::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration)
{
	Super::NotifyBegin(MeshComp, Animation, TotalDuration);
	// 게임모드가 아니라면 MeshComp가 NULL일 수 있음
	CheckNull(MeshComp);

	// Character가 IIRifle로 casting됨
	IIRifle* rifle = Cast<IIRifle>(MeshComp->GetOwner());
	CheckNull(rifle);

	rifle->GetRifle()->Begin_UnEquip();
}

void UCAnimNotifyState_UnEquip::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
	Super::NotifyEnd(MeshComp, Animation);
	CheckNull(MeshComp);

	IIRifle* rifle = Cast<IIRifle>(MeshComp->GetOwner());
	CheckNull(rifle);

	rifle->GetRifle()->End_UnEquip();
}

 

 

 

결과