Documentación de Steamworks
Paso a paso: logros

Introducción

La siguiente es una guía rápida paso a paso para integrar logros muy básicos de Steam en tu aplicación en menos de 10 minutos y con menos de 10 líneas de código integradas en tu código base. El SDK de Steamworks contiene una magnífica aplicación de ejemplo, Spacewar], que no solo exhibe el espectro completo de funciones de Steam, sino que también debería ser el punto de partida para todo desarrollador que desee verlas en acción. Este tutorial resume la información que se encuentra en Spacewar y en la API de estadísticas y logros, condensándola en lo que es estrictamente imprescindible para las estadísticas de Steam, sin mayores complicaciones. Ten en cuenta que hay una cantidad considerable de solapamiento entre estadísticas y logros, de forma que, si se están integrando ambos, es necesario ser consciente de que pueden fusionarse multitud de llamadas.

Paso 1: definir los logros del juego

Los logros son específicos de la aplicación y se configuran en la página Configuración de logros en el backend del administrador de la aplicación Steamworks. Lo siguiente es la lista de logros de la aplicación de muestra Steamworks Spacewar:
spacewar_achievements.png

Paso 2: descripción de la integración de logros

El código que sigue no depende de ningún juego y los desarrolladores pueden agregarlo al suyo a su propia discreción. La clase es perfectamente funcional tal cual, pero se puede ampliar fácilmente para dar respuesta a cualquier necesidad a mayores. Todo este código se ha tomado directamente de los archivos de ejemplo de Spacewar StatsAndAchievements.cpp/h.

Archivo de encabezado

Primero definimos una estructura para contener los datos de logros recibidos de Steam y suministramos una macro para crear objetos de ese tipo. Estos datos se asignan directamente a lo que está disponible en la páginaConfiguración de logros.
#define _ACH_ID( id, name ) { id, #id, name, "", 0, 0 } struct Achievement_t { int m_eAchievementID; const char *m_pchAchievementID; char m_rgchName[128]; char m_rgchDescription[256]; bool m_bAchieved; int m_iIconImage; };

