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