Hace unos días estuve hablando con un posible cliente sobre la posibilidad de realizar un pequeño juego de preguntas y respuestas que sería usado en el marco de una exposición.
Al final dicho proyecto no prosperó, por que luego no volví a ser contactado, pero de todas formas me parecía una buena escusa para realizar un pequeño proyecto en Phaser Framework.
Pueden verlo andando en: https://demos.greenborn.com.ar/ruleta_html5/
y el repo en: https://github.com/Greenborn/ruleta_html5
En que consiste el juego
El juego se puede dividir en 3 vistas o escenas, en la primera contamos con una ruleta que se acciona con un botón, la cual al detenerse se selecciona una categoría luego de lo cual se pasa a la siguiente escena.
En donde se presenta una pregunta al azar correspondiente a la categoría seleccionada con 4 posibles respuestas, una vez seleccionada la respuesta, se pasa a la siguiente escena.
La cual muestra si la pregunta se contestó de forma correcta y el detalle de la respuesta, luego de lo cual se vuelve a la pantalla inicial.
La estructura del proyecto
La estructura básica de archivos y directorios que definí para el juego fue la siguiente:
- Assets
- audio
- bien1.mp3: Respuesta correcta
- click1.mp3: Sonido de ruleta.
- mal1.mp3: Respuesta incorrecta
- wclick1.mp3: Botón presionado
- imagen
- areapreg.svg: Fondo para texto de pregunta
- b1.svg – b4.svg: Botones para elegir opción.
- btn_sig.svg: Botón siguiente.
- btn_tirar.svg: Botón para girar ruleta.
- ruleta.svg: Imagen de la ruleta.
- select_ruleta.svg: Imagen del selector de categoría.
- js
- boton.js: Usado para definir el comportamiento de todos los botones.
- display.pregunta.js: Define el comportamiento del display de preguntas.
- elemento.juego.js: Código común a todos los elementos del juego.
- general.js: Usado para definición de funciones auxiliares, como la de conversión de ángulos.
- listado.preguntas.js: La lista de preguntas en si misma.
- pantalla.preguntas.js: Se define el comportamiento de la vista de pregunta.
- pantalla.respuestas.js: Define el comportamiento de la vista de respuesta.
- pantalla.ruleta.js: Define el comportamiento de la vista de la ruleta.
- principal.js: Crea la instancia general del juego, define las vistas y gestiona la precarga.
- puntuador.js: Por ahora no se usa, a futuro para manejar puntajes.
- ruleta.js: Define el comportamiento de la ruleta.
- audio
- index.html: Importa todos los scripts.
La ruleta
La ruleta consiste en una imagen circular, y al igual que el resto, se trata de una imagen vectorial.
Las categorías están definidas en un arreglo de configuración, de la siguiente forma:
categorias:[
{ id:0, color: '#dfdd48', a_i:0, a_f:0, nombre: 'Mitología' },
{ id:1, color: '#7d03ff', a_i:0, a_f:0, nombre: 'Deportes' },
{ id:2, color: '#ff8203', a_i:0, a_f:0, nombre: 'Gastronomía' },
{ id:3, color: '#36dc22', a_i:0, a_f:0, nombre: 'Música' },
{ id:4, color: '#FFFFFF', a_i:0, a_f:0, nombre: 'Ciencia' },
{ id:5, color: '#0bace8', a_i:0, a_f:0, nombre: 'Política' },
{ id:6, color: '#bf32b7', a_i:0, a_f:0, nombre: 'Cine' },
],
Estando ordenadas de forma antihoraria con respecto a como se definen en la imagen, el parámetro color e id los agregue pensando a futuro, ya que por el momento están hardcodeadas, pero si a futuro hago un backend en donde puedan definirse las mismas, son parámetros necesarios.
La aleatoriedad
Para que la misma se detenga en cada tirada en una posición diferente, definí dos propiedades: aceleración y velocidad que toman valores aleatorios con cada tiro.
tirar(){
this.resultado_entregado = false;
this.aceleracion = - ((Math.random() * 3)+3)/30;
this.velocidad = Math.floor(Math.random() * 30)+15;
}
Efecto de giro y sonido
El efecto de giro por el cual comienza girando rápido y luego reduce su velocidad hasta detenerse, no es algo complicado, por ejemplo por cada frame se ejecuta el siguiente código:
update(){
this.phaserSprite.angle += this.velocidad;
this.velocidad += this.aceleracion;
if (this.velocidad < 0){
this.velocidad = 0;
if (!this.resultado_entregado){
this.resultado_entregado = true;
this.ultimo_resultado = this.getResultado();
this.ultima_pregunta = this.listado_preguntas.getPregunta( this.ultimo_resultado );
this.callback_resultado();
}
} else {
//Se hace el sonido de la ruleta
this.click_cnt = Math.round(this.phaserSprite.angle/this.intervalo_subdivision);
if (this.click_cnt != this.click_cnt_ant){
this.click_cnt_ant = this.click_cnt;
this.juego.sound.play('click_ruleta');
}
}
}
Obtención del resultado
El resultado de la categoría en la cual se detiene la ruleta se obtiene por medio del ángulo en la cual queda al detenerse, el único problema al que me enfrenté es que Phaser define el valor del ángulo de los sprites entre 0 y 180 y luego entre -180 y 0, cuando lo lógico ubiera sido que el mismo se defina entre 0 y 360.
Para suplirlo agregué un par de funciones que se encargan de convertir el valor de los ángulos:
function anguloComunAPhaser( angulo ){
if (angulo > 180){
return 360 - angulo;
}
return angulo;
}
function anguloPhaserAComun( angulo ){
if (angulo < 0){
return angulo + 360;
}
return angulo;
}
Para saber en que valor de ángulo comienza y finaliza la categoría, luego de crear la ruleta agregué un for que se encarga de definir los valores de los mismos en el arreglo de categorías:
this.intervalo_subdivision = 360/this.config.categorias.length;
//Se asignan los valores de angulos a las categorias
//Se usan angulos de 0 a 360 como seria lògico
for (let c=0; c < this.config.categorias.length; c++){
this.config.categorias[c].a_i = c*this.intervalo_subdivision;
this.config.categorias[c].a_f = (c+1)*this.intervalo_subdivision;
}
Por lo que una vez que ya están definidos dichos valores, la categoría se obtiene facilmente:
getResultado(){
//Se le suma 90º por que el selector esta arriba, no a la derecha de la ruleta
let pos = anguloPhaserAComun( this.phaserSprite.angle + 90 );
this.ultimo_resultado = null;
for (let c=0; c < this.config.categorias.length; c++){
if ( numeroEntre(pos, this.config.categorias[c].a_i, this.config.categorias[c].a_f) ){
return this.config.categorias[c];
}
}
return null;
}
Allí también llamo a una función que se encarga de verificar si el angulo obtenido se encuentra entre dos números sin importar si num1 es mayor a num2 o viceversa (inicialmente la definí así por la forma en la que Phaser define los ángulos):
function numeroEntre(n, num1, num2){
if (num2 < num1){
let aux = num2;
num2 = num1;
num1 = aux;
}
if (n >= num1 && n <= num2){
return true;
}
return false;
}
Obtención de la preguntas:
Las preguntas por el momento están definidas en un arreglo, también hardcodeadas como las categorías, aunque a futuro podría guardarlas en una base de datos y consultarlas vía API.
Creo que lo único interesante de las mismas es que para hacer que no se repitan lo más simple fue borrarlas del arreglo cada vez que saliera una nueva elegida.
Bueno eso es todo por ahora!