안선생의 개발 블로그

[UE5 멀티플레이 게임 만들기] 6. 스킬 실행(RPC) 본문

언리얼/멀티플레이 게임 만들어보자

[UE5 멀티플레이 게임 만들기] 6. 스킬 실행(RPC)

안선생 2024. 1. 5. 12:40

이번시간에는 스킬을 만든 거를 게임에서 작동되게 할 거예요. 

 

스킬을 8개를 만들어왔는데요,

 

키보드를 누르면 스킬이 작동되게 해야돼요 

 

그러면 Input 바인딩을 해야돼여 키보드를 누르면 스킬이 나갈 수 있게요.

 

먼저 캐릭터 클래스에 8개의 액션을 추가해 줄게요.

 

	/** Q스킬 액션 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* QSkillAction;

	/** W스킬 액션 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* WSkillAction;

	/** E스킬 액션 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* ESkillAction;

	/** R스킬 액션 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* RSkillAction;

	/** A스킬 액션 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* ASkillAction;

	/** S스킬 액션 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* SSkillAction;

	/** D스킬 액션 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* DSkillAction;

	/** F스킬 액션 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* FSkillAction;

 

그리고 키보드를 누르면 동작할 함수를 만들어야 돼요 8개의 함수를 만들어줄게요.

	/** Skills*/
	void QSkill();
	void WSkill();
	void ESkill();
	void RSkill();
	void ASkill();
	void SSkill();
	void DSkill();
	void FSkill();

이렇게 8개의 에 Skill Action과 Callback함수를 만들어 주시고 cpp에 BindAction 해줄게요.

 

// Skill
EnhancedInputComponent->BindAction(QSkillAction, ETriggerEvent::Started, this, &AJHCharacter::QSkill);
EnhancedInputComponent->BindAction(WSkillAction, ETriggerEvent::Started, this, &AJHCharacter::WSkill);
EnhancedInputComponent->BindAction(ESkillAction, ETriggerEvent::Started, this, &AJHCharacter::ESkill);
EnhancedInputComponent->BindAction(RSkillAction, ETriggerEvent::Started, this, &AJHCharacter::RSkill);
EnhancedInputComponent->BindAction(ASkillAction, ETriggerEvent::Started, this, &AJHCharacter::ASkill);
EnhancedInputComponent->BindAction(SSkillAction, ETriggerEvent::Started, this, &AJHCharacter::SSkill);
EnhancedInputComponent->BindAction(DSkillAction, ETriggerEvent::Started, this, &AJHCharacter::DSkill);
EnhancedInputComponent->BindAction(FSkillAction, ETriggerEvent::Started, this, &AJHCharacter::FSkill);

SetupPlayerInputComponent함수에 추가해 줄게요. ETriggerEvent가 Started면 키보드 누를 시 함수가 호촐돼여

 

그럼 다음에 언리얼을 실행시키고 Input Action을 만들어줘야 돼요.

 

실행시키고 Input폴더에서 

입력 액션을 추가해 줄게요.

 

기본값 그대로 bool형으로 할게요.

 

같은 방법으로 8개의 액션을 만들었으니 

 

8개 만들어줄게요!

이렇게 만들어 주신다음에

 

 Mapping Context에 연결해 주셔야 돼요.

데이터 에셋이며 여기서 연결해야 키보드 바인딩이 돼요.

 

 

들어가셔서 +눌러줄게요

 

저희가 아까 만든 InputAction을 선택해 주시고 왼쪽에 화살표를 눌러 원하는 키설정 해줄게요!

 

똑같이 8개 추가하고 키설정해 주시면 돼요!

그런 다음에 캐릭터 클래스로 가서 Input Action을 설정해 줘야 돼요. 그래야 C++에서 알 수 있겠죠?

 

캐릭터 Bluerpint로 가서 

알맞은 에셋을 넣어줄게요!

 

그러면 키보드를 누르면 바인딩된 콜백함수가 실행될 거예요!

 

하지만 실행해 보시면 아무것도 안되는 걸 알 수 있어요

 

그 이유는 콜백함수에다 아무런 코드를 작성하지 않아서겠죠?

 

콜백함수를 작성해 줄게요!

 

콜백함수는 간단해요 저희가 만든 몽타주를 재생해 주면 돼요!

 

그전에 먼저 해야 될 게 있어요 스킬마다 스킬이름, 스킬 아이콘, 스킬 설명, 스킬 키보드, 등 스킬만에 정보가 있겠죠. 

 

먼저 스킬 클래스를 만들어줄게요.

 

Actor를 상속받고 이름을 정해주세요 스킬 부모인 추상화 클래스로 만들 생각이에요!

 

만들어 주시고

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SkillIInfoEnum.h"
#include "Skills.generated.h"

