어제 캐릭터 파일이 올라와서 테스트를 해보니 아이템에 충돌 문제가 있었어서 해결하는 과정이 있었다.
//CRItemBase.cpp
...
ACRItemBase::ACRItemBase()
{
...
Collision->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
Collision->SetGenerateOverlapEvents(true);
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
Mesh->SetupAttachment(Collision);
Mesh->SetCollisionProfileName(TEXT("NoCollision"));
...
}
void ACRItemBase::BeginPlay()
{
Super::BeginPlay();
if (!Collision) return;
Collision->OnComponentBeginOverlap.AddDynamic(this, &ACRItemBase::OnOverlapBegin);
}
void ACRItemBase::OnOverlapBegin(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult)
{
...
if (APawn* Pawn = Cast<APawn>(OtherActor))
{
bPicked = true;
OnRep_Picked();
Activate(OtherActor);
Destroy();
}
}
...
우선 생성자에서의 콜리전 설정들을 몇 개 바꾸어주었고, OnOverlapBegin() 함수에서 태그대신 폰을 캐스팅해서 체크하는 방식으로 바꾸어주었다.

또한 블루프린트 쪽에서 문제가 있었던 것 같아서 아이템 BP 로 들어가 File > Reparent Bluprint 버튼을 눌러 Actor 로 바꿨다가 다시 기존 아이템 클래스로 변경해주었더니 문제가 해결되었다.
잘 몰랐어서 그냥 캐릭터에게 효과를 적용하려했는데, GAS 시스템을 알게되어서 활용해보려고 한다.
앞으로 웬만한 아이템은 GAS 시스템을 이용해서 플레이어에게 아이템의 효과를 적용시킬 예정이다.
//CRExtraLifeItem.h
class COPANDROBBER_API ACRExtraLifeItem : public ACRItemBase
{
...
public:
...
UPROPERTY(EditDefaultsOnly, Category="GAS|Effects")
TSubclassOf<class UGameplayEffect> GE_ExtraLife;
...
};
먼저 ExtraLifeItem 의 헤더 파일이다.
TSubclassOf 로 게임 이펙트 클래스 GE_ExtraLife 를 생성해준다.
//CRExtraLifeItem.cpp
...
#include "AbilitySystemGlobals.h"
#include "AbilitySystemComponent.h"
#include "GameplayEffect.h"
#include "GameplayTagContainer.h"
void ACRExtraLifeItem::Activate(AActor* Player)
{
if (!HasAuthority() || !Player || !GE_ExtraLife) return;
if (UAbilitySystemComponent* ASC = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Player))
{
FGameplayEffectContextHandle Ctx = ASC->MakeEffectContext();
Ctx.AddSourceObject(this);
FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(GE_ExtraLife, 1.f, Ctx);
if (Spec.IsValid())
{
const FGameplayTag TagAmount = FGameplayTag::RequestGameplayTag(TEXT("Data.LifeAmount"));
Spec.Data->SetSetByCallerMagnitude(TagAmount, (float)LifeAmount);
ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get());
}
}
...
}
먼저 GAS 시스템 관련 헤더 파일들을 추가하고, 클라이언트이고 플레이어가 존재하는지와 GE 가 존재하는지를 모두 체크하고 GAS 시스템 코드를 작성해주면 된다.


에디터에서 새로운 BP 를 생성하고 부모 클래스는 GameplayEffect 로 해서 하나 생성해준다.

BP 에 들어가고 Details 패널에서 Duration > Duration Policy 를 Instant 로 해준다.
그리고 Gameplay Effect > Modifiers 에서 + 버튼을 눌러 추가해주고 Index[0] 을 열어 Attribute 는 CRAttributeSet.Health 로,
Modifier OP 는 Add 로,
Modifier Magnitude 에서 Magnitude Calculation Type 은 Scalable Float,
Scalable Float Magnitude 값을 1.0 으로 세팅해주었다.

