* 주의 *
1. 해당 작업은 StandAlone에서만 테스트되었습니다.
2. 해당 작업은 임시 연동을 위해 작업되었으며, 타 환경 (네트워크 환경 등)에서는 정상 작동을 보장하지 않습니다.
3. 필요시, 코드를 적당히 수정해서 사용해주세요!
4. 해당 작업은 Unreal 5.3에서 수행되었습니다. 타 버전에서는 방법이 다를 수 있습니다.
이전에 썼던 AbilitySystem 관련 글이 있다.
https://locketgoma.tistory.com/80
짧막 팁 : AbilitySystem 관련 헤더를 불러오지 못할때
대충 이런 상황이다. AbilitySystemComponent 및 기타 관련 헤더를 사용해야하는데 못불러오는경우... 해당 문제는 모듈이 빠져있는 상황으로, 프로젝트 명칭으로 된 폴더에 있는 "(ProjectName).Build.cs"
locketgoma.tistory.com
https://locketgoma.tistory.com/81
(UnrealEngine 5.3 or Higher) EnhacedInput과 AbilitySystem 연동
* 주의 *1. 해당 작업은 StandAlone에서만 테스트되었습니다.2. 해당 작업은 임시 연동을 위해 작업되었으며, 타 환경 (네트워크 환경 등)에서는 정상 작동을 보장하지 않습니다.3. 필요시, 코드를 적
locketgoma.tistory.com
요건데...
사실 AbilitySystem에는 따라오는 항목들이 몇개 존재한다.
GameplayTag
GameplayAbility
GameplayEffect
그리고 이번에 설명할 GameplayCue...
그 외에도 몇개 있긴한데 주로 사용되는 신규 에셋(?) 들은 저 4개정도에서 정리된다.
나머지는 그냥 AbilitySystem Plugin을 추가하면 그만인데, GameplayCue는 선행작업이 좀 필요했다.
해당 내용을 검색을 해봐도, AI에게 물어봐도 시원하게 답을 주지 않았기에 직접 찾아보고 구현한 방법을 공유하고자 한다.
그래서 문제가 무엇인가
일단, 문제가 무엇인지부터 알아보자.
GameplayCue를 호출하기 위해서는 AbilitySystem이 추가될때 같이 추가되는 GameplayCueManager (이하 GCM이라 부름) 에 사용하고자 하는 GameplayCue들을 등록해주어야 한다.
이를 위해서는, AbilitySystemGlobal에 있는 GCM의 AddGameplayCueNotifyPath 라는 함수를 호출[1] 해주어야 하는데...
[1] (정확히는, 해당 함수를 호출해서, 경로에 있는 GameplayCue를 등록하게 해야한다)
...여기서 해당 함수를 호출하는 부분이 보이는가?
나는 아무리 눈을 씻고 찾아봐도 안보이더라
해서, 진짜 없나? 해서 뒤적대보니까 없는게 맞았다.
AbilitySystem 관련 샘플인 Lyra를 따라가자면, 해당 함수를 호출하는 Feature를 GameFeatureAction 항목에 추가하고, 경로를 지정해서 호출하게 해주어야 한다.
예시 확인
그럼 Lyra 프로젝트를 뜯어보는것으로 시작하자.
지금 진행중인 프로젝트의 버전과 동일한 엔진 버전을 가진 Lyra 프로젝트를 열고, 실행을 해보았다.
Lyra 프로젝트에 보면, 모든 게임피쳐 플러그인마다 해당 파일이 존재한다.
이 친구가 오늘의 핵심 되시겠다.
해당 파일을 열면, "액션" 탭에 여러 기능들이 포함되어있다.
여기서 이번에 추가할건 Add Gameplay Cue Path 라는 기능.
당연하게도 AbilitySystem만 세팅한 경우에는 저 항목이 존재하질 않는다.
해당 GameFeatureAction이 뭘 하는지, 어떻게 작동시키는지 살펴보자.
우선, 해당 기능은 2개의 클래스로 나뉜다.
1. GameFeatureAction으로 등록되는 클래스
2. GameFeatureAction에 의해 실제로 작동되는 클래스
왜 2개인지 까지는 이해를 못하겠는데, 일단 두개라고 하니까 둘다 구현해줘야한다.
"그냥 하나로 합치면 안되나요?"
라는 의문이 들 수 있는데, 시도해봤는데 안되더라.
문제점
이유는 다음과 같다.
우선, Lyra 프로젝트에서 ULyraGameFeature_AddGameplayCuePaths 라는 클래스를 보자.
여기서 중요한건 "OnGameFeatureRegistering" 라는 함수.
해당 함수가 GameFeatureSubsystem 에 의해 호출되어야 해당 GameFeatureAction이 작동하는 구조인데... 호출되어야 하는 함수의 파라미터가 예상되는 파라미터와 전혀 다르다.
위의 잘린 이미지만 보면 알 수 있듯이, 파라미터가 좀 긴데
서브시스템 코드를 보면, 저 길다란 파라미터를 가진 함수를 호출하는 함수 호출부가 존재하지 않는다.
그 말은 즉슨, 다른 방식으로 호출해야한다는 뜻.
해당 방식은 먼곳 갈 필요없이, 바로 위에 있는 CallbackObservers에서 호출된다.
Observer에 등록해두면, 해당 파라미터타입을 가진 클래스가 호출된다는것.
...뭔가 이상하지 않은가?
1. OnGameFeatureRegistering 라는 함수가 호출되어야 한다는건 알겠고
2. OnGameFeatureRegistering가 Observer에 의해 호출되어야 한다는건 알겠는데
3. 그럼 GameFeatureAction은 어떻게 만드는데?
놀랍게도 GameFeatureAction 클래스는 따로 만들어줘야한다.
구현하기
이제 차근차근 구현해보도록 하자.
1. 타 GameFeatureAction이나, Lyra 프로젝트를 참고해서 "AddGameplayCuePath"를 수행할 수 있는 GameFeatureAction 클래스를 생성한다.
UCLASS(MinimalAPI, meta = (DisplayName = "Add Gameplay Cue Path"))
class UGameFeatureAction_AddGameplayCuePath final : public UGameFeatureAction
{
GENERATED_BODY()
public:
UGameFeatureAction_AddGameplayCuePath();
//~UObject interface
#if WITH_EDITOR
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
//~End of UObject interface
const TArray<FDirectoryPath>& GetDirectoryPathsToAdd() const { return DirectoryPathsToAdd; }
private:
/** List of paths to register to the gameplay cue manager. These are relative tot he game content directory */
UPROPERTY(EditAnywhere, Category = "Game Feature | Gameplay Cues", meta = (RelativeToGameContentDir, LongPackageName))
TArray<FDirectoryPath> DirectoryPathsToAdd;
};
해당 클래스는 "껍데기" 에 해당하는 클래스라서, 구현도 크게 고민 안해도 된다.
그냥 참고한 프로젝트들이나 코드를 보면서 작성하면 되는데, 추가로 설명해주자면..
#include "GameFeatureAction_AddGameplayCuePath.h"
#if WITH_EDITOR
#include "Misc/DataValidation.h"
#endif
#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_AddGameplayCuePath)
#define LOCTEXT_NAMESPACE "GameFeatures"
UGameFeatureAction_AddGameplayCuePath::UGameFeatureAction_AddGameplayCuePath()
{
// Add a default path that is commonly used
DirectoryPathsToAdd.Add(FDirectoryPath{ TEXT("/GameplayCues") });
}
#if WITH_EDITOR
EDataValidationResult UGameFeatureAction_AddGameplayCuePath::IsDataValid(FDataValidationContext& Context) const
{
EDataValidationResult Result = Super::IsDataValid(Context);
for (const FDirectoryPath& Directory : DirectoryPathsToAdd)
{
if (Directory.Path.IsEmpty())
{
const FText InvalidCuePathError = FText::Format(LOCTEXT("InvalidCuePathError", "'{0}' is not a valid path!"), FText::FromString(Directory.Path));
Context.AddError(InvalidCuePathError);
Result = CombineDataValidationResults(Result, EDataValidationResult::Invalid);
}
}
return CombineDataValidationResults(Result, EDataValidationResult::Valid);
}
#endif // WITH_EDITOR
#undef LOCTEXT_NAMESPACE
그냥 이게 끝이다.
별거 없다. 어차피 DataValid는 Editor 전용 코드기 때문에 가려질것이고, 기본 Path만 추가해주면 끝.
문제는 지금부터다.
2. 실제로 기능하는 GameFeatureAction을 만들어준다.
여기서 참고해야하는 클래스는 Lyra 프로젝트의 ULyraGameFeature_AddGameplayCuePaths 라는 클래스다.
근데 해당 클래스를 보면, UObject와 IGameFeatureStateChangeObserver 라는 인터페이스를 상속받는 형태의 클래스로 되어있다.
우선, 따라서 구현한다.
UCLASS()
class UCLGameFeature_AddGameplayCuePaths : public UObject, public IGameFeatureStateChangeObserver
{
GENERATED_BODY()
public:
UCLGameFeature_AddGameplayCuePaths();
public:
virtual void OnGameFeatureRegistering(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FString& PluginURL) override;
virtual void OnGameFeatureUnregistering(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FString& PluginURL) override;
};
여기엔 OnGameFeatureRegistering, OnGameFeatureUnregistering만 호출하면 된다.
그리고, 두 함수의 내용을 채워주면 되는데, Lyra 프로젝트에 구현된 부분을 보자.
void ULyraGameFeature_AddGameplayCuePaths::OnGameFeatureRegistering(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FString& PluginURL)
{
TRACE_CPUPROFILER_EVENT_SCOPE(ULyraGameFeature_AddGameplayCuePaths::OnGameFeatureRegistering);
const FString PluginRootPath = TEXT("/") + PluginName;
for (const UGameFeatureAction* Action : GameFeatureData->GetActions())
{
if (const UGameFeatureAction_AddGameplayCuePath* AddGameplayCueGFA = Cast<UGameFeatureAction_AddGameplayCuePath>(Action))
{
const TArray<FDirectoryPath>& DirsToAdd = AddGameplayCueGFA->GetDirectoryPathsToAdd();
if (ULyraGameplayCueManager* GCM = ULyraGameplayCueManager::Get())
{
UGameplayCueSet* RuntimeGameplayCueSet = GCM->GetRuntimeCueSet();
const int32 PreInitializeNumCues = RuntimeGameplayCueSet ? RuntimeGameplayCueSet->GameplayCueData.Num() : 0;
for (const FDirectoryPath& Directory : DirsToAdd)
{
FString MutablePath = Directory.Path;
UGameFeaturesSubsystem::FixPluginPackagePath(MutablePath, PluginRootPath, false);
GCM->AddGameplayCueNotifyPath(MutablePath, /** bShouldRescanCueAssets = */ false);
}
// Rebuild the runtime library with these new paths
if (!DirsToAdd.IsEmpty())
{
GCM->InitializeRuntimeObjectLibrary();
}
const int32 PostInitializeNumCues = RuntimeGameplayCueSet ? RuntimeGameplayCueSet->GameplayCueData.Num() : 0;
if (PreInitializeNumCues != PostInitializeNumCues)
{
GCM->RefreshGameplayCuePrimaryAsset();
}
}
}
}
}
우선 코드 설명.
1. 현재 Action에 등록된 GameFeatureAction 중, UGameFeatureAction_AddGameplayCuePath 클래스가 있는지 찾는다.
2. 해당 클래스가 있다면, 해당 클래스 액터에서 파일을 추가할 디렉토리 정보를 가져온다.
(먼저 작성한 클래스에 있는 DirectoryPathsToAdd 값을 가져오게 된다)
3. 다음, GCM을 호출하여, 디렉토리에 있는 GameplayCue들을 GCM에 등록한다.
4. 마지막으로, Init 및 Refresh 과정을 수행한다.
...라는 그렇게 복잡하진 않은 구조이다.
호출부를 직접 다 구현할 필요는 없고, 일부 변경되는 함수들이 있어서 그것만 잘 확인해서 호출해주면 되겠다.
//바뀌는것들
//Lyra -> Native UE
//Lyra
UGameplayCueSet* RuntimeGameplayCueSet = GCM->GetRuntimeCueSet();
const int32 PostInitializeNumCues = RuntimeGameplayCueSet ? RuntimeGameplayCueSet->GameplayCueData.Num() : 0;
if (PreInitializeNumCues != PostInitializeNumCues)
{
GCM->RefreshGameplayCuePrimaryAsset();
}
//Native Ue
UGameplayCueManager* GCM = UAbilitySystemGlobals::Get().GetGameplayCueManager();
//해당 함수는 기본 GameplayCueManager에 구현이 안되어있다.
//필요하면 추가해주어야 할것이다.
//const int32 PostInitializeNumCues = RuntimeGameplayCueSet ? RuntimeGameplayCueSet->GameplayCueData.Num() : 0;
//if (PreInitializeNumCues != PostInitializeNumCues)
//{
// GCM->RefreshGameplayCuePrimaryAsset();
//}
자 이렇게 하면 짜잔! 하고 호출이....... 되면 좋겠지만!
당연히 안된다.
3. 신규 GameFeaturePolicy를 추가해준다.
이게 무슨소리인가? 싶은데 지금 우리는 "필요한 파라미터를 사용하는 함수가 존재하지 않아서, 해당 함수를 호출할 수 있도록 클래스를 2개로 나눠서 작업" 한 상태인다.
그렇다는것은, 두개의 클래스가 모두 정상적으로 호출될 수 있도록 해야 한다는 뜻.
혹시, 위에 있던 "Observer" 라는 존재가 기억나는가? 이제 Observer 를 추가하고, 등록해서 내가 원하는 GameFeatureAction이 등록될 수 있도록 해야한다.
방금까지 보고 있던 Lyra 프로젝트 파일의 윗부분을 보면, 해당 클래스가 있다.
UCLASS(MinimalAPI, Config = Game)
class ULyraGameFeaturePolicy : public UDefaultGameFeaturesProjectPolicies
{
GENERATED_BODY()
public:
LYRAGAME_API static ULyraGameFeaturePolicy& Get();
ULyraGameFeaturePolicy(const FObjectInitializer& ObjectInitializer);
//~UGameFeaturesProjectPolicies interface
virtual void InitGameFeatureManager() override;
virtual void ShutdownGameFeatureManager() override;
virtual TArray<FPrimaryAssetId> GetPreloadAssetListForGameFeature(const UGameFeatureData* GameFeatureToLoad, bool bIncludeLoadedAssets = false) const override;
virtual bool IsPluginAllowed(const FString& PluginURL) const override;
virtual const TArray<FName> GetPreloadBundleStateForGameFeature() const override;
virtual void GetGameFeatureLoadingMode(bool& bLoadClientData, bool& bLoadServerData) const override;
//~End of UGameFeaturesProjectPolicies interface
private:
UPROPERTY(Transient)
TArray<TObjectPtr<UObject>> Observers;
};
이 클래스가 바로 3번째로 작성해야하는 클래스다.
우선, 각 클래스 함수의 내용이 수정될건은 없기에, 해당 클래스의 함수 구현부에 가서 내용을 전부 채워주면 된다.
이정도는 할수있을거라 믿음...
void ULyraGameFeaturePolicy::InitGameFeatureManager()
{
Observers.Add(NewObject<UCLGameFeature_AddGameplayCuePaths>());
UGameFeaturesSubsystem& Subsystem = UGameFeaturesSubsystem::Get();
for (UObject* Observer : Observers)
{
Subsystem.AddObserver(Observer);
}
Super::InitGameFeatureManager();
}
굳이 함수 설명을 하자면, 이 상황을 위한 함수인 Init 함수를 확인해보자.
GameFeatureSubsystem 싱글톤 오브젝트를 가져와서, 작업한 Observer를 SubsystemObserver에 추가한다.
이렇게 되면, 내가 추가한 클래스들이 Observer에 들어가면서, OnGameFeatureRegistering가 호출될때 Observer에 등록된 클래스도 같이 호출되는... 구조로 보인다.
등록 및 확인하기
사실 이정도까지 했으면 바로 짜잔! 하고 되면 좋겠지만, 한가지 과정이 남아있다.
우리는 지금 새로운 UDefaultGameFeaturesProjectPolicies 클래스를 만든 상황이다.
그렇다면, 해당 신규 Policy Class를 쓰도록 등록해줘야 하는데
이친구, 생각보다 숨겨져있다.
Edit->ProjectSetting에 가서 "GameFeature" 라고 검색을 해보자.
그럼 해당 위치에, Game Feature Policy Class를 바꿀 수 있게 되어있고, 해당 박스를 열었을때 내가 만든 클래스가 있으면 된다.
없으면? 뭔가 실수한거니까 잘 찾아보기.
잘 추가가 되었다면, 이제 GameFeatureData에 가서 추가한 Action이 있는지도 봐준다.
GameplayCue Path 라는 이름의 FeatrueAction이 추가되어있으면 끝.
이제 경로를 지정하고, 해당 경로에 GameplayCue를 몰아넣고 호출하면 된다.
캐릭터가 맞았을때 GameplayCue를 호출하게 해보았다.
잘되는것을 확인할 수 있다.
문제 확인부터 해결까지 생각보다 시간을 많이써서, 헷깔리지 않게 + 다른사람들은 도움받을수 있도록 글 작성해본다.
혹시 잘못된것이 있다면 알려주세요.
'게임 개발 > Unreal' 카테고리의 다른 글
Unreal Workflow (Pipeline) (0) | 2024.10.10 |
---|---|
짐벌락과 기타등등 (0) | 2024.09.12 |
지금까지 받은 언리얼 관련 면접자료 (v24.09.01) (0) | 2024.09.01 |
(UnrealEngine 5.3 or Higher) EnhacedInput과 AbilitySystem 연동 (0) | 2024.08.16 |
짧막 팁 : AbilitySystem 관련 헤더를 불러오지 못할때 (0) | 2024.08.12 |