class ACharacter;

/**
 * 
 */
UCLASS()
class JH_MULTI_RPG_API ASkills : public AActor
{
	GENERATED_BODY()
private:
	friend class USkillComponent;
public:
	ASkills();
	/** 스킬 실행*/
	void SkillExecute(ACharacter* Character);
protected:
	/** 스킬 이름*/
	UPROPERTY(EditDefaultsOnly)
	ESkillName SkillName;

	/** 스킬 Input*/
	UPROPERTY(EditDefaultsOnly)
	ESkillInput SkillInput;

	/** 스킬 실행 몽타추*/
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TObjectPtr<UAnimMontage> SkillMontage;

};

스킬이 가지고 있어야 함수와 변수를 작성해 줄게요!

 

SKillInput은 나중에 스킬을 원하는 키보드로 바꾸고 싶을 때 사용되는 변수예요!

 

아 그리고 먼저 Enum클래스도 만들어줘야 돼요 그래야 나중에 스킬 찾을 때 유용해요!

 

Enum클래스 추가방법은 언리얼에서 하는 게 아닌 VS에서 해야 돼요 하는 방법은

 

Skill 폴더 우클릭

우클릭 후 추가 -> 새 항목

 

헤더파일로 설정해 주시고

이름과 위치를 설정해 줘야 돼요

 

위치는 여러분이 놓고 싶은 위치가 아니라 소스 폴더가 있는 위치로 넣어주셔야 됩니다.

 

추가해 줄게요!

 

추가한 헤더파일에 작성해 줄게요.

#pragma once

UENUM(BlueprintType)
enum class ESkillName : uint8
{
	ESN_Default UMETA(DisplayName = "Default"),
	ESN_Skill1 UMETA(DisplayName = "Skill1"),
	ESN_Skill2 UMETA(DisplayName = "Skill2"),
	ESN_Skill3 UMETA(DisplayName = "Skill3"),
	ESN_Skill4 UMETA(DisplayName = "Skill4"),
	ESN_Skill5 UMETA(DisplayName = "Skill5"),
	ESN_Skill6 UMETA(DisplayName = "Skill6"),
	ESN_Skill7 UMETA(DisplayName = "Skill7"),
	ESN_Skill8 UMETA(DisplayName = "Skill8"),
};


UENUM(BlueprintType)
enum class ESkillInput : uint8
{
	ESI_Default UMETA(DisplayName = "Default"),
	ESI_InputQ UMETA(DisplayName = "InputQ"),
	ESI_InputW UMETA(DisplayName = "InputW"),
	ESI_InputE UMETA(DisplayName = "InputE"),
	ESI_InputR UMETA(DisplayName = "InputR"),
	ESI_InputA UMETA(DisplayName = "InputA"),
	ESI_InputS UMETA(DisplayName = "InputS"),
	ESI_InputD UMETA(DisplayName = "InputD"),
	ESI_InputF UMETA(DisplayName = "InputF")
};

이와 같이 스킬 이름과 스킬 입력 키보드를 열거형으로 해줬어요.

 

만약 스킬 이름을 하나하나 String으로 했다고 해볼게요. 

 

그럼 나중에 스킬을 찾고 싶을 때 이름으로 찾겠죠? 

 

그러면 String으로 그 값을 오타 없이 넣어줘야 되고 하드코딩 될 가능성이 커요.

 

그러면 유지보수 및 가독성이 안 좋아질 가능성이 매우 커요, 그러므로 열거형으로 하는 게 여러 면에서 좋은 이점을 갖고 있어요.

 

 

 

자 그럼 이제 스킬을 8개를 만들었으니 

 

스킬 8개 만들어줄게요 저희가 아까 만든 스킬부모 클래스 자식으로 8개 만들어줄게요!

 

파생 클래스 C++클래스 생성을 누른 후 

 

 8개 Skill 클래스를 만들어줄게요!

그러면 이렇게 자식클래스가 있을 거예요!

 

그런 다음에 만들 C++클래스를 블루프린트로 만들어줄게요.

 

블루클래스 만들기 누르신 다음에 8개의 스킬을 블루프린트로 만들어야 해요 그래야 스킬 클래스에 있던 변수를 설정할 수 있어요.

 

이렇게 8개의 블루프린트클래스를 만든 후 저희가 만든 변수를 설정해 줄게요!

 

저희가 만든 변수가 있을 텐데 원하는 이름과 원하는 키보드를 선택해 주시고 몽타주도 맞는 스킬 몽타주도 설정해 줄게요!

 

8개 클래스 다 반복입니다! 이름 몽타주 Input다 달라야겠죠..

 

자 그럼 이제 캐릭터는 이 스킬을 실행해야겠죠

 