이후 BP_CRExtraLifeItem 으로 들어가 Details 패널에서 GAS > Effects > GE Extra Life 를 방금 만든 GE 로 설정해준다.
// CRStealthItem.h
#pragma once
#include "CoreMinimal.h"
#include "Gimmick/Item/Base/CRItemBase.h"
#include "CRStealthItem.generated.h"
UCLASS()
class COPANDROBBER_API ACRStealthItem : public ACRItemBase
{
GENERATED_BODY()
public:
ACRStealthItem() { Duration = 10.f; };
UPROPERTY(EditDefaultsOnly, Category="GAS|Effects")
TSubclassOf<class UGameplayEffect> GE_Stealth;
protected:
virtual void Activate(AActor* Player) override;
};
// CRStealthItem.cpp
#include "CRStealthItem.h"
#include "AbilitySystemGlobals.h"
#include "AbilitySystemComponent.h"
#include "GameplayEffect.h"
void ACRStealthItem::Activate(AActor* Player)
{
if (!HasAuthority() || !Player || !GE_Stealth) return;
if (UAbilitySystemComponent* ASC = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Player))
{
FGameplayEffectContextHandle Ctx = ASC->MakeEffectContext();
Ctx.AddSourceObject(this);
FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(GE_Stealth, 1.f, Ctx);
if (Spec.IsValid())
{
Spec.Data->SetDuration(Duration, true);
ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get());
}
}
UE_LOG(LogTemp, Warning, TEXT("%s is invisible during %f sec"), *Player->GetName(), Duration);
}
다음은 StealthItem 헤더 파일과 소스 파일이다.
구성은 앞의 ExtraLifeItem 과 동일하다.

Project Settings 에서 검색창에 GameplayTags 라고 검색한다.
여기서 Gameplay Tags > Gameplay Tags List 에 있는 Manage Gameplay Tags... 버튼을 눌러준다.

왼쪽에 있는 + 버튼을 눌러 해당 창을 띄워준다.
우선 Source 는 DefaultGameplayTags.ini 로 선택해준다.
그리고 Name 칸에 State.Stealth 라는 이름의 태그를 하나 만들어주고, 또 GameplayCue.Stealth 라는 이름의 태그를 하나 더 만들어준다.

다음으로 BP_GE_Stealth 를 열어준다.
Duration 은 Has Duration 으로 해준다.
Gameplay Effect > Components 에서 + 버튼을 눌러주고,
Target Tags 를 Grant Tags to Target Actor 를 선택한 다음 Add Tags > Add to Inherited 에 아까 만든 State.Stealth 를 넣어준다.
마지막으로 Gameplay Cues > Gameplay Cues 에서 + 버튼으로 추가하고,
Index > Gameplay Cue Tags 에도 아까 만든 GameplayCue.Stealth 를 넣어주면 된다.

이후 BP_CRStealthItem 에 GAS > Effects > GE Stealth 에 BP_GE_Steatlth 를 넣어준다.


