Serializing AbilitySystemComponent data into a saved game [GAS]

I’m working[1] on building a save game system and have run into the difficulty of determining what exactly needs to be saved from the AbilitySystemComponent.

Ideally, I’d save just enough data to restore the game to the precise state it was in when the player saved the game. There are a lot of moving parts in the AbilitySystemComponent though, and I’m not entirely sure what the most minimal set of data would be.

My current list is:

  • The base values of all the Gameplay Attributes that the ASC initially possesses that differ from their initial value.
  • Any attribute sets that were granted that the ASC did not initially possess – the base value for each, if it differs from the default.
  • All Gameplay Effects that are currently active. I think each of these need to include the effect instance or class, the level, any granted tags, and any set-by-caller magnitudes.
  • All granted Gameplay Abilities that were not granted by a Gameplay Effect. I think each of these need to include the ability instance or class, the source object (as a reference), the input datal, any dynamic tags, and any set-by-caller magnitudes.
  • Loose Gameplay Tags, if any

I’m not at all confident that that’s everything, nor that I haven’t included something unnecessary. In particular, it seems like the above list won’t take into account the possibility that some abilities are currently ongoing.

This seems like something that others surely must have figured out before. Does anyone else have their own list of what must be saved?

I’m explicitly excluding granted abilities that were granted by an effect, because the effect will automatically re-grant them. But I’m not completely sure that this is always right – if the ability is instanced, I might need to save it anyway and somehow relink it to its granting effect on load?

I’m also a little unsure about serializing the base values of the attribute sets, as I’m pretty sure I remember that Gameplay Effects can modify the base value… so it could become very messy trying to figure out the base state that everything else is going to build on.


  1. I think it mostly doesn’t matter for this question, but just for the record, I’m currently working in UE 5.0. ↩︎

I’m not claiming to have the best answer possible, but to my understanding trying to save ASC directly is a pure madness.

I had a similar problem (albeit a bit easier and with a power to limit game design where it is inconvenient), and may be my approach will lead you to some adequate solution.
The general idea of solution was: not to save ASC itself, but to save a bunch of top level data that defines the complete ASC state.

For example, my characters has a kinda “actions queue component”, where each action is an object itself. You may consider this actions as a variation of GA, where only one ongoing action is permited.

My save process for actor was:
Save actor => save all saveable components => actions queue component in particular was saving only current action.
The current action has a custom Save and Load functions defined, where action itself defines what to save into passed in save object\structure.
And on practice, 90% of my abilities could been described by:

  • Inputs: target point\unit\etc, a small finite set of data that would let ability to be repeated in exactly the same way if it launched again
  • Phase: literally a enum. Most of my abiltities had a similar phase structure like “initialized => move to target => play montage => end”. Most abiltiies had about 4 phases, a few had up to 7, which are stored in the same single enum.

And that’s it: this is enough to replicate the saved state.

Loading goes as:
Load actor => load all saveable components => action queue component.
Action queue component in its turn checks if save for this unit has an abilitiy data. If so: start this ability with stored Inputs and Phase.
Ability on its start also has a special handler that depending on passed in Phase jumps to corresponding phase of ability. In some cases you would need to instantly apply effects of skipped phases.

This was on load you have a unit with active ability in a state that corresponds to state it was saved in.

I should note that my abilities are a custom UObjects, that sometimes do start a child GAs and sometimes don’t

That’s was a part about saving GAs.

For GEs… you probably can try store data about GE directly from ASC: the class, the inputs, the remaining duration.
In my case i had a custom GE-like system that is more save-friendly, but it may be just not applicable to your\general case.

Tags - those are defined by GEs, so should be solved automatically.
LooseTags - i still wonder why would you ever want to use them in first place. But surely it’s just an FGameplayTagsContainer, so you may easily save\load it.

Attributes saving depends on your game structure.
In my case i have a “map” <FString PresetName, FDefaultAttributesForPreset>. This way i may just save a single string to store all the base attributes and the rest could be adjusted by loaded GEs.

Hope this will give you some ideas on how to proceed. Goodluck

Yes, this is the crux of the question – identifying all the top level data that defines the complete state.

Sorry if I gave the impression that I was trying to directly serialize the ASC. That’s not what I’m doing. I’m copying data from the ASC into a custom struct which I then serialize. Currently I’ve only implemented save and not load, but the idea will be that I deserialize that struct and then copy the data back into the ASC.

Okay, so the implication of this would be that I do, in fact, need to serialize data from any abilities that are currently active, right? The inputs and, if applicable, the phase. Besides target, do you have any other examples of what might count as an input? Not a problem if you don’t, though.

