Saturday, October 20, 2007

Superfast Model Driven Development with EMF (II)

Creating the actual adventure game
In the previous posting I used EMF to create a model and the generated editor to create a file (My.adventure) containing the definition of my text adventure game. In this posting I'm going to create the actual game.

Copy and paste the My.adventure file created in the previous posting into the src directory of the EMF_Adventure project. Also create a game package where we'll do our development. One of the reasons to do the actual development in a different place or different package than the code generated by EMF is that this way, if you make a change to your EMF code you can simply delete all the generated code and regenerate it, without having to watch out for your own files. So the EMF-generated code is in the adventure package and our code is in the game package.

The little text adventure that we'll be writing allows you to move from one location to another. It allows you to pick up items you may find or perform actions in order to find hidden items. Certain items can only be obtained once you have other items first. For example, in the sample adventure game definition (which is based on a game I wrote with my kids, get it here) you need a spade in order to dig up the treasure.

To give you an idea, here's the output from a sample running of it:
Welcome to Adventure Island. This is the start of the adventure game. You are at the house of a Viking.
Hints: (q)uit, (i)nventory, (w)est, (e)ast, save, load, you see a Spade

Please enter command:

> pick up spade
You pick up Spade...

Viking's house.
Hints: (q)uit, (i)nventory, (w)est, (e)ast, save, load
Please enter command:

> w
You're going west

You are at the Viking's boat. Will you help the Viking with his fishing?
Hints: (q)uit, (i)nventory, (e)ast, save, load
Please enter command:

etc...

I know, it's text only, which is slightly boring, but the point of this posting is about EMF. We've added images to the locations and that's actually quite easy to do, but I'll leave it up to yourselves.

The first class we'll make is the Main class which kicks off the game and holds the game loop:
package game;
 

import java.io.BufferedReader;
import java.io.InputStreamReader;
 

public class Main {
  public static void main(String[] args) throws Exception {
    Status.intializeGame();
 

    for (;;) {
      Status.describeLocation();
      String command = new BufferedReader(
        new InputStreamReader(System.in)).readLine();
      Status.executeCommand(command);
    }
  }
}

The Status class holds the game state and controls the state transitions. The first thing it does it read the EMF model instance we've created that defines the game:
public class Status {
  static Adventure adventure;
  static Location location;
  static Inventory inventory =
    AdventureFactory.eINSTANCE.createInventory();
 

  static Map<Location, List<Action>> actionMap =
    new HashMap<Location, List<Action>>();
  static ResourceSet resourceSet = new ResourceSetImpl();
 

  public static void intializeGame() throws IOException {
    resourceSet.getResourceFactoryRegistry().
      getExtensionToFactoryMap().
      put(Resource.Factory.Registry.DEFAULT_EXTENSION,
      new XMIResourceFactoryImpl());
    resourceSet.getPackageRegistry().
      put(AdventurePackage.eNS_URI,
      AdventurePackage.eINSTANCE);
 

    URI gameDefURI = URI.createURI(Status.class.
      getResource("/My.adventure").toString());
    Resource resource = resourceSet.getResource(
      gameDefURI, true);
    adventure = (Adventure) EcoreUtil.getObjectByType(
      resource.getContents(),
      AdventurePackage.eINSTANCE.getAdventure());
    location = adventure.getStartLocation();
    initializeActions();
  }


In EMF objects are persisted in a resource, which could take many forms. I am using the XMIResource which basically is an XML file in the XMI format. Not all the objects in an EMF model have to be in the same file, objects could be spread over multiple files. To associate these files with each other EMF puts them in a ResourceSet.

The first two lines of initializeGame() set up the ResourceSet to use XMI resources and also informs the EMF package registry about our Adventure model.
Another thing you might notice in the class is the creation of the inventory object. Instead of going inventory = new InventoryImpl() (which would actually work) EMF prefers you to use the Factory pattern. So we create our inventory object by calling createInventory() on the factory instead.
Next, I'm loading the game definition from the My.adventure file. EMF wants to have a URI to the resource (rather than a simple file path).
After this I use the EcoreUtil.getObjectByType() to find my single Adventure object in the Resource that has loaded the file. There are other ways to get to the objects in a resource, the simplest way is just calling resource.getContents() which returns a list of the objects in the resource. The AdventurePackage.eINSTANCE.getAdventure() returns an EClass object that represents the Adventure EMF entity. EMF has its own metamodel which is richer than the Java reflection metamodel. It basically allows you to find out all the things that you have specified in your model, over and above what can be reflected in the Java signatures. But in a way AdventurePackage.eINSTANCE.getAdventure() in EMF-speak is similar to what would be Adventure.class in Java-speak.

We now finally have our adventure object and life becomes much easier. At this point can pretty much use the EMF model instance by utilizing Pojo bean-conventions. So we set our initial location to be the location that was marked as the start location in our Adventure definition file.
Last but not least we'll initialize the actionMap.

The actionMap is a map from a location to a list of Actions that are available on that location. These actions can be added statically (e.g. the 'quit' action is available everywhere) or dynamically, such as actions that relate to routes you can take from a particular place or actions associated with items. All actions in the game implement the game.Action interface:
public interface Action {
  /** A hint to the user how to execute this action, or
  * null if there is no direct hint.
  * @return A hint such as "(q)uit" or "You see a Spade"
  */
  String hint();
 

