Announcement

Collapse
No announcement yet.

[Tutorial] Utility functions for gameplay tags

Collapse
X
  • Filter
  • Time
  • Show
Clear All
new posts

    [Tutorial] Utility functions for gameplay tags

    Here's a short tutorial for something I showed on this tweet.

    Problem 1
    You want to turn a string like "Combat.Invulnerable" into an FTag struct.

    The solution is calling UGameplayTagsManager::Get().RequestGameplayTag()

    What if you want to do that several times? Writing all that is a bit cumbersone, so the first utility function is this

    Code:
    #include "GameplayTags.h"
    
    static FTag GetTagFromString(const char* TagName)
    {
        return UGameplayTagsManager::Get().RequestGameplayTag(FName(Tag))
    }
    This is nifty, but there was something else I wanted to show you.

    Problem 2
    Let's say you want to check tags inside a container. For example, you may want to get all the tags that your character has such as Combat.Bloodied, Combat.Invulnerable, Debuffs.Cursed. FGameplayTagContainer has some methods to check against tags but the annoying thing is that unless you're checking one tag only, you have to wrap all the tags you want to check for inside another FGameplayTagContainer. Which I find annoying. What I'd much rather have is a function returning a bool that I can call like ContainerHasAllTags(CharacterTags, "Combat.Invulnerable", "Buffs.Blessed"). That's what I wanted to show you.

    Code:
    public:
    
        /** Checks if all the tags are in the container */
        template<typename... Args>
        static bool ContainerHasAllTags(const FGameplayTagContainer* Container, Args... TagsToCheck)
        {
            return ContainerHasAllTagsImpl(Container, TagsToCheck...);
        }
    
    private:
    
        static bool ContainerHasAllTagsImpl(const FGameplayTagContainer* Container, FGameplayTag Tag)
        {
            return Container->HasTag(Tag);
        }
    
        static bool ContainerHasAllTagsImpl(const FGameplayTagContainer* Container, const char*  Tag)
        {
            const auto FoundTag{ UGameplayTagsManager::Get().RequestGameplayTag(FName(Tag)) };
            return Container->HasTag(FoundTag);
        }
    
        template<typename... Args>
        static bool ContainerHasAllTagsImpl(const FGameplayTagContainer* Container, FGameplayTag FirstTag, Args... OtherTags)
        {
            return ContainerHasAllTagsImpl(Container, FirstTag) && ContainerHasAllTagsImpl(Container, OtherTags...);
        }
    
        template<typename... Args>
        static bool ContainerHasAllTagsImpl(const FGameplayTagContainer* Container, const char*  FirstTag, Args... OtherTags)
        {
            return ContainerHasAllTagsImpl(Container, FirstTag) && ContainerHasAllTagsImpl(Container, OtherTags...);
        }
    The way this function works, you can call it either ContainerHasAllTags(Container, Tag1, Tag2, Tag_n) if Tag1, ..., Tag_n are all of type FTag or ContainerHasAllTags(Container, "Tag1", "Tag2", "Tag3") which is what I like the most, using strings. If you're familiar with recursive functions you'll understand how it works.

    Extra: More functions
    From here it's trivial to implement any variations that you may need such as ContainerDoesntHaveTags or ContainerHasAnyTag by changing the boolean checks in the private implementations.

    Extra: Type checking at compile time
    This works already, but what if you make a mistake and you call ContainerHasAllTags with something that isn't either an FTag or a string? C++ has a decent type system that we can leverage in our favor to get the compiler to complain if we call the functions with the wrong parameters. Unfortunately, because I don't think UE4 plays nicely with C++17, we'll have to write a little hack.

    Code:
    #include <type_traits>
    
    // I suggest wrapping this into a namespace, call it whatever you want
    namespace rstd {
    
        /** Implementation of C++17's conjunction. Performs a logical AND on the sequence of traits */
        template<typename...> struct conjunction : std::true_type { };
        template<typename B1> struct conjunction<B1> : B1 { };
        template<typename B1, typename... Bn> struct conjunction<B1, Bn...> :
            std::conditional<bool(B1::value), conjunction<Bn...>, B1>::type { };
    
        /** Implementation of C++17's disjunction. Performs a logical OR on the sequence of traits */
        template<typename...> struct disjunction : std::false_type { };
        template<typename B1> struct disjunction<B1> : B1 { };
        template<typename B1, typename... Bn> struct disjunction<B1, Bn...> :
            std::conditional<bool(B1::value), B1, disjunction<Bn...>>::type { };
    }
    So what's this useful for? Well, now we can revisit our ContianerHasAllTags function and rewrite it like this:

    Code:
    /** Checks if all the tags are in the container */
    template<typename... Args>
    static bool ContainerHasAllTags(const FGameplayTagContainer* Container, Args... TagsToCheck)
    {
        static_assert(
            rstd::disjunction<
            rstd::conjunction<std::is_same<const char*, Args>... >,
            rstd::conjunction<std::is_same<FGameplayTag, Args>...>>::value,
            "'Args' must be of type 'const char*' or 'FGameplayTag'");
    
        return ContainerHasAllTagsImpl(Container, TagsToCheck...);
    }
    You can check the finished version on this gist. Let me know if you found this useful!

    This tutorial is released into the public domain. If you liked this post and want to share it, improve it, repost it in your blog, etc. feel free to do so. I would however appreciate it if you mention my Twitter.
    sybloom ::: Website Coming Soon
Working...
X