Tuesday, June 26, 2012

Single Canvas Theory

When you decide to create a custom UI based on low level graphics you might be tempted to fall into one of the following: have a [Game]Canvas child for each screen or have a single [Game]Canvas child to render all the UI.

I advocate to use a single [Game]Canvas child because we do not how how much memory each [Game]Canvas instance might need. Different JVMs will use more or less memory for [Game]Canvas and Displayable classes.

But it is a bad idea to keep all UI logic in a sigle file. We could easily get above a thousand lines of code.

A simple way to use a single Canvas child and split UI logic in different classes is to use a Paintable class. It will have all event delivery methods from Canvas ([show|hide]Notify, key[Press|Repeat|Releas]ed, pointer[Press|Dragg|Releas]ed and paint) so it can render itself and react to visibility transiction and user input. If we were to use a single GameCanvas instance the approach would be different.

The differences to Canvas are:

  • key[Press|Repeat|Releas]ed methods will receive an extra parameter: action, which is initiated with getGameAction(keyCode).
  • key[Press|Repeat|Releas]ed and pointer[Press|Dragg|Releas]ed methods will return boolean: true if the screen needs to be repainted.
  • from all other Canvas methods only get[Width|Height] are really necessary
The Paintable instance will have a CommandListener attribute to fire specific events to the Canvas: repaint, exit or change screen.

Bellow is the code for the Paintable class:

abstract class Paintable {

  private int width, height;
  private CommandListener listener;

  final void init(CommandListener listener, int w, int h) {
    this.listener = listener;
    this.width = w;
    this.height = h;
  }

  final public int getWidth() {
    return width;
  }

  final public int getHeight() {
    return height;
  }

  final void repaint() {
    listener.commandAction(MainCanvas.CMD_REPAINT, null);
  }

  final void exit() {
    listener.commandAction(MainCanvas.CMD_EXIT, null);
  }

  /**
   * @param id one of SCR constants from MainCanvas
   */
  final void show(int id) {
    Command change = new Command("", Command.SCREEN, id);

    listener.commandAction(change, null);
  }

  boolean keyPressed(int keyCode, int gameAction) {
    return false;
  }

  boolean pointerPressed(int x, int y) {
    return false;
  }

  void showNotify() {
  }

  void hideNotify() {
  }

  abstract void paint (Graphics g);
}
Below is my MainCanvas class:
public class MainCanvas extends Canvas 
  implements CommandListener 
{

  static final Command CMD_REPAINT = new Command("",
    Command.SCREEN, 1);
  static final Command CMD_EXIT = new Command("", 
    Command.EXIT, 1);
  
  static final int SCR_SPLASH = 1;
  static final int SCR_MENU = 2;
  static final int SCR_PLAY = 3;
  
  private Paintable paintable;
  private int paintableId;
  private MIDlet midlet;
  
  public MainCanvas(MIDlet midlet) {
    this.midlet = midlet;
  }
  
  public int getGameAction(int keyCode) {
    int action = 0;

    try {
      action = super.getGameAction(keyCode);
    } catch (Exception e) {}

    return action;
  }
  
  protected void keyPressed(int keyCode) {
    if (paintable.keyPressed(keyCode, getGameAction(keyCode)))
      repaint();
  }
  
  protected void pointerPressed(int x, int y) {
    if (paintable.pointerPressed(x, y))
      repaint();
  }
  
  protected void paint(Graphics g) {
    paintable.paint(g);
  }

  public void commandAction(Command c, Displayable d) {
    if (c == CMD_REPAINT) {
      repaint();
    }
    else if (c == CMD_EXIT) {
      midlet.notifyDestroyed();
    }
    else if (c.getCommandType() == Command.SCREEN) {
      changeScreen(c.getPriority());
    }
  }
  
  public void changeScreen(int id) {
    if (paintableId == id) {
      return;
    }

    Paintable nextPaintable = newPaintableFromId(id);

    if (nextPaintable != null) {
      if (paintable != null) {
        paintable.hideNotify();
      }
      nextPaintable.init(this, getWidth(), getHeight());
      paintable = nextPaintable;
      paintable.showNotify();
      paintableId = id;
      repaint();
    }
  }

  private Paintable newPaintableFromId(int id) {
    Paintable nextPaintable = null;

    switch (id) {
      case SCR_SPLASH:
        nextPaintable = new SplashScreen();
        break;
      case SCR_MENU:
        nextPaintable = new MainMenuScreen();
        break;
      case SCR_PLAY:
        nextPaintable = new BoardScreen();
        break;
    }

    return nextPaintable;
  }
}
Below is the MIDlet that initiates and show MainCanvas:
public class C extends MIDlet {

  private MainCanvas mainCanvas;
  
  public C() {
    mainCanvas = new MainCanvas(this);
    mainCanvas.changeScreen(MainCanvas.SCR_SPLASH);
  }

  protected void startApp() {
    Display.getDisplay(this).setCurrent(mainCanvas);
  }

  protected void pauseApp() {
  }

  protected void destroyApp(boolean arg0) {
  }
}
When SplashScreen needs to show MainMenuScreen it will call show(MainCanvas.SCR_MENU).

Related topics: