Llevo varios días jugando a Mewgenics y estoy completamente enganchado.
Más allá del combate táctico y del tono absurdo del juego, lo que realmente me atrapó fue su sistema de crianza. Cada gato que nace puede cambiar completamente tu estrategia, y lo que suele ser más interesante no es si un gato es poderoso ahora mismo, sino qué potencial tiene su linaje.
Después de unas cuantas horas jugando me entró la curiosidad inevitable: ¿cómo implementaría algo así en Unity?
Así que decidí construir un prototipo funcional completo. Este artículo es básicamente un resumen de cómo lo enfoqué: la arquitectura que utilicé, los errores que cometí y los problemas que me encontré por el camino. La implementación completa está disponible en GitHub para que puedas ejecutarla, romperla y ampliarla tú mismo.
No pretende ser una reproducción exacta del sistema de Mewgenics (no conozco la implementación interna), sino una exploración técnica de cómo diseñar algo similar desde cero.
1. El primer error: empezar por el gato
Mi primer impulso fue crear un CatController con stats, apariencia, genes y lógica de reproducción, todo junto en la misma clase.
Duró unos diez minutos.
En cuanto empiezas a pensar en generaciones, mutaciones, rasgos latentes o genealogía, queda claro que esto no es un objeto visual, sino un sistema de dominio. El gato que aparece en pantalla es simplemente la representación final de un conjunto de datos mucho más complejo.
Al final terminé separando el sistema en tres capas bastante claras:
- Dominio: genética, herencia y mutaciones.
- Runtime: estado del gato durante la partida.
- Presentación: sprites, animaciones y UI.
La regla que me impuse fue simple:
El sistema genético debe poder ejecutarse sin Unity.
Si el sistema puede ejecutarse en tests o incluso en una pequeña app de consola, entonces probablemente está bien desacoplado. Esta restricción me obligó a mantener la lógica de dominio libre de dependencias de MonoBehaviour desde el principio. Todas las clases del dominio viven bajo Assets/Scripts/Domain/ sin ninguna dependencia de Unity (son C# puro que puede ejecutarse en cualquier runtime .NET).
2. Separar genotipo y fenotipo
Este fue probablemente el cambio de mentalidad más importante. En lugar de pensar en un gato como un conjunto de stats, el sistema lo modela en dos capas distintas.
Genotipo
La información heredable (lo que se transmite a través de las generaciones):
- Alelos
- Mutaciones
- Rasgos latentes
- Afinidades
Fenotipo
Lo que realmente vemos en el juego (el resultado observable):
- Apariencia
- Stats finales
- Rasgos activos
En el prototipo terminé representándolo de esta forma:
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}El fenotipo se deriva a partir de ese genoma:
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}Separar ambas capas tiene una ventaja fundamental: se puede cambiar el balance sin romper los datos guardados. El genoma es la fuente de verdad; los stats siempre se derivan, nunca se almacenan directamente.
3. No guardar los stats finales
Uno de los primeros errores del prototipo fue guardar directamente los valores finales del personaje.
1// Frágil — ¿de dónde vienen estos números?
2cat.hp = 18;
3cat.attack = 9;Una vez que haces esto, pierdes toda la trazabilidad del sistema. Ya no queda claro qué parte del resultado proviene de la genética, qué parte de modificadores y qué parte de las reglas de cálculo. Cualquier ajuste de balance requiere tocar los datos guardados.
Guardar valores derivados como datos finales acopla tu lógica de balance al formato de guardado. Cualquier rebalanceo futuro se convierte en un problema de migración. Evítalo desde el primer día.
Una alternativa más robusta consiste en guardar únicamente el potencial genético y calcular los valores finales cuando se necesitan. El CatStatResolver recibe un genoma y devuelve un fenotipo completamente calculado:
1public class CatStatResolver
2{
3 public CatPhenotype Resolve(CatGenome genome)
4 {
5 var phenotype = new CatPhenotype();
6
7 // Base 8 + suma de ambos alelos de vitalidad (rango 0–3 cada uno)
8 phenotype.maxHp = Math.Max(1,
9 8 + genome.vitality.alleleA + genome.vitality.alleleB);
10
11 // Base 3 + suma de ambos alelos de agresión
12 phenotype.attack = Math.Max(1,
13 3 + genome.aggression.alleleA + genome.aggression.alleleB);
14
15 // Base 4 + suma de ambos alelos de agilidad
16 phenotype.speed = Math.Max(1,
17 4 + genome.agility.alleleA + genome.agility.alleleB);
18
19 // El tamaño corporal determina la escala visual
20 float avg = (genome.bodySize.alleleA + genome.bodySize.alleleB) / 2f;
21 phenotype.sizeScale = 0.8f + avg * 0.15f;
22
23 // Evaluar qué rasgos latentes deben expresarse
24 foreach (var trait in genome.latentTraits)
25 {
26 if (GenomeRules.ShouldExpressTrait(trait, genome))
27 phenotype.expressedTraits.Add(trait.id);
28 }
29
30 // Las mutaciones siempre se expresan
31 foreach (var mutation in genome.mutations)
32 phenotype.expressedTraits.Add(mutation.id);
33
34 return phenotype;
35 }
36}Con este enfoque el sistema se vuelve mucho más flexible. Si mañana se cambia la fórmula asociada a vitality, todos los gatos se recalculan automáticamente, sin necesidad de migrar datos.
4. El sistema de reproducción
La parte divertida es, obviamente, la crianza.
La lógica básica del prototipo sigue tres pasos:
- Cada gen contiene dos alelos.
- El hijo hereda un alelo de cada padre.
- Después se evalúan y aplican las posibles mutaciones.
Un detalle importante: el servicio recibe una interfaz IRng en lugar de llamar directamente a Random de Unity. Esto hace que el sistema sea completamente testeable y determinista. Pasa una semilla fija y obtienes resultados de crianza reproducibles en cada ejecución.
Una semilla fija permite reproducir cualquier resultado de crianza al instante. Cuando un jugador reporta algo inesperado, puedes replicarlo exactamente, sin conjeturas ni tests inconsistentes.
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 del padre {(fromA ? "A" : "B")}, " +
40 $"alleleB del padre {(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}No es un sistema especialmente complejo, pero ya genera variación interesante. Lo más importante es que la lógica queda contenida en un servicio aislado, lo que facilita ampliarla con nuevas reglas de herencia, selección de alelos ponderada o seguimiento de rasgos entre generaciones.
5. Genes latentes
Una de las cosas que más me gustan del sistema de Mewgenics es que no todo se manifiesta inmediatamente. Hay rasgos que aparecen varias generaciones después.
Para replicar esa idea añadí genes latentes que solo se activan bajo ciertas condiciones (alelos recesivos que necesitan aparecer en ambas copias para expresarse, o rasgos con un umbral de potencia):
1public static bool ShouldExpressTrait(
2 TraitGene trait, CatGenome genome)
3{
4 // Los rasgos recesivos requieren ambos alelos coincidentes
5 if (trait.recessive &&
6 !GenomeRules.HasDoubleAllele(genome, trait.id))
7 return false;
8
9 // Una potencia por debajo del umbral permanece latente
10 return trait.potency > 0.65f;
11}Este tipo de reglas introduce bastante más profundidad en el sistema. Un gato mediocre puede resultar estratégicamente interesante simplemente porque porta un rasgo que todavía no se expresa, convirtiendo cada decisión de crianza en una pregunta sobre potencial, no solo sobre stats actuales.
Los rasgos latentes también se heredan. Durante la reproducción, cada rasgo latente de los padres tiene un 40% de probabilidad de pasar al hijo, aunque ninguno de los dos padres lo exprese. Un linaje puede cargar un gen recesivo en silencio durante generaciones hasta que la combinación correcta de alelos lo saque a la superficie.
El 40% de probabilidad de herencia es un parámetro de diseño deliberado. Bájalo y los rasgos raros se vuelven casi míticos; súbelo y el pool genético se satura rápido. Ajústalo por rasgo usando GeneDefinition para dar a cada gen su propia sensación de rareza.
6. ScriptableObjects para definir genes
Para definir el catálogo genético utilicé ScriptableObjects, no para guardar instancias de gatos, sino para representar definiciones de genes dentro del editor de Unity:
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}Este enfoque permite a los diseñadores ajustar el catálogo genético desde el editor sin tocar el código. Nuevos genes, variantes de alelos o probabilidades de mutación se convierten en cambios de datos, no de programación.
7. Debugging del sistema
Cuando se trabaja con sistemas emergentes, el mayor problema no suele ser implementarlos, sino entender por qué ha ocurrido un resultado concreto.
Añadí un log de reproducción sencillo que registra cada evento relevante durante la construcción del genoma:
1public class BreedingLog
2{
3 public List<string> events = new();
4
5 public void Record(string message) => events.Add(message);
6}
7
8// Dentro de BreedingService:
9log.Record(
10 $"Heredado alleleA del padre {(fromA ? "A" : "B")}");
11log.Record(
12 $"Mutación: {mutation.id} en gen {child.generation}");Algo tan simple como registrar qué alelos se heredan o cuándo aparece una mutación facilita muchísimo el proceso de balance. Sin esto, el comportamiento emergente se vuelve opaco. Ves el resultado pero no puedes rastrear la causa.
8. Tests fuera de Unity
Como la capa de dominio no tiene ninguna dependencia de Unity, el sistema completo puede ejecutarse en una app de consola .NET independiente. Ejecútalo con dotnet run, o pasa una semilla para reproducir cualquier ejecución exactamente: dotnet run -- 15348218.
El proyecto incluye 21 tests unitarios en tres suites (una por servicio principal). Se ejecutan automáticamente antes de los 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
24 [PASS] DominantTraitExpressesWithHighPotency
25 [PASS] RecessiveWithDoubleAlleleAndHighPotencyExpresses
26 [PASS] RecessiveWithoutDoubleAlleleDoesNotExpress
27 [PASS] PotencyBelowThresholdPreventsExpression
28
29 Unit tests: 3 passed, 0 failedTras los tests, tres demos se ejecutan automáticamente, cada uno orientado a un aspecto distinto del sistema:
Demo 1. Linaje de 6 generaciones
Cría la misma pareja de padres repetidamente e imprime el fenotipo completo de cada descendiente. Observa cómo los rasgos se acumulan a lo largo de las generaciones, iron_hide y speed_burst emergiendo de genes latentes, vibrant_coat disparándose dos veces como mutación aleatoria, y los alelos de agresión cayendo en la generación 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. Frecuencia de mutaciones
Genera 30 descendientes aleatorios y cuenta cuántas veces aparece cada mutación. Muy útil para ajustar los pesos del pool de mutaciones antes de abrir el editor de Unity:
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. Expresión de rasgo recesivo
Ambos padres portan iron_hide como rasgo latente recesivo. El demo cría repetidamente hasta que un descendiente lo expresa y muestra el genoma ganador:
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: [🛡]Cada demo también renderiza un gato en ASCII mediante CatAsciiRenderer. Cada elemento visual mapea directamente a un gen (forma de las orejas a agilidad, ojos a agresión, patrón de la boca a vitalidad, longitud de la cola a tamaño corporal), así que el arte es una lectura literal del genoma:
1 /*\ /*\ ← orejas puntiagudas (alelos agilidad: 2+3)
2 < @ @ > ← ojos alerta (alelos agresión: 3+1)
3 ( ## ) ← boca fuerte (alelos vitalidad: 3+3)
4 `-vvvvvv-` ← cuerpo grande (alelos bodySize: 1+3)
5 [🛡 ⚡] ← iron_hide, speed_burstTener una salida visual concreta para cada evento de crianza facilitó enormemente detectar bugs y validar las reglas, especialmente la lógica de rasgos recesivos, donde un único allele check incorrecto produce un fenotipo completamente diferente.
Conclusiones
Intentar replicar este tipo de sistemas es una forma excelente de aprender diseño de gameplay, no tanto por la genética en sí, sino por todo lo que hay alrededor:
- Arquitectura de datos y separación de responsabilidades.
- Modelado de dominio independiente del motor.
- Generación procedural mediante reglas simples y componibles.
- Diseño emergente: resultados complejos a partir de inputs mínimos.
Después de construir este prototipo, tengo bastante más claro por qué sistemas como el de Mewgenics funcionan tan bien. No es solo que los gatos sean raros.
Es que cada generación crea nuevas decisiones interesantes, y eso es, al final, lo que hace que quieras seguir jugando.
El código completo está en GitHub. Clónalo, ejecuta los demos de consola, adjunta el componente de Unity a un GameObject vacío y comprueba hasta dónde puedes llevarlo.


