sábado, 24 de octubre de 2015

Crear juego RPG en C++ y Allegro 4 (24) Tiendas III

Aquí esta una nueva entrega del curso crea tu juego RPG en C++ y Allegro. Continuamos con las tiendas.




En la entrega anterior se puso la función pinta_tienda(), y se explicó un poco su funcionamiento. Ahora iremos explicando algunas partes del código.

               rect(buffer, x_tienda+470, y_tienda, x_tienda+490, y_tienda+372,0xffffff );
               rect(buffer, x_tienda+470, y_tienda+30, x_tienda+490, y_tienda+342,0xffffff );
               triangle( buffer, x_tienda+473,y_tienda+25, x_tienda+487,y_tienda+25,
                             x_tienda+480,y_tienda+5,0xffffff ); 
               triangle( buffer, x_tienda+473,y_tienda+347, x_tienda+487,y_tienda+347,
                             x_tienda+480,y_tienda+367,0xffffff ); 

Este código se encarga de dibujar la barra de desplazamiento, dibujando dos rectángulos y dos triángulos. El comando rect recibe una imagen que es en la que se va a dibujar el rectángulo, y dos posiciones para indicar las dimensiones del rectángulo dando la esquina superior izquierda, y la esquina inferior derecha. Y finalmente se indica el color en el que se pinta.
El comando triangle funciona de manera similar solo que ahora pide tres posiciones, las tres esquinas del triángulo.

               // distancia unidad
               float ny1 = ( 340.0 - 32.0 ) / vlista_tienda.size() ;
               // parte superior por donde se inicia
               int ny2 = int(inicia_objeto * ny1);
               // siempre se muestran 10 - longitud barra
               int ny3 = int(10.0 * ny1);

Estas variables contienen información para poder pintar la barra interior que va moviendose dentro de la barra de desplazamiento.
ny1: contiene el tamaño total de la barra de desplazamiento dividido por la cantidad de objetos que se van a mostrar.
ny2: contiene donde se va a situar la barra, según el elemento mostrado que se indica en inicia_objeto.
ny3: contiene el tamaño de la barra. Como en el espacio que tenemos solo se puede mostrar 10 objetos, por eso se multiplica por 10.

               if ( mouse_x > x_tienda+460 && mouse_x < x_tienda+490 &&
                    mouse_y > y_tienda  && mouse_y < y_tienda+30)
               {
                    // esta situado encima de boton
                    triangle( buffer, x_tienda+473,y_tienda+25, x_tienda+487,y_tienda+25,
                             x_tienda+480,y_tienda+5,0xffff00 ); 
                    if ( mouse_b&1 )
                    {
                         // pulsa hacia arriba
                         inicia_objeto--;
                         if (inicia_objeto < 0 ) inicia_objeto = 0;
                    }         
               }     
               
               if ( mouse_x > x_tienda+460 && mouse_x < x_tienda+490 &&
                    mouse_y > y_tienda+342  && mouse_y < y_tienda+372)
               {
                    // esta situado encima de boton
               triangle( buffer, x_tienda+473,y_tienda+347, x_tienda+487,y_tienda+347,
                             x_tienda+480,y_tienda+367,0xffff00 ); 
                    if ( mouse_b&1 )
                    {
                         // pulsa hacia abajo
                         inicia_objeto++;
                         if (inicia_objeto+10 > vlista_tienda.size() ) inicia_objeto--;
                    }         
               }    

Estas dos condiciones se encargan de controlar los dos botones de la barra de desplazamiento, la primera para el botón de arriba y la segunda para el de abajo. Se controla si esta encima de la imagen del botón y si se ha pulsado. Dependiendo de cual pulse se añade o se resta a la variable inicia_objeto, esta variable indica en que objeto se empieza a mostrar.

          for ( int it=inicia_objeto; it < vlista_tienda.size() && it < inicia_objeto+10; it++ )  
          {
              int ty = y_tienda+((it+1)*34) - (inicia_objeto*34);
              masked_blit( (BITMAP*)datobjetos[vlista_tienda[it].id].dat, buffer,
                           0,0,x_tienda,ty, 32,32);
              
              textprintf_ex( buffer, (FONT *)datosjuego[dftextos].dat, x_tienda+34, ty, 0xFFFFFF, -1, "%s", vlista_tienda[it].nombre );
              
              textprintf_ex( buffer, (FONT *)datosjuego[dftextos].dat, x_tienda+367, ty, 0xFFFFFF, -1, "%8d", vlista_tienda[it].precio );                                         


                int numi = it - inicia_objeto;
                                                  
                if ( mouse_x > x_tienda+1    && mouse_x < x_tienda+470 &&
                     mouse_y > y_tienda+32+(numi*34) && mouse_y < y_tienda+64+(numi*34))
                {
                     rect(buffer, x_tienda+2, y_tienda+34+(numi*34),
                                  x_tienda+468, y_tienda+63+(numi*34),0xffff00 );
                     
                     
                     tdesc.cambia_texto( descripcion_objeto(vlista_tienda[it].nid) );
                     tdesc.pinta(buffer);
                     textprintf_ex( buffer, (FONT *)datosjuego[dftextos].dat, x_tienda+274, y_tienda, 0xFFFFFF, -1, 
                                    "Tienes:%2d", jugador.cuantos_objetos(vlista_tienda[it].nid) );
                                      
                     if (  mouse_b&1 )
                     {
                           // intenta comprar objeto
                           // comprobar que existe hueco para la compra
                           // tiene dinero suficiente ?
                           if ( jugador.num_inventario() < 12 && 
                                jugador.getdinero() > vlista_tienda[it].precio )
                           {
                                sel_obj = it;
                           }else{
                              if ( swerror == 0 )
                              {    
                                 swerror = 1;
                              }
                           }      
                     }// fin se pulso el raton          
                }// fin dentro de un objeto               
              
          }

Con este bucle for se van mostrando los 10 posibles objetos que se pueden comprar. La variable ty contiene la posición absoluta donde se irán pintando cada una de las lineas. Con el comando masked_blit se pinta la imagen del objeto. Con los dos textprintf_ex se muestran el nombre y el precio del objeto. La variable numi se utiliza  para obtener un valor que vaya de 0 a 10.
La condición comprueba si el ratón esta situado encima de alguno de los objetos, en ese caso se pinta un rectángulo que rodea al objeto y muestra la descripción del objeto utilizando la variable tdesc.
En el caso de que se haga clic se comprueba que se tenga hueco en el inventario y dinero para comprarlo, en el caso que se pueda sel_obj toma el valor de it, que nos indica que objeto se a comprado.

Todo esto se repite para la venta de objetos que tiene el jugador en el inventario, cambiando la lista de objetos que se mira, ahora se comprueba todos los objetos que se tiene en el inventario.

          if ( comprar.ratonb() && tienda_estado != 1)
          {
               tienda_estado = 1;
               inicia_objeto = 0;
               sel_obj = -1;
               sonido_boton();
          }
          
          if ( vender.ratonb() && tienda_estado != 2)
          {
               tienda_estado = 2;
               inicia_objeto = 0;
               sel_obj = -1; 
               sonido_boton();              
          }
                    
          if ( salir.ratonb() ){
                // oculta la flecha y sale
                swtienda = 0;                       
                muestra_tienda = false;  
                sonido_boton();
          } 

Estas tres condiciones, se encarga de comprobar si se pulsa alguno de los botones, y realiza la acción correspondiente, ya sea cambiar a la ventana de compra, a la de venta o salir de la tienda. Espero que con esto haya quedado un poco mas claro en funcionamiento de la función.

Seguimos con los cambios o añadidos que se deben hacer al código para tener operativo la tienda.

En el archivo audio.h, se añade un nuevo sonido.

void sonido_error(){
    play_sample ( (SAMPLE *)datosjuego[dserror].dat, 110,128, 1000, 0 );   
}

En el archivo global.h se declara la nueva variable datotiendas.

DATAFILE *datotiendas;

int swtienda;
int sel_obj; 
int swerror;

En el archivo mijuego.h. se quita la declaración del jugador, y se pone en el archivo players.h, después de la definición de la clase.

player jugador;

En la función carga_juego(), se añade  lo siguiente, justo después de cargar los otros archivos DAT.

    datotiendas = load_datafile("datostienda.dat");
    if ( !datobjetos ){
       allegro_message("Error: archivo datostienda.dat no encontrado\n%s\n", allegro_error);       
    }

