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


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

In this tutorial I will present in a rather comprehensive way the implementation of Steam's achievements in Java. I used the libgdx framework, but the tutorial should apply regardless of the technology used, if it is java.

The tutorial was tested only for a desktop game (Windows). I must also mention that I tested adding achievements on a game that had been already released, but the same procedure should also apply to a game in a beta state. In addition, I used Eclipse and the standard structure of  projects for libgdx, i.e. 3 projects for desktop, core and android (which contains the assets folder).

1. Steam achievements

Achievements are a Steam's service that allows you to extend the functionality of our game. It's something to strive to "achieve". Instead of doing the main campaign you can also attempt to get the achievements which add to your game completion on your steam profile. This is a simple way to increase the value and vitality of our game.

2. Before we start - links

Before we start the implementation of achievements, please read the official documentation about the achievements provided by Valve. It is available here:


and here


However, it should be emphasized that the above documentation is quite useless, especially from the point of view of Java. Even the Step by Step guide does not explain exactly what steps to take.

3. What we need - libs

We will need two types of libraries to take advantage of the achievements. First it’s the core library that connects to Steam service. It is available here:


We need to download the Standalone version. Direct link is here:


At the time of reading the version may be higher.

In the archive you will find 3 folders:

Linux OSX-64
Windows64
Windows32

We are only interested in folders for WIN. Inside each folder you will find three files:

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

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

From these two folders we will need 3 files: steam_api.dll, steam_api64.dll and steam_appid.txt.

Let's create a 'steam' folder (the name actually does not matter) wherever we want (later we copy it to the assets folder) and copy these files there. The txt file is a simple text file containing the id of our application.

The second library is of course a library for Java. It is available here:


On the page, click on the ‘Clone or Download’ button and then ‘Download ZIP’. Among all folders, we will only be interested in "java-wrapper". More specifically, we are interested in libraries from the 'java-wrapper \ src \ main \ resources' folder:

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

As I mentioned earlier, this is a tutorial for Windows, so we will only need the last two files. If our goal is OSX or Linux, I suspect that you should also copy the .so and .dylib files. As for Windows, we will need both steam_api files regardless of the version of our game. The files will be copied to the previously mentioned 'steam' folder.

At this stage, we should have the 'steam' folder with the following content (files for OSX and Linux are optional).

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


The second thing we need from steamworks4j is the source code that will allow us to connect to libraries and the Steam service. It is here "java-wrapper \ src \ main \ java". We need to copy the whole 'com' folder to our source folder, ie 'src'. In my case, the 'src' folder is in the libgdx 'core' project.

Of course, we also need the Steam Client installed on the computer and our own game in the library. It will be available without buying if you log in from the developer account. It does not have to be released. It is also not required for the game to be uploaded on Steam for testing.

UPDATE !!!
It turns out that all libraries are available via Maven from this link.


We need to download one jar file. At the time of writing, the latest version is 1.8.0. Direct link here:


To get the dlls just open the jar file (you can simply change the extension to .zip and use WinRar or 7zip).


The jar file contains the steamworks4j classes and all necessary dll libraries. However, this jar can not be used to get rid of the 'steam' directory with libraries. You can only replace the source code in the 'src' directory by  copying the jar file to the 'libs' directory of the android project. In Eclipse, you should also point out this jar file - Eclipse -> Right click Core Project -> Properties -> Java Build Path -> Add external jars.

4. Placement of the libraries
The 'steam' folder you created earlier should be placed directly in the assets folder. If you use a configuration like me, this folder is in the Android project. So the assets folder should look like this:

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

