LyraLog15 物品系统

物品定义

有物品,才能把武器弄出来

先规划一下。

物品系统,lyra叫他库存 Inventory

首先一个物品要有它自己的配置,UE的配置喜欢起名 Definition

UCLASS(MinimalAPI, DefaultToInstanced, EditInlineNew, Abstract)
class UEqZeroInventoryItemFragment : public UObject
{
    GENERATED_BODY()

public:
    virtual void OnInstanceCreated(UEqZeroInventoryItemInstance* Instance) const {}
};

/**
 * UEqZeroInventoryItemDefinition
 */
UCLASS(Blueprintable, Const, Abstract)
class UEqZeroInventoryItemDefinition : public UObject
{
    GENERATED_BODY()

public:
    UEqZeroInventoryItemDefinition(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Display)
    FText DisplayName;

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Display, Instanced)
    TArray<TObjectPtr<UEqZeroInventoryItemFragment>> Fragments;

public:
    const UEqZeroInventoryItemFragment* FindFragmentByClass(TSubclassOf<UEqZeroInventoryItemFragment> FragmentClass) const;
};

需要一个名字和片段。

比如有一个装备的片段,那么这个物品就是一个装备的物品。

UEqZeroInventoryItemFragment 这个就是片段,基类啥也没有。

片段的子类

UCLASS()
class UInventoryFragment_EquippableItem : public UEqZeroInventoryItemFragment
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, Category=EqZero)
    TSubclassOf<UEqZeroEquipmentDefinition> EquipmentDefinition;
};

装备,关联的是装备的定义

UCLASS()
class UInventoryFragment_PickupIcon : public UEqZeroInventoryItemFragment
{
    GENERATED_BODY()

public:
    UInventoryFragment_PickupIcon();

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance)
    TObjectPtr<USkeletalMesh> SkeletalMesh;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance)
    FText DisplayName;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance)
    FLinearColor PadColor;
};

一个能捡起来的东西,包括skeletal mesh,显示的名字和颜色。这是干嘛的我忘了。。。

UCLASS()
class UInventoryFragment_QuickBarIcon : public UEqZeroInventoryItemFragment
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance)
    FSlateBrush Brush;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance)
    FSlateBrush AmmoBrush;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance)
    FText DisplayNameWhenEquipped;
};

另一个物品,QuickBar 记得是右下角,手枪,步枪哪个武器切换那里的装备。代表装备栏的装备?

UCLASS()
class UInventoryFragment_SetStats : public UEqZeroInventoryItemFragment
{
    GENERATED_BODY()

protected:
    UPROPERTY(EditDefaultsOnly, Category=Equipment)
    TMap<FGameplayTag, int32> InitialItemStats;

public:
    virtual void OnInstanceCreated(UEqZeroInventoryItemInstance* Instance) const override;

    int32 GetItemStatByTag(FGameplayTag Tag) const;
};

这个更通用了,直接一个TAG=>int32。直接拿来描述子弹,例如子弹30发。

物品实例

/**
 * UEqZeroInventoryItemInstance
 */
UCLASS(BlueprintType)
class UEqZeroInventoryItemInstance : public UObject
{
    GENERATED_BODY()

private:
    UPROPERTY(Replicated)
    FGameplayTagStackContainer StatTags;

    UPROPERTY(Replicated)
    TSubclassOf<UEqZeroInventoryItemDefinition> ItemDef;
};

前面的定义相当于配置表,在角色身上应该有具体的实例物品。包括一个定义,一个tag=>数量的维护。

这个tag到数量能维护很多信息。例如物品的数量,除此之外都是一些辅助接口。和IRIS目前我们暂不关心。

库存组件

UCLASS(MinimalAPI, BlueprintType)
class UEqZeroInventoryManagerComponent : public UActorComponent
{
    GENERATED_BODY()

private:
    UPROPERTY(Replicated)
    FEqZeroInventoryList InventoryList;
};

InventoryList 是一个 Fast Array

USTRUCT(BlueprintType)
struct FEqZeroInventoryEntry : public FFastArraySerializerItem
{
    GENERATED_BODY()
private:
    UPROPERTY()
    TObjectPtr<UEqZeroInventoryItemInstance> Instance = nullptr;

    UPROPERTY()
    int32 StackCount = 0;

    UPROPERTY(NotReplicated)
    int32 LastObservedCount = INDEX_NONE;
};

