

Platform :
PC
Team size :
Solo Project
Time span :
Not Finished Yet
Project Survivor is a multiplayer online hack’n slash prototype inspired by games like Path of Exile and Vampire Survivors. The core idea is to create a fast-paced and scalable combat experience where players can face waves of enemies together or solo, within dynamically generated instances hosted on a dedicated Linux server.
The networking architecture is built on Mirror (v96.0.1 with KCP transport) to ensure low latency and efficient data flow. Each match runs in an independent server instance, managed by a custom Instance Manager that dynamically assigns ports and creates isolated maps similar to the mapping system of Path of Exile.
The gameplay relies on a modular spell system, state-based enemy AI, and object pooling to efficiently handle large numbers of enemies on screen. Using lightweight quad rendering ensures stable performance while supporting intense combat scenarios.
From a technical perspective, the project is supported by an automated build pipeline through Jenkins, headless Linux servers, network profiling, spatial interest management, and a scalable server architecture all focused on delivering smooth and responsive gameplay.
I developed a scalable server architecture using
Mirror with
KCP transport, allowing each player to play in its
own dynamic instance.
Each new map allocates a new port and launches a
headless server build.
This system enables Path of Exile-style instancing, clean
session isolation, and
easier scaling on
dedicated servers.
public class InstanceManager : NetworkBehaviour
{
public static InstanceManager Instance { get; private set; }
private readonly Dictionary<int, InstanceInfo> activeInstances = new();
private int nextInstanceId = 1;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
[Server]
public void CreateInstance(NetworkConnectionToClient conn)
{
int id = nextInstanceId++;
int port = 7777 + id;
string scene = "MapScene";
int seed = Random.Range(0, 999999);
// Start the server process
var psi = new ProcessStartInfo
{
FileName = "MyServerBuild.exe",
Arguments = $"-batchmode -nographics -scene {scene} -port {port} -seed {seed}",
CreateNoWindow = true,
UseShellExecute = false
};
Process.Start(psi);
activeInstances[id] = new InstanceInfo
{
id = id,
port = port,
scene = scene,
seed = seed
};
// Send instance info to client
TargetSendInstanceInfo(conn, "127.0.0.1", port);
}
[TargetRpc]
private void TargetSendInstanceInfo(NetworkConnectionToClient conn, string ip, int port)
{
if (ClientSideInstanceManager.Instance != null)
ClientSideInstanceManager.Instance.SwitchToInstance(ip, (ushort)port);
else
Debug.LogWarning("ClientSideInstanceManager instance not found!");
}
}
[System.Serializable]
public class InstanceInfo
{
public int id;
public int port;
public string scene;
public int seed;
}
I designed a fully modular spell system allowing fast iteration and
clean separation between gameplay logic and
data definitions.
Each spell is defined through a SpellData that contains
its damage, speed,
range, scaling, and targeting rules.
All spell executions are server authoritative,
making it deterministic and secure.

using UnityEngine;
[System.Serializable]
public abstract class Spell
{
public Spell() { }
public Spell(SpellData spellData)
{
this.data = spellData;
}
[System.Serializable]
public class SpellData
{
public SpellTypeReference spellType;
public Sprite UISprite;
public string spellName;
public GameObject prefab;
public float damage;
public float speed;
public float range;
public float duration;
public int manaCost;
public float cooldown = 2f;
public float lastCastTime;
public bool autoCast = true;
public Transform firePoint;
public NetworkEntity owner;
public int maxLevel = 3;
public int currentLevel = 1;
public string description;
public SpellData Clone()
{
return (SpellData)this.MemberwiseClone();
}
}
protected SpellData data;
public void Init(SpellData spellData)
{
this.data = spellData;
}
public virtual void OnAdd(NetworkEntity owner) { }
public virtual void OnRemove(NetworkEntity owner) { }
public virtual void UpdateSpell(NetworkEntity owner)
{
if (this.data.autoCast && Time.time >= this.data.lastCastTime + this.data.cooldown)
{
if (owner != null)
{
owner.CmdCastSpell(GetType().Name);
}
else
{
ExecuteServer(owner);
}
this.data.lastCastTime = Time.time;
}
}
public void TryCast(NetworkEntity netEntity)
{
if (Time.time >= this.data.lastCastTime + this.data.cooldown)
{
if (netEntity != null)
netEntity.CmdCastSpell(GetType().Name);
else
ExecuteServer(netEntity);
this.data.lastCastTime = Time.time;
}
}
public SpellData GetData() { return data; }
public abstract void ExecuteServer(NetworkEntity owner);
}
I implemented a runtime StatContainer system stored directly on
NetworkEntity components (e.g. player or enemy) using using ScriptablesObjects.
This container handles base stats,
modifiers, and synchronization
between the server and connected clients.

