✨ Lesson 3.4: Spawning, Destroying & Iterating Actors
Levels aren't static. Bullets, enemies, effects, and pickups come into being and vanish constantly. This lesson covers the runtime lifecycle from code: creating actors with SpawnActor, removing them with Destroy, and finding the ones already out there.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Spawn actors at runtime with
SpawnActorand set spawn transform & params - Use
TSubclassOf<T>to let designers pick what to spawn - Initialize spawn-time data safely with
SpawnActorDeferred - Destroy actors correctly and understand deferred destruction
- Find existing actors with
TActorIteratorand gameplay statics
Estimated Time: 60 minutes
Engine Version: Unreal Engine 5.8
In This Lesson
TSubclassOf: What to Spawn
To spawn something, code needs to know which class. Hardcoding AEnemy::StaticClass() works but ties the spawner to one type. TSubclassOf<T> is a type-safe class reference you expose as a UPROPERTY, so a designer picks the exact Blueprint to spawn from the editor.
// A designer sets this to BP_Goblin, BP_Skeleton, etc. — restricted to AEnemy subclasses.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning")
TSubclassOf<AEnemy> EnemyClass;
// A whole list of spawnable projectile types:
UPROPERTY(EditAnywhere, Category = "Spawning")
TArray<TSubclassOf<AProjectile>> ProjectileTypes;
📖 Why TSubclassOf and not a plain pointer?
TSubclassOf<AEnemy> guarantees at the editor level that only AEnemy (or subclass) can be assigned — the class picker filters to valid types, preventing "spawn a light as an enemy" mistakes. It's the standard, safe way to make spawning data-driven, and it's central to the hybrid pattern we complete in Module 6.
SpawnActor
The world spawns actors. You call GetWorld()->SpawnActor<T>(...) with a class, a transform, and optional parameters.
void ASpawner::SpawnEnemy()
{
if (!EnemyClass) { return; } // designer hasn't set a class
UWorld* World = GetWorld();
if (!World) { return; }
const FVector Location = GetActorLocation() + FVector(200.f, 0.f, 0.f);
const FRotator Rotation = GetActorRotation();
FActorSpawnParameters Params;
Params.Owner = this; // who spawned it (useful for damage credit)
Params.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
AEnemy* NewEnemy = World->SpawnActor<AEnemy>(EnemyClass, Location, Rotation, Params);
if (NewEnemy)
{
UE_LOG(LogTemp, Log, TEXT("Spawned %s"), *NewEnemy->GetName());
}
}
⚠️ Spawning can fail — always null-check
SpawnActor returns nullptr if it couldn't place the actor (for example, blocked collision with the default handling method). Never assume success. The SpawnCollisionHandlingOverride above tells the engine to nudge the location rather than refuse — useful when you'd rather spawn slightly off than not at all.
Deferred Spawning
Sometimes you need to set properties on an actor before its BeginPlay runs — for example, telling a projectile its damage and speed before it starts moving. A normal SpawnActor runs BeginPlay immediately, too late to configure. Deferred spawning splits it into two steps.
(actor exists, BeginPlay NOT run)"] --> B["Set properties
Damage, Speed, Owner..."] B --> C["FinishSpawning
(now BeginPlay runs)"]
void AWeapon::FireProjectile()
{
const FTransform SpawnTM(GetActorRotation(), GetMuzzleLocation());
// Step 1: create it, but don't run BeginPlay yet.
AProjectile* Proj = GetWorld()->SpawnActorDeferred<AProjectile>(
ProjectileClass, SpawnTM, this, GetInstigator());
if (Proj)
{
// Step 2: configure it while it's "asleep".
Proj->Damage = CurrentDamage;
Proj->Speed = MuzzleSpeed;
// Step 3: finish — NOW BeginPlay runs, with our values already set.
Proj->FinishSpawning(SpawnTM);
}
}
✅ When to reach for it
Use SpawnActorDeferred whenever an actor's BeginPlay depends on values only the spawner knows. If the actor is fully self-contained (a static pickup), a plain SpawnActor is simpler. This pattern returns in the Module 12 capstone for spawning configured gameplay objects.
Destroying Actors
To remove an actor, call Destroy(). Recall from Lesson 2.5: you never delete it — Destroy() marks it for removal and the garbage collector reclaims the memory later.
void AProjectile::OnImpact()
{
// Spawn an effect, apply damage... then remove ourselves.
Destroy(); // marked for destruction; GC frees it at the next collection
}
// A self-cleaning actor: destroy after a lifespan (great for effects/projectiles).
void AProjectile::BeginPlay()
{
Super::BeginPlay();
SetLifeSpan(3.0f); // auto-Destroy() after 3 seconds
}
⚠️ A destroyed actor isn't gone this instant
After Destroy(), the actor is pending kill but may still exist for the rest of the frame. Any pointer you hold to it should be checked with IsValid() before use (Lesson 2.5). And an actor should never assume it's safe to touch another actor it just asked to destroy. SetLifeSpan is a clean, self-contained way to auto-destroy short-lived actors without manual timers.
Finding Actors
To act on actors already in the world, you iterate them. Two common tools:
TActorIterator — the C++ way
#include "EngineUtils.h" // required for TActorIterator
void AGameManager::CountEnemies()
{
int32 Count = 0;
// Iterates every AEnemy currently in the world.
for (TActorIterator<AEnemy> It(GetWorld()); It; ++It)
{
AEnemy* Enemy = *It;
if (IsValid(Enemy) && !Enemy->IsDead())
{
++Count;
}
}
UE_LOG(LogTemp, Log, TEXT("Live enemies: %d"), Count);
}
UGameplayStatics — the convenient way
#include "Kismet/GameplayStatics.h"
TArray<AActor*> FoundEnemies;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AEnemy::StaticClass(), FoundEnemies);
// FoundEnemies now holds every AEnemy — but note the AActor* type, so Cast to use specifics.
⚠️ Iteration is not free — don't do it every frame
Both approaches walk actors in the world; on a large level that's real cost. Never call GetAllActorsOfClass or a full actor iterator inside Tick. Cache results, or better, have actors register themselves with a manager or subsystem on BeginPlay (Module 7) so you keep a ready list instead of searching. TActorIterator is generally faster than GetAllActorsOfClass for one-off scans.
Hands-on Exercise & Quiz
🏋️ Exercise: A wave spawner
Objective: Combine spawning, TSubclassOf, and iteration.
Write AWaveSpawner : public AActor that: exposes TSubclassOf<AEnemy> EnemyClass and int32 EnemiesPerWave; has SpawnWave() that spawns that many enemies around itself (null-check each); and has int32 GetLiveEnemyCount() const using TActorIterator. Bonus: make spawned projectiles/enemies self-destruct with SetLifeSpan if they wander (skip if not applicable).
💡 Hint — SpawnWave loop
for (int32 i = 0; i < EnemiesPerWave; ++i)
{
const FVector Loc = GetActorLocation() + FVector(i * 150.f, 0.f, 0.f);
AEnemy* E = GetWorld()->SpawnActor<AEnemy>(EnemyClass, Loc, GetActorRotation());
if (!E) { UE_LOG(LogTemp, Warning, TEXT("Spawn %d failed"), i); }
}
✅ Design note
For a real game, having each enemy register with the spawner on BeginPlay and unregister on EndPlay beats iterating every time you need a count — that's the pattern Module 7's subsystems formalize. Iteration is fine for occasional queries.
🎯 Quick Quiz
Question 1: What does SpawnActor return if placement fails?
Question 2: You must set a projectile's damage before its BeginPlay. Use:
Question 3: What's the problem with calling GetAllActorsOfClass every Tick?
Summary
🎉 Key Takeaways
- Expose
TSubclassOf<T>to let designers choose what to spawn safely. GetWorld()->SpawnActor<T>(Class, Location, Rotation, Params)spawns at runtime — always null-check the result.- Use
SpawnActorDeferred→ set properties →FinishSpawningwhenBeginPlayneeds spawn-time data. Destroy()removes an actor (neverdelete); it's deferred, so guard held pointers withIsValid().SetLifeSpanauto-destroys.- Find actors with
TActorIteratororGetAllActorsOfClass— but never every frame; cache or use registration.
📚 Additional Resources
🚀 What's Next?
Actors can now appear, act, and vanish. The last piece of the framework module is making them talk to each other — actor communication patterns: direct references, casting, and the interfaces and delegates that keep it clean.
🎉 Lesson complete!
The world is dynamic now. Let's get its inhabitants talking.