Sistema de Puntuación Multi-Métrica
Un modelo de puntuación dinámico que rastrea cualquier número de métricas nombradas a través de un único servicio, devolviendo instantáneas inmutables que las reglas de juego nunca necesitan poseer.
Para qué sirve este sistema
La mayoría de los juegos necesitan más de un número para describir el rendimiento del jugador. Un shooter rastrea kills, precisión y tiempo. Un plataformas rastrea monedas, vidas perdidas y mejor tiempo por vuelta. Un juego de ritmo rastrea notas acertadas, combo y una puntuación final ponderada. Cada proyecto reinventa el mismo cableado: un manager estático, un montón de floats públicos y un HUD que los lee directamente.
El agregado Score de Serenity reemplaza ese cableado con un servicio propio. Las métricas se registran por nombre al inicio y se mutan mediante operaciones tipadas. El servicio nunca sabe lo que kills o precisión significan para el juego — solo sabe cómo sumar, restar, multiplicar, dividir, asignar y resetear un valor nombrado, y devolver el resultado como una instantánea inmutable.
El problema en Unity
Los singletons de puntuación se rompen en cuanto un juego necesita dos contextos de puntuación — una puntuación por nivel y una puntuación total de la partida, por ejemplo — o cuando una característica como los multiplicadores de combo necesita aplicarse en varias métricas a la vez. Los floats públicos en un MonoBehaviour hacen que los HUDs escriban directamente en el estado de juego, lo que convierte cualquier refactor en una búsqueda y reemplazo manual por cada escena y cada script que alguna vez llamó a `ScoreManager.Instance.kills++`.
La persistencia es el otro punto de fallo. Guardar una puntuación máxima implica acceder a la forma que tenga el singleton en ese momento. No hay contrato estable, no hay un momento claro de captura y no hay manera de comparar dos sesiones sin duplicar la lógica de lectura en la pantalla de guardado y en la de clasificación.
Cómo lo aborda Serenity
Serenity expone la puntuación a través de IScoreService. Cada métrica se registra con una ScoreKey — un identificador de cadena inmutable — y un ScoreMetricKind que describe cómo debe interpretarse el valor: Integer, Float, Percentage o Time. Una vez registrada, la métrica puede modificarse mediante Add, Subtract, Multiply, Divide, Set o ResetKey, y todo el modelo puede limpiarse con ResetAll. Ninguna operación devuelve nada; el servicio posee el estado.
El contrato de instantánea es el punto de diseño central. Llamar a GetSnapshot devuelve un ScoreSnapshot: un struct inmutable que contiene un diccionario de solo lectura con cada ScoreValue actual indexado por ScoreKey. Los HUDs, las pantallas de clasificación y la capa de persistencia reciben la misma instantánea. ScoreRecord captura el mejor, peor, promedio y número de sesiones de una métrica a lo largo del tiempo, con IScoreRepository como puerto de persistencia para que la estrategia de almacenamiento nunca filtre al dominio.
Las métricas ya no tienen que registrarse desde código. Un asset UnityScoreSettings lista filas Key/Kind a través del asistente Score Settings (Tools ▸ Serenity ▸ Create ▸ Score ▸ Score Settings), y la tarea InstallScore de la Pipeline de Inicialización registra cada fila declarada en el arranque — de forma aditiva a, nunca en sustitución de, las llamadas a IScoreService.Register de tu propio código. Score arranca ahora como parte del pipeline estándar en lugar de existir solo cuando un test lo construye directamente.
Dos piezas cierran el hueco entre una Transform en movimiento y una partida puntuada. UnityTransformDistanceScoreMeter alimenta una métrica declarada con la distancia recorrida por una Transform — una máscara por eje decide qué cuenta, Score Per Unit escala la recompensa, y una salvaguarda Max Delta Per Frame vuelve a sembrar el origen en lugar de pagar un teletransporte o un respawn. AddScoreSignal define un par Key/Delta como señal entrante sin código, de modo que cualquier disparador tipo SignalEmitterComponent puede otorgar una cantidad fija a una métrica sin una línea de glue de gameplay.
Score y Clasificaciones se encuentran en una garantía de orden de arranque y un componente. InstallScore siempre registra las métricas antes de que corra InstallLeaderboard, así que una clasificación nunca pide una ScoreKey que aún no existe, y UnityLeaderboardScoreSubmitter envuelve el flujo de enviar la puntuación actual en una única llamada SubmitNow() sin parámetros — sin construir un UnityLeaderboardScoreBridge a mano desde el clic de un botón o un SignalReactionComponent.
Cómo encaja en Serenity
Score vive en el namespace Serenity.Score y sigue la misma estructura por capas que el resto de agregados de Serenity. La capa de Dominio define los value objects ScoreKey, ScoreValue y ScoreSnapshot, la entidad ScoreRuntime, el enum ScoreMetricKind y el tipo de registro ScoreRecord. La capa de Aplicación expone IScoreService y el puerto de persistencia IScoreRepository. La capa de Infraestructura proporciona UnityScoreService y UnityScoreRepository como implementaciones concretas. ScoreInstaller y UnityScoreInstaller registran todo a través de la pipeline de inicialización.
Score coopera con el agregado Game Session, que dispara ResetAll al inicio de cada sesión y llama a GetSnapshot al final para que el resultado pueda persistirse a través de IScoreRepository. El Event Dispatcher lleva la instantánea a cualquier componente HUD que se haya suscrito a las señales de cambio de puntuación, de modo que el HUD nunca mantiene una referencia al servicio.
Combo no tiene página propia en la landing, así que su contrato vive aquí: IComboService (AddCombo, BreakCombo, GetSnapshot) rastrea un único contador de kills consecutivas de la sesión, y el glue que lo llama despacha ComboIncreasedSignal y ComboBrokenSignal. El servicio no aplica reglas propias sobre cuándo sube o se rompe el combo — ese criterio pertenece a la capa de juego que lo consume — pero el contador que produce es exactamente lo que alimenta un multiplicador de puntuación: el glue del juego suele llamar a Multiply en la ScoreKey correspondiente usando el combo actual, escalando los puntos que vale una kill sin que Score y Combo se acoplen directamente entre sí.
Flujo de trabajo práctico
- Define las ScoreKeys como constantes o una clase estática para que todos los sistemas referencien los mismos identificadores.
- Registra cada métrica al inicio de la sesión llamando a IScoreService.Register con el ScoreMetricKind correspondiente, o declara filas Key/Kind en un asset UnityScoreSettings y deja que InstallScore las registre en el arranque.
- Dispara Add, Subtract, Multiply, Divide o Set desde la lógica de juego en respuesta a eventos de gameplay — o coloca un UnityTransformDistanceScoreMeter en un objeto en movimiento, o conecta AddScoreSignal a un disparador, para puntuar sin código.
- Llama a IComboService.AddCombo() en una kill consecutiva y a BreakCombo() al recibir daño o expirar el tiempo, despachando ComboIncreasedSignal / ComboBrokenSignal junto a cada llamada para que el HUD y Score reaccionen.
- Deja que el glue del combo llame a Multiply en la ScoreKey objetivo usando CurrentCombo cuando haya un multiplicador de combo activo.
- Suscríbete al evento de cambio de puntuación a través del Event Dispatcher para actualizar el HUD sin hacer polling.
- Llama a GetSnapshot al final de una sesión o punto de control y pásalo a IScoreRepository.Save para persistirlo, o llama a UnityLeaderboardScoreSubmitter.SubmitNow() para guardar la instantánea y enviarla a una tabla en un solo paso.
Qué incluye
- Interfaz de servicio IScoreService con Add, Subtract, Multiply, Divide, Set, ResetKey y ResetAll
- Claves de métrica nombradas mediante el value object ScoreKey — cualquier identificador de cadena, inmutable y hashable
- Enum de tipo de métrica ScoreMetricKind: Integer, Float, Percentage y Time
- ScoreValue que combina un valor float con su tipo en un único struct inmutable
- ScoreSnapshot: diccionario inmutable de solo lectura de todas las métricas actuales, seguro para pasar entre sistemas
- ScoreRecord para persistencia: mejor, peor, promedio y número de sesiones por clave de métrica
- Puerto de persistencia IScoreRepository con Save, TryLoadLatest, LoadAll y Clear
- UnityScoreInstaller registra el servicio a través de la pipeline de inicialización estándar de Serenity, conectado mediante la tarea de pipeline InstallScore
- Métricas declarativas mediante UnityScoreSettings (filas Key/Kind) y el asistente Score Settings — sin llamadas a Register escritas a mano
- Componente de escena UnityTransformDistanceScoreMeter: máscara por eje, escalado Score Per Unit y salvaguarda de teletransporte/respawn para puntuación por distancia
- AddScoreSignal para otorgar puntos sin código, con delta constante, desde cualquier disparador que emita señales
- Agregado Combo: IComboService con AddCombo, BreakCombo y GetSnapshot; ComboIncreasedSignal y ComboBrokenSignal como superficie pública de eventos; el contador de combo suele alimentar un multiplicador de puntuación vía Multiply
Cuándo usarlo
- Juegos que rastrean dos o más dimensiones de puntuación independientes como kills, tiempo, precisión o combo.
- Proyectos que necesitan un contrato de instantánea estable para que HUDs, archivos de guardado y clasificaciones lean los mismos datos.
- Bases de código que quieren que el sistema de combo o sesión envíe multiplicadores a la puntuación sin acoplarse directamente a un singleton.
- Diseñadores que quieren puntuación por distancia o disparada por señales configurada en un componente o un asset en lugar de escrita en código.
- Cualquier proyecto que haya superado un score manager estático y necesite un servicio reemplazable y testeable detrás de una interfaz.
Sistemas relacionados
Utiliza Serenity cuando quieras que la puntuación sea un servicio de dominio propio — uno que habla en claves nombradas e instantáneas inmutables, permanece ajeno a las reglas de juego, arranca automáticamente junto al contador de combo que lo multiplica y se integra limpiamente con el ciclo de vida de la sesión, la clasificación y el HUD a través del Event Dispatcher.
English
Español
Català