< Volver 

Rod and Reef: Minijuego de pesca

2026-09-26

Cada pez de Rod and Reef se pelea en un dial donde una aguja y un objetivo giran en sentidos opuestos. La dificultad sale de la caña contra la rareza y el tamaño del pez, la puntuación es un tira y afloja, y los efectos se dibujan con Spray, mi librería de partículas para GUI.

LuauRobloxGameplayUIVFX
Ir a vista rápida

Dos agujas que se encuentran de frente.

Advertencia

Rod and Reef todavía no ha sido lanzado, y no se pueden mostrar videos ni GIFs de este sistema hasta que salga.

Resumen. En Rod and Reef, cuando un pez pica empieza un minijuego: un dial circular donde una aguja y una caja objetivo giran en sentidos opuestos, y el jugador hace clic cuando se alinean. Lo que parece un minijuego simple tiene bastante geometría y algo de probabilidad detrás. La dificultad es una función pura de la caña y del pez: la caña controla la velocidad de la aguja, la rareza y el tamaño del pez controlan la de la caja, y de la velocidad con la que se acercan salen la distancia a la que aparece la caja y el ancho de la ventana de acierto, con un piso en segundos para que siga siendo jugable. La caja siempre viene de frente, la distancia entre las dos se mide con aritmética modular y una pasada sin clic cuenta como fallo. La puntuación es un tira y afloja, y modelada como una cadena de Markov resulta más sensible a la habilidad que una carrera simple. El motor del dial está separado de las reglas de cada minijuego que lo usa, y sus efectos se dibujan con Spray, mi librería de partículas para interfaces.

1. El problema

El dial se usa en dos juegos distintos. Uno es la pelea con el pez, que se gana con cierta cantidad de aciertos y se pierde con la misma cantidad de fallos. El otro es el desafío amistoso entre dos jugadores, que acumula puntos y sube de dificultad por rondas. Al principio los dos compartían el marco visual (la aguja, los arcos, las casillas, los emisores de partículas) y nada más, y todo lo que hacía que ese marco se comportara como un dial estaba escrito dos veces. Las dos copias no coincidían en lo que dos copias nunca coinciden. El desafío nunca escondía su arco al terminar, así que la siguiente pelea con un pez legendario, que usa un arco más angosto, lo dibujaba encima del ancho que seguía visible, y el jugador veía dos arcos de dos anchos, uno de los cuales no puntuaba nada. Ese era el reporte del “círculo duplicado”.

Además, la dificultad tenía que salir de la caña contra el pez de una forma que se entendiera, y tenía que ser justa para jugadores jóvenes, que son buena parte del público de Roblox.

2. Arquitectura

El código quedó en cuatro piezas:

  • CircularDial, el motor: los dos elementos que giran, la prueba de acierto, las casillas, la barra de progreso, qué arco se ve y toda la retroalimentación. No sabe cuántos aciertos ganan ni qué vale un acierto.
  • CircularMinigame, las reglas de la pelea con el pez.
  • ChallengeMinigame, las reglas del desafío entre jugadores.
  • BiteDifficulty, una función pura que convierte la caña y el pez en los números de una ronda.

El motor recibe una configuración de números ya resueltos y tres funciones: OnHit(precisión), OnMiss() y OnTimeout(). Quien juega en el dial decide qué significa cada cosa. Esa separación es la que hizo imposible el bug del círculo duplicado: el motor es el único que muestra arcos, y mostrar uno esconde todos los demás.

3. Geometría del dial

Los ángulos están en grados. Envolver un ángulo al intervalo [−180,180) se hace con módulo,

wrap(θ)=((θ+180)mod360)−180

y no sumando o restando 360 una vez, porque eso solo corrige una vuelta, y un cuadro largo puede llevar un elemento rápido más allá de 540 grados. La distancia angular entre dos ángulos es

Δ(a,b)=min⁡(|a−b|mod360, 360−|a−b|mod360)

Cada elemento lleva una velocidad con signo en vez de una rapidez y una bandera de dirección aparte. Así invertir el giro es negar un número, y no hay forma de que las dos mitades de “hacia dónde y qué tan rápido” queden en desacuerdo. Para animar un elemento hasta un ángulo se usa el equivalente de ese ángulo al que se llega sin cruzar la costura de ±180, porque la rotación es un número plano y una animación directa hacia un ángulo envuelto daría la vuelta larga.

