Saturday, August 13, 2011

A cutscene framework for Android - part 3

We've reached the third and final part of the tutorial on how to make cutscenes. Now that we've already defined Actors and Animations, we just have to define the Cutscene. The cutscene should consist of a list of actors, the current frame and the frame at which the cutscene ends. Animations don't have to be included explicitly, because they are part of the Actor class.

You may ask why we need to explicitly define an end frame. We could also look at the frame at which all the animations of each actor have finished. If we set that frame as the end frame, the cutscene is cut off immediately after the last transition. This leaves no time to actually see what's going on. That's why we use an end frame.

public class Cutscene {
 private Set<Actor> actors;
 private int endFrame;
 private int currentFrame;
 
 public Cutscene(Context context, int cutsceneResource) {
  load(context, cutsceneResource);
  currentFrame = 0;
 }
 
 public boolean isActive() {
  return currentFrame <= endFrame;
 }
 
 public Set<Actor> getActors() {
  return actors;
 }
 
 public void update() {
  if (!isActive()) {
   return;
  }
  
  for (Actor actor : actors) {
   actor.update();
  }
  
  currentFrame++;
 }
 
 public void draw(Canvas canvas) {
  for (Actor actor : actors) {
   actor.draw(canvas);
  }
 }
} 

As you can see, the Cutscene class is pretty straightforward. Using the framework so far, we can construct a cutscene from source code, with relative ease. It would be even better if we could just read it in from an XML file. For example, look at the following structure:

<?xml version="1.0" encoding="UTF-8"?>
<cutscenes>
 <cutscene end="140">
  <actor type="text" name="Intro" x="10.0" y="40.0"
   visible="true" text="This is the starting text.">
   <animation type="text" start="50" text="@string/anim_text" />
  </actor>
  <actor type="bitmap" name="Player" visible="true" x="0.0" y="50.0" bitmap="@drawable/player">
            <animation type="translate" start="4" end="60" from_x="0.0" from_y="50.0" to_x="100.0" to_y="50.0" />
  </actor>
 </cutscene>
</cutscenes>

All the elements of a simple scene are there: there is a text actor that displays two lines of text and a player bitmap that is moved across the screen. I've added two different ways to add text: you either hardcode it, or you give a reference to a string. The latter is recommended to support internationalisation.

I've given each actor a name, so that you can easily add additional information from source code. For example, in my project the player bitmap is a subrectangle of the resource @drawable/player. When this cutscene is parsed, I look for the actor with the name Player, and I substitute the bitmap. If you want to have a custom bitmap, you should use bitmap="custom" in the XML.

Now we have to parse the file:

public void load(Context context, int id) {
    actors = new HashSet<Actor>();
    
    XmlResourceParser parser = context.getResources().getXml(id);
    try {
        int eventType = parser.getEventType();
        
        Actor currentActor = null;
        
        while (eventType != XmlPullParser.END_DOCUMENT) {
            if (eventType == XmlPullParser.START_TAG) {
                if (parser.getName().equals("cutscene")) {
                    endFrame = parser.getAttributeIntValue(null, "end", 0);
                }
                
                if (parser.getName().equals("actor")) {
                    String type = parser.getAttributeValue(null, "type");
                    String name = parser.getAttributeValue(null, "name");
                    float x = parser.getAttributeFloatValue(null, "x", 0);
                    float y = parser.getAttributeFloatValue(null, "y", 0);
                    boolean visible = parser.getAttributeBooleanValue(null,
                    "visible", false);
                    
                    /* Read type specifics. */
                    if (type.equals("text")) {
                        String text = parser
                        .getAttributeValue(null, "text");
                        int res = parser.getAttributeResourceValue(null,
                        "text", -1);
                        if (res != -1) {
                            text = context.getResources().getString(res);
                        }
                        
                        // TODO: do something for paint. */
                        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
                        paint.setColor(Color.WHITE);
                        currentActor = new TextActor(name, text,
                        new Vector2f(x, y), paint);
                        currentActor.setVisible(visible);
                    }
                    
                    if (type.equals("bitmap")) {
                        Bitmap bitmap = null;
                        if (!parser.getAttributeValue(null, "bitmap")
                        .equals("custom")) {
                            int bitmapID = parser
                            .getAttributeResourceValue(null,
                            "bitmap", -1);
                            bitmap = BitmapFactory.decodeResource(
                            context.getResources(), bitmapID);
                        }
                        
                        currentActor = new BitmapActor(name, bitmap,
                        new Paint());
                        currentActor.setVisible(visible);
                        currentActor.getTransform().setTranslate(x, y);
                    }
                    
                    if (currentActor != null) {
                        actors.add(currentActor);
                    }
                }
                
                if (parser.getName().equals("animation")) {
                    String type = parser.getAttributeValue(null, "type");
                    int start = parser.getAttributeIntValue(null, "start",
                    0);
                    int end = parser.getAttributeIntValue(null, "end",
                    start);
                    
                    if (type.equals("text")) {
                        String text = parser
                        .getAttributeValue(null, "text");
                        currentActor.addAnimation(new TextAnimation(text,
                        start));
                    }
                    
                    if (type.equals("translate")) {
                        float from_x = parser.getAttributeFloatValue(null,
                        "from_x", 0);
                        float from_y = parser.getAttributeFloatValue(null,
                        "from_y", 0);
                        float to_x = parser.getAttributeFloatValue(null,
                        "to_x", 0);
                        float to_y = parser.getAttributeFloatValue(null,
                        "to_y", 0);
                        currentActor.addAnimation(new TranslationAnimation(
                        new Vector2f(from_x, from_y), new Vector2f(
                        to_x, to_y), start, end));
                    }
                }
            }
            
            eventType = parser.next();
        }
        } catch (XmlPullParserException e) {
        e.printStackTrace();
        } catch (IOException e) {
        e.printStackTrace();
    }
}

This function reads the XML and creates a cutscene. I have left some things out, like defining a Paint in the XML, because I haven't implemented those myself. If you want to do such a thing, I would recommend creating another XML structure just for Paints. Then you can add a reference to the resource in the cutscene structure. As you can see, this framework is easily extendable.

And there you have it. In this tutorial you've seen how to build a framework for cutscenes. We've started with simple building blocks like animations and actors and finally we've seen how to put them together to make cutscenes and read these from XML files. I hope you liked it!

No comments:

Post a Comment