Y se modifica la cantidad de NPC, y enemigos, y se inicializan las nuevas variables.

    cambio = 0;
    npersonaje = 10;
    
    personajes[0].crea( (BITMAP *)datosjuego[diper001].dat, 1300,700, 1,1,3); 
    personajes[1].crea( (BITMAP *)datosjuego[diper005].dat, 280, 450, 0,2,3);
    personajes[2].crea( (BITMAP *)datosjuego[diper005].dat, 230, 280, 3,2,3);
    personajes[3].crea( (BITMAP *)datosjuego[diper003].dat, 960, 310, 2,3,3);
    personajes[4].crea( (BITMAP *)datosjuego[diper005].dat, 1120, 450, 0,4,3);
    personajes[5].crea( (BITMAP *)datosjuego[diper004].dat, 900, 650, 1,5,3);
    personajes[6].crea( (BITMAP *)datosjuego[diper006].dat, 850, 800, 0,0,3);
    personajes[7].crea( (BITMAP *)datosjuego[diper001].dat, 530, 280, 1,5,3);
    personajes[8].crea( (BITMAP *)datosjuego[diper007].dat, 334, 170, 0,0,4);
    personajes[9].crea( (BITMAP *)datosjuego[diper008].dat, 142, 170, 0,0,4);
    
    nmalos = 3;
    malos[0].crea( (BITMAP *)datosjuego[diene001].dat, 380, 280, 3,5,2,100);
    malos[1].crea( (BITMAP *)datosjuego[diene001].dat, 400, 720, 0,5,2,100);
    malos[2].crea( (BITMAP *)datosjuego[diene001].dat, 380, 240, 0,5,2,100);

    texto.crea("Demo tiendas. Ejemplo del Curso Crea tu juego RPG en C++ y Allegro ",
       font, 5,5,230,60 );

    dialogo.crea("", (FONT *)datosjuego[dftextos].dat, 10, PANTALLA_ALTO-100, PANTALLA_ANCHO-10, PANTALLA_ALTO-10);  
    hablando = 0; 
    
    mision = 1;
    swraton=-1;
       
    swinv=0;
    muestra_tienda = false;
    swtienda=0;

Este código sustituye al anterior que va desde la inicialización a cero de cambio hasta el final de la función.

En la función carga_escenario(), se añade el nuevo escenario de la tienda.

    case 4:// tienda1
              fondo  = (BITMAP *)datosjuego[ditienda1].dat;
              choque = (BITMAP *)datosjuego[ditienda1choque].dat;
              cielo  = (BITMAP *)datosjuego[ditienda1sup].dat;  
              
              desplaza=false;
              sonido_abrirpuerta(); 
         break;   

Se añade un nuevo case, para el escenario de la tienda. Para indicar las imagenes del nuevo escenario.

En la función cambia_escenario(), se sustituye el código a partir del case 3, hasta el final de la función, incluida el cierre de la llave ( } ).

    case 3:   // ciudad
         if ( cambio == 1 )
         {
              // cambiamos a otro lugar
              // bosque             
              lugar = 2;
              carga_escenario();     
              // situamos al prota en el camino del bosque        
              jugador.posiciona( 650,30 ); 
              desplazamiento_map_x=200;
              desplazamiento_map_y=0; 
              cambio = 0;
         }
         // color amarillo que existen muchos
         if ( cambio == 3 && desplazamiento_map_x > 800 )
         {
              // cambiamos a otro lugar
              // tienda1            
              lugar = 4;
              carga_escenario();     
              // situamos al prota en el camino del bosque        
              jugador.posiciona( 376,460 ); 
              desplazamiento_map_x=-170;
              desplazamiento_map_y=-100; 
  
              cambio = 0;
         }         
         break;   
    case 4:   // tienda1
         if ( cambio == 1)
         {
              // cambiamos a la ciudad
              lugar=3;
              carga_escenario();
              jugador.posiciona( 400,300 );
              desplazamiento_map_x=1090;
              desplazamiento_map_y=85; 
              cambio = 0;    
              sonido_abrirpuerta();         
         }    
              
                
    default:
         break;
    }       
}

Estos cambios, son para que desde la ciudad se pueda ir a la tienda el nuevo escenario, y desde la tienda se pueda volver a la ciudad. Todas las posiciones del personaje y desplazamiento del mapa depende del mapa, y solo son válido para nuestros mapas.
Recuerda que si deseas añadir nuevos mapas, estos datos de posición del personaje y desplazamiento debe calcularlo uno mismo según las imagenes de los escenarios.

