jueves, 23 de abril de 2009

Estados de Juego

Muy bien, vamos a empezar con otro concepto, al cual llamo estados de juego.
Todos los juegos tienen diferentes estados, por ejemplo cuando empezamos, es muy seguro que se nos presente el menu de inicio o una animacion, luego pasamos a lo que es el juego, y finalmente los creditos. Tambien hay muchos mas estados, un ejemplo es cuando pausamos el juego o especificamos las opciones.
Hasta ahora veniamos muy directo, solo mostrando los conceptos y como ponerlos en practica, pero si queremos hacer un juego mas serio, vamos a querer que aunquesea tenga un menu, donde podemos elejir opciones, comenzar el juego, creditos, etc, para eso usaremos los estados de juego.
Comenzemos con el esqueleto basico. Las clases Main.java y Board.java ya estan explicadas anteriormente, usaremos la version de Board.java que usa el timer de swing que veniamos usando. Veamos ahora como agregar estados. Primero que nada, agregemos una interface, lo que esta tendra seran variables globales a todas nuestras clases.

package gamestates;

/**
*
* @author fede
*/
public interface Commons {
enum GameState {MenuScreen, Pause, Playing, GameOver};
}


Como veran solo definimos un nuevo tipo llamado GameState con los estados que deseamos, estos pueden ir modificandose mas adelante.

public class Board extends JPanel implements ActionListener, Commons {


Luego modificamos la clase para implementar la interface.
El constructor ahora se vera algo asi

public static GameState currentState;
public Board(){
setDoubleBuffered(true);
setFocusable(true);
addKeyListener(new Listener());
setBackground(Color.white);

timer = new Timer(5, this);
timer.start();
currentState = GameState.MenuScreen;
}


La pantalla inicial sera MenuScreen, es decir, la pantalla del menu.
Fijense que la variable currentState es estatica, esto es importante, para poder modificar el estado desde otras clases.
Estas clases seran una representando cada estado, en este caso haremos una para MenuScreen, otra Playing y otra GameOver.

MenuScreen.java
package gamestates;

import java.awt.Graphics2D;
import java.awt.event.KeyEvent;

/**
*
* @author fedekun
*/
public class MenuScreen implements Commons {
public void draw(Graphics2D g2d){
g2d.drawString("Este es el menu, presiona [ENTER] para jugar", 3, 10);
}

public void keyPressed(KeyEvent e){
if(e.getKeyCode() == KeyEvent.VK_ENTER)
Board.currentState = GameState.Playing;
}
}


Hay 3 partes en donde llamaremos a funciones de esta clase, una es la parte de dibujar, otra la de actualizar y otra la de input. En este caso no necesitamos actualizar, asi que solo dibujamos, y hacemos input.

Playing.java
package gamestates;

import java.awt.Graphics2D;
import java.awt.event.KeyEvent;

/**
*
* @author fedekun
*/
public class Playing implements Commons {
private int x, y, dx, dy, speed;

public Playing(){
x = 3;
y = 10;
dx = dy = 1;
speed = 1;
}

public void draw(Graphics2D g2d){
g2d.drawString("Presiona [Escape] para salir", 3, 10);
g2d.drawString("Estamos jugando!", x, y);
}

public void update(){
if(x > 200 || x < 0)
dx*=-1;
if(y > 280 || y < 0)
dy*=-1;
x+=speed*dx;
y+=speed*dy;
}

public void keyPressed(KeyEvent e){
if(e.getKeyCode() == KeyEvent.VK_ESCAPE)
Board.currentState = GameState.GameOver;
}
}


Aca tenemos las 3 implementaciones, draw, udpdate e input.
Y finalmente...

GameOver.java
package gamestates;

import java.awt.Graphics2D;

/**
*
* @author fedekun
*/
public class GameOver {

public void draw(Graphics2D g2d){
g2d.drawString("GAME OVER", 100, 100);
}

}


Esos son los fundamentos, ahora unimos todo en Board.java

package gamestates;

import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;

// timer
import javax.swing.Timer;
import java.awt.Toolkit;

// teclas y timer
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

// teclas
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

/**
*
* @author fede
*/
public class Board extends JPanel implements ActionListener, Commons {
private Timer timer;
public static GameState currentState;
private MenuScreen menuScreen;
private Playing playing;
private GameOver gameOver;
public Board(){
setDoubleBuffered(true);
setFocusable(true);
addKeyListener(new Listener());
setBackground(Color.white);

timer = new Timer(5, this);
timer.start();
currentState = GameState.MenuScreen;
menuScreen = new MenuScreen();
playing = new Playing();
gameOver = new GameOver();
}

public void paint(Graphics g){
super.paint(g);
Graphics2D g2d = (Graphics2D)g;

// Dibuja aca
if(currentState == GameState.MenuScreen)
menuScreen.draw(g2d);
if(currentState == GameState.Playing)
playing.draw(g2d);
if(currentState == GameState.GameOver)
gameOver.draw(g2d);

Toolkit.getDefaultToolkit().sync();
g.dispose();
}

public void actionPerformed(ActionEvent e){
// Logica aca
if(currentState == GameState.Playing)
playing.update();

repaint();
}

private class Listener extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e){
// Metodo de los objetos que reciben input
if(currentState == GameState.MenuScreen)
menuScreen.keyPressed(e);
if(currentState == GameState.Playing)
playing.keyPressed(e);
}

@Override
public void keyReleased(KeyEvent e){
// Metodo de los objetos que reciben input
}
}
}


Como veran, en la parte de draw, update e input, hacemos un if para ver en que estado estamos, y segun el estado hacemos lo que debemos hacer.
En un proyecto mas grande esto es muy util, es lo basico de programacion modular, dividir el problema en subproblemas, con orientacion a objetos, es mas facil aun :)
Espero hallan podido entender la idea, cualquier cosa, contactenme.

Codigo fuente: de 4shared

sábado, 18 de abril de 2009

Esqueleto Basico

Bueno, este es el esqueleto basico que seguiran la mayoria de los juegos, no voy a explicar mucho, solo sera dar el codigo, ya que esta explicado en Conceptos.

Main.java
package packagenamehere;

import javax.swing.JFrame;

/**
*
* @author fede
*/
public class Main extends JFrame {
public Main(){
add(new Board());
setTitle("Skeleton");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(300,300);
setLocationRelativeTo(null);
setVisible(true);
setResizable(false);
}

public static void main(String[] args) {
new Main();
}

}


Board.java
package packagenamehere;

import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;

// timer
import javax.swing.Timer;
import java.awt.Toolkit;

// teclas y timer
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

// teclas
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

/**
*
* @author fede
*/
public class Board extends JPanel implements ActionListener {
private Timer timer;
public Board(){
setDoubleBuffered(true);
setFocusable(true);
addKeyListener(new Listener());
setBackground(Color.white);

timer = new Timer(5, this);
timer.start();
}

public void paint(Graphics g){
super.paint(g);
Graphics2D g2d = (Graphics2D)g;

// Dibuja aca

Toolkit.getDefaultToolkit().sync();
g.dispose();
}

public void actionPerformed(ActionEvent e){
// Logica aca

repaint();
}

private class Listener extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e){
// Metodo de los objetos que reciben input
}

@Override
public void keyReleased(KeyEvent e){
// Metodo de los objetos que reciben input
}
}
}


Si leyeron el post de Animacion, veran que digo que hay mas timers, este utiliza threads, y es una forma mas efectiva aunque un poco mas complicada de hacerlo.

Board.java
package packagenamehere;

import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;

// timer
import java.awt.Toolkit;

// teclas
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

/**
*
* @author fede
*/
public class Board extends JPanel implements Runnable {
private Thread thread;
private final int DELAY = 50;
private GameState gameState;
private MenuScreen menuScreen;

public Board(){
setDoubleBuffered(true);
setFocusable(true);
addKeyListener(new Listener());
setBackground(Color.white);
}

@Override
public void addNotify(){
super.addNotify();
thread = new Thread(this);
thread.start();
}

@Override
public void paint(Graphics g){
super.paint(g);
Graphics2D g2d = (Graphics2D)g;

// Dibuja aca

Toolkit.getDefaultToolkit().sync();
g.dispose();
}

public void cycle(){
// Logica aca
}

@Override
public void run(){
long beforeTime, timeDiff, sleep;

beforeTime = System.currentTimeMillis();

while(true){
cycle();
repaint();

timeDiff = System.currentTimeMillis() - beforeTime;
sleep = DELAY - timeDiff;

if (sleep < 0)
sleep = 2;

try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
System.out.println("interrupted");
}

beforeTime = System.currentTimeMillis();
}
}

private class Listener extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e){
// Metodo de los objetos que reciben input
}

@Override
public void keyReleased(KeyEvent e){
// Metodo de los objetos que reciben input
}
}
}

Space Invaders

Otro juego popular "ochentoso". Vamos a clonarlo, consistira basicamente en una nave que podemos controlar, la cual se mueve solo horizontalmente, y muchas naves enemigas que van bajando y moviendose, si destruimos todas antes que llegen al piso, ganamos, de lo contrario, perdemos x)
En este caso las naves enemigas seran llamados Aliens. Para descargar las imagenes que vamos a utilizar, clickea aca.
Muy bien, empezamos como veniamos hasta ahora, creamos un nuevo proyecto en netbeans, en este caso lo llame Space Invaders, y el package spaceinvaders. En la clase principal heredamos JFrame y hacemos la ventana.

package spaceinvaders;

import javax.swing.JFrame;

/**
*
* @author fede
*/
public class Main extends JFrame {

  public Main(){
      add(new Board());
      setTitle("JSpace Invaders");
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setSize(300,300);
      setLocationRelativeTo(null);
      setVisible(true);
      setResizable(false);
  }

  public static void main(String[] args) {
      new Main();
  }

}


Nada nuevo hasta ahora, y de hecho no deberia haber nada nuevo en las otras clases tampoco, solo logica.
Veamos ahora la clase Ship. Esta clase es nuestra nave principal, la cual movemos y es capaz de disparar.
Realmente no tiene cosas nuevas ninguna de estas clases, pero si no sabes ArrayList por ejemplo, recomendaria leer la documentacion a parte.
Aca tenemos la clase Ship.

package spaceinvaders;

import javax.swing.ImageIcon;

import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.Rectangle;

import java.util.ArrayList;

/**
*
* @author fede
*/
public class Ship {
  private Image image;
  private int x,y,dx;
  private final int SPEED = 2;
  private ArrayList lasers;
  private boolean shot;
  public Ship(){
      ImageIcon ii = new ImageIcon(this.getClass().getResource("images/ship.png"));
      image = ii.getImage();
      y = 250;
      x = 150-image.getWidth(null)/2;
      dx = 0;
      lasers = new ArrayList();
      shot = true;
  }

  public int getX(){
      return x;
  }

  public int getY(){
      return y;
  }

  public Image getImage(){
      return image;
  }

  public ArrayList getLasers(){
      return lasers;
  }

  public void logic(){ // Toda la logica la pondre aca
      if((x>0 && dx<0)>0))
          x += dx;
  }

  public void keyPressed(KeyEvent e){
      int key = e.getKeyCode();

      if(key == KeyEvent.VK_RIGHT)
          dx = SPEED;
      if(key == KeyEvent.VK_LEFT)
          dx = SPEED * -1;
      if(key == KeyEvent.VK_SPACE && shot)
      {
          lasers.add(new Laser(x + image.getWidth(null)/2, y));
          shot = false;
      }
  }

  public void keyReleased(KeyEvent e){
      int key = e.getKeyCode();

      if(key == KeyEvent.VK_LEFT && dx < dx =" 0;" key ="="> 0)
          dx = 0;
      if(key == KeyEvent.VK_SPACE)
          shot = true;
  }

  public Rectangle getBounds(){
      return new Rectangle(x, y, image.getWidth(null), image.getHeight(null));
  }
}