자 여기서 캐릭터 클래스에서 스킬을 관리한다면 너무 캐릭터 클래스가 커지고 관리하기가 어려워질거같아요 그래서 스킬을 관리하는 Component를 만들어줄게요! 그러면 캐릭터 클래스는 SKillComponent를 의존해서 함수를 사용하면 되겠죠?

 

스킬을 관리할 Component를 만들어줄게요

 

액터 컴포넌트를 만들어줄게요! 여기서 씬 컴포넌트와 차이는 씬 컴포넌트는 Transform을 갖고 있어서 Transform이 필요할 때는 씬 컴포넌트가 낫겠죠 하지만 저희는 스킬만 관리할 거 기 때문에 Transform이 필요 없어요 액터컴포넌트로 만들어줄게요

 

이름을 정하고 만들어줄게요.

 

만들어주시고

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Skill/SkillIInfoEnum.h"
#include "SkillComponent.generated.h"

class USkills;

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class JH_MULTI_RPG_API USkillComponent : public UActorComponent
{
	GENERATED_BODY()
private:
	friend class AJHCharacter;
public:	
	USkillComponent();
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
protected:
	virtual void BeginPlay() override;

	UFUNCTION(Server,Reliable)
	void ServerSkill(ACharacter* Character,const ESkillInput& SkillInput);

	UFUNCTION(NetMulticast,Reliable)
	void MultiSkill(ACharacter* Character, const ESkillInput& SkillInput);

private:
	UPROPERTY()
	TArray<TObjectPtr<USkills>> ActivatableSkills;

	UPROPERTY(EditDefaultsOnly)
	TArray<TSubclassOf<USkills>> StartSkillsClass;
};

이렇게 작성할게요

 

하나하나 설명할게요

friend class는 말 그대로 친구예요 AJHCharacter랑 친구야 이 뜻은 AJHCharacter는 내 private도 써도 돼 그 뜻이에요 

저희는 AJHCharacter클래스가 커질까 봐 Component를 만들었죠 즉 AJHCharacter클래스는 Component를 알고 있어야 하기 때문에 friend로 설정해줬어요.

 

그다음은 RPC인데 중요해요 먼저 RPC는 https://iiii4.tistory.com/153  여기서 확인 할 수 있어요!  

RPC개념을 정확히 알 고 있어야 할 수 있어요!!

 

Server는 서버에서 실행하겠다는 거고 

 

Multicast는 서버 및 클라이언트에서 실행하겠다는 거예요 즉 동기화해 줍니다!

 

그다음 변수는 활성화된 스킬 변수와, 시작할 때 갖고 있을 스킬 클래스입니다!

 

cpp코드를 볼게요

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "Skill/SkillComponent.h"
#include "Skill/Skills.h"
#include "Skill/SkillInfo.h"

USkillComponent::USkillComponent()
{
	PrimaryComponentTick.bCanEverTick = false;
}

const TArray<ESkillName>& USkillComponent::GetActivatableSkillNames() const
{
	return ActivatableSkillNames;
}

void USkillComponent::BeginPlay()
{
	Super::BeginPlay();
	
	/* 서버만 스킬을 가질 수 있습니다.**/
	if (GetOwner() && GetOwner()->HasAuthority())
	{
    	/* 캐릭터는 스타트스킬 갯수만큼 인스턴스를 만들어줌**/
		for (int32 i = 0; i < StartSkillsClass.Num(); i++)
		{
			ActivatableSkills.Add(GetWorld()->SpawnActor<ASkills>(StartSkillsClass[i]));
			ActivatableSkills[i]->SetOwner(GetOwner());
			ActivatableSkillNames.Add(ActivatableSkills[i]->SkillName);
		}
	}
}

void USkillComponent::ServerSkill_Implementation(ACharacter* Character, const ESkillInput& SkillInput)
{
	for (ASkills* Skill : ActivatableSkills)
	{
		if (Skill->SkillInput == SkillInput)
		{
			MultiSkill(Character, SkillInput, Skill);
		}
	}
}

void USkillComponent::MultiSkill_Implementation(ACharacter* Character, const ESkillInput& SkillInput, ASkills* Skill)
{
	if (Skill)
	{
		Skill->SkillExecute(Character);
	}

}

void USkillComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

}

 

BeginPlay부터 보시면 HasAuthority()는 서버에서만 실행가능합니다.

for문은 스타트 스킬 8개를 인스턴스로 스폰해서 배열에 넣어줄거에요!  배열에 넣어야 나중에 접근할 수 있어요.

Owner는 캐릭터로 해줍니다.

그다음 나중에 스킬 이름 찾기 편하게 배열을 만들어서 ActivatableSkillNames에 넣어줍니다. GetActivatableSkillNames() 함수에서 나중에 이름으로 스킬 찾을 수 있음

 

