라이라 인터랙션 시스템

라이라 게임 샘플의 라이라 인터랙션 시스템 개요입니다.

라이라 인터랙션 시스템

라이라(Lyra)는 플레이어가 게임 내의 오브젝트와 상호작용하는 방식 및 오브젝트가 플레이어와 상호작용하는 방식 간의 인과관계를 확립하는 인터랙션 인터페이스/IInterface를 자체 게임플레이 어빌리티/UGameplayAbility를 통해 사용합니다.

LyraGameplayAbility_Interact 클래스를 사용하면 인터랙션을 어떻게 호출할지에 관한 로직을 관리할 수 있습니다.

ULyraGameplayAbility_Interact.h

    #pragma once
    #include "CoreMinimal.h"
    #include "AbilitySystem/Abilities/LyraGameplayAbility.h"
    #include "Interaction/InteractionQuery.h"
    #include "Interaction/IInteractableTarget.h"
    #include "LyraGameplayAbility_Interact.generated.h"

    class FIndicatorDescriptor;
    /**

     * ULyraGameplayAbility_Interact
     *
     * 캐릭터 상호작용에 사용되는 게임플레이 어빌리티
     */
    UCLASS(Abstract)
    class ULyraGameplayAbility_Interact : public ULyraGameplayAbility
    {
        GENERATED_BODY()
    public:

        ULyraGameplayAbility_Interact(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
        virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;

        UFUNCTION(BlueprintCallable)
        void UpdateInteractions(const TArray<FInteractionOption>& InteractiveOptions);

        UFUNCTION(BlueprintCallable)
        void TriggerInteraction();

    protected:

        UPROPERTY(BlueprintReadWrite)
        TArray<FInteractionOption> CurrentOptions;

        TArray<TSharedRef<FIndicatorDescriptor>> Indicators;

    protected:

        UPROPERTY(EditDefaultsOnly)
        float InteractionScanRate = 0.1f;

        UPROPERTY(EditDefaultsOnly)
        float InteractionScanRange = 500;

        UPROPERTY(EditDefaultsOnly)
        TSoftClassPtr<UUserWidget> DefaultInteractionWidgetClass;

    };

AbilityTask_WaitForInteractableTargets_SingleLineTrace 는 라인 트레이스를 수행하고 인터페이스를 구현하는 액터에 적중할 때까지 반복 타이머에서 대기하는 게임플레이 어빌리티 태스크(Ability Task)입니다.

예를 들어,

체력이 낮은 LyraPawnActor를 컨트롤하는 플레이어가 폰을 조종하여 수집할 수 있는 체력 아이템을 픽업한다고 합시다. 플레이어가 조준선을 수집 아이템에 맞추고 '사용/상호작용' 키를 누르면, 폰에서 라인 트레이스(Line Trace)가 발사됩니다. 트레이스가 아이템에 적중하면 아이템에 구현된 인터랙션 인터페이스가 로직을 처리하여 플레이어의 체력을 최대치로 복원합니다.

인터랙션 어빌리티 태스크

UAbilityTask_WaitForInteractableTargets 을 사용하면 상호작용할 수 있는 타깃의 새로운 트레이싱 함수를 만듭니다.

예를 들어,

LyraPawnActor를 컨트롤하는 플레이어가 문을 열려고 다가간다고 합시다. 플레이어가 조준선을 문에 맞추고 '사용' 키를 누르면 문을 '잠금해제/잠금'하는 옵션이나 문을 열려고 시도하는 옵션이 있는 메뉴가 나타납니다.

언리얼의 라인 트레이스에 대한 추가 정보는 트레이싱(Tracing)을 참고하세요.

UAbilityTask_WaitForInteractableTargets.h

    #pragma once
    #include "CoreMinimal.h"
    #include "UObject/ObjectMacros.h"
    #include "Abilities/Tasks/AbilityTask.h"
    #include "Engine/EngineTypes.h"
    #include "CollisionQueryParams.h"
    #include "WorldCollision.h"
    #include "Engine/CollisionProfile.h"
    #include "Abilities/GameplayAbilityTargetDataFilter.h"
    #include "Interaction/InteractionOption.h"
    #include "Interaction/InteractionQuery.h"
    #include "Interaction/IInteractableTarget.h"
    #include "AbilityTask_WaitForInteractableTargets.generated.h"

    class AActor;
    class UPrimitiveComponent;
    class UGameplayAbility;

    DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FInteractableObjectsChangedEvent, const TArray<FInteractionOption>&, InteractableOptions);

    UCLASS(Abstract)
    class UAbilityTask_WaitForInteractableTargets : public UAbilityTask
    {
        GENERATED_UCLASS_BODY()

    public:

        UPROPERTY(BlueprintAssignable)
        FInteractableObjectsChangedEvent InteractableObjectsChanged;

    protected:

        static void LineTrace(FHitResult& OutHitResult, const UWorld* World, const FVector& Start, const FVector& End, FName ProfileName, const FCollisionQueryParams Params);
        void AimWithPlayerController(const AActor* InSourceActor, FCollisionQueryParams Params, const FVector& TraceStart, float MaxRange, FVector& OutTraceEnd, bool bIgnorePitch = false) const;
        static bool ClipCameraRayToAbilityRange(FVector CameraLocation, FVector CameraDirection, FVector AbilityCenter, float AbilityRange, FVector& ClippedPosition);
        void UpdateInteractableOptions(const FInteractionQuery& InteractQuery, const TArray<TScriptInterface<IInteractableTarget>>& InteractableTargets);
        FCollisionProfileName TraceProfile;

        // 트레이스가 조준 피치에 영향을 미치는지
        bool bTraceAffectsAimPitch = true;

        TArray<FInteractionOption> CurrentOptions;

    };

