오늘의 코드카타
- 올바른 괄호
- 최솟값 만들기
- 숫자의 표현
- 다음 큰 숫자
- 짝지어 제거하기
팀 프로젝트 진행 내용
1. 오버랩 시 플레이어에게 데미지를 가하는 투명 영역 구현
- 팀원이 레벨에서 용암 같은 곳을 밟으면 데미지를 가하는 볼륨이 필요하다고 해서 플레이어에게 데미지를 가하는 액터를 제작했다.
BoxComponent와 오버랩 이벤트가 발생하면 DamageEffectClass를 가하는 방식으로 구현했다.
- 또한 오버랩이 끝나면 해당 Effect를 해제해야 하기 때문에, 해당 EffectHandle을 맵으로 가지고 있다가 해제시키는 방향으로 구현했다.
// AO_DamageVolume.h
UCLASS()
class AO_API AAO_DamageVolume : public AActor
{
GENERATED_BODY()
public:
AAO_DamageVolume();
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Component")
TObjectPtr<UBoxComponent> BoxComponent;
UPROPERTY(EditAnywhere, Category = "GAS")
TSubclassOf<UGameplayEffect> DamageEffectClass;
UPROPERTY(EditAnywhere, Category = "Damage")
float DamagePerSecond = 10.f;
TMap<TWeakObjectPtr<AActor>, FActiveGameplayEffectHandle> ActiveHandles;
UFUNCTION()
void OnBeginOverlap(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult);
UFUNCTION()
void OnEndOverlap(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex);
};
- 오버랩 이벤트가 발생하면 데미지 이펙트 클래스의 데미지 값을
Data.DamagePerSecond 태그로 SetByCaller를 통해 데미지를 설정해서 이펙트를 부여하게끔 구현했다.
- 해당 데미지를 가하는 것은 서버에서만 실행되어야 하기 때문에 서버에서만 실행되게 제한했다.
// AO_DamageVolume.cpp
AAO_DamageVolume::AAO_DamageVolume()
{
...
BoxComponent->OnComponentBeginOverlap.AddDynamic(this, &AAO_DamageVolume::OnBeginOverlap);
BoxComponent->OnComponentEndOverlap.AddDynamic(this, &AAO_DamageVolume::OnEndOverlap);
}
void AAO_DamageVolume::OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (!HasAuthority())
{
return;
}
...
FGameplayEffectContextHandle Context = ASC->MakeEffectContext();
Context.AddSourceObject(this);
FGameplayEffectSpecHandle SpecHandle = ASC->MakeOutgoingSpec(DamageEffectClass, 1.f, Context);
checkf(SpecHandle.IsValid(), TEXT("Handle is invalid!"));
SpecHandle.Data->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.DamagePerSecond")), DamagePerSecond);
FActiveGameplayEffectHandle ActiveHandle = ASC->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
ActiveHandles.Add(OtherActor, ActiveHandle);
}
void AAO_DamageVolume::OnEndOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
...
if (FActiveGameplayEffectHandle* Handle = ActiveHandles.Find(OtherActor))
{
ASC->RemoveActiveGameplayEffect(*Handle);
ActiveHandles.Remove(OtherActor);
}
}
- GE_AreaDamage는 1초를 주기로 SetByCaller 값을 캐릭터의 체력에 가하게끔 아래와 같이 설정했다.

- 지금 데미지는 정상적으로 캐릭터에게 들어가지만, 캐릭터의 리액션에 대한 구현은 하지 않았다.
- 해당 과정은 애니메이션보다는 GameplayCue를 통해 화면에 데미지를 입고 있다는 이펙트를 보여주는 방식으로 추후에 구현할 예정이다.