5. App id  and steam_appid.txt
The text file which we have placed in the 'steam' folder contains the number 480, which is supposed to be the id of Steam’s test application. We need to change this number to the id  of our application. This number can be found by logging in to Steamworks (https://partner.steamgames.com) where in the list of applications we have an item such as Game (123456). This number is also available in the link to the game’s page on Steam, e.g. https://store.steampowered.com/app/123456. Therefore, we put only this number in the file.

We also need to change the location of this file. It can not be in the 'steam' folder. We have to move it up to the assets folder directly. So the assets folder should look like this:

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

I should make one note here. According to the documentation, the steam_appid.txt file is for testing only and should be removed in the production version. Of course, there is no need to delete it, because it does not contain any secret information. However, I noticed that without this file you can not run achievements even in the production version (more about this below). I do not know if it is always or only in the version of the game using steamworks4j.

6. Creating achievements
At this point, we should have all the files needed to implement the achievements. Achievements in Steam are defined by a graphic element (icons) and a text element (name and description). Each achievement requires two icons (achieved and unachieved) in jpg format at 64x64 resolution. It is recommended that the unachieved icon was gray and achieved in color.

In addition, for each achievement, we need to come up with its name and a brief description of how to get a certain achievement. Both elements will be visible to the player from the level of the Steam Client. Also, you should come up with a unique identifier for each achievement, through which we will identify the achievement from the code level. The identifier is a String variable. It is suggested to use capital letters and snake_case eg ACHIEV_BATTLE_1, ACHIEV_BATTLE_2 etc.
  
We have been doing things offline so far. To define achievements, we must log in to our Steam partner account (https://partner.steamgames.com). Then choose our application. In the main window we're looking for a link to define achievements.





Now, for each achievement, click the blue 'New Achievement' button, fill in the form and press Save.



Warning! Unfortunately, there is a bug (or feature) is the Steam platform which appears when you try to save an achievement. While completing the form, you can not upload icon files. An error occurs during the attempt. To fix it - first, fill out the form without icons, Save, then click on the Edit button and point to the icons and Save.


   
After defining all the icons, you MUST publish the changes. Otherwise, the achievements will not be visible to us. To make the achievements visible in the Steam client you have to log out and log in again to the Steams Client(not the web platform). If this does not help, please try uninstalling and reinstalling your game.

7. Code - preparation
Having all libraries and defined achievements, you can proceed to implement achievements in our game. The use of achievements in the game consists of three stages:

- loading libraries and initiation
- settings achievement
- closing libraries

While the first stage is necessary, the last one does not seem to be a must. Of course, you can set an unlimited number of achievements between start and closing. Initiations and closing are carried out once in the game. I set the initiation in the main menu (right after the Loading page), and closing when leaving the game also from the main menu.

Warning! When testing achievements in the game, you MUST have the Steam client switched on. It seems that the game connects to the client and not directly to the Steam servers.

Testing setting of the achievement can be done in the IDE without the need to compile and put the game on the Steam.

8. Code – the class
Let's start creating of the code by defining a general class. It will be very simplified code, especially when it comes to exceptions. The client is defined as a static class in the main menu.

The class will be called SteamClient:

public class SteamClient {
}

Then we will add the necessary imports. Of course, they refer to the previously added source code.

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;

Then we will add 3 variables:

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


Next, we need to define an ID for each achievement. As mentioned above, it is a String value. We define them as static variables.

static public String achievBattle1SteamId= "ACHIEV_BATTLE_1";
static public String achievBattle2SteamId= "ACHIEV_BATTLE_2";
static public String achievBattle3SteamId= "ACHIEV_BATTLE_3";
...
 
In addition, we must define one callback within our SteamClient class. We do not have to handle the callback methods in any way.

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) {
                                    }
                       
            };


In the next step, we define three basic methods corresponding to the 3 required stages.

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


9. Code - initiation

The first initAndConnect () method must properly initiate the Steam libraries. Therefore, it must contain the following code (loading the libraries is carried out with a parameter indicating the name of the folder where you previously placed the dll libraries):

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


Initiation of a connection with the Steam client:

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

Getting the client achievements statistics. Here we indicate the previously defined callback:

//Get stat object
 userStats = new SteamUserStats( steamUserStatsCallback);
 //A must before setting achievements
 userStats.requestCurrentStats();
                     
Setting the flag to indicate that the library initiation has been done correctly.
                  
isOnline=true;

