Mes: abril 2016
Notas sobre el código en (TANK BATTLE)
Con el fin de facilitar un poco la compresión del código utilizado en el juego os pasaremos unas notas que creemos que os ayudarán.
Máquina de estados: Según mis apuntes «Entendemos por un estado de un videojuego un módulo independiente que realiza una funcionalidad específica y que tiene su propio gestor de eventos, su bucle principal y su engine gráfico para mostrar la información por pantalla» ¿No esta claro?Nota:En la entrada damos un ejemplo de la implementación de un estado en el juego
Alta de un jugador: En este apartado explicamos como crear un jugador nuevo en esta caso player3, creación de los objetos, como especificar la teclas de movimientos, etc.
Control de disparo: ¿Donde se ha visto un juego de disparos en el que no se pueda disparar? Pues eso, en esta caso explicamos los detalle de control de disparos movimiento de los proyectiles, puntuación del jugador y mucho mas.
Creación de niveles: Ultimo punto pero no menos importa te, aquí describiremos como se crea el escenario donde transcurre la partida
Recordad que el juego ha sido desarrollado en C++ y OpenGL. Es código abierto sobre licencia y que esta disponible en githud
Encontrareis puntos expuestos:
Creación de niveles
La estrategia has seguida para que modelar el escenario se podra definir en dos partes principales.
En el primer punto leeremos un fichero (bmp) y buscaremos los pixel de color negro una vez localizados pasaremos esas coordenadas referentes a la imagen y crearemos un objeto del tipo Wall
Finalmente en la creación de este objeto se añadirá a la lista de elementos para el control de colisiones
Creamos dos objetos del tipo BITMAPFILEHEADER BITMAPINFOHEADER
BITMAPFILEHEADER oBMPHeader; BITMAPINFOHEADER oBMPInfo;
Leemos el fichero bmp donde está definido el mapa dl escenario
// read binary file FILE* pFile = fopen("./Images/level003.bmp","rb"); // read both headers fread(&oBMPHeader,sizeof(BITMAPFILEHEADER),1,pFile); fread(&oBMPInfo,sizeof(BITMAPINFOHEADER),1,pFile)
Información de las estructuras con la imagen de ejemplo
oBMPHeader
- bfType 19778
bfSize 632
bfReserved1 0
bfReserved2 0
bfOffBits 54
oBMPInfo
- biSize 40
biWidth 16
biHeight 12
biPlanes 1
biBitCount 24
biCompression 0
biSizeImage 578
biXPelsPerMeter 2834
biYPelsPerMeter 2834
biClrUsed 0
biClrImportant 0
Recorremos los la imagen y por cada pixel de color negro llamamos a createWall el cual se encargara de crean un objeto tipo Wall y será añadido a la lista de elementos para el control de colisiones
Búsqueda de los pixel negros
Pixel24* curPos = aoPixels; for (int y = 0; y < oBMPInfo.biHeight; y++) { for (int x = 0; x < oBMPInfo.biWidth; x++) { //get pixel and save it to array using switch Pixel24 pixel = *curPos; curPos++; int r = (pixel.r&0xff)<<16; int g = (pixel.g&0xff)<<8; int b = (pixel.b&0xff); int p = r|b|g; switch(p) { case 0x00000000: this->createWall(x*64+32, y*64+32, m_Wallfile); break; default: break; } } } fclose(pFile); delete [] aoPixels;
Creación de los obstáculos
Wall* pWall = new Wall(a_filename, a_x, a_y); pWall->SetFaction(FT_WALL); if (m_pkGridIndex != nullptr) m_pkGridIndex->AddNode(pWall);
Control de disparo
Gestión de los disparos
Enumeramos los diferentes jugadores que habrá:
enum FACTION { FT_TANK_GREEN, FT_TANK_RED, FT_MISSILE_GREEN, FT_MISSILE_RED, FT_WALL, };
Con estos identificadores podremos asignar las puntuaciones y validar si los impactos son válidos o no
Control del jugador cuando validamos en los impactos de un misil que no sea del mismo bando. Es decir si misil azul con un tanque azul, etc.
En el caso del misil
// Return FALSE if: Green Missile collide on the Green Tank OR Red Missile collide on the Red Tank else if (a_node2->GetFaction() == FT_TANK_GREEN && m_eFaction == FT_MISSILE_GREEN || a_node2->GetFaction() == FT_TANK_RED && m_eFaction == FT_MISSILE_RED) return false;
En el caso del jugador
// No Collision for Green Tank & Green Missile if (this->GetFaction() == FT_TANK_GREEN && a_node2->GetFaction() == FT_MISSILE_GREEN) return false; // No Collision for Red Tank & Red Missile if (this->GetFaction() == FT_TANK_RED && a_node2->GetFaction() == FT_MISSILE_RED) return false;
En el caso de recibir un impacto mostramos la destrucción del jugador
if (this->GetFaction() == FT_TANK_GREEN) { m_tank->LoadTexture("./Images/tank1_explode.png"); Respawn(900, 400); }
Mostramos al jugador cuando se carga otra vez en las coordenadas de inicio
if (this->GetFaction() == FT_TANK_GREEN) m_tank->LoadTexture("./Images/tank1_body.png"); else m_tank->LoadTexture("./Images/tank2_body.png");
Función donde ser realiza la gestión de impacto de los proyectiles
bool Missile::TestCollide(Node* a_node2) { if (this->GetPosition().isCollidingCircles(a_node2->GetPosition(), m_radius, a_node2->GetRadius() )) { // Missile collide on the Wall for 1st time, so it will bounce back if (a_node2->GetFaction() == FT_WALL && m_collideCount == 0) { if (m_worldTrans.GetRotation(1) == 0) { // Robound 180 degree for top side wall this->Rotate(180); } else if (this->prevpos.m_X < a_node2->GetPosition().GetX() && this->prevpos.m_Y >= a_node2->GetPosition().GetY() - a_node2->GetRadius() && this->prevpos.m_Y <= a_node2->GetPosition().GetY() + a_node2->GetRadius()) { // Working OK for right side wall float t_Rotation = m_worldTrans.GetRotation(1); this->Rotate(360 - t_Rotation * -2.0f); } else if (this->prevpos.m_X > a_node2->GetPosition().GetX() && this->prevpos.m_Y >= a_node2->GetPosition().GetY() - a_node2->GetRadius() && this->prevpos.m_Y <= a_node2->GetPosition().GetY() + a_node2->GetRadius()) { // Working OK for left side wall float t_Rotation = m_worldTrans.GetRotation(1); this->Rotate(360 - t_Rotation * 2.0f); } else if (this->prevpos.m_Y > a_node2->GetPosition().GetY()) { // Working OK for upper wall float t_Rotation = m_worldTrans.GetRotation(2); this->Rotate(180 - t_Rotation * 2.0f); } else { // Working OK for lower wall float t_Rotation = m_worldTrans.GetRotation(2); this->Rotate(180 + t_Rotation * 2.0f); } m_collideCount = m_collideCount + 1; //Increment count of Collision return false; } // Return FALSE if: Green Missile collide on the Green Tank OR Red Missile collide on the Red Tank else if (a_node2->GetFaction() == FT_TANK_GREEN && m_eFaction == FT_MISSILE_GREEN || a_node2->GetFaction() == FT_TANK_RED && m_eFaction == FT_MISSILE_RED) return false; else return true; //Otherwise, return TRUE for other conditions } else return false; }
Fragmento de código encargado de la creación de los proyectiles
//Missile initial velocity Vector3 missileVelocity; missileVelocity.m_X = 0.0f; missileVelocity.m_Y = -5.0f; missileVelocity.m_Z = 0.0f; Matrix3x3 m; m.Identity(); m.SetTranslation(0, -35); Matrix3x3 missileWorld = this->m_worldTrans * m; if (m_eFaction == FT_TANK_GREEN) GameController::createMissile(missileWorld, missileVelocity, FT_MISSILE_GREEN); else GameController::createMissile(missileWorld, missileVelocity, FT_MISSILE_RED);
Para el pintado de los proyectiles seguirá el mismo funcionamiento que el resto de elementos de juego desde DrawGrid se llamara a Draw() que finamente llamara a la misma función de Sprite
Alta de un jugador
Pasos seguidos en la creación de un tercer jugador:
Declaramos el objeto player en GameController
Tank* m_player1; Tank* m_player2;
Añadimos nuevos valores e la enumeración FACTION en Node
enum FACTION { FT_TANK_GREEN, FT_TANK_RED, FT_MISSILE_GREEN, FT_MISSILE_RED, FT_WALL, };
En la función LoadAssets llamamos al constructor de Tank al cual enviamos las imágenes necesarias para Sprite así como las coordenadas de creación por
defecto y la facción a la cual pertenecerá el jugador
m_player1 = new Tank("./Images/tank1_body.png", "./Images/tank1_turret.png", 900, 400, FT_TANK_GREEN); m_player2 = new Tank("./Images/tank2_body.png","./Images/tank2_turret.png", 125, 400, FT_TANK_RED);
Añadimos el objeto a la lista de elementos a tener en cuenta cuando se realice el cálculo de colisiones
m_pkGridIndex->AddNode(m_player1); m_pkGridIndex->AddNode(m_player2);
Añadimos el destructor para liberar memoria
if( m_player1 != NULL ) { //TODO free player tank and turret sprites delete m_player1; }
Una vez iniciado el estado de GAME procedemos a la asignación de los controles del jugador
Player 1
- Desplazarse hacia delante Flecha arriba
- Desplazarse hacia atrás Flecha abajo
- Girar derecha Flecha derecha
- Girar izquierda Flecha izquierda
- Disparar misil Enter
- Girar la torreta a derecha Tecla Control derecho
- Girar la torreta a izquierda Tecla Mayús> Derecho
/***********************************************************/ // Player 1 Key Control /***********************************************************/ //If RIGHT KEY PRESSED if(m_keyboard->GetKeyStates(SDLK_RIGHT) == KEY_HELD) m_player1->Rotate(-50.0f * dT); //If LEFT KEY PRESSED if(m_keyboard->GetKeyStates(SDLK_LEFT) == KEY_HELD) m_player1->Rotate(50.0f * dT); //If UP KEY PRESSED if(m_keyboard->GetKeyStates(SDLK_UP) == KEY_HELD) { //moving forward m_player1->m_acc.m_X += 0.0f; m_player1->m_acc.m_Y -= 2.0f; if(m_player1->m_acc.m_Y < -100.0f) m_player1->m_acc.m_Y = -100.0f; m_player1->Translate(0.0f, m_player1->m_acc.m_Y * dT); } else if (m_player1->m_acc.m_Y < 0) { m_player1->m_acc.m_Y += 2.0f; if(m_player1->m_acc.m_Y > 0) m_player1->m_acc.m_Y = 0.0f; m_player1->Translate(0.0f, m_player1->m_acc.m_Y * dT); } //If DOWN KEY PRESSED if(m_keyboard->GetKeyStates(SDLK_DOWN) == KEY_HELD) { //moving backward m_player1->m_acc.m_X += 0.0f; m_player1->m_acc.m_Y += 2.0f; if(m_player1->m_acc.m_Y > 100.0f) m_player1->m_acc.m_Y = 100.0f; m_player1->Translate(0.0f, m_player1->m_acc.m_Y * dT); } else if (m_player1->m_acc.m_Y > 0) { m_player1->m_acc.m_Y -= 2.0f; if(m_player1->m_acc.m_Y < 0) m_player1->m_acc.m_Y = 0.0f; m_player1->Translate(0.0f, m_player1->m_acc.m_Y * dT); } //If RIGHT SHIFT KEY PRESSED if(m_keyboard->GetKeyStates(SDLK_RSHIFT) == KEY_HELD) { m_player1->RotateTurret(50.0f * dT); } //If RIGHT CTRL KEY PRESSED if(m_keyboard->GetKeyStates(SDLK_RCTRL) == KEY_HELD) { m_player1->RotateTurret(-50.0f * dT); } //If RETURN KEY PRESSED if(m_keyboard->GetKeyStates(SDLK_RETURN) == KEY_HELD) { m_player1->Shoot(); //Fire Missile }
Desde la función Draw mostraremos la puntuación del jugador
if (m_player1->m_szProfileName == "") { tmpScore = "Tank 1: "; tmpScore = tmpScore.append(itoa(m_player1->m_score, tmpNum, 10)); } else tmpScore = m_player1->m_szProfileName + ": " + itoa(m_player1->m_score, tmpNum, 10); //Draw Player1 Score m_textfont->DrawTxt(tmpScore, 75, 70, 20, 30, 1);
Añadimos al control de colisiones al tercer jugador :
if ((nd2->GetFaction() == FT_MISSILE_GREEN || nd2->GetFaction() == FT_MISSILE_RED) && nd2->m_timer == 0 || (nd2->GetFaction() == FT_MISSILE_GREEN || nd2->GetFaction() == FT_MISSILE_RED) && (nd1->GetFaction() == FT_TANK_GREEN || nd1->GetFaction() == FT_TANK_RED)) { TNode::iterator it = AllNodes.begin(); while(it != AllNodes.end()) { if(nd2 == *it) { AllNodes.erase(it); tn1[j] = NULL; if (nd2->GetFaction() != FT_MISSILE_GREEN && nd2->GetFaction() != FT_MISSILE_RED) delete nd2; break; } it++; } }
Creamos los objetos misiles de la nueva facción
if (m_eFaction == FT_TANK_GREEN) GameController::createMissile(missileWorld, missileVelocity, FT_MISSILE_GREEN); else GameController::createMissile(missileWorld, missileVelocity, FT_MISSILE_RED);
Añadimos también el control del tercer jugador cuando validamos en los impactos de un misil que no sea del mismo bando.
Es decir si misil azul con un tanque azul, etc.
En el caso del misil:
// Return FALSE if: Green Missile collide on the Green Tank OR Red Missile collide on the Red Tank else if (a_node2->GetFaction() == FT_TANK_GREEN && m_eFaction == FT_MISSILE_GREEN || a_node2->GetFaction() == FT_TANK_RED && m_eFaction == FT_MISSILE_RED) return false; else return true; //Otherwise, return TRUE for other conditions
En el caso del jugador:
// No Collision for Green Tank & Green Missile if (this->GetFaction() == FT_TANK_GREEN && a_node2->GetFaction() == FT_MISSILE_GREEN) return false;
Mostramos al jugador cuando se carga otra vez en las coordenadas de inicio
if (this->GetFaction() == FT_TANK_GREEN) m_tank->LoadTexture("./Images/tank1_body.png"); else m_tank->LoadTexture("./Images/tank2_body.png");
Máquina de estados
La mejor forma para ver el funcionamiento de la máquina de estados es con un ejemplo, en nuestro caso será el estado «SPLASH» en el cual aparece el logo des estudio Pepes Games.
Para añadir una ventana con el logotipo del estudio crearemos un nuevo objeto del tipo Sprite y un nuevo estado en el que se mostrara la imagen y cargará el menú principal al presionar ESC
enum State { STARTUP, SPLASH, MAIN_MENU, GAME, HIGHSCORE, SELECT_PROFILE, CREATE_PROFILE1, CREATE_PROFILE2, EXIT = -1 };
Y un objeto del tipo Sprite En el constructor de GameController asignamos g_ g_NextGameState el estado de SPLASH
g_NextGameState = SPLASH;
En la función Update capturamos el nuevo estado y definimos que en el caso de pulsar ‘ESC’ por el jugador el estado pasará a MAIN_MENU
else if (g_GameState == SPLASH) { //If ESCAPE KEY PRESSED int inKey = m_keyboard->DetectOneKeyReleased(SDLK_ESCAPE); if (inKey != 0) g_NextGameState = MAIN_MENU; }
En la función Draw realizaremos el dibujo del objeto m_splashscreen y un texto con
el mensaje presione escape cuando el estado sea SPLASH
//Draw splash screen else if (g_GameState == SPLASH) { m_splashscreen->DrawImage(280, 150, 500, 400); m_textfont->DrawTxt("Press 'Esc'", 400, 700, 20, 30, 1); }
Declaración del objeto
Sprite* m_splashscreen;
En la función LoadAssets Cargamos la imagen y construimos el objeto
m_splashscreen = new Sprite("./Images/PepeLogo.png");

TANK BATTLE
Sangre y acero en un proyecto de código abierto desarrollado en C++ y OpenGL, pensado para todo aquel que quiera iniciarse en el mundo del desarrollo videojuegos.
Puedes descargarlo con GitHud, te recomendamos Windows y con el IDE Visual Studio
Antes de empezar a trabajar con motores de videojuegos tales como Unity 3D o UDK , te recomendamos que bajes a las trincheras del C++ y OpenGL ¿Preparado?
¿Cómo Jugar?
Cada jugador controla un tanque por el mapa con el objetivo de destruir el tanque del contrario.
El tanque del jugador puede ser destruido por un golpe directo o golpe indirecto de un misil. Un impacto directo es
el misil de disparar desde el tanque sin ninguna acción de rebote mientras que golpe indirecto es el misil de disparar
desde el tanque y luego volver a rebotar en la pared. Sin embargo, el misil va a explotar después de recorrer una
distancia determinada a pesar de que no puede golpear cualquier objetivo
Todo el código fuente del juego esta disponible «Y compila!!!!» os lo prometemos a través de github