트레이싱에 대해 선택된 어빌리티 태스크(AbilityTask)는 FInteractionQuery 구조체에서 상호작용할 수 있는 타깃 세트를 제공합니다.

struct FInteractionQuery

    #pragma once
    #include "CoreMinimal.h"
    #include "Abilities/GameplayAbility.h"
    #include "InteractionQuery.generated.h"

    /**  */
    USTRUCT(BlueprintType)
    struct FInteractionQuery
    {

        GENERATED_BODY()

    public:
        /** 요청하는 폰입니다. */
        UPROPERTY(BlueprintReadWrite)
        TWeakObjectPtr<AActor> RequestingAvatar;

        /** 컨트롤러를 지정하는 기능을 제공합니다. 요청하는 아바타의 오너와 맞출 필요는 없습니다. */
        UPROPERTY(BlueprintReadWrite)
        TWeakObjectPtr<AController> RequestingController;

        /** 인터랙션에 필요한 추가 데이터를 제공하는 일반적인 UObject입니다. */
        UPROPERTY(BlueprintReadWrite)
        TWeakObjectPtr<UObject> OptionalObjectData;
    };

UAbilityTask_WaitForInteractableTargets::UpdateInteractableOptions 함수:

    void UAbilityTask_WaitForInteractableTargets::UpdateInteractableOptions(const FInteractionQuery& InteractQuery, const TArray<TScriptInterface<IInteractableTarget>>& InteractableTargets)
    {

        TArray<FInteractionOption> NewOptions;

        for (const TScriptInterface<IInteractableTarget>& InteractiveTarget : InteractableTargets)

        {

            TArray<FInteractionOption> TempOptions;

            FInteractionOptionBuilder InteractionBuilder(InteractiveTarget, TempOptions);

            InteractiveTarget->GatherInteractionOptions(InteractQuery, InteractionBuilder);

            for (FInteractionOption& Option : TempOptions)

            {

                FGameplayAbilitySpec* InteractionAbilitySpec = nullptr;

                // 핸들과 타깃 어빌리티 시스템이 있으면 타깃의 어빌리티를 트리거합니다.

                if (Option.TargetAbilitySystem && Option.TargetInteractionAbilityHandle.IsValid())

                {

                    // 사양을 찾습니다

                    InteractionAbilitySpec = Option.TargetAbilitySystem->FindAbilitySpecFromHandle(Option.TargetInteractionAbilityHandle);

                }

                // 인터랙션 어빌리티가 있으면 직접 활성화합니다.

                else if (Option.InteractionAbilityToGrant)

                {

                    // 사양을 찾습니다

                    InteractionAbilitySpec = AbilitySystemComponent->FindAbilitySpecFromClass(Option.InteractionAbilityToGrant);

                    if (InteractionAbilitySpec)

                    {

                        // 옵션을 업데이트합니다

                        Option.TargetAbilitySystem = AbilitySystemComponent;

                        Option.TargetInteractionAbilityHandle = InteractionAbilitySpec->Handle;

                    }

                }

                if (InteractionAbilitySpec)

                {

                    // 어떤 이유로든 현재 활성화할 수 없는 옵션에 필터를 적용합니다.

                    if (InteractionAbilitySpec->Ability->CanActivateAbility(InteractionAbilitySpec->Handle, AbilitySystemComponent->AbilityActorInfo.Get()))

                    {

                        NewOptions.Add(Option);

                    }

                }

            }

        }

        bool bOptionsChanged = false;

        if (NewOptions.Num() == CurrentOptions.Num())

        {

            NewOptions.Sort();

            for (int OptionIndex = 0; OptionIndex < NewOptions.Num(); OptionIndex++)

            {

                const FInteractionOption& NewOption = NewOptions[OptionIndex];

                const FInteractionOption& CurrentOption = CurrentOptions[OptionIndex];

                if (NewOption != CurrentOption)

                {

                    bOptionsChanged = true;

                    break;

                }

            }

        }

        else

        {

            bOptionsChanged = true;

        }

        if (bOptionsChanged)

        {

            CurrentOptions = NewOptions;

            InteractableObjectsChanged.Broadcast(CurrentOptions);

        }

    }

