Es/Adding the Game Instructor

right|450px|thumb|El "Game Instructor" permite mostrar instrucciones mientras se juega.
El Game Instructor en breves palabras es un sistema ubicado en el Cliente enfocado para mostrar instrucciones sobre como jugar o realizar ciertas acciones mientras al mismo tiempo se juega. El mismo apareció por primera vez en el Branch de (o hasta donde imagino, puedo estar equivocado) y actualmente es usado por los juegos de .
En este articulo veremos a como incorporarlo dentro del Multiplayer ( No he probado si funciona dentro del Branch para Singleplayer o en Source SDK 2007 ). El código que usaremos es proveniente del código fuente de Alien Swarm con algunas modificaciones para hacerlo funcionar dentro del Source SDK 2013.
== Requisitos ==
* Un Mod basado en el 
* Conocimientos en C++
== CLocatorTarget ==
La clase CLocatorTarget de los archivos hud_locator_target.cpp/hud_locator_target.h es la encargada de mostrar los mensajes en la pantalla del jugador ( Cliente ) y ( en caso de dirigirse a un objeto, por ejemplo un botón ) de mover el mismo hacia la ubicación del objeto a usar ( De ahí proviene su nombre ). Así que para empezar necesitaremos incorporar los siguientes 2 archivos dentro del proyecto del Cliente:
== UtlSymbol ==
Ahora es necesario editar el archivo utlsymbol.h que se encuentra en public/tier1/utlsymbol.h, una vez que hayamos abierto el archivo eliminamos TODO su contenido y pegamos el nuevo código:
Para ser honesto no entiendo mucho acerca de este archivo y para que sirven exactamente las actualizaciones hechas al mismo, sin embargo son necesarios para hud_locator_target.
== C_GameInstructor y CBaseLesson ==
Ahora pasaremos al sistema real del Game Instructor, este se encarga de leer 2 archivos ( scripts/instructor_lessons.txt y scripts/mod_lessons.txt ) que son los responsables de tener las "lecciones por aprender" que básicamente son los eventos que el Game Instructor puede mostrar en la pantalla junto a las opciones que van desde el icono que aparecerá junto al mensaje hasta cuando se marcará como "lección aprendida" o "lección vista".
Se puede decir que es como un tipo de lenguaje que nos permite crear lecciones personalizadas que pueden "aprenderse" o sencillamente cerrarse cuando cierta condición se cumple dentro del código, algo complicado de entender al principio pero veremos una introducción de como crearlos más adelante, por ahora pasemos al código de este sistema:
=== CBaseLesson ===
En resumen es la clase encargada de contener toda la información acerca de cada lección que puede aparecer con el Game Instructor. Tendremos que agregar estos 2 archivos igual en el Cliente:
=== C_GameInstructor ===
Y ahora si, esta clase es el Game Instructor encargada de leer los archivos que contienen las lecciones por aprender, guardarlas, actualizar la información, mostrarlas, etc... Al igual que antes, debemos agregar estos 2 archivos en el Cliente:
== Arreglando problemas ==
El Game Instructor ya ha sido incorporado sin embargo es necesario ahora arreglar ciertos problemas con los demás archivos.
=== UTIL_CountNumBitsSet ===
Esta función es necesaria para el funcionamiento de las lecciones y debería estar en el archivo game/shared/util_shared.cpp por lo que abriremos tal archivo y buscaremos la función UTIL_StringFieldToInt aproximadamente en la línea 1089:
int UTIL_StringFieldToInt( const char *szValue, const char **pValueStrings, int iNumStrings )
{
	if ( !szValue || !szValue[0] )
		return -1;
	for ( int i = 0; i < iNumStrings; i++ )
	{
		if ( FStrEq(szValue, pValueStrings[i]) )
			return i;
	}
	Assert(0);
	return -1;
}
Y justo debajo de ella incorporaremos este código:
static char s_NumBitsInNibble[ 16 ] = 
{
	0, // 0000 = 0
	1, // 0001 = 1
	1, // 0010 = 2
	2, // 0011 = 3
	1, // 0100 = 4
	2, // 0101 = 5
	2, // 0110 = 6
	3, // 0111 = 7
	1, // 1000 = 8
	2, // 1001 = 9
	2, // 1010 = 10
	3, // 1011 = 11
	2, // 1100 = 12
	3, // 1101 = 13
	3, // 1110 = 14
	4, // 1111 = 15
};
int UTIL_CountNumBitsSet( unsigned int nVar )
{
	int nNumBits = 0;
	while ( nVar > 0 )
	{
		// Look up and add in bits in the bottom nibble
		nNumBits += s_NumBitsInNibble[ nVar & 0x0f ];
		// Shift one nibble to the right
		nVar >>= 4;
	}
	return nNumBits;
}
int UTIL_CountNumBitsSet( uint64 nVar )
{
	int nNumBits = 0;
	while ( nVar > 0 )
	{
		// Look up and add in bits in the bottom nibble
		nNumBits += s_NumBitsInNibble[ nVar & 0x0f ];
		// Shift one nibble to the right
		nVar >>= 4;
	}
	return nNumBits;
}
Y ahora como es lógico definiremos esas 2 funciones dentro de game/shared/util_shared.h, como antes buscaremos la definición de la función UTIL_StringFieldToInt que se encuentra aproximadamente en la línea 587:
int UTIL_StringFieldToInt( const char *szValue, const char **pValueStrings, int iNumStrings );
Y justo debajo de ella agregamos:
int UTIL_CountNumBitsSet( unsigned int nVar );
int UTIL_CountNumBitsSet( uint64 nVar );
=== GetPlayerName ===
Esta función como bien dice, sirve para obtener el nombre de un jugador, sin embargo en el Source SDK 2013 la misma solo es accesible desde CBasePlayer mientras que el Game Instructor lo requiere en CBaseEntity. Por lo que abriremos el archivo game/client/c_baseentity.h y deberemos agregar esta función en alguna sección accesible públicamente "public:", en mi caso la he puesto debajo de GetDebugName que se encuentra aproximadamente en la línea 860:
char const						*GetDebugName( void );
Ahora debajo de ella agregamos:
virtual const char				*GetPlayerName() const { return NULL; }
=== CHudIcons ===
Esta clase es la encargada de mostrar los iconos que se encuentran al lado del mensaje cuando una lección aparece en la pantalla y la misma no existe en el Source SDK 2013. Para agregarla deberemos abrir el archivo game/client/hud.h y aproximadamente en la línea 195 encontraremos lo siguiente:
extern CHud gHUD;
Ahora debajo de ella agregamos la clase:
//-----------------------------------------------------------------------------
// Purpose: CHudIcons
//-----------------------------------------------------------------------------
class CHudIcons
{
public:
	CHudIcons();
	~CHudIcons();
	void						Init();
	void						Shutdown();
	CHudTexture					*GetIcon( const char *szIcon );
	// loads a new icon into the list, without duplicates
	CHudTexture					*AddUnsearchableHudIconToList( CHudTexture& texture );
	CHudTexture					*AddSearchableHudIconToList( CHudTexture& texture );
	void						RefreshHudTextures();
private:
	void						SetupNewHudTexture( CHudTexture *t );
	bool						m_bHudTexturesLoaded;
	// Global list of known icons
	CUtlDict< CHudTexture *, int >		m_Icons;
};
CHudIcons &HudIcons();
Ahora abrimos el archivo game/client/hud.cpp y al final de todo el archivo es decir en la línea 1200 agregamos lo siguiente:
CHudIcons::CHudIcons() :
	m_bHudTexturesLoaded( false )
{
}
CHudIcons::~CHudIcons()
{
	int c = m_Icons.Count();
	for ( int i = c - 1; i >= 0; i-- )
	{
		CHudTexture *tex = m_Icons[ i ];
		g_HudTextureMemoryPool.Free( tex );
	}
	m_Icons.Purge();
}
void CHudIcons::Init()
{
	if ( m_bHudTexturesLoaded )
		return;
	m_bHudTexturesLoaded = true;
	CUtlDict< CHudTexture *, int >	textureList;
	// check to see if we have sprites for this res; if not, step down
	LoadHudTextures( textureList, "scripts/hud_textures", NULL );
	LoadHudTextures( textureList, "scripts/mod_textures", NULL );
	LoadHudTextures( textureList, "scripts/instructor_textures", NULL );
	LoadHudTextures( textureList, "scripts/instructor_modtextures", NULL );
	int c = textureList.Count();
	for ( int index = 0; index < c; index++ )
	{
		CHudTexture* tex = textureList[ index ];
		AddSearchableHudIconToList( *tex );
	}
	FreeHudTextureList( textureList );
}
void CHudIcons::Shutdown()
{
	m_bHudTexturesLoaded = false;
}
//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
CHudTexture *CHudIcons::AddUnsearchableHudIconToList( CHudTexture& texture )
{
	// These names are composed based on the texture file name
	char composedName[ 512 ];
	if ( texture.bRenderUsingFont )
	{
		Q_snprintf( composedName, sizeof( composedName ), "%s_c%i",
			texture.szTextureFile, texture.cCharacterInFont );
	}
	else
	{
		Q_snprintf( composedName, sizeof( composedName ), "%s_%i_%i_%i_%i",
			texture.szTextureFile, texture.rc.left, texture.rc.top, texture.rc.right, texture.rc.bottom );
	}
	CHudTexture *icon = GetIcon( composedName );
	if ( icon )
	{
		return icon;
	}
	CHudTexture *newTexture = ( CHudTexture * )g_HudTextureMemoryPool.Alloc();
	*newTexture = texture;
	SetupNewHudTexture( newTexture );
	int idx = m_Icons.Insert( composedName, newTexture );
	return m_Icons[ idx ];
}
//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
CHudTexture *CHudIcons::AddSearchableHudIconToList( CHudTexture& texture )
{
	CHudTexture *icon = GetIcon( texture.szShortName );
	if ( icon )
	{
		return icon;
	}
	CHudTexture *newTexture = ( CHudTexture * )g_HudTextureMemoryPool.Alloc();
	*newTexture = texture;
	SetupNewHudTexture( newTexture );
	int idx = m_Icons.Insert( texture.szShortName, newTexture );
	return m_Icons[ idx ];
}
//-----------------------------------------------------------------------------
// Purpose: returns a pointer to an icon in the list
//-----------------------------------------------------------------------------
CHudTexture *CHudIcons::GetIcon( const char *szIcon )
{
	int i = m_Icons.Find( szIcon );
	if ( i == m_Icons.InvalidIndex() )
		return NULL;
	return m_Icons[ i ];
}
//-----------------------------------------------------------------------------
// Purpose: Gets texture handles for the hud icon
//-----------------------------------------------------------------------------
void CHudIcons::SetupNewHudTexture( CHudTexture *t )
{
	if ( t->bRenderUsingFont )
	{
		vgui::HScheme scheme = vgui::scheme()->GetScheme( "ClientScheme" );
		t->hFont = vgui::scheme()->GetIScheme(scheme)->GetFont( t->szTextureFile, true );
		t->rc.top = 0;
		t->rc.left = 0;
		t->rc.right = vgui::surface()->GetCharacterWidth( t->hFont, t->cCharacterInFont );
		t->rc.bottom = vgui::surface()->GetFontTall( t->hFont );
	}
	else
	{
		// Set up texture id and texture coordinates
		t->textureId = vgui::surface()->CreateNewTextureID();
		vgui::surface()->DrawSetTextureFile( t->textureId, t->szTextureFile, false, false );
		int wide, tall;
		vgui::surface()->DrawGetTextureSize( t->textureId, wide, tall );
		t->texCoords[ 0 ] = (float)(t->rc.left + 0.5f) / (float)wide;
		t->texCoords[ 1 ] = (float)(t->rc.top + 0.5f) / (float)tall;
		t->texCoords[ 2 ] = (float)(t->rc.right - 0.5f) / (float)wide;
		t->texCoords[ 3 ] = (float)(t->rc.bottom - 0.5f) / (float)tall;
	}
}
//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CHudIcons::RefreshHudTextures()
{
	if ( !m_bHudTexturesLoaded )
	{
		Assert( 0 );
		return;
	}
	CUtlDict< CHudTexture *, int >	textureList;
	// check to see if we have sprites for this res; if not, step down
	LoadHudTextures( textureList, "scripts/hud_textures", NULL );
	LoadHudTextures( textureList, "scripts/mod_textures", NULL );
	LoadHudTextures( textureList, "scripts/instructor_textures", NULL );
	// fix up all the texture icons first
	int c = textureList.Count();
	for ( int index = 0; index < c; index++ )
	{
		CHudTexture *tex = textureList[ index ];
		Assert( tex );
		CHudTexture *icon = GetIcon( tex->szShortName );
		if ( !icon )
			continue;
		// Update file
		Q_strncpy( icon->szTextureFile, tex->szTextureFile, sizeof( icon->szTextureFile ) );
		if ( !icon->bRenderUsingFont )
		{
			// Update subrect
			icon->rc = tex->rc;
			// Keep existing texture id, but now update texture file and texture coordinates
			vgui::surface()->DrawSetTextureFile( icon->textureId, icon->szTextureFile, false, false );
			// Get new texture dimensions in case it changed
			int wide, tall;
			vgui::surface()->DrawGetTextureSize( icon->textureId, wide, tall );
			// Assign coords
			icon->texCoords[ 0 ] = (float)(icon->rc.left + 0.5f) / (float)wide;
			icon->texCoords[ 1 ] = (float)(icon->rc.top + 0.5f) / (float)tall;
			icon->texCoords[ 2 ] = (float)(icon->rc.right - 0.5f) / (float)wide;
			icon->texCoords[ 3 ] = (float)(icon->rc.bottom - 0.5f) / (float)tall;
		}
	}
	FreeHudTextureList( textureList );
	// fixup all the font icons
	vgui::HScheme scheme = vgui::scheme()->GetScheme( "ClientScheme" );
	for (int i = m_Icons.First(); m_Icons.IsValidIndex(i); i = m_Icons.Next(i))
	{
		CHudTexture *icon = m_Icons[i];
		if ( !icon )
			continue;
		// Update file
		if ( icon->bRenderUsingFont )
		{
			icon->hFont = vgui::scheme()->GetIScheme(scheme)->GetFont( icon->szTextureFile, true );
			icon->rc.top = 0;
			icon->rc.left = 0;
			icon->rc.right = vgui::surface()->GetCharacterWidth( icon->hFont, icon->cCharacterInFont );
			icon->rc.bottom = vgui::surface()->GetFontTall( icon->hFont );
		}
	}
}
static CHudIcons g_HudIcons;
CHudIcons &HudIcons()
{
	return g_HudIcons;
}
Como se puede ver en el código anterior los iconos se definirán en los archivos: scripts/hud_textures.txt, scripts/mod_textures.txt y scripts/instructor_textures.txt
Aunque eso no es todo, ya hemos agregado la clase pero ahora es necesario iniciarla/prepararla, para ello en ese mismo archivo ( hud.cpp ) buscaremos la función CHud::Init que se encuentra aproximadamente en la línea 395 y al final de la función ( Debajo de "FreeHudTextureList( textureList );" ) agregamos lo siguiente:
HudIcons().Init();
=== GetColor ===
El código para los comandos de consola "ConVar" tiene una nueva función llamada GetColor la cual como su nombre lo indica sirve para obtener directamente del valor de un comando convertido en Color y es necesario para el Game Instructor. Así que abriremos el archivo public/tier1/convar.h y aproximadamente en la línea 352 encontraremos lo siguiente:
FORCEINLINE_CVAR int			GetInt( void ) const;
Y justo debajo agregamos:
FORCEINLINE_CVAR Color			GetColor( void ) const;
Ahora en ese mismo archivo y aproximadamente en la línea 430 encontraremos lo siguiente:
//-----------------------------------------------------------------------------
// Purpose: Return ConVar value as an int
// Output : int
//-----------------------------------------------------------------------------
FORCEINLINE_CVAR int ConVar::GetInt( void ) const 
{
	return m_pParent->m_nValue;
}
Justo debajo agregamos:
//-----------------------------------------------------------------------------
// Purpose: Return ConVar value as a color
// Output : Color
//-----------------------------------------------------------------------------
FORCEINLINE_CVAR Color ConVar::GetColor( void ) const 
{
	unsigned char *pColorElement = ((unsigned char *)&m_pParent->m_nValue);
	return Color( pColorElement[0], pColorElement[1], pColorElement[2], pColorElement[3] );
}
Nota: Ten en cuenta que "GetColor" solo funcionaría en 'ConVar', hacerlo funcionar en 'ConVarRef' tendría que ser el mismo proceso ( Aunque no es necesario para el Game Instructor )
¡Oh! Y casi lo olvido, es necesario agregar la definición de Color, para ello en el mismo archivo solo agrega:
#include "color.h"
Al inicio, justo debajo de los demás "#include"
¡Y eso es todo! Oh bueno al menos para el Game Instructor. Si compilas tu Mod ahora solo obtendrías acceso a los comandos y a los mensajes en la consola generados por el mismo. Ahora hay que prepararlo para que empiece a funcionar...
== Preparando al Game Instructor ==
Lo primero que debemos hacer es importar las texturas y ciertos archivos principales para que el Game Instructor pueda funcionar correctamente, para facilitar la tarea he creado un pequeño .zip que contiene estos archivos:
http://www.mediafire.com/download/8rm0xiyb3xp2lr0/GameInstructor.zip
Mirror: https://mega.co.nz/#!XQFVCBaJ!CmtqpjyjKziWgrYaJXygQHlH8g1lSzT59YC4RVZLNw8
Mirror: http://raegquit.com/beerdude26/GameInstructor(2).zip
Es un archivo pequeño ( 144 KB ) pero para los desconfiados...
https://www.virustotal.com/en/file/fce648a7ca6ce0933243f231d36a258f561228fd15fc47063a7e056c7d86b35f/analysis/1379772975/
Aquí explico el contenido de los archivos/carpetas:
materials/
Contiene las texturas de los iconos que aparecerán al lado del mensaje.
resource/modevents.res
Contiene los eventos que pueden ser llamados del Servidor al Cliente. Al agregar una nueva lección será necesario agregar su respectivo evento aquí o de lo contrario no funcionara. ( Contiene los eventos principales para el Game Instructor )
scripts/instructor_lessons.txt
Yep, este será el archivo que contendrá todas las lecciones que podemos hacer aparecer con el Game Instructor. También es válido usar el archivo mod_lessons.txt ( Creandolo )
scripts/mod_textures.txt
Contiene los nombres para los iconos y la textura que usará. Desde aquí puedes agregar nuevos iconos (Claro, si ya has hecho su textura)
Nota: Algunos iconos del archivo provienen de .
Nota 2: Algunos archivos como "mod_textures" y "mod_events" podrían ya existir en la carpeta de tu Mod, si es así te recomiendo "fusionar" el contenido original con el contenido del .zip. Reemplazar el archivo hará que algunas funciones dejen de funcionar.
=== Fuentes ===
Antes de crear una lección es necesario configurar las Fuentes, es decir, el tipo de letra que se usará en los mensajes y a la hora de mostrar teclas como iconos. Estos no se incluyeron en el .zip ya que es muy posible que hayas editado el archivo resource/ClientScheme.res. Así que abre el archivo mencionado y busca la sección que tiene algo similar a esto:
 //////////////////////// FONTS /////////////////////////////
	//
	// describes all the fonts
	Fonts
	{
Y justo debajo pega lo siguiente:
 "InstructorTitle"
		{
			"1"
			{
				"name"			"Arial"
				"tall"			"20"
				"weight"		"400"
				"antialias"		"1"
				"dropshadow"	"1"
			}
		}
		"InstructorKeyBindings"
		{
			"1"
			{
				"name"			"Arial"
				"name"			"Trade Gothic Bold"
				"tall"			"18"
				"weight"		"600"
				"antialias"		"1"
				"dropshadow"	"0"
			}
		}
		"InstructorButtons"
		{
			"1"
			{
				"name"			"Arial"
				"name"			"Trade Gothic Bold"
				"tall"			"15"
				"weight"		"600"
				"antialias"		"1"
				"dropshadow"	"1"
			}
		}
		"InstructorTitleGlow"
		{
			"1"
			{
				"name"			"Arial"
				"name"			"Trade Gothic Bold"
				"tall"			"20"
				"weight"		"400"
				"antialias"		"1"
				"dropshadow"	"1"
			}
		}
Como puedes ver la he ajustado para que use el tipo de letra "Arial" que básicamente es la más común en todo el universo, puedes cambiarla si lo quieres.
Rápidamente para lo que sirven:
InstructorTitle
Este sera el tipo de letra que se usará en los mensajes.
InstructorKeyBindings
Este sera el tipo de letra que se usará en las teclas que aparecerán como iconos. ( Véase la imagen del inicio )
InstructorButtons
InstructorTitleGlow
En si indica el tipo de letra para un mensaje "brillante", sin embargo parece que no funciona... Se supone que se activa con el comando locator_text_glow
== Creando lecciones ==
== Incorporando las lecciones en el código ==
Como dijimos anteriormente para hacer que una lección se abra ( se muestre ) es necesario enviar el evento que registramos para el mismo desde el código, para ello y tomando como referencia la lección de Introducción "Usando tu arma" haremos lo siguiente:
=== "Usando tu arma" ===
Esta lección le muestra al usuario a como disparar un arma por lo que lo lógico sería que se mostrará cuando el mismo obtiene un arma, para ello hay que dirigirnos al archivo game/server/hl2mp/hl2mp_player.cpp ( Así es, dejamos Cliente y ahora entramos a Server ) y ubicaremos la función:
void CHL2MP_Player::Spawn(void)
¡Nota!: Como sabemos, en Half-Life 2: Deathmatch los jugadores obtienen armas al empezar, como ejemplo mostraremos la lección en la función "Spawn" después de que el jugador obtiene las armas básicas. Si tu MOD no empieza con armas al empezar, un mejor lugar para hacer esto sería en la función "Weapon_Equip" ( Hay que crearla )
Al final de la función escribiremos:
IGameEvent *pEvent = gameeventmanager->CreateEvent("instructor_primaryattack");
if ( pEvent )
{
	pEvent->SetInt("userid", GetUserID());
	gameeventmanager->FireEvent(pEvent);
}
Si compilamos nuestro Mod podremos ver por primera vez el Game Instructor en acción:
http://www.youtube.com/watch?v=pGMZyv3_5b4&hd=1