[Guide] Building LWJGL project for publishing

Started by Lightbuffer, December 10, 2021, 08:21:32

Previous topic - Next topic

Lightbuffer

For admin(s)/moderator(s): I had to post this guide in two parts because the forum went 500 Internal Server error on me. I guess it's character limit...  :-[

Hello! I noticed that there are no tutorials or guides on how to build an LWJGL game for publishing, and after a couple of days of tinkering, I managed to figure out how to build a game that can be zipped and sent to another user to run and play around with it. So here is a guide I put together.  :D

So let's get started! But before we start, let's talk about our sponsors I have to warn you that my solution probably isn't the best out there, it's working on my machine, but I haven't tested it out on other machines but be my guest (in theory it should work though). If I find any issues in the future, I'll try to not forget to edit this guide. Also, in this guide I'm using LWJGL3 (which in theory should work with LWJGL2 projects as well) and Gradle.

Theory

So in order to run the game outside of the IDE, the exported jar needs to have all of its dependencies, and any additional provided program and JVM arguments, passed into the program when getting run through Java. IDE handles injecting all of your dependencies from Gradle into the classpath (-cp/-classpath argument of Java CLI) and passing any program and JVM arguments. What we need to do in theory, is to get all the dependencies from Gradle into a separate folder, and then launch the built final jar of the game with all of the dependencies and the game jar itself provided into the classpath alongside any program and JVM arguments.

Gradle

For getting out Gradle dependencies into a separate folder, I found some snippet online on how to get all of the dependencies into a separate folder. Dropping this code snippet in the end of the build.gradle:

Code: java
task copyRuntimeLibs(type: Copy) {
    from configurations.compile
    into "build/dependencies/"
}

tasks.build.dependsOn.add(copyRuntimeLibs)


This would add a task to the gradle build task, which would copy all of the dependencies in build.gradle's dependencies {} block marked as compile into build/dependencies folder after running gradle build. I didn't managed to make it work with implementation or other type of dependency types unfortunately (maybe that's because I'm using Gradle 4.*).  :-\

Lightbuffer

Launching

In order to make the final game double clickable to launch, in other words as easily launchable, you have two options:

  • Provide a .sh or .bat (depending on OS) script that would launch the game with hardcoded dependency list and JVM arguments.
  • Create a launch class within your game that will relaunch itself with all dependencies provided, JVM and program arguments.

Launch script

In the case with a shell script, it would look something like this:

Code: sh
#!/usr/bin/sh

# For macOS, -XstartOnFirstThread flag is required for LWJGL3 before -Dfile.encoding alongside with -macos natives
java -Dfile.encoding=UTF-8 -classpath "dependencies/joml-1.9.2.jar;dependencies/lwjgl-3.2.1-natives-linux.jar;dependencies/lwjgl-3.2.1.jar;dependencies/lwjgl-glfw-3.2.1-natives-linux.jar;dependencies/lwjgl-glfw-3.2.1.jar;dependencies/lwjgl-nfd-3.2.1-natives-linux.jar;dependencies/lwjgl-nfd-3.2.1.jar;dependencies/lwjgl-openal-3.2.1-natives-linux.jar;dependencies/lwjgl-openal-3.2.1.jar;dependencies/lwjgl-opengl-3.2.1-natives-linux.jar;dependencies/lwjgl-opengl-3.2.1.jar;dependencies/lwjgl-stb-3.2.1-natives-linux.jar;dependencies/lwjgl-stb-3.2.1.jar;launcher.jar" mchorse.game.Main -gd "path/to/game"


Or as a batch script:

Code: bat
java -Dfile.encoding=UTF-8 -classpath "dependencies/joml-1.9.2.jar;dependencies/lwjgl-3.2.1-natives-windows.jar;dependencies/lwjgl-3.2.1.jar;dependencies/lwjgl-glfw-3.2.1-natives-windows.jar;dependencies/lwjgl-glfw-3.2.1.jar;dependencies/lwjgl-nfd-3.2.1-natives-windows.jar;dependencies/lwjgl-nfd-3.2.1.jar;dependencies/lwjgl-openal-3.2.1-natives-windows.jar;dependencies/lwjgl-openal-3.2.1.jar;dependencies/lwjgl-opengl-3.2.1-natives-windows.jar;dependencies/lwjgl-opengl-3.2.1.jar;dependencies/lwjgl-stb-3.2.1-natives-windows.jar;dependencies/lwjgl-stb-3.2.1.jar;launcher.jar" mchorse.game.Main -gd "path\to\game"


