Back to portfolio

MonoGame · C# · Group Project

Hell Space

A top-down shooter built in MonoGame. The player defends against escalating alien waves on the moon. I designed and built all four enemy types, their spawn system, and the player's shooting mechanics.

MonoGameC#Enemy AISpawn SystemPlayer CombatOOP
Play on itch.io

Overview

My role

Enemies & Combat

Engine

MonoGame (C#)

Genre

Top-down shooter

Platform

Windows

Hell Space is a top-down survival shooter on a lunar surface. Aliens approach from all sides and the player fights back with a randomly assigned starting weapon — Minigun, Shotgun, or Lasergun — upgraded through a DNA collection system.

I built all four enemy types with their individual behaviours, the timed spawn system that escalates difficulty over the session, and the player's full shooting mechanic — aim, fire rate, and bullet damage falloff.

Enemy Types

Four classes inherit from a shared Enemy base. Each overrides movement and behaviour while reusing the base spawn, separation, and health logic.

Enemy

Basic chaser

HP

10

Speed

140

Dmg

1

Spawns from a random screen edge from the start. Chases the player directly, separates from other enemies to avoid clumping.

BigEnemy

Tank

HP

45

Speed

80

Dmg

3

Unlocks after 30 seconds. Slower but absorbs far more damage — forces the player to commit more firepower.

ShooterEnemy

Ranged

HP

15

Speed

75

Dmg

5

Spawns every 3 seconds from the start. Keeps 200 units of distance and fires a bullet every 4 seconds. Threatens the player even at range.

SuicideEnemy

Kamikaze bomber

HP

5

Speed

150

Dmg

20

Unlocks after 60 seconds. Low health but deadly — within 150 units it boosts speed 2.5× and detonates 0.8s later for 20 damage.

Enemies.cs — base class
public class Enemy : Lifeform, IDestroyable
{
    protected Player player;
    private float avoidDistance = 20;
    public List<Enemy> otherEnemies = new List<Enemy>();
    protected int damage = 1;

    public Enemy(...) : base(...)
    {
        position = CalculateSpawnPosition();
    }

    public override void Update(GameTime gameTime)
    {
        base.Update(gameTime);
        MoveTowardsPlayer();
        AvoidOtherEnemies();
    }

    public virtual void MoveTowardsPlayer()
    {
        if (player.camouflage == null)
        {
            Vector2 direction = Vector2.Normalize(
                player.Position - position);
            velocity += direction * speed;
            if (velocity.Length() > speed)
            {
                velocity.Normalize();
                velocity *= speed;
            }
        }
        else velocity = Vector2.Zero; // player is hidden
    }

    private void AvoidOtherEnemies()
    {
        foreach (var other in otherEnemies)
        {
            if (other != this &&
                Vector2.Distance(position, other.Position) < avoidDistance)
            {
                Vector2 dir = Vector2.Normalize(position - other.Position);
                position += dir * (avoidDistance
                          - Vector2.Distance(position, other.Position));
            }
        }
    }
}

Spawn System

Enemies spawn from a random screen edge so the player can never camp one direction. Harder types are gated behind elapsed time — the session stays manageable early and escalates as it goes on.

Enemies.cs — CalculateSpawnPosition()
private Vector2 CalculateSpawnPosition()
{
    Random random = new Random();
    int side = random.Next(4); // 0=Top 1=Right 2=Bottom 3=Left

    switch (side)
    {
        case 0: return new Vector2(random.Next(Screen.X), -800);
        case 1: return new Vector2(Screen.X + 100,
                                   random.Next(Screen.Y));
        case 2: return new Vector2(random.Next(Screen.X),
                                   Screen.Y + 100);
        case 3: return new Vector2(-800, random.Next(Screen.Y));
    }
}
GameState.cs — timed spawn escalation
// GameState.cs — Update()
enemySpawnTimer += elapsed;
if (enemySpawnTimer >= enemySpawnInterval) // every 2s
{
    spawnEnemy();                           // basic — from 0:00

    if (elapsed >= 30)
        spawnBigEnemy();                    // tank — from 0:30

    if (elapsed >= 60)
        spawnSuicideEnemy();               // bomber — from 1:00

    enemySpawnTimer = 0;
}

ShooterEnemySpawnTimer += elapsed;
if (ShooterEnemySpawnTimer >= ShooterSpawnInterval) // every 3s
{
    SpawnShooterEnemy();                   // ranged — from 0:00
    ShooterEnemySpawnTimer = 0;
}

ShooterEnemy & SuicideEnemy

Both types override the base MoveTowardsPlayer() with distinct behaviour patterns that force different player responses.

ShooterEnemy.cs
public class ShooterEnemy : Enemy
{
    private float ShootCooldown = 4f;
    private float ShootTimer = 0f;
    private int DistanceFromPlayer = 200;

    public override void MoveTowardsPlayer()
    {
        Vector2 dir = player.Position - position;
        float distance = dir.Length();

        if (distance > DistanceFromPlayer)
        {
            dir.Normalize();
            velocity = dir * speed;      // close the gap
        }
        else velocity = Vector2.Zero;    // hold position and shoot
    }

    public void ShootAtPlayer()
    {
        if (player.camouflage != null) return;

        Vector2 dir = Vector2.Normalize(player.Position - Position);
        Bullet b = new Bullet(dir, BulletSpeed=200, BulletDamage=5,
                              new MoveStraightBehavior(), "EnemyBullet");
        b.Position = this.Position;
        bullets.Add(b);
    }
}
SuicideEnemy.cs
public class SuicideEnemy : Enemy
{
    private bool isCharging = false;
    private float detonationTime = 0.8f;  // explodes 0.8s after charge

    public override void MoveTowardsPlayer()
    {
        if (!isCharging)
        {
            base.MoveTowardsPlayer();

            float dist = (player.Position - position).Length();
            if (dist < 150)
            {
                velocity *= 2.5f;   // dash toward the player
                isCharging = true;
            }
        }
    }

    public override void Update(GameTime gameTime)
    {
        base.Update(gameTime);
        if (isCharging)
        {
            detonationTime -= (float)gameTime.ElapsedGameTime
                                              .TotalSeconds;
            if (detonationTime < 0) hp = 0;  // triggers destroy
        }
    }
}

Player Combat

The gun rotates toward the mouse cursor every frame. Left-click fires when the cooldown allows. Bullet damage applies a falloff reduction past a distance threshold, rewarding close-range play.

GameState.cs — CheckForPlayerInput()
// GameState.cs — CheckForPlayerInput()
Vector2 aimDir = new Vector2(
    input.MousePosition.X - Screen.X / 2,
    input.MousePosition.Y - Screen.Y / 2
);

// Rotate the gun sprite toward the mouse
player.Gun.UpdateRotation(input.MousePosition);

// Left-click fires if the gun's cooldown has elapsed
if (input.MouseLeftButtonDown && player.Gun.readyToFire)
{
    player.currentState = PlayerState.State.Shooting;
    Bullet[] bullets = player.Gun.ShootBullet(aimDir);
    foreach (Bullet b in bullets)
        Add(b);
}

// Releasing the button restores walk state + fire rate
if (!input.MouseLeftButtonDown && !player.IsOverloading)
{
    player.Gun.Firerate = player.Gun.StartFirerate;
    player.currentState = PlayerState.State.Walking;
}
Gun.cs — ShootBullet + damage falloff
// Gun.cs — ShootBullet (base implementation)
public virtual Bullet[] ShootBullet(Vector2 direction)
{
    currentTime = timeToWait;
    Bullet b = (Bullet)bullet.Clone();
    b.Position  = position;
    b.Direction = direction;
    return new Bullet[] { b };
}

// Each subclass (Shotgun, Minigun, Lasergun) overrides
// this to change spread, count, or bullet type.

// Bullet damage falls off past falloffDistance:
private void BulletDamagesEnemy(Bullet bullet, Enemy enemy)
{
    if (bullet.TraveledTime > player.Gun.FalloffDistance)
        bullet.Damage -= player.Gun.DamageReduction;
    enemy.Hp -= bullet.Damage;
}

Lasergun

High fire rate, moderate damage. Continuous beam-style fire.

Shotgun

Fires a spread of pellets. High burst damage up close.

Minigun

Rapid single shots. Fastest DPS but needs sustained aim.

Back to portfolioPlay on itch.io →