Hmm, so if I do have multiphase abilities, I’ll need to have a custom ability subclass to handle skipping to a specified phase on load. I’ll keep that in mind. I’m not sure yet if I need multiphase abilities.

Yes, I think the gameplay effects are indeed easier to save than the abilities, probably. They don’t have the whole instancing thing going on. Though it’s technically possible to dynamically construct and apply a gameplay effect at runtime, that’s usually frowned upon from what I understand. That said, even if it’s frowned upon, it barely complicates the process at all.

Loose tags are a tricky one. From what I can tell, the ASC makes no distinction between loose tags and tags from effects. I’m not sure yet whether I’ll want to use them, but if I do, I think what I might need to do is add wrapper functions in my custom ASC subclass that separately keep track of the loose tags while also pushing them through to the superclass calls managing them.

I’m not sure I understand this. What is a preset and the default attributes for a preset? Seems like something fairly specific to your game, or am I missing something?

My current WIP saves the base value of all attributes that exist on the ASC. I think it’s a bit excessive, but I’ve yet to think of a way to cut it down further.

just to make it clear: it’s hard to generalize some things, so in some cases i’m talking about my particlar project, which is RTS-like, which may not be applicable to your case.

That’s a hard to put in words, but i think it’s not quite it. Anyway, anything that will work is okay.

That’s correct. Unless you doing turn-based game where you can enforce “no abilities is currently running” you have to save them to load game in correct state (in the middle of ability usage)

I mean, GA’s execution is determined by world state, avatar actor, ability class and params passed to ActivateAbility().

  • World state (generally - the actors presense and locations) should be set up before loading abilities and out of scope of this question
  • Avatar actor & Ability class - kinda trivial in context of saving\loading state of particular actor
  • params passed to ActivateAbility() - exactly what i mean by Inputs. If you activate abilities with no params - there is no inputs; If you use targeting to generate inputs mid-execution - that would require some extra tricks to save those (ex. save new inputs and don’t request targeting if target is already known)
    • Worth noting that i mean “inputs\params” conceptually, not literally. I’m include any additional settings you made to ability instance at runtime before calling Activate. // This clarification may not even be relevant to you, but if you doing a bunch of custom activation setups - it surely is

That’s kinda hard to imagine that even phase “get closer to target and then start” is not applicable. But probably possible

That’s what i’ve heard, yes. Iirc the only real problem is network replication, but even then - never need them in my cases

To give you idea:
a unit has a set of params: maxhp, attack, defence, etc.
the map of presets is a table:

warrior: 100, 10, 5
rogue: 50, 15, 3
mage: 20, 20, 0

On my actor instance i can just set it “this one is Warrior”. Then custom ASC init would pull data from table and set the maxph, attack, defence to 100, 10, 5.
This way instead of saving a dozens of BaseValue of attibutes you may save just a single FString “warrior”.
After that, any lvlup bonuses, equipment bonuses, etc, are handled by corresponding systems by applying GEs to modify CurrentValue of attributes.

That’s surely specific for my game, but i think this concept may be applied widely. I don’t force it, just giving it as an idea\example

So you’re saying level up bonuses are just a GE for you? If I go that way I might be able to entirely skip saving attributes, since I don’t have classes – the initial attributes would be the same for every player (monsters/npcs would have different ones, but those are determined by the monster/npc definition).

That’s the case where it’s surely possible to solve problem by several different ways and every way will be a correct one. So think twice if you really want to switch your already existing system to this one.

But yes, in my particular case - that’s the case iirc.

No switching needed – I haven’t even implemented level ups yet. But I will think twice about whether or not I want to do it with a gameplay effect.

I’ve been considering how to serialize an active ability instance. My thought is that I can basically handle it in two steps:

  1. Go through the ability’s own properties and save anything that needs to persist.
  2. Grab the ability’s list of active tasks. For each task, go through its properties and save anything that needs to persist.

In theory, this should be able to handle resuming an ability in the middle of the execution graph, as long as you don’t use latent nodes (by which I mean things like Delay that don’t have an Exec pin that triggers immediately), since resuming the graph from the correct point is simply triggering a delegate.

I haven’t yet looked into how to reverse this though; I suspect respawning a gameplay task in mid-execution could prove tricky.

It also has the problem that many of the built-in gameplay tasks don’t even mark all their data with UPROPERTY(), so I can’t rely on reflection for built-in tasks. I could either write some special code to handle those tasks (similar to what I’ve done with several common actor components), or make custom versions of the built-in tasks and only use those.

Does this approach seem viable? Or how did you handle active ability instances?

Posting again because I got it (almost) all working, so I thought I’d share a general outline of what I actually did. I won’t post the full code, because the exact details are going to be project-dependent, but I’ll post a few snippets.

