Embedding Mozilla Rhino

From Slick2D Wiki
Jump to: navigation, search

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.