public class NetworkEntity : NetworkBehaviour
{
// List of active spells on this entity
protected List activeSpells = new List();
// Stats to give at start
[SerializeField] private StatsDataSO SO;
[SyncVar] public String entityName;
// Leveling Stats
[SyncVar] public int level;
[SyncVar] public float experience;
[SyncVar] public float maxExperience;
[SyncVar] public float expMultiPerLevel;
// Defensive stats
[SyncVar] public float maxHealth;
[SyncVar] public float maxMana;
[SyncVar] public float currentHealth;
[SyncVar] public float currentMana;
[SyncVar] public float healthRegen;
[SyncVar] public float manaRegen;
//Offensive stats
[SyncVar] public float movementSpeedMultiplier;
[SyncVar] public float cooldownReduction;
[SyncVar] public float criticalStrikeChance;
[SyncVar] public float criticalStrikeDamage;
[SyncVar] public float projectileSpeed;
[SyncVar] public float durationMultiplier;
[SyncVar] public float damageMultiplier;
[SyncVar] public float experienceGiven;
protected virtual void Start()
{
if (!isServer) return; // block client-side execution
ApplyStatsFromSO(SO);
}
public void ApplyStatsFromSO(StatsDataSO statsDataSO)
{
if (!isServer) return; // Only Server
var soFields = typeof(StatsDataSO).GetFields(BindingFlags.Public | BindingFlags.Instance);
var entityFields = typeof(NetworkEntity).GetFields(BindingFlags.Public | BindingFlags.Instance);
foreach (var soField in soFields)
{
// Look for a matching field in NetworkEntity
var entityField = entityFields.FirstOrDefault(f => f.Name == soField.Name);
if (entityField == null) continue;
// Get the value from the ScriptableObject
var soValue = soField.GetValue(statsDataSO);
// Make sure the field is not read-only
if (entityField.IsPublic && !entityField.IsInitOnly)
{
// Assign the value to the NetworkEntity field
entityField.SetValue(this, soValue);
}
}
}
[CreateAssetMenu(fileName = "NewStatsData", menuName = "Stats/StatsData")]
public class StatsDataSO : ScriptableObject
{
//Name
public String stringName = "Unnamed Entity";
// Leveling Stats
public int level = 1;
public float experience = 0f;
public float maxExperience = 100f;
public float expMultiPerLevel = 1.5f;
// Defensive stats
public float maxHealth = 100f;
public float maxMana = 40f;
public float currentHealth = 100f;
public float currentMana = 40f ;
public float healthRegen = 0f;
public float manaRegen = 1f;
//Offensive stats
public float movementSpeedMultiplier = 1f;
public float cooldownReduction = 0f;
public float criticalStrikeChance = 3f;
public float criticalStrikeDamage = 150f;
public float projectileSpeed = 0f;
public float durationMultiplier = 0f;
public float damageMultiplier = 1f;
public float experienceGiven = 10f;
}
After repeatedly building and deploying manually,
I realized how time-consuming and error-prone the process was.
That’s when I decided to create a fully automated solution with
Jenkins, turning what used to be a tedious manual task
into a smooth one-click deployment pipeline.
I built a complete CI/CD pipeline using
Jenkins to automate every step of my Unity project’s
build and deployment process.
The system automatically fetches the latest commits from GitHub,
builds the client and headless server through Unity’s
batchmode, and
deploys the new server version directly to my
Debian VPS.
Each build script handles process cleanup,
commit verification,
file synchronization, and
server restarts with detailed Jenkins logs.
This automation allows me to deploy a new version with a single click,
ensuring speed,
stability, and
traceability across the entire
Project Survivor build cycle.
public static class BuildScript
{
[MenuItem("Build/Server Build")]
public static void BuildServer()
{
string buildPath = "Builds/Server";
if (!Directory.Exists(buildPath))
Directory.CreateDirectory(buildPath);
//Get all enabled scenes in build settings
string[] scenes = EditorBuildSettings.scenes
.Where(s => s.enabled)
.Select(s => s.path)
.ToArray();
// Configure build options
BuildPlayerOptions options = new BuildPlayerOptions
{
scenes = scenes,
locationPathName = Path.Combine(buildPath, "ServerBuild.x86_64"),
target = BuildTarget.StandaloneLinux64,
options = BuildOptions.CompressWithLz4
};
// subTarget for a server build
EditorUserBuildSettings.standaloneBuildSubtarget = StandaloneBuildSubtarget.Server;
//Deactivate development build
EditorUserBuildSettings.development = false;
// Launch build
var report = BuildPipeline.BuildPlayer(options);
// BuildResult logging
if (report.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded)
{
UnityEngine.Debug.LogError($"Build failed: {report.summary.result} ({report.summary.totalErrors} errors)");
}
else
{
UnityEngine.Debug.Log($"Build succeeded: {report.summary.outputPath}");
BuildInfo.Version = "Build " + System.DateTime.Now.ToString("yyyyMMddHHmm");
}
}
}

After repeatedly building and deploying manually,
I realized how time-consuming and error-prone the process was.
That’s when I decided to create a fully automated solution with
Jenkins, turning what used to be a tedious manual task
into a smooth one-click deployment pipeline.
This Bash automation script handles Unity build deployment, server restarts, and log management through Jenkins for a fast, reliable, and repeatable CI/CD workflow.
#!/bin/bash
set -e
echo "───────────────────────────────"
echo "Starting Server Deployment"
echo "───────────────────────────────"
# --- CONFIG ---
WORKSPACE="/var/lib/jenkins/workspace/FreeStyleBuild"
SERVER_DIR="/home/server"
PORT=7777
echo "Updating Git repository..."
cd $WORKSPACE
git fetch origin
git reset --hard origin/main
git clean -fdx
# --- BUILD UNITY SERVER PROJECT ---
echo "Building Unity server..."
$UNITY_PATH \
-batchmode -nographics \
-projectPath $WORKSPACE \
-executeMethod BuildScript.BuildServer \
-quit
echo "───────────────────────────────"
echo "Deployment complete"
echo "───────────────────────────────"
Even though this project is not finished yet , it already helped me a lot understanding basics network concepts and how to manage a project alone. Overall I feel like it's going to be a great experience.