Unreal Engine/Functional Implementation

GAS를 사용하여 Scan 기능 구현

보별 2025. 7. 10. 12:46

GAS에 대한 간단한 설명은 아래에 있으니 참고하면 좋다.

https://neighborly-badger-f67.notion.site/GAS-Game-Ability-System-22c12e93349480aa93b7d7cfc051db9c?source=copy_link

 

GAS (Game Ability System) | Notion

개요

neighborly-badger-f67.notion.site

위의 정리 내용을 한 번 보고 이 글을 보거나, 정리 내용을 펼쳐놓고 이 글을 같이 보는게 이해하는 데에 도움이 될 것이라 생각한다.

GAS를 활용하여 Scan에 대한 능력을 구현할 것이기에 이름은 "Scan" 으로 할 것이고, GameAbility에 속하기 때문에 작성할 C++ 코드는 "GA_Scan" 이라고 명명해주었다.
코드는 아래와 같이 작성해주었다.

GA_Scan.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#pragma once
 
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "GA_Scan.generated.h"
 
/**
 * 
 */
UCLASS()
class MyGame_API UGA_Scan : public UGameplayAbility
{
    GENERATED_BODY()
 
public:
    UGA_Scan();
 
    // Scan에 필요한 함수를 BP_Scan_Player가 갖고 있어서, 해당 클래스 지정
    UPROPERTY(EditDefaultsOnly, Category = "Scan")
    TSubclassOf<AActor> ScanPlayerBPClass;
 
    virtual void ActivateAbility(
        const FGameplayAbilitySpecHandle Handle,
        const FGameplayAbilityActorInfo* ActorInfo,
        const FGameplayAbilityActivationInfo ActivationInfo,
        const FGameplayEventData* TriggerEventData
    ) override;
 
protected:
    UPROPERTY()
    AActor* CachedScanActor = nullptr;
};
cs

GA_Scan.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include "GA_Scan.h"
#include "GameFramework/Actor.h"
#include "AbilitySystemComponent.h"
 
UGA_Scan::UGA_Scan()
{
    // 플레이어당 하나의 Ability 인스턴스가 생성되도록 설정
    InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
 
void UGA_Scan::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
    AActor* Avatar = ActorInfo->AvatarActor.Get();
    if (!Avatar || !ScanPlayerBPClass)
    {
        EndAbility(Handle, ActorInfo, ActivationInfo, truefalse);
        return;
    }
 
    // 캐시된 Actor가 없거나 유효하지 않으면 새로 생성
    if (!CachedScanActor || !CachedScanActor->IsValidLowLevel() || CachedScanActor->IsPendingKillPending())
    {
        FActorSpawnParameters SpawnParams;
        SpawnParams.Owner = Avatar;
        SpawnParams.Instigator = Avatar->GetInstigator();
 
        CachedScanActor = GetWorld()->SpawnActor<AActor>(
            ScanPlayerBPClass,
            Avatar->GetActorTransform(),
            SpawnParams
        );
 
        if (!CachedScanActor)
        {
            EndAbility(Handle, ActorInfo, ActivationInfo, truefalse);
            return;
        }
 
        // 생성된 ScanActor를 Avatar에 붙여서 따라다니도록 설정; 이렇게 해주지 않으면 ScanActor가 처음 생성된 위치에 고정되어 해당 위치에서만 Scan 수행
        CachedScanActor->AttachToComponent(
            Avatar->GetRootComponent(),
            FAttachmentTransformRules::SnapToTargetNotIncludingScale
        );
    }
 
    // StartScan 함수 호출
    if (CachedScanActor->FindFunction(TEXT("StartScan")))
    {
        CachedScanActor->CallFunctionByNameWithArguments(TEXT("StartScan"), *GLog, nullptr, true);
    }
 
    EndAbility(Handle, ActorInfo, ActivationInfo, truefalse);
}
cs

Scan은 외부 Asset을 그대로 사용하여 구현하였는데, 사용한 Asset이 전부 BP로 구성되어있고 BP간 긴밀히 얽혀있어서 Asset을 그대로 사용하였다.
Scan은 Asset에 있는 BP의 "StartScan" 이라는 함수를 실행시켜야 작동하게 되기에, 구현부를 보면 Asset에 있는 BP를 Spawn한 뒤에 해당 BP에 있는 "StartScan" 을 실행시켜주도록 하였다.

