A Grid-Based Side-Scroller Auto-Battler built in Unity 6 with URP 2D.
Inspired by management-strategy titles like Dwarves: Glory, Death and Loot and Totally Accurate Battle Simulator — tactical army preparation meets fully automated, physics-driven combat.
Talha's Researches - Go to Web Documentation
🎮 Play on itch.io: talha-dogan.itch.io/ottomanchessauto
Engine: Unity 6000.4.6f1 · Universal Render Pipeline 2D · Language: C# / .NET Standard 2.1
- Game Overview
- Core Gameplay Loop
- Grid & Spawning System
- AI & Combat Logic
- Equipment & Upgrade System
- Technical Architecture
- Project Structure
- System Dependency Map
- Key Constants Reference
- Designer Guides
- Screenshots
- Planned Features & Roadmap
OttomanChessAuto is a 2D side-scrolling tactical auto-battler. Players act as battlefield commanders: configure your army composition and equipment loadout before each engagement, then watch your Ottoman soldiers fight autonomously using a custom AI system.
The visual perspective is a lateral side-scroller — units face each other across a horizontal battlefield, with player forces on the left and enemy formations on the right. Combat is fully physics-driven via Unity's 2D Rigidbody system, giving units natural push-and-slide interactions as they close in on their targets.
Three Scenes:
| Scene | Purpose |
|---|---|
StartScene |
Main menu, settings panel, game entry point |
UpgradeScene |
Army management, equipment drag-and-drop, pawn shop, loot boxes |
GridScene |
Main battle arena — preparation + combat + resolution |
The game alternates between two distinct phases per level:
┌─────────────────────────────────────────────────────────────────┐
│ PREPARATION PHASE │
│ │
│ • LevelManager loads the next LevelDataSO asset │
│ • UnitSpawner places the enemy formation (EnemyFormationSO) │
│ onto the right side of the grid (Columns 6–7) │
│ • Player reviews the enemy layout and presses [WAR!] │
│ • UnitSpawner spawns player units onto Column 0 │
│ │
└──────────────────────────────┬──────────────────────────────────┘
│ WAR! button pressed
▼
┌─────────────────────────────────────────────────────────────────┐
│ COMBAT PHASE │
│ │
│ • BattleManager.StartBattle() sets isBattleStarted = true │
│ • All BaseUnit FSMs unlock and begin executing │
│ • Units autonomously seek targets via Lane Priority AI │
│ • Melee units close distance and swing; Ranged units fire │
│ pooled projectiles via ProjectileFactory │
│ • BattleManager monitors unit deaths via OnDeath events │
│ • When one side reaches 0 units → Win or Lose broadcast │
│ │
└──────────────────────────────┬──────────────────────────────────┘
│ GameEvents.LevelWin / LevelLose
▼
┌─────────────────────────────────────────────────────────────────┐
│ RESOLUTION PHASE │
│ │
│ • LevelManager awards gold (goldReward from LevelDataSO) │
│ • Surviving units play Victory animation │
│ • Board is cleared (UnitFactory returns all units to pools) │
│ • GridManager.ResetGrid() wipes all occupancy flags │
│ • Next LevelDataSO is loaded after a 3-second delay │
│ │
└─────────────────────────────────────────────────────────────────┘
The entire battlefield is defined by a single GridManager singleton that owns an authoritative GridNode[8, 8] data model. The grid is generated once in Awake() and never changes at runtime.
| Property | Value |
|---|---|
| Dimensions | 8 columns × 8 rows |
Origin [0,0] world center |
(1.3, 1.3, 0) |
| Cell size (center-to-center) | 2.6 Unity units |
| Column axis | X → left (0) to right (7) |
| Row axis | Y → bottom (0) to top (7) |
GridNode is a plain C# class (not a MonoBehaviour) storing grid coordinates, pre-calculated world-space position, and a mutable occupancy flag. An OnDrawGizmos() overlay renders the grid in the Scene view at all times — green for free nodes, red for occupied ones.
Side-Scroller Lateral View (Y = row / "lane"):
Col: 0 1 2 3 4 5 6 7
┌────────────────────────────┐
│ [P] ──────────── [E2] [E1] │ Row 7
│ [P] ──────────── [E2] [E1] │ Row 6
│ [P] ──────────── [E2] [E1] │ Row 5
│ [P] ──────────── [E2] [E1] │ Row 4
│ [P] ──────────── [E2] [E1] │ Row 3
│ [P] ──────────── [E2] [E1] │ Row 2
│ [P] ──────────── [E2] [E1] │ Row 1
│ [P] ──────────── [E2] [E1] │ Row 0
└────────────────────────────┘
Player Overflow Primary
Col 0 Col 6 Col 7
Player Spawning (UnitSpawner.SpawnPlayerUnits) — Column X = 0. Melee units fill rows Y = 0..7 first (front line), then Ranged units fill remaining rows. Maximum 8 units.
Enemy Spawning (UnitSpawner.SpawnEnemyFormation) — Two-pass, two-column strategy:
| Pass | Column | Rows | Capacity |
|---|---|---|---|
| Pass 1 — Primary | X = 7 |
Y = 0..7 |
8 units |
| Pass 2 — Overflow | X = 6 |
Y = 0..7 |
8 units |
| Total | 16 units max |
Every unit runs a lightweight FSM with five states, managed entirely inside BaseUnit:
┌──────────────────────────────────────┐
│ │
┌─────────▼──────────┐ ┌───────────▼──────────┐
│ IDLE STATE │◄─────────────│ ATTACKING STATE │
│ Scans for targets │ target dead │ Fires on cooldown │
└─────────┬──────────┘ └───────────▲──────────┘
│ target found │ in range
▼ │
┌─────────────────────┐ │
│ MOVING STATE ├─────────────────────────┘
│ Closes on target │
└─────────┬───────────┘
│ health = 0
▼
┌─────────────────────┐ ┌─────────────────────┐
│ DEAD STATE │ │ VICTORY STATE │
│ Returns to pool │ │ Plays bounce anim │
└─────────────────────┘ └─────────────────────┘
State objects (IdleState, MovingState, AttackingState) are shared static instances — one object per state type, not per unit, eliminating per-unit allocations.
BattleManager.GetClosestTargetFor() implements a three-tier Lane Priority system:
┌──────────────────────────────────────────────────────────────────┐
│ LANE PRIORITY TARGETING │
│ │
│ TIER 1 — Same Lane (seekerRow == candidateRow) │
│ → Find the closest living enemy in the exact same row. │
│ │
│ TIER 2 — Adjacent Lanes (seekerRow ± 1, excluding same row) │
│ → Only reached when the seeker's own lane is empty. │
│ → Prevents units from marching across the entire board. │
│ │
│ TIER 3 — Global Fallback (entire opposing team) │
│ → Only reached when same AND adjacent lanes are empty. │
│ → Returns the absolute closest living enemy anywhere. │
│ │
│ All distance comparisons use sqrMagnitude (no Mathf.Sqrt). │
└──────────────────────────────────────────────────────────────────┘
Before escalating to the global BattleManager query, each unit runs a local Physics2D.OverlapCircle scan (radius = attackRange × 1.5) using the non-allocating ContactFilter2D API. A hysteresis factor (0.5625 = 0.75²) prevents target-switching jitter — a new candidate must be significantly closer before a switch occurs.
Units that die during a foreach traversal are not removed immediately. BattleManager uses a _pendingRemovals queue: deaths are queued in Update() and the queue is flushed safely in LateUpdate() — preventing InvalidOperationException: Collection was modified without requiring list copies on every frame.
The Upgrade Scene provides a full drag-and-drop equipment management interface for the player's army of up to 8 units.
EquipmentDataSO — Each piece of equipment defines identity (equipmentName, EquipmentSlot), stat bonuses (bonusHealth, bonusDamage, bonusAttackSpeed), and an Addressable sprite loaded on demand.
PlayerArmyDataSO — A persistent ScriptableObject shared between scenes. Stores 8 ArmySlot entries, each containing a base unit reference and five optional equipment slots (Helmet, Vest, Pants, Weapon, Shield).
Pawn Shop — Players start with 1 unlocked pawn slot (free). Additional slots cost 300 gold each, up to a maximum of 8. Slot count is persisted via GameSaveService.
Loot Box System — Costs 200 gold per box. Spawns a random equipment item into the stash with an open/close animation.
| UI Component | Responsibility |
|---|---|
UpgradeDragItemUI |
Handles pointer drag events, creates ghost image |
UpgradeCharacterDropZoneUI |
Accepts drops onto character equipment slots |
StashDropZoneUI |
Accepts drops back into the inventory stash |
ShowcasePawnController |
Manages 3D pawn showcase visibility |
| Grade | System / Pattern | Status |
|---|---|---|
| A | SOLID Principles | ✅ Fully applied across all systems |
| A | Abstraction | ✅ BaseUnit, BaseUnitDataSO, IGameView, IUnitState |
| A | Singleton | ✅ 9 singletons — BattleManager, GridManager, LevelManager, UnitFactory, UnitSpawner, ProjectileFactory, GameSaveService, SoundManager, VFXManager |
| A | ScriptableObject Architecture | ✅ Fully data-driven: units, levels, formations, equipment, stat progressions |
| A | Factory Pattern | ✅ UnitFactory + ProjectileFactory with ObjectPool<T> |
| A | Polymorphism | ✅ BaseUnit → MeleeUnit / RangedUnit hierarchy |
| A | Decoupling | ✅ GameEvents static event bus — zero direct cross-system references |
| B | Projectile Pooling | ✅ ProjectileFactory — one pool per unique prefab |
| B | Floating Text Pooling | ✅ DamageTextManager — pooled damage numbers |
| B | VFX Pooling | ✅ VFXManager — ObjectPool<ParticleSystem> per VFX type |
| B | Enemy Pooling | ✅ UnitFactory — one pool per unique unit prefab |
| B | Sound Pooling | ✅ SoundManager — AudioSource pool (10 initial, 20 max) |
| B | MVP Pattern | ✅ GamePresenter + IGameView + GameUIManager |
| C | Lazy Loading | ✅ Addressables async for equipment sprites |
| C | Memory Unload | ✅ Addressable handle release on scene unload |
| C | Sprite Bundles | ✅ Addressable groups: Arena, Audio, Items, UI, Units |
| E | Async Scene Loading | ✅ LoadSceneAsync with allowSceneActivation control |
| F | Easing Library | ✅ EasingLibrary — 22 easing functions (Quad, Cubic, Bounce, Elastic…) |
| F | Stat Progression Curves | ✅ StatProgressionSO — AnimationCurve per stat |
| F | Movement & Spawn Pacing Curves | ✅ movementCurve + spawnPacingCurve in StatProgressionSO |
| G | Binary Save System | ✅ SaveManager — AES-256 encrypted binary + SHA-256 checksum |
| G | Save Versioning & Migration | ✅ CurrentSaveVersion = 2, SaveMigrationService v1→v2 |
| G | Save Meta Data | ✅ saveVersion, savedAt, saveCount, totalPlayTimeSeconds |
| H | New Input System | ✅ GameInputHandler — gamepad + keyboard unified |
| I | Debug & QA Tools | ✅ DebugToolsWindow — stage jump, inventory fill, stress test |
| J | Localization | ✅ LocalizationManager — TR/EN JSON files, fallback chain |
| J | Localization Key Naming | ✅ LocalizationKeys.cs — typed constants, no magic strings |
| D | Google Sheets Sync | ❌ Not implemented — planned for a future update |
| Principle | Implementation |
|---|---|
| Single Responsibility | UnitSpawner only spawns. BattleManager only manages battle state and targeting. GameUIManager only updates UI. UnitFactory only manages pools. Each class has one reason to change. |
| Open/Closed | BaseUnit is abstract and open for extension (MeleeUnit, RangedUnit) without modifying the base. BaseUnitDataSO is extended by MeleeUnitDataSO and RangedUnitDataSO. |
| Liskov Substitution | MeleeUnit and RangedUnit are fully substitutable for BaseUnit anywhere in the codebase (BattleManager.playerUnits, UnitFactory.CreateUnit). |
| Interface Segregation | IDamageable and IAttacker are separate, minimal interfaces. IUnitState defines only Enter, Execute, Exit. IGameView defines only the UI surface the Presenter needs. |
| Dependency Inversion | BattleManager depends on the BaseUnit abstraction, not concrete types. Projectile depends on IObjectPool<Projectile>. GamePresenter depends on IGameView, not GameUIManager. |
All game content is defined in ScriptableObject assets, completely decoupled from scene objects. Designers can create and tune levels, units, and equipment without touching code.
Assets/Scripts/Data/
├── BaseUnitDataSO.cs ← Abstract base: name, prefab, health, damage, speed, range, cooldown
│ ├── MeleeUnitDataSO.cs ← Extends base: swingAngle, swingDuration
│ └── RangedUnitDataSO.cs ← Extends base: projectilePrefab, projectileSpeed
│
├── LevelDataSO.cs ← meleeLimit, rangedLimit, unit data refs, formation ref, goldReward
├── StatProgressionSO.cs ← AnimationCurve per stat (health, damage, speed, cooldown, range…)
│
├── Units/
│ ├── EnemyFormationSO.cs ← List<UnitPlacement> { unitData, offset }
│ └── PlayerArmyDataSO.cs ← 8 ArmySlot entries (baseUnitData + 5 equipment refs)
│
├── Equipment/
│ ├── EquipmentDataSO.cs ← equipmentName, slot, bonuses, Addressable spriteReference
│ └── EquipmentSlot.cs ← Enum: Helmet, Vest, Pants, Weapon, Shield
│
└── Text/
└── DamageTextDataSO.cs ← Floating damage number visual configuration
All five pools use Unity's UnityEngine.Pool.ObjectPool<T> and follow an identical architectural pattern.
UnitFactory — One ObjectPool<BaseUnit> per unique unit prefab. A reverse lookup dictionary (_unitToPool) lets any unit release itself in O(1) without factory references.
ProjectileFactory — One pool per unique projectile prefab, shared across all units firing it. Previously each RangedUnit had its own pool — ProjectileFactory consolidates them into one shared pool per prefab (default capacity 10, max 60).
DamageTextManager — Pooled DamageText instances spawned above units on hit. Auto-returned after the float-and-fade animation completes.
VFXManager — One ObjectPool<ParticleSystem> per VFXType enum entry. All pools are pre-warmed at Awake(). A WaitWhile(() => ps.isPlaying) coroutine auto-returns each effect on completion.
SoundManager — Queue<AudioSource> idle pool + List<AudioSource> active list. 10 initial sources, 20 max. If the pool is exhausted, the oldest active source is forcibly reclaimed. Pitch variation and random clip selection are applied per-play.
GameEvents is a static C# event bus — no MonoBehaviour, no scene references. Any system broadcasts by invoking a method; any system reacts by subscribing/unsubscribing in OnEnable/OnDisable.
// BattleManager broadcasts — never touches UI directly
GameEvents.LevelWin(rewardMessage);
// SoundManager reacts independently
GameEvents.OnLevelWin += OnLevelWin;
// VFXManager reacts independently
GameEvents.OnUnitDied += OnUnitDied;
// GamePresenter routes to IGameView (UI layer)
GameEvents.OnLevelWin += HandleLevelWin;GameEvents.ClearAllEvents() is called by SceneCleanupPipeline on every scene unload to prevent stale scene-local subscribers from receiving events after their scene is gone.
The battle UI follows a strict Model → Presenter → View flow:
- Model —
GameEventsstatic bus fires raw data events (int gold,string message). - Presenter —
GamePresentersubscribes toGameEvents, translates events into display decisions, and calls methods onIGameView. Zero knowledge of Unity components. - View —
IGameViewdefines three methods:ShowStatusText,ShowLevelIndex,ShowGold.GameUIManagerimplements this interface and is the only class allowed to write toTextMeshProcomponents.
The entire UI layer can be replaced or mocked by swapping the IGameView implementation.
See AI & Combat Logic for the full FSM diagram. Key design decisions:
- State objects are shared static instances — no per-unit allocation.
DeadandVictorystates are handled inline inBaseUnit.Update()to avoid unnecessary interface dispatch.- Visual updates (
UpdateVisuals()) run before the battle-logic gate, ensuring animations play during the pre-battle placement phase.
Player progression (gold, inventory, army composition, active level) is securely saved via SaveManager.
- Binary + AES-256 Encryption — Data is serialised to JSON, encrypted with AES-256-CBC, and written as raw bytes to
gamesave.datinApplication.persistentDataPath. - SHA-256 Checksum — Computed from the full JSON string and embedded before every write. Validated on every load to detect tampered files.
- Automatic Backup — Before every write, the current file is copied to
gamesave.dat.bak. If the primary file fails on load, the backup is tried automatically. - Versioning & Migration —
CurrentSaveVersion = 2.SaveMigrationServiceapplies migrations step-by-step (v1→v2: added inventory list, language code, null-safe army slots). No data is ever discarded. - Meta Fields — Every save carries
saveVersion,savedAt(ISO 8601 UTC),totalPlayTimeSeconds, andsaveCount.
All in-game text is managed through a custom JSON-based system via LocalizationManager. Currently supports Turkish (TR, default) and English (EN).
- No magic strings — All keys are
const stringfields inLocalizationKeys.csfollowing adomain.objectnaming convention (ui.start_button,battle.level_cleared,item.sword). - Two-level fallback chain — Current language → English → raw key string.
- Live switching —
LocalizationManager.SetLanguage(code)swaps the active dictionary and firesOnLanguageChangedso allLocalizedTextcomponents refresh simultaneously without manual wiring. - Parameterised keys —
{0},{1}placeholders in JSON resolved viastring.Format.
EasingLibrary — 22 easing functions typed by the EaseType enum (Quad, Cubic, Bounce, Elastic, and more).
StatProgressionSO — Replaces every flat numeric stat with an AnimationCurve evaluated on a normalised level value [0, 1]. Designers reshape curves in the Inspector with no code changes.
healthCurve Linear(0→50, 1→500) — HP scaling
damageCurve Linear(0→10, 1→100) — damage scaling
attackCooldownCurve Linear(0→2s, 1→0.5s) — attack speed
goldRewardCurve Linear(0→10g, 1→200g) — economy curve
movementCurve EaseInOut(0,0, 1,1) — unit acceleration profile
spawnPacingCurve EaseInOut(0,2s, 1,0.3s) — enemy trickle → assault
- Lazy Loading — Equipment sprites are stored as
AssetReferenceT<Sprite>and only loaded into memory when the slot is opened. Handles are registered withSceneCleanupPipelineand released automatically on scene unload. - Sprite Bundles — Five Addressable groups (
Units,Items,UI,Arena,Audio), each bundled asPackTogether. - Async Scene Loading —
SceneLoader.TransitionTo()fades to black → cleans up → loads additively withallowSceneActivation = false(holds at 90% until ready) → activates → fades back in. AnOnLoadProgressaction fires at each step for accurate progress bar display. - Scene Cleanup Pipeline — A deterministic 7-step
SceneCleanupPipelineruns before every unload: release Addressable handles → return pooled units → stop VFX → log sound state → clear GameEvents →UnloadUnusedAssets→GC.Collect().
GameInputHandler is the sole integration point with the Unity New Input System. It wraps the auto-generated InputSystem_Actions class and exposes clean C# events — no other class references InputSystem_Actions directly.
| Action | Keyboard | Gamepad |
|---|---|---|
| Confirm / Click | Space / Left Click | A / Cross |
| Cancel | Escape / Right Click | B / Circle |
| Ready (WAR!) | Enter | Start |
| Speed Cycle | Tab | RB |
| Navigate | WASD / Arrow Keys | Left Stick / D-Pad |
Two action maps are toggled: SwitchToUI() (Upgrade scene — drag-and-drop) and SwitchToPlayer() (Grid scene — battle controls). GamepadCursor drives a VirtualMouseInput component from the left stick, enabling UGUI drag-and-drop to work identically on gamepad and mouse.
DebugToolsWindow is a custom EditorWindow (Tools → TDEV → Debug Tools, Ctrl+Shift+D) with six collapsible sections, functional in both Edit Mode and Play Mode.
- Quick Stage Start — Jump to any level index instantly; Previous/Next level navigation via Reflection; Auto Start Battle toggle.
- Quick Inventory Test — Unlock all 8 pawn slots; fill random equipment into all army slots; clear all equipment; add/reset gold.
- Quick Time Test —
Time.timeScaleslider (0–10) with preset buttons (0.25×, 0.5×, 1×, 2×, 4×, 8×). Live FPS and Fixed Timestep readout. - Stress Testing — Spawn 1–64 units on random grid nodes (both teams or enemy only); Clear All Units with full pool + state reset. Live unit count display per team.
|
|
Assets/
├── Scripts/
│ ├── Core/ // Managers, Bootstrap, Singletons, GameEvents
│ ├── Units/ // BaseUnit, MeleeUnit, RangedUnit, FSM States
│ ├── Data/ // ScriptableObjects (LevelData, UnitData, Equipment)
│ ├── UI/ // Presenter, IGameView, Drag-Drop Systems
│ ├── Systems/ // Save, Localization, VFX, Easing, SceneLoader
│ ├── Managers/ // UnitFactory, ProjectileFactory, SoundManager
│ └── Editor/ // DebugToolsWindow, AddressablesGroupSetup
├── Prefabs/
│ ├── Units/ // Player and Enemy unit prefabs
│ ├── UI/ // Menus, popups, drag items
│ └── VFX/ // Particle systems
├── GameData/
│ ├── Levels/ // 212 LevelDataSO instances
│ ├── Units/ // EnemyFormationSO, PlayerArmyDataSO assets
│ └── Equipment/ // EquipmentDataSO assets (Gun, Sword, Shield…)
└── AddressableAssets/ // Sprites (Items, Units, UI, Arena), Audio
Systems are designed with strictly unidirectional dependencies:
UnitSpawner ──► GridManager, UnitFactory, LevelManager
BattleManager ──► GridManager, GameEvents
GamePresenter ──► GameEvents, IGameView
RangedUnit ──► ProjectileFactory
GameUIManager ──► Unity UI components only (zero game logic)
VFXManager ──► GameEvents (subscribes), ParticleSystem pools
SoundManager ──► GameEvents (subscribes), AudioSource pool
SceneLoader ──► SceneCleanupPipeline, FaderUI
SaveManager ──► GameSaveService (persists), SaveMigrationService
| Constant | Value | Description |
|---|---|---|
GridXSize |
8 |
Number of columns on the battlefield |
GridYSize |
8 |
Number of rows (lanes) on the battlefield |
PlayerSpawnColumn |
0 |
Far-left column where player units spawn |
EnemyPrimaryColumn |
7 |
Far-right column where enemies spawn |
EnemyOverflowColumn |
6 |
Secondary enemy column when count exceeds 8 |
MaxArmySize |
8 |
Maximum units per side on the board |
HysteresisFactor |
0.5625f |
AI target-switch tolerance (0.75² units) |
AudioInitialPool |
10 |
Starting AudioSource pool size |
AudioMaxPool |
20 |
Hard cap on simultaneous audio sources |
CurrentSaveVersion |
2 |
Save schema version — drives migration |
Step-by-step instructions for adding content without touching any code:
Navigate to TDEV > Grid Level Generator from the top Unity menu to open the tool.
Expand the Enemy Unit Pool list and assign the enemy Scriptable Object files you want to include in the pool.
Under the Player Unit Setup section, drag and drop your player unit data into the Player Melee Data and Player Ranged Data fields.
In the Campaign Settings, set the total number of levels you wish to create via the Levels to Generate slider and define the Base Gold Reward.
Adjust the game's difficulty progression by modifying the Enemy Count Curve graph under Enemy Difficulty Curve (the X-axis represents normalized level progression, and the Y-axis sets the enemy count).
Once your setup is complete, click the Generate Diverse Levels button (e.g., Generate 500 Diverse Levels) at the bottom to batch-create your levels.
- In the Project window, right-click inside
GameData/Units/. - Select Create > AutoBattler > Units > Melee Unit Data (or Ranged).
- Name the SO and fill in base stats (Health, Damage, Speed, Range) via the Inspector.
- Drag the unit prefab from
Prefabs/Units/into theunitPrefabfield. - Optionally assign a
StatProgressionSOto enable level-based scaling.
- Create a new
LevelDataSOvia Create > TDEV > Level Data. - Create a new
EnemyFormationSOto define the enemy grid layout, and assign it to the Level Data. - Set
goldRewardfor level completion and the melee/ranged unit limits for the player. - Add the Level Data to the end of the Level List inside the
LevelManagerprefab.
- Create the item via Create > AutoBattler > Equipment > Equipment Data.
- Select the slot from the
EquipmentSlotenum (Helmet, Vest, Pants, Weapon, Shield). - Input the stat bonuses (
bonusHealth,bonusDamage,bonusAttackSpeed). - Add the item's sprite to the "Items" Addressables group, then link it to the
spriteReferencefield.
---




