Android Game Framework

This is an attempt to provide the skeleton of an Android motion graphics game which you will be able to adapt for your own games. It is based on Daniel Nadeau's beginner's tutorial, Android Canvas. We show how to make an image move and how you can use standard buttons to control the motion. As usual, changing existing code is much easier than producing your own program from scratch. We learned with applets to carry out calculations in the run method of the Runnable interface then to use the results in the paint method. Here we carry out calculations in the run method of our thread (and also in a button event-handling method) and display the results of the calculations in the onDraw method of the SurfaceView (our DrawingPanel class). The run method calls onDraw with the instruction postInvalidate. Daniel finds that this performs better than calling onDraw directly.

The program demonstrates several facilities of the language:

  • setting the layout in code (instead of using a layout file) as demonstrated in the sample app HelloWorldManualUI supplied with Oxygene for Java;
  • displaying text with the drawText method of a canvas;
  • obtaining an image from resources (one medium size alien.png image in the folder \res\drawable-mdpi) and displaying it with the drawBitmap method of a canvas;
  • object-oriented code with private fields and public getter and setter methods in a Java-like manner (instead of using properties as demonstrated on the following page);
  • callbacks to instruct the called method to call a method implemented by the calling class. The interface SurfaceHolder.Callback has the methods surfaceChanged, surfaceCreated and surfaceDestroyed. All methods in the interface will be called and therefore must be implemented even if, as in surfaceChanged, the method body is empty.

The aim of this trivial game is either to guide the alien away from the yellow obstacle, or towards it when you have had enough. We found the motion graphics in the Nexus S emulator to be painfully slow so now run the game in AndroBOX.

Game playing in the AndroBOX VM

Game playing in the AndroBOX VM

The next screenshot shows the display of text as the game finishes.

End of game

End of game

We hope that budding game developers amongst you will be able to understand much of the following code and:
  • create your own player and/or alien and/or missile classes similar to AlienObject;
  • control their movement by performing calculations in the thread run method and in the ButtonOnClick method;
  • render them all in the onDraw method.

This is our first attempt at a framework and we expect that some of you will offer suggestions for improvement as you gain experience in game development. We look forward to feedback and to publishing our first student Android apps later this year (2013).

Download here a zip file containing the code below (in game_framework1.txt), the code of the program adapted to use properties that we show on the next page (in game_framework2.txt) and alien.bmp.

The code of MainActivity.pas

namespace org.me.game_framework1;

interface 

uses
  android.app, android.os, android.util, android.view,  android.widget,
  android.content.*, android.graphics.*, java.util.concurrent;

type
  MainActivity = public class(Activity)
  private
    const
      DX = 30;
      DY = 30;
    var
      dp : DrawingPanel;
      btnUp, btnDown, btnLeft, btnRight, btnRestart : Button;
      myAlien : AlienObject;
    class var collided : Boolean := false;  // Must be class var to be acccessible by getCollided and setCollided 
  protected
    method onCreate(savedInstanceState : Bundle); override;
    method ButtonOnClick(v : View);
  public
    class method getCollided : Boolean;
    class method setCollided(collision : Boolean);   
  end;

  DrawingPanel = class(SurfaceView, SurfaceHolder.Callback)
  private   
    _alien, finalAlien : AlienObject;   
    alien_id : Integer;
    alien : Drawable;
    bmpAlien : Bitmap;
    rectObstacle : Rect := new Rect(150, 250, 250, 260);
    paint : Paint;
     _thread : PanelThread;
  public
    constructor(context : Context);
    method onDraw(canvas : Canvas); override;
    method surfaceChanged(arg0 : SurfaceHolder; arg1, arg2, arg3 : Integer);
    method surfaceCreated(holder : SurfaceHolder);
    method surfaceDestroyed(holder : SurfaceHolder);
    method getBMP : Bitmap;
    method setAlienObject(ao : AlienObject);
    method getAlienObject : AlienObject;
    method setThread(pt : PanelThread);
    method getThread : PanelThread;
  end;

  AlienObject = class(Object)
  private
    x, y, width, height : Integer; 
    crashed : Boolean := false;
  public 
    constructor(newX, newY, newWidth, newHeight : Integer);
    method set_X(newX : Integer);
    method set_Y(newY : Integer);
    method get_X : Integer; 
    method get_Y : Integer;
    method get_Width : Integer; 
    method get_Height : Integer;
    method getBounds : Rect;
  end;

  PanelThread = class(Thread)
  private
    _surfaceHolder : SurfaceHolder;
    _panel : DrawingPanel;
    _run : Boolean := false;
  public
    constructor (surfaceHolder : SurfaceHolder; panel : DrawingPanel);
    method setRunning(running : Boolean);  //Allow us to stop the thread
    method run;  override;   
  end; 

