What's the proper way to reference and destroy gameplay cue notify actors?

What happens seem to be related to the order in which the client receives the message for the cue.

LogTemp: Warning: GameplayCueNotify_Actor::OnExecute_Implementation
LogTemp: Warning: GameplayCueNotify_Actor::GameplayCuePendingRemove
LogTemp: Warning: GameplayCueNotify_Actor::GameplayCueFinishedCallback
LogTemp: Warning: GameplayCueNotify_Actor::Recycle
LogTemp: Warning: GameplayCueNotify_Actor::SetOwner : null
LogTemp: Warning: GameplayCueNotify_Actor::SetOwner : not null
LogTemp: Warning: GameplayCueNotify_Actor::ReuseAfterRecycle
LogTemp: Warning: GameplayCueNotify_Actor::OnExecute_Implementation

The cue is recycled allright and hidden when we receive UGameplayCueManager::NotifyGameplayCueActorFinished() , but then another “execute” message comes in and calls UGameplayCueManager::GetInstancedCueActor that reactivate the object. (which then never gets destroyed)

Still not sure how to handle that.

In a perfect scenario I woulnd want the server to tell the client to execute a cue, that seem like a waste of bandwidth. But even with Minimal networking it seems that the cues are always sent.

Any tips ?

I was able to make it work with " If you need something in a GameplayCue to be ‘reliable’, then apply it from a GameplayEffect and use WhileActive to add the FX and OnRemove to remove the FX."

Spawning my particle from WhileActive

and destroying it in the OnRemove.

It is not as sexy or design friendly in the editor, But I can expose the Niagara and write a good doc :slight_smile:

As it is using unreliable packet, I don’t think there is an official solution, but I could revisit that is some of you have a suggestion.

2 Likes

Thing is I don’t need to think of workarounds i put the mode to mixed aswell everything is working perfectely in all modes.
People I see having troubles are not applying the effect from abilities. I am not claiming that’s impossible but it seems going outside the abilitysystem will require extra replication work.
Sure you can activate the effects manually in the WhileActive but that function in the source simply shows the effect actor and onremove simply hides it.
While cues can be unreliable i don’t think your setup meets those requirements (to manifest unreliability) anyway.

Also referring to the docs you linked watch out with the PreAttributeChange() because you will need

void UAttributeSet::PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const

to actually clamp your health.

I’ve kind of given up on relying on documentation and most certainly given up on following youtube tutorials about anything related to code. Those things have confused and misled me time and time again.

@ezgoin thanks for your inputs,

Sorry for the big thread :slight_smile:

I’m not activation an Gameplayeffect in the WhileActive, I’m only using the cue for particles.

Current setup is local 1 player, play as client (i’m building for MP but this is the easiest to repro)

On a actor collision I apply a GameplayEffect on the AbilityComponent

The Effect has a 2 sec duration , Period of 0.5 and Add a gameplayCue.

My gameplayCues are pretty simple, I removed all code and i’m doing nothing, I just want to see the Actor get activated then recycled. Which don’t happen
image

image

If we look at the stack of execution, we can see that the cue is removed from the client,with a packet notifying GameplayCueActorFinished, then we receive another HandleGameplayCue that GetInstancedCueActor and make it visible again.

I’m not 100% confident on the order of logs between the blueprint and C++, but the c++ stack enough give us enough information.

All this being said, if I change the Effect period to 0.51 instead of 0.5 the call to the last Notify append before the GameplayCueActorFinished and thus Recycling and removing the actor correctly.

This change of behavior is “understandable” even tho it is not “wanted” , as the server is processing both even at the exact same frame. But still show an underlining flaw with the Cue recycling that could be hard to track one in production in a more chaotic environment.

1 Like

Yeah i saw you just spawn the particles there sorry using the word effects might be confusing.
Try this by having it trigger an ability which will apply the effect. Because no matter the period it works and is replicated perfectly for me.
Like your projectile implements an interface with which you can retrieve the originator and feed that to your ability logic.

I have a theory that applying effects willy nilly is actually working against the system rather than with.
Everything is an ability right why would we go about applying things outside of this?
I mean it is tempting, i did it too but things did not work right.
Results from this experiment might confirm or deny.

Just to be clear :
The effect itself , that reduces the health works and stop correctly, it’s only the gameplay cue actor that reappear.

It was a good idea worth trying, however I get the same behavior, maybe there is something that changed between our versions.

On Collision with an item, I apply an ability using ABS

the ability gets the ABS from the player and apply the effect on it

The effect gets applied correctly ans stay for the right duration, but I received the same order of message that reactivates the Polled GameplayCue if I play with the timings (0.5 vs 0.51s) .

For anyone interested I have implemented a simple watchdog to recycle back in this case.

On Activation I set a flag that is removed only when all Effects for that cue are removed