USTRUCT(BlueprintType)
struct FEqZeroInventoryList : public FFastArraySerializer
{
    GENERATED_BODY()

private:
    UPROPERTY()
    TArray<FEqZeroInventoryEntry> Entries;

    UPROPERTY(NotReplicated)
    TObjectPtr<UActorComponent> OwnerComponent;
};

简单理解组件上有一个物品数组,数组的每一项是物品实例和数量的组合

我们看看相关接口

    //~FFastArraySerializer contract
    void PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize);
    void PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize);
    void PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize);
    //~End of FFastArraySerializer contract

物品变化,广播物品数量变化,并更新 LastObservedCount。

需要关注的地方自己会监听这个消息。

    UEqZeroInventoryItemInstance* AddEntry(const TSubclassOf<UEqZeroInventoryItemDefinition> &ItemClass, int32 StackCount);
    void AddEntry(UEqZeroInventoryItemInstance* Instance);
    void RemoveEntry(UEqZeroInventoryItemInstance* Instance);

几个操作,

根据物品定义添加对应物品 若干。

void AddEntry(UEqZeroInventoryItemInstance* Instance);

  • 未实现

void RemoveEntry(UEqZeroInventoryItemInstance* Instance);

  • 找到这个物品实例,然后全部删除

然后就是 UEqZeroInventoryManagerComponent 这个组件类。都是对于这个Array的CURD。但是这里有个网络同步的问题

物品网络同步

UE 的网络系统(Replication System)只认 AActor

  • 如果你有一个 AActor,引擎会自动帮你同步。
  • 如果你在 Actor 里有个 int32 Health,引擎也会通过 DOREPLIFETIME 同步。

痛点:如果你有一个 UObject* MyItem 指针:

  • 引擎只能同步这个指针的地址(NetGUID)。
  • 引擎不会自动把这个对象内部的数据(比如 Item 的 Level, XP)打包发给客户端。
  • 如果客户端不存在这个对象,指针同步过来就是 NULL。

具体流程:

【1】

由于我们处理的是这个指针,

USTRUCT(BlueprintType)
struct FEqZeroInventoryEntry : public FFastArraySerializerItem
{
    UPROPERTY()
    TObjectPtr<UEqZeroInventoryItemInstance> Instance = nullptr;
}

所以这个UObject要实现 IsSupportedForNetworking

/**
 * UEqZeroInventoryItemInstance
 */
UCLASS(BlueprintType)
class UEqZeroInventoryItemInstance : public UObject
{
    GENERATED_BODY()

    //~UObject interface
    virtual bool IsSupportedForNetworking() const override { return true; }
    //~End of UObject interface
}

【2】

实现UObject类的这个函数

void UEqZeroInventoryItemInstance::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(ThisClass, StatTags);
    DOREPLIFETIME(ThisClass, ItemDef);
}

ps: 只要不是纯IRIS都是必须的

这里有个非必需的宏包裹的函数

#if UE_WITH_IRIS
void UEqZeroInventoryItemInstance::RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags)
{
    using namespace UE::Net;

    // Build descriptors and allocate PropertyReplicationFragments for this object
    // 为该对象构建描述符并分配属性复制片段
    FReplicationFragmentUtil::CreateAndRegisterFragmentsForObject(this, Context, RegistrationFlags);
}
#endif // UE_WITH_IRIS
  • 传统方式(上面的 GetLifetimeReplicatedProps)是通过反射遍历属性,效率在海量物体时有瓶颈
  • Iris 方式:RegisterReplicationFragments 将状态预先分块(Fragments),以获得极其高效的并发序列化性能。

在组件中实现

bool UEqZeroInventoryManagerComponent::ReplicateSubobjects(UActorChannel* Channel, class FOutBunch* Bunch, FReplicationFlags* RepFlags)
{
    bool WroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);

    for (FEqZeroInventoryEntry& Entry : InventoryList.Entries)
    {
        UEqZeroInventoryItemInstance* Instance = Entry.Instance;

        if (Instance && IsValid(Instance))
        {
            WroteSomething |= Channel->ReplicateSubobject(Instance, *Bunch, *RepFlags);
        }
    }

    return WroteSomething;
}

这是把每一个Fast Array 的 Item序列化的方法。

网络复制的时候就会直接跑进 ReplicateSubobjects 这个写法。

【这是老的写法】

但是,这种写法,每次网络更新(Tick)都要遍历整个数组。

所以:

UE5 引入了“注册列表(Registered SubObject List)”模式:

只需要在对象创建时调用一次 AddReplicatedSubObject(Item),销毁时调用 RemoveReplicatedSubObject。

引擎内部维护一个列表。网络更新时,引擎自己去检查这个列表里的对象状态,不需要你写那个 ReplicateSubobjects 函数了(或者说引擎替你做了)。

UEqZeroInventoryItemInstance* UEqZeroInventoryManagerComponent::AddItemDefinition(TSubclassOf<UEqZeroInventoryItemDefinition> ItemDef, int32 StackCount)
{
    UEqZeroInventoryItemInstance* Result = nullptr;
    if (ItemDef != nullptr)
    {
        Result = InventoryList.AddEntry(ItemDef, StackCount);

        if (IsUsingRegisteredSubObjectList() && IsReadyForReplication() && Result)
        {
            AddReplicatedSubObject(Result);
        }
    }
    return Result;
}

void UEqZeroInventoryManagerComponent::AddItemInstance(UEqZeroInventoryItemInstance* ItemInstance)
{
    InventoryList.AddEntry(ItemInstance);
    if (IsUsingRegisteredSubObjectList() && IsReadyForReplication() && ItemInstance)
    {
        AddReplicatedSubObject(ItemInstance);
    }
}

void UEqZeroInventoryManagerComponent::RemoveItemInstance(UEqZeroInventoryItemInstance* ItemInstance)
{
    InventoryList.RemoveEntry(ItemInstance);

    if (ItemInstance && IsUsingRegisteredSubObjectList())
    {
        RemoveReplicatedSubObject(ItemInstance);
    }
}

两种方式的使用和切换:

  • ReadyForReplication:这是组件初始化网络时的回调。在这里根据 IsUsingRegisteredSubObjectList()(通常由项目设置决定是否开启)来决定是否把现有的物品注册进列表。
  • 如果开启了这个特性,使用 AddReplicatedSubObject。
  • 如果没开启,可能回退到传统的 ReplicateSubobjects 循环。

这两种方式切换的关键是

bReplicateUsingRegisteredSubObjectList = true;

可拾取接口

一个交互物Actor,他也同时可以被拾取。

身上挂一个这个,这里可以通过 物品定义+数量的 方式提供给 交互后转换为物品的信息

USTRUCT(BlueprintType)
struct FInventoryPickup
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    TArray<FPickupInstance> Instances;

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    TArray<FPickupTemplate> Templates;
};

这个Actor通过实现可拾取接口,来提供 FInventoryPickup 给外部系统

class IEqZeroPickupable
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintCallable)
    virtual FInventoryPickup GetPickupInventory() const = 0;
};

然后可拾取就OK了

其余还有一个简单的辅助接口

这个是用在那里的呢?

有一个实例地图,里面有一个交互物Actor,他同时实现了交互和可拾取的接口。

交互

L_InventoryTestMap

/ShooterExplorer/Maps/L_InventoryTestMap.L_InventoryTestMap

物品测试地图,场景里的方块是 /ShooterExplorer/Items/B_InteractableRock.B_InteractableRock 是

UCLASS(Abstract, Blueprintable)
class AEqZeroWorldCollectable : public AActor, public IInteractableTarget, public IPickupable
{
protected:
    UPROPERTY(EditAnywhere)
    FInteractionOption Option;

    UPROPERTY(EditAnywhere)
    FInventoryPickup StaticInventory;
};

FInventoryPickup 前面提过,描述了交互成功后生成的是什么物品

FInteractionOption 描述了 交互的本身,例如交互的时候UI是什么,交互技能是什么

这个石头的配置,交互的时候显示文本 Collect,交互的时候获得 GA_..._Collect技能

跑起来是这样。按G获取物品。

I 打开背包,能看到刚刚获得的物品

M 打开地图,但是地图没东西。

B 表情技能,跳舞。

P 在目标位置创建一个检测的球

===

/ShooterExplorer/Input/Abilities/AbilitySet_InventoryTest.AbilitySet_InventoryTest

这个配置能找到四个技能

GA_Interact,GA_ToggleMap,GA_ToggleInventory,GA_ToggleMarkerInWorld

查一下快捷键【G, M, I, P】

找到这里技能

/ShooterExplorer/Input/Abilities/GA_Interact.GA_Interact

/ShooterExplorer/Interact/GA_Interaction_Collect.GA_Interaction_Collect

GA_Interact 一直是激活中。

GA_Interaction_Collect 未激活状态。查看引用是那个交互的石头上配置的。

GA_Interact