List of data stored

The ability system component as a whole is represented by the following values in the save data:

  • A map of gameplay attributes, TMap<FGameplayAttribute, float>
  • A list of data for each gameplay effect that has is currently applied
  • A list of data for each granted ability

For each applied effect, I store the following data:

  • The effect’s class
  • Data from the Gameplay Effect Context used when the effect was applied (see below for more detail on this)
  • The level of the effect
  • Dynamic asset tags and dynamic granted tags
  • Set by caller magnitudes, as a TMap<FGameplayTag, float>
  • The number of stacks applied
  • The current elapsed duration on the effect (that is, the time elapsed since it was applied); I store this even for infinite effects, though I’m uncertain whether it’s useful there.
  • The time left until the effect’s next periodic trigger (that is, the effect’s period minus the time since it last triggered), only for periodic effects
  • Data from the effect object itself (only if it was a dynamically constructed effect); this would vary depending on its class

For the Gameplay Effect Context, I store the following data:

  • Paths to the instigator, the original instigator (if different from the instigator), the effect causer, and the source object
  • Path to the hit actor, if any, and the name of the hit component
  • Paths to any other involved actors (perhaps from target data, for example)
  • The hit result and origin, if any
  • The ability that initiated it, if any

For each granted ability, I store the following data:

  • The ability’s class
  • The class of the gameplay effect that granted the ability, if any
  • Path to the source object that granted the ability
  • The level of the ability
  • Path to the Enhanced Input Action that triggers the ability (I have a system that maps these to the Ability System’s input IDs)
  • Dynamic ability tags
  • If the ability is an instanced ability, a list of all active instances. I don’t store inactive instances, but you could if you needed to have abilities that save state across activations.

For each active ability, I store the following data:

  • Persistent data required by the specific ability
  • Data from the gameplay event that triggered the ability, if any; there’s not much more to say here, as it’s literally everything that’s in the FGameplayEventData struct. The only complicated wrinkle is the need to store the objects as string paths (eg TSoftObjectPtr).
  • A list of Ability Tasks current present on the ability, along with any data they need for their work, including the bindings on the delegates that will be called when they complete.

Some things were tricky to deal with. In some cases, there are no proper public setters for the values I needed to change, so I needed to find workarounds to revive them from the save data. For example, using the reflection system to set a private property, or making a dummy class inheriting from the class so that I can access a protected method.

The two hardest things to handle on restore were probably ability instances and the gameplay effect context.

Restoring Ability Instances

The code to restore instances for one granted ability looks something like this:

for(int i = 0; i < this->instances.Num(); i++) {
	FGameplayEventData* event = nullptr;
	if(bHasEventData) {
		event = &this->StoredEventData;
	}
	// Construct a new ability object
	// grantedSpec is obtained from the return value of GiveAbility
	UMyGameplayAbility* abilityInstance = asc->ReviveAbilityInstance(*grantedSpec, event);
	// Copy reflected properties from the save data to the ability
	instances[i].Restore(abilityInstance);
	grantedSpec->ActiveCount++;
}

Where ReviveAbilityInstance is defined like this:

class AbilityInfo_ProtectedWorkaround : public UGameplayAbility {
public:
	// This parameter MUST not be UMyGameplayAbility. It only works if it's exactly UGameplayAbility.
	static void SetInfo(UGameplayAbility* abil, const FGameplayAbilitySpecHandle spec, const FGameplayAbilityActorInfo* actor) {
		//  This needs to be a static_cast instead of Cast because the ability is not actually of the AbilityInfo_ProtectedWorkaround class; I just need to make the compiler think it is so I can call the protected function on it.
		auto workaround = static_cast<AbilityInfo_ProtectedWorkaround*>(abil);
		workaround->SetCurrentInfo(spec, actor, FGameplayAbilityActivationInfo());
	}
};

UGameplayAbility* UMyAbilitySystemComponent::ReviveAbilityInstance(FGameplayAbilitySpec& spec, FGameplayEventData* event) {
	auto abil = CreateNewInstanceOfAbility(spec, spec.Ability);
	AbilityInfo_ProtectedWorkaround::SetInfo(abil, spec.Handle, AbilityActorInfo.Get());
	if(event) {
		if(auto myAbil = Cast<UMyGameplayAbility>(abil)) {
			myAbil->ReviveEventData(*event);
		}
	}
	return abil;
}

The ReviveEventData function just assigns the CurrentEventData, so not really worth showing (I did also make it set a boolean that the ability was activated by an event). The other key piece of the puzzle is restarting tasks:

class AbilityTask_ProtectedWorkaround : public UAbilityTask {
public:
	static void Init(UAbilityTask* task, FAbilityTaskInfo taskData, IGameplayTaskOwnerInterface& owner, bool pause) {
		// Again, use static_cast to fool the compiler so I can call protected methods
		auto workaround = static_cast<AbilityTask_ProtectedWorkaround*>(task);
		workaround->InitTask(owner, taskData.Task->GetPriority());
		workaround->InstanceName = taskData.Name;
		// Copy any necessary data into the task.
		// This depends on the specific task, and may require very ugly hacks as some of the built-in tasks don't even mark their data with UPROPERTY()
		// I will not show any such hacks here
		taskData.Restore(task);
		workaround->ReadyForActivation();
		if(pause) workaround->Pause();
	}
};

void UMyGameplayAbility::RestartTasks() {
	// AllTasks is populated during gameplay by overriding the OnGameplayTaskXXX methods
	// On restore, it's populated as part of restoring the ability's data (the instances[i].Restore() call earlier)
	auto toRestart = AllTasks;
	AllTasks.Empty();
	for(const auto taskData : toRestart) {
		auto task = NewObject<UAbilityTask>(GetTransientPackage(), taskData.Task->GetClass());
		AbilityTask_ProtectedWorkaround::Init(task, taskData, *this, taskData.State == EGameplayTaskState::Paused);
	}
}
Restoring the Gamplay Effect Context

The Gameplay Effect Context is a polymorphic struct, which massively complicates serializing and deserializing it. The Unreal reflection system has a big assumption built into it: a class is always polymorphic and a struct is never polymorphic. So, the reflection system doesn’t directly handle a polymorphic struct.

So, the first step in restoring it is to figure out the real struct that needs to be restored. It’s not that hard – I’d already stored the struct path into the save data (it can be obtained from FGameplayContext::GetScriptStruct()), so it’s just a matter of constructing a new one of that type… simple, right?

Well… not really.

This is my solution for the simple act of allocating an instance of the correct sub-struct:

// Deleter functor to deallocate a struct which is determined at runtime and was allocated with FMemory::Malloc.
struct FDynamicStructDeleter {
	TStrongObjectPtr<UStruct> Def;
	FDynamicStructDeleter(UStruct* d) : Def(d) {}
	void operator()(void* ptr) {
		Def->DestroyStruct(ptr);
		FMemory::Free(ptr);
	}
};

TSharedPtr<FGameplayEffectContext> AllocateGameplayEffectContext(UStruct* def) {
	if(def->IsChildOf(FGameplayEffectContext::StaticStruct())) {
		if(void* ptr = FMemory::Malloc(def->GetStructureSize(), def->GetMinAlignment())) {
			def->InitializeStruct(ptr);
			return MakeShareable(reinterpret_cast<FGameplayEffectContext*>(ptr), FDynamicStructDeleter(def));
		}
	}
	return nullptr;
}

It might be overkill. For any give game, the specific sub-struct is probably known, so I could’ve just hard-coded it for that one sub-struct. But I wanted to come up with code that I could copy-paste to a new project later with minimal changes, so I ended up with this more generic solution.

Restoring the data itself is not that complicated. Nearly everything in FGameplayEffectContext is either public or has a public setter. The only wrinkle I ran into is that there’s no way to directly set HasOrigin. It’s set to true when you call AddOrigin, but there’s no way to directly set it to false. So, I ended up using the reflection system to set it.

There was one last remaining wrinkle to get past: all of this work gave me a TSharedPtr<FGameplayEffectContext>, which I then needed to turn into an FGameplayEffectContextHandle. But there is, surprisingly, absolutely no way to do that.

  • The FGameplayEffectContextHandle constructors only take a raw pointer, which they immediately take ownership of and wrap in a TSharedPtr, but that would not do here, because said pointer was already owned by a different TSharedPtr. Having a pointer owned by two different TSharedPtrs is jstu asking for trouble. (Hint: It would definitely crash.)
  • The FGameplayEffectContextHandle only has one member, which is the TSharedPtr I needed to update. So my first instinct was to use type punning to set it, as the address of a struct’s first member is normally the same as the address of the struct itself. But no, FGameplayEffectContextHandle has virtual functions, so its first member is the invisible vtable, and I can only guess at the address of the TSharedPtr. This is silly – it doesn’t need them to be virtual, as it’s not meant to ever be inherited from. But it is what it is.

I ended up using the clone functionality to make a copy of the FGameplayEffectContext I’d just restored, passing that into the FGameplayEffectContextHandle:

TSharedPtr<FGameplayEffectContext> data = AllocateContext(def);
// restore data here
FGameplayEffectContextHandle handle(data->Duplicate());
return handle

I’m open to questions if anyone is interested in other details of what I did.