10. Code - setting an achievement
To set an achievement, simply call the setAchiev (String achivName) method anywhere in the game with an appropriate achievement name. We have previously defined the names in the class using static identifiers. In addition, the call of the method is conditioned by the correct initiation of libraries.
  
Our method:
 public boolean setAchiev(String achivName)
            {
                           try {
                                        //set
                                        userStats.setAchievement(achivName);
                                        //save
                                        boolean result=userStats.storeStats();
                                       
                                        return result;
                                      }
                             catch ( Exception e) {
                                                isOnline=false;
                                                return false;
                                      }
                         

            }
And the call:
                                
//Steam
 if(SteamClient.isOnline)
   SteamClient.setAchiev(SteamClient.achievBattle1SteamId );

And that’s pretty much all that is necessary for setting achievements.

Warning! Even if you correctly set the achievement during the test, it is possible that it will not appear immediately in the Steam Client application. I do not know what is causing the delay. It is possible that the client sends information to the server from time to time. The best way to check if achievement is set correctly is to log out and log in again.


UPDATE !!! The lack of an immediate appearance of the achievement in Steam Client is the result of not calling storeStats() after setAchievement (). If this method is called correctly, the achievement will immediately appear in the Steam Client.

11. Disconnection

To disconnect, you must call the shutdown method. As mentioned, this is not absolutely necessary.

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

12. Deployment
When testing achievements in IDE, everything will work properly. Unfortunately, if we create a jar file (Runnable Jar) our game will either crash or the achievements will not be set. The problem is the location of the 'steam' folder and the steam_appid.txt file. As indicated earlier, the 'steam' folder and txt file are in the assets folder (and now inside the jar file). Unfortunately, the game can not see them. Therefore, you need to copy the ‘steam’ folder and steam_appid.txt file and place it next to the file that starts our game. To clarify how it should be done, it will show my game file structure. In my game folder, I only have an exe file and a lib folder containing jre and the jar of my game. Exe calls only jar file by referring to java.exe. It looks like this:

-MyGame
--mygame.exe
--lib

So if you want to add the 'steam' folder and the steam_appid.txt file, they must be located directly in the MyGame folder. They should be copied and not removed from the assets folder.

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

With this configuration, the achievements should work in production.

UPDATE!!!

According to the author's note (code-disaster): “The probable cause for this is how you launch your game. On startup, the Steam client "injects" into the game's process. If your executable just invokes "java -jar ..." and exits, there's nothing left to identify your application on Steam.init() - it's a new, pristine process, and needs to look for steam_appid.txt as a fallback.”.

13. Errors
Below I present the most common errors (both during testing and in production).

A. Can not find libraries.

Exception in thread "LWJGL Application" com.badlogic.gdx.utils.GdxRuntimeException: java.lang.UnsatisfiedLinkError: Can not load library: .... \ android \ assets \ steam \ steam_api.dll
  
During testing this error will occur if we do not place the steam folder in the assets folder. In production, this error will occur if we do not put the folder next to the exe file. In addition, an error occurs when the SteamAPI.loadLibraries ("./ steam") method indicates an invalid folder name.


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

This error will occur when we put the steam_api.dll file only for the Win32 version or vice versa.

Warning! In my case testing in Eclipse required Win32, but in the production the Win64. In both cases I used the same computer. I do not know what is causing this. Maybe I'm using the old version of Eclipse.


C. SteamAPI.init () returns false

This error is caused by the Steam Client applications failing to log in to their account during testing. In addition, it results from the lack of the steam_appid.txt file both in the assets folder and the main folder next to the exe file.


D. Not loaded libraries

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 ()


This error occurs when we try to call the SteamAPI.init () method before calling the SteamAPI.loadLibraries ("./ steam") method.
It seems that the earlier version of the Java API did not require a calling of SteamAPI.loadLibraries () which could be the source of the problem.
                     
14. The end
I hope that the tutorial will be helpful. If you want to see my game in which I implemented achievements according to this tutorial, check this link:


Follow me also on Twitter:


15. Helpful links
  

Komentarze

Popularne posty z tego bloga

ATU logo

Making of "The Collapse: Space Supremacy" game