从GA_Interact 蓝图开始

ActivateAbility => Look For Interactables => Interact Press Scan

偷偷看下父类,重点OnSpawn,其他InstancedPerActor,LocalPredicted

ULyraGameplayAbility_Interact::ULyraGameplayAbility_Interact(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    ActivationPolicy = ELyraAbilityActivationPolicy::OnSpawn;
    InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
    NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
}

UAbilityTask_GrantNearbyInteraction

UAbilityTask_GrantNearbyInteraction 激活的时候服务器启动这个异步任务

void UEqZeroGameplayAbility_Interact::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
    Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

    UAbilitySystemComponent* AbilitySystem = GetAbilitySystemComponentFromActorInfo();
    if (AbilitySystem && AbilitySystem->GetOwnerRole() == ROLE_Authority)
    {
        UAbilityTask_GrantNearbyInteraction* Task = UAbilityTask_GrantNearbyInteraction::GrantAbilitiesForNearbyInteractors(this, InteractionScanRange, InteractionScanRate);
        Task->ReadyForActivation();
    }
}

可以看得出来,这个异步任务以一定的频率执行检测

void UAbilityTask_GrantNearbyInteraction::Activate()
{
    SetWaitingOnAvatar();

    UWorld* World = GetWorld();
    World->GetTimerManager().SetTimer(QueryTimerHandle, this, &ThisClass::QueryInteractables, InteractionScanRate, true);
}

每次检测玩家一定范围内的交互物,查看是否能赋予玩家这个交互技能

void UAbilityTask_GrantNearbyInteraction::QueryInteractables()
{
    UWorld* World = GetWorld();
    AActor* ActorOwner = GetAvatarActor();

    if (World && ActorOwner)
    {
        FCollisionQueryParams Params(SCENE_QUERY_STAT(UAbilityTask_GrantNearbyInteraction), false);

        // 球形碰撞检测
        TArray<FOverlapResult> OverlapResults;
        World->OverlapMultiByChannel(OUT OverlapResults, ActorOwner->GetActorLocation(),
            FQuat::Identity, EqZero_TraceChannel_Interaction, FCollisionShape::MakeSphere(InteractionScanRange), Params);

        if (OverlapResults.Num() > 0)
        {
            // 从重叠结果中获取所有的可交互目标
            TArray<TScriptInterface<IInteractableTarget>> InteractableTargets;
            UInteractionStatics::AppendInteractableTargetsFromOverlapResults(OverlapResults, OUT InteractableTargets);

            FInteractionQuery InteractionQuery;
            InteractionQuery.RequestingAvatar = ActorOwner;
            InteractionQuery.RequestingController = Cast<AController>(ActorOwner->GetOwner());

            // 遍历可交互的目标
            TArray<FInteractionOption> Options;
            for (TScriptInterface<IInteractableTarget>& InteractiveTarget : InteractableTargets)
            {
                // InteractiveTarget 就是场景中实现了可交互接口的 Actor,从中收集 Options
                FInteractionOptionBuilder InteractionBuilder(InteractiveTarget, Options);
                InteractiveTarget->GatherInteractionOptions(InteractionQuery, InteractionBuilder);
            }

            // 根据收集的Options检查是否有交互限制
            for (FInteractionOption& Option : Options)
            {
                if (Option.InteractionAbilityToGrant)
                {
                    // 赋予玩家这个对应的交互技能
                    FObjectKey ObjectKey(Option.InteractionAbilityToGrant);
                    if (!InteractionAbilityCache.Find(ObjectKey))
                    {
                        FGameplayAbilitySpec Spec(Option.InteractionAbilityToGrant, 1, INDEX_NONE, this);
                        FGameplayAbilitySpecHandle Handle = AbilitySystemComponent->GiveAbility(Spec);
                        InteractionAbilityCache.Add(ObjectKey, Handle);
                    }
                }
            }
        }
    }
}

UAbilityTask_WaitForInteractableTargets_SingleLineTrace

另外,在蓝图中这个Look For Interactables

激活的时候调用 UAbilityTask_WaitForInteractableTargets_SingleLineTrace

父类是 UAbilityTask_WaitForInteractableTargets

void UAbilityTask_WaitForInteractableTargets_SingleLineTrace::Activate()
{
    SetWaitingOnAvatar();

    UWorld* World = GetWorld();
    World->GetTimerManager().SetTimer(TimerHandle, this, &ThisClass::PerformTrace, InteractionScanRate, true);
}

主要还是tick执行 PerformTrace