4. El modelo de dificultad

4.1 La caña mueve la aguja

Cada caña tiene un valor de control C entre 10 (la inicial) y 100 (las secretas). Normalizado,

σ=clamp(C−1090, 0, 1),vn=110+(60−110)σ

en grados por segundo. Una caña débil es difícil de controlar y su aguja es rápida, una fuerte es fácil y su aguja es lenta, casi la mitad de rápida. Esa es toda la contribución de la caña a la velocidad.

4.2 El pez mueve la caja

La rareza del pez es un nivel τf de 1 (común) a 7 (secreto). La caña también tiene un nivel τc en esa misma escala, leído de su control por interpolación lineal en una tabla (control 10 es nivel 1, 45 es 2.75, 70 es 4, 80 es 5 y 100 es 6). Leerlo del control y no del nombre de la caña tiene una ventaja: las cañas de pago, que por equilibrio tiran como una caña rara, quedan en el nivel de una caña rara sin ningún caso especial. La velocidad de la caja es

vb=55⋅(1+0.25max⁡(0, τf−τc)−0.05min⁡(3, max⁡(0, τc−τf)))⋅κ

Cada nivel que el pez está por encima de la caña acelera la caja un 25%, en línea recta, para que “un nivel por encima” signifique lo mismo en cualquier parte de la escala: una caña común contra un pez épico está tres niveles por debajo y la caja va un 75% más rápido. Cada nivel por debajo la frena un 5%, con un máximo de tres. Es poco, porque la aguja lenta de una caña fuerte ya es la recompensa, y solo evita que una caña legendaria se sienta igual contra un común que contra un legendario. El factor κ es el tamaño: 1.05 para un pez grande, 1.10 para uno gigante y 1.16 para uno de tamaño legendario. El tamaño condimenta, pero no es el plato principal. Por último, vb≤225−vn, un riel que solo toca una caña baja contra un pez legendario grande y que existe para que una rareza futura o una errata nunca produzcan un dial que nadie puede jugar.

4.3 La ventana

Lo que el jugador cronometra es la velocidad a la que la aguja y la caja se acercan, vc=vn+vb. La ventana de acierto tiene un ancho w en grados, y el tiempo que la aguja pasa dentro de ella es tw=w/vc. El ancho sale del arte del arco, se ensancha con la caña, tiene un piso en tiempo y un techo en el propio arte:

w=min⁡(max⁡(a(1+0.35σ), 0.15vc), ag)

Hay dos arcos. El ancho mide a=24∘ y se puede agrandar hasta g=1.35 veces, y el angosto, que se usa con peces legendarios o mejores, mide a=11∘ y se puede agrandar hasta g=1.8 veces. El angosto tiene más margen porque lo necesita: a 150 grados por segundo, 11 grados de arte son una ventana de 0.07 s, que es tirar una moneda y no un reflejo.

El piso de 0.15vc garantiza que la aguja pase al menos 0.15 s dentro de la ventana. Sincronizar un clic con un objetivo que se ve venir es una tarea de anticipación, y el diseño supone que el error de un jugador joven anda en unos ±0.07 s. Una ventana de 0.15 s recoge la mayoría de sus clics, y una mucho más angosta deja de premiar la habilidad y empieza a premiar la suerte. Cuando el techo del arte obliga, el piso cede, y eso es lo que hace que una caña inicial contra un pez legendario siga siendo difícil de verdad.

4.4 La distancia

Cada vez que la caja aparece, lo hace a una distancia γ por delante de la aguja. Esa distancia sale de cuánto tiempo de preparación merece la ronda:

P=0.85+(1.45−0.85)clamp(vc−11080, 0, 1),γ=clamp(vcP, 95, 270)

Una ronda rápida recibe más tiempo sobre un arco mucho más grande, y una lenta recibe menos tiempo sobre un arco chico, así que una ronda tranquila también es una ronda corta y cinco aciertos nunca se vuelven una tarea. El piso de 95 grados mantiene visible el acercamiento en la ronda más lenta, y el techo de 270 evita que la caja aparezca justo detrás de la aguja, donde se leería como una pasada que acaba de ocurrir.

Con las cañas y los peces del juego, la función da esto:

Caña Pez vn vb vc w tw γ
Inicial Común 110 55 165 24.8° 0.15 s 208°
Inicial Épico 110 96 206 30.9° 0.15 s 270°
Rara 1 Raro 91 58 149 27.3° 0.18 s 170°
Épica Épico 77 55 132 29.6° 0.22 s 133°
Legendaria Legendario gigante 71 61 132 19.7° 0.15 s 133°
Secreta Mítico 60 55 115 17.3° 0.15 s 102°
Inicial Legendario de tamaño legendario 110 115 225 19.8° 0.09 s 270°

La última fila es la única donde el techo del arte vence al piso de tiempo: 0.09 s dentro de la ventana, con la caja apareciendo casi detrás de la aguja.

5. El acercamiento

5.1 Siempre de frente

La pelea con el pez usa un modo del motor que llamo de acercamiento. La aguja y la caja siempre giran en sentidos opuestos, así que siempre se están cerrando una sobre la otra por un lado del dial. La ronda abre con la caja a una distancia γ por delante. Un acierto invierte el giro de las dos, deja la aguja donde está y desliza la caja de nuevo a una distancia γ por delante, en la nueva dirección. Una pasada sin clic cuenta como fallo y vuelve a poner la caja, sin invertir, porque invertir es lo que gana un acierto. Así, la siguiente alineación siempre está a un acercamiento de distancia y nunca a una vuelta entera.

5.2 Medir la distancia

La distancia de la caja a la aguja, medida en la dirección en que avanza la aguja, es

D=((θb−θn)s+B)mod360−B,B=70

donde s=±1 es la dirección de giro. D es positiva mientras se acercan, cero en la alineación y negativa cuando la aguja ya pasó. El módulo deja D en [−70,290): “280 adelante” y “80 atrás” son el mismo par de ángulos, y algo tiene que decidir cuál de los dos significa. Hasta 70 grados atrás se lee como atrás y el resto como adelante, y eso solo es inequívoco porque ninguna distancia de aparición pasa de 280 grados. Los 10 grados que quedan entre los dos números son margen para un deslizamiento que llega un cuadro tarde.

Cada aparición varía la distancia un ±10% al azar y la recorta entre w+12 (para que una aparición nunca sea un acierto gratis sobre la alineación que se acaba de puntuar) y 280. La velocidad de la caja también varía, hasta un 8% más rápida en cada acercamiento, y se mantiene constante durante ese acercamiento, para que el jugador mire al pez y no cuente compases.

5.3 Deslizar hacia donde va a estar la aguja

Mientras la caja se desliza a su nueva posición, que tarda 0.22 s, la aguja sigue girando. Si el deslizamiento apuntara a la distancia sorteada medida desde donde está la aguja ahora, la distancia que el jugador ve al terminar sería menor. Por eso el destino es

Ddestino=γ′+|vn|⋅0.22

y la distancia que el jugador ve es la que se sorteó.

El sentido del deslizamiento también importa. Desde encima de la aguja, después de un acierto, el camino corto siempre es el correcto, porque cualquiera de los dos lados se aleja de ella. Desde atrás de la aguja, después de una pasada, la caja sigue hacia atrás aunque tenga que dar la vuelta larga. El camino corto la arrastraría por encima de la aguja, que se leería como un acierto que no contó, y justo en el momento en que el jugador está haciendo clic.

5.4 Pasadas y clics

Una pasada se detecta cuando la aguja supera el borde lejano de la ventana por más de un margen de gracia de 0.06 s:

D<−(w2+vc⋅0.06)

o cuando un cuadro tan largo hizo que la aguja cruzara el pliegue, lo que se ve como una distancia que pasa de menos de 90 a más de 180 de un cuadro al siguiente. Un clic tardío dentro de la gracia se puntúa como fallo y termina la pasada ahí mismo, así que un clic tarde cuesta un fallo y nunca el clic más la pasada.

