Guardado rendimiento y pruebas - JRuizGil/MeltTheDeep GitHub Wiki
TDD · Documento técnico
La partida se guarda cada vez que subo a la superficie. Elegí ese momento porque no hay bolitas activas que guardar, y así no tengo que serializar cientos de cuerpos con física.
Lo que se guarda:
- La semilla de la partida y los módulos generados.
- Por cada chunk que el jugador ha modificado, su densidad comprimida con GZip.
- El estado abierto o cerrado de cada región de cueva.
- Las vetas que quedan y la integridad de cada una.
- Las mejoras compradas, el dinero y los minerales.
- Las especies registradas.
- Los huesos recogidos y los montados.
- Los módulos completados.
El formato es un JSON con los datos pequeños y un binario comprimido con los chunks, ambos en Application.persistentDataPath, y el JSON lleva un número de versión para poder migrar partidas si cambio la estructura. Al cargar no leo los módulos enteros: regenero cada uno desde la semilla (Generación de cuevas) y encima aplico los chunks guardados. Como la generación es determinista, las vetas y las regiones salen en el mismo orden, y el guardado las identifica por su índice.
Mido antes de optimizar. Los sistemas que más cuestan llevan un ProfilerMarker:
| Marcador | Qué mide |
|---|---|
CaveGenerator.Generate |
Generación de las cuevas de un módulo |
MeltSystem.Apply |
Un golpe de fundido |
ChunkMesher.Build |
Mallado de un chunk |
WaterBallPool.Spawn |
Creación de bolitas |
VacuumMode.Tick |
Búsqueda y fuerza de la aspiradora |
SnapValidator.Check |
Validación de un encaje |
Para poder comparar un cambio con el estado anterior uso escenarios fijos: la misma semilla y el mismo recorrido, de modo que la diferencia entre dos capturas viene del cambio y no del azar. Los bucles calientes no reservan memoria por fotograma: uso consultas NonAlloc, buffers reutilizados y nada de LINQ en Update. Los milisegundos de cada sistema los vuelco a un CSV para el informe, y cada captura queda anotada en el registro de medidas de Planificación.
| Riesgo | Señal | Plan |
|---|---|---|
| El remallado pasa de 3 ms |
ChunkMesher.Build por encima de 3 ms en una captura de referencia |
Remallo un chunk por fotograma en vez de dos y reviso el tamaño de chunk |
| Abrir una cueva grande ensucia muchos chunks de golpe | Pico de fotograma justo al abrir una cueva | Remallo primero los más cercanos y reparto el resto en varios fotogramas |
| La generación tarda demasiado | La carga del módulo se nota o CaveGenerator.Generate no mejora entre iteraciones en Sandbox_Caves
|
Ruido más grueso y paralelizo por chunk |
| La curva de aprendizaje de Jobs y Burst | Me atasco o aparecen errores que no sé interpretar | Escribo primero una versión en C# normal, mido, y solo después paso a Burst |
| Picos al cocinar colliders | Saltos en el Profiler tras remallar | El cocinado va en un job y el collider anterior sigue mientras tanto |
| Demasiadas bolitas | La física pasa de su presupuesto de 4 ms | Bajo el límite de activas, pongo más litros por bolita y duermo las bolitas antes |
| Objetos que atraviesan hielo fino | Un pez o un hueso aparece al otro lado de una pared | Colisión continua para los objetos grandes y suelto los atrapados solo cuando el collider del chunk ya está actualizado |
| FMOD rompe el build | La compilación falla o el juego arranca sin sonido | Compilo pronto y a menudo para enterarme en el momento |
Los umbrales de la columna Señal son valores iniciales y los ajusto con las primeras capturas.
Las pruebas EditMode comprueban lógica pura, sin escena:
- El generador es determinista: el hash del campo es el mismo en 200 semillas.
- Todas las anclas quedan conectadas.
- Las zonas protegidas no tienen aire.
- El volumen de cuevas cae dentro del rango de su módulo.
- Las vetas solo aparecen en superficies de cuevas.
- La fórmula de coste de las mejoras.
- El freno por agua acumulada.
- El recálculo de
PlayerStatsal comprar una mejora. - La validación del encaje, incluido el lado equivocado.
Las pruebas PlayMode comprueban comportamientos que dependen de la física y de la escena:
- Una bolita nunca se fusiona dos veces.
- Con el tanque lleno, la aspiradora deja de aspirar.
- La apertura bloquea los objetos más grandes que ella.
- Una cueva se abre cuando se funde su pared.
- Un hueso se monta en su sitio y un hueso mal colocado resbala.
El repositorio está en GitHub con estas ramas:
| Rama | Uso |
|---|---|
main |
Siempre jugable |
dev |
Integración del trabajo en curso |
feature/<sistema> |
Una rama por sistema, por ejemplo feature/cuevas
|
Marco las entregas con las etiquetas ae2, ae3 y ae4. Los archivos pesados van con Git LFS: *.fbx, *.png, *.psd, *.wav y *.bank. Para que Git pueda leer las escenas y los prefabs uso Asset Serialization en Force Text y UnityYAMLMerge, y parto del .gitignore estándar de Unity.
Trabajo con commits pequeños. Al empezar cada sesión hago pull y al terminarla hago push, para que lo que hay en GitHub y lo que hay en el PC de clase nunca difieran mucho.