En la función evento_escenario(), se añade un nuevo case, para controlar los eventos que hay dentro de la tienda.

    case 4: // en la tienda
    
         if ( personajes[8].getestado() == 6 && cambio == 0 && !personajes[8].posicion_cerca())
         {
              personajes[8].cambia_estado(0);
         } 
         if ( personajes[9].getestado() == 6 && cambio == 0 && !personajes[9].posicion_cerca())
         {
              personajes[9].cambia_estado(0);
         }            
    
         if ( cambio == 2 && jugador.getx()< 400 )
         {
              personajes[9].cambia_estado(6); 
              cambio = 0;
         }     
         if ( cambio == 2 && jugador.getx()> 400 )
         {
              personajes[8].cambia_estado(6); 
              cambio = 0;
         }     
         
         if ( personajes[8].posicion_cerca() )
         {
              personajes[8].cambia_estado(6);
         }
         
         if ( personajes[9].posicion_cerca() )
         {
              personajes[9].cambia_estado(6);
         }   
         
         if ( personajes[8].posicion_cerca(9) &&  personajes[8].alineado_vertical() 
              && jugador.accion() && !personajes[8].posicion_cerca(2) )
         {
              lee_tienda(2);
              muestra_tienda = true;
         }     
         
         if ( personajes[9].posicion_cerca(9) &&  personajes[9].alineado_vertical() 
              && jugador.accion() && !personajes[9].posicion_cerca(2) )
         {
              lee_tienda(1);
              muestra_tienda = true;
         }           
             
         
         if (  personajes[8].frente() && jugador.accion() &&
               !jugador.hablando() && personajes[8].posicion_cerca())
         {
                  dialogo.cambia_texto(" Y tu que estas mirando!! " );
                  hablando = 1;                 
         }       
         
         if ( personajes[9].frente() && jugador.accion() && 
              !jugador.hablando() && personajes[9].posicion_cerca())
         {
                  dialogo.cambia_texto(" Vienes a comprar ? " );
                  hablando = 1;                 
         } 
         
         if ( hablando == 1 && !jugador.accion() )
         {
              hablando = 2;
              jugador.habla();
         }  
         
         // obliga a esperar minimo 1 segundo
         if ( hablando > FRAME_RATE && jugador.accion() ){
              hablando = 0;
         }
                     
         if ( hablando == 0 && !jugador.accion() && jugador.hablando() )
         {
              jugador.no_habla();
         }                        

         break;   

Esto de los eventos de los personajes, se puede mejorar pero por el momento lo dejamos así. En este código anterior se controla los dos personajes que hay en la tienda, si nos colocamos delante de ellos detrás del mostrador se abrirá la tienda, en el caso de que nos acerquemos a ellos nos dirán unas frases.

En la función pinta_juego(), al igual que en otras funciones se añade un nuevo case, para la tienda.

    case 4: // tienda1
             bx = 170;   
             by = 100;
             ancho = 448;
             alto = 416;      
             break;       

También se tiene que añadir la llamada a la función pinta_tienda(), justo después de pinta_inventario(), para que de este modo la tienda lo oculte todo.

En el archivo players.h, en la clase player se añade una nueva variable privada:

    int dinero;

Y se añaden nuevas funciones

       int getdinero(){ return dinero; };
       void masdinero(int n){ dinero = dinero + n; };
       void menosdinero(int n){ dinero = dinero - n; };       
       // devuelve cuantos objetos tienes en el inventario
       int num_inventario();
       int cuantos_objetos(int id);

La función player::cuantos_objetos(), se encarga de recorrer todo el vector inventario y devuelve cuantos objetos hay en el inventario con el mismo id.  Y la funcion player::num_inventario() devuelve la cantidad de objetos que hay en el inventario.

int player::cuantos_objetos(int id)
{
    int n=0;
    for ( int i=0; i < 12; i++)
    {
        if ( inventario[i] == id ) n++;
    }    
    return n;    
}


int player::num_inventario()
{
    int n=0;
    for ( int i=0; i < 12; i++)
    {
        if ( inventario[i] != 0 ) n++;
    }    
    return n;
} 

En la función player::inicia(), se inicializa la nueva variable precio.

    dinero = 50000;

En el archivo npc.h, en la clase npc se añaden nuevas funciones.

      bool posicion_cerca(int num=0);
      void cambia_estado(int _estado){ estado = _estado; };
      int getestado(){ return estado; };
      bool frente();
      bool alineado_vertical();

Y a continuación tienen las nuevas funciónes:

 bool npc::alineado_vertical(){
      int jx = jugador.getx() + desplazamiento_map_x;      
      int jy = jugador.gety() + desplazamiento_map_y;

      return (  y+desplazamiento*2 < jy   && abs(jx-x) <= desplazamiento*2  );             
 }

