A tactical 2D core featuring tactical drawing mechanics and an autonomous combat system.
👉 Click here to play the WebGL build!
The loop here is dead simple: you sketch a formation on the ground, hit "WAR!", and let the AI take the wheel. I was aiming for a totally hands-off experience—once your units hit the field, they’re on their own. They handle their own targeting and movement logic without any babysitting from the player.
I'm not a fan of "spaghetti code," so I tried to keep this project as modular as possible using SOLID principles. It makes adding new units or mechanics way less of a headache:
- Event-Driven Chaos: I used a GameEvents bus so the UI and gameplay don't even need to know each other exist. They just trigger events, and whoever needs to listen, listens.
- Unit AI (FSM): Dealing with unit states can get messy fast. I stuck with a Finite State Machine so each unit always knows if it should be moving, attacking, or just standing there.
- Data-Driven (SO): All unit stats—health, damage, speed—are tucked away in ScriptableObjects. This means I can balance the game in the Inspector without waiting for the code to recompile every time.
- Optimization: Constant
InstantiateandDestroycalls are a performance killer. I built a custom Object Pooling system to recycle units and projectiles, keeping the frame rate steady.
A core mechanic isn't complete without the right "juice." The visual systems are completely decoupled from the logic:
- Breathing & Locomotion: Units have idle breathing animations and walking sways (
UnitBreathingVisuals). - Dynamic Aiming: Ranged units track targets with their weapons, and melee units have dedicated swing animations.
- Damage Readability: A pooled
DamageTextManagerhandles rising combat text so the player can always read the battle flow.
🛠️ A Bit More Technical (Expand)
1. ⚔️ Smart Targeting & Zero-Allocation Physics
The FSM runs 4 main states. Target detection relies on zero-allocation physics (Physics2D.OverlapCircle techniques) to prevent GC spikes. To make combat look natural, I implemented a Hysteresis system (units won't switch targets unless the new target is significantly closer, e.g., 12%) and a Focus Timer (minimum 0.5s lock-on) to prevent jittery targeting.
2. 🎯 Battle Cleanup
A tricky part was units dying while the game was still looping through the active list. To fix this, I added a _pendingRemovals queue to safely clean up dead units at the end of each frame, avoiding InvalidOperationException errors.
3. 🤖 Dynamic WaveDirector The AI doesn't just pick random enemies. It calculates a dynamic budget based on the base reward and weights your current army composition. If you spam ranged units, the AI dynamically assigns a +4 weight bias to anti-ranged counter units to give you a real challenge.
4. ✏️ Drawing UX (Backtracking)
The line drawing system calculates path length limits dynamically (PathUtils.GetTotalLength). I also implemented path backtracking—if the player moves the cursor backward, the line safely undoes itself, creating a much smoother UX before deploying UnitSpawner.SpawnUnitsOnPath().
5. 📊 Level Economy
The LevelManager rewards you for being efficient. If you win with unused units still in your hand, you get a gold bonus.
I’m pretty strict about not wasting CPU cycles. By using UnitFactory and ProjectileFactory, the game barely has to create anything new during a fight. Everything is recycled from the pool.
I think a good project should be easy to balance for people who don't want to touch code. I built these two tools to speed things up:
graph TD
StartScene[Start Scene] --> ModeSelection{Pick Mode}
ModeSelection -->|Campaign| CampaignScene[Load 100-Level Campaign]
ModeSelection -->|AI Wave| AiWaveScene[Dynamic AI Drafting]
CampaignScene --> Drawing[Player draws formation lines]
AiWaveScene --> Drawing
Drawing --> War["WAR!" starts the FSM simulation]
War --> BattleEnd[Battle ends & Rewards calculated]