아래의 코드들은 위 코드를 짜기 전에 작성한 것으로, 각각 부족한 부분이 있어서 위 코드와 같이 수정하게 되었다.

수정된 코드 - 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include "GA_Scan.h"
#include "GameFramework/Actor.h"
#include "AbilitySystemComponent.h"
 
void UGA_Scan::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
    AActor* Avatar = ActorInfo->AvatarActor.Get();
    if (!Avatar || !ScanPlayerBPClass)
    {
        EndAbility(Handle, ActorInfo, ActivationInfo, truefalse);
        return;
    }
 
    // BP_Scan_Player 스폰
    FActorSpawnParameters Params;
    Params.Owner = Avatar;
    Params.Instigator = Avatar->GetInstigator();
    AActor* ScanActor = GetWorld()->SpawnActor<AActor>(
        ScanPlayerBPClass,
        Avatar->GetActorTransform(),
        Params
    );
 
    if (ScanActor)
    {
        const FString FuncName = TEXT("StartScan");
        if (ScanActor->FindFunction(FName(*FuncName)))
        {
            ScanActor->CallFunctionByNameWithArguments(
                TEXT("StartScan"),
                *GLog,
                nullptr,
                true
            );
        }
        else
        {
            UE_LOG(LogTemp, Warning, TEXT("StartScan() 함수가 BP에서 정의되어 있지 않음"));
        }
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("ScanActor를 생성할 수 없음"));
    }
 
    EndAbility(Handle, ActorInfo, ActivationInfo, truefalse);
}
cs

위 코드는 맨 위에 있는 코드와 같이 Asset에 있는 BP를 Spawn한 뒤 Scan을 하도록 하였으나, Scan 버튼을 누르면 Spawn이 계속 되어 BP가 계속 생성되는 일이 있어서, 수정을 하게 되었다.
BP가 계속 생성되면 당연하게도 컴퓨터 자원을 많이 잡아먹어서 최적화에도 안 좋을 뿐더러, Scan에 재사용 대기시간이 적용되지 않게 된다.

수정된 코드 -  2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "GA_Scan.h"
#include "GameFramework/Actor.h"
#include "AbilitySystemComponent.h"
 
void UGA_Scan::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
    AActor* Avatar = ActorInfo->AvatarActor.Get();
    if (!Avatar || !ScanPlayerBPClass)
    {
        EndAbility(Handle, ActorInfo, ActivationInfo, truefalse);
        return;
    }
 
    // 최조 1회만 Spawn
    if (!CachedScanActor)
    {
        FActorSpawnParameters P;
        P.Owner = Avatar;
        P.Instigator = Avatar->GetInstigator();
        CachedScanActor = GetWorld()->SpawnActor<AActor>(
            ScanPlayerBPClass, Avatar->GetActorTransform(), P);
    }
 
    // 이미 스폰된 CachedScanActor에 StartScan 호출 → 쿨다운 유지
    if (CachedScanActor->FindFunction(TEXT("StartScan")))
    {
        CachedScanActor->CallFunctionByNameWithArguments(
            TEXT("StartScan"), *GLog, nullptr, true);
    }
 
    EndAbility(Handle, ActorInfo, ActivationInfo, truefalse);
}
cs

위 코드는 "수정된 코드 - 1" 을 수정한 것으로, BP를 Spawn한 뒤 함수를 실행하는 기본적인 원리는 같으나, 'CachedScanActor' 로 Spawn이 하나만 되도록 수정해주었다.
이렇게 했을 경우의 문제는 Player가 처음 Scan을 실행했던 위치에 함수를 실행하는 BP가 생성되어, 다음 번 Scan 버튼을 누를 때 Scan 버튼을 누르는 Player의 위치가 아닌, BP가 생성됐던 위치에서 Scan이 실행되는 문제가 있어 수정하게 되었다.

코드를 작성했으니 Compile을 해주고, 위에서 생성한 "GA_Scan" 을 부모로 하는 BP를 생성해준다.
해당 BP의 이름은 "BPGA_Scan" 으로 명명해주었다.