다음으로는 GameplayCueNotify_Loopping 을 부모 클래스로한 C++ 클래스 GCN_CRStealth 하나를 생성해준다.
이 GCN 은 은신 아이템을 획득한 캐릭터가 실제로 은신이 적용되도록 하는 클래스이다.
#pragma once
#include "CoreMinimal.h"
#include "GameplayCueNotify_Looping.h"
#include "GCN_CRStealth.generated.h"
UCLASS()
class COPANDROBBER_API AGCN_CRStealth : public AGameplayCueNotify_Looping
{
GENERATED_BODY()
public:
AGCN_CRStealth();
UPROPERTY(EditDefaultsOnly, Category = "Stealth")
FName OpacityParam = TEXT("StealthAlpha");
UPROPERTY(EditDefaultsOnly, Category = "Stealth", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float OwnerOpacity = 0.3f;
void OnActive(AActor* Target, const FGameplayCueParameters& Params);
void OnRemove(AActor* Target, const FGameplayCueParameters& Params);
protected:
static void ForEachMesh(AActor* Actor, TFunctionRef<void(class UMeshComponent*)> Fn);
static bool IsOwner(const AActor* Target);
void SetOpacityOnMeshes(AActor* Target, float Value);
};
헤더 파일이다.
우선 OpacityPram. 캐릭터의 머티리얼에 투명도 관련 노드의 이름을 StealthAlpha 라고 하고 이를 가져올 예정이다.
그리고 은신을 하면 본인은 반투명, 다른 사람에겐 투명하게 보이게 하기 위해 OwnerOpacity 라는 변수를 만들어 0.3f 로 설정해준다.
#include "GCN_CRStealth.h"
AGCN_CRStealth::AGCN_CRStealth()
{
GameplayCueTag = FGameplayTag::RequestGameplayTag(TEXT("GameplayCue.Stealth"));
bAutoDestroyOnRemove = true;
bAllowMultipleOnActiveEvents = false;
bAutoAttachToOwner = true;
}
void AGCN_CRStealth::OnActive_Implementation(AActor* Target, const FGameplayCueParameters& Params)
{
if (!Target) return;
ForEachMesh(Target, [](UMeshComponent* Mesh)
{
Mesh->SetOnlyOwnerSee(true);
Mesh->SetOwnerNoSee(false);
});
if (IsOwner(Target))
{
SetOpacityOnMeshes(Target, OwnerOpacity);
}
}
소스 파일이다.
먼저 생성자에서 설정한 것들을 차례로 보자면,
만일을 위해 태그를 붙여주었고,
bAutoDestroyOnRemove 는 Cue 가 끝나면 자동으로 파괴되야하기 때문에 true.
bAllowMultipleOnActiveEvents 는 중복 이벤트 활성화를 막기 위해 false.
bAutoAttachToOwner 는 대상 액터에 붙어야하기 때문에 true.
OnActive() 함수에서는 활성화된 대상의 메시를 ForEachMesh() 함수로 가져와서
SetOnlyOwnerSee 와 SetOwnerNoSee 를 통해 본인만 볼 수 있게한다.
그리고 만약 IsOwner() 함수를 통해 본인임을 확인하면 SetOpacityOnMehes() 함수로 투명도를 OwnerOpacity(0.3f) 로 설정해준다.
void AGCN_CRStealth::OnRemove_Implementation(AActor* Target, const FGameplayCueParameters& Params)
{
if (!Target) return;
ForEachMesh(Target, [](UMeshComponent* Mesh)
{
Mesh->SetOnlyOwnerSee(false);
Mesh->SetOwnerNoSee(false);
});
if (IsOwner(Target))
{
SetOpacityOnMeshes(Target, 1.0f);
}
}
OnRemove() 함수이다.
마찬가지로 ForEachMesh() 를 통해 메시를 가져와 모두가 볼 수 있게 설정하고, 자기 자신에게도 투명도를 1.0f 로 설정해준다.
void AGCN_CRStealth::ForEachMesh(AActor* Actor, TFunctionRef<void(class UMeshComponent*)> Fn)
{
if (!Actor) return;
TArray<UMeshComponent*> Meshes;
Actor->GetComponents<UMeshComponent>(Meshes, true);
for (UMeshComponent* Mesh : Meshes)
{
if (Mesh) Fn(Mesh);
}
}
ForEachMesh() 함수이다.
은신 아이템을 먹은 액터의 메시를 하나하나 가져와서 담아준다.
bool AGCN_CRStealth::IsOwner(const AActor* Target)
{
const APawn* Pawn = Cast<APawn>(Target);
return Pawn && Pawn->IsLocallyControlled();
}
IsOwner() 함수이다.
폰을 캐스팅해서 로컬에서 조작되고 있는지 확인하여 부울 값으로 반환한다.
void AGCN_CRStealth::SetOpacityOnMeshes(AActor* Target, float Value)
{
ForEachMesh(Target, [this, Value](class UMeshComponent* Mesh)
{
int32 Num = Mesh->GetNumMaterials();
for (int32 i = 0; i < Num; ++i)
{
if (UMaterialInstanceDynamic* MID = Mesh->CreateAndSetMaterialInstanceDynamic(i))
{
MID->SetScalarParameterValue(OpacityParam, Value);
}
}
});
}
마지막으로 투명도를 변경하는 SetOpacityOnMeshes() 함수이다.
ForEachMesh() 함수로 메시를 가져와 각각 OpacityParam 을 통해 Value 에 맞게 투명도를 변경해준다.

이제 블루프린트로 상속해주고 BP_GCN_Stealth 를 열어서 Details 패널에 Gameplay Cue 라고 검색한다.
Gameplay Cue > Gameplay Cue Tag 에 아까 만든 태그인 GameplayCue.Stealth 를 넣어준다.

마지막으로 캐릭터의 Mesh 를 설정하기 위해 머티리얼 쪽으로 와서
Details 패널에서 Material > Blend Mode 를 Masked 로 설정해주고,
Opacity Mask Clip Value 를 기본값인 0.5 로 설정해준다.


그리고 ScalarParameter 와 DitherTemporalAA 노드를 하나씩 생성해준다.

그리고 다음과 같이 아래에서 StealthAlpha 를 Default Value 를 1.0 으로 설정한 뒤,
DitherTemporalAA 의 Alpha Threshold 에 연결한다.
그런 다음 Result 를 머티리얼의 Opacity Mask 에 연결해준다.


한쪽이 은신 아이템을 먹으면 반투명해지고, 다른 플레이어에게는 보이지 않는 모습이다.
이번에 만든 은신 아이템에서는 GAS 시스템을 적용하려고 했다보니 공부하는 시간과 직접 적용하는 시간이 꽤나 걸렸다...
심지어 먹어도 은신이 안된다거나 핑이 갑자기 튄다거나 하는 여러가지 자잘한 버그들이 있어서 꽤나 고생했지만,
마지막엔 결국 잘 작동하는 걸 보고 굉장히 뿌듯하고 기쁘다.
이제 자기장도 만들어야 하는데 얼마나 힘들지 감이 안 잡힌다.....