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.
Enemy Types
Ghost
The Swarm Unit
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.
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

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.
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

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)
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
★ Aimed at player
ZombieProjectile
Used by: Zombie
★ Aimed at player
ZombieBossProjectile
Used by: Boss
★ 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.