"BPGA_Scan" 을 실행하면 위 그림과 같은 창이 뜨는데, 여기서 설정해줄 것이 있다.
현재 코드에는 Scan을 위한 Actor를 Spawn만 하지 어떤 Actor인지는 지정해주지 않았기에, "ScanPlayerBP Class" 에 Scan을 실행하는 함수가 있는 Actor BP를 할당해준다.
그리고 맨위의 최종적으로 작성한 코드를 보면 Player가 하나의 BP를 소환하도록 설정해주었는데, 해당 코드로 BP를 생성해주면 코드의 내용과는 다르게 'InstancingPolicy' 가 "InstancedPerActor" 로 적용되지 않는다.
BP의 'InstancingPolicy' 도 "InstancedPerActor" 로 설정해주면 된다.

이번엔 Player BP로 가서, 기존에 Player에서 생성했던 GAS Component의 'StartAbilities' 에 위에서 생성한 "BPGA_Scan" 을 할당해준다.

마지막으로 Widget의 버튼을 눌렀을 때 해당 Ability를 실행해주기 위해 위 그림과 같이 연결해주었다.
GAS의 Ability를 실행할 때에는 위 그림과 같이 "Try Active Ability" 를 통해 실행시켜주면 된다.

이번엔 Scan을 사용 중이라는 상태를 나타내주는 Gameplay Tag를 생성한 뒤 연동시켜보도록 하겠다.

"Project Settings" 의 "GameplayTags" 에 가서 "Scan" 이라는 Tag를 생성해주고, Scan이 재사용 대기상태라는 걸 알려주기 위한 Tag인 "Cooldown.Scan" 도 생성해준다.

 * 위 사진에도 나와있지만, Tag는 프로젝트 폴더의 "Config" 에 있는 "DefaultGameplayTags.ini" 에서 확인 가능

GAS로 데이터 관리를 하는게 개개인마다 다 다른데, 일반적으로는 DataRegistry(이하 'DR' 이라고 칭함)를 Ability 별로 따로따로 관리해주는 편이지만, 해당 글에서는 하나의 DR에서 관리하는 방법을 이용해보겠다.

우선 위에서 생성한 Tag를 보면 Scan을 'Common' 으로 따로 분류해줬기에, 'Common' Data Table(이하 'DT' 라고 칭함)을 생성해준다.

해당 DT에 "BPGA_Scan" 을 할당해주고, 해당 스킬에 관한 정보(소모 자원, 재사용 대기시간 등)나 Tag도 설정해준다.

그리고 하나의 DR에서 관리하는 방법을 사용하였기에, 기존에 생성해두었던 DR에 위에서 생성한 DT를 추가해주면 된다.

그러면 위 그림과 같이 DR에 위에서 생성했던 Scan이라는 DT가 들어가 관리가 가능하게 된다.

Tag를 생성한 뒤 붙여주고, DR에 생성한 DT를 연결해줬으니, 다음으로 GAS의 Cooldown을 전체적으로 관리해주고 있는 GE_Cooldown에 GA_Scan이 관리되도록 추가해줘야한다.
해당 작업은 코드로 작성해주면 되고, 아래는 위의 작업들이 모두 반영된 코드이다.

GA_Scan.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#pragma once
 
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
 
class AActor;
 
#include "GA_Scan.generated.h"
 
/**
 *
 */
UCLASS()
class MyGame_API UGA_Scan : public UGameplayAbility
{
    GENERATED_BODY()
 
public:
    UGA_Scan();
 
    // Scan에 필요한 함수를 BP_Scan_Player가 갖고 있어서, 해당 클래스 지정
    UPROPERTY(EditDefaultsOnly, Category = "Scan")
    TSubclassOf<AActor> ScanPlayerBPClass;
 
    virtual void ActivateAbility(
        const FGameplayAbilitySpecHandle Handle,
        const FGameplayAbilityActorInfo* ActorInfo,
        const FGameplayAbilityActivationInfo ActivationInfo,
        const FGameplayEventData* TriggerEventData
    ) override;
 
    virtual void EndAbility(
        const FGameplayAbilitySpecHandle Handle,
        const FGameplayAbilityActorInfo* ActorInfo,
        const FGameplayAbilityActivationInfo ActivationInfo,
        bool bReplicateEndAbility,
        bool bWasCancelled
    ) override;
 
protected:
    UPROPERTY()
    AActor* CachedScanActor = nullptr;
 
    // Scan 중 붙일 태그, Scan Cooldown 태그
    FGameplayTag ScanAbilityTag;
    FGameplayTag ScanCooldownTag;
 
    // Commit 시점 파라미터 저장
    FGameplayAbilitySpecHandle       SavedHandle;
    const FGameplayAbilityActorInfo* SavedActorInfo = nullptr;
    FGameplayAbilityActivationInfo   SavedActivationInfo;
 
