NDC Gameplay Burst - Entry release assumes Infinite loop behavior

Summary:

We are using NDC Gameplay Burst for crowd death effects. The Niagara System managing the crowd moves over time, so its OwningComponent position changes and data gets written to different Grid cells.

Issue:

When the Burst receiver NS has its LoopBehavior set to anything other than Infinite (e.g., Once), the Entry is never released after the NS completes, and no new NS is re-spawned.

We traced the cause in engine code to the following check in FNDCMapEntryBase::BeginFrame:

bInUse = Data.IsUnique() == false;

The writer NS’s (crowd NS) NDC Write DI caches DataChannelData (a SharedPtr), and even with bUpdateDestinationDataEveryTick = true, the reference is never released. As a result, IsUnique() never returns true, and the Entry is never freed — even after the Burst NS has completed.

Setting LoopBehavior to Infinite makes it work, but since the crowd NS moves across cell boundaries, new Entries and NSs accumulate indefinitely without being released, causing a memory leak.

Questions:

Is using a non-Infinite loop on the receiver NS a supported use case for NDC Gameplay Burst?

Is there a recommended pattern for releasing old cell Entries when writing from a moving OwnerComponent?

Is it safe to add logic to FNDCMapEntry_GameplayBurst::BeginFrame that forces Entry release when all SpawnedComponents are Complete? Are there any side effects we should be aware of?

Interim Workaround:

We have applied the following modification to NiagaraDataChannel_GameplayBurst.cpp:

bool FNDCMapEntry_GameplayBurst::BeginFrame(float DeltaTime, FNiagaraWorldManager* OwningWorld, const FNDCMapKey& Key)
{
  if (SpawnedComponents.Num() > 0)
  {
    bool bAllComplete = true;
    for (const TObjectPtr<UNiagaraComponent>& Comp : SpawnedComponents)
    {
      if (Comp && !Comp->IsComplete())
      {
        bAllComplete = false;
        break;
      }
    }
    if (bAllComplete)
    {
      return false;
    }
  }

  bool bAttachmentCompValid = AttachComponent.IsExplicitlyNull() || AttachComponent.IsValid();
  return Super::BeginFrame(DeltaTime, OwningWorld, Key) && bAttachmentCompValid;
}



[Attachment Removed]

Hello,

Apologies for the delay, I’ve reached out to my colleague who is more familiar with this system and hope to have more information for you soon.

[Attachment Removed]

Hi,

Thanks for the detailed investigation of the issue.

Yeah I think this is an issue with burst recievers and long lived writers who hold onto the reference.

If you need to follow this pattern, a safer fix would be to re-activate the receiver systems imo.

However a cleaner pattern is to have infinitely lived looping recievers that clean themselves up when unused using CompleteIfUnused module in the system update.

Then you can have wrirters moving through the world activating new cells as needed and old cells clean themselves up.

This pattern is also useful when you have unpredicable writers that may often occupy the same cells.

A 5s delay on cleanup of the receiver system can avoid constant activation churn cost for new receivers as writers leave and re-enter the same cell.

[Attachment Removed]

Thank you for the response.

I tried adding the CompleteIfUnused module to the System Update, and it is now working correctly.

[Attachment Removed]

Awesome. I’ll close out this ticket.

Lmk if you have any other issues.

[Attachment Removed]