View Format: Multi-Page Single Page

Simple Enemy Forge

Generate Complete Enemy Databases, Squads, Spawn Tables, and More in Minutes.

Wave Forge

Wave Forge orchestrates enemies and squads over time. It lets you design wave sequences for tower defense, horde mode, roguelike encounters, and any game mode that spawns enemies in timed waves. Each wave sequence is built from a three-level hierarchy: Sequences > Waves > Entries.

Wave Forge links to your Enemy Database (required) for individual enemy spawns and optionally to a Squad Database for spawning pre-configured squad groups. Open it from Window → Simple Enemy Forge → Wave Forge Wizard.

The wizard has 5 steps: Setup, Definitions, Sequences, Settings, and Generate.

Understanding the Wave Hierarchy

Wave sequences use a three-level nesting structure. A Sequence contains multiple Waves, and each wave contains multiple Entries that define what to spawn and when.

Wave Sequence: "Forest Assault" | +-- Wave 1: "Scout Wave" | +-- Entry: Goblin Scout (count: 3, delay: 0s, start: 0s) | +-- Entry: Wolf (count: 2, delay: 1s, start: 2s) | +-- Wave 2: "Main Force" | +-- Entry: Orc Warrior (count: 5, delay: 0.5s, start: 0s) | +-- Entry: ORC_SQUAD (squad, count: 1, start: 5s) | +-- Wave 3: "Boss Wave" +-- Entry: Forest Troll (count: 1, delay: 0s, start: 0s)

In this example, the "Forest Assault" sequence plays three waves in order. The first wave sends scouts and wolves, the second brings the main force including a full squad group, and the final wave spawns a boss.

Entry Structure

Each entry within a wave is either a single enemy or a squad group, toggled in the wizard. Entries have the following fields:

Field Type Description
enemyCode string Enemy code from the linked Enemy Database. Empty if using a squad.
squadCode string Squad code from the linked Squad Database. Empty if using a single enemy.
count int How many to spawn from this entry.
spawnDelay float Delay in seconds between each individual spawn within this entry.
startTime float Time offset in seconds from the start of the wave before this entry begins spawning.

The IsSquad property returns true if the entry references a squad code, allowing your spawn handler to branch between single-enemy and squad-group spawning logic.

Loop Settings

Each wave sequence has built-in loop settings for endless or escalating game modes:

Field Type Description
loopAfterLast bool When true, the sequence restarts from Wave 1 after the last wave finishes.
difficultyScalePerLoop float Multiplier applied each loop iteration (e.g., 1.2 means 20% harder per loop). Compounds across loops.
maxLoops int Maximum number of loop iterations. 0 means infinite looping.

Loop settings make Wave Forge ideal for endless modes. A sequence with loopAfterLast = true, difficultyScalePerLoop = 1.15, and maxLoops = 0 creates an infinitely escalating challenge.

Step 1 — Setup

Configure the basics and link your databases.

1 Class Prefix
Enter a prefix for generated types (e.g., "ForestWaves" generates ForestWavesType.cs, ForestWavesDatabase.cs, etc.)
2 Enemy Databases (Required)
Link one or more Enemy Database ScriptableObjects. These provide the enemy codes that entries can reference. At least one enemy database is required.
3 Squad Databases (Optional)
Link Squad Database ScriptableObjects if you want entries that can spawn entire squad groups. When linked, entries gain an enemy/squad toggle.

Multiple databases are supported for both types. When duplicates exist across databases, the first database's version wins.

Step 2 — Definitions

Define the dynamic property system for your wave sequences. These properties are attached to sequences, not individual waves or entries.

  • Categories — Dropdown properties like Biome, Difficulty Tier, or Game Mode
  • Flags — Boolean toggles like Is Tutorial, Has Boss Wave, Is Endless
  • Numerics — Number fields like Recommended Level, Total Enemy Count, Estimated Duration
  • Texts — String fields like Wave Briefing, Completion Reward Description

This step also provides schema export and import for AI-assisted content creation. Export your definitions as Full JSON, Light JSON, or Markdown, send them to an AI, and import the generated sequences back.

Step 3 — Sequences

Build your wave sequences in a split-panel editor. The left panel shows a paginated list of sequences, and the right panel shows the selected sequence's details.

For each sequence, you define:

  • Identity — Name, Code (auto-generated from name), Description
  • Waves — An ordered list of waves, each with a name, preDelay, postDelay, and entries
  • Entries per wave — Each entry has an enemy/squad toggle, code dropdown, count, spawnDelay, and startTime
  • Loop Settings — loopAfterLast, difficultyScalePerLoop, maxLoops
  • Dynamic Properties — Values for all categories, flags, numerics, and texts defined in Step 2

Waves and entries use nested ReorderableLists with foldouts, so you can reorder waves by dragging and expand/collapse each one to manage complex sequences.

Step 4 — Settings

Choose output paths for the generated scripts and database asset.

  • Scripts Path — Where to write the generated .cs files (enum, database, editor)
  • Asset Path — Where to create the .asset database file

Step 5 — Generate

Click Generate to create all output files. Wave Forge uses the same two-phase generation as all other forges:

1 Phase 1: Scripts
Generates {Prefix}Type.cs (enum), {Prefix}Database.cs (ScriptableObject), and {Prefix}DatabaseEditor.cs (custom editor). Triggers AssetDatabase.Refresh() and domain reload.
2 Phase 2: Asset
After domain reload, an [InitializeOnLoad] handler detects a SessionState flag and creates the {Prefix}Database.asset from temporary JSON data via delayCall.

After generation, your project will contain:

Assets/ └─ Generated/ ├─ Waves/Scripts/ │ ├─ ForestWavesType.cs (enum) │ ├─ ForestWavesDatabase.cs (ScriptableObject) │ └─ Editor/ │ └─ ForestWavesDatabaseEditor.cs (custom inspector) └─ Waves/Data/ └─ ForestWavesDatabase.asset (your data)

Runtime Helper: SimpleWaveRunner

SimpleWaveRunner is a pure C# event-driven wave runner with no MonoBehaviour dependency. You drive it by calling Update(deltaTime) each frame.

