martes, 29 de septiembre de 2015

Crear juego RPG en C++ y Allegro 4 (15) Lucha

Continuamos con el curso de crea tu juego RPG en C++ y Allegro. En esta entrega se añadirá algo de acción.





Objetivo del curso

En esta entrega, se añadirá la acción atacar al jugador, una nueva clase llamada enemigo al cual se puede atacar, nuevos sonidos.


Programación

En el archivo audio.h, se añade lo siguiente:

void sonido_espada_aire(){
    play_sample ( (SAMPLE *)datosjuego[dsespada1].dat, 100,128, 1200, 0 );   
}

void sonido_espada_da(){
    play_sample ( (SAMPLE *)datosjuego[dsespada2].dat, 160,128, 2300, 0 );   
}

void sonido_muere(){
    play_sample ( (SAMPLE *)datosjuego[dsmuerte01].dat, 120,128, 1000, 0 );   
}

Estas tres funciones representa los tres nuevos sonidos que se van a añadir al proyecto, el primero cuando se pulsa la tecla ataque, el segundo cuando golpea a un enemigo, y el tercero un sonido de muere enemigo. El funcionamiento en todas ellas es el mismo, mediante el comando play_sample se ejecuta un sample que debe estar incluido en el fichero DAT con los nombres que vienen dados entre corchetes: dsespada1, dsespada2 y dsmuerte01.

En el archivo player.h, se añade una variable privada llamada ataca, y dos funciones: atacando() y no_ataca(). Quedando la clase de la siguiente manera:

class player
{
    BITMAP *prota;  
 int x,y;
 int direccion;
 int animacion;
    bool hablar;
    int ataca; 
          
    public:  
       void inicia();    
       void pinta(); 
       bool choca();
       void teclado(); 
       int getx(){ return x; };
       int gety(){ return y; };
       void posiciona( int _x, int _y);
       void cambia_escenario();
       bool accion();
       void habla();
       void no_habla();
       bool hablando(){ return hablar; };
       bool atacando(){ return ataca>1; };
       void no_ataca(){ ataca = -3; };
}

Como se puede observar las dos nuevas funciones son bastantes sencilla. atacando() se encarga de devolver TRUE cuando el valor de la variable ataca es superior a 1. La función no_ataca() asigna un valor a la variable ataca, en este caso -3.

En la función inicia(), se debe añadir la inicialización de la nueva variable, por tanto se añade lo siguiente:

    ataca = 0;

La función pinta(), cambia a lo siguiente:

void player::pinta()
{
    if ( ataca > 1 && ( direccion == 1 || direccion == 3 ) )
    {
           masked_blit((BITMAP *)datosjuego[diespada].dat, buffer, 0, direccion*96, x-32, y-32, 96,96);  
    } 
     
    masked_blit(prota, buffer, animacion*32, direccion*32, x, y, 32,32);  
    
    if ( ataca > 1 && ( direccion == 0 || direccion == 2 ) )
    {
           masked_blit((BITMAP *)datosjuego[diespada].dat, buffer, 0, direccion*96, x-32, y-32, 96,96);  
    } 
    if ( ataca > 1 || ataca < 0) ataca++;       
}

Existen muchas formas de haber programado esto. En el caso de poseer las imagenes de nuestro personaje portando la espada, en todas direcciones y con animación, la programación hubiese sido añadir una condición y poner otro masked_blit.
En este caso, no se posee dicha imagen. Se ha creado una imagen con el arma aparte. Debido a esto en la función se añade la nueva imagen, que es la espada del personaje.  Llegado a este punto, se puede hacer de dos formas. Una de ella será poner la imagen espada para cada una de las distintas direcciones y animaciones, lo cual es bastante tedioso programar.
Y la otra forma de hacerlo, es la que finalmente se ha utilizado. Para no complicar la programación de colocar la espada en cada una de las situaciones, se ha creado una imagen que es tres veces mas grande que el personaje, si el personaje es de 32x32, la imagen de la espada es de 96x96. Esto se ha hecho así para que no se tenga que colocar la espada al personaje mediante programación, sino cuando se crea la imagen, de esta forma la espada independientemente de la dirección de personaje siempre se sitúa en el mismo lugar en la posición x-32, y-32.
Existen dos condiciones parecidas, ya que según la dirección interesa que la espada se pinte por encima del personaje o por debajo.
La ultima condición, se encarga de incrementar la variable ataca, siempre que no sea ni 0, ni 1.

La funcion teclado()

void player::teclado()
{     
      int ax = x;
      int ay = y;
                  
         if ( !hablar )   
         {  
              // teclas control usuario
              if ( key[KEY_UP] )
              {
                   y-=desplazamiento; 
                   direccion = 3;              
              }
              if ( key[KEY_DOWN] )
              {
                   y+=desplazamiento;
                   direccion = 0;
              }
              if ( key[KEY_LEFT] )
              {
                   x-=desplazamiento;
                   direccion = 1;
              }
              if ( key[KEY_RIGHT] )
              {
                   x+=desplazamiento;
                   direccion = 2;
              } 
        
              if ( key[KEY_SPACE] && ataca == 0 )
              {
                   ataca = 1;
              }
              if ( !key[KEY_SPACE] && ataca == 1 )
              {
                   ataca = 2;
                   sonido_espada_aire();                   
              }
        
        }             
      if ( ax != x || ay != y )
      {
           // entra si a cambiado alguna de las variables x,y
           if ( choca() )
           {
                x =ax;
                y =ay;
           }else{ 
               int mx = desplazamiento_map_x;
               int my = desplazamiento_map_y;
               
               rectfill( choque, ax+4+mx, ay+17+my, ax+28+mx, ay+30+my, 0x000000);
               
               // control choque para npcs
               rectfill( choque, x+4+mx, y+17+my, x+28+mx, y+30+my, 0xffffff);             
           }                                                 
           int num = FRAME_RATE / 12; 
           if ( tiempo_total % num == 0 )
           {
                   sonido_pasos(); 
                   
                   animacion++;
                   if ( animacion > 2 ) animacion = 0;
           } 
      }                            
      if ( ataca > (FRAME_RATE / 4) ) ataca = 0;
}

En esta función, se añadió la tecla que se utiliza para atacar ( la barra espaciadora ), se ha cambiado lo que controla la animación, para que aunque se choque se siga moviendo el personaje.

En el archivo npc.h, se ha añadido la nueva clase enemigo, ya que se ha creado como una derivada de la clase npc.

class enemigo : public npc {
      int vida;
      int v_actual; 
      bool muerto;     
      
     public:
        void crea( BITMAP *_img, int _x, int _y, int dir, int _estado, int _lugar, int v );
        void herida( int d ); 
        void pinta();
        bool ha_muerto() { return muerto; };
};


void enemigo::crea( BITMAP *_img, int _x, int _y, int dir, int _estado, int _lugar, int v )
{
    x = _x;
    y = _y;
    direccion = dir;
    animacion = 0;
    escena = _lugar;
    
    imagen = create_bitmap(_img->w, _img->h);
    mifondo = create_bitmap(32, 32);
    
    blit( _img, imagen, 0,0, 0,0, _img->w, _img->h);
    
    estado = _estado;
    
    primer = false;                   
    vida = v;
    v_actual = vida;      
    muerto = false;              
};     

void enemigo::herida( int d )
{
  if ( !muerto )
  {   
     v_actual-=d;
     if ( v_actual <= 0 )
     {
           muerto = true;
           blit( mifondo, fondo, 0,0, x,y, 32,32); 
           rectfill( choque, x+2, y+1, x+30, y+31, 0x000000);
           sonido_muere();
     }
  }   
};


void enemigo::pinta()
{ 
     if ( lugar == escena && !muerto )
     {
           
          if ( !primer )
          {
               // obtiene una copia de lo anterior
               blit( fondo, mifondo, x,y,0,0,32,32);
               primer = true;
                
          }
          actualiza();
          masked_blit(imagen, fondo, animacion*32, direccion*32, x, y, 32,32);
          
          int nm = (v_actual * 30 ) / vida;
          
          if ( !muerto )
          {
              rectfill( fondo, x+1, y, x+nm, y+5, 0x00ff00);
              rect( fondo, x, y, x+31, y+5, 0x000000);
          }
     }       
}