Where:

  • dependencies/ is the copied folder with all dependencies in build/dependencies/ folder of the project after running gradle build.
  • launcher.jar is the built jar after running gradle build which can be found in build/libs/ folder.
  • mchorse.game.Main is the main class in your game.
  • -gd "path\to\game" is your program arguments, in my case it's game directory.

The structure of the final folder would be something like:

launcher.jar
dependencies/
    joml-1.9.2.jar
    lwjgl-3.2.1-natives-linux.jar
    lwjgl-3.2.1.jar
    lwjgl-glfw-3.2.1-natives-linux.jar
    lwjgl-glfw-3.2.1.jar
    lwjgl-nfd-3.2.1-natives-linux.jar
    lwjgl-nfd-3.2.1.jar
    lwjgl-openal-3.2.1-natives-linux.jar
    lwjgl-openal-3.2.1.jar
    lwjgl-opengl-3.2.1-natives-linux.jar
    lwjgl-opengl-3.2.1.jar
    lwjgl-stb-3.2.1-natives-linux.jar
    lwjgl-stb-3.2.1.jar


And after user double clicking the shell or batch script, the game should be launched and playable. The issue with this solution is that you would have to manually update the dependencies, or to regenerate the script every time you add or remove a dependency. Here is where comes the launch class...

Launch class

Launch class would be a utility class within your game's jar, which would be responsible for launching another JVM instance with needed classpath, program and JVM arguments and closing itself, so it's kind of like some sort of proxy/delegate. Mine looks something like this:

Code: java
package mchorse;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.StringJoiner;

/**
 * Launcher.
 *
 * This class is responsible for launching the game with all of its dependencies,
 * in a user friendly manner (i.e. by double clicking on the jar).
 */
public class Launcher
{
    public static void main(String[] strings)
    {
        List<String> args = new ArrayList<String>();
        String mainClass = "mchorse.game.Main";

        args.add("java");

        if (System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac"))
        {
            args.add("-XstartOnFirstThread");
        }

        args.add("-Dfile.encoding=UTF-8");
        args.add("-classpath");
        args.add(getClasspath());
        args.add(mainClass);
        
        /* Here are you can add your own program arguments */

        try
        {
            ProcessBuilder process = new ProcessBuilder(args);

            process.redirectErrorStream(true);
            process.redirectOutput(new File("launcher.log"));
            process.start();
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

    private static String getClasspath()
    {
        File folder = new File(new File("").getAbsolutePath());
        StringJoiner joiner = new StringJoiner(";");

        for (File file : folder.listFiles())
        {
            String name = file.getName();

            if (name.equals("launcher.jar"))
            {
                joiner.add(name);
            }
            else if (name.equals("dependencies") && file.isDirectory())
            {
                for (File dep : file.listFiles())
                {
                    joiner.add("dependencies" + File.separator + dep.getName());
                }
            }
        }

        return joiner.toString();
    }
}


Place (and modify!) this class somewhere in your game. Important: this code assumes that the final jar's name is launcher.jar, adjust if needed!

Now, in order to make this class be launched upon double clicking, you need to add to your build.gradle following code (unless you already have jar { manifest {} } block):

Code: groovy
jar {
    manifest {
        attributes 'Main-Class': 'mchorse.Launcher'
    }
}


That would add META-INF/MANIFEST.MF file after building the game with gradle build which would have main class provided. Once you'll build the final jar, place it alongside dependencies folder, and after double clicking the final game jar, the game should launch as it was in IDE, if everything was done correctly.

Extra: I also made this script to automatically build the final game and place it into release/ folder of the project with dependencies:

Code: sh
#!/usr/bin/sh

# Build the game
printf "Building...\n\n"

gradle build

# Copy assets
printf "\nCopying assets..."

mkdir -p release
cp build/libs/game-1.0.jar release/launcher.jar
cp -r build/dependencies/ release/dependencies/


You may want to add release/ folder to your .gitignore (if you use git).

Conclusion

Hopefully this tutorial was helpful, and you managed to make a publishable build of your LWJGL project. Now, all you have to do is to zip the release folder, and upload it to a game store, your website, etc.  :)

An important thing to keep in mind, is if you're going to publish your game with all of the dependencies provided, you are technically legally obligated to provide licenses of dependencies you use (however from one example of a Steam game I saw made with LWJGL, there was no licenses provided, so it's not fatal at this stage, but may potentially bite in the ass in the future). Here are links to LWJGL3's and JOML's licenses which you can copy to your final game build.