Skip to main content

✨ 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 SpawnActor and 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 TActorIterator and 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.

graph LR A["SpawnActorDeferred
(actor exists, BeginPlay NOT run)"] --> B["Set properties
Damage, Speed, Owner..."] B --> C["FinishSpawning
(now BeginPlay runs)"]
Figure 1: Deferred spawning gives you a window to initialize the actor before it "wakes up" in BeginPlay.
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 → FinishSpawning when BeginPlay needs spawn-time data.
  • Destroy() removes an actor (never delete); it's deferred, so guard held pointers with IsValid(). SetLifeSpan auto-destroys.
  • Find actors with TActorIterator or GetAllActorsOfClass — 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.