2. 카메라 관리 매니저 구현
- 파쿠르, 스프린트 동작 시에 카메라 줌아웃되는 것을 구현하려고 하는데, 스프링암의 값을 임의로 계속 변경하거나 하면 의존성과 복잡성이 너무 증가하는 것으로 보여서 카메라 관리 매니저를 별도로 두고자 했다.
- 나는 GAS 기반으로 캐릭터의 동작들이 구분되기 때문에
GameplayTag를 활용하여 해당 Tag가 붙는 이벤트가 발생하면 카메라가 자동으로 변경되게끔 구현하고자 했다.
- 먼저 카메라 세팅에 필요한 정보를 저장하는 구조체와 구조체를 저장한 배열을 가지고 있는 데이터 에셋을 구현했다.
- 우선순위를 기반으로 디폴트 카메라 세팅을 0으로 두고, 태그가 겹쳐서 부여되는 상황을 고려해서 우선순위가 높은 카메라 세팅을 우선하도록 구현하고자 했다.
// AO_CameraProfile.h
USTRUCT(BlueprintType)
struct FAO_CameraSettings
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FGameplayTag Tag;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
int32 Priority = 0;
// Spring Arm
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float TargetArmLength = 150.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FVector SocketOffset = FVector(0.f, 50.f, 50.f);
UPROPERTY(EditAnywhere, BlueprintReadOnly)
bool bEnableCameraLag = true;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float CameraLagSpeed = 10.f;
// Camera
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float FOV = 90.f;
// Blend
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float BlendInTime = 0.25f;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float BlendOutTime = 0.25f;
};
UCLASS()
class AO_API UAO_CameraProfile : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TArray<FAO_CameraSettings> Profiles;
const FAO_CameraSettings* FindByTag(const FGameplayTag& Tag) const;
};
// AO_CameraProfile.cpp
const FAO_CameraSettings* UAO_CameraProfile::FindByTag(const FGameplayTag& Tag) const
{
for (const auto& P : Profiles)
{
if (P.Tag == Tag) return &P;
}
return nullptr;
}
- 플레이어 컨트롤러에 부착할 카메라 매니저 컴포넌트를 아래와 같이 구현했다.
- 카메라 전환 시에 블렌딩을 위한 정보를 가지는 구조체도 별도로 선언을 했다.
// AO_CameraManagerComponent.h
USTRUCT()
struct FAO_CameraRequest
{
GENERATED_BODY()
FGameplayTag Tag;
int32 Priority = 0;
float Weight = 0.f;
float BlendIn = 0.25f;
float BlendOut = 0.25f;
bool bActive = false;
};
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class AO_API UAO_CameraManagerComponent : public UActorComponent
{
GENERATED_BODY()
public:
UAO_CameraManagerComponent();
protected:
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
public:
UPROPERTY(EditAnywhere, Category = "Camera")
TObjectPtr<UAO_CameraProfile> ProfileDB;
public:
void BindCameraComponents(USpringArmComponent* InSpringArm, UCameraComponent* InCamera);
void BindToASC(UAbilitySystemComponent* InASC);
void PushCameraState(const FGameplayTag& CameraTag);
void PopCameraState(const FGameplayTag& CameraTag);
private:
void OnStateTagChanged(FGameplayTag Tag, int32 NewCount);
void UpdateRequests(float DeltaTime);
const FAO_CameraSettings* ChooseTargetProfile() const;
void ApplyProfileBlended(const FAO_CameraSettings& Target, float DeltaTime);
private:
UPROPERTY()
TObjectPtr<USpringArmComponent> SpringArm;
UPROPERTY()
TObjectPtr<UCameraComponent> Camera;
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> ASC;
UPROPERTY()
TMap<FGameplayTag, FAO_CameraRequest> Requests;
FTimerHandle TimerHandle_Update;
// 현재 적용 중인 세팅
float CurArmLength = 150.f;
FVector CurSocketOffset = FVector(0.f, 50.f, 50.f);
float CurFOV = 90.f;
bool bCurEnableLag = true;
float CurLagSpeed = 10.f;
// 캐릭터 상태 관련 태그
FGameplayTag State_Dead;
FGameplayTag State_Sprint;
FGameplayTag State_Traversal;
// 카메라 프로필 태그
FGameplayTag Camera_Default;
FGameplayTag Camera_Dead;
FGameplayTag Camera_Sprint;
FGameplayTag Camera_Traversal;
// 델리게이트 핸들
TArray<FDelegateHandle> TagEventHandles;
};
- 먼저 생성자에서 캐릭터의 상태 관련 태그와 카메라 프로필 태그를 지정했다. 추후에 수정할지는 모르겠지만, 카메라 전환 시에 블렌딩 과정이 필수여서 Tick을 통해 해당 과정을 처리하고자 했다.
// AO_CameraManagerComponent.cpp
UAO_CameraManagerComponent::UAO_CameraManagerComponent()
{
PrimaryComponentTick.bCanEverTick = true;
State_Dead = FGameplayTag::RequestGameplayTag(FName("Status.Death"));
State_Sprint = FGameplayTag::RequestGameplayTag(FName("Status.Action.Sprint"));
State_Traversal = FGameplayTag::RequestGameplayTag(FName("Status.Action.Traversal"));
Camera_Default = FGameplayTag::RequestGameplayTag(FName("Camera.Default"));
Camera_Dead = FGameplayTag::RequestGameplayTag(FName("Camera.Dead"));
Camera_Sprint = FGameplayTag::RequestGameplayTag(FName("Camera.Sprint"));
Camera_Traversal = FGameplayTag::RequestGameplayTag(FName("Camera.Traversal"));
}
- Charcter에 있는 SpringArm과 Camera 컴포넌트를 바인딩하는 함수와 ASC를 연결하고 태그 이벤트로 상태가 변경되었을 때의 처리를 바인딩하는 함수를 별도로 구현했다.
// AO_CameraManagerComponent.cpp
void UAO_CameraManagerComponent::BindCameraComponents(USpringArmComponent* InSpringArm, UCameraComponent* InCamera)
{
SpringArm = InSpringArm;
Camera = InCamera;
if (SpringArm)
{
CurArmLength = SpringArm->TargetArmLength;
CurSocketOffset = SpringArm->SocketOffset;
bCurEnableLag = SpringArm->bEnableCameraLag;
CurLagSpeed = SpringArm->CameraLagSpeed;
}
if (Camera)
{
CurFOV = Camera->FieldOfView;
}
}
void UAO_CameraManagerComponent::BindToASC(UAbilitySystemComponent* InASC)
{
ASC = InASC;
TagEventHandles.Reset();
checkf(ASC, TEXT("ASC is null"));
const int32 DeadTagCount = ASC->GetTagCount(State_Dead);
const int32 SprintTagCount = ASC->GetTagCount(State_Sprint);
const int32 TraversalTagCount = ASC->GetTagCount(State_Traversal);
OnStateTagChanged(State_Dead, DeadTagCount);
OnStateTagChanged(State_Sprint, SprintTagCount);
OnStateTagChanged(State_Traversal, TraversalTagCount);
TagEventHandles.Add(
ASC->RegisterGameplayTagEvent(State_Dead, EGameplayTagEventType::NewOrRemoved)
.AddUObject(this, &UAO_CameraManagerComponent::OnStateTagChanged));
TagEventHandles.Add(
ASC->RegisterGameplayTagEvent(State_Sprint, EGameplayTagEventType::NewOrRemoved)
.AddUObject(this, &UAO_CameraManagerComponent::OnStateTagChanged));
TagEventHandles.Add(
ASC->RegisterGameplayTagEvent(State_Traversal, EGameplayTagEventType::NewOrRemoved)
.AddUObject(this, &UAO_CameraManagerComponent::OnStateTagChanged));
}
- 위에서 바인딩한 태그가 추가되거나 제거되었을 때 발생할 함수로, 상태 태그에 따라 관련된 카메라 프로필이 추가되거나 삭제되는 구조로 구현했다. 추후에 카메라 관련 상태가 많아질 경우에는 Map을 만들어서 관리하는 것도 좋은 방법으로 보인다.
// AO_CameraManagerComponent.cpp
void UAO_CameraManagerComponent::OnStateTagChanged(FGameplayTag Tag, int32 NewCount)
{
const bool bOn = (NewCount > 0);
if (Tag == State_Dead)
{
if (bOn)
{
PushCameraState(Camera_Dead);
}
else
{
PopCameraState(Camera_Dead);
}
}
else if (Tag == State_Sprint)
{
if (bOn)
{
PushCameraState(Camera_Sprint);
}
else
{
PopCameraState(Camera_Sprint);
}
}
else if (Tag == State_Traversal)
{
if (bOn)
{
PushCameraState(Camera_Traversal);
}
else
{
PopCameraState(Camera_Traversal);
}
}
}
- 데이터 에셋을 통해 설정한 카메라 프로필에서 관련 태그를 찾아서 블렌딩을 위한 값 정보로 추가하고 제거하는 함수이다.
bActive 변수를 통해 해당 요청을 BlendIn할지, BlendOut할지 구분하는 역할을 하게 된다.
// AO_CameraManagerComponent.cpp
void UAO_CameraManagerComponent::PushCameraState(const FGameplayTag& CameraTag)
{
checkf(ProfileDB, TEXT("ProfileDB is null"));
const FAO_CameraSettings* Settings = ProfileDB->FindByTag(CameraTag);
checkf(Settings, TEXT("Failed to find CameraSettings by CameraTag"));
FAO_CameraRequest& Request = Requests.FindOrAdd(CameraTag);
Request.Tag = CameraTag;
Request.Priority = Settings->Priority;
Request.BlendIn = FMath::Max(0.01f, Settings->BlendInTime);
Request.BlendOut = FMath::Max(0.01f, Settings->BlendOutTime);
Request.bActive = true;
}
void UAO_CameraManagerComponent::PopCameraState(const FGameplayTag& CameraTag)
{
if (FAO_CameraRequest* Request = Requests.Find(CameraTag))
{
Request->bActive = false;
}
}
- 다음으로
Tick 과정을 보면 먼저 (1) 요청 상태들의 가중치(Weight)를 시간에 따라 갱신하고, (2) 그 중에서 이번 프레임에 적용할 카메라 프로필을 선택한 다음, (3) 현재 카메라 값을 목표 프로필 쪽으로 보간하면서 이동하는 구조로 작동한다.
// AO_CameraManagerComponent.cpp
void UAO_CameraManagerComponent::TickComponent(float DeltaTime, enum ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!Camera || !SpringArm)
{
return;
}
// 1. 요청 상태들의 가중치를 시간에 따라 갱신
UpdateRequests(DeltaTime);
// 2. 이번 프레임에 적용할 카메라 프로필 선택
const FAO_CameraSettings* Target = ChooseTargetProfile();
if (!Target)
{
if (const FAO_CameraSettings* Default = ProfileDB->FindByTag(Camera_Default))
{
ApplyProfileBlended(*Default, DeltaTime);
}
return;
}
// 3. 현재 카메라 값을 목표 프로필 쪽으로 보간하면서 이동
ApplyProfileBlended(*Target, DeltaTime);
}
UpdateRequests()
- 카메라 상태 요청들의 Weight를 업데이트하고, 필요 없는 요청은 정리함
bActive == true -> Weight를 0 -> 1로 올림 (Blend In 속도로)
bActive == false -> Weight를 1 -> 0으로 내림 (Blend Out 속도로)
- Weight가 0까지 떨어지면 Map에서 제거
ChooseTargetProfile()
- 현재 Requests 중에서 이번에 적용할 카메라 프로필을 하나 선택함
- Weight > 0인 후보 중에서 Priority가 가장 높은 프로필을 선택하게 됨
ApplyProfileBlend()
- 목표 프로필을 향해 현재 카메라 상태를 부드럽게 이동시킴
- 보간하는 방식은 현재
InterpTo()를 사용해서 보간하고 있음. 추후에 변경 가능
// AO_CameraManagerComponent.cpp
void UAO_CameraManagerComponent::UpdateRequests(float DeltaTime)
{
TArray<FGameplayTag> ToRemove;
for (auto& It : Requests)
{
FAO_CameraRequest& CurRequest = It.Value;
const float Speed = CurRequest.bActive ? (1.f / CurRequest.BlendIn) : (1.f / CurRequest.BlendOut);
const float Dir = CurRequest.bActive ? 1.f : -1.f;
CurRequest.Weight = FMath::Clamp(CurRequest.Weight + Dir * Speed * DeltaTime, 0.f, 1.f);
if (!CurRequest.bActive && CurRequest.Weight <= 0.f)
{
ToRemove.Add(It.Key);
}
}
for (const auto& Tag : ToRemove)
{
Requests.Remove(Tag);
}
}
const FAO_CameraSettings* UAO_CameraManagerComponent::ChooseTargetProfile() const
{
const FAO_CameraSettings* Best = nullptr;
int32 BestPriority = -1;
float BestWeight = 0.f;
for (const auto& It : Requests)
{
const FAO_CameraRequest& Request = It.Value;
if (Request.Weight <= 0.f)
{
continue;
}
const FAO_CameraSettings* Profile = ProfileDB->FindByTag(Request.Tag);
if (!Profile)
{
continue;
}
if (Profile->Priority > BestPriority
|| (Profile->Priority == BestPriority && Request.Weight > BestWeight))
{
BestPriority = Profile->Priority;
BestWeight = Request.Weight;
Best = Profile;
}
}
return Best;
}
void UAO_CameraManagerComponent::ApplyProfileBlended(const FAO_CameraSettings& Target, float DeltaTime)
{
const float InterpSpeed = 12.f;
CurArmLength = FMath::FInterpTo(CurArmLength, Target.TargetArmLength, DeltaTime, InterpSpeed);
CurSocketOffset = FMath::VInterpTo(CurSocketOffset, Target.SocketOffset, DeltaTime, InterpSpeed);
CurFOV = FMath::FInterpTo(CurFOV, Target.FOV, DeltaTime, InterpSpeed);
bCurEnableLag = Target.bEnableCameraLag;
CurLagSpeed = Target.CameraLagSpeed;
SpringArm->TargetArmLength = CurArmLength;
SpringArm->SocketOffset = CurSocketOffset;
SpringArm->bEnableCameraLag = bCurEnableLag;
SpringArm->CameraLagSpeed = CurLagSpeed;
Camera->SetFieldOfView(CurFOV);
}
- 컴포넌트를 부착하고 초기화를 플레이어 컨트롤러에서 진행했다.
- 초기화 함수에서는 카메라 컴포넌트와 ASC를 바인딩하고, Default 카메라를 초기에 Push했다.
- 초기화 시점은 리슨 서버를 기준으로 서버에서는
OnPossess() 함수에서 진행했고, 클라에서는 OnRep_Pawn() 함수에서 진행했다.
// AO_PlayerController_InGameBase
void AAO_PlayerController_InGameBase::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (!IsLocalController())
{
return;
}
InitCameraManager();
}
void AAO_PlayerController_InGameBase::OnRep_Pawn()
{
Super::OnRep_Pawn();
if (!IsLocalController())
{
return;
}
InitCameraManager();
}
void AAO_PlayerController_InGameBase::InitCameraManager()
{
checkf(CameraManagerComponent, TEXT("CameraManagerComponent not found"));
AAO_PlayerCharacter* PlayerCharacter = Cast<AAO_PlayerCharacter>(GetPawn());
checkf(PlayerCharacter, TEXT("Character not found"));
CameraManagerComponent->BindCameraComponents(PlayerCharacter->GetSpringArm(), PlayerCharacter->GetCamera());
CameraManagerComponent->PushCameraState(FGameplayTag::RequestGameplayTag(FName("Camera.Default")));
if (UAbilitySystemComponent* ASC = PlayerCharacter->GetAbilitySystemComponent())
{
CameraManagerComponent->BindToASC(ASC);
}
}
- 프로필 데이터 에셋을 아래와 같이 생성을 했다.
- 너무 많은 이동이 있는 경우, 다소 어지러움이 생길 수도 있을 것으로 보인다. 또한 디폴트가 캐릭터와 카메라 거리가 상당히 짧기 때문에 많이 움직이면 휙휙 움직이는 느낌을 받았다.
- 따라서 스프린트, 파쿠르 시에 캐릭터의 중간이 아닌 살짝 중간으로 가는 느낌만 받게끔 값을 설정했다.

결과
