Project Survivor - 2025 (WIP)

     Platform :

  PC

Team size :

Solo Project

Time span :

Not Finished Yet

About the game :

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.

What I worked on :

1 : Networking & Instance Management

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;
}
    
Show full script ↓

2 : Spell System & Combat Logic

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);
}



  

3 : Stats System & Entity Storage

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;
}




  

4 : Automated Builds & Jenkins

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 "───────────────────────────────"
Show full script ↓

Conclusion :

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.