← Projects · Survivors-like / action roguelite

Horde Hours

A 3D survivors-like. Auto-firing abilities, pooled enemies and VFX, timed waves on a NavMesh, level-up cards, passives and a JSON save system.

Unity 6 · URP · Solo — design & programming · 2026-01 – 2026-02

The player in a grass arena with a fireball bursting on a slime, HUD with timer, kills and ability slots
Fig. 1 — a run in the web build

What I built

my contribution — nothing here is third-party work

  • Ability framework: an abstract ScriptableObject `Ability` with Projectile, Aura, Whip and Instant variants, instantiated per run so levels and cooldowns are per-player; damage rolls with crit chance from a cloned `UnitStatistics` sheet.
  • Projectile behaviour (`MoveTo`): homing, piercing, bouncing with smart re-targeting to the nearest living enemy, and explosive variants with radial knockback.
  • Passive items that additively modify the runtime stat sheet (damage, cooldown, range, speed, knockback, magnet range, extra bounces) and level up like weapons.
  • Object pooling on top of `UnityEngine.Pool.ObjectPool`, keyed by prefab name, shared by enemies, projectiles, VFX, damage popups and XP gems; every pooled object resets itself in `OnEnable`.
  • Wave spawner that places enemies on the NavMesh around the player on a timeline of waves.
  • Enemy behaviour: NavMeshAgent chase, contact damage with interval, physics knockback that hands control back to the agent, hit-flash and dissolve-death via a Shader Graph material.
  • Level-up flow: XP gems with a recoil-then-fly pickup animation, XP curve, a three-card upgrade screen that only offers valid options (free slots, level caps), controller/keyboard selection.
  • Save system: `ISaveSystemElement` discovery, JSON save and load of level, XP, kills, position, abilities and passives, optional XOR obfuscation.
  • HUD (XP bar, timer, kill counter, inventory slots, world-space health bars), pause menu with animated panel, game-over screen, Cinemachine helpers (dynamic FOV when moving, angle-based zoom).

The game

Horde Hours is a top-down 3D survivors-like. You move; your weapons fire on their own at the nearest enemies. Enemies arrive in timed waves and chase you across a NavMesh arena, drop XP gems when they die, and every level-up pauses the game to offer three upgrade cards: a new weapon, a new passive, or a level for one you already have. The run ends when your health hits zero. There is no win state — the score is how long you lasted and how many you took down.

My goal for this 3D survivors-like was to balance player power progression with time-based difficulty scaling by architecting a modular, data-driven ability system using Unity’s ScriptableObjects. This project taught me how to tie independent mechanics into a cohesive core loop while scoping features to prioritize scalable functionality.

Technical breakdown

1. Abilities as data

Every weapon is a ScriptableObject asset derived from Ability: name, icon, VFX prefab, cooldown, range, damage range, damage type, targeting mode and level cap. Concrete subclasses override Execute — a projectile spawns a pooled MoveTo object and aims it, an aura attaches a pulsing DamagingAura to the player, a whip spawns an AreaDamage volume in front of the player. The controller clones the assets at runtime so each run has its own levels and cooldown timers, and passives mutate a cloned UnitStatistics sheet that every ability reads through. Adding a weapon means adding an asset, not touching the controller.

The whole loop is one method on the player. Every frame, each ability ticks its own cooldown; when it is ready, it asks for a target and executes:

private void HandleAbility(Ability ability) {
    ability.cooldownTimer -= Time.deltaTime;

    if (ability.cooldownTimer > 0) return;

    GameObject target = GetTargetForAbility(ability);

    bool targetFound = target != null;
    bool isNonTargetedAbility = ability.targetingMode == TargetingMode.Self || ability.targetingMode == TargetingMode.Directional;

    if (targetFound || isNonTargetedAbility) {
        ability.Execute(runtimeStatistics, gameObject, target);
        ability.cooldownTimer = ability.cooldown * runtimeStatistics.cooldownMultiplier;
    }
}

AbilitiesController.cs — HandleAbility, unedited.

Damage is rolled in one place that every weapon shares, so a passive that raises the damage multiplier or crit chance improves every weapon at once:

public int GetDamage(UnitStatistics stats, out bool isCritical) {
    float baseDamage = Random.Range(minDamage, maxDamage);
    float multiplier = stats.damageMultiplier;

    isCritical = Random.value <= stats.critChance;

    if (isCritical) {
        multiplier *= stats.critMultiplier;
    }
    return Mathf.RoundToInt(baseDamage * multiplier);
}