Después de un acierto, el motor se desarma y no se vuelve a armar hasta que los dos elementos estén separados por un ancho de ventana completo, no por medio, y hasta que la caja haya terminado de deslizarse. Sin eso, la alineación que el jugador acaba de puntuar seguiría siendo una alineación en el cuadro siguiente, y hacer clic muchas veces durante una sola pasada sumaría todos los aciertos de una vez. Los fallos, en cambio, nunca se bloquean. Cada uno ya le cuesta algo al jugador, así que hacer clic sin control sigue castigado.

5.5 El otro modo

El desafío amistoso usa un segundo modo, de dispersión, donde después de cada acierto tres diales independientes deciden si la pareja invierte el giro, si uno de los dos elementos se detiene o arranca, y si uno de los dos salta a otro lugar del dial. Al principio era una sola tirada que elegía la forma completa entre seis combinaciones, incluida la que ya tenía, así que una probabilidad de inversión de 1 significaba “siempre vuelve a tirar” y no “siempre invierte”. Con tres diales, cada uno hace exactamente lo que dice su nombre. Un estado nunca puede alcanzarse: que los dos elementos giren en el mismo sentido. Dos elementos girando igual mantienen su separación para siempre y la ronda deja de poder ganarse. Cuando uno de los dos está detenido, el que se mueve lleva toda la velocidad de cierre, así que una ronda con un elemento quieto es exactamente igual de difícil que una con los dos girando.

6. Puntuación

6.1 Precisión

Cada acierto se califica por su distancia angular al centro, relativa a media ventana h=w/2: excelente si Δ≤0.1h, bueno hasta 0.4h, aceptable hasta 0.7h y malo hasta h. Más allá es fallo. Los niveles se recorren en orden, del más estricto al más permisivo, porque cada distancia que califica para uno también califica para todos los más permisivos y la respuesta es el primero que coincide. Antes se copiaban a un diccionario indexado por nombre y se recorrían con un for genérico, que en Luau no tiene un orden definido, así que un clic perfecto en el centro salía “malo” más o menos tan seguido como “excelente”. Los umbrales siempre estuvieron bien, y lo único que fallaba era el recorrido.

6.2 Tira y afloja

Los peces comunes se ganan con 3 aciertos y todos los demás con 5, y la misma cantidad de fallos pierde. Pero un acierto primero quita un fallo y después suma un acierto, y un fallo primero quita un acierto y después suma un fallo. Las casillas se ven como [rojas][verdes][vacías], y la ronda es un tira y afloja y no una carrera.

Si cada intento sale bien con probabilidad q, independiente de los demás, la ronda es una cadena de Markov absorbente [2] sobre los pares (aciertos, fallos). Con un objetivo de A, los estados transitorios son los pares (h,m) con h+m≤A−1, que son 15 para A=5. Desde (h,m), un acierto lleva a (h+1, max⁡(0,m−1)) y un fallo a (max⁡(0,h−1), m+1). Resolviendo el sistema se obtiene la probabilidad de ganar, y se puede comparar con una carrera simple a A aciertos contra A fallos, donde nada se cancela:

q Tira y afloja, A=5 Carrera, A=5 Intentos esperados
0.3 0.036 0.099 10.3
0.4 0.181 0.267 13.4
0.5 0.500 0.500 15.0
0.6 0.819 0.733 13.4
0.7 0.964 0.901 10.3
0.8 0.997 0.980 7.8

La curva del tira y afloja es más empinada. Un jugador que acierta el 70% de las veces gana el 96% de las peleas en vez del 90%, y uno que acierta el 40% gana el 18% en vez del 27%. La pelea premia más la habilidad y castiga más la falta de ella, y un error se puede recuperar, porque el siguiente acierto lo borra. El costo es la duración: con q=0.5, una pelea de 5 dura en promedio 15 intentos, contra un máximo de 9 en la carrera. Por eso los peces comunes, que son la mayoría de los lances, piden solo 3.

7. El frontend

7.1 Partículas con Spray

Las partículas de cada acierto son emisores de partículas normales de Roblox, creados en el panel de propiedades dentro de la interfaz. Roblox solo dibuja partículas en el mundo 3D, y para mostrarlas en la interfaz uso Spray, una librería que escribí y que el juego incluye como dependencia propia. Spray lee el emisor y lo reproduce con ImageLabels, calculando cada partícula como una función cerrada del reloj del efecto. Cómo funciona por dentro está en su propio artículo. Aquí importan tres decisiones de integración.

