LearnARPG-基于UE4 AbilitySystem的动作游戏Demo

LearnARPG-基于UE4 AbilitySystem的动作游戏Demo

动画

代码链接:RootEntertainment/LearnARPG

Demo是一个基于UE4 AbilitySystem的动作类游戏。主要设计了数据驱动的武器和技能框架,背包库存装备系统,全局动态数据层。在动画方面学习了ALS的动画系统,主要包括分层混合提供不同武器的Overlap能力和蒙太奇,六方向状态机和基于步频步幅的走跑混合等。

技能系统的设计方面参考了ActionRPG和GASShooter。

数据驱动设计

全局基础配置

利用DataAsset和单例读取全局配置为角色初始化装备槽。

image-20250324010536767

物品资产

物品的信息使用PrimaryAssetData设计,由AssetManager负责加载,后续技能、UI等系统都会依据其Item对象进行显示等逻辑。

image-20250324010747948

全局动态数据

利用Guid为全局的物品构造动态数据。所有的物品数据存放在AssetManager单例中。

USTRUCT(BlueprintType)
struct LEARNARPG_API FARPGItemData
{
    GENERATED_BODY()

    FARPGItemData():Level(0), Count(0)
    {}
    
    FARPGItemData(const FGuid& InGID, int InLevel, const FPrimaryAssetId& InAssetId, int InCount = 1, UARPGInventory* InInventory = nullptr, FName InSlotName = NAME_None)
        : ItemAssetId(InAssetId), GID(InGID), Level(InLevel), Count(InCount), Inventory(InInventory), CurrentSlot(InSlotName)
    {}
    
    UPROPERTY(BlueprintReadWrite)
    FPrimaryAssetId ItemAssetId;
    UPROPERTY(BlueprintReadWrite)
    FGuid GID;
    
    //动态数据
    UPROPERTY(BlueprintReadWrite)
    int Level;
    UPROPERTY(BlueprintReadWrite)
    int Count;
    UPROPERTY(BlueprintReadWrite)
    TWeakObjectPtr<UARPGInventory> Inventory;
    UPROPERTY(BlueprintReadWrite)
    FName CurrentSlot;
}

为每个Character设计Inventory存放拥有的物品GID,装备槽等情况。通过InventorySource接口的设计让AI和玩家可以拥有不同的Inventory并能够被Character使用。Inventory是有Controller创建并持有的,因为Controller是玩家持久化的代表对象而不是Character。

class LEARNARPG_API UARPGInventory : public UObject
{
    ...
        
protected:
    //Inventory
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = ARPGInventory)
    TSet<FGuid> OwnedItems;
    TMap<FPrimaryAssetId, TSet<FGuid>> ItemsMap;
    TMap<FPrimaryAssetType, TSet<FGuid>> TypedItems;
    
protected:
    //Equips
    //优化蓝图遍历
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = ARPGInventory)
    TArray<FARPGItemSlot> Slots;
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = ARPGInventory)
    TMap<FName, int32> SlotMap;
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = ARPGInventory)
    TMap<FName, FGuid> SlottedItems;

public:
    FOnInventoryItemUpdateNative OnInventoryItemUpdateNative;
    FOnInventorySyncNative OnInventorySyncNative;
    FOnSlottedItemUpdateNative OnSlottedItemUpdateNative;
    
    ...
    
};

在逻辑层提供物品的消耗、授予等功能。他会对Inventory和全局数据进行对应的修改,并发起事件。不应该使用除了该逻辑层以外的方法修改Inventory和AssetManager中的全局数据。

