Wednesday, February 23, 2022

Using TargetData in Blueprints (UnrealEngine's Gameplay Ability System)

The promised follow-up! You don't need to read up on part one, but do so if you're curious on how to make a custom TargetData. In this tutorial, we'll go over using a TargetData in Blueprints. Sound good? Let's go!

Disclaimer! This post assumes that you know the basics of UnrealEngine's Gameplay Ability System. If you don't know what a GameplayAbility or how to apply GameplayEffects, this tutorial will probably not going to make much sense. I'd suggest following Tranek's GAS Documentation on GitHub or watch the official Guided Tour of Gameplay Abilities by UnrealEngine.

Why use TargetData?

Before we get started, I want to reiterate something that I wrote in part one: the reason we use TargetData is that it allows a networked sharing of data. There are other ways of doing things without TargetData, but this is available to you out of the box. 

For example, say we want to teleport two networked players to another place on the map. Perhaps there's an ability that allows you to immediately teleport you and a teammate somewhere else. You could call SetActorLocationAndRotation node within your GameplayAbility:


But this has the following issues
  • OtherPlayer, SelfTeleportLocation, and OtherTeleportLocation will either have to be hard coded, networked, or calculated by both client and server.
  • If we calculate on both client and server, we need our inputs to be non-changing data, as the network delay will cause different outcomes.
  • If there are any lost packets or delays in networking, we'll have to create a way to roll back changes.
Using the TargetData system fly past these issues by allowing all data to be dynamic and sent to the server to send out to all interested clients. What's more, it has rollback networking as a feature, allowing any discrepancies to self correct.

This does mean that we need to do a little bit more set up and have multiple pieces, but the outcome is streamlined and easier than fixing any of the issues mentioned above.

Event Gameplay Ability

Let's start from the end, shall we? What we'll call the Event Gameplay Ability is a Gameplay Ability that is activated through an event. Pretty self explanatory. In the example above, we'd have both players to have an Event Gameplay Ability. We'll go over the next section how to grant these abilities onto them. Below is an example of a simple Event Gameplay Ability:


Instead of activating an ability through the Event ActivateAbility node, we use the Event ActivateAbilityFromEvent node. Notice that this node has an Event Data Target Data input (you may need to split the input parameter). This is the TargetData we'll be building in the Instigator Gameplay Ability section. 

Important to note, the TargetData input parameter is actually a TargetDataHandle, so it can contain multiple TargetData, hence our check if the TargetData has an endpoint. To learn more about the anatomy of TargetData, check out  Tranek's GAS Documentation or part one of this tutorial. 

There's a few other "boiler plate" items to take care. You'll want the Net Execution Policy to be set to Server Initiated. This is because when we send the event in the next section, we're telling the server to broadcast the Event Gameplay Ability to the necessary server and client instances. We'll also want to add as many Ability Triggers that you want this ability to respond to. For our purposes, it would probably be a tag: Event.Teleport.


With that, we're halfway done! Or a third of the way, depends on how which approach you take in the next section.

Instigator Gameplay Ability

The Instigator Gameplay Ability is the Gameplay Ability that sends the event along with it's data to the server. Any Gameplay Ability can be an instigator, even an Event Gameplay Ability. 

Side note - Creating a TargetData using GameplayAbilityTargetData_LocationInfo drops the rotator from the transform. Unsure why this was done this way, but it's simple to create your own custom TargetData that doesn't drop that information. I'll leave it as a challenge to you to do so. 

This assumes that the actor we're sending the event to already has the ability granted to them. Depending on our approach, it might be passive or granted.

Passive Ability Approach

This is the simpler of the two approaches. It assumes that the ability is always on the player because it is a passive ability. Granting passive abilities is unique to different projects, so we won't be going into it right now. While this is the simpler of the two options, it should be reserved for abilities that occur often. For example, in SLIMECORE, knocking the player around happens often enough that we feel comfortable adding a passive ability. Teleport on the other hand, will only happen every so often, so we're going to go with the granted ability approach.  

Granted Ability Approach

For this approach, we'll have to add a couple of nodes to our original Instigator Gameplay Ability blueprint.


The way we grant abilities is through a GameplayEffect, in this case GE_GrantTeleport. Granting abilities requires a client-server sync, so we add a WaitGameplayTagAdd node, waiting for a tag that the GameplayEffect will add. So this is how the GameplayEffect ends up looking like: 

In Summary...

That's it! Using TargetData is much easier that creating your own, but I felt compelled to do the other tutorial first since it's what less information is available on the internet and also the more interesting problem space to tackle. Hopefully with these two tutorials your third eye has opened and you can level up your GAS game. 

Until next time!

- Francisco 






Custom TargetData in UnrealEngine's Gameplay Ability System

This is it! I've had a hard time finding any information on the internet to create your very own TargetData in GAS, but no more. In this post, we'll talk about what is TargetData and how to create your own. In a follow up post, I'll go a little more in depth on how to use your new TargetData within a GameplayAbility.

Disclaimer! This post assumes that you know the basics of UnrealEngine's Gameplay Ability System. If you don't know what a GameplayAbility or how to apply GameplayEffects, this tutorial will probably not going to make much sense. I'd suggest following Tranek's GAS Documentation on GitHub or watch the official Guided Tour of Gameplay Abilities by UnrealEngine.

What is TargetData?

TargetData is a generic structure used in GAS to pass data across the network and between AbilitySystemComponents. It allows the sharing of necessary information so that you don't centralize all behavior for a GameplayAbility and reuse certain behaviors while keeping them agnostic from the GameplayAbility that spawned them. 

Out of the box, GAS offers the TargetData FGameplayAbilityTargetData. This structure is not meant to be used, but to be subclassed. GAS also offers three subclasses of FGameplayAbilityTargetData:
  • FGameplayAbilityTargetData_ActorArray
  • FGameplayAbilityTargetData_LocationInfo
  • FGameplayAbilityTargetData_SingleTargetHit
As one can tell from the names, each TargetData is very specialized in the data they hold. This is by design to avoid serialization of superfluous data. TargetData is passed between systems through a TargetDataHandle, which has an array of multiple TargetData. This way, you can chain together any TargetData that is needed for different behaviors. 

Creating a Custom TargetData Struct

In this tutorial, we'll sub-classing the FGameplayAbilityTarget and creating FGameplayAbilityTargetData_ImpulseInfo. Our game has multiple knock-direction effects (knock-back, knock-up, knock-from-point, etc.). To reduce code duplication, we'll be creating a new GameplayAbility GA_Event_KnockDirection which will be applied to an Actor that will be knocked around and pass the impulse information of that through a TargetData. For this, we'll create two separate pieces the TargetData object itself and a static utility to use and translate TargetData in Blueprint. 

TargetData Object

GameplayAbilityTargetData_ImpulseInfo.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
USTRUCT(BlueprintType)
struct GASSHOOTER_API FGameplayAbilityTargetData_ImpulseInfo : public FGameplayAbilityTargetData
{
    GENERATED_USTRUCT_BODY()
	
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
    FVector Direction;

    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
    float Magnitude;

    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
    float Duration;

    virtual FTransform GetEndPointTransform() const override;

    virtual UScriptStruct* GetScriptStruct() const override
    {
    	return FGameplayAbilityTargetData_ImpulseInfo::StaticStruct();
    }

    virtual FString ToString() const override
    {
    	return TEXT("FGameplayAbilityTargetData_ImpulseInfo");
    }

    bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
};

template<>
struct TStructOpsTypeTraits<FGameplayAbilityTargetData_ImpulseInfo> : public TStructOpsTypeTraitsBase2<FGameplayAbilityTargetData_ImpulseInfo>
{
	enum
	{
		WithNetSerializer = true	// For now this is REQUIRED for FGameplayAbilityTargetDataHandle net serialization to work
	};
};

Here we have the important pieces for our header. First thing to notice is lines 7 - 13. These are all the values we'll need to apply impulse to our actor. These values can be anything that is serializable. If you want to send data that is not readily serializable, you'll have to create your own serialization code to convert the data into serializable objects. 

Next up is GetScriptStruct() (lines 17 - 20). This is used to identify what what type of struct this is. This is a great utility for when you have multiple TargetData chained in a TargetDataHandle. With this implemented, you can fetch the necessary TargetData for different GameplayAbilities. We will see this function in action in part 2, where we implement the GameplayAbility using this TargetData. 

Lastly, we have our networking pieces. Line 27 has NetSerialize(...) which will be required to serialize the values of this struct. Lines 30 - 37 has boilerplate code to let the engine know that our struct has a NetSerialize(...) implementation. This code is needed if you want the data to be networked between server and client. If this code will only run on client, feel free to not implement serialization and set line 35 to false. 

GameplayAbilityTargetData_ImpulseInfo.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
FTransform FGameplayAbilityTargetData_ImpulseInfo::GetEndPointTransform() const
{
	FTransform ImpulseInfo = FTransform(Direction.GetSafeNormal() * Magnitude);
	ImpulseInfo.SetScale3D(FVector(
		Duration,
		0,
		0));
	
	return ImpulseInfo;
}

bool FGameplayAbilityTargetData_ImpulseInfo::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
	Ar << Direction;
	Ar << Magnitude;
	Ar << Duration;
	
	bOutSuccess = true;
	return true;
}

