Esta es la segunda parte de la serie de refactorizaciones del motor Masbits. En la primera parte documentamos la descomposición de EditorActions de 373 líneas a 27. Ahora tocaba el turno a loadLevelFromFile — la función que más dolores de cabeza causaba.
El Problema: 5 Responsabilidades en una Función
loadLevelFromFile era el punto de entrada para cargar niveles desde archivos de texto. El problema no era solo su tamaño (391 líneas), sino que mezclaba cinco responsabilidades completamente distintas en un solo archivo de 537 líneas:
- Parsing de archivos de nivel (triggers, colliders, tiles, player)
- Creación de entidades del juego (triggers, colliders, jugador)
- Cálculo de chunks y sorting de tiles
- Renderizado de tiles y sistema parallax
- Orquestación general del flujo de carga
Métricas Originales
| Métrica | Valor |
|---|---|
| Líneas totales del .cpp | 537 |
loadLevelFromFile LOC |
391 |
| Complejidad cognitiva | 187 |
| Nesting máximo | 9 niveles |
| Maintainability Index | 13.6 |
| Responsabilidades | 5 mezcladas |
Con un MI de 13.6, el código estaba en territorio crítico. Cualquier cambio en el formato de nivel requería entender 391 líneas de lógica entrelazada.
La Solución: Refactorización en Dos Fases
Fase 1: Extracción de Funciones
El primer paso fue extraer las lógicas identificables en funciones independientes, sin moverlas a otros archivos. Esto redujo loadLevelFromFile de 391 a 80 líneas, pero el .cpp seguía teniendo 537 líneas con todo mezclado.
15 funciones extraídas en 4 categorías:
Parsing (7):
findOrAddFile()— búsqueda semántica de archivos constd::filesystem::pathparseBoolField()— parseo de "True"/"False"parseHangTrigger()— parsing de HANG con climbBorder condicionalparseTrigger()— parsing genérico para SLIDE/SPIKE/WATERparseCollider()— parsing de colliders con loop de vérticesparseTile()— parsing de tilesparsePlayer()— parsing de spawn del jugador
Creación de entidades (5):
createHangTrigger(),createTriggerSlide/Spike/Water()createCollider(),createPlayer()
Tile rendering (1):
computeTileRenderData()— cálculo de offset, scale, model matrix
Chunk y sorting (4):
calculateSceneBounds(),createChunkGrid()assignTilesToChunks(),createParallaxTiles(),sortAndAssignToRender()
Código eliminado en Fase 1:
- 6
std::coutde debug - 3 bloques de código comentado obsoleto
- 2 implementaciones inconsistentes de asset lookup (unificadas en
findOrAddFile) - Lógica de tile rendering duplicada (unificada en
computeTileRenderData)
Fase 2: Modularización en 4 Archivos
Aunque la Fase 1 resolvió el problema de la función gigante, el .cpp seguía siendo un archivo monolítico con 5 responsabilidades. La Fase 2 aplicó Single Responsibility dividiendo en 4 módulos:
1. parsedLevelData.hpp — Structs y Parseo
116 líneas con structs de datos intermedios y funciones de parseo inline:
ParsedHangTrigger,ParsedTrigger,ParsedCollider,ParsedTile,ParsedPlayerTileRenderInfo— resultado de cálculo de rendering- Funciones
parse*()inline — triviales (istringstream → struct), no justifican un .cpp
2. levelEntityFactory.hpp/cpp — Creación de Entidades
43 + 73 líneas. Cada create*() depende de CGameObjectFactory, CPhysic, y rend::CVulkanRender. Agruparlas evita incluir headers pesados en el orquestador.
3. levelChunkUtils.hpp/cpp — Chunks y Rendering
60 + 238 líneas. El módulo más grande, con lógica pura de datos sin dependencias de ECS/física. Incluye findOrAddFile(), cálculo de chunks, y sorting de tiles. Fácil de testear por separado.
4. levelLoadingUtils.cpp — Orquestador
Solo 128 líneas de orquestación:
Read lines → Parse → Create entities → Build chunks → Sort → Load assets
El dispatch mantiene la cadena if/else (7 tipos con line.contains()). Un std::variant+visitor sería over-engineering para este caso.
Resultados: Métricas Comparativas
| Métrica | Original | Fase 1 | Fase 2 | Mejora total |
|---|---|---|---|---|
| Líneas totales del .cpp | 537 | 537 | 128 | -76% |
loadLevelFromFile LOC |
391 | 80 | 72 | -82% |
| Complejidad cognitiva | 187 | ~30 | < 15 | -92% |
| Max nesting | 9 | 5 | 3 | -67% |
| Maintainability Index | 13.6 | > 40 | > 50 | +268% |
| Responsabilidades | 5 mezcladas | 5 en 1 archivo | 1 por archivo | SRP |
API Pública Intacta
levelLoadingUtils.hpp no cambió. Los consumidores existentes no necesitan modificaciones:
src/Game/Manager/gamemanager.cppsrc/Editor/Manager/editormanager.cpp
Decisiones de Diseño
| Decisión | Razón |
|---|---|
Free functions en namespace GameUtils |
Sigue convenciones del proyecto |
| Forward declarations en headers | Evita incluir headers pesados (renderer, physic, ECS) |
inline en parse functions |
Funciones triviales, no necesitan .cpp |
| Dispatch if/else mantenido | 7 tipos con line.contains() es legible y directo |
kDefaultScale en anonymous namespace |
Solo lo usa el orchestrator |
LevelConstants en levelChunkUtils |
Solo lo usan las funciones de chunks |
Lecciones Aprendidas
- Refactorización incremental funciona: Primero extraer funciones (Fase 1), luego reorganizar archivos (Fase 2). Cada paso es verificable por separado.
- SRP no es solo funciones: Un archivo con 537 líneas y 15 funciones extraídas sigue siendo un problema. SRP aplica a módulos, no solo a funciones.
- Las métricas cuentan la historia: Pasar de MI 13.6 a >50 no es solo números — es la diferencia entre código que da miedo tocar y código que se entiende en segundos.
- El parseo trivial va inline: Las funciones de 3-5 líneas que solo hacen
istringstream → structno necesitan un .cpp separado. Inline evita duplicación sin sacrificar claridad. - La orquestación simple se mantiene: Un if/else con 7 tipos es más legible que un patrón visitor para este caso. No todo necesita abstracción.
Conclusión — Día 1
loadLevelFromFile pasó de ser la función más temida del proyecto a ser un orquestador claro de 128 líneas. La combinación de extracción incremental + modularización SRP demostró que incluso las funciones más complejas se pueden domar sin rewrite completo.
La clave fue no intentar refactorizar todo de una. Primero functions, luego files. Cada paso verificable, cada cambio pequeño y seguro.
Si te perdiste la primera parte, aquí está la refactorización de EditorActions.
Día 2: 2026-06-08
3. Renderer — buildCommandBuffers split
Archivo: src/EngineCores/RenderCore/Renderer/renderer.cpp (1572 → 1489 LOC, -83 LOC)
Problema: buildCommandBuffers era 456 LOC con 3 concerns mezclados: offscreen rendering (tiles, renderables, editor grid), screen composite (swapchain), y editor visualization.
Solución: Split en 2 métodos + orchestrator.
buildCommandBuffers() {
for (size_t i{}; i < drawCmdBuffers_.size(); ++i) {
vkBeginCommandBuffer(drawCmdBuffers_[i], &cmdBufInfo);
recordOffscreenPass(drawCmdBuffers_[i], i);
recordScreenPass(drawCmdBuffers_[i], i);
vkEndCommandBuffer(drawCmdBuffers_[i]);
}
}| Método | LOC | Responsabilidad |
|---|---|---|
buildCommandBuffers |
13 | Orchestrator |
recordOffscreenPass |
276 | Tiles, renderables, editor grid, selection, overlays |
recordScreenPass |
83 | Offscreen→shader read, swapchain composite, GUI |
También se limpió código comentado muerto (parallax back/front, camera correction).
4. GUIMenus — nextFrame cleanup
Archivo: src/Editor/RenderUI/GUIMenus.cpp (2423 → 2316 LOC, -107 LOC)
4.1 Extract renderCameraControlsWindow
Bloque: 78 LOC inline en nextFrame para la ventana de Camera Controls.
Resultado: nextFrame de 208 → 82 LOC. Nuevo método de 67 LOC.
4.2 Extract handleTilesetDialogResult
Bloque: 52 LOC de result handling (create/edit mode routing). Se limpió todo el debug logging innecesario.
Resultado: Bloque de 52 → 33 LOC.
4.3 Extract detail::getIconForType<T>
Problema: renderObjectList tenía un if/else chain de 15 líneas para seleccionar iconos por tipo.
Solución: Helper constexpr con if constexpr.
Resultado: renderObjectList de 72 → 52 LOC.
Resumen: Motor (2 días)
| Archivo | LOC antes | LOC después | Cambio |
|---|---|---|---|
| levelLoadingUtils.cpp | 537 | 128 | -409 |
| renderer.cpp | 1572 | 1489 | -83 |
| GUIMenus.cpp | 2423 | 2316 | -107 |
| Neto motor | ~600 LOC |
Archivos creados (Día 1)
src/Game/Util/parsedLevelData.hpp(116 LOC)src/Game/Util/levelEntityFactory.hpp/cpp(43+73 LOC)src/Game/Util/levelChunkUtils.hpp/cpp(60+238 LOC)
Patrones aplicados (Día 2)
- Orchestrator pattern: buildCommandBuffers (13 LOC orchestrator)
- Method extraction: recordOffscreenPass, recordScreenPass, renderCameraControlsWindow, handleTilesetDialogResult
- Shared helper: detail::getIconForType
- Dead code removal: commented parallax code in renderer
Build verification
Todos los cambios del Día 2 pasaron build sin errores nuevos.