Buenas a todos en este post les quiero compartir el código para implementar un juego de consola de los clásicos más grandes, “El Snake”. El código esta implementado en C++, es un poco viejo dado a que la mayoría de la implementación lo hice mientras cursaba la universidad, pero me tome el tiempo para hacerle algunas mejoras y cambios menores con el fin de compartirlo y que pueda servirles de ejemplo para proyectos similares. El juego solo tiene los aspectos básicos de la lógica de snake, cuenta con dos escenarios distintos uno con y otro sin paredes, se maneja con las flechas de dirección del teclado y el objetivo para ganar el juego es alcanzar la puntuación máxima configurada que por default es 500 pts. En la siguiente imagen podemos observar cómo se vería el juego en ejecución.
Básicamente la estructura del juego se basa en dos archivos de cabecera y cinco clases (Nodo, Snake, Plataforma, Juego e Interfaz). La clase Snake tiene una relación de composición con Nodo mientras que la Juego tiene también una composición con Snake y Plataforma. Por último la interfaz se relaciona por agregación con la clase Juego. En el siguiente diagrama podemos observar mejor la relación de las clases.
Ahora veamos la implementación de los archivos cabecera, el primero "UtilidadezInterfaz.h" ya lo he utilizado antes en el ejemplo de Fundamentos Juegos en Consola C++ donde explique que implementa algunas funciones básicas como cambios de color de consola, posicionar el cursor dadas las coordenadas entre otras. Este es el código del encabezado.
//////////////////////////////////////////// // Definición Bibliotecas y definiciones // q' utilizaremos en el código /////////////////////////////////////////// #include <windows.h> #include <conio.h> #include <stdio .h=> //Definiciones de teclas #define ARR 72//Valor ASCII para tecla flecha arriba. #define ABJ 80//Valor ASCII para tecla flecha abajo. #define DER 77//Valor ASCII para tecla flecha Derecha. #define IZQ 75//Valor ASCII para tecla flecha Izquierda. #define ENT 13//Valor ASCII para tecla Enter. #define ESC 27//Valor ASCII para tecla Esc. //Definiciones de configuración pantalla Salir #define LIN_HOR_SALIR 205//Lineas Horizontales #define LIN_VER_SALIR 186//Lineas Vericales #define ESQ_SUP_DER_SALIR 201//Esquina superior Derecha #define ESQ_SUP_IZQ_SALIR 200//Esquina superior Izquierda #define ESQ_INF_DER_SALIR 187//Esquina inferior Derecha #define ESQ_INF_IZQ_SALIR 188//Esquina inferior Izquierda #define COLOR_CAJA_SALIR 0x0E//Color de la caja de la pantalla salir. #define OPC_SELEC_SALIR 0xDE//Color de la opción seleccionada en la pantalla salir. //////////////////////////////////////////// // Clase UtilidadesInterfaz //Donde se implementara la lógica //necesaria para crear una interfaz con flechas. /////////////////////////////////////////// class UtilidadesInterfaz{ public: //Funcion para capturar la tacla digitada omitiendo el el Enter. static int GetKey() { int Tecla=getch(); if(Tecla==0xE0 || Tecla==0) Tecla=getch(); return Tecla; } //Método para Establecer los Colores recibe un numero de 2 digitos //el primer digito estabece el color del background de la consola. //el segundo numero establece el color de la letra de la consola. static void setColor(unsigned short color){ //Funcion para cambiar el color de la consola SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),color); } //Método que me posiciona el foco de cursor de la consola dadas 2 coordenadas. static void gotoxy(int x,int y){ //Variable Coordenada que guarda las coordenadas donde voy a imprimir. COORD conCord; conCord.X=x; conCord.Y=y; //Método posiciona el foco de cursor de la consola según coordenadas especificadas. SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),conCord); } //Método para cambiar propiedades del cursor. static void ocultaCursor(){ //Variable para controlar atributos del cursor; CONSOLE_CURSOR_INFO myCursor; myCursor.dwSize = 25; myCursor.bVisible = FALSE; //Funcion para cambiar propiedades del cursor de la consola. SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&myCursor); } //Función que confirma la desición del Usuario de salir del programa. static bool Salir(){ system("CLS"); system("color 0F"); setColor(COLOR_CAJA_SALIR); //Dibujo las lineas horizontales de la caja. for(int i=19;i<59;i++){ gotoxy(i,15);printf("%c",LIN_HOR_SALIR); gotoxy(i,5);printf("%c",LIN_HOR_SALIR); } //Dibujo las lineas verticales de la caja. for(int i=6;i<15;i++){ gotoxy(18,i);printf("%c",LIN_VER_SALIR); gotoxy(59,i);printf("%c",LIN_VER_SALIR); } //Dibujo las esquinas de la caja gotoxy(18,5);printf("%c",ESQ_SUP_DER_SALIR); gotoxy(18,15);printf("%c",ESQ_SUP_IZQ_SALIR); gotoxy(59,5);printf("%c",ESQ_INF_DER_SALIR); gotoxy(59,15);printf("%c",ESQ_INF_IZQ_SALIR); //Dibujo el titulo de la caja. gotoxy(19,6); printf(" \n"); gotoxy(19,7); printf(" Desea salir por completo del programa? \n"); gotoxy(19,8); printf(" \n"); //Dibujo las opciones a esocger en la caja. int tecla = 0;//Tecla presionada, inicializada en 0 para primer caso. bool Opcion =true;//Variable que almacena el número de opción en el q se encuentra el Usuario. while(true){ gotoxy(32,12); //Dibujamos las opciones a seleccionar setColor(COLOR_CAJA_SALIR); if(Opcion) setColor(OPC_SELEC_SALIR); printf("SI"); gotoxy(42,12); setColor(COLOR_CAJA_SALIR); if(!Opcion) setColor(OPC_SELEC_SALIR); printf("NO"); tecla=GetKey();//Leemos la tecla digitada. //En caso que la tecla digitada sea alguna de las aceptadas como acciones en el menú if(tecla==DER||tecla==IZQ||tecla==ENT){ switch(tecla){ case DER:Opcion=false;break; case IZQ:Opcion=true;break; case ENT:system("CLS");system("color 0F");return Opcion; } } Sleep(100);//Dormimos el proceso por 100 milisegundos.(Recomendable para liberar procesador) } } //Método para dibujar la portada inicial del Programa. static void Portada(){ //Dibujo las lineas horizontales de la caja para la portada for(int i=2;i<78;i++){ if(i>9 && i<69){ setColor(0x0A); gotoxy(i,2);printf("%c",31); gotoxy(i,21);printf("%c",30); }else{ setColor(0x0D); gotoxy(i,2);printf("%c",205); gotoxy(i,21);printf("%c",205); } } //Dibujo las lineas verticales de la caja para la portada for(int i=3;i<22;i++){ if(i>4&&i<18){ setColor(0x0A); gotoxy(2,i);printf("%c",16); gotoxy(77,i);printf("%c",17); } else{ setColor(0x0D); gotoxy(2,i);printf("%c",186); gotoxy(77,i);printf("%c",186); } } //Dibujo las esquinas de la caja para la portada gotoxy(2,2);printf("%c",201); gotoxy(2,21);printf("%c",200); gotoxy(77,2);printf("%c",187); gotoxy(77,21);printf("%c",188); //Dibujo creditos de la Portada. setColor(0x0E); gotoxy(27,8);printf("Mover Objetos con Flechas C++"); gotoxy(25,12);printf("Programador: Greivin Chaves R."); gotoxy(25,18);printf("Derechos Reservados. 26/11/2013"); setColor(0x0F); printf("\n"); gotoxy(20,23);system("PAUSE"); } };
El segundo archivo de encabezado “Defines.h” define algunas variables globales de configuración del juego, como son los caracteres ASCII a utilizar para dibujar el snake o las paredes, la puntuación máxima del juego, el tamaño del área de juego entre otras.
#ifndef DEFINES_H_INCLUDED #define DEFINES_H_INCLUDED //Definiciones de texturas globales del juego (Basado en caracteres Ascii). #define piel 176 //Textura de piel del Snake #define comida 2 //Carácter a mostrar como comida #define espacio 0 //Carácter de espacio vacío. #define pared 219 //Textura de la pared del área de juego. #define limY 60 //Tamaño o limite en Y del área de juego #define limX 20 //Tamaño o limite en X del área de juego #define maxPoints 500 //Puntación máxima para ganar el juego. #endif // DEFINES_H_INCLUDED
Ahora de las clases veamos primero la clase Nodo, esta permite guardar dos números enteros un “X” y “Y” que serían las coordenadas de cada sección que conforma el snake. Estos nodos son los que se agregan en caso que la culebra coma y tenga que crecer. Este es el código:
#include <iostream> #include <sstream> using namespace std; //////////////////////////////////////////////////////////////////////////////////////////////// // Clase Nodo // Define cada uno de los nodos o secciones que conforman la snake // y guardan su ubicación dentro del área de juego /////////////////////////////////////////////////////////////////////////////////////////////// class Nodo { private: unsigned int x,y; //Coordenadas para conocer la posición dentro del área de juego. public: Nodo() { y=x=0; } Nodo(unsigned int _x,unsigned int _y) { y=_y; x=_x; } unsigned int getX() { return x; } unsigned int getY() { return y; } void setX(unsigned int _x) { x=_x; } void setY(unsigned int _y) { y=_y; } string toString() { stringstream s; s<<"X: "<<x<<" Y: "<<y<<endl; return s.str(); } ~Nodo() {} };
La clase Snake la cual esta implementada como una lista de nodos que van a conformar cada sección de la culebra controla los aspectos básicos como crecer (en el caso que coma), caminar, entre otras. El código de la clase es el siguiente:
#include <list> #include "Nodo.cpp" #define limX 60 #define limY 20 //////////////////////////////////////////////////////////////////////////////////////////////// // Clase Snake // Define la snake, y controla cada uno de los nodos que la conforman. /////////////////////////////////////////////////////////////////////////////////////////////// class Snake { private: list<Nodo> *snake; list<Nodo>::iterator it; /* Crea un nuevo Nodo basado en la dirección que lleva la snake y las posiciones del primer nodo o cabeza. El secreto de este método es posicionar los nuevos nodos al inicio de la culebra y no al final, para facilitar el efecto de que la culebra avanza. */ Nodo * nuevoNodo(unsigned int opc){ int x=snake->front().getX(); int y=snake->front().getY(); //OPC INDICA HACIA DONDE SE ENCUENTRA CAMINANDO //EL SNAKE PARA ASI AGREGAR LA COLA //case 1-> Arriba //case 2-> Abajo //case 3->Derecha //case 4->Izqierda switch(opc) { case 1: { y--; break; } case 2: { y++; break; } case 3: { x++; break; } case 4: { x--; break; } } if(x>=limX) x=0; if(x<0) x=limX-1; if(y>=limY) y=0; if(y<0) y=limY-1; Nodo * nuevo=new Nodo(x,y); return nuevo; } public: Snake() { snake=new list<Nodo>(); } //Método para inicializar la snake. void Inicia(unsigned int x,unsigned int y) { snake->clear(); //Inicializamos la Cabeza snake->push_back(*new Nodo(x,y)); //Agregamos la Cola para iniciar el Juego Crece(3); Crece(3); } //Método para que la snake crezca, el mismo llama al método privado nuevoNodo. //Para efectos de visualización por cada crecimiento se agregan dos nodos. void Crece(unsigned int opc) { snake->push_front(*nuevoNodo(opc)); snake->push_front(*nuevoNodo(opc)); } //Método para que la snake camine //(Se basa en eliminar el ultimo nodo y agregar uno al inicio, creando así un efecto de movimiento.) void Camina(int opc) { snake->pop_back(); snake->push_front(*nuevoNodo(opc)); } //Imprimir la snake. string toString() { stringstream r; for (it=snake->begin(); it!=snake->end(); it++) r<<(*it).toString(); return r.str(); } //Destructor (Importarnte liberar memoria de todo el snake) ~Snake() { snake->clear(); delete snake; } //Método para obtener el contenido de donde se encuentra el iterador. Nodo Contenido(){ return (*it); } //Cabeza de la snake void Inicio(){ it=snake->begin(); } //Cola de la Snake bool Final(){ return it!=snake->end(); } //Control del avance del iterador que recorre la snake. void Avanzo(){ it++; } };
La clase plataforma la cual es el área de juego donde se va a mover la culebra y donde aparecerá la comida de la misma está definida como una matriz de caracteres del tamaño definido en “Defines.h”. Existen dos tipos de plataformas una con paredes y otra sin paredes. Este es el código de la clase:
#include <iostream> #include <sstream> #include "../Defines.h" using namespace std; //////////////////////////////////////////////////////////////////////////////////////////////// // Clase Plataforma // Define el área del juego donde puede moverse la snake // (En este caso es manejado mediante una matriz de caracteres) /////////////////////////////////////////////////////////////////////////////////////////////// class Plataforma { private: char **matriz; public: Plataforma() { matriz=NULL; } //Método para generar el área de juego, según los límites de X y Y establecidos en Defines.h void Genera() { matriz=new char*[limX]; for (int i=0; i<limX; i++) matriz[i]=new char[limY]; } //Método para presentar el primer escenario de juego. (Campo sin paredes en los bordes) void Escenario0() { for (int i=0; i<limX; i++) for (int j=0; j<limY; j++) if(matriz[i][j]!=(char)comida) matriz[i][j]=' '; } //Método para presentar el segundo escenario de juego. (Campo con paredes en los bordes) void Escenario1() { for (int i=0; i<limX; i++) { for (int j=0; j<limY; j++) { if(matriz[i][j]!=(char)comida) { if(j==0||j==limY-1) matriz[i][j]=pared; else if(i==0||i==limX-1) matriz[i][j]=pared; else matriz[i][j]=' '; } } } } //Método para cambiar un sector del área de juego, según coordenadas X y Y //Cambia el valor del area de juego por un nuevo caracter. bool setSector(int x,int y,int caracter) { bool retorno=false; if(getSector(x,y)==(char)piel||getSector(x,y)==(char)pared) throw 1; else { if(matriz[x][y]==char(comida)) retorno=true; matriz[x][y]=caracter; return retorno; } } //Método para obtener un sector del área de juego, según coordenadas X y Y char getSector(int x,int y) { return matriz[x][y]; } //Método para limpiar todos los sectores del área de juego. void Limpia() { for (int i=0; i<limX; i++) delete matriz[i]; } //Impime el encabezado del área de juego. void Encabezado(stringstream &s) { s<<(char)218; for (int i=0; i<limY; i++) s<<(char)196; s<<(char)191<<endl; } //Impime el pie del área de juego. void Pie(stringstream &s) { s<<(char)192; for (int i=0; i<limY; i++) s<<(char)196; s<<(char)217<<endl; } //Impime el área de juego. string toString() { stringstream s; Encabezado(s); for (int i=0; i<limX; i++) { s<<(char)179; for (int j=0; j<limY; j++) { s<<matriz[i][j]; } s<<(char)179<<endl; } Pie(s); return s.str(); } //Destructor (Importante liberar memoria del objeto) ~Plataforma() { if(matriz) { Limpia(); delete []matriz; } } };
La clase juego es la que implementa todas las reglas del juego y lógica principal, permite la interacción entre la plataforma y la snake. Contiene validaciones para determinar si se ganó el juego llegando al máximo puntaje o si se perdió al chocar contra la misma snake o con algún obstáculo. Este es el código de la clase:
#include <Windows.h> #include <conio.h> #include "Snake.cpp" #include "../AreaJuego/Plataforma.cpp" #include "../UtilidadesInterfaz.h" #define ut UtilidadesInterfaz #include <stdlib.h> //Variable camina sentido del Snake //1-> Arriba //2-> Abajo //3->Derecha //4->Izqierda unsigned int camina; bool pauseGame; bool gameOver; // Hilo de proceso que procesa las teclas que son digitadas durante la ejecución del juego. DWORD WINAPI Tecla(LPVOID iValue) { int Tecla; pauseGame=false; while(true) { if(gameOver) return 0; Tecla=getch(); if(Tecla==0xE0 || Tecla==0) Tecla=getch(); switch(Tecla) { case 72: { if(camina!=2) camina=1; pauseGame=false; break; } case 80: { if(camina!=1) camina=2; pauseGame=false; break; } case 75: { if(camina!=3) camina=4; pauseGame=false; break; } case 77: { if(camina!=4) camina=3; pauseGame=false; break; } case 112: { pauseGame=true; break; } case 27: { pauseGame=false; gameOver=true; return 0; break; } } }; return 0; } //////////////////////////////////////////////////////////////////////////////////////////////// // Clase Juego // Define las reglas del juego y permite interacción entre la Snake y el Área de Juego /////////////////////////////////////////////////////////////////////////////////////////////// class Juego { private: //Variables para utilización de hilos HANDLE hThread1; DWORD dwGenericThread; //Snake del juego Snake *s; //Variables del juego. int Puntos; //Variable aux para determinar si se perdió el juego. bool perdi; //Escenario del juego unsigned short int escenario; public: Plataforma * p; Juego() { gameOver=false; perdi=false; camina=3; s=new Snake(); p=new Plataforma(); Puntos=0; escenario=0; } void setEscenario( unsigned short int _escenario) { escenario=_escenario; } ~Juego() { delete s; delete p; } //Método para cargar el escenario seleccionado en el menú de escenarios. void CargarEscenario() { switch(escenario) { case 0: { p->Escenario0(); break; } case 1: { p->Escenario1(); break; } } } //Cambia el color del área de juego según el escenario. void ColorArea(){ switch(escenario) { case 0: { ut::setColor(23); break; } case 1: { ut::setColor(11); break; } } } //Inicializa la partida y desarrolla la lógica del juego. int Comienzo() { gameOver=false; perdi=false; s->Inicia(1,2); p->Genera(); CargarEscenario(); camina=3; Puntos=0; //Creamos el hilo que se encarga de detectar las teclas. hThread1 = CreateThread(NULL,0,Tecla,NULL,0,&dwGenericThread); if(hThread1 == NULL) { DWORD dwError = GetLastError(); cout<<"Error al iniciar Juego"<<dwError<<endl ; return 0; } ut::gotoxy(0,0); DibujaSnake(); Comida(); cout<<p->toString(); //Bucle principal del juego. while(!gameOver) { if(!pauseGame && Puntos<maxPoints) { if(camina==3||camina==4) Sleep(100); else Sleep(130); ut::gotoxy(0,0); s->Camina(camina); DibujaSnake(); cout<<p->toString(); ut::gotoxy(65,19); ut::setColor(12); cout<<"Pts: "<<Puntos; ColorArea(); }else gameOver=true; } if(perdi) { ut::gotoxy(15,22); ut::setColor(12); cout<<"PERDISTE!! Juego Terminado"<<endl; }else{ ut::gotoxy(15,22); ut::setColor(0x02); cout<<"GANASTE!! Juego Terminado"<<endl; } ut::setColor(15); WaitForSingleObject(hThread1,INFINITE); return 0; } //Método para dibujar el Snake, validar si crece o si tuvo alguna colisión. void DibujaSnake() { try { CargarEscenario(); //Avanza el snake y valida si debe crecer. for (s->Inicio(); s->Final()!=false; s->Avanzo()) if(p->setSector(s->Contenido().getY(),s->Contenido().getX(),piel)) { s->Crece(camina); Puntos+=10; Comida(); } } catch(...) { for (s->Inicio(); s->Final()!=false; s->Avanzo()) try { p->setSector(s->Contenido().getY(),s->Contenido().getX(),piel); } catch(...) {} pauseGame=false; gameOver=true; perdi=true; } } //Método para posicionar la nueva comida de la snake. // Basado en valores random siempre y cuando el sector a posicionar este vacío. void Comida() { int x=0; int y=0; do { x=rand()%limX; y=rand()%limY; } while(p->getSector(x,y)==(char)pared||p->getSector(x,y)==(char)piel); p->setSector(x,y,comida); } //Método para mostrar menu informativo lateral. void MenuInfo() { ut::setColor(14); ut::gotoxy(60,0); cout<<"--------------------"; ut::gotoxy(63,1); cout<<"Juego de Snake"; ut::gotoxy(63,4); cout<<"Instrucciones: "; ut::gotoxy(65,6); cout<<char(24)<<" = Arriba."; ut::gotoxy(65,7); cout<<char(25)<<" = Abajo."; ut::gotoxy(65,8); cout<<char(26)<<" = Der."; ut::gotoxy(65,9); cout<<char(27)<<" = Izq."; ut::gotoxy(65,10); cout<<"P = Pausa."; ut::gotoxy(65,11); cout<<"Esc = gameOver."; ut::gotoxy(63,15); cout<<"Creado por:"<<endl; ut::gotoxy(63,16); cout<<"Ing. Greivin Ch."; ut::gotoxy(60,17); cout<<"--------------------"; ut::gotoxy(65,19); ut::setColor(12); cout<<"Pts: "<<Puntos; // ut::setColor(23); ColorArea(); } };
Por último la clase interfaz, la cual brinda los menús principales para iniciar el juego y seleccionar el tipo de escenario a jugar. Existe una relación de agregación mediante la cual la interfaz y los menús pueden interactuar con el juego. Este es el código de la clase:
#include "../Juego/Juego.cpp" #define ARR 72 #define ABJ 80 #define DER 77 #define IZQ 75 #define ENT 13 #define ESC 27 class Interfaz { private: Juego * nuevo; public: /*Constructor de la Interfaz del juego*/ Interfaz() { nuevo=new Juego(); } /*Menu Principal*/ void Menu() { ut::Portada(); int b=1; int men=0; bool salir=false; system("CLS"); while(!salir) { ut::setColor(14); ut::gotoxy(25,2); cout<<"MENU SNAKE"<<endl; cout<<"--------------------------------------------------------------------------------"<<endl; if(b==1) ut::setColor(5); cout<<"1->Juego Nuevo"<<endl<<endl; ut::setColor(14); if(b==2) ut::setColor(5); cout<<"2->Escenario"<<endl<<endl; ut::setColor(14); ut::gotoxy(15,20); cout<<"Utilice las teclas "<<char(24)<<" "<<char(25)<<" press enter para selecionar"; ut::gotoxy(1,23); cout<<"ESC/Salir"<<endl; men=ut::GetKey(); if(men==ARR||men==ABJ||men==ESC||men==ENT) { if(men==ARR) b--; if(men==ABJ) b++; if(men==ESC) { salir= ut::Salir(); } if(men==ENT) { switch (b) { case 1: { system("CLS"); nuevo->MenuInfo(); nuevo->Comienzo(); ut::gotoxy(15,23); system("PAUSE"); system("CLS"); break; } case 2: { MenuEscenario(); system("CLS"); break; } }//fin del switch }//fin del if(men==ENT) } else cout<<'\a'; if(b==0) b=2; if(b==3) b=1; }//fin del while } /*Menu de Escenarios*/ void MenuEscenario() { int b=1; int men=0; system("CLS"); while(men!=27) { ut::setColor(14); ut::gotoxy(25,2); cout<<"MENU ESCENARIO"<<endl; cout<<"--------------------------------------------------------------------------------"<<endl; if(b==1) ut::setColor(5); cout<<'\t'<<'\t'<<"1->Clasico"<<endl<<endl; ut::setColor(14); if(b==2) ut::setColor(5); cout<<'\t'<<'\t'<<"2->Moderno"<<endl<<endl; ut::setColor(14); ut::gotoxy(15,20); cout<<"Utilice las teclas "<<char(24)<<" "<<char(25)<<" press enter para selecionar"; ut::gotoxy(1,23); cout<<"ESC/Volver"<<endl; men=ut::GetKey(); if(men==ARR||men==ABJ||men==ESC||men==ENT) { if(men==ARR) b--; if(men==ABJ) b++; if(men==ENT) { switch (b) { case 1: { nuevo->setEscenario(0); men=27; break; } case 2: { nuevo->setEscenario(1); men=27; break; } } } else cout<<'\a'; if(b==0) b=2; if(b==3) b=1; } } } ~Interfaz() { delete nuevo; } };
Por último el archivo principal del programa o main únicamente
crea un objeto de interfaz y llama al método Menu() , el cual ya se relaciona
con el resto de objetos y clases. Veamos el código:
#include "Vista/Interfaz.cpp" int main() { Interfaz * inter=new Interfaz(); inter->Menu(); delete inter; return 0; }
Este es todo el código del juego, trate de dejarlo lo más comentado, claro y configurable posible por si alguno quiere divertirse un rato y hacerle algunos cambios o implementar funcionalidades extra. De igual forma dejo el proyecto completo el cual está hecho con CodeBlocks por si gusta descargarlo en el siguiente link. Muchas gracias y espero que el tiempo que invertí en publicar esto le sea de provecho a alguno.
No hay comentarios :
Publicar un comentario