Implementation is quite short but a little bit obtuse due to holdover naming's of certain fields and functions. To see what I mean, let's look at a snippet of from GameplayAbilityTargetTypes:

GameplayAbilityTargetTypes.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
/** Override to true if GetOrigin will work */
virtual bool HasOrigin() const
{
	return false;
}

/** Override to return an origin point, which may be derived from other data */
virtual FTransform GetOrigin() const
{
	return FTransform::Identity;
}

/** Override to true if GetEndPoint/Transform will work */
virtual bool HasEndPoint() const
{
	return false;
}

/** Override to return a target/end point */
virtual FVector GetEndPoint() const
{
	return FVector::ZeroVector;
}

/** Override to return a transform, default will create one from just the location */
virtual FTransform GetEndPointTransform() const
{
	return FTransform(GetEndPoint());
}

Originally TargetData was hyper-specific in its use. As such, it used specific object types. Now that we're building on top of TargetData, we are required to fit our data into the fields provided by TargetData. In our case, we are putting the the direction and magnitude of our impulse into the the location field of the FTransform returned in GetEndPointTransform() (line 1 - 10 of GameplayAbilityTargetData_ImpulseInfo.cpp) and the duration will be placed inside the scale field of the FTransform, returned by the same function. One could go through the refactoring process of making these names more generic, but for our purposes we'll be creating a static utility component later on to translate this data for more generic use in Blueprints. 