using SimpleEnemyForge; using UnityEngine; public class WaveController : MonoBehaviour { [SerializeField] ScriptableObject waveDatabase; SimpleWaveRunner runner = new SimpleWaveRunner(); void Start() { // Subscribe to events runner.OnWaveStarted += (index, name) => Debug.Log($"Wave {index + 1}: {name}"); runner.OnSpawnRequested += (request) => { if (request.IsSquad) SpawnSquad(request.squadCode, request.difficultyScale); else SpawnEnemy(request.enemyCode, request.difficultyScale); }; runner.OnWaveCompleted += (index, name) => Debug.Log($"Wave {index + 1} complete!"); runner.OnSequenceCompleted += () => ShowVictoryScreen(); runner.OnLoopStarted += (loopCount, scale) => Debug.Log($"Loop {loopCount}, difficulty: {scale:F1}x"); // Start a sequence var db = waveDatabase as ISimpleWaveDataSource; var sequence = db.GetWaveSequence("FOREST_ASSAULT"); if (sequence.HasValue) runner.Start(sequence.Value); } void Update() { runner.Update(Time.deltaTime); } }

Runner Controls

Method Description
Start(sequence) Begin running a wave sequence from the beginning.
Update(deltaTime) Drive the state machine forward. Call each frame.
Pause() Pause the runner. No spawns are dispatched until resumed.
Resume() Resume after pausing.
Stop() Stop the runner and reset to idle.
SkipCurrentWave() Skip remaining spawns in the current wave and advance to the next.
SkipToWave(index) Jump to a specific wave index.

Runner State

Property Type Description
CurrentSequence SimpleWaveSequence? The wave sequence currently being run, or null if idle/stopped. Read-only.
Phase WaveRunnerPhase Current phase: Idle, PreDelay, Spawning, PostDelay, or Complete.
CurrentWaveIndex int Index of the current wave within the sequence.
LoopCount int Number of completed loop iterations (0 on first play-through).
CurrentDifficultyScale float Cumulative difficulty scale (starts at 1.0, multiplied each loop).
ElapsedTime float Total elapsed time since Start, excluding paused time.
IsRunning bool True if actively running (not idle, not complete, not paused).
IsComplete bool True if the sequence has finished.
IsPaused bool True if the runner is paused.

Events

Event Args Description
OnWaveStarted (int waveIndex, string waveName) Fires when a wave's preDelay ends and spawning begins.
OnSpawnRequested (WaveSpawnRequest request) Fires once per individual spawn. Handler should instantiate the enemy or squad.
OnWaveCompleted (int waveIndex, string waveName) Fires when all entries in a wave have been dispatched.
OnSequenceCompleted (none) Fires when the entire sequence is done (no more waves or loops).
OnLoopStarted (int loopCount, float difficultyScale) Fires when the sequence loops back to the beginning.
OnPaused (none) Fires when the runner is paused.
OnResumed (none) Fires when the runner is resumed.

Runtime Data Structures

SimpleWaveSequence

A complete wave sequence with identity, waves, loop settings, and dynamic properties.

public struct SimpleWaveSequence { public string code; // Unique identifier (e.g., "FOREST_ASSAULT") public string name; // Display name public string description; // Description public SimpleWave[] waves; // Ordered waves in this sequence // Loop settings public bool loopAfterLast; // Whether to loop after the last wave public float difficultyScalePerLoop; // Multiplier applied per loop (e.g., 1.2) public int maxLoops; // Max loops (0 = infinite) // Dynamic properties public int[] categoryValues; public bool[] flagValues; public float[] numericValues; public string[] textValues; }

SimpleWave

A single wave within a sequence, containing timed spawn entries.

public struct SimpleWave { public string name; // Display name (e.g., "Scout Wave") public float preDelay; // Seconds before spawning begins public float postDelay; // Seconds after spawning before next wave public SimpleWaveEntry[] entries; // Entries to spawn during this wave }

SimpleWaveEntry

A single spawn entry within a wave — either an enemy or a squad.

public struct SimpleWaveEntry { public string enemyCode; // Enemy code (empty if squad) public string squadCode; // Squad code (empty if enemy) public int count; // How many to spawn public float spawnDelay; // Delay between each individual spawn public float startTime; // Offset from wave start public bool IsSquad => !string.IsNullOrEmpty(squadCode); }

ISimpleWaveDataSource

Interface implemented by generated wave databases.

public interface ISimpleWaveDataSource { int WaveSequenceCount { get; } SimpleWaveSequence[] GetWaveSequences(); SimpleWaveSequence? GetWaveSequence(string code); string[] GetWaveSequenceCodes(); // Property metadata string[] GetCategoryLabels(); string[] GetCategoryEntries(int categoryIndex); string[] GetFlagNames(); string[] GetNumericNames(); string[] GetTextNames(); // Utility queries SimpleWaveSequence[] GetSequencesContainingEnemy(string enemyCode); SimpleWaveSequence[] GetSequencesContainingSquad(string squadCode); }

Tips

Use preDelay and postDelay

Give players a breather between waves. A 3-5 second postDelay lets players prepare, while a short preDelay can display a "Wave incoming!" warning.

Mix Enemies and Squads

Combine individual enemy spawns with squad groups in the same wave. Use squads for coordinated groups and individual entries for stragglers or bosses.

Stagger with startTime

Use different startTime values within a wave to create staggered spawns. Fast enemies at 0s, ranged at 2s, and heavies at 5s creates natural pressure waves.

Loop for Endless Modes

Enable loopAfterLast with a difficultyScalePerLoop of 1.1-1.3 for escalating endless modes. Use maxLoops = 0 for truly infinite play.