Achievementy Steam w Java(libgdx). Przewodnik krok po kroku.



W tym tutorialu przedstawię w dość całościowy sposób implementacji achiementów Steama dla języka java. Używałem frameworka libgdx, ale tutorial powinien mieć zastosowania bez względu na użytą technologie o ile to java.

Tutorial był testowany tylko dla gry na desktop (Windows). Muszę także wspomnieć, że testowałem dodawanie achievementów na grze już opublikowanej, ale ta sama procedura powinna odnosić się także do gry w wersji beta. Ponadto używam Eclipse i standardowej struktury projektu dla libgdx czyli 3 projekty desktop, core i android (który zawiera katalog assets).

1. Steam achievements

Achievementy to usługa Steama pozwalająca na rozszerzenie funkcjonalności naszej gry. To coś do czego dążymy w grze. Zamiast przechodzić główną kampanię można także starać się osiągnąć achievementy, które są widoczne na twoim profilu Steama.  To prosty sposób na zwiększenie wartości i żywotności naszej gry.

2. Zanim zaczniemy - linki
Zanim zaczniemy implementacje achievementów należy zapoznać się z oficjalną dokumentacją na temat achievementów zapewnioną przez Valve. Dostępna jest tutaj.


oraz tutaj


Należy jednak podkreślić, że powyższa dokumentacja jest dość bezużyteczna, zwłaszcza z punktu widzenia Javy. Nawet przewodnik Step by Step nie wyjaśnia jakie dokładnie kroki należy podjąć.

3. Czego potrzebujemy - libs
 Dla skorzystania z achievementów będziemy potrzebowali dwóch rodzajów bibliotek. Po pierwsza to właściwa biblioteka łączącą się ze Steam. Dostępna jest tutaj:


Musimy pobrać wersję Standalone. Bezpośredni link tutaj:


W momencie czytanie wersja może być wyższa.

W archiwum znajdziemy 3 katalogi.  

OSX-Linux-64
Windows64
Windows32

Interesują nas tylko katalogi dla WIN. Wewnątrz każdego katalogu znajdziemy trzy pliki

dla Win32
steam_api.dll
steam_appid.txt
Steamworks.Net.dll

dla Win64
steam_api64.dll
steam_appid.txt
Steamworks.Net.dll

Z tych dwóch folderów będziemy potrzebowali 3 pliki: steam_api.dll, steam_api64.dll i steam_appid.txt.

Stwórzmy katalog 'steam' (sama nazwa nie ma akurat znaczenia) gdziekolwiek chcemy (później przekopiujemy do katalogu assetów) i skopiuj tam te pliki. Plik txt to zwykły plik tekstowy zawierający numer naszej aplikacji.

Druga biblioteka to oczywiście biblioteka dla Javy. Dostępna jest tutaj


Na stronie należy kliknąć na przycisk Clone or Download, a następnie Download ZIP. Pośród wszystkich katalogów będzie nas interesował tylko "java-wrapper". Dokładniej interesują nas biblioteki z katalogu 'java-wrapper\src\main\resources' czyli:

libsteamworks4j.dylib
libsteamworks4j.so
steamworks4j.dll
steamworks4j64.dll

Jak wspomniałem wcześniej to tutorial dla Windows dlatego będziemy potrzebować tylko tych dwa ostatnie pliki. Jeżeli naszym celem jest OSX lub Linux podejrzewam, że należy skopiować też plik .so i .dylib. Odnoście Windows będziemy potrzebowali obu plików steam_api bez względu na wersję naszej gry. Pliki skopiujemy do wcześniej wspomnianego katalog ‘steam’.

Na tym etapie powinniśmy mieć utworzony katalog ‘steam’ z następującą zawartością (pliki dla OSX i Linuxa opcjonalnie).

steam_api.dll
steam_appid.txt
steam_api64.dll
steamworks4j.dll
steamworks4j64.dll
libsteamworks4j.dylib
libsteamworks4j.so


Drugą rzeczą, którą potrzebujemy ze steamworks4j to kod źródłowy, który umożliwi nam połączenie z bibliotekami i usługą Steam. Znajduje się tutaj "java-wrapper\src\main\java". Musimy skopiować cały katalog 'com' do naszego katalogu ze źródłem gry tj. 'src'. W moim przypadku katalog ‘src’ znajduje się  w projekcie libgdx 'core'.

Oczywiście potrzebujemy także zainstalowanej na komputerze aplikacji klienta Steam oraz własnej gry w bibliotece. Będzie ona dostępna bez kupowania jeżeli zalogujemy się z konta deweloperskiego. Nie musi być opublikowana. Nie jest także wymagane, aby gr już była załadowana na Steam.  



UPDATE!!!
Okazuje się, że całość bibliotek jest dostępna poprzez Maven  z tego linku:


Musimy pobrać jeden plik jar. W momencie pisania najnowsza wersja to 1.8.0. Bezpośredni link tutaj:


Aby pobrać biblioteki dll wystarczy otworzyć jara (można po prostu zmienić rozszerzenie na .zip i użyć WinRara lub 7zip).

Plik jar zawiera klasy steamworks4j oraz wszystkie potrzebne biblioteki dll. Jednak tym jarem nie można zastąpić katalogu ‘steam’ z bibliotekami. Jedynie zamiast umieszczania kodu źródłowego w katalogu ‘src’ można załączyć jara kopiując go do katalogu ‘libs’ projektu android. W Eclipsie należy także wskazać tego jara - Eclipse -> Prawy klik Core Project -> Properties -> Java Build Path  -> Add external jars


4. Umieszczenie bibliotek
Utworzony wcześniej katalog 'steam' należy umieścić w bezpośrednio w katalogu assetów. Jeżeli używasz konfiguracji jak ja katalog ten znajduje się w projekcie Android. Zatem katalog assets powinien wyglądać tak:

-assets
--models
--images
--icons
--steam
...

5. Id aplikacji  i steam_appid.txt
Plik tekstowy, który umieściliśmy w katalogu ‘steam’ zawiera numer 480, który ma być numerem testowej aplikacji Steam. Musimy zamienić ten numer na numer naszej aplikacji. Numer ten znajdziemy bez problem logując się Steamworks (https://partner.steamgames.com) gdzie na liście aplikacji mamy pozycję np. Game (123456). Numer ten jest także dostępny w linku do strony na Steam np. https://store.steampowered.com/app/123456. Zatem umieszczamy tylko ten  numer w pliku.

Musimy także zmienić lokalizację tego pliku. Nie może się on znajdować w katalogu ‘steam’. Musimy przenieść go poziom wyżej bezpośrednio do katalogu assets. Zatem katalog assets powinien wyglądać tak:

-assets
--models
--images
--icons
--steam_appid.txt
--steam
...

W tym momencie należy uczynić jedną uwagę. Zgodnie z dokumentacją plik steam_appid.txt służy jedynie testom i powinien być usunięty  w wersji produkcyjnej. Oczywiście nie ma konieczności jego usuwania, bowiem nie zawiera żadnych tajnych informacji. Jednakże zauważyłem, że bez tego pliku nie da się uruchomić achievementów nawet w wersji produkcyjnej (o tym poniżej). Nie wiem czy jest tak zawsze czy tylko w wersji gry używającej steamworks4j.  

6. Tworzenie achievementów
W tym momencie powinniśmy mieć wszystkie pliki potrzebne do implementacji achievementów. Achievementy w Steam określone są przez element graficzny(ikony) i tekstowy (nazwa i opis). Każdy achievement wymaga dwóch ikon (osiągniętą i nieosiągniętą) w formacie jpg w rozdzielczości 64x64. Rekomenduje się aby ikona nieosiągnięta była szara, a osiągnięta kolorowa.

Ponadto, do każdego achievementu należy wymyślić jego nazwę i krótki opis jak uzyskać dany achievement. Oba elementy będą widoczne dla gracza z poziom klienta Steam. Ponadto,   należy wymyślić unikalny identyfikator dla każdego achievementa, za pomocą którego z poziomu kodu będziemy identyfikować dany achievement. Identyfikator to zmiana typu String. Sugeruje się użycie dużych liter i snake_case np. ACHIEV_BATTLE_1, ACHIEV_BATTLE_2 itd.
  
Do tej pory działaliśmy offline. Aby zdefiniować achievementy musimy zalogować się na naszym koncie partnera Steam (https://partner.steamgames.com). Następnie wybieramy naszą aplikację. W głównym oknie szukamy linku do definiowania achievementów.





Teraz dla każdego achievementu klikamy niebieski przycisk ‘New Achievement’, wypełniamy formularz i naciskamy Save.



Uwaga! Niestety przy zapisywaniu achievementu pojawia się bug (a może feature) platformy Steam. Podczas wypełniania formularza nie da się uploadować plików ikon. Przy próbie pojawia się błąd. Najpierw należy wypełnić formularz bez ikon, zachować, następnie kliknąć na przycisk Edit i wskazać ikony i ponownie zachować.


  
Po zdefiniowaniu wszystkich ikon NALEŻY opublikować zmiany. Inaczej achievementy nie będą widoczne. Żeby achievementy były widoczne w kliencie Steam musisz wylogować się i zalogować ponownie do klienta (nie platformy webowej). Jeżeli to nie pomaga proszę spróbować odinstalować i ponownie zainstalować swoją grę.

7. Kod - przygotowanie
Mając już wszystkie biblioteki i zdefiniowane achievementy można przystąpić do implementowania achievementów w naszej grze. Użycie achievementów w grze składa się z 3 etapów:

-załadowania biliotek i inicjacja
-ustawienia achievementu
-zamkniecia bibliotek

O ile pierwszy etap jest niezbędny to ostatni nie wydaje się być konieczny. Oczywiście pomiędzy rozpoczęciem a zamknięciem można ustawić nieograniczoną ilość achievementów. Inicjacje i zakończenie przeprowadza się raz w grze. Ja inicjacje ustawiłem w głównym menu (zaraz po Loading page), a zakończenie przy wychodzeniu z gry także z poziomu głównego menu.


Uwaga! Testując achievementy w grze NALEŻY mieć włączonego klienta Steam. Wydaje się, że gra łączy się z klientem, a nie bezpośrednio z serwerami Steam.

Testowanie ustawiania achievementów może odbywać się w IDE bez potrzeby kompilowania i umieszczania gry na steamie.

8. Kod - klasa
Tworzenie kodu zacznijmy od zdefiniowania ogólnej klasy. Będzie ona bardzo uproszczona, zwłaszcza jeżeli chodzi o wyjątki. Samego klienta definiujemy jako statyczną klasę w głównym menu.

Klasa będzie nazywała się SteamClient:

public class SteamClient {
}

Następnie dodamy potrzebne importy. Oczywiście nawiązują do wcześniej dodanego kodu źródłowego.

import com.codedisaster.steamworks.SteamAPI;
import com.codedisaster.steamworks.SteamException;
import com.codedisaster.steamworks.SteamID;
import com.codedisaster.steamworks.SteamLeaderboardEntriesHandle;
import com.codedisaster.steamworks.SteamLeaderboardHandle;
import com.codedisaster.steamworks.SteamResult;
import com.codedisaster.steamworks.SteamUserStats;
import com.codedisaster.steamworks.SteamUserStatsCallback;
import com.codedisaster.steamworks.SteamUtils;

Następnie dodamy 3 zmienne:

private SteamUtils utils;
private SteamUserStats userStats;
public boolean isOnline=false;


Następnie musimy zdefiniować identyfikator dla każdego achievementa. Jak wspomniano wyżej to String. Zdefiniujemy je jako zmienne statyczne.

static public String achievBattle1SteamId= "ACHIEV_BATTLE_1";
static public String achievBattle2SteamId= "ACHIEV_BATTLE_2";
static public String achievBattle3SteamId= "ACHIEV_BATTLE_3";
...
 
Ponadto musimy zdefiniować w ramach naszej klasy SteamClient jeden callback. Nie musimy obsługiwać w żaden sposób metod tego callbacka.   

SteamUserStatsCallback steamUserStatsCallback=new SteamUserStatsCallback()
            {

                        @Override
                        public void onUserStatsReceived(long gameId, SteamID steamIDUser,
                                               SteamResult result)
                              {
                                     }

                        @Override
                        public void onUserStatsStored(long gameId, SteamResult result) {
                         }

                        @Override
                        public void onUserStatsUnloaded(SteamID steamIDUser) {
                                   
                        }

                        @Override
                        public void onUserAchievementStored(long gameId,
                                               boolean isGroupAchievement, String achievementName,
                                               int curProgress, int maxProgress) {
                         }

                        @Override
                        public void onLeaderboardFindResult(SteamLeaderboardHandle leaderboard,
                                               boolean found) {
                         }

                        @Override
                        public void onLeaderboardScoresDownloaded(
                                               SteamLeaderboardHandle leaderboard,
                                               SteamLeaderboardEntriesHandle entries, int numEntries) {
                         }

                        @Override
                        public void onLeaderboardScoreUploaded(boolean success,
                                               SteamLeaderboardHandle leaderboard, int score,
                                               boolean scoreChanged, int globalRankNew, int globalRankPrevious) {
                         }

                        @Override
                        public void onGlobalStatsReceived(long gameId, SteamResult result) {
                                    }
                       
            };


W kolejnym kroku definiujemy 3 podstawowe metody odpowiadające 3 wymaganym etapom.

public boolean initAndConnect(){};
public boolean setAchiev(String achivName) {};
public void disconnect(){};


9. Kod - inicjacja

Pierwsza metoda initAndConnect() musi prawidłowo zainicjować biblioteki Steam. Dlatego musi zawierać następujący kod (załadowanie bibliotek z parametrem wskazującym nazwę katalogu gdzie wcześniej umieściliście biblioteki dll):


 try {
             SteamAPI.loadLibraries("./steam");
     }
 catch (SteamException e1)
             {
             System.out.println("Load libraries error");
            }         


Inicjacja połączenia z klientem Steam:

 try {
      if (!SteamAPI.init())
            {
            System.out.println("Initialisation failed");
            isOnline=false;
            return false ;
            }
       } catch (SteamException e)
            {
            e.printStackTrace();
            }

Pobranie statystyk z achievementami. Tutaj wskazujemy wcześniej zdefiniowany callback:

//Get stat object
 userStats = new SteamUserStats( steamUserStatsCallback);
 //A must before setting achievements
 userStats.requestCurrentStats();
                     
Ustawienie flagi wskazującej, że poprawnie wykonano inicjacje bibliotek.        
                  
isOnline=true;

10. Kod - ustawienie achievementu
Aby ustawić achievement wystarczy wywołać metodę setAchiev(String achivName) w dowolnym miejscu w grze z odpowiednią nazwą achievementu. Nazwy wcześniej zdefiniowalismy w klasie za pomocą statycznych identyfikatorów. Dodatkowo wykonanie metody warunkowane jest poprawnym zainicjowaniem bibliotek.

Nasza metoda:

public boolean setAchiev(String achivName)
            {
                       
                         
                           try {
                                        //set
                                        userStats.setAchievement(achivName);
                                        //save
                                        boolean result=userStats.storeStats();
                                       
                                        return result;
                                      }
                             catch ( Exception e) {
                                                isOnline=false;
                                                return false;
                                      }
                         
            }



I wywołanie:
                                 
//Steam
 if(SteamClient.isOnline)
   SteamClient.setAchiev(SteamClient.achievBattle1SteamId );

I to wszystko co jest konieczne dla ustawienia achievementu.

Uwaga! Nawet jeżeli poprawnie ustawiono achievement w czasie testu możliwe, że nie pojawi się on natychmiast w aplikacji Steam Client. Nie wiem co jest przyczyną opóźnienia. Możliwe że Klient wysyła informacje na serwer co jakiś czas. Najlepszą metodą sprawdzenia czy poprawnie ustawiono achievement jest wylogowanie się i ponowne zalogowanie przez Klienta.


UPDATE!!! Brak natychmiastowego pojawienia się achievementu w Steam Klient jest wynikiem niewywołania  storeStats()  po setAchievement(). Jeżeli ta metoda zostanie poprawnie wywołana, to achievement od razu pojawi się w Steam Client. 

11. Rozłączenie się


Aby rozłączyć się należy wywołać metodę shutdown. Jak wspomniano nie jest to bezwzględnie konieczne.

public void disconnect()
            {
                          SteamAPI.shutdown();
            }


12. Deployment
Testując achievementy w IDE wszystko będzie poprawnie działało. Niestety jeżeli utworzymy plik jar (Runnable Jar) nasza gra albo się wysypie albo achievementy nie będą ustawiane. Problemem jest umiejscowienie katalogu 'steam' oraz pliku steam_appid.txt. Jak wskazano wcześniej katalog ‘steam’ i plik txt znajdują się w katalogu assets (a obecnie wewnątrz pliku jar). Niestety gra ich nie widzi. Dlatego należy skopiować katalog steam i plik steam_appid.txt i umieścić zaraz obok pliku, który rozpoczyna naszą grę. Żeby wyjaśnić jak to ma wyglądać pokaże moją strukturę plików gry. W moim katalogu gry mam tylko plik exe oraz katalog lib zawierający jre oraz jar mojej gry. Exe wywołuje jedynie jara odwołując się do java.exe. Wygląda to tak

-MyGame
--mygame.exe
--lib

Zatem jeżeli chcemy dodać katalog ‘steam’ i plik steam_appid.txt muszą się oni znajdować bezpośrednio w katalogu MyGame. Należy go skopiować, a nie usunąć z katalogu assets.

-MyGame
--mygame.exe
--lib
--steam
--steam_appid.txt

Przy takiej konfiguracji achievementy powinny działać po uplodowaniu gry Steam.

UPDATE!!!

Zgodnie z uwagą autora biblioteki (code-disaster) „konieczność umieszczenia katalogu ‘steam’i pliku txt wynika ze sposobu uruchomienia gry. Na początku klient Steam ‘wstrzykuje’ się w proces gry. Jeżeli plik exe wywołuje "java -jar ..." nie ma niczego, co mogłoby zidentyfikować aplikacje przy wywołaniu Steam.init()- to nowy process i musi szukać pliku steam_appid.txt jako wyjścia awaryjne”. 

13. Błędy
Poniżej przedstawiam najczęściej występujące błędy (zarówno w trakcie testowania jak i działania gry).

A. Nie można znaleźć bibliotek.

Exception in thread "LWJGL Application" com.badlogic.gdx.utils.GdxRuntimeException: java.lang.UnsatisfiedLinkError: Can't load library: ....\android\assets\steam\steam_api.dll
 
W czasie testowania ten błąd wystąpi jeżeli nie umieściliśmy katalogu steam w katalogu assets. W czasie działania gry ten błąd wystąpi jeżeli nie umieścimy katalogu obok pliku exe. Ponadto błąd pojawia się gdy metoda SteamAPI.loadLibraries("./steam") wskazuje nieprawidłową nazwę katalogu.


B. Win32 i Win64
  
Exception in thread "LWJGL Application" com.badlogic.gdx.utils.GdxRuntimeException: java.lang.UnsatisfiedLinkError: ...\android\assets\steam\steam_api.dll: Can't load AMD 64-bit .dll on a IA 32-bit platform

Ten błąd wystąpi gdy umieściliśmy plik steam_api.dll jedynie dla wersji Win32 lub odwrotnie.

Uwaga! W moim przypadku  testowanie w Eclipse wymagało Win32, a wersja produkcyjna Win64.  W obu przypadkach korzystałem z tego samego komputera. Nie wiem co jest tego przyczyną. Może to, że używam starej wersji Eclipsa.


C. SteamAPI.init() zwraca false

Błąd ten wywołuje brak zalogowania do swojego konta przez aplikacje Steam Client w czasie testowania. Ponadto wynika z braku umieszczenia pliku steam_appid.txt w katalogu assets i głównym katalogu obok pliku exe.


D. Biblioteki nie załadowane

com.codedisaster.steamworks.SteamException: Native libraries not loaded.
Ensure to call SteamAPI.loadLibraries() first!
at com.codedisaster.steamworks.SteamAPI.init(SteamAPI.java:44)

Exception in thread "LWJGL Application" com.badlogic.gdx.utils.GdxRuntimeException: java.lang.UnsatisfiedLinkError:
com.codedisaster.steamworks.SteamAPI.getSteamUserPointer() at com.badlogic.gdx.backends.lwjgl.LwjglApplication$1.run(LwjglApplication.java:133)
Caused by: java.lang.UnsatisfiedLinkError: com.codedisaster.steamworks.SteamAPI.getSteamUserPointer()
             

Ten błąd pojawia się gdy probujemy wywołać metodę SteamAPI.init() przed wywołaniem metody SteamAPI.loadLibraries("./steam").
Wydaje się, że wcześniejsza wersja API dla Javy nie wymagała wywołania SteamAPI.loadLibraries() co może być źródłem problemu.
                     
14. Zakończenie

Mam nadzieję, że powyższy tutorial będzie pomocny. Jeżeli chcesz zobaczyć moją grę w której zaimplementowałem achievementy w powyższy sposób sprawdź ten link


Proszę też o suba na Twitterze.



15. Pomocne linki
  

Komentarze

Popularne posty z tego bloga

Steam achievements in Java(libgdx). Step by step for dummies.

Making of "The Collapse: Space Supremacy" game

Reactor