  /** Called on the action to see does it want to execute
  * the command. The command is passed in as an array of
  * strings. If the action does not want to execute the
  * command it can just return false.
  * @param commands The commands in an Array e.g.
  * ["pick", "up", "spade"]
  * @return true if the action was performed,
  * false otherwise.
  */
  boolean execute(String[] commands);
 

  /** The minimum number of words needed for this action
  * to qualify. So if your action is triggered by "pick
  * up spade" it would return 3. The action will not be
  * visited if the number of commands is less.
  * @return The minimum number for words required to
  * visit this action
  */
  int minLength();
 

  /** Whether or not to remove the action after
  * performing it. An item action would typically be
  * removed after you've picked it up (you can't pick
  * it up twice) but other actions normally remain.
  * @return Whether or not to remove the action after
  * successful execution.
  */
  boolean removeAfter();
}




Status.initializeActions() initializes the map of all the locations in the game with the actions that are available there.

Status.describeLocation() simply prints out the description of the current location and also prints out the hints for all the commands available here. Status.executeCommand() parses the string typed in by the user and then visits the actions registered with the current location to see whether any one action wants to execute the command. See here for the rest of the Status class, apart from the initializeGame() it's fairly simple Java. There are also a number of actions provided, such as InventoryAction, ItemAction, QuitAction and RouteAction which all take care of their particular things.

Finally, because we're using the XMI functionality to load the model, we need to make sure to specify this dependency in the bundle that holds our code. EMF generates code as OSGi bundles/eclipse plugins which use the META-INF/MANIFEST.MF file to describe their dependencies, for those who want to run this as a simple Java app we'll see later in this posting how to get these jar files on the classpath. For now just open the MANIFEST.MF file and in the Dependencies tab add 'org.eclipse.emf.ecore.xmi' to the required plugins.

Now we can actually run our game. To do this open the Main class, right-click in the editor and select 'Run As -> Java Application'. The game will start:

Welcome to Adventure Island. This is the start of the adventure game. You are at the house of a Viking.
Hints: (q)uit, (i)nventory, (w)est, (e)ast, you see a Spade
Please enter command:

All the code above is available in the Subversions repository at http://coderthoughts.googlecode.com/svn/trunk/emf_adventure for more info on how to check it out look here. Want all the projects including the generated code, get it here: http://coderthoughts.googlecode.com/svn/trunk/emf_adventure_full

In the next posting I'll use EMF to provide savegame/loadgame functionality and run the game as a plain Java App that uses EMF outside of Eclipse.

4 comments:

David Bosschaert said...

Note that the projects in subversion are based on JDK 1.6, but you I've used EMF with JDK 1.4 and upwards too.

AlBlue said...

You might want to change your blog code to only do the .readline() in each loop, not instantiate a new BufferedReader each time. I'm sure people will cut-n-paste the same code.

Also, what's up with for(;;) ? Isn't 'while(true)' more Java-esque?

Alex

David Bosschaert said...

Comment copied from here GlaucoCarneiro said...

I enjoy very much this blog. And I have read all the tutorials.

I would appreciate if you explain me what's going on with the following exception. I have already reviewed everything, but I can not find out the solution.

Exception in thread "main" org.eclipse.emf.ecore.resource.impl.ResourceSetImpl$1DiagnosticWrappedException: org.eclipse.emf.ecore.xmi.PackageNotFoundException: Package with uri 'http://adventure' not found. (file:/C:/.../EMF_Adventure1/bin/My.adventure.xml, 2, 120)
at org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.handleDemandLoadException(ResourceSetImpl.java:316)
at org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.demandLoadHelper(ResourceSetImpl.java:275)
at org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.getResource(ResourceSetImpl.java:398)
at game.Status.intializeGame(Status.java:39)
at game.Main.main(Main.java:9)
Caused by: org.eclipse.emf.ecore.xmi.PackageNotFoundException: Package with uri 'http://adventure' not found. (file:/C:/.../EMF_Adventure1/bin/My.adventure.xml, 2, 120)

David Bosschaert said...

Hi GlaucoCarneiro, I wasn't able to reproduce your problem, but the easiest way to get all this code to work is to take JDK 1.6 and use Eclipse 3.3.1.1 as outlined here.
Then check out all 5 projects from SVN from this location: http://coderthoughts.googlecode.com/svn/trunk/emf_adventure_full
Finally run the game.Main class in the EMF_Adventure project as a Java Application and it should all work. For more information on checking out the projects from SVN straight into your eclipse workspace, see this posting and search for the word 'subversion' in that posting.

Hope this helps, David