I have a tickable world system handling my projectiles movement logic, but if a projectile is destroyed during the iteration it causes the following error.
Container has changed during ranged-for iteration!
I’m having a hard time figuring out how to structure the for loop to avoid this. I currently have the following.
I think I’m on the right track here, but the for loop isn’t structured right and the examples I’ve found in source are confusing, which all seam to be TArray but I’m using TSet. Does anyone have any suggestions?
The loop should work no problems unless you are changing the projectiles mid loop (adding or deleting them)
MyActor
.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TraceProjectile.h"
#include "MyActor.generated.h"
UCLASS()
class YOUR_API AMyActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMyActor();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
UPROPERTY(BlueprintReadWrite,EditAnywhere)
TSet<UTraceProjectile*> Projectiles;
UFUNCTION(BlueprintCallable)
void UpdateProjectiles(float DeltaTime);
};
.cpp
#include "MyActor.h"
AMyActor::AMyActor()
{
PrimaryActorTick.bCanEverTick = true;
}
void AMyActor::BeginPlay()
{
Super::BeginPlay();
Projectiles.Empty();
UWorld* world = GetWorld();
for (int i = 0; i < 10; i++) {
UTraceProjectile* tp = NewObject<UTraceProjectile>(this);
tp->RegisterComponent();
tp->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::SnapToTargetNotIncludingScale);
if (tp != nullptr) {
Projectiles.Add(tp);
}
}
}
// Called every frame
void AMyActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
UpdateProjectiles(DeltaTime);
}
void AMyActor::UpdateProjectiles(float DeltaTime)
{
for (UTraceProjectile* Projectile : Projectiles) {
Projectile->UpdateProjectile(DeltaTime);
if (GEngine) {
GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Cyan, FString::Printf(TEXT("Projectile %s Speed: %s"), *Projectile->GetName(), *FString::SanitizeFloat(Projectile->InitSpeed)));
}
}
}
TraceProjectile
.h
#pragma once
#include "CoreMinimal.h"
#include "Components/SceneComponent.h"
#include "TraceProjectile.generated.h"
UCLASS(Blueprintable,BlueprintType)
class YOUR_API UTraceProjectile : public USceneComponent
{
GENERATED_BODY()
public:
UTraceProjectile();
UPROPERTY(BlueprintReadWrite,EditAnywhere)
float InitSpeed = 3000;
UFUNCTION(BlueprintCallable)
void UpdateProjectile(float DeltaTime);
};
As noted in my post they can be destroyed during the loop. The way around this is a CreateConstIterator or CreateIterator, but I can’t figure out how they work for TSet. Otherwise the only other thing I can think of is using TArray and looping it in reverse to avoid skipping indexes.
I put in a parallel array that holds the set element to be able to quickly access it via index then added a destroy function. No errors, the tick function iterates constantly over the array.
The set elements drop in real-time with no complaint.
You can’t modify an iterator while itinerating it. I don’t know what more to tell you, lol.
The projectiles don’t all get destroyed at a perfect set time. There’s variance based off where they collided, etc…
All I’m trying to figure out is how to properly itinerate a TSet while allowing modifying it at the same time. Normally you’d just do a for loop against the number of entries via a TArray, but this is a TSet and I can’t do that. The document states I need to use CreateConstIterator, but I can’t figure out how to get it to work from their example.
Projectiles will be added to the TSet and removed from it mid-loop. That’s why it’s throwing for-range errors. You can’t do this. I’m aware you can’t do this, but that’s not really what I’m asking about.
I’m asking about how to loop them so that I can modify the TSet mid-loop and the way to do that is an index loop for TArray, but for TSet the documentation states to use CreateConstIterator, but I can’t figure out how.
This isn’t really Unreal Engine specific issue as modifying a for-range invalidates the range in C++ in general. So even though your current example isn’t erroring now it can and eventually will and in compiled release would crash the game. For example my current test has 1000 projectiles blipping in and out of existence and absolutely triggers this since some are being added/remove at any given time during the loop.
I think I’m getting closer with the below, but still not right yet since Projectile is a TIterator and not the projectile object yet.
Seems there is even a simpler version skipping the getasset variant:
for (auto Projectile = Projectiles.CreateConstIterator(); Projectile; ++Projectile)
{
if (UTraceProjectile* projectile = Cast<UTraceProjectile>(*Projectile))
{
projectile->UpdateProjectile(DeltaTime);
}
}
If you are destroying the projectile in the update function then don’t. Set a boolean flag bDestruct = true. The after the update checj tge flag, if true put the element into an array (declared outside of the loop). Once the foreach loop is complete check if the array has any elements, if so then do a for loop and delete each element.
You will skip the problem of deleting in a foreach loop then. You can delete from within tarrays
Projectiles can be added/removed during the loop so it’s not just a matter of avoiding the removals and doing them later. I’d have to schedule add/remove for later, which just seams like not great code to have to do. Will give the above a try as that’s what I was trying to figure out from source, but was having a hard time. Worst case I think I can just call Array() and loop it instead of the TSet.