Luaj is a lua interpreter based on the 5.2.x version of lua which allows you to execute lua from inside java, compile lua to lua bytecode and compile lua to java bytecode. It is used in many games to allows players to create and add mods to a game. A list of some games can be found here.
What’s the point in adding modding to a game? well I’ll tell you. The amount of people out there who make mods is huge and they all have ideas and skills which you may not have. Allowing modding means your game can be improved by the players so they get to add the things they want in the game and all you need to do is allow them access. Modding will allow your game to be more entertaining for a longer period as mods can always be added even when development of the game is finished.
So how would you allow modding? The first thing you should do is decide on what you will use to allow your users to mod the game. I will be using Lua with LuaJ but you could allow them to use anything from JavaScript to XML. You could even create your own scripting language designed to work with your game.
To start using LuaJ with your java project you need to include it in your project. I am using gradle but you could add it manually if you’re not using gradle. Below is an excerpt from the gradle.build file I use.
1 2 3 4 5 6 7 8 9 10 11 12 |
project(":desktop") { apply plugin: "java" dependencies { compile project(":core") compile "com.badlogicgames.gdx:gdx-backend-lwjgl:$gdxVersion" compile "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" compile "com.badlogicgames.gdx:gdx-ai:1.7.0" compile "com.badlogicgames.gdx:gdx-tools:$gdxVersion" compile "org.luaj:luaj-jse:3.0.1" } } |
With LuaJ in the build file I refreshed dependencies and built the project. Now I have LuaJ in my project I can start using it. I wanted a system that would allow me to use more than one scripting language later on so I made a script interface for the scripts, a class for Lua Scripts, an execute script for registering functions and a script manager.
Below in the Script interface. This sets the methods should be defined in a script.
1 2 3 4 5 6 7 8 |
package com.johnday.luajtest; public interface Script { boolean canExecute(); boolean executeInit(Object... objects); boolean executeFunction(String functionName, Object... objects); boolean reload(); } |
The LuaScript class defines all the methods for executing Lua functions and storing the return values. LibGDX code is commented out here but can replace the current code to use within a LibGDX project.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
package com.johnday.luajtest; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import org.luaj.vm2.Globals; import org.luaj.vm2.LuaError; import org.luaj.vm2.LuaValue; import org.luaj.vm2.Varargs; import org.luaj.vm2.lib.TwoArgFunction; import org.luaj.vm2.lib.jse.CoerceJavaToLua; import org.luaj.vm2.lib.jse.JsePlatform; /* * Lua script file loader and function executer */ public class LuaScript implements Script { private Globals globals = JsePlatform.standardGlobals(); private LuaValue chunk; // Script exists and is otherwise loadable private boolean scriptFileExists; // Keep the file name, so it can be reloaded when needed public String scriptFileName; // store the returned values from function public Varargs lastResults; // Init the object and call the load method public LuaScript(String scriptFileName) { this.scriptFileName = scriptFileName; this.scriptFileExists = false; this.load(scriptFileName); } // Load the file public boolean load(String scriptFileName) { this.scriptFileName = scriptFileName; URL scriptFile = getClass().getClassLoader().getResource(scriptFileName); if (scriptFile == null) { this.scriptFileExists = false; return false; } else { this.scriptFileExists = true; } try { //chunk = globals.load(Gdx.files.internal(scriptFileName).readString()); URI uri = getClass().getClassLoader().getResource(scriptFileName).toURI(); Path path = Paths.get(uri); byte[] data = Files.readAllBytes(path); String scriptText = new String(data); chunk = globals.load(scriptText); } catch (LuaError | IOException | URISyntaxException e) { // If reading the file fails, then log the error to the console this.scriptFileExists = false; return false; } // An important step. Calls to script method do not work if the chunk is not called here chunk.call(); return true; } // Load the file again @Override public boolean reload() { return this.load(this.scriptFileName); } // Returns true if the file was loaded correctly @Override public boolean canExecute() { return scriptFileExists; } // Call the init function in the Lua script with the given parameters passed @Override public boolean executeInit(Object... objects) { return executeFunction("init", objects); } // Call a function in the Lua script with the given parameters passed @Override public boolean executeFunction(String functionName, Object... objects) { return executeFunctionParamsAsArray(functionName, objects); } // Now this function takes the parameters as an array instead, mostly meant so we can call other Lua script functions from Lua itself public boolean executeFunctionParamsAsArray(String functionName, Object[] objects) { if (!canExecute()) { return false; } LuaValue luaFunction = globals.get(functionName); // Check if a functions with that name exists if (luaFunction.isfunction()) { LuaValue[] parameters = new LuaValue[objects.length]; int i = 0; for (Object object : objects) { // Convert each parameter to a form that's usable by Lua parameters[i] = CoerceJavaToLua.coerce(object); i++; } try { // Run the function with the converted parameters lastResults = luaFunction.invoke(parameters); //System.out.println(lastResults.toString()); } catch (LuaError e) { // Log the error to the console if failed return false; } return true; } return false; } // With this we register a Java function that we can call from the Lua script public void registerJavaFunction(TwoArgFunction javaFunction) { globals.load(javaFunction); } } |
The Execute Script which allows Lua scripts to call other Lua functions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
package com.johnday.luajtest; import org.luaj.vm2.LuaTable; import org.luaj.vm2.LuaValue; import org.luaj.vm2.lib.ThreeArgFunction; import org.luaj.vm2.lib.TwoArgFunction; public final class ExecuteScript extends TwoArgFunction { private static ExecuteScript instance = new ExecuteScript(); public ExecuteScript() {} @Override public LuaValue call(LuaValue modname, LuaValue env) { env.set("ExecuteScript", new ExecuteScriptImplementation()); return env; } public static ExecuteScript getInstance() { return instance; } static class ExecuteScriptImplementation extends ThreeArgFunction { public LuaValue call(LuaValue key, LuaValue functionName, LuaValue objects) { if (!key.isstring()) { return LuaValue.valueOf(false); } if (!functionName.isstring()) { return LuaValue.valueOf(false); } // Table is Lua's version of an array if (!objects.istable()) { return LuaValue.valueOf(false); } // Convert Lua value to a table LuaTable luaTable = objects.checktable(); // Construct a Java array from the table Object[] objectArray = new Object[luaTable.length()]; for (int i = 0; i < luaTable.length(); i++) { objectArray[i] = luaTable.get(i + 1); } // Run the Lua function functionName in script file key and pass the parameters to it, convert the result to a form that Lua can use return LuaValue.valueOf(ScriptManager.executeFunctionParamsAsArray(key.toString(), functionName.toString(), objectArray)); } } } |
Finally the ScriptManager which is used to call Lua functions in the scripts. Again here commented out code is the LibGDX version.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
package com.johnday.luajtest; import java.util.HashMap; import org.luaj.vm2.Varargs; /* * Manages the Lua script cache and provides methods to call Lua script functions */ public final class ScriptManager { //private static ObjectMap<String, LuaScript> scripts; private static HashMap<String, LuaScript> scripts; //private static JsonValue json; // Constructor public ScriptManager() { ScriptManager.scripts = new HashMap<String, LuaScript>(); //ScriptManager.json = new JsonReader().parse(Gdx.files.internal("data/scripts/scripts.json")); } // Load file by script key, returns false if fails // You don't need to call this yourself, since the function calling methods do it already public static boolean load(String key) { //if (ScriptManager.json.has(key)) { //if (ScriptManager.json.get(key).isString()) { // Get the script filename from the json file and load the script boolean success = ScriptManager.add(key, key); if (!success) { System.out.println("Loading of " + key + " script failed!"); //Gdx.app.log("Debug", "Loading of " + key + " script failed!"); } else { System.out.println("Loading of " + key + " script successful!"); //Gdx.app.log("Debug", "Loading of " + key + " script successful!"); } return success; //} //} //Gdx.app.log("Debug", "Loading of " + key + " script failed! Script not registered."); //return false; } // Add the script to cache, returns false when fails // You don't need to call this yourself, since the function calling methods do it already public static boolean add(String key, String fileName) { LuaScript script = new LuaScript(fileName); if (script.canExecute()) { // If already exists in cache, then delete the old one first if (ScriptManager.scripts.containsKey(key)) { ScriptManager.scripts.remove(key); } // Add to the cache ScriptManager.scripts.put(key, script); // Register ExecuteScript function, so it can be called from Lua scripts script.registerJavaFunction(ExecuteScript.getInstance()); return true; } else { System.out.println(key + " script " + fileName + " not found!"); //Gdx.app.log("Debug", key + " script " + fileName + " not found!"); } return false; } // Reload the script, but only if it already exists in the cache public static boolean reload(String key) { if (ScriptManager.scripts.containsKey(key)) { return add(key, ScriptManager.scripts.get(key).scriptFileName); } return false; } // Execute a Lua function functionName in script file (key) and pass the rest of the parameters to the function // Returns false when fails public static boolean executeFunction(String key, String functionName, Object... objects) { // Run the function if the script file is the cache if (ScriptManager.scripts.containsKey(key)) { return ScriptManager.scripts.get(key).executeFunction(functionName, objects); } else { // Try to load the script to the cache if (!ScriptManager.load(key)) { return false; } // Run the function return ScriptManager.scripts.get(key).executeFunction(functionName, objects); } } // Execute a Lua function functionName in script file (key) and pass the array of parameters to the function // Returns false when fails public static boolean executeFunctionParamsAsArray(String key, String functionName, Object[] objects) { // Run the function if the script file is the cache if (ScriptManager.scripts.containsKey(key)) { return ScriptManager.scripts.get(key).executeFunctionParamsAsArray(functionName, objects); } else { // Try to load the script to the cache if (!ScriptManager.load(key)) { return false; } // Run the function return ScriptManager.scripts.get(key).executeFunctionParamsAsArray(functionName, objects); } } // Execute a Lua function "init" in script file (key) and pass the array of parameters to the function // Returns false when fails public static boolean executeInit(String key, Object... objects) { // Run the function if the script file is the cache if (ScriptManager.scripts.containsKey(key)) { return ScriptManager.scripts.get(key).executeInit(objects); } else { // Try to load the script to the cache if (!ScriptManager.load(key)) { return false; } // Run the function return ScriptManager.scripts.get(key).executeInit(objects); } } // Execute a Lua function "update" in script file (key) and pass the array of parameters to the function // Returns false when fails public static boolean executeUpdate(String key, Object... objects) { return ScriptManager.executeFunction(key, "update", objects); } // Clear the whole script cache public static void dispose() { ScriptManager.scripts = new HashMap<String, LuaScript>(); } public static Varargs getLastResults(String key){ return ScriptManager.scripts.get(key).lastResults; } } |
How to use these scripts.
- Create a Source folder for your modding scripts. I called mine Mods 🙂
- Add your lua scripts to your Mods folder
- Use example code below to create your own Lua function calls.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// create the ScriptManager ScriptManager sm = new ScriptManager(); //check script exists if(ScriptManager.load("luajtest.lua")){ // Script exists and is now loaded } // execute the Init function if(ScriptManager.executeInit("powerPlantTest.lua", "")){ // get the last results Varargs res = ScriptManager.getLastResults("powerPlantTest.lua"); //loop through result data for(int i = 1; i <= res.narg(); i++){ // check if 1st arg is a string if(res.arg(i).isstring()){ if(res.arg(i).tojstring().equalsIgnoreCase("powerplant")){ PowerPlant pp = new PowerPlant("powerPlantTest.lua"); System.out.println(pp.getPPT()); System.out.println("pp"); }else if(res.arg(i).tojstring().equalsIgnoreCase("np")){ System.out.println("np"); } } } } // execute gethp function in script "luatest.lua" passing "" ScriptManager.executeFunction("luatest.lua", "gethp", ""); // Execute script "luatest.lua" use function Update and pass "1234" ScriptManager.executeUpdate("luatest.lua", "1234"); // Execute Script "luatest.lua" and use function "getnoise" with the parameters new Object[]{10,10} ScriptManager.executeFunction("luatest.lua", "getnoise", new Object[]{10,10}); |
Further Reading:
Adapted From Code by: Riho Peterson 2014
then how to make the game when we script the game?