Como veran, la mayoria son variables, algunos sets y gets, logica y teclas, no es mucho realmente, lo que resaltaria de aca es cuando apretamos la barra espaciadora, vemos que agregamos un nuevo objeto Laser (que sera otra clase) a una ArrayList, la idea es luego devolver esa lista y asi manipular todos los lasers en un bucle.
Ahora, veamos la clase Laser, esta es bastante simple, solo se necesita la imagen, y algo de logica

package spaceinvaders;

import java.awt.Image;
import javax.swing.ImageIcon;
import java.awt.Rectangle;

/**
*
* @author fede
*/
public class Laser {
  protected Image image;
  protected int x,y;
  private final int SPEED = 5;
  private boolean visible;

  public Laser(int x, int y){
      ImageIcon ii = new ImageIcon(this.getClass().getResource("images/laser.png"));
      image = ii.getImage();
      visible = true;
      this.x = x;
      this.y = y;
  }

  public int getX(){
      return x;
  }

  public int getY(){
      return y;
  }

  public Image getImage(){
      return image;
  }


  public Rectangle getBounds(){
      return new Rectangle(x, y, image.getWidth(null), image.getHeight(null));
  }

  public boolean isVisible(){
      return visible;
  }

  public void setVisible(boolean visible){
      this.visible = visible;
  }

