Embedding Mozilla Rhino
Scripting engines have started to become very popular game development tools. They allow developers to make changes to game logic without needing to recompile, or possibly even restart, the game.
This tutorial describes the steps needed to get start using the Mozilla Rhino Javascript engine in your Slick-based games.
Getting Rhino
The obvious first step is to get the Rhino distribution. Unpack the zip file and place the js.jar file in your project. Make sure to add it to your classpath and double check your build setup to be sure the jar file is included when your program is compiled and run.
Running Rhino and Slick together
The following program is a simple implementation of Rhino running inside a Slick game. It is pretty well commented, and should explain the necessary steps as they happen.
/* This file demonstrates using the Rhino Javascript engine within the Slick 2d game engine. Rhino can be found at: http://www.mozilla.org/rhino/ Slick can be found at: http://slick.cokeandcode.com/ */ import org.newdawn.slick.AppGameContainer; import org.newdawn.slick.BasicGame; import org.newdawn.slick.GameContainer; import org.newdawn.slick.Graphics; import org.newdawn.slick.Input; import org.newdawn.slick.SlickException; import org.mozilla.javascript.Context; import org.mozilla.javascript.ScriptableObject; public class Rhino extends BasicGame { /* The ScriptProxy class defined here is one half of the interface between Java code and the Javascript environment. It is used to, among other things, provide access to Java objects within Rhino. The ScriptableObject class implements the majority of the Scriptable interface. The Scriptable interface is used to define functions and objects that will be available to the javascript environment. */ class ScriptProxy extends ScriptableObject { String test1; /* This is the only method required to fully implement the abstract class ScriptableObject. I'm not sure what it is supposed to do. The "global" value is used in at least one example in the Rhino documentation. */ public String getClassName() { return "global"; } /* Access to the "test" object in the javascript environment is implemented through calls to the setTest and getTest methods. */ public void setTest(String str) { test1 = str; } public String getTest() { return test1; } /* A test function that can be called within the JavaScript environment. */ public void testfunc(String s) { // System.out.println("test function called"); System.out.println(s); } } /* A Context is an instance of the javascript environment. The context is used to load and interpret .js files and interpret Javascript code as strings, among other things. In this example it is used exclusively to interpret Javascript strings. */ protected Context scriptContext; protected ScriptProxy gameProxy; public void init(GameContainer container) throws SlickException { /* The static method Context.enter is, apparently, the easiest way to obtain a javascript environment. */ scriptContext = Context.enter(); gameProxy = new ScriptProxy(); /* We'll set a string for the "test" property that will be exposed in Javascript now so we know it is calling across the bridge. */ gameProxy.setTest("This was set within java, but called from javascript."); /* This must be called before scripts can be evaluated in this Context. It creates some of the basic Javascript objects. */ scriptContext.initStandardObjects(gameProxy); /* Functions provided by a ScriptableObject (such as our example 'testfunc') are initialized in the following manner. */ String[] scriptAvailableFunctions = { "testfunc" }; gameProxy.defineFunctionProperties(scriptAvailableFunctions, ScriptProxy.class, ScriptableObject.DONTENUM); /* A "property" is a Javascript object that is exposed through getters and setters in the ScriptableObject. Rhino automatically prepends the "set" and "get" terms and uppercases the first letter, so exact naming is important. */ gameProxy.defineProperty("test", ScriptProxy.class, ScriptableObject.DONTENUM); } public void render(GameContainer container, Graphics g) { /* Here we are using the context (javascript engine) to evaluate a string. The first parameter is our ScriptableObject, which is used to provide definitions for the engine. The second argument is the code to be evaluated. In this case simply writing "test" evaluates the Javascript object named "test", which is looked up in our gameProxy, which passed the result of it's getTest() method to the Javascript engine. Since this is the only instruction in the string to be evaluated it is used as the return value for evaluateString. To provide flexibility in return type handling evaluateString returns a java.lang.Object, which is why the result must be cast back into a String. The third parameter is a string that describes the source for the Javascript code being evaluated. This may be a filename if that is where the string comes from. The fourth parameter is the starting line number for this script, which might be useful if a script is being pieced together from a variety of components. The last parameter is used to provide a security domain for the script to run under. I *think* this is used to ensure that the Javascript code cannot execute certain methods or instantiate some objects, but haven't done enough research to be sure. Passing a null value here does not restrict the evaluation of Javascript code. */ String result = (String) scriptContext.evaluateString(gameProxy, "test;", "js", 1, null); g.drawString("Javascript result: " + result, 40, 120); } public void update(GameContainer container, int delta) { } public void keyPressed(int key, char c) { if(key == Input.KEY_ESCAPE) { /* The script context will most likely be shut down properly, but it's a good idea to be neat and tidy, right? */ scriptContext.exit(); System.exit(0); } if(key == Input.KEY_SPACE) { /* This example is largely similar to the above, except that now we are calling the javascript function "testfunc()". This is found and executed on gameProxy, printing a string to the console." */ scriptContext.evaluateString(gameProxy, "testfunc(\"testing console output\");", "js", 5, null); } } public Rhino(String s) { super(s); } public static void main(String[] Args) { try { AppGameContainer container = new AppGameContainer(new Rhino("Rhino")); container.setDisplayMode(600,480,false); // container.setShowFPS(false); container.setMinimumLogicUpdateInterval(30); container.start(); } catch (SlickException e) { e.printStackTrace(); } } }
Next Steps
The next step to consider from here is how the Javascript code handles game logic updates. One simple option is to create a Javascript function named tick that your game calls every time update() runs. This in turn calls all of the functions that need to be run in Javascript on every game update.
One obvious step to take from this point is to extend the access of the Javascript environment into your game. You don't need to use an inner class to implement your ScriptableObject, and could easily have it in another file. If you already have a scene graph it probably has quite a few methods for interacting with your game environment, which might make it a good idea to turn this into a ScriptableObject and make it available to the Javascript engine.
Since the ScriptableObject we are using also stores all of the definitions that are in scope, a second ScriptableObject can be created for a user interaction console. This allows you to expose only very specific parts of the game to the user and prevent them from directly modifying the underlying game.
Hot Loading Definitions
One interesting use for the Javascript environment is being able to modify the game logic as the game is running. This is remarkably easy to set up. We can create a command-line input prompt that runs with the game by declaring a boolean consolePrinted member variable and adding the following code to the update method:
if(!consolePrinted) { System.out.print("rhino> "); consolePrinted = true; } // Only parse input strings when the input buffer is empty so we // don't block the game loop. if(System.in.available() != 0) { byte[] b = new byte[System.in.available()]; System.in.read(b); String s = new String(b); gameProxy.setTest(s); consolePrinted = false; }
This example simply alter the test property in Javascript, but we could just as easily be executing the string directly as Javascript or using it to provide a simple command system for reloading Javascript files.