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:

  1. Parsing de archivos de nivel (triggers, colliders, tiles, player)
  2. Creación de entidades del juego (triggers, colliders, jugador)
  3. Cálculo de chunks y sorting de tiles
  4. Renderizado de tiles y sistema parallax
  5. 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 con std::filesystem::path
  • parseBoolField() — parseo de "True"/"False"
  • parseHangTrigger() — parsing de HANG con climbBorder condicional
  • parseTrigger() — parsing genérico para SLIDE/SPIKE/WATER
  • parseCollider() — parsing de colliders con loop de vértices
  • parseTile() — parsing de tiles
  • parsePlayer() — 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::cout de 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, ParsedPlayer
  • TileRenderInfo — 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.cpp
  • src/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

  1. Refactorización incremental funciona: Primero extraer funciones (Fase 1), luego reorganizar archivos (Fase 2). Cada paso es verificable por separado.
  2. 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.
  3. 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.
  4. El parseo trivial va inline: Las funciones de 3-5 líneas que solo hacen istringstream → struct no necesitan un .cpp separado. Inline evita duplicación sin sacrificar claridad.
  5. 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.