I've been playing Mewgenics for the past few days and I'm completely hooked.
Beyond the tactical combat and the game's absurd tone, what really caught me was its breeding system. Every cat that's born can completely shift your strategy, and what's often most interesting isn't whether a cat is powerful right now, but what potential its lineage carries.
After a few hours of play, curiosity got the better of me: how would I actually implement something like this in Unity?
So I decided to build a full working prototype. This article is a writeup of how I approached it: the architecture I used, the mistakes I made, and the problems I ran into along the way. The complete implementation is available on GitHub so you can run it, break it, and extend it yourself.
This isn't meant to be an exact reproduction of Mewgenics' internals (I don't know how they actually work), but a technical exploration of how to design something similar from scratch.
1. The First Mistake: Starting with the Cat
My first instinct was to create a CatController with stats, appearance, genes, and breeding logic all bundled together.
It lasted about ten minutes.
The moment you start thinking about generations, mutations, latent traits, and genealogy, it becomes obvious that this is not a visual object. It's a domain system. The cat on screen is just the final representation of a much more complex set of data.
I ended up splitting the system into three distinct layers:
- Domain: genetics, inheritance, and mutations.
- Runtime: the cat's state during a match.
- Presentation: sprites, animations, and UI.
The rule I set for myself was simple:
The genetic system must be able to run without Unity.
If the system can execute in tests or even in a small console application, it's probably well-decoupled. This constraint forced me to keep the domain logic free of MonoBehaviour dependencies from the start. All core classes live under Assets/Scripts/Domain/ with zero Unity dependencies, they're plain C# that could run on any .NET runtime.
2. Separating Genotype and Phenotype
This was probably the most important shift in thinking. Instead of treating a cat as a bundle of stats, the system models it in two distinct layers.
Genotype
The heritable information (what gets passed down across generations):
- Alleles
- Mutations
- Latent traits
- Affinities
Phenotype
What actually appears in the game (the observable result):
- Appearance
- Final stats
- Active traits
In the prototype, I ended up representing it like this:
1[Serializable]
2public class CatGenome
3{
4 public GenePair coatColor;
5 public GenePair bodySize;
6 public GenePair vitality;
7 public GenePair agility;
8 public GenePair aggression;
9
10 public List<MutationGene> mutations = new();
11 public List<TraitGene> latentTraits = new();
12
13 public int generation;
14}
15
16[Serializable]
17public struct GenePair
18{
19 public byte alleleA;
20 public byte alleleB;
21}The phenotype is then derived from that genome:
1public class CatPhenotype
2{
3 public float sizeScale;
4 public int maxHp;
5 public int attack;
6 public int speed;
7
8 public List<string> expressedTraits = new();
9}Separating both layers has a fundamental advantage: you can change balance without breaking saved data. The genome is the source of truth; stats are always derived, never stored.
3. Don't Store Final Stats
One of the first mistakes in the prototype was storing the character's final values directly.
1// Fragile — where did these numbers come from?
2cat.hp = 18;
3cat.attack = 9;Once you do this, you lose complete traceability. It's no longer clear what part of the result comes from genetics, what from modifiers, and what from calculation rules. Any balance change requires touching saved data.
Storing derived values as final data couples your balance logic to your save format. Every future rebalance becomes a migration problem. Avoid it from day one.
A more robust approach is to store only the genetic potential and calculate final values on demand. The CatStatResolver takes a genome and returns a fully computed phenotype:
1public class CatStatResolver
2{
3 public CatPhenotype Resolve(CatGenome genome)
4 {
5 var phenotype = new CatPhenotype();
6
7 // Base 8 + sum of both vitality alleles (range 0–3 each)
8 phenotype.maxHp = Math.Max(1,
9 8 + genome.vitality.alleleA + genome.vitality.alleleB);
10
11 // Base 3 + sum of both aggression alleles
12 phenotype.attack = Math.Max(1,
13 3 + genome.aggression.alleleA + genome.aggression.alleleB);
14
15 // Base 4 + sum of both agility alleles
16 phenotype.speed = Math.Max(1,
17 4 + genome.agility.alleleA + genome.agility.alleleB);
18
19 // Body size drives the visual scale
20 float avg = (genome.bodySize.alleleA + genome.bodySize.alleleB) / 2f;
21 phenotype.sizeScale = 0.8f + avg * 0.15f;
22
23 // Evaluate which latent traits should express
24 foreach (var trait in genome.latentTraits)
25 {
26 if (GenomeRules.ShouldExpressTrait(trait, genome))
27 phenotype.expressedTraits.Add(trait.id);
28 }
29
30 // Mutations always express
31 foreach (var mutation in genome.mutations)
32 phenotype.expressedTraits.Add(mutation.id);
33
34 return phenotype;
35 }
36}With this approach the system becomes much more flexible. If you change the formula tied to vitality tomorrow, all cats recalculate automatically, with no data migration needed.
4. The Breeding System
The fun part is, obviously, the breeding itself.
The basic logic in the prototype follows three steps:
- Each gene contains two alleles.
- The offspring inherits one allele from each parent.
- Possible mutations are then evaluated and applied.
One detail worth highlighting: the service receives an IRng interface rather than calling Unity's Random directly. This makes the system fully testable and deterministic: pass a fixed seed and you get reproducible breeding results every time.
A fixed seed lets you replay any breeding outcome exactly. When a player reports a surprising result, you can reproduce it instantly, with no guessing and no flaky tests.
1public class BreedingService
2{
3 private readonly IRng _rng;
4
5 public BreedingService(IRng rng) => _rng = rng;
6
7 public CatGenome Breed(
8 CatGenome parentA, CatGenome parentB, BreedingLog log)
9 {
10 var child = new CatGenome();
11
12 child.coatColor = InheritPair(
13 parentA.coatColor, parentB.coatColor, log);
14 child.bodySize = InheritPair(
15 parentA.bodySize, parentB.bodySize, log);
16 child.vitality = InheritPair(
17 parentA.vitality, parentB.vitality, log);
18 child.agility = InheritPair(
19 parentA.agility, parentB.agility, log);
20 child.aggression = InheritPair(
21 parentA.aggression, parentB.aggression, log);
22
23 child.generation = Math.Max(
24 parentA.generation, parentB.generation) + 1;
25
26 InheritLatentTraits(child, parentA, parentB, log);
27 MaybeInjectMutation(child, log);
28
29 return child;
30 }
31
32 private GenePair InheritPair(
33 GenePair a, GenePair b, BreedingLog log)
34 {
35 bool fromA = _rng.Next(0, 2) == 0;
36 bool fromB = _rng.Next(0, 2) == 0;
37
38 log.Record(
39 $"alleleA from parent {(fromA ? "A" : "B")}, " +
40 $"alleleB from parent {(fromB ? "A" : "B")}");
41
42 return new GenePair
43 {
44 alleleA = fromA ? a.alleleA : a.alleleB,
45 alleleB = fromB ? b.alleleA : b.alleleB,
46 };
47 }
48}It's not a particularly complex system, but it already produces interesting variation. The most important thing is that the logic lives inside an isolated service, making it easy to extend with new inheritance rules, weighted allele selection, or cross-generational trait tracking.
5. Latent Genes
One of the things I love most about Mewgenics' system is that not everything manifests immediately. Some traits only appear several generations later.
To replicate this, I added latent genes that only activate under certain conditions: recessive alleles that need to appear on both copies to express, or traits with a potency threshold:
1public static bool ShouldExpressTrait(
2 TraitGene trait, CatGenome genome)
3{
4 // Recessive traits require both alleles to match
5 if (trait.recessive &&
6 !GenomeRules.HasDoubleAllele(genome, trait.id))
7 return false;
8
9 // Potency below threshold stays dormant
10 return trait.potency > 0.65f;
11}This type of rule adds a lot more depth to the system. A mediocre cat can become strategically interesting simply because it carries a trait that hasn't expressed yet, turning every breeding decision into a question about potential, not just current stats.
Latent traits can also be inherited. During breeding, each parent's latent traits have a 40% chance of passing to the child, even if neither parent actually expressed the trait. A lineage can carry a recessive gene silently for generations before the right combination of alleles causes it to surface.
The 40% inheritance rate is a deliberate design knob. Lower it and rare traits become almost mythical; raise it and the gene pool saturates quickly. Tune it per-trait using GeneDefinition to give each gene its own rarity feel.
6. ScriptableObjects for Gene Definitions
To define the genetic catalog I used ScriptableObjects, not to store cat instances, but to represent gene definitions within the Unity editor:
1[CreateAssetMenu(menuName = "Genetics/Gene")]
2public class GeneDefinition : ScriptableObject
3{
4 public string id;
5 public string displayName;
6 public AlleleDefinition[] alleles;
7 public bool canMutate;
8 public float mutationChance;
9}This approach lets designers adjust the genetic catalog from the editor without touching code. New genes, allele variants, or mutation probabilities become data changes rather than programming tasks.
7. Debugging the System
When working with emergent systems, the biggest challenge usually isn't implementing them: it's understanding why a specific result occurred.
I added a simple breeding log that records every meaningful event during genome construction:
1public class BreedingLog
2{
3 public List<string> events = new();
4
5 public void Record(string message) => events.Add(message);
6}
7
8// Inside BreedingService:
9log.Record(
10 $"Inherited alleleA from parent {(fromA ? "A" : "B")}");
11log.Record(
12 $"Mutation: {mutation.id} at gen {child.generation}");Something as simple as recording which alleles were inherited, or when a mutation appeared, makes the balancing process significantly easier. Without this, emergent behavior becomes opaque: you see the result but can't trace the cause.
8. Testing Outside Unity
Because the domain layer has zero Unity dependencies, the entire system runs in a standalone .NET console app. Run it with dotnet run, or pass a seed to replay any specific run exactly: dotnet run -- 15348218.
The project ships with 21 unit tests across three suites, one per core service. They run automatically before the demos:
1=== BreedingServiceTests ===
2 [PASS] ChildGenerationIsParentPlusOne
3 [PASS] ChildAllelesComefromParents
4 [PASS] BreedingLogRecordsEvents
5 [PASS] MutationsCanBeInjected
6 [PASS] LatentTraitsCanBeInherited
7 [PASS] MultipleGenerationsIncrementCorrectly
8
9=== CatStatResolverTests ===
10 [PASS] MinimumHpIsOne
11 [PASS] MaxVitalityGivesMaxHp
12 [PASS] ZeroAgilityGivesBaseSpeed
13 [PASS] FullAggressionGivesMaxAttack
14 [PASS] SizeScaleIsWithinExpectedRange
15 [PASS] MutationsAlwaysExpress
16 [PASS] DominantTraitExpressesWithoutDoubleAllele
17 [PASS] RecessiveTraitRequiresDoubleAllele
18 [PASS] LowPotencyTraitDoesNotExpress
19
20=== GenomeRulesTests ===
21 [PASS] HomozygousVitalityIsDoubleAllele
22 [PASS] HeterozygousVitalityIsNotDoubleAllele
23 [PASS] DominantTraitExpressesWithHighPotency
24 [PASS] RecessiveWithDoubleAlleleAndHighPotencyExpresses
25 [PASS] RecessiveWithoutDoubleAlleleDoesNotExpress
26 [PASS] PotencyBelowThresholdPreventsExpression
27
28 Unit tests: 3 passed, 0 failedAfter the tests, three demos run automatically, each targeting a different aspect of the system:
Demo 1: 6-Generation Lineage
Breeds the same parent pair repeatedly and prints each offspring's full phenotype. Watch traits accumulate across generations: iron_hide and speed_burst surfacing from latent genes, vibrant_coat firing twice as a random mutation, aggression alleles drifting down by generation 6:
1[Parent A] Gen:0 HP:12 ATK:7 SPD:7 Scale:1.02
2 Latent: iron_hide (recessive, potency:0.90)
3
4[Parent B] Gen:0 HP:12 ATK:7 SPD:8 Scale:1.10
5 Latent: iron_hide (recessive, potency:0.90), speed_burst (potency:0.80)
6
7── Gen 1 ── HP:12 ATK:7 SPD:6 Scale:1.10 [🛡 ⚡ ✦]
8 Mutations: vibrant_coat (potency:0.10)
9
10── Gen 2 ── HP:12 ATK:7 SPD:8 Scale:1.10 [⚡ 🛡]
11── Gen 3 ── HP:12 ATK:7 SPD:8 Scale:1.25 [🛡 ⚡]
12── Gen 4 ── HP:12 ATK:7 SPD:8 Scale:1.10 [🛡 ✦]
13 Mutations: vibrant_coat (potency:0.10)
14
15── Gen 5 ── HP:12 ATK:7 SPD:8 Scale:1.25 [🛡 ⚡]
16── Gen 6 ── HP:12 ATK:5 SPD:8 Scale:1.10 [🛡]Demo 2: Mutation Frequency
Creates 30 random offspring and counts how often each mutation appears. Useful for tuning mutation pool weights before you even open the Unity editor:
1 vibrant_coat ████ 4/30 (13%)
2 giant_paws ██ 2/30 (7%)
3 hollow_bones █ 1/30 (3%)
4 feral_instinct █ 1/30 (3%)Demo 3: Recessive Trait Expression
Both parents carry iron_hide as a latent recessive trait. The demo breeds repeatedly until an offspring expresses it, then shows the winning genome:
1 Attempt 1: vitality (2, 1) dormant
2 Attempt 2: vitality (2, 1) dormant
3 Attempt 3: vitality (2, 1) dormant
4 Attempt 4: vitality (2, 1) dormant
5 Attempt 5: vitality (2, 2) iron_hide EXPRESSED ✓
6
7 HP:12 ATK:5 SPD:8 Scale:0.88 Traits: [🛡]Each demo also renders an ASCII cat via CatAsciiRenderer. Every visual element maps directly to a gene (ear shape to agility, eyes to aggression, mouth pattern to vitality, tail length to body size), so the art is a literal read of the genome:
1 /*\ /*\ ← spiked ears (agility alleles: 2+3)
2 < @ @ > ← alert eyes (aggression alleles: 3+1)
3 ( ## ) ← strong mouth (vitality alleles: 3+3)
4 `-vvvvvv-` ← large body (bodySize alleles: 1+3)
5 [🛡 ⚡] ← iron_hide, speed_burstHaving a concrete visual output for every breeding event made it significantly easier to catch bugs and validate the rules, especially the recessive trait logic, where a single wrong allele check produces a completely different phenotype.
Conclusions
Trying to replicate these kinds of systems is an excellent way to learn gameplay design, not so much for the genetics itself, but for everything around it:
- Data architecture and separation of concerns.
- Domain modeling independent of the engine.
- Procedural generation through simple, composable rules.
- Emergent design: complex outcomes from minimal inputs.
After building this prototype, I have a much clearer sense of why systems like Mewgenics' work so well. It's not just that the cats are weird.
It's that every generation creates new interesting decisions, and that's ultimately what keeps you playing.
The full source is on GitHub. Clone it, run the console demos, attach the Unity component to an empty GameObject, and see how far you can push it.