void UAbilityTask_WaitForInteractableTargets_SingleLineTrace::PerformTrace()
{
    AActor* AvatarActor = Ability->GetCurrentActorInfo()->AvatarActor.Get();
    if (!AvatarActor)
    {
        return;
    }

    UWorld* World = GetWorld();

    TArray<AActor*> ActorsToIgnore;
    ActorsToIgnore.Add(AvatarActor);

    const bool bTraceComplex = false;
    FCollisionQueryParams Params(SCENE_QUERY_STAT(UAbilityTask_WaitForInteractableTargets_SingleLineTrace), bTraceComplex);
    Params.AddIgnoredActors(ActorsToIgnore);

    /*
     * 利用玩家摄像机的朝向,结合 TraceStart 和 InteractionScanRange(交互距离)\
     * 输出 TraceEnd
     */
    FVector TraceStart = StartLocation.GetTargetingTransform().GetLocation();
    FVector TraceEnd;
    AimWithPlayerController(AvatarActor, Params, TraceStart, InteractionScanRange, OUT TraceEnd);

    // 计算好的 Start 和 End 进行真正的物理射线检测
    FHitResult OutHitResult;
    LineTrace(OutHitResult, World, TraceStart, TraceEnd, TraceProfile.Name, Params);

    // 从射线检测的结果中获取所有的可交互目标
    TArray<TScriptInterface<IInteractableTarget>> InteractableTargets;
    UInteractionStatics::AppendInteractableTargetsFromHitResult(OutHitResult, InteractableTargets);

    // 更新交互选项
    UpdateInteractableOptions(InteractionQuery, InteractableTargets);
}
void UAbilityTask_WaitForInteractableTargets::UpdateInteractableOptions(const FInteractionQuery& InteractQuery, const TArray<TScriptInterface<IInteractableTarget>>& InteractableTargets)
{
    TArray<FInteractionOption> NewOptions;

    /*
     * 遍历所有交互物,从中获取能激活技能的Options
     * 1. 如果Option里直接有技能了,就用这个技能
     * 2. 如果Option里没有技能,但是有技能类,就先从玩家身上赋予这个技能,再用它
     * 3. 如果Option里没有技能也没有技能类,那这个Option就没法用了,丢弃
     * 4. 最后根据玩家当前的状态(例如,是否在CD中,是否满足激活条件等)来过滤掉一些Option
     */
    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.Get();
                    Option.TargetInteractionAbilityHandle = InteractionAbilitySpec->Handle;
                }
            }

            if (InteractionAbilitySpec)
            {
                if (InteractionAbilitySpec->Ability->CanActivateAbility(InteractionAbilitySpec->Handle, AbilitySystemComponent->AbilityActorInfo.Get()))
                {
                    NewOptions.Add(Option);
                }
            }
        }
    }

    // 检查新旧 Options 是否有变化,如果有变化就广播事件
    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);
    }
}

如果这个事件广播了,蓝图中就会调用 UpdateInteractions 这里面主要是一些UI上的更新

如果有案件激活就会触发 UEqZeroGameplayAbility_Interact::TriggerInteraction

这里面会具体的激活这个技能。

/ShooterExplorer/Maps/L_InteractionTestMap.L_InteractionTestMap

另一个地图怎么样呢?emmm 体验配置是一样的

区别貌似是,椅子交互技能无法拾取这一个区别。

获得物品

好像没看到获得的物品是怎么样的

/ShooterExplorer/Interact/GA_Interaction_Sit.GA_Interaction_Sit

/ShooterExplorer/Interact/GA_Interaction_Collect.GA_Interaction_Collect

这是需要获得这个交互物的具体技能,其中sit我们试过了拿不到。我们先看看Collect

  • GA_Interaction_Collect

激活技能的时候

关闭物体碰撞,设置生命周期3s

激活一个Cue做效果,关闭碰撞,然后物体玩角色运动,缩小获得的效果。

如果权威:

调用加物品接口,播放蒙太奇

  • GA_Interaction_Sit

有什么不同的呢?只是蓝图里面没写加物品那一句。

交互系统总结

通过体验配置的技能集合,为角色添加了 GA_Interact 这个技能。

这个技能会启动一个ability task - UAbilityTask_GrantNearbyInteraction

这个task会一直做范围内的碰撞检测,检测到交互物,就从交互物上获取交互技能加到角色身上。

角色激活这个交互技能,就可以做一些自定义逻辑,例如可以获得物品

上一篇
下一篇