Fearosis: Monster Pandemic Simulator
Turn-based strategy meets monstrous horror
Project Overview
Fearosis is a turn-based strategy game where players design and spread monstrous diseases like Cult, AI, and Monstrosis to infect populations. While inspired by Plague Inc., Fearosis distinguishes itself with turn-based gameplay that allows for deeper strategic planning and deliberate decision-making. As Lead Programmer for this Android title, I built the core day-by-day simulation system and most other game systems from the ground up.
Technical Highlights
Stat-Based Simulation
Dynamic event system that reacts to player choices and modifies spread mechanics in real-time
Adaptive Upgrade Tree
Smart prerequisite system that unlocks new abilities based on player strategy
A* Pathfinding
Custom implementation for realistic disease propagation across the map
Object Pooling
Performance optimization techniques to ensure smooth gameplay on mobile devices
Secure Save System
Binary serialization to protect game state from external tampering
Music Loop System
Seamless audio transitions between intro and looping tracks
Code Showcase
Secure Save System with Binary Serialization
Implemented using MemoryPack for efficient binary serialization, preventing player manipulation of save files. The system handles complex game state including all stats, events, and upgrades.
SaveSystem.cs - Binary Save/Load Implementation
using UnityEngine;
using System.IO;
using MemoryPack;
public class SaveSystem : MonoBehaviour
{
private string saveFilePath;
void Awake()
{
saveFilePath = Path.Combine(Application.persistentDataPath, "savefile.dat");
}
public void SaveGame(SaveData saveData)
{
byte[] bytes = MemoryPackSerializer. Serialize(saveData);
File.WriteAllBytes(saveFilePath, bytes);
Debug.Log("Game saved to " + saveFilePath);
}
public SaveData LoadGame()
{
if (! File.Exists(saveFilePath))
{
Debug. LogWarning("Save file not found at " + saveFilePath);
return null;
}
byte[] bytes = File.ReadAllBytes(saveFilePath);
SaveData saveData = MemoryPackSerializer.Deserialize(bytes);
Debug.Log("Game loaded from " + saveFilePath);
return saveData;
}
}
Advanced Music Looper with Seamless Transitions
Dynamically transitions from intro to looping music with precise timing control. Uses events to notify other systems when the main loop starts, allowing for volume control synchronization.
MusicLooper.cs - Dynamic Audio Transition System
using System;
using UnityEngine;
using UnityEngine.Audio;
public class MusicLooper : MonoBehaviour
{
public AudioClip introClip;
public AudioClip mainClip;
public AudioMixerGroup audioMixerGroup;
public float transitionTime = 0.1f;
public event Action OnMainLoopStarted;
private AudioSource audioSource;
private AudioSource mainAudioSource;
private bool mainStarted = false;
void Start()
{
audioSource = GetComponent();
audioSource.outputAudioMixerGroup = audioMixerGroup;
audioSource.clip = introClip;
audioSource.loop = false;
audioSource.Play();
}
void Update()
{
float timeRemaining = audioSource.clip. length - audioSource.time;
if (! mainStarted && timeRemaining <= transitionTime)
{
mainAudioSource = gameObject.AddComponent();
mainAudioSource.volume = audioSource.volume;
mainAudioSource.outputAudioMixerGroup = audioMixerGroup;
mainAudioSource.clip = mainClip;
mainAudioSource.loop = true;
mainAudioSource.Play();
mainStarted = true;
OnMainLoopStarted?. Invoke(mainAudioSource);
}
}
}
Parent Point Class (Simulation Foundation)
Base class for all game stats including Fear, Notoriety, Prejudice, and Pain. Tracks points from multiple sources and applies event-based modifiers for dynamic gameplay.
Point.cs - Stat System Architecture
using UnityEngine;
using UnityEngine.Events;
public class Point : MonoBehaviour
{
//Total of all points
public int numPointsTotal;
//Starting points from defining trait
public int numPointsStart;
//Points gained today
public int numPointsGainedToday;
//Points from modifiable sources
public int numPointsFromBlood;
public int numPointsFromPhysical;
public int numPointsFromBehavior;
public int numPointsFromPsychological;
//Event modifiers
public float eventStatModifier;
public float eventBloodModifier;
public float eventPhysicalModifier;
public float eventBehaviorModifier;
public float eventPsychologicalModifier;
void Start()
{
numPointsFromBlood = 0;
numPointsFromPhysical = 0;
numPointsFromBehavior = 0;
numPointsFromPsychological = 0;
eventStatModifier = 1.0f;
eventBloodModifier = 1.0f;
eventPhysicalModifier = 1.0f;
eventBehaviorModifier = 1.0f;
eventPsychologicalModifier = 1.0f;
numPointsTotal = numPointsStart;
}
public virtual void GainPoints(int pointsToGain, string source)
{
numPointsGainedToday += pointsToGain;
if (pointsToGain >= 0)
{
//Add to source-specific points
switch (source)
{
case "Blood":
numPointsFromBlood += pointsToGain;
break;
case "Physical":
numPointsFromPhysical += pointsToGain;
break;
case "Behavior":
numPointsFromBehavior += pointsToGain;
break;
case "Psychological":
numPointsFromPsychological += pointsToGain;
break;
default:
Debug.Log("Error: Invalid source for points.");
break;
}
}
}
//Math to calculate total points with modifiers
public int GetTotalPoints()
{
numPointsTotal = Mathf.RoundToInt((numPointsFromBlood * eventBloodModifier
+ numPointsFromPhysical * eventPhysicalModifier
+ numPointsFromBehavior * eventBehaviorModifier
+ numPointsFromPsychological * eventPsychologicalModifier)
* eventStatModifier) + numPointsStart;
return numPointsTotal;
}
}
Day Handler (Turn-Based Simulation Engine)
The heart of Fearosis's turn-based gameplay. Each turn (day) processes infection spread, hunter spawning, and influence gain based on player stats and choices. This deterministic system lets players strategize their upgrades knowing exactly how each decision affects the next turn. Uses Unity Events for extensibility.
DayHandler.cs - Game Loop Logic
using UnityEngine;
using UnityEngine.Events;
public class DayHandler : MonoBehaviour
{
//Infection rate modifier
public float infectionRate;
public float populationInfluenceModifier;
public int hunterThreshold;
public int huntersPerThreshold;
public int painThreshold;
//References to other scripts
private Fear fearScript;
private Notoriety notorietyScript;
private Prejudice prejudiceScript;
private Pain painScript;
private Influence influenceScript;
private FullGameStats fullGameStatsScript;
//Unity event on day start
public UnityEvent dayStartEvent = new UnityEvent();
public void OnNextDay()
{
//Get stats
int numFear = fearScript.GetTotalPoints();
int numNotoriety = notorietyScript.GetTotalPoints();
int numPrejudice = prejudiceScript.GetTotalPoints();
int numPain = painScript. GetTotalPoints();
int instability = Mathf. Abs(numNotoriety - numPrejudice);
//Calculate new infections
numInfectedToGain += Mathf.RoundToInt(numFear + Random.Range(
Mathf.Max(numFear - instability, 0), numFear + instability) * infectionRate);
//Update full game stats script
fullGameStatsScript. AddInfected(numInfectedToGain);
//Calculate hunter kills
fullGameStatsScript.KillInfected(fullGameStatsScript.hunters);
//Calculate pain kills
if (numPain >= painThreshold)
{
fullGameStatsScript.KillInfected(numPain - painThreshold);
}
//Influence logic
int populationModifiedInfectionInfluence =
Mathf.Max(Mathf.RoundToInt(numInfectedToGain / populationInfluenceModifier), 1);
numInfluenceToGain = Mathf.Max(Mathf.RoundToInt(numPain*. 67f +
populationModifiedInfectionInfluence + Mathf.Max(numNotoriety*.50f, 1)), 5);
influenceScript.influencePoints += numInfluenceToGain;
//Hunter logic
if (instability > hunterThreshold)
{
int numHuntersToAdd = instability * huntersPerThreshold;
fullGameStatsScript.hunters += numHuntersToAdd;
}
//Call day start event
dayStartEvent. Invoke();
}
}
Dynamic Upgrade System
Event-driven upgrade tree that unlocks new abilities based on prerequisites. Uses C# events for clean dependency management between upgrades.
Upgrade.cs - Prerequisite & Event System
using UnityEngine;
using System;
public class Upgrade : MonoBehaviour
{
public string upgradeName;
public string upgradeDescription;
public int upgradeCost;
public bool isFirstUpgrade;
[Header("Add prerequisite upgrades (if applicable)")]
public GameObject[] prerequisiteUpgrades;
public bool isPurchased = false;
public event Action upgradePurchased;
public event Action upgradeUnlocked;
[Header("Add value you wish to buff")]
[SerializeField] private int fearBuff;
[SerializeField] private int notorietyBuff;
[SerializeField] private int prejudiceBuff;
[SerializeField] private int painBuff;
private enum SourceType { Blood, Physical, Behavior, Psychological };
[SerializeField] private SourceType source;
void Awake()
{
if (prerequisiteUpgrades. Length > 0)
{
foreach (GameObject prereq in prerequisiteUpgrades)
{
Upgrade prereqStatus = prereq.GetComponent();
prereqStatus.upgradePurchased += this.CheckPrerequisites;
}
}
}
public void Purchase()
{
if (!isPurchased && influenceScript.influencePoints >= upgradeCost)
{
influenceScript.influencePoints -= upgradeCost;
isPurchased = true;
upgradePurchased?.Invoke();
ApplyUpgrade();
}
}
public void CheckPrerequisites()
{
bool unlockable = true;
if (prerequisiteUpgrades != null)
{
foreach (GameObject prereq in prerequisiteUpgrades)
{
Upgrade prereqStatus = prereq.GetComponent();
if (prereqStatus.isPurchased == false)
{
unlockable = false;
break;
}
}
}
if (unlockable) upgradeUnlocked?.Invoke();
}
}
A* Pathfinding for Disease Spread
Custom A* implementation using Poisson Disc Sampling for grid generation. Optimized for mobile performance with efficient neighbor lookups and path retracing.
AStar.cs - Pathfinding Algorithm
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class AStar : MonoBehaviour
{
private PoissonDiscGrid grid = PoissonDiscGrid.Instance;
public int maxSearchIterations = 1000;
public List FindPath(Node startNode, Node targetNode)
{
if (! startNode.valid || !targetNode.valid) return null;
targetNode.MakeTargetNode(startNode);
startNode.MakeStartNode(targetNode);
//Initialize open and checked sets
List openSet = new List();
HashSet checkedSet = new HashSet();
openSet.Add(startNode);
int iterations = 0;
//fCost = gCost(Distance from start) + hCost (Distance from target)
while (openSet.Count > 0 && iterations < maxSearchIterations)
{
iterations++;
//Find node in openSet with lowest fCost
Node currentNode = openSet.OrderBy(n => n.fCost).ThenBy(n => n.hCost).First();
//Move currentNode from openSet to checkedSet
openSet.Remove(currentNode);
checkedSet.Add(currentNode);
//End condition - reached target
if (currentNode == targetNode) return RetracePath(startNode, currentNode);
//Check each neighbor of currentNode
List neighbors = grid.GetNeighbors(currentNode);
foreach (Node neighbor in neighbors)
{
if (! neighbor.valid || checkedSet.Contains(neighbor)) continue;
//Calculate tentative gCost for this path to neighbor
float tentativeGCost = currentNode. gCost +
(currentNode.worldPosition - neighbor.worldPosition).magnitude;
//If neighbor is not in openSet or we found a better path
if (!openSet.Contains(neighbor) || tentativeGCost < neighbor.gCost)
{
//Update neighbor with better path
neighbor.GiveReferences(currentNode, targetNode);
neighbor.gCost = tentativeGCost;
if (!openSet.Contains(neighbor)) openSet.Add(neighbor);
}
}
}
return null; //No path found
}
private List RetracePath(Node startNode, Node endNode)
{
List path = new List();
Node currentNode = endNode;
while (currentNode != startNode)
{
path. Add(currentNode);
if (currentNode.parent == null) break;
currentNode = currentNode.parent;
}
path.Add(startNode);
path.Reverse();
return path;
}
}
Team
Developer Reflection
Fearosis pushed me to design systems that felt both complex and intuitive. The turn-based structure let me create a more strategic experience than typical real-time pandemic sims; players could actually think through their upgrade paths and predict outcomes. Watching players develop their own strategies and experiment with different disease types confirmed that we'd successfully balanced simulation depth with accessible gameplay. The deliberate pacing made every decision feel meaningful.