Ability.cs — GetDamage, unedited.

One frame of the ability loop For every ability clone, HandleAbility counts the cooldown down, finds a target with an overlap sphere, and calls Execute. Each subclass decides what happens: a pooled projectile, an aura that ticks, a whip area or an instant hit. All of them roll damage with GetDamage against the run's cloned stat sheet, which passives modify. After executing, the cooldown restarts, scaled by the cooldown multiplier. cooldown restarts: base × cooldownMultiplier Update()for every ability(a per-run clone) HandleAbilitycooldown −= Δtnot ready → skip GetTargetForAbilityOverlapSphere(range)closest or random enemy Execute(stats…)the subclass decideswhat happens Projectilepooled MoveTo: pierce, bounce, explode AuraDamagingAura, rolls damage per tick WhipAreaDamage volume in front Instanthit on the target UnitStatistics — runtime clone damageMultiplier · critChance · critMultipliercooldownMultiplier · range, speed, magnet…passives write here: PassiveItem.Apply()every weapon reads it GetDamage
Fig. — one frame of the ability loop. Ability assets are cloned per run, so levels and cooldown timers never leak between runs.

2. One pool for everything

Enemies, projectiles, hit VFX, damage popups and gems all go through one ObjectPoolManager that builds a UnityEngine.Pool.ObjectPool per prefab and keys it by prefab name. Spawning returns an inactive instance and activates it; despawning deactivates and releases. Each pooled script resets its own state in OnEnable (timers, collected flags, agent warp) so an instance can be reused without a special reset call. Death effects run a flash-and-fade before the enemy is returned to the pool.

3. Level-up offers and saves

LevelUpManager turns every enemy kill into XP, drives the XP curve, and builds the three-card offer from the ability and passive databases: owned items appear as “level N+1” only below their cap, unowned ones only while a slot is free. The screen pauses time, unlocks the cursor and preselects the first card for controller play. The save system finds every ISaveSystemElement in the scene, lets each write into one SaveData object, serialises it to JSON in the persistent data path and restores abilities by name from the database on load.

What changed for the web build (October 2026)

  • A real main menu (Play, Credits, Quit) replaced the empty first scene.
  • The Static Field effect used VFX Graph, which browsers don’t support; it is now a particle-system ring driven by the same DamagingAura logic.
  • The XP-magnet range check was inverted, so the magnet passive did nothing. Fixed, along with a frame-rate-dependent wave timer, a save field that was written backwards, and a pointer lock the game never needed.
  • Forward+ rendering switched to Forward, audio moved to Vorbis, textures capped at 1024 px for the web.

Gallery

Credits

third-party assets used — with thanks

AssetAuthorLicence
Character Pack: Free SampleSupercyanAsset Store EULA
Character Pack: Zombie SampleSupercyanAsset Store EULA
Level 1 Monster PackPI Entertainment LimitedAsset Store EULA
Stylized Free SkeletonThunderForgeStudioAsset Store EULA
FREE – RPG WeaponsBlinkAsset Store EULA
Simple Gems and Items Ultimate Animated Customizable PackBenjaTheMakerAsset Store EULA
Comic Explosion EffectCartoonVFX9xAsset Store EULA
(Free) StylizedVFX Fire PackHun0FXAsset Store EULA
Slash Effects FREEMatthew GuzAsset Store EULA
Hit Impact Effects FREETravis Game AssetsAsset Store EULA
VFX – Impact and Hit – Light VersionCartoon VFX by WallcoeurAsset Store EULA
Zap VFX – URPVefectsAsset Store EULA
Stylized Grass TextureLowlyPolyAsset Store EULA
Free – Casual & Relaxing Game Music Pack (track "Happy Harmony")SLD AudioAsset Store EULA
Hints Stars Points & Rewards Sound Effects – Lite PackCyberwave OrchestraAsset Store EULA
Rat model (mesh and texture)Original modeller unknown — the file came rigged through Mixamo. If this is your model, please contact me for credit or removal.Unknown
Flair dance animation (retargeted)Mixamo (Adobe)Mixamo terms, royalty-free
League Gothic fontThe League of Moveable TypeSIL OFL
Caslon Antique fontTypographer Mediengestaltung1001Fonts Free For Commercial Use
Ability iconsAI-generatedn/a (generated)