implementation

method MainActivity.ButtonOnClick(v : View);
var
  tempAlien : AlienObject;
begin
  tempAlien := dp.getAlienObject;
  if v.equals(btnUp) then
    tempAlien.set_Y(tempAlien.get_Y - DY)
  else if v.equals(btnDown) then
     tempAlien.set_Y(tempAlien.get_Y + DY)
  else if v.equals(btnLeft) then
     tempAlien.set_X(tempAlien.get_X - DX)
  else if v.equals(btnRight) then
    tempAlien.set_X(tempAlien.get_X + DX)
  else if v.equals(btnRestart) and getCollided then
    begin
      tempAlien.set_X(10);
      tempAlien.set_Y(20);
      setCollided(false);
    end; 
  dp.setAlienObject(tempAlien);
end;

method MainActivity.onCreate(savedInstanceState : Bundle);
var
  mainLayout, buttonLayout : LinearLayout;
begin
  inherited;
  mainLayout := new LinearLayout(Self);
  //Make a linear layout that fills the screen and centres child views horizontally & vertically
  mainLayout.Orientation := LinearLayout.VERTICAL;
  mainLayout.LayoutParams := new LinearLayout.LayoutParams(
      LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
  mainLayout.Gravity := Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL;
  //Make a linear layout for a row of four buttons
  buttonLayout := new LinearLayout(Self);
  buttonLayout.Orientation := LinearLayout.HORIZONTAL;
  buttonLayout.LayoutParams := new LinearLayout.LayoutParams(
      LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
  buttonLayout.Gravity := Gravity.CENTER_VERTICAL or Gravity.FILL_HORIZONTAL;
  //Create 4 buttons
  btnUp := new Button(Self);
  btnUp.Text := 'Up';
  btnUp.Width := 100;
  btnDown := new Button(Self);
  btnDown.Text := 'Down';
  btnDown.Width := 100;
  btnLeft := new Button(Self);
  btnLeft.Text := 'Left';
  btnLeft.Width := 100;
  btnRight := new Button(Self);
  btnRight.Text := 'Right';
  btnRight.Width := 100;
  btnRestart := new Button(self);
  btnRestart.Text := 'Restart';
  btnRestart.Width := 100;
  //Add buttons to their layout
  buttonLayout.addView(btnUp);
  buttonLayout.addView(btnDown);
  buttonLayout.addView(btnLeft);
  buttonLayout.addView(btnRight); 
  buttonLayout.addView(btnRestart); 

  mainLayout.addView(buttonLayout);  //Adds button layout to main layout
  dp := new DrawingPanel(self); //Creates drawing panel
  mainLayout.addView(dp);       //Adds drawing panel to main layout
  ContentView := mainLayout;
  //Set listeners 
  btnUp.setOnClickListener(@ButtonOnClick);
  btnDown.setOnClickListener(@ButtonOnClick);
  btnLeft.setOnClickListener(@ButtonOnClick);
  btnRight.setOnClickListener(@ButtonOnClick);
  btnRestart.setOnClickListener(@ButtonOnClick);
end;

class method MainActivity.getCollided : Boolean;
begin
  result := collided;
end;
class method MainActivity.setCollided(collision : Boolean);
begin
  collided := collision;                             
end;

constructor DrawingPanel(context : Context);
begin
  inherited;
  getHolder.addCallback(self);
  paint := new Paint;
  paint.setStyle(paint.Style.FILL);
  alien_id := Resources.getIdentifier('alien', "drawable", context.PackageName);
  alien := Resources.getDrawable(alien_id);
  bmpAlien := BitmapDrawable(alien).getBitmap;
  _alien := new AlienObject(10, 20, bmpAlien.Width, bmpAlien.Height); 
  finalAlien := new AlienObject(0, 0, bmpAlien.Width, bmpAlien.Height); 
end;

method DrawingPanel.onDraw(canvas : Canvas);
begin
  paint.setColor($ff000060);
  canvas.drawPaint(paint);   // Makes the entire canvas dark blue 
  paint.setColor(Color.YELLOW);  
  canvas.drawRect(rectObstacle, paint);
  if not MainActivity.getCollided then
    begin
      canvas.drawBitmap(getBMP, _alien.get_X, _alien.get_Y, paint);
      if Rect.intersects(_alien.getBounds, rectObstacle) then
        begin 
          finalAlien.set_X(_alien.get_X);
          finalAlien.set_Y(_alien.get_Y);
          MainActivity.setCollided(true);
        end; 
    end
  else
    begin
      canvas.drawBitmap(getBMP, finalAlien.get_X, finalAlien.get_Y, paint);
      paint.setColor(Color.MAGENTA);
      paint.TextSize := 48;
      canvas.drawText('Game Over', 100, 250, paint);  
    end;
end;

method DrawingPanel.surfaceChanged(arg0 : SurfaceHolder; arg1, arg2, arg3 : Integer);
begin
end;

method DrawingPanel.surfaceCreated(holder : SurfaceHolder);
var
  newThread : PanelThread;
begin
  setWillNotDraw(false); //Allows us to use postInvalidate to call onDraw
  newThread := new PanelThread(getHolder, self); //Start the thread that
  newThread.setRunning(true);                    //will make calls to
  newThread.start;                               //onDraw
  setThread(newThread);
end;

method DrawingPanel.surfaceDestroyed(holder : SurfaceHolder);
begin
  try
    getThread.setRunning(false); //Tells thread to stop
    getThread.join; //Removes thread from mem.
  except
    on e : InterruptedException do
      begin
      end;
  end;
end; 

method DrawingPanel.getBMP : Bitmap;
begin
  result := bmpAlien;
end;

method DrawingPanel.setAlienObject(ao : AlienObject);
begin
  _alien := ao;
end;

method DrawingPanel.getAlienObject : AlienObject;
begin
  result := _alien;
end;

method DrawingPanel.setThread(pt : PanelThread);
begin
  _thread := pt;
end;

method DrawingPanel.getThread : PanelThread;
begin
  result := _thread;
end;

constructor AlienObject(newX, newY, newWidth, newHeight : Integer);
begin
  x := newX;
  y := newY;
  width := newWidth;
  height := newHeight;
end;

method AlienObject.set_X(newX : Integer);
begin
  x := newX;
end;

method AlienObject.set_Y(newY : Integer);
begin
  y := newY;
end;

method AlienObject.get_X : Integer;
begin
  result := x;
end;

method AlienObject.get_Y : Integer;
begin
  result := y;
end; 

method AlienObject.get_Width : Integer;
begin
  result := width;
end;

method AlienObject.get_Height : Integer;
begin
  result := height;
end; 

method AlienObject.getBounds : Rect;
begin
  result := new Rect(get_X, get_Y, get_X + width, get_Y + height);
end;

constructor PanelThread(surfaceHolder : SurfaceHolder; panel : DrawingPanel);
begin
  _surfaceHolder := surfaceHolder;
  _panel := panel;
end;

method PanelThread.setRunning(running : Boolean);  //Allow us to stop the thread
begin
  _run := running;
end;

method PanelThread.run;
var
  c : Canvas;
  tempAlien : AlienObject;
  tempX, tempY : Integer;
begin
  while _run do //When setRunning(false) occurs, _run is
    begin         //set to false and loop ends, stopping thread
       c := nil;
       try
         c := _surfaceHolder.lockCanvas(nil);
         locking _surfaceHolder do
           begin
             tempAlien := _panel.getAlienObject;
             tempX := tempAlien.get_X + 1;
             if (tempX + tempAlien.get_Width) > _panel.Right then 
               tempX := _panel.Left;
             tempY := tempAlien.get_Y + 1;
             if (tempY + tempAlien.get_Height) > _surfaceHolder.SurfaceFrame.bottom  then 
               tempY := _surfaceHolder.SurfaceFrame.top;
             tempAlien.set_X(tempX);
             tempAlien.set_Y(tempY);
             _panel.setAlienObject(tempAlien);
             _panel.postInvalidate;
             sleep(5);
           end;
       finally
         if not (c = nil) then
           _surfaceHolder.unlockCanvasAndPost(c);
       end;
    end;
end;

end.

Strings File

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="app_name">Game Framework</string>
</resources>
Programming - a skill for life!

How to write games in Oxygene for Java