Another implementation required is NetSerialize(...). We're fortunate enough that all our data is serializable, but if you had an object that wasn't this is where you would call the appropriate serialization functions to get a serialized version of your objects and add it to FArchive.

And that's it! Now that we have created a custom TargetData, let's create a utility class with node implementations to use in Blueprint. 

Creating a Utility Class to Use TargetData

At this point, you should be able to use the custom TargetData. However, it becomes unwieldy to use outside of C++, so let's create a UBluprintFunctionLibrary for your new TargetData. Below is a snippet from ImpulseTargetDataLibrary:

ImpulseTargetDataLibrary.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
FGameplayAbilityTargetDataHandle USWImpulseTargetDataLibrary::MakeImpulseTargetData(
	const FVector Direction,
	const float Magnitude,
	const float Duration,
	const bool IsAdditive)
{
	FGameplayAbilityTargetData_ImpulseInfo* NewData =
		new FGameplayAbilityTargetData_ImpulseInfo();

	NewData->Direction = Direction.GetSafeNormal();
	NewData->Magnitude = Magnitude;
	NewData->Duration = Duration;
	NewData->IsAdditive = IsAdditive;
	
	FGameplayAbilityTargetDataHandle Handle;
	Handle.Data.Add(TSharedPtr<FGameplayAbilityTargetData_ImpulseInfo>(NewData));
	return Handle;
}

void USWImpulseTargetDataLibrary::BreakImpulseTargetData(
	const FGameplayAbilityTargetDataHandle& TargetData,
	const int TargetDataIndex,
	FVector& Direction,
	float& Magnitude,
	float& Duration,
	bool& IsAdditive)
{
	if(!HasImpulseTargetData(TargetData))
	{
	 	return;
	}

	Direction = GetDirectionFromImpulseTargetData(TargetData, TargetDataIndex);
	Magnitude = GetMagnitudeFromImpulseTargetData(TargetData, TargetDataIndex);
	Duration = GetDurationFromImpulseTargetData(TargetData, TargetDataIndex);
	IsAdditive = GetIsAdditiveFromImpulseTargetData(TargetData, TargetDataIndex);
}

FVector USWImpulseTargetDataLibrary::GetDirectionFromImpulseTargetData(
	const FGameplayAbilityTargetDataHandle& TargetData,
	const int TargetDataIndex)
{
	return TargetData.Get(TargetDataIndex)->GetEndPointTransform().GetLocation().GetSafeNormal();
}

MakeImpulseTargetData(...) and BreakImpulseTargetData(...) are the most important in this file, as they allow you to create a node to construct the TargetData and create a node to deconstruct the TargetData. There's a few functions omitted for spacing (lines 34-36), but they are rather similar to GetDirectionFromImpulseTargetData(...).

In Summary...

That's it! Now you know how to create your own TargetData and write up blueprint nodes for it. I'll be making another post soon to show how to use TargetData in general with blueprints. That tutorial should be generic for any TargetData, so it will be a good follow to see how this custom TargetData works in practice. 

Until next time!

-Francisco

Friday, September 11, 2020

Entity Bridges and the Path Of Least Resistance Philosophy

Wanted to write up a quick blog post about a development philosophy that I've taken up during this project. Since I'm working with a preview package (not meant for release) of Unity DOTS, I have found multiple instances of features that I would have expect to work but still do not. 