bool npc::frente()
{
    int jx = jugador.getx() + desplazamiento_map_x;
    int jy = jugador.gety() + desplazamiento_map_y; 
    
    int d =  jugador.dire();
    
    if ( jx > x )
    {
        if ( abs ( jy - y ) < desplazamiento*2 )
        {
             if ( d == 1 )
             {
                  return true;
             }else{
                  return false; 
             }
        }
    }  
    
    if ( jx < x )
    {
        if ( abs ( jy - y ) < desplazamiento*2 )
        {
             if ( d == 2 )
             {
                  return true;
             }else{
                  return false; 
             }
        }
    }    
    
    if ( jy < y )
    {
        if ( abs ( jx - x ) < desplazamiento*2 )
        {
             if ( d == 0 )
             {
                  return true;
             }else{
                  return false; 
             }
        }
    }  
       
    if ( jy > y )
    {
        if ( abs ( jx - x ) < desplazamiento*2 )
        {
             if ( d == 3 )
             {
                  return true;
             }else{
                  return false; 
             }
        }
    }    
    
    return false;            
}

// num distancia considerada cercana en pasos
bool npc::posicion_cerca(int num)
{
     int _x = jugador.getx() + desplazamiento_map_x;
     int _y = jugador.gety() + desplazamiento_map_y;
     int d = 32 + (desplazamiento*(2+num));
     int d2 =abs ( _x - x ) + abs ( _y - y );
     return d2 <= d && lugar == escena ;
}


Si todo se ha copiado correctamente, solo nos falta añadir las nuevas imagenes, sonidos, etc.


Esta vez hacen faltan tres archivos DAT, el datosjuego, datostienda, y objetos.

Haz clic aquí para descargar el RAR del archivo datosjuego.
Haz clic aquí para descargar el RAR del archivo datosjuego.h
Haz clic aquí para descargar el RAR del archivo datostienda.
Haz clic aquí para descargar el RAR del archivo objetos.

Aquí tenéis un vídeo de como quedará la tienda.

10 comentarios:

  1. Otra vez faltan los headers :D Muchas gracias por el curso, y perdón por ser tan pesado.

    ResponderEliminar
    Respuestas
    1. Pues si tienes razón, se me olvidó de nuevo añadir datosjuego.h, gracias por avisar. De todos modos recuerda que con el grabber puedes generarlo.

      Eliminar
    2. Soy incapaz de con grabber abrir tus .dat. Mirando por ahí, vi que un .dat no es modificable con grabber si tiene contraseña y fue generado en otro equipo. igual cambiando los metadatos... pero no me dió por ahí jajajaja. Muchas gracias :D

      Eliminar
    3. La contraseña la tienes en el programa, sino no sería capaz de utilizarlo.

      Eliminar
  2. Hola. El primer link de descarga del archivo datosjuego no funciona. Gracias.

    ResponderEliminar
  3. Hola. ¿Podrías añadir el datosjuego.h correspondiente a este capitulo? Gracias

    ResponderEliminar
  4. Tres cosas. Una, que las funciones nombre_objeto() y descripcion_objeto() de misobjetos.h deben devolver NULL, en lugar de una cadena vacía. Así como está, el dev-c++ me manda una advertencia, y con razón: el valor que devuelven es un puntero char, no una cadena como tal. Si fueran a devolverla, creo que daría un error.
    Segundo, que los valores de arma y armadura del personaje deberían actualizarse al nuevo datafile, ya no son 4 y 5 respectivamente, sino 5 y 6 (si los dejo como están sale el prota desde el principio con armadura de placas)
    Y tercero, en la función pinta_inventario(), a la hora de mostrar los objetos equipados, esta mal. Creo que el código también debería adaptarse al nuevo datafile. Gracias

    ResponderEliminar
    Respuestas
    1. En el caso que realices el cambio y devuelvas NULL, asegúrate de que cuando utilices estas funciones pueden devolver un valor nulo y por tanto hay algunos casos que no puedes utilizarlo directamente.
      Esto quiere decir, que previamente debes guardar el valor en una variable y comprobar de que no sea NULL para evitar errores.
      Con respecto al resto no puedo comprobarlo, actualmente no tengo para compilar el código.

      Eliminar
    2. En realidad soy yo el que esta mal, disculpa. Lo único que mantengo es lo de los valores de retorno en char*, es decir, a lo que te referiste en tu respuesta. El resto era una confusion mía porque cuando termine de transcribir el código, me dieron muchos errores, que eran entre errores de transcripción y cosas que no habías incluido en las explicaciones, que tuve que descargar el .h después para corregirlos. El resultado fue una gran confusión, y me pase el día entero tratando de corregir el código para que funcionara bien. Esas propuestas las di en un momento en el que así me funcinaban pero no de otra manera, pero ya lo reparé y es justo como sale en tu código. Solo mantengo lo del NULL en lugar de la cadena vacía, creo que es más confiable pues de devolver una cadena vacía como puntero el programa se crashearía. Eso es todo, espero haya quedado claro pata evitar mas confusiones

      Eliminar