Libgdx, Artemis-ODB and Lua

I have been playing around with an idea of a card-based rpg battle game. With the objective to be able to quickly modify the behavior of a unit during combat without having to create a new class I decided to script the effects using Lua (this should even allow to deliver updates on cards if the scripts are gotten from a remote server as a string, for example).

Anyways, since I like using Artemis-ODB, i went on to find a way to be able to retrieve / change components from within lua..turned out to be quite simple.

To use lua with our libgdx, we are going to be using the Luaj library (supposedly pretty nice performance, but I haven't benchmarked it yet), add this to your project to add the luaj dependency in your main build.gradle file:

project(":core") {  
    dependencies {
        ...
        compile "org.luaj:luaj-jse:3.0.1"       
        ...
    }
}

So first things first, create a java interface to interact with our lua scripts:

public interface Script {  
    boolean canExecute();
    boolean executeInit(Object... objects);
    boolean executeFunction(String functionName, Object... objects);
}

This allows us to initialize a script (init function) and to execute any function contained within, with any kind of parameter.

Let's implement it (by loading luaj lua libraries in our context and all that)

public class LuaScript implements Script {

    private final Globals globals;
    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;

    // Init the object and call the load method
    public LuaScript(String scriptFileName) {
        this(scriptFileName, JsePlatform.standardGlobals());
    }

    public LuaScript(String scriptFileName, Globals globals){
        this.scriptFileExists = false;
        this.globals = globals;
        this.load(scriptFileName);
    }

    // Load the file
    public boolean load(String scriptFileName) {
        this.scriptFileName = scriptFileName;

        if (!Gdx.files.internal(scriptFileName).exists()) {
            this.scriptFileExists = false;
            return false;
        } else {
            this.scriptFileExists = true;
        }

        try {
            chunk = globals.load(Gdx.files.internal(scriptFileName).readString());
        } catch (LuaError e) {
            // If reading the file fails, then log the error to the console
            Gdx.app.log("Debug", "LUA ERROR! " + e.getMessage());
            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
                luaFunction.invoke(parameters);
            } catch (LuaError e) {
                // Log the error to the console if failed
                Gdx.app.log("Debug", "LUA ERROR! " + e.getMessage());
                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);
    }
}

Note: This is some implementation i found online to use lua with libgdx but can't find the link again, if you have it, please let me know to credit its author!

So, now we are able to load a lua script and execute any function inside it by calling the executeFunction function. As you can see, we hardcode our executeInit to call a lua init function, which forces us to have a function to initalize our script, even if the script doesn't need any initialization.

Now, lets create a custom luaj library to load our artemis world into lua.

public class MappingLib extends TwoArgFunction {  
    private final World world;

    public MappingLib(World world){
        this.world = world;
    }

    @Override
    public LuaValue call(LuaValue modname, LuaValue env) {
        LuaTable mapper = new LuaTable(0,10);
        mapper.set("component", new CompMapper(this.world));
        env.set("mapper", mapper);
        env.get("package").get("loaded").set("mapper", mapper);
        return mapper;
    }

    static class CompMapper extends OneArgFunction{
        static final StringBuilder strBuilder = new StringBuilder();
        static final String COMPONENTS_PACKAGE = "com.xguzm.gridbattle.components";
        private final World world;

        public CompMapper(World world){
            this.world = world;
        }

        @Override
        public LuaValue call(LuaValue arg) {
            arg.checkstring();
            strBuilder.length = 0;
            strBuilder.append(COMPONENTS_PACKAGE).append(".").append(arg.toString());
            Class clazz;
            try {
                clazz = ClassReflection.forName(strBuilder.toString());
            }catch(Exception e){
                throw new LuaError("Error getting class " + strBuilder.toString());
            }

            return CoerceJavaToLua.coerce(world.getMapper(clazz));
        }
    }
}

Here, we are created a library which will be set to a global variable "mapper", inside that variable we add function called "component" which will receive one parameter and return a value.
In this specific case, what we will be returning is an artemis ComponentMapper. For me, since i have all of my components under the same package, having a constant with the components package name is enough (that way, i can request the component mapper just by the name of my component).

Nice, now...lets create a system which allows us to interact with the scripts.

Since I won't have an specific "Script" component in my entities, I create a VoidEntitySystem:

public class ScriptExecutionSystem extends VoidEntitySystem {

    private ObjectMap<String, Script> scripts;
    private Globals luaGlobals;

    @Override
    protected void initialize() {
        luaGlobals = new Globals();
        luaGlobals.load(new JseBaseLib());
        luaGlobals.load(new PackageLib());
        luaGlobals.load(new LuajavaLib());
        luaGlobals.load(new MathLib());
        luaGlobals.load(new MappingLib(world));
        LoadState.install(luaGlobals);
        LuaC.install(luaGlobals);

        scripts = new ObjectMap<String, Script>();

        LuaScript dmgScript = new LuaScript("scripts/collision/damage.lua", luaGlobals);
        dmgScript.executeInit(world);
        scripts.put("damage", dmgScript);
        setPassive(true);
    }

    public void execute(String script, Object... params){
        if (scripts.containsKey(script)){
            scripts.get(script).executeFunction("execute", params);
        }
    }

    @Override
    protected void processSystem() { }
}

During the initialize, we will charge all the lua libraries (hello there MappingLib) and all the script we plan to use from files (remote server maybe?), and leave them loaded since they will most likely be used more than once during this screen lifetime.

We are almost there.... Let's create a lua script to test it out:

-- after doing damage, the collision component is removed

local collisionClass

function execute( world, atkEntity, defEntity )  
    local combatStatsMapper = mapper.component("CombatStats")
    local hitPointsMapper = mapper.component("HitPoints")
    local atkStats = combatStatsMapper:get(atkEntity)
    local defStats = combatStatsMapper:get(defEntity)

    local diff = defStats.def - atkStats.atk
    defStats.def = math.max(0, diff)

    if diff < 0 then
        local hp = hitPointsMapper:get(defEntity)
        hp.current = math.max(0, hp.current + diff)
    end

    atkEntity:edit():remove(collisionClass)
end

function init(world)  
    collisionClass = luajava.bindClass("com.xguzm.gridbattle.components.Collision")
end  

See how when we call executeInit from our system, we pass in the world...it's not used in this script, but it would allow us to do so much more.

As you can see, you can see, I have default access to the "mapper" object, which injects my own MappingLib and allows me to get the component I require, in case I needed to retrieve an EntityManager instead of a ComponentMapper it would be just a matter of creating another small lib just like CompMapper and return the desired value.

As for the way to actually execute a script from another artemis system? This is how I do it (I pass the world and both the colliding entities to the lua function, and name them accordingly)

public class CollisionDetectionSystem extends IntervalEntitySystem {

    @Override
    protected void processEntities(IntBag entities) {
        // lots of blabla to check for collisions

        AttackCollisionBehaviors behaviors = atkBMapper.get(playerUnit); //this contains a list of the 'scripts' which are going to be executed when the unit does damage
        for (String behav : behaviors.behaviorScripts) {
            world.getSystem(ScriptExecutionSystem.class).execute(behav, world, playerUnit, enemyUnit);
        }

        //more blabla
    }

}
comments powered by Disqus