Todos los efectos se construyen una vez al cargar el módulo y no en cada ronda, porque armar el conjunto de etiquetas es la parte cara, y hacerlo en el primer acierto de un lance es justo cuando un tirón se nota más. También por eso se construyen en el motor y no en cada minijuego: dos conjuntos dibujando los mismos emisores duplicarían cada explosión en pantalla y pagarían dos veces el costo.

Todo el efecto vive en un contenedor que se dibuja detrás del dial, y eso le da la forma. La cara del dial queda limpia y legible, y lo que se ve es el halo desbordando el borde y las chispas saliendo hacia afuera. Que quede detrás depende de en qué contenedor vive el emisor y no de su índice Z, porque con ZIndexBehavior.Sibling una partícula nunca supera a los hermanos de su padre por alto que sea su propio índice.

Cada acierto emite una cantidad de partículas según su precisión (30 para excelente, 15 para bueno, 3 para aceptable y 2 para malo), salvo las capas que son un solo destello o una onda, que declaran su propia cantidad con un atributo. Al terminar la ronda, los efectos siguen corriendo 1.2 s para que las chispas del último acierto se apaguen solas en vez de congelarse en el aire, y después se pausan para no recalcular una línea de tiempo que nadie está mirando.

7.2 Retroalimentación

Un acierto toca uno de dos sonidos al azar, hace vibrar el control 0.1 s, emite partículas, sacude un poco la pantalla y la tiñe del color de su precisión. Un fallo tiene su propio sonido, sacude el dial, lo tiñe de rojo y sacude más la pantalla. La vibración es solo para los aciertos. El fallo ya tiene sacudida, destello y sonido, y una vibración encima haría que fallar fuera el evento más ruidoso de los dos. Es lo que Swink llama la sensación del juego [1]: buena parte de que un clic se sienta bien o mal no está en la regla, sino en lo que pasa en la pantalla durante la décima de segundo siguiente.

La barra de progreso es un degradado de transparencia con dos puntos de control muy juntos, en f y f+0.001, que dibujan un borde nítido. Para animarla se anima un NumberValue y se reconstruye el degradado cada vez que cambia.

7.3 Detalles

El tutorial congela la primera mordida. La aguja se detiene, el tiempo límite se detiene, el arco objetivo late en amarillo, y el clic que libera la pausa se consume en vez de puntuarse, porque es el jugador diciendo “entendí” y no un intento. Una función WouldHit() responde, sin tocar nada, si un clic ahora mismo acertaría, y la usa la fila de pistas contextuales para decirle al jugador cuándo hacer clic sin que el dial sepa que las pistas existen. Y la función que abre una ronda se llama OpenRound y nunca Start, porque el arranque del cliente llama Start en cada módulo que lo tenga, y bajo ese nombre cada arranque abría una ronda con la tabla del módulo como configuración.

8. Limitaciones

El modelo de Markov supone intentos independientes con una probabilidad fija, y un jugador real mejora durante la pelea y además puede fallar varias veces en un mismo acercamiento haciendo clic antes de tiempo. La cifra de 0.07 s para el error de anticipación de un jugador joven es una suposición de diseño, no algo que medí. Y los números del modelo de dificultad están afinados a mano, así que una caña o una rareza nueva necesita revisar la tabla de la sección 4.4.

9. Lo que me dejó

Separar el motor de las reglas resolvió bugs que no eran de lógica sino de duplicación, y también hizo posible escribir la dificultad como una función pura que se puede probar sin abrir el juego, como hice para la tabla de este artículo. La otra lección vino del tira y afloja: una regla de puntuación que parece un detalle cambia mucho la forma de la curva de victoria, y vale la pena calcularla antes de decidir cuántos aciertos pedir.

Referencias

  1. S. Swink. Game Feel: A Game Designer’s Guide to Virtual Sensation. Morgan Kaufmann, 2009.
  2. J. G. Kemeny y J. L. Snell. Finite Markov Chains. Van Nostrand, 1960.

Este texto de Mastedore. Todos los derechos reservados.