A continuación, definimos una clase auxiliar que encapsulará todas las llamadas a la API de estadísticas de Steam, además de crear todas las devoluciones de llamada de Steam.
class CSteamAchievements { private: int64 m_iAppID; // Nuestro AppID actual Achievement_t *m_pAchievements; // Datos de logros int m_iNumAchievements; // El número de logros bool m_bInitialized; // ¿Hemos hecho la llamada de petición de estadísticas y recibido la devolución de llamada? public: CSteamAchievements(Achievement_t *Achievements, int NumAchievements); ~CSteamAchievements(); bool RequestStats(); bool SetAchievement(const char* ID); STEAM_CALLBACK( CSteamAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived ); STEAM_CALLBACK( CSteamAchievements, OnUserStatsStored, UserStatsStored_t, m_CallbackUserStatsStored ); STEAM_CALLBACK( CSteamAchievements, OnAchievementStored, UserAchievementStored_t, m_CallbackAchievementStored ); };

Archivo de código

Constructor

Parámetros El constructor toma un puntero a un vector de logros junto con la longitud del mismo. El formateado de ese vector será abordado más adelante, en el código principal del juego.
Devoluciones - N/D
Lo que hace: el constructor inicializa un gran número de miembros, además de hacerse con el id. de aplicación con el que operamos actualmente. Además, enlaza los métodos de devolución de llamada para gestionar llamadas asíncronas a Steam. Finalmente, realiza una llamada inicial a RequestStats() para obtener estadísticas y logros para el usuario actual.
CSteamAchievements::CSteamAchievements(Achievement_t *Achievements, int NumAchievements): m_iAppID( 0 ), m_bInitialized( false ), m_CallbackUserStatsReceived( this, &CSteamAchievements::OnUserStatsReceived ), m_CallbackUserStatsStored( this, &CSteamAchievements::OnUserStatsStored ), m_CallbackAchievementStored( this, &CSteamAchievements::OnAchievementStored ) { m_iAppID = SteamUtils()->GetAppID(); m_pAchievements = Achievements; m_iNumAchievements = NumAchievements; RequestStats(); }

RequestStats()

Parámetros - No
Devoluciones un resultado booleano que representa si la llamada se completó. Si la llamada dio error, lo más probable es que no se haya inicializado Steam. Es necesario asegurarse de que se tiene un cliente de Steam abierto al intentar realizar esta llamada y de que previamente se ha llamado a SteamAPI_Init.
Lo que hace: básicamente, este método realiza una llamada a ISteamUserStats::RequestCurrentStats, que es una llamada asincrónica a Steam que pide las estadísticas y los logros del usuario actual. Es necesario que esta llamada se realice antes de poder establecer cualquier estadística o logro. La llamada inicial a este método se realiza en el constructor. Puede llamarse de nuevo a esta función en cualquier momento si se desea comprobar las estadísticas o logros actualizados.
bool CSteamAchievements::RequestStats() { // ¿Se ha cargado Steam? Si no es así no podremos obtener estadísticas. if ( NULL == SteamUserStats() || NULL == SteamUser() ) { return false; } // ¿Ha iniciado sesión el usuario? Si no es así, no podremos obtener estadísticas. if ( !SteamUser()->BLoggedOn() ) { return false; } // Pedir estadísticas de usuario. return SteamUserStats()->RequestCurrentStats(); }

SetAchievement()

Parámetros: el identificador de cadena del logro que se quiere establecer (p. ej. "ACH_WIN_ONE_GAME")
Devoluciones: un resultado booleano que representa si la llamada se completó. Si la llamada no se ha procesado con éxito, Steam no está inicializado o aún no ha procesado la devolución de la llamada inicial a RequestStats. No se pueden establecer logros hasta que se haya recibido esa devolución de llamada.
Lo que hace: este método establece un logro dado como desbloqueado y envía los resultados a Steam. Se puede establecer varias veces un mismo logro, así que no hay que preocuparse de configurar solo aquellos que todavía no se hayan configurado. Se trata de una llamada asincrónica que activará dos devoluciones de llamada: OnUserStatsStored() y OnAchievementStored().
bool CSteamAchievements::SetAchievement(const char* ID) { // ¿Ya hemos recibido una llamada de Steam? if (m_bInitialized) { SteamUserStats()->SetAchievement(ID); return SteamUserStats()->StoreStats(); } // Si aún no se recibió, no podemos determinar los logros return false; }

OnUserStatsReceived()

Parámetros: N/D
Devoluciones: Ninguna
Lo que hace: este método es una devolución de llamada que se realiza siempre que se intentan solicitar estadísticas. Las estadísticas y los logros se solicitan mediante el uso de RequestStats(). Este método actualiza la variable miembro: m_pAchievements, para reflejar los datos de las estadísticas y los logros más recientes devueltos por Steam.
void CSteamAchievements::OnUserStatsReceived( UserStatsReceived_t *pCallback ) { // podemos empezar a recibir devoluciones de llamadas para otros juegos, ignóralas por ahora if ( m_iAppID == pCallback->m_nGameID ) { if ( k_EResultOK == pCallback->m_eResult ) { OutputDebugString("Received stats and achievements from Steam\n"); m_bInitialized = true; // cargar los logros for ( int iAch = 0; iAch < m_iNumAchievements; ++iAch ) { Achievement_t &ach = m_pAchievements[iAch]; SteamUserStats()->GetAchievement(ach.m_pchAchievementID, &ach.m_bAchieved); _snprintf( ach.m_rgchName, sizeof(ach.m_rgchName), "%s", SteamUserStats()->GetAchievementDisplayAttribute(ach.m_pchAchievementID, "name")); _snprintf( ach.m_rgchDescription, sizeof(ach.m_rgchDescription), "%s", SteamUserStats()->GetAchievementDisplayAttribute(ach.m_pchAchievementID, "desc")); } } else { char buffer[128]; _snprintf( buffer, 128, "RequestStats - failed, %d\n", pCallback->m_eResult ); OutputDebugString( buffer ); } } }

OnUserStatsStored()

Parámetros - N/D
Devoluciones - Nada
Lo que hace: este método es una devolución de llamada que se realiza siempre que se intentan almacenar estadísticas en Steam.
void CSteamAchievements::OnUserStatsStored( UserStatsStored_t *pCallback ) { // podemos empezar a recibir devoluciones de llamadas para otros juegos, ignóralas por ahora if ( m_iAppID == pCallback->m_nGameID ) { if ( k_EResultOK == pCallback->m_eResult ) { OutputDebugString( "Stored stats for Steam\n" ); } else { char buffer[128]; _snprintf( buffer, 128, "StatsStored - failed, %d\n", pCallback->m_eResult ); OutputDebugString( buffer ); } } }

OnAchievementStored()

Parámetros - N/D
Devoluciones - Nada
Lo que hace: este método es una devolución de llamada que se realiza siempre que se almacenen logros correctamente en Steam.
void CSteamAchievements::OnAchievementStored( UserAchievementStored_t *pCallback ) { // podemos empezar a recibir devoluciones de llamadas para otros juegos, ignóralas por ahora if ( m_iAppID == pCallback->m_nGameID ) { OutputDebugString( "Stored Achievement for Steam\n" ); } }

Paso 3: integración en el juego

La siguiente es una lista completa de segmentos de código que los desarrolladores necesitarán integrar en su juego en las ubicaciones adecuadas.

Definiciones y generalidades

La siguiente es una lista completa de inclusiones que se necesitan para trabajar con logros, una enumeración de los logros específicos de nuestro juego y un puntero global a nuestro objeto auxiliar. Téngase en cuenta que los logros se corresponden con los de la página del administrador de Steamworks.
... #include "steam_api.h" // Definir nuestros logros enum EAchievements { ACH_WIN_ONE_GAME = 0, ACH_WIN_100_GAMES = 1, ACH_TRAVEL_FAR_ACCUM = 2, ACH_TRAVEL_FAR_SINGLE = 3, }; // Vector de logros que incluirá los datos sobre los logros y su estado Achievement_t g_Achievements[] = { _ACH_ID( ACH_WIN_ONE_GAME, "Winner" ), _ACH_ID( ACH_WIN_100_GAMES, "Champion" ), _ACH_ID( ACH_TRAVEL_FAR_ACCUM, "Interstellar" ), _ACH_ID( ACH_TRAVEL_FAR_SINGLE, "Orbiter" ), }; // Acceso global al objeto de los logros CSteamAchievements* g_SteamAchievements = NULL; ...

Inicialización

La llamada a SteamAPI_Init inicializa todo Steam y debe llamarse antes que nada. Si la llamada se completa, creamos el objeto auxiliar pasando el vector de logros junto con el tamaño del vector.
... // Inicializar Steam bool bRet = SteamAPI_Init(); // Crear el objeto para los logros SteamAchievements si Steam se inició con éxito if (bRet) { g_SteamAchievements = new CSteamAchievements(g_Achievements, 4); } ...

Procesar devoluciones de llamada

Para garantizar que procesamos todas las devoluciones de llamada de Steam, debemos obtener nuevos mensajes con regularidad. Esto se logra mediante la adición de esta llamada al bucle del juego.
... SteamAPI_RunCallbacks(); ...

Activar logros

Activar un logro es tan simple como ejecutar una única llamada con el identificador del logro.
... if (g_SteamAchievements) g_SteamAchievements->SetAchievement("ACH_WIN_100_GAMES"); ...

Apagar

La llamada a SteamAPI_Shutdown es probablemente algo que ya tiene en su código. La función cierra Steam y debe llamarse antes de que la aplicación del desarrollador se cierre. Por último, borramos el objeto de ayuda que hemos creado.
... // Cerrar Steam SteamAPI_Shutdown(); // Borrar el objeto SteamAchievements if (g_SteamAchievements) delete g_SteamAchievements; ...

Paso 4: testeo y solución de problemas

Este código de muestra envía información a la consola de depuración que puede ayudar al desarrollador a entender qué llamadas están ejecutándose correctamente o están dando error. Los siguientes son mensajes de error típicos y sus soluciones:

Esta aplicación no pudo iniciarse porque no se encontró steam_api.dll. Reinstalar la aplicación podría solucionar este problema.
Asegúrate de que steam_api.dll esté en el mismo directorio que el ejecutable.

[S_API FAIL] SteamAPI_Init() ha fallado; no se puede encontrar una instancia en ejecución de Steam o un steamclient.dll local.
Lo más probable es que no se tenga el cliente de Steam en ejecución. Es necesario iniciar Steam e iniciar una sesión.

[S_API FAIL] SteamAPI_Init() ha fallado; no se ha encontrado una appID.
Lo más probable es que no tenga el archivo steam_appid.txt en su lugar. Es necesario ubicarlo en la carpeta de origen y asegurarse de que esta contiene el id. de aplicación propio.