  public void update(){
      y -= SPEED;
      if(y<0) visible =" false;">


Vemos que en el constructor ponemos las coordenadas iniciales, es importante ya que queremos los lasers en las mismas coordenadas que nuestra nave. Como veran solo tenemos cinco variables, image, x, y, SPEED y visible.
Son bastante descriptivas por si mismas, visible quizas sea mas complicada que las demas ahora, simplemente dice si debemos dibujar y considerar este objeto para colision, cuando esta como no visible, es como si no existiera y luego es borrado.
Los aliens de nuestro juego tambien pueden tirar bombas, son muy similares a los lasers, la unica diferencia es que caen, envez de subir, ya que los aliens estan arriba y van bajando, dejan caer bombas. La clase Bomb especifica estas bombas, ya que son tan similares a los lasers, heredamos de esta clase y cambiamos solo lo necesario.

package spaceinvaders;

/**
*
* @author fede
*/
public class Bomb extends Laser {
  public Bomb(int x, int y){
      super(x, y);
  }

  @Override
  public void update(){
      y++;
  }
}


Ahora, la clase Alien, despues de la principal (Board) creo que es la mas complicada, asi que intentare explicarla mas a fondo.

package spaceinvaders;

import java.awt.Image;
import java.awt.Rectangle;
import javax.swing.ImageIcon;
import java.util.Random;
import java.util.ArrayList;

/**
 *
 * @author fede
 */
public class Alien {
    private Image image;
    private int x, y, speed, direction, movedX, movedY, wentDown, bombChance;
    private final int RANGE;
    private boolean visible, goDown;
    private Random random;
    private ArrayList bombs;
    public Alien(int x, int y){
        ImageIcon ii = new ImageIcon(this.getClass().getResource("images/alien.png"));
        image = ii.getImage();
        this.x = x;
        this.y = y;
        speed = 1;
        RANGE = 100;
        movedX = 0;
        direction = 1; // 1 = derecha, -1 = izquierda
        visible = true;
        goDown = false;
        movedY = 0;
        wentDown = 0;
        random = new Random();
        bombChance = 700; // 1 in 40
        bombs = new ArrayList();
    }

    public int getX(){
        return x;
    }

    public int getY(){
        return y;
    }

    public void setX(int x){
        this.x = x;
    }

    public void setY(int y){
        this.y = y;
    }

    public Image getImage(){
        return image;
    }

    public int getSpeed(){
        return speed;
    }

    public void setSpeed(int speed){
        this.speed = speed;
    }

    public void setVisible(boolean visible){
        this.visible = visible;
    }
    
    public boolean isVisible(){
        return visible;
    }

    public Rectangle getBounds(){
        return new Rectangle(x, y, image.getWidth(null), image.getHeight(null));
    }

    public void update(){
        if(movedX>RANGE){
            movedX = 0;
            goDown = true;
            direction*=-1;
        }

        if(goDown){
            y++;
            movedY++;
            if(movedY > image.getHeight(null))
            {
                goDown = false;
                movedY = 0;
                wentDown++;
                if(wentDown%2==0)
                    speed++;
            }
        } else {
            x += speed * direction;
            movedX+= speed;
        }

        if(random.nextInt()%bombChance==1 && y < 150)
            bombs.add(new Bomb(x, y));
    }

    public ArrayList getBombs(){
        return bombs;
    }
}


A primera vista, notamos muchas variables, la mayoria son faciles de deducir que hacen pero las demas las ire explicando a medida que explique que hacen los aliens.
Para poder explicar esta clase es necesario que entiendas como se mueven los aliens. Los aliens tienen un rango, se mueven hacia la derecha ese rango, luego bajan, y se mueven hacia la izquierda, el mismo rango, y repiten. Ese movimiento lo hacemos en el update, para eso necesitamos muchas variables, creo la mayoria son de esa logica. Otra cosa que hacemos es un random para tirar las bombas, constantemente genera numeros aleatorios hasta que tira una bomba cuando es igual a uno que pidamos dentro del rango. Lo puse en 700, es decir, tenes una chance en 700 de que tire la bomba, pero con todos los aliens y la velocidad que se actualiza, es bastante seguido. Tiene un metodo getBombs, similar al de ship para los lasers, y el resto son puros sets y gets.

Finalmente, llegamos a la clase Board, la que junta todas las demas clases, aca esta

package spaceinvaders;

import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;

// timer
import javax.swing.Timer;
import java.awt.Toolkit;

// teclas y timer
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

// teclas
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

import java.util.ArrayList;

import java.awt.Font;

/**
 *
 * @author fede
 */
public class Board extends JPanel implements ActionListener {
    private Timer timer;
    private Ship ship;
    private Alien alien[][];
    private final int ALIENFILES, ALIENROWS, ALIEN_STARTX, ALIEN_STARTY, ALIEN_PADDING;
    private Font font;
    private String msg;
    private int aliensLeft;
    private boolean gameEnded;
    public Board(){
        setDoubleBuffered(true);
        setBackground(Color.white);
        setFocusable(true);
        addKeyListener(new Listener());
        
        ship = new Ship();
        
        // Aliens
        ALIENFILES = 6;
        ALIENROWS = 3;
        ALIEN_STARTX = 20;
        ALIEN_STARTY = 20;
        ALIEN_PADDING = 3;
        alien = new Alien[ALIENFILES][ALIENROWS];
        for(int i = 0; i < ALIENFILES; i++)
            for(int j = 0; j < ALIENROWS; j++){
                alien[i][j] = new Alien(ALIEN_STARTX, ALIEN_STARTY);
                alien[i][j].setX(ALIEN_STARTX + i*alien[i][j].getImage().getWidth(null) + i*ALIEN_PADDING);
                alien[i][j].setY(ALIEN_STARTY + j*alien[i][j].getImage().getHeight(null) + j*ALIEN_PADDING);
            }

        aliensLeft = ALIENFILES * ALIENROWS;
        font = new Font("Verdana", Font.PLAIN, 12);
        msg = "Aliens left: " + aliensLeft;

        gameEnded = false;

        timer = new Timer(15, this);
        timer.start();
    }

    public void paint(Graphics g){
        super.paint(g);
        Graphics2D g2d = (Graphics2D)g;

        // Draw
        g2d.drawImage(ship.getImage(), ship.getX(), ship.getY(), this);

        // Draw Lasers
        ArrayList lasers = ship.getLasers();
        for(int i = 0; i < lasers.size(); i++){
            Laser l = lasers.get(i);
            g2d.drawImage(l.getImage(), l.getX(), l.getY(), this);
        }

        // Draw aliens
        for(int i = 0; i < ALIENFILES; i++)
            for(int j = 0; j < ALIENROWS; j++){
                if(alien[i][j].isVisible())
                    g2d.drawImage(alien[i][j].getImage(), alien[i][j].getX(),
                            alien[i][j].getY(), this);
                ArrayList bombs = alien[i][j].getBombs();
                for(int k=0; k < bombs.size(); k++){
                    Bomb bomb = bombs.get(k);
                    g2d.drawImage(bomb.getImage(), bomb.getX(), bomb.getY(), this);
                }
            }

        
        // Draw text
        g2d.setColor(Color.black);
        g2d.setFont(font);
        g2d.drawString(msg, 3, 12);

        Toolkit.getDefaultToolkit().sync();
        g.dispose();
    }

    public void actionPerformed(ActionEvent e){
        // Updates
        ship.logic();
        
        // Lasers
        ArrayList lasers = ship.getLasers();
        for(int i = 0; i < lasers.size(); i++){
            Laser l = lasers.get(i);
            if(l.isVisible())
                l.update();
            else
                lasers.remove(i);
        }

        // Aliens
        for(int i = 0; i < ALIENFILES; i++){
            for(int j = 0; j < ALIENROWS; j++){
                alien[i][j].update();
                if(alien[i][j].getY() >= 250)
                    gameOver(0); // 0 = lose, 1 = win

                // Aliens Bombs
                ArrayList bombs = alien[i][j].getBombs();
                for(int k=0; k < bombs.size(); k++){
                    Bomb bomb = bombs.get(k);
                    if(bomb.isVisible())
                        bomb.update();
                    else
                        bombs.remove(k);

                    if(bomb.getBounds().intersects(ship.getBounds()))
                    {
                        bomb.setVisible(false);
                        gameOver(0);
                    }
                }

                //Hit Test
                for(int li = 0; li < lasers.size(); li++){
                    Laser l = lasers.get(li);
                    if(l.getBounds().intersects(alien[i][j].getBounds()) && l.isVisible() && alien[i][j].isVisible()){
                        alien[i][j].setVisible(false);
                        l.setVisible(false);
                        aliensLeft--;
                        if(aliensLeft <= 0)
                            gameOver(1);
                    }
                }
            }
        }

        if(!gameEnded)
            msg = "Aliens left: " + aliensLeft;

        repaint();
    }

    private class Listener extends KeyAdapter{
        @Override
        public void keyPressed(KeyEvent e){
            ship.keyPressed(e);
            if(e.getKeyCode() == KeyEvent.VK_ENTER){
                if(gameEnded){
                    for(int i = 0; i < ALIENFILES; i++)
                        for(int j = 0; j < ALIENROWS; j++){
                            alien[i][j] = null;
                            alien[i][j] = new Alien(ALIEN_STARTX, ALIEN_STARTY);
                            alien[i][j].setX(ALIEN_STARTX + i*alien[i][j].getImage().getWidth(null) + i*ALIEN_PADDING);
                            alien[i][j].setY(ALIEN_STARTY + j*alien[i][j].getImage().getHeight(null) + j*ALIEN_PADDING);
                        }
                    gameEnded = false;
                    aliensLeft = ALIENFILES * ALIENROWS;
                    timer.start();
                }
            }
        }

        @Override
        public void keyReleased(KeyEvent e){
            ship.keyReleased(e);
        }
    }

    public void gameOver(int status){
        gameEnded = true;
        if(status == 0)
            msg = "You lose!";
        else
            msg = "You won!";
        msg += " - Press [ENTER] to play again.";
        timer.stop();
    }
}


Bueno, la clase mas larga, Board, los conceptos estan todos explicados creo yo, si hay algo sin explicar por favor contactenme asi lo arreglo.

Ciertamente hay cosas que los pueden confundir pero seria puro Java. Uso una array bidimensional para los aliens, asi que especifico la cantidad de filas y columnas que quiero. Los bucles mas complicados serian los de deteccion de colision, que tenemos que comparar todos los lasers con todos los aliens, quizas sea eso lo que mas confunda pero es bastante simple, solo prestar atencion a como se hace. Recomiendo ver el pong, y todos los conceptos, si practicaron con eso, no deberia haber mayores inconvenientes con esto.

Para cualquier duda o sugerencia contactenme!

Descargar: de mediafire