Instalación interactiva para el Día de Ingeniería Multimedia: los participantes crean un avatar y el sistema genera automáticamente un leitmotiv musical único mediante audio procedural. Ver docs/Propuesta_Proyecto_Avatares_Sonoros.md para la propuesta completa.
- Motor: Unity 6.3 LTS
- Audio (fase actual): síntesis procedural en C# puro (sin dependencias externas)
- Audio (fase futura): integración con FMOD Studio — la arquitectura ya está separada para ese cambio (ver
Assets/Scripts/Audio/IMusicRenderer.cs)
- Instalar Unity Hub y, dentro de él, Unity 6.3 LTS (mismo build para todo el equipo — no usar otra versión).
- Clonar este repo.
- Abrir Unity Hub → "Add project from disk" → seleccionar la carpeta clonada. Unity generará automáticamente
Library/,ProjectSettings/yPackages/la primera vez que se abra (por eso no están versionados). - Confirmar que
Assets/Scripts/aparece en el Project window de Unity con las subcarpetasAudio/,Avatar/,World/,UI/.
Assets/
Scripts/
Avatar/ -> datos y lógica de personalización del avatar
Audio/ -> generación del leitmotiv (lógica) + renderers (síntesis)
World/ -> mundo 2.5D, galería, spawn de avatares
UI/ -> personalización, ficha informativa
docs/
Propuesta_Proyecto_Avatares_Sonoros.md -> propuesta y plan de sprints
Separada en capas para que Diseño pueda ajustar valores sin tocar código, y para poder cambiar de motor de síntesis sin tocar la lógica musical:
AvatarAttributes(Avatar/) — los datos que elige el participante.LeitmotivGenerator(Audio/) — decide qué se toca (escala, tónica, tempo, notas) a partir de los atributos. Ya NO tiene las reglas de mapeo escritas en código — las lee de unLeitmotivMappingConfig.LeitmotivMappingConfig(Audio/, ScriptableObject) — esto lo edita Diseño en el Inspector de Unity, sin abrir código: qué escala corresponde a cadaTrait, qué tempo a cadaAccessory, qué instrumento a cadaClothing. Crear uno con clic derecho en Project →Create → AudioProceduralAvatar → Leitmotiv Mapping Config.InstrumentPreset(Audio/, ScriptableObject) — un timbre completo (forma de onda + volumen + ADSR). Diseño crea los que necesite conCreate → AudioProceduralAvatar → Instrument Preset. El campoPresetIddebe coincidir con el instrumento referenciado enLeitmotivMappingConfig.IMusicRenderer(Audio/) — contrato de cómo suena. Implementación actual:SimpleSynthRenderer, con osciladores (seno/cuadrada/sierra/triángulo) y envolvente ADSR real, tomando el timbre de losInstrumentPresetasignados. Implementación futura:FMODRenderer, sin tocarLeitmotivGeneratorni los presets.MusicTheory(Audio/) — conversión pura de grado de escala → nota MIDI → frecuencia. Sin dependencias de Unity.LeitmotivDemo(Audio/) — script de prueba: genera un avatar random y lo reproduce al presionar Play o Espacio. Bórralo o desactívalo cuando empiece la UI real — es solo para validar que se escucha algo.
- Crea un GameObject vacío en una escena de prueba.
- Agrégale
LeitmotivGenerator,SimpleSynthRenderer,LeitmotivDemo. - Crea al menos un
InstrumentPreset(Project → Create → AudioProceduralAvatar → Instrument Preset) y asígnalo en la listaPresetsdelSimpleSynthRenderer, además de comoFallback Preset. - (Opcional) Crea un
LeitmotivMappingConfigy asígnalo alLeitmotivGenerator— si no lo asignas, usa valores por defecto internos. - Dale Play. Deberías escuchar un motivo corto. Presiona Espacio para generar otro avatar random y comparar.
La personalización real (hecha por el equipo) arma el avatar con AvatarCreator + AvatarLayer: capas de sprites (Body, Head, Hair, ...) navegables por índice, más nombre y código estudiantil vía AvatarData. Nuestro modelo de datos se ajustó a esto:
AvatarProfile(Avatar/) — reemplaza al viejoAvatarAttributes. EsId + AvatarName + StudentCode + List<LayerSelection>, donde cadaLayerSelectiones(LayerName, SpriteIndex). No asume cuáles capas existen ni cuántas — si se agrega una capa nueva, no hay que tocar esta clase.RootNoteStrategy(Audio/, ScriptableObject abstracto) — la fuente de la tónica musical todavía no está decidida (no hay selector de color en la UI real). Se dejó como pieza intercambiable a propósito: hoy usaLayerHashRootNoteStrategy(hash determinista de las capas elegidas), y el día que se decida otra fuente, se crea una nueva estrategia y se reasigna en el Inspector delLeitmotivGenerator— cero cambios en el resto del sistema.LeitmotivMappingConfig— las reglas de Diseño (escala/tempo/instrumento) ahora se definen por(nombre de capa, índice de sprite)en vez de por categoría con nombre. Ej: "capa Hair, índice 2 → escala Dórica".
AvatarJsonStorage— guarda/cargaAvatarProfilecomo JSON enApplication.persistentDataPath/avatars/{id}.json, y opcionalmente una imagen capturada en{id}.png. Desacopla personalización de galería: no se hablan en memoria, solo a través de estos archivos — así da igual si terminan en la misma pantalla o en dos PCs distintas para el evento.AvatarCreationController(Avatar/) — el pegamento con los scripts de tu compañero. Se conecta al botón "Crear avatar" (hay que agregarlo a la UI, no existe todavía): leeAvatarCreator+AvatarData, arma elAvatarProfile, genera el leitmotiv, captura la imagen conAvatarCapture(si está asignado), y guarda todo conAvatarJsonStorage.AvatarGalleryLoader(World/) — al iniciar la escena de galería, lee todos los avatares guardados en disco y los coloca en el mundo conAvatarGalleryManager.CreateAndSpawn. El leitmotiv se regenera ahí mismo a partir del perfil (no se duplica en el JSON) — mientras la configuración sea la misma en ambas escenas, el resultado es siempre igual.AvatarDisplay— ya no pinta un cubo de color: usa unSpriteRenderery muestra la imagen real capturada del avatar (o unfallbackSpritesi todavía no hay captura).
- En la escena de personalización, agrega un componente
AvatarCreationControllera cualquier GameObject (ej.Managers). - Asigna en el Inspector:
Avatar Creator,Avatar Data(los que ya existen enManagers), yLeitmotiv Generator(agrégalo si no está en la escena).Avatar Capturees opcional — si el render de captura no está armado todavía, simplemente se guarda sin imagen. - Verifica que
Layer Namestenga exactamenteBody,Head,Hair(deben coincidir con loslayerNameconfigurados enAvatarCreator). - Agrega un botón "Crear avatar" a la UI (Canvas), y en su
OnClick()arrastra el GameObject conAvatarCreationControllery seleccionaCreateAvatar().
- En la escena de personalización: personaliza un avatar, dale clic a "Crear avatar". Revisa la Console — debe loguear que se guardó con su id.
- En la escena de galería: en vez de (o junto con)
GalleryDemo, agregaAvatarGalleryLoaderalGalleryManager, y asígnale el mismoLeitmotivGenerator. - Dale Play a la escena de galería — el avatar que acabas de crear debería aparecer ahí, con su imagen real (si
AvatarCaptureestaba asignado).
Alcance mínimo definido en la propuesta, y su estado:
- Personalizar un avatar
- Generar automáticamente su leitmotiv
- Asignarle un nombre
- Almacenar el personaje dentro del mundo interactivo
- Visualizar su ficha informativa al seleccionarlo — pop-up sobre el avatar, se oculta sola. Ver sección "Ficha informativa" más abajo.
- Reproducir automáticamente su identidad sonora
- Validación de nombre/código (palabras prohibidas + código estudiantil único)
MVP completo. Quedan los pendientes de pulido/expansión listados abajo.
Otros pendientes identificados en el camino:
- Mejorar la generación melódica — hoy
LeitmotivGenerator.GenerateNoteses un patrón determinista simple, no tiene reglas melódicas reales (evitar saltos grandes, resolver hacia la tónica, etc.). - Decidir la fuente real de la tónica musical (
RootNoteStrategy— hoy usa hash de capas por defecto, sin decidir si eso es lo definitivo). - Configurar
AvatarCapture(cámara + RenderTexture) si todavía no está armado, para que la galería muestre la imagen real en vez delfallbackSprite. - Confirmar con el equipo audiovisual el número de pantallas y conectar
Display.displays[]. - Asignar nombres del equipo a los sprints del plan (
Propuesta_Proyecto_Avatares_Sonoros.docx). - Rendimiento con muchos avatares acumulados durante horas de evento (pooling / desactivar renderers fuera de cámara) — solo un riesgo identificado, no atacado aún.
El escenario es un diorama de planos paralelos en profundidad (eje Z): cada plano es como un "piso" donde viven avatares. El jugador se mueve en X dentro del plano actual (side-scroller) y salta entre planos con una tecla dedicada — no es cámara libre. Los avatares son sprites 2D planos ("cartón"), sin rotación hacia la cámara — quedan siempre de frente porque la cámara solo cambia de plano en Z, nunca rodea al avatar.
GalleryPlane(World/) — datos de un plano: su posición Z, capacidad, y qué avatares contiene.AvatarInstance(World/) — el "expediente" de un avatar creado: suAvatarProfile+ su leitmotiv ya generado + la imagen capturada (si existe).AvatarGalleryManager(World/, singleton) — punto único de entrada:CreateAndSpawn(profile, leitmotiv, capturedImage)registra el avatar y lo coloca en el primer plano con espacio. Soporta planos autorados por Diseño (una lista deTransformcolocados a mano en el editor) y creación automática de planos nuevos si todos los autorados ya están llenos.AvatarDisplay(World/) — vive en cada avatar instanciado. Muestra la imagen real capturada del avatar (sprite plano, sin rotación) o unfallbackSpritesi aún no hay captura. Expone un eventoSelected(clic sobre el avatar) como gancho para el sistema de selección/ficha.GalleryPlayerController(World/) — movimiento en X acotado a los avatares del plano actual, y salto discreto entre planos con flechas arriba/abajo (no continuo, no instantáneo del todo — una transición corta en Z). La cámara sigue al jugador en ambos ejes.GalleryDemo(World/) — SOLO pruebas: crea 14 avatares random al iniciar (con la capacidad por defecto de 6/plano, esto genera 3 planos automáticamente) para poder probar el recorrido y el salto entre planos sin esperar a la UI real.
- En una escena, crea un
Plane(oCubeaplastado) como piso. - (Opcional pero recomendado) Crea 2-3 GameObjects vacíos como marcadores de plano — por ejemplo en Z=0, Z=5, Z=10 — y asígnalos en orden a la lista
Authored Plane MarkersdelAvatarGalleryManager. Si no asignas ninguno, arranca con un plano en Z=0 y crea los demás automáticamente conforme se llenan. - Crea un GameObject
PlayerconGalleryPlayerController, asígnale la cámara de la escena enCamera Transform, y elAvatarGalleryManageren el campoGallery Manager. - Crea un GameObject
GalleryManagerconAvatarGalleryManager. Asígnale:- Un prefab con
AvatarDisplay— necesita unSpriteRenderery unBoxCollider(agrega un GameObject vacío, ponleSprite Renderer,Box Collider, yAvatar Display; asígnale unfallbackSpritecualquiera para que se vea algo mientras no hay captura real). - Un
Transformvacío comoSpawn Origin(dónde arranca la fila de avatares en cada plano). - (Opcional) el
SimpleSynthRendererde la escena, para poder escuchar el leitmotiv al hacer clic en un avatar.
- Un prefab con
- En el mismo GameObject (o cualquier otro), agrega
LeitmotivGenerator+GalleryDemo, y asigna el generador enGalleryDemo. - Dale Play: deberías ver avatares en fila dentro del primer plano, moverte con A/D o flechas izq/der (acotado a ese plano), y saltar a los siguientes planos con flecha arriba (y volver con flecha abajo). Al hacer clic sobre un avatar se reproduce su leitmotiv (si asignaste el
SimpleSynthRenderer).
AvatarInfoCardController (UI/) — pop-up en el mundo (Canvas World Space) que aparece encima del avatar seleccionado y se oculta sola después de unos segundos. Se mueve directamente en coordenadas del mundo — sin conversiones de pantalla, así que no depende de cómo esté configurada la cámara.
- Hierarchy →
UI → Canvas. Selecciónalo, y en el componenteCanvas, cambiaRender Modea World Space. - En su
Rect Transform:Width = 400,Height = 200. En suTransform:Scale X/Y/Z = 0.01(un Canvas World Space se mide en píxeles, hay que achicarlo para que se vea del tamaño correcto en el mundo 3D). - Dentro del Canvas:
UI → Image(el fondo de la ficha — ponle un color sólido para que se lea bien encima del mundo). Dentro de esa Image: otraUI → Image(para la foto del avatar) y unUI → Text - TextMeshPro(para el nombre). - Selecciona el Canvas (no un hijo) y agrégale
Add Component → Avatar Info Card Controller. - Asigna:
Panel= el Image de fondo del paso 3,Avatar Image,Name Text. - Desactiva el GameObject del Canvas en el editor (el script lo activa solo).
- Dale Play, selecciona un avatar — la ficha debería aparecer justo encima, girada hacia la cámara, y ocultarse sola a los 3 segundos.
Si el texto se ve muy grande/pequeño o mal escalado, ajusta el tamaño de fuente del TMP_Text directamente (es normal tener que afinarlo una vez, por el Scale 0.01 del Canvas).
mainprotegida.- Una rama por tarea/feature:
feature/<nombre-corto>, ejemplofeature/leitmotiv-mapping. - Pull request antes de mergear a
main.
Este repo usa Git LFS para binarios pesados (audio, texturas, modelos). Tras clonar, correr una vez:
git lfs install