Then when On Execute happens after the fact, I Recycle the actor again.

Thanks @ezgoin for sticking with me.

I can reproduce when i untick the option in the cue bp 'Auto Destroy on Remove"
edit: what happens for me is cue plays and stops correctly the first time but keeps playing from the second time i enable the ability.

That’s the execution path that calls the reuseonrecycle() hmm i’m gonna have a closer look at the others later today. Is there another bug or is this intended behaviour?

This should be good news if it works correctly with the auto remove.
I do think effects should be applied from within an ability because everything within the code of UGameplayAbility is set up for this.

Side note: Play as listen server and standalone works allright, it’s play as client that cause the behavior. (Auto removed is enabled)

I think we’re experiencing different bugs in different versions.

A well… Unreal 5.1 came out, so my behavior changed also :slight_smile:

In Play as ListenServer I get it to work without the watchdog.

But in play as client even the watchdog doesn’t cut it anymore as the second time the cue gets activated the removed is never called If I use period 0.5, duration 2.0. But period 0.51 works fine.

I wanted to get this clean, but I’m working on a proto for now so I might stick to ListenServer untill I get time to figure this out.

Also… ■■■■ 5.1 with debug symbol is over 100g

Yes in my version remove is never called because it falls through without resetting bHasHandledOnRemove and the other bHasHandled the auto destroy if condition does call the onfinished. That’s the function where they are resetted right in the start of the function.
Hence why it’s working the first but not the following.

I fixed this in my source and the prealloc spawns nicely attaches and then removes again after still nicely visible in the editor not attached attaching on activation.
Like it should have worked from the start.
As far as notifycue_actor goes i think i have it rather bugfree now.

this is where it goes wrong imho:

void AGameplayCueNotify_Actor::HandleGameplayCue(AActor* MyTarget, EGameplayCueEvent::Type EventType, const FGameplayCueParameters& Parameters)
SCOPE_CYCLE_COUNTER(STAT_HandleGameplayCueNotifyActor);
{
	if (Parameters.MatchedTagName.IsValid() == false)
	{
		ABILITY_LOG(Warning, TEXT("GameplayCue parameter is none for %s"), *GetNameSafe(this));
	}

	// Handle multiple event gating
	{
		if (EventType == EGameplayCueEvent::OnActive && !bAllowMultipleOnActiveEvents && bHasHandledOnActiveEvent)
		{
			return;
		}

		if (EventType == EGameplayCueEvent::WhileActive && !bAllowMultipleWhileActiveEvents && bHasHandledWhileActiveEvent)
		{
			ABILITY_LOG(Log, TEXT("GameplayCue Notify %s WhileActive already handled, skipping this one."), *GetName());
			return;
		}

		if (EventType == EGameplayCueEvent::Removed && bHasHandledOnRemoveEvent)
		{
			return;
		}
	}

	// If cvar is enabled, check that the target no longer has the matched tag before doing remove logic. This is a simple way of supporting stacking, such that if an actor has two sources giving him the same GC tag, it will not be removed when the first one is removed.
	if (GameplayCueNotifyTagCheckOnRemove > 0 && EventType == EGameplayCueEvent::Removed)
	{
		if (IGameplayTagAssetInterface* TagInterface = Cast<IGameplayTagAssetInterface>(MyTarget))
		{
			if (TagInterface->HasMatchingGameplayTag(Parameters.MatchedTagName))
			{
				return;
			}			
		}
	}

	if (IsValid(MyTarget))
	{
		K2_HandleGameplayCue(MyTarget, EventType, Parameters);

		// Clear any pending auto-destroy that may have occurred from a previous OnRemove
		SetLifeSpan(0.f);

		switch (EventType)
		{
		case EGameplayCueEvent::OnActive:
			OnActive(MyTarget, Parameters);
			bHasHandledOnActiveEvent = true;
			break;

		case EGameplayCueEvent::WhileActive:
			WhileActive(MyTarget, Parameters);
			bHasHandledWhileActiveEvent = true;
			break;

		case EGameplayCueEvent::Executed:
			OnExecute(MyTarget, Parameters);
			break;

		case EGameplayCueEvent::Removed:
			bHasHandledOnRemoveEvent = true;
			OnRemove(MyTarget, Parameters);

			if (bAutoDestroyOnRemove)
			{
				if (AutoDestroyDelay > 0.f)
				{
					FTimerDelegate Delegate = FTimerDelegate::CreateUObject(this, &AGameplayCueNotify_Actor::GameplayCueFinishedCallback);
					GetWorld()->GetTimerManager().SetTimer(FinishTimerHandle, Delegate, AutoDestroyDelay, false);
				}
				else
				{
					GameplayCueFinishedCallback();
				}
			}
			break;
		};
	}
	else
	{
		ABILITY_LOG(Warning, TEXT("Null Target called for event %d on GameplayCueNotifyActor %s"), (int32)EventType, *GetName() );
		if (EventType == EGameplayCueEvent::Removed)
		{
			// Make sure the removed event is handled so that we don't leak GC notify actors
			GameplayCueFinishedCallback();
		}
	}

The Removed case without the Auto Destroy on Remove falls through and nothing catches it to reset these variables that really do need resetting.
Imo GameplayCueFinishedCallback(); should always be called unless there is a delay set for the Auto Destroy on Remove. In that case the GameplayCueFinishedCallback(); is called delayed.

1 Like

Thanks I’ll give it a look

I’ve settled on this:

void ASnortGameplayCueNotify_Actor::HandleGameplayCue(AActor* MyTarget, EGameplayCueEvent::Type EventType, const FGameplayCueParameters& Parameters)
{
	Super::HandleGameplayCue(MyTarget, EventType, Parameters);

	if (EventType == EGameplayCueEvent::Removed && !bAutoDestroyOnRemove && bHasHandledOnRemoveEvent && IsValid(MyTarget))
	{
		bHasHandledOnActiveEvent = false;
		bHasHandledWhileActiveEvent = false;
		bHasHandledOnRemoveEvent = false;
	}
}

Keeps the autodestroy logic intact and fixes the oversight.

2 Likes

If you’re finding your Niagara particle actor to be at the location where the previous ended when reactivating, (I have only worked with niagara up until now) when using Auto Destroy On Remove. You will need to use NiagaraComponent->Deactivate(); before the next use and NiagaraComponent->Activate(); on OnActive or WhileActive. This will ensure it’s only visible at your owning actor’s location when auto attach to owner is set.
Without Auto Destroy it just works because the actor is never detached.

This whole actornotify class really needs tailor made derived classes to obtain the desired effects. I really do pitty the blueprint guys on this one.

I haven’t found out why the reattach is only updated on activation cause as soon as the effect ends it is detached and disowned and immediately reowned and attached and ready for activation, this is done in UGameplayCueManager::GetInstancedCueActor but in the editor it isn’t attached at all.
So it plays on the last location and you see it jump to the actor.

Just to clarify all these problems occur when using network play, in standalone everything just seems to work.

1 Like

I can confirm this is still present in 5.1 - however, it presents slightly different: In both Standalone and Listen Server modes my particle system never goes away. In Client mode, however, it goes away like I expect it to.

1 Like

Still present in 5.3

It’s been fixed on 5.4.

If you’re on 5.3 like I am, and can’t build a custom engine version, luckily you can apply their intermediate fix since AbilitySystemComponent::RemoveGameplayCue_Internal is virtual:

// Copied from UE 5.4 to fix Cues not being removed on server
// git deb931486115a3ab79e319db81821366dfdb969c
void ULyraAbilitySystemComponent::RemoveGameplayCue_Internal(const FGameplayTag GameplayCueTag, FActiveGameplayCueContainer& GameplayCueContainer)
{
	if (IsOwnerActorAuthoritative())
	{
		int32 NumMatchingCues = 0;
		for (const FActiveGameplayCue& GameplayCue : GameplayCueContainer.GameplayCues)
		{
			NumMatchingCues += (GameplayCue.GameplayCueTag == GameplayCueTag);
		}

		if (NumMatchingCues > 0)
		{
			// AbilitySystem.GameplayCueNotifyTagCheckOnRemove assumes the tag is removed before any invocation of EGameplayCueEvent::Removed.
			// We cannot use GameplayCueContainer.RemoveCue because that removes the cues while updating the TagMap.
			// Instead, we need to manually count the removals, update the tag map, then Invoke the Cue events while removing the Cues.
			UpdateTagMap(GameplayCueTag, -NumMatchingCues);

			for (int32 Index = GameplayCueContainer.GameplayCues.Num() - 1; Index >= 0; --Index)
			{
				const FActiveGameplayCue& GameplayCue = GameplayCueContainer.GameplayCues[Index];
				if (GameplayCue.GameplayCueTag == GameplayCueTag)
				{
					// Call on server here, clients get it from repnotify on the GameplayCueContainer rather than a multicast rpc
					InvokeGameplayCueEvent(GameplayCueTag, EGameplayCueEvent::Removed, GameplayCue.Parameters);
					GameplayCueContainer.GameplayCues.RemoveAt(Index);
				}
			}

			// Ensure that the clients are aware of these changes ASAP
			GameplayCueContainer.MarkArrayDirty();
			ForceReplication();
		}
	}
	else if (ScopedPredictionKey.IsLocalClientKey())
	{
		GameplayCueContainer.PredictiveRemove(GameplayCueTag);
	}
}

The final fix is 45e395166cdb14ef49088b945e61fe3196efbd9c.

1 Like