    // Scan 완료 시 호출 되는 함수
    UFUNCTION()
    void OnScanComplete();
 
    void ApplyCooldown(
        const FGameplayAbilitySpecHandle Handle,
        const FGameplayAbilityActorInfo* ActorInfo,
        FGameplayAbilityActivationInfo ActivationInfo) const;
};
cs

GA_Scan.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#include "GA_Scan.h"
#include "DataRegistrySubsystem.h"
#include "GameplayEffect.h"
#include "GameplayEffectExtension.h"
#include "GameplayTagContainer.h"
#include "GameFramework/Actor.h"
#include "MyGame/Struct/AbilityData.h"
#include "AbilitySystemComponent.h"
#include "Abilities/Tasks/AbilityTask_WaitDelay.h"
 
UGA_Scan::UGA_Scan()
{
    // 플레이어당 하나의 Ability 인스턴스가 생성되도록 설정
    InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
 
    // 태그 캐싱
    ScanAbilityTag = FGameplayTag::RequestGameplayTag(TEXT("Ability.Common.Scan"));
    ScanCooldownTag = FGameplayTag::RequestGameplayTag(TEXT("Cooldown.Common.Scan"));
 
    ActivationOwnedTags.AddTag(ScanAbilityTag);
    ActivationBlockedTags.AddTag(ScanCooldownTag);
 
    /*UE_LOG(LogTemp, Log,
        TEXT("Scan tags initialized: %s, %s"),
        *ScanAbilityTag.ToString(),
        *ScanCooldownTag.ToString());*/
}
 
void UGA_Scan::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
    Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
 
    if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
    {
        EndAbility(Handle, ActorInfo, ActivationInfo, truefalse);
        return;
    }
 
    // 태그가 제대로 붙었는지 확인
    {
        FGameplayTagContainer Tags;
        ActorInfo->AbilitySystemComponent->GetOwnedGameplayTags(Tags);
        UE_LOG(LogTemp, Log, TEXT("[GA_Scan] Activated, Tags = %s"), *Tags.ToString());
    }
 
    // BP_Scan_Player Spawn + StartScan 호출
    AActor* Avatar = ActorInfo->AvatarActor.Get();
    if (Avatar && ScanPlayerBPClass)
    {
        FActorSpawnParameters Params;
        Params.Owner = Avatar;
        Params.Instigator = Avatar->GetInstigator();
 
        // 최초 1회만 Spawn
        if (!IsValid(CachedScanActor))
        {
            CachedScanActor = GetWorld()->SpawnActor<AActor>(
                ScanPlayerBPClass,
                Avatar->GetActorTransform(),
                Params);
 
            if (CachedScanActor)
            {
                CachedScanActor->AttachToComponent(
                    Avatar->GetRootComponent(),
                    FAttachmentTransformRules::SnapToTargetNotIncludingScale);
            }
        }
 
        // Spawn된 액터가 있으면 StartScan 호출
        if (IsValid(CachedScanActor))
        {
            CachedScanActor->CallFunctionByNameWithArguments(
                TEXT("StartScan"), *GLog, nullptr, true);
        }
    }
 
    // Scan 및 Cooldown 모두 3초로 동기 실행
    SavedHandle = Handle;
    SavedActorInfo = ActorInfo;
    SavedActivationInfo = ActivationInfo;
 
    // 3초 뒤 Scan 완료 → OnScanComplete
    UAbilityTask_WaitDelay* ScanWait = UAbilityTask_WaitDelay::WaitDelay(this, 3.f);
    ScanWait->OnFinish.AddDynamic(this, &UGA_Scan::OnScanComplete);
    ScanWait->ReadyForActivation();
}
 
void UGA_Scan::OnScanComplete()
{
    if (!SavedActorInfo || !SavedActorInfo->AbilitySystemComponent.Get())
        return;
 
    UE_LOG(LogTemp, Log, TEXT("[GA_Scan] Scan complete"));
 
    // Ability 종료 → ActivationOwnedTags 자동 제거
    EndAbility(SavedHandle, SavedActorInfo, SavedActivationInfo, falsefalse);
}
 
void UGA_Scan::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
    Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}
 