그다음 함수 보기 전에 캐릭터 클래스부터 가볼게요! RPC함수는 캐릭터 설명하고 할게요

 

Character.cpp

void AJHCharacter::QSkill()
{
	SkillComponent->ServerSkill(this,ESkillInput::ESI_InputQ);
}

void AJHCharacter::WSkill()
{
	SkillComponent->ServerSkill(this, ESkillInput::ESI_InputW);
}

void AJHCharacter::ESkill()
{
	SkillComponent->ServerSkill(this, ESkillInput::ESI_InputE);
}

void AJHCharacter::RSkill()
{
	SkillComponent->ServerSkill(this, ESkillInput::ESI_InputR);
}

void AJHCharacter::ASkill()
{
	SkillComponent->ServerSkill(this, ESkillInput::ESI_InputA);
}

void AJHCharacter::SSkill()
{
	SkillComponent->ServerSkill(this, ESkillInput::ESI_InputS);
}

void AJHCharacter::DSkill()
{
	SkillComponent->ServerSkill(this, ESkillInput::ESI_InputD);
}

void AJHCharacter::FSkill()
{
	SkillComponent->ServerSkill(this, ESkillInput::ESI_InputF);
}

 

스킬 콜백함수입니다. 인자로 캐릭터와, SkillInput을 전달합니다. SkillInput으로 나중에 스킬 Input이랑 비교해서 스킬 Input을 바꿔 원하는 키로 지정할 수 있게 설계했어요. 스킬은 원하는 위치에 넣고 싶을 수 있잖아요!

 

 

 

 

다시 Component로 가볼게요 캐릭터에서 Server_RPC를 호출하는 걸 봤는데

 

그 이유는 멀티플레이 게임 로직은 서버에서 실행하고 클라이언트로 전달해 동기화해주기 때문이에요. 

 

만약 클라이언트에서 실행하면 해킹 위험성도 있고, 각각 클라이언트마다 속도가 달라 공정성에 어긋나기 때문에 서버에서 동기화를 해줘야 돼요.(서버에서 실행 안 하면 어차피 로컬컴퓨터에서만 실행됨)

 

위에 SKillComponet입니다.
void USkillComponent::ServerSkill_Implementation(ACharacter* Character, const ESkillInput& SkillInput)
{
	for (ASkills* Skill : ActivatableSkills)
	{
		if (Skill->SkillInput == SkillInput)
		{
			MultiSkill(Character, SkillInput, Skill);
		}
	}
}

void USkillComponent::MultiSkill_Implementation(ACharacter* Character, const ESkillInput& SkillInput, ASkills* Skill)
{
	if (Skill)
	{
		Skill->SkillExecute(Character);
	}

}

위에 캐릭터에서 매개변수를 전달해 줬죠?

 

먼저 서버에서 해야 하는 일은 활성화된 스킬들 중에서 스킬에 있는 Input과 매개변수로 전달받은 Input을 비교해서 맞으면 MultiSkill_RPC를 호출해 줄 거예요. 서버에서 중요 로직을 실행하고 서버는 클라이언트에 알려줘야 됨

 

 

그다음 MultiSkill_RPC에서는 공통적으로 스킬에 있는 함수를 실행시켜 줍니다.  그럼 모든 클라이언트가 SkillExecute를 실행할 거예요.

 

저희가 만들었던 Skills클래스가 있었죠 거기에 SkillExecute함수가 있었는데 아래와같이 정의할게요.

//Skills.cpp
#include "Skill/Skills.h"
#include "GameFramework/Character.h"
#include "Animation/AnimMontage.h"

ASkills::ASkills()
{
	bReplicates = true;
}

void ASkills::SkillExecute(ACharacter* Character)
{
	
	UAnimInstance* AnimInstance = Character->GetMesh()->GetAnimInstance();
	AnimInstance->Montage_Play(SkillMontage);
}

SkillExecute는 간단한 스킬몽타주만 재생하는 함수예요 스킬마다 몽타주가 다르니 스킬마다 몽타주를 설정해 줘야겠죠.

 

아 참고로 bReplicates = true를 해줘야돼요 안하면 서버에서 Skills 값이 바뀌면 클라인트가 모르기 때문에 꼭 해줘야 동기화가 가능힙니다.! 

 

 

여기까지 코드작성은 끝났고 테스트해볼게요

 

언리얼 에디터에서 리슨서버로 바꿔줄게요

그런 다음에 실행해 보시면

 

맨 왼쪽이 서버예요 보시면 서버 클라이언트에서 동기화돼서 실행되는 걸 볼 수 있어요!

 

아래 HUD는 다음시간에 해볼게요!

 

 

 

혼자 공부하면서 하는 거라 틀릴 수 있음!