All Case Studies

Keep Calm and Kill Demons

February, 2025

Keep Calm and Kill Demons is a fast-paced wave-based survival FPS, created in Unity, where players battle relentless waves of demons in a procedurally generated town. Armed with only a revolver, an AK-47, and a radio playing punk tunes, the challenge is to survive as long as possible. Built mostly for fun and as a technical showcase, it highlights procedural generation, a save system, event-driven architecture, AI behavior, weapons system, an FPS player controller, performance optimizations, and more.

Introduction

  • Overview: Keep Calm and Kill Demons is a wave-based survival FPS where players fend off demons in a procedurally generated town. With each wave, the difficulty increases as more enemies spawn, forcing the player to be fast on their feet, while keeping an eye on their health. The game features an interactive in-game radio, persistent game state saving, dynamic AI behavior, and more.
  • Motivation: Originally developed as a smaller scope technical test for a job interview, but I had fun with the development process, and decided to expand it with a few more features, optimizations, new UI, models, polish, and do a small release on itch.io.

Key Features

  • First-Person Combat: The player use two distinct weapons – a revolver and an AK-47 – each with unique fire rates, ammo capacities, bullet spread and firing modes (semi-auto/full-auto), muzzle flash VFX, reload animations, and surface-specific impact effects.
  • Wave-Based Gameplay: Enemies spawn from portals in increasing numbers, currently capped at 100 per wave. Once a wave is cleared, dead enemies are retrieved through portals before the next wave begins.
  • FSM-driven Enemies: Demons operate on a three-state FSM with state pattern (Spawn, Chase, Dead) using NavMesh pathfinding. They navigate toward the player, deal damage on collision, and have animation states for movement, damage reactions, and death.
  • Procedural Level Generation: The level, including terrain, buildings, trees, and enemy spawn points, is deterministically generated from a player-inputted seed. The same seed recreates the exact level layout and enemy spawn patterns.
  • Player Performance Tracking: Stats such as accuracy, bullets fired, enemies killed, time survived, and distance traveled are recorded and displayed on the game-over screen.
  • In-World Music Player: Instead of traditional background music, players can find and carry a radio that plays punk/metal tracks in 3D space. Shooting the radio changes the track. Picking it up transitions the music to 2D stereo to follow the player around.
  • Full Save System: Saves all relevant gameplay data, including player health, inventory, ammo, wave progression, enemy states, level layout, and active music track position.
  • Minimap System: Displays enemy and item positions relative to the player, rotating to match the player’s forward direction for intuitive navigation.
  • Dynamic Audio & VFX: Includes proximity-based enemy growls, surface-dependent bullet impact sounds/VFX, muzzle flash VFX, footstep and damage sounds, and post-processing for atmospheric effects.
  • UI Toolkit-Based Interface: Clean and modular UI implementation for in-game HUD and menus.

Key Challenges & Solutions

1. Efficient Dependency Management

  • Challenge: As the project grew, managing dependencies between gameplay systems (e.g., player, AI, level generation, UI, input, and audio) became increasingly complex. Hard dependencies between objects would have made it difficult to refactor or expand individual components.
  • Solution: Implemented a service locator pattern for dependency management, centralizing access to game services while avoiding tight coupling. The GameSceneInjector class registers key dependencies such as Player, GameStateManager, InputManager, EnemyAudioManager, and LevelGenerator to a Locator service via interface instead of class, ensuring they can be retrieved globally without direct references. This approach allows for modular and easily extendable game architecture.

2. Procedural Level Generation

  • Challenge: The original static level design became repetitive quickly. The goal was to implement a deterministic procedural generation system that could create varied yet structured environments based on a seed. The system needed to produce diverse but structured environments while maintaining performance and preventing extreme randomness.
  • Solution: Wrote a grid-based procedural generation system with banding rules to ensure logical placement of objects. The world is divided into forest, city, and open space bands with probability-based placement rules. System.Random (instead of UnityEngine.Random that only has a global seed) ensures deterministic level generation for a given seed, with more granular control over the seed per use. Once the level is generated, NavMesh baking and static batching is performed with StaticBatchingUtility.Combine() to optimize performance. Buildings are instantiated with deterministically random rotations and GPU instancing for efficiency.

3. Custom Save System

  • Challenge: Implementing a robust save system that could persist not only static game data but also active entities like enemies, projectiles, and animations, and having a logical save restoration lifecycle where objects are fully restored before the game starts.
  • Solution: Developed a generic save system with ISaveable and ISubsystemSaveable interfaces, allowing automatic detection and management of persistent objects. Saveable class state is serialized into a dictionary using JSON and written to disk. Instead of relying on MonoBehaviour.Start, objects initialize through ISaveable.InitializeNew() or ISaveable.RestoreState(), ensuring proper load order. Complex objects such as player inventory, weapons, and UI states are handled hierarchically through ISubsystemSaveable, enabling structured and recursive data restoration. The save system also respects an initialization order defined by ISaveable and ISubsystemSaveable, ensuring dependencies are restored in the correct sequence, which was crucial for objects that rely on others being initialized first.

4. Persisting Enemy Animations & States

  • Challenge: The enemy state system relied on a mix of animations, and procedural tweens (DOTween), making it difficult to take a precise snapshot and restore it mid-action.
  • Solution: Used DOTween Sequences to structure enemy state transitions, ensuring predictable playback. The system saves the current state machine state, animation progress, tween time, and portal particle effects at the moment of saving. On load, the sequences and portal particle systems are restored to their exact time positions, allowing enemies to seamlessly resume their previous actions without visual or behavioral inconsistencies.

5. Minimap System

  • Challenge: The minimap needed to track enemies and items dynamically while rotating with the player’s perspective.
  • Solution: Implemented a MiniMapService where objects implementing IMapTrackable automatically register for tracking. The system updates UI positions relative to the player, clamps out-of-range objects to the minimap’s edges, and ensures the minimap rotates in sync with the player’s forward direction, providing intuitive navigation.

6. UI Toolkit and MVC

  • Challenge: Initially implemented with UGUI, but scalability and maintainability issues made transitioning to UI Toolkit more appealing.
  • Solution: Originally, I was tasked with implementing the UI using UGUI, which I did. However, I later redesigned the UI with my own layout using UI Toolkit for better scalability and maintainability. Having just worked extensively with UI Toolkit in my other project, WordBaby, the switch was efficient. The UI followed an MVC (Model-View-Controller) pattern, allowing for a clean separation of concerns. UI Toolkit was used for HUD and menus due to its scalable vector-based UI and faster iteration workflow, while UGUI was retained for the minimap. Because the UI was already structured using MVC, transitioning to UI Toolkit only required swapping the View layer, keeping all underlying logic intact.

7. Dynamic Audio and VFX Systems

  • Challenge: Sound effects needed to be responsive to gameplay events, including player movement, shooting, environment interactions, and enemy behaviors.
  • Solution: Developed an audio system that integrates with gameplay. Surface-specific bullet impact sounds and VFX were implemented for materials like wood, stone, foliage, and demon flesh to enhance realism. Enemy play growl sounds based on their proximity to the player, while weapon audio features distinct variations for different fire rates and reload actions. Footstep sounds play according to the player’s velocity. Additionally, the in-game radio utilizes 3D positional sound when placed in the world and transitions to 2D stereo when picked up.

8. Async/Additive Scene Loading with a Press-to-Continue Mechanic

  • Challenge: Traditional scene loading caused visible stutters, breaking immersion.
  • Solution: Implemented asynchronous and additive scene loading to ensure smooth transitions. A loading screen remains active until the scene is fully prepared, preventing incomplete asset loading from affecting gameplay. Instead of forcing an automatic transition, a “Press Any Key” mechanic allows the player to control when to enter the game.

9. Performance Optimization

  • Challenge: Even with a low-poly art style, performance could degrade due to the high number of active enemies, environmental objects, and background processes such as NavMesh pathfinding, AI state machines, and minimap updates. Running everything at full frequency would be inefficient, leading to unnecessary CPU and memory load.
  • Solution: Implemented multiple optimization techniques to maintain stable performance. Object pooling was used for frequently instantiated objects like enemies, bullet impact particles, and portals, reducing memory allocation and garbage collection overhead. The audio system uses an AudioSource component pool, limiting active enemy sounds to the five closest ones to prevent CPU overload. Static objects are flagged and batch combined using StaticBatchingUtility.Combine() reducing draw calls, while GPU instancing minimizes the overhead of rendering multiple materials. Update loops were optimized by running non-critical processes in coroutines at reduced frequencies, e.g., enemy pathfinding updates every 0.5 seconds instead of every frame, and other background tasks update at lower intervals. Finally, an event-driven architecture ensures systems react only when necessary, reducing redundant update cycles and improving performance.

10. Enemy Death, Portal Retrieval & Weapon Animations

  • Challenge: While most enemy animations came from an asset pack, additional animations were needed for death sequences and retrieving enemies into portals, as well as weapon animations. Lacking strong animation skills, creating smooth and visually consistent animations posed a challenge.
  • Solution: Used a combination of procedural animation with DOTween and manually keyframed animations to achieve the desired effects. Procedural tweening controlled elements like enemy rotation and movement on death, while simple keyframed animations handled their transition back into the portals. I also manually keyframed the weapon bobbing, fire, and reload animations. Though the animations are quite basic, they turned out much better than expected given my limited animation experience.

More Code Snippets

Player

Enemy

State Machine

Guns

Music Radio

Future Development

If I expand the game, potential additions might include:

  • Expanded Procedural Generation: More environmental variety and randomization.
  • New Weapons & Enemies: Additional enemy types, possibly a boss, and weapons that can be found in the level instead of starting pre-equipped.
  • Power-ups & Items: Damage boosts, speed modifiers, health packs, and more.
  • Gamepad Support: Input is already built with the Unity Input Actions System, making gamepad integration straightforward.
  • Online Leaderboards: Global stat tracking for competitive play.

Have a Game or XR App in Mind?

Let’s chat about how I can help making it happen.

I’ll get back to you the next business day.

Get in Touch