void UGA_Scan::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
    // 쿨다운 GE 적용 및 태그 확인
    if (UGameplayEffect* GE = GetCooldownGameplayEffect())
    {
        // Spec 생성
        FGameplayEffectSpecHandle Spec =
            MakeOutgoingGameplayEffectSpec(GE->GetClass(), GetAbilityLevel());
 
        Spec.Data.Get()->DynamicGrantedTags.AddTag(ScanCooldownTag);
 
        // SetByCaller 매그니튜드 설정
        FDataRegistryId Id{ FDataRegistryType(TEXT("Ability")), ScanAbilityTag.GetTagName() };
        if (const FAbilityData* Data = UDataRegistrySubsystem::Get()->GetCachedItem<FAbilityData>(Id))
        {
            Spec.Data.Get()->SetSetByCallerMagnitude(
                FGameplayTag::RequestGameplayTag(TEXT("Data.Cooldown")),
                Data->BaseCooldown);
        }
 
        ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, Spec);
 
        // 태그 확인
        FGameplayTagContainer Tags;
        ActorInfo->AbilitySystemComponent->GetOwnedGameplayTags(Tags);
        UE_LOG(LogTemp, Log, TEXT("[GA_Scan] After Cooldown GE, Tags = %s"), *Tags.ToString());
    }
}
cs

GA_Scan에 GE_Cooldown을 연결해주었고, 지정된 Cooldown 시간과 Scan 중인 상태에 맞춰서, Ability.Common.Scan Tag와 Cooldown.Common.Scan Tag가 등장하고 사라지도록 해주었다.

그리고 전에 생성했던 BPGA_Scan에 들어가서 초기화를 한 번 해줘야한다.
위 코드를 작성하기 전의 GA_Scan으로 BPGA_Scan을 생성했기에, 위 코드의 변경점이 BP에 반영이 되지 않기 때문이다.

위 그림과 같이 Tag가 추가된 것을 확인하고 실행해보면 아래와 같이 작동하는 것을 확인할 수 있다.

Scan이 실행됨과 동시에 Scan Tag와 Scan Cooldown Tag가 동시에 출력되고, GE_Cooldown에 입력한 시간이 지나면 자동으로 사라지는 것을 확인할 수 있다.

아래의 코드는 위 코드를 짜기 전에 작성한 것으로, 각각 부족한 부분이 있어서 위 코드와 같이 수정하게 되었다.

수정된 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#include "GA_Scan.h"
#include "DataRegistrySubsystem.h"
#include "GameplayEffect.h"
#include "GameplayEffectExtension.h"
#include "GameplayTagContainer.h"
#include "GameFramework/Actor.h"
#include "MyGame/Struct/AbilityData.h"
#include "AbilitySystemComponent.h"
#include "Abilities/Tasks/AbilityTask_WaitDelay.h"
 
UGA_Scan::UGA_Scan()
{
    // 플레이어당 하나의 Ability 인스턴스가 생성되도록 설정
    InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
 
    // 태그 캐싱
    ScanAbilityTag = FGameplayTag::RequestGameplayTag(TEXT("Ability.Common.Scan"));
    ScanCooldownTag = FGameplayTag::RequestGameplayTag(TEXT("Cooldown.Common.Scan"));
 
    ActivationOwnedTags.AddTag(ScanAbilityTag);
    ActivationBlockedTags.AddTag(ScanCooldownTag);
    UE_LOG(LogTemp, Log,
        TEXT("Scan tags initialized: %s, %s"),
        *ScanAbilityTag.ToString(),
        *ScanCooldownTag.ToString());
}
 