class LEARNARPG_API UARPGInventoryLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable, Category = "ARPGInventory")
    static class UARPGItem* GetItemByGID(const FGuid& GID);

    UFUNCTION(BlueprintCallable, Category = "ARPGInventory")
    static FARPGItemData GetItemDataByGID(const FGuid& GID);
    
    UFUNCTION(BlueprintCallable, Category = "ARPGInventory")
    static bool GiveItemsToInventory(UARPGInventory* Inventory, const FGuid& GID, int Count = 0);
    
    UFUNCTION(BlueprintCallable, Category = "ARPGInventory")
    static bool ConsumeItemsInInventory(const FGuid& GID, int Count = 0, UARPGInventory* Inventory = nullptr);
    
    UFUNCTION(BlueprintCallable, Category = "ARPGInventory")
    static bool SetSlottedItem(UARPGInventory* Inventory, FName SlotName, const FGuid& GID);

    UFUNCTION(BlueprintCallable, Category = "ARPGInventory")
    static bool RemoveSlottedItem(UARPGInventory* Inventory, FName SlotName);

    UFUNCTION(BlueprintCallable, Category = "ARPGInventory")
    static class UARPGItem* GetSlottedItem(UARPGInventory* Inventory, FName SlotName);

    UFUNCTION(BlueprintCallable, Category = "ARPGInventory")
    static TArray<FGameplayAbilitySpecHandle> GetAllSlottedAbilityHandles(AARPGCharacterBase* Character, FName SlotName);
    UFUNCTION(BlueprintCallable, Category = "ARPGInventory")
    static FGameplayAbilitySpecHandle GetSlottedAbilityHandlesByClass(AARPGCharacterBase* Character, FName SlotName, TSubclassOf<UARPGGameplayAbility> Class);
    UFUNCTION(BlueprintCallable, Category = "ARPGInventory")
    static void GetSlottedPrimaryAbilityHandleAndInstanceByClass(AARPGCharacterBase* Character, FName SlotName, TSubclassOf<UARPGGameplayAbility> Class, FGameplayAbilitySpecHandle& OutHandle, UARPGGameplayAbility*& OutAbilityInstance);
};

技能系统设计

基本结构

首先,所有的技能基本使用InstancePerActor的实例化策略。这样好处有很多,除了节省性能以外,在整个技能循环当中几乎不需要与CDO交互,所有的操作都可以找到对应的技能实例,因此可以自定义装备和卸下的事件,也可以在技能中与其他技能实例交互,这对于复杂的连招技能有帮助。

使用Slot来存放物品和技能实例,slot存放的物品在Inventory中,slot存放的技能在Character中,这样Inventory专注于物品的管理,而Character专注于技能的give和remove。Character通过监听slot变化事件来装备和卸载技能。

class LEARNARPG_API AARPGCharacterBase : public ACharacter, public IAbilitySystemInterface
{
    ...
    //Equipment
    UPROPERTY(BlueprintReadOnly)
    UARPGInventory* Inventory;
    
    TMap<FName, TArray<FGameplayAbilitySpecHandle>> SlottedAbility;
    ...
};

一个武器可能有多个Actor,比如双刀、盾牌和剑,也可能只有一个,所以用PrimaryAsset来统一Actor并不合适,同时一个武器可能有多个技能,比如枪械有开枪、换弹、瞄准,而剑则可以攻击和防御他们之间的按键也不一样,所以需要让技能自行决定释放的按键。

使用PrimaryAsset存放武器所有的GA class,在装备武器时授予技能并设计一个sourceobject存放该物品的GID,这样能够让技能获取到其所在的槽位等信息。

每当技能被装备时,使用一个主要技能负责将该技能期望的Actor生成在Character身上并利用ALS提供的接口设置Overlap。

动画2

技能触发

将ASC绑定到Playerinput来实现技能的触发,对于每个技能而言有三种可能的触发方式,一个是按下装备槽对应的按键如攻击、使用物品,一个是按下自己的专属按键如瞄准,另一个是被其他技能触发。

所以在GA中设计了InputID来确定技能触发按键,在授予时通过CDO即可获取。

EARPGInputs InputID = Ability.GetDefaultObject()->InputID;
if(Ability.GetDefaultObject()->bUseSlotInput) InputID = Inventory->GetSlot(SlotName).InputAction;
UARPGAbilitySource* AbilitySource = NewObject<UARPGAbilitySource>();
AbilitySource->SourceItemGID = CurrentItemID;
FGameplayAbilitySpecHandle Handle = AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(Ability, ItemData.Level, static_cast<int32>(InputID), AbilitySource));
AbilityHandles.Add(Handle);

技能循环

对于GE的设计参考了ActionRPG的做法,利用EffectContainer将event tag和effect绑定起来。

8e80a8e3-4a3a-42f5-b763-800e22582aa7

与ALS动画结合

CC BY-NC-SA 4.0 Deed | 署名-非商业性使用-相同方式共享
最后更新时间:2025-03-24 02:01:58

0

    暂无讨论