이렇게 하면 상호작용할 수 있는 타깃의 IInteractableTarget::GatherInteractionOptions 을 호출합니다.

    virtual void GatherInteractionOptions(const FInteractionQuery& InteractQuery, FInteractionOptionBuilder& OptionBuilder) = 0;

상호작용할 수 있는 오브젝트 세트를 업데이트하면, 플레이어가 상호작용할 오브젝트에 초점을 맞추고 해당 오브젝트와 상호작용하겠다는 입력을 보낼 때 인터랙션 어빌리티(GA_Interact)가 TriggerInteraction 함수를 호출합니다.

현재 옵션을 호출하면 두 가지 함수로 인터랙션이 발생할 수 있습니다. 첫 번째는 FInteractionOption::InteractionAbilityToGrant 함수로 플레이어의 어빌리티 시스템 컴포넌트(Ability System Component)로 연결되는 어빌리티를 제공합니다. 무기 픽업 액터(Weapon Pickup Actor)와 같이 간단한 로직에는 이 함수를 사용하는 것이 좋습니다.

하지만 자체 어빌리티 시스템 컴포넌트로 복잡한 로직을 처리하는 오브젝트와 상호작용하는 경우, FInteractionOption::TargetAbilitySystemFInteractionOption::TargetInteractionHandle 함수를 호출할 수 있습니다. 이렇게 하면 라이라 캐릭터(아바타)의 어빌리티 시스템 컴포넌트에 있는 어빌리티를 호출하는 대신 상호작용할 수 있는 오브젝트의 어빌리티를 호출합니다.

인터랙션 기능 FInteractionOption::InteractionAbilityToGrantULyraGameplayAbility_Interact 인터랙션 어빌리티의 베이스 클래스에서 상속받습니다. 이 인터랙션 어빌리티는 태스크 함수 AbilityTask_GrantNearbyInteraction 을 범위 루프와 타이머로 실행하여 상호작용을 시도하기 전에 인접한 어빌리티를 수집하고 캐릭터에게 제공합니다. InteractionScanRate 를 높여 InteractionRange 보다 반경을 크게 할 수 있으며, 그러지 않으면 리플리케이션이 어빌리티를 클라이언트에 충분히 빠르게 제공하지 않습니다.

어빌리티는

[게임플레이 태그(Gameplay Tag)](programming-and-scripting/interactive-framework/Tags)
FInteractionOption::InteractionEventTag 이벤트를 통해 호출됩니다. 이 태그는 어빌리티의 트리거에 대응해야 합니다. 예를 들어 GA_Collect_Interaction 은 인터랙션 옵션에 설정된 Ability.Type.Interact.Collect 이벤트가 전송되었을 때 트리거됩니다.

InteractInterface.png

GA_Collect_Interaction 은 단 한 가지 인터랙션만 나타내는데, 바로 바닥에 있는 오브젝트를 집어 인벤토리에 추가할 수 있는 어빌리티입니다. 상상력에는 한계가 없습니다. 땅에 있는 사과를 먹으면 플레이어의 체력을 보충하는 어빌리티를 만들거나, 문을 열거나, 비히클에 타거나, 상자를 여는 어빌리티를 만들 수 있습니다.

이처럼 분리된 행동은 중앙 수동 인터랙션 스캐너에서 다양한 인터랙션을 제공합니다.

중요 라이라 인터랙션 용어

상호작용 가능한 타깃(InteractableTarget) - IInteractableTarget 인터페이스를 구현하는 액터나 컴포넌트로, 월드의 어떤 오브젝트가 상호작용 가능한지 결정합니다.

상호작용 옵션(InteractionOption) - '행동 유도성' 또는 '옵션'으로, 예를 들어 사과는 '수집'할 수도 있고 '사용'할 수도 있습니다.

상호작용 유발자(InteractionInstigator) - 인터랙션을 개시하는 폰(LyraPawnActor)입니다. 이 폰은 옵션과 표시 방식을 더 자세히 커스터마이징할 수 있는 IInteractionInstigator 인터페이스를 구현할 수도, 구현하지 않을 수도 있습니다.