void UGA_Scan::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
    Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
 
    if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
    {
        EndAbility(Handle, ActorInfo, ActivationInfo, truefalse);
        return;
    }
 
    // 태그가 제대로 붙었는지 확인
    {
        FGameplayTagContainer Tags;
        ActorInfo->AbilitySystemComponent->GetOwnedGameplayTags(Tags);
        UE_LOG(LogTemp, Log, TEXT("[GA_Scan] Activated, Tags = %s"), *Tags.ToString());
    }
 
    ApplyCooldown(Handle, ActorInfo, ActivationInfo);
 
    // BP_Scan_Player Spawn + StartScan 호출
    AActor* Avatar = ActorInfo->AvatarActor.Get();
    if (Avatar && ScanPlayerBPClass && !IsValid(CachedScanActor))
    {
        FActorSpawnParameters Params;
        Params.Owner = Avatar;
        Params.Instigator = Avatar->GetInstigator();
 
        CachedScanActor = GetWorld()->SpawnActor<AActor>(
            ScanPlayerBPClass,
            Avatar->GetActorTransform(),
            Params);
 
        if (CachedScanActor)
        {
            CachedScanActor->AttachToComponent(
                Avatar->GetRootComponent(),
                FAttachmentTransformRules::SnapToTargetNotIncludingScale);
            CachedScanActor->CallFunctionByNameWithArguments(TEXT("StartScan"), *GLog, nullptr, true);
        }
    }
 
    // 3초 뒤 OnScanComplete 호출; Scan의 지속시간 및 Cooldown이 3초
    SavedHandle = Handle;
    SavedActorInfo = ActorInfo;
    SavedActivationInfo = ActivationInfo;
 
    // Scan 완료 (고정 3초)
    UAbilityTask_WaitDelay* ScanWait = UAbilityTask_WaitDelay::WaitDelay(this, 3.f);
    ScanWait->OnFinish.AddDynamic(this, &UGA_Scan::OnScanComplete);
    ScanWait->ReadyForActivation();
 
    // Cooldown 완료 (DataRegistry 기준)
    float CooldownTime = 3.f;
    {
        FDataRegistryId Id{ FDataRegistryType(TEXT("Ability")), ScanAbilityTag.GetTagName() };
        if (const FAbilityData* Data = UDataRegistrySubsystem::Get()->GetCachedItem<FAbilityData>(Id))
        {
            CooldownTime = Data->BaseCooldown;
        }
    }
 
    UAbilityTask_WaitDelay* CdWait = UAbilityTask_WaitDelay::WaitDelay(this, CooldownTime);
    CdWait->OnFinish.AddDynamic(this, &UGA_Scan::OnCooldownComplete);
    CdWait->ReadyForActivation();
}
 
void UGA_Scan::OnScanComplete()
{
    if (!SavedActorInfo || !SavedActorInfo->AbilitySystemComponent.Get())
        return;
 
    // Ability 종료 → ActivationOwnedTags 자동 제거
    EndAbility(SavedHandle, SavedActorInfo, SavedActivationInfo, falsefalse);
}
 
void UGA_Scan::OnCooldownComplete()
{
    if (!SavedActorInfo || !SavedActorInfo->AbilitySystemComponent.Get())
        return;
 
    FGameplayTagContainer Tags;
    SavedActorInfo->AbilitySystemComponent->GetOwnedGameplayTags(Tags);
    UE_LOG(LogTemp, Log, TEXT("[GA_Scan] Cooldown expired, Tags = %s"), *Tags.ToString());
}
 
void UGA_Scan::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
    Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}
 
void UGA_Scan::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
    // 쿨다운 GE 적용 및 태그 확인
    if (UGameplayEffect* GE = GetCooldownGameplayEffect())
    {
        // Spec 생성
        FGameplayEffectSpecHandle Spec =
            MakeOutgoingGameplayEffectSpec(GE->GetClass(), GetAbilityLevel());
 
        Spec.Data.Get()->DynamicGrantedTags.AddTag(ScanCooldownTag);
 
        // SetByCaller 매그니튜드 설정
        FDataRegistryId Id{ FDataRegistryType(TEXT("Ability")), ScanAbilityTag.GetTagName() };
        if (const FAbilityData* Data = UDataRegistrySubsystem::Get()->GetCachedItem<FAbilityData>(Id))
        {
            Spec.Data.Get()->SetSetByCallerMagnitude(
                FGameplayTag::RequestGameplayTag(TEXT("Data.Cooldown")),
                Data->BaseCooldown);
        }
 
        ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, Spec);
 
        // 태그 확인
        FGameplayTagContainer Tags;
        ActorInfo->AbilitySystemComponent->GetOwnedGameplayTags(Tags);
        UE_LOG(LogTemp, Log, TEXT("[GA_Scan] After Cooldown GE, Tags = %s"), *Tags.ToString());
    }
}
 
cs

위의 49번째 줄 if (Avatar && ScanPlayerBPClass && !IsValid(CachedScanActor)) 에서 !IsValid(CachedScanActor 이 조건 때문에, Scan이 다시 진행되지 않는 문제가 있었다.
해당 조건이 모두 충족돼야 아래 조건문이 진행되는데, 한 번 Spawn되면 뒤의 조건이 만족하지 않게 되면서 아예 실행이 안돼버렸던 것이다.