Un enemigo se ha considerado que es igual a un NPC, pero con vida. Para controlar la vida tiene tres variables, vida, vida actual (v_actual), muerto. Vida indica el máximo de puntos que tiene de vida, vida actual es un valor entre 0 y el máximo, y muerto indica que la vida llegó a cero.
La función herida(), es la que se encarga de ir restando el daño que recibe.
La función pinta() es igual que la de el NPC pero con el añadido, de que pinta un marcador de vida.

En la declaración de la clase npc, las variables privadas las declaramos como variables protegidas, añadiendo al principio de la clase la palabra protected.

class npc {
    protected: 


En la función posicion_cerca() del npc, se añade el control del escenario.

bool npc::posicion_cerca(int _x, int _y)
{
     int d = 32 + (desplazamiento*2);
     int d2 =abs ( _x - x ) + abs ( _y - y );
     return d2 <= d && lugar == escena ;
}


En el archivo mijuego.h, se crea una variable global:

enemigo malo;


En la funcion carga_juego(), se inicializa la nueva variable con la que se crea el enemigo.

malo.crea( (BITMAP *)datosjuego[diene001].dat, 380, 280, 3,5,2,100);

En la función evento_escenario(),  al principio justo después de la declaración de las variables pzx,pzy , se añade una condición para controlar que se golpea al enemigo.

void evento_escenario()
{
    int pzx = jugador.getx() + desplazamiento_map_x;
    int pzy = jugador.gety() + desplazamiento_map_y;     
    
    
    if ( jugador.atacando() && malo.posicion_cerca(pzx,pzy)
       && !malo.ha_muerto() )
    {
        int xn = 2 + rand()%2;
        jugador.no_ataca();
        sonido_espada_da();
        malo.herida(xn);               
    }

...


En la función actualiza_juego(), se cambia el orden en el que se llaman las funciones. Esto se debe a que con el orden antiguo no se llega a ver la imagen de la espada cuando se golpea al enemigo.

// actualiza el estado del juego
void actualiza_juego()
{

    scroll_escenario();

    evento_escenario();    
    
    jugador.teclado(); 
                 
    cambia_escenario();         
}

En la función pinta_juego(), se añade la siguiente línea para mostrar al enemigo, justo después de que se muestren los NPC y antes de que se pinte el fondo en el buffer.

            malo.pinta();

Y con esto ya se tiene todo para tener un enemigo en el mapa. Solo faltaría añadir las nuevas imágenes y sonidos al fichero DAT.

Haz clic aqui para descargar el nuevo fichero DAT comprimido en RAR


Recuerda

Si tienes algún problema con los ejemplos de la pagina, o alguna duda. Puedes plantear tu pregunta en el foro de programación:  http://creatusjuegosdecero.webege.com/index.php

8 comentarios:

  1. Hola. Al ejecutar el código de esta entrega, no se ejecuta el código y el programa termina repentinamente. Además, al intentar abrir el fichero Datosjuego.dat con grabber, éste devuelve un error y no se puede editar.

    Gracias.

    ResponderEliminar
    Respuestas
    1. Para saber como utilizar el fichero .dat , vea el apartado 9 que habla sobre este tema.

      Eliminar
  2. ¿Es posible que faciliten el código fuente y junto con los ficheros .dat funcionando?

    Gracias

    ResponderEliminar
    Respuestas
    1. Solo se facilitan los archivos externos, imágenes, sonidos, etc.. En este caso se facilita el fichero DAT. El código no se facilita, ya que se desea que el interesado vaya realizándolo paso a paso según se va explicando en el curso, y de este modo vaya aprendiendo como funciona lo que se está programado.

      Eliminar
    2. Hola. He realizado el curso hasta el capítulo 14II sin problemas y he leido la parte de grabber. Sin embargo, en el 15, intento abrir el fichero .dat con grabber y me dice: error reading c:\datosjuegos.dat.

      Quisiera preguntar si hay alguna manera especial de abrirlo o qué podría estar pasando. Gracias.

      Eliminar
    3. Hola. Ya lo resolví. El problema con el fichero .dat era simplemente que al cargarlo, no estaba introduciendo el password correctamente.

      Muchas gracias.

      Eliminar
    4. ¿cual es el password correcto? tengo el mismo problema

      Eliminar
    5. Para cualquier información sobre el fichero DAT, ve al apartado correspondiente. En este caso el password esta en el curso 9 que habla del fichero DAT.

      Eliminar