For example, the physics engine currently has a way to detect TriggerEvents when two triggers intersect, but not when they stop intersecting. I need this for drawing and undrawing a bow. Since on the MonoBehaviour side we have OnTriggerEnter and OnTriggerExit, I'd expect this to eventually exist in DOTS. Now, it wouldn't be too difficult to implement my own trigger system in DOTS, but it would be more work. And since this is something that I hope is included in the release version, it would be unnecessary extra work. So what to do?

Well, you don't. Instead I've created an abstract class EntityBridge:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public abstract class EntityBridge : MonoBehaviour
{
    protected bool isInitialized = false;
    protected Entity entity;

    protected EntityManager entityManager;

    protected virtual void Awake()
    {
        entityManager =
            World.DefaultGameObjectInjectionWorld.EntityManager;
    }

    public void Initialize(Entity entity)
    {
        this.entity = entity;
        isInitialized = true;
    }
}

The idea here is to create a bridge between MonoBehaviours and Entities, allowing inheriting classes to work in the world of MonoBehaviours and communicate with Entities. For example, now we have an implementation of EntityBridge called BowEntityBridge. BowEntityBridge has trigger callbacks:

 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
        private void OnTriggerEnter(Collider other)
        {
            if (other.tag == "UndrawnArrow")
            {
                _audioSource.PlayOneShot(
                    _handleAudioClips[Random.Range(0, _handleAudioClips.Length)]);

                entityManager.SetComponentData(
                    entity,
                    new BowDrawAreaTriggerComponent
                    {
                        IsArrowInBowDrawArea = true
                    });
            }
        }

        private void OnTriggerExit(Collider other)
        {
            if (other.tag == "UndrawnArrow")
            {                
                entityManager.SetComponentData(
                    entity,
                    new BowDrawAreaTriggerComponent
                    {
                        IsArrowInBowDrawArea = false
                    });

                _isBowDrawnAudioClipPlaying = false;
            }
        }

This allows me to have the enter and exit tech without having to reinvent the wheel. Then when Unity DOTS is update to have it (if it ever does), then I'll move it all to ECS. 

Now, I understand that I lose a lot of the performance boosts by mixing legacy Unity and Unity DOTS. I would never use this in a professional setting unless absolutely necessary. However, the time saved by just doing this instead of having to make my own physics system for triggers is enough of a boon for me to decide to just have it this way for now. Besides, I soon found that I would need EntityBridge for another application. But that's a story for another time. 

Anyways wanted to post my thoughts on this. If some future reader out there wants me to go a little more technical on how this EntityBridge is used let me know and I'd be willing to talk more about it. Probably commenting below is the best way to reach me for now. 

Monday, September 7, 2020

Starting a Dev Diary for LocalPineappleRuinsEverything

Starting a dev diary as a keepsake of how much I've done. Don't expect this to be updated on a frequent cadence until I start sharing the game more publicly. For now, this will just be a place to write down my thoughts on Project: LocalPineappleRuinsEverything. 

The project has two goals; to give me an excuse to get my feet wet in Unity DOTS and to make a compelling VR archery game for the Oculus Quest. 

I've gotten to a somewhat comfortable development cycle with ECS, thinking of my code in forms of entities, components and systems. In addition, I'm following a Functional Core/Imperative Shell philosophy on my systems, as to be able to run tests on functional pieces. The goal is to make a game that is highly performant for what I'm thinking will probably be the most consumer friendly (and popular) yet not as power intensive VR headset; the Oculus Quest. This will allow for more effects, more arrows, and just an overall more pleasant looking game in VR.

Which leads to my second goal. VR archery games exist both on SteamVR and Oculus Quest, but I find the offerings on the latter lackluster. Most seem to be bare minimum and just plain ugly at times. I find it hard to believe that the best VR archery game, both in controls and aesthetic, is Longbow, a demo game available freely on Vive. This is why I want to make this game. That and my girlfriend's mother absolutely loves Longbow but only owns a Quest.

While these are my goals, I by no expecting this to be a break out success that I'll be pouring my heart and soul into. This is first and foremost a learning experience, and as such I expect to be redoing a lot. It doesn't help either that Unity DOTS is still in preview, and as such they do not recommend it for release. Besides, I am doing this in my free time on top of a full time software developer position, so I bet there will be days, weeks even that I just want to get off the computer after work. Still I'm trudging ahead to what I expect will be a fun game. 

Anyways that seems to be enough for an intro. Guess I'll post a pic of the test level I set up for when I start doing pathfinding in ECS:

Using TargetData in Blueprints (UnrealEngine's Gameplay Ability System)

The promised follow-up! You don't need to read up on part one , but do so if you're curious on how to make a custom TargetData. In t...