Back to portfolio

MonoGame · C# · 2023

Graveyard Madness

A 2D top-down shooter built on a custom MonoGame engine. I designed and implemented the entire enemy system — every enemy type, every projectile, and the multi-phase boss fight.

MonoGameC#Enemy AIBoss DesignProjectile SystemsSocket.IO

Overview

My role

Enemy Systems

Engine

MonoGame (C#)

Type

Group project

Built on a shared custom engine (Blok3Game.Engine), the game is a 2D top-down shooter set in a zombie-filled environment. My contribution was the complete enemy layer — the base class, three distinct enemy types, a boss fight, and the full projectile system. The game also connects to a backend via Socket.IO to record session data on completion.

Enemy Architecture

All enemies share a clean inheritance hierarchy built on top of the engine's SpriteGameObject.

SpriteGameObject (engine)└── Entities (health, damage, TakeDamage, LookAt)└── Enemy (spawn, player ref, collision, Socket.IO)├── Ghost├── Skeleton└── ZombieBoss
Entities└── Projectile (angle-based movement, despawn timer)├── BoneProjectile (Skeleton)├── ZombieProjectile└── ZombieBossProjectile (interceptable)

Enemy Types

Ghost

The Swarm Unit

Enemy
Health3 HP
SpeedVariable (normalized)
BehaviourDirect player tracking

The ghost is the simplest enemy but required the most careful movement design. It chases the player using a normalized direction vector, but the key feature is separation force — when ghosts get within 50px of each other they push apart, preventing the entire swarm from stacking into one pixel.

Ghost.cs — ApplySeparationForce()
private void ApplySeparationForce()
{
    Vector2 separation = Vector2.Zero;
    foreach (var child in (Parent as GameObjectList).children)
    {
        if (child is Enemy other && other != this)
        {
            float dist = Vector2.Distance(Position, other.Position);
            if (dist < 50f)
            {
                Vector2 away = Vector2.Normalize(Position - other.Position);
                float strength = (50f - dist) / 50f;
                separation += away * strength * 0.5f;
            }
        }
        velocity += separation;
    }
}

Skeleton

The Repositioning Sniper

Enemy
Health6 HP
Speed1.8 (slower)
BehaviourMove → stop → fire → repeat
Skeleton enemies surrounding the player

The skeleton doesn't chase the player. Instead it picks a random position on the screen, walks to it, stops, and begins throwing BoneProjectiles aimed at the player every 4 seconds. Once it stops moving, it never moves again — making it a static threat that punishes the player for ignoring it.

Skeleton.cs — MoveToRandomPosition()
private void MoveToRandomPosition()
{
    Vector2 direction = Vector2.Normalize(randomStopPosition - position);
    Velocity += direction * MovementSpeed;

    if (Vector2.Distance(position, randomStopPosition) < 1f)
    {
        Velocity = Vector2.Zero;
        isStopped = true;   // start shooting
    }
    Position += Velocity;
}

private void ThrowBoneProjectile()
{
    Add(new BoneProjectile(player, position, "images/Bullets/BoneV1"));
}

ZombieBoss

Multi-Phase Boss

Enemy
Health20 HP
SpeedStationary
Behaviour4 attack patterns, 7s each
ZombieBoss encounter

The boss cycles through four distinct bullet patterns every 7 seconds with a 2-second pause between switches. Three patterns fire 10-projectile spreads with different angular rotations per volley, while pattern 4 fires a tight 3-shot burst aimed directly at the player's current position.

Pattern 1

10-shot spread, +170° per volley

Pattern 2

10-shot spread, +230° per volley

Pattern 3

10-shot spread, +45° per volley

Pattern 4

3-shot aimed burst (atan2)

ZombieBoss.cs — FirePattern4() (aimed burst)
private void FirePattern4()
{
    // Calculate direction from boss to player
    Vector2 dir = Vector2.Normalize(player.Position - Position);
    float angle = (float)Math.Atan2(dir.Y, dir.X);

    // Fire 3 projectiles in a tight spread
    for (int i = 0; i < 3; i++)
    {
        var proj = new ZombieBossProjectile(player, Position);
        proj.Angle = angle + MathHelper.ToRadians(i * 10);
        Add(proj);
    }
}

Projectile System

All projectiles inherit from a shared base class. Movement is angle-based using trigonometry, and each subclass sets its own speed, damage, and despawn time.

BoneProjectile

Used by: Skeleton

Speed3
Despawn4s

Aimed at player

ZombieProjectile

Used by: Zombie

Speed2
Despawn4s

Aimed at player

ZombieBossProjectile

Used by: Boss

Speed5
Despawn3s

Interceptable by player shots

A notable design detail: ZombieBossProjectile checks for collisions with PlayerProjectile each frame and destroys both on contact — giving the player a way to actively defend against boss attacks by shooting them down.

Backend Integration

When the final enemy is defeated, the game sends a session summary to a backend server via Socket.IO — including the player's remaining health, total enemies killed, and a victory flag. This was wired into the Enemy.Die() method at the kill-count threshold.

Back to portfolio