J2ME vs. Android: Game Trap

| | Comments (0)
[Note] Updated: Adopted code to Android SDK 1.0 R2

To become familiar with mobile programming plattforms I've implemented a little game (a minesweeper clone) in J2ME. After having finished this I ported this game to Android.



While porting to Android I explored the following features:

  • Rendering images via View.onDraw()
  • Working with multiple Activities (Main Activity, Options Activity)
  • Passing data from one Activity to another
  • Handling Activity states (freeze, restore)
  • Handling custom menues (onCreateOptionsMenu)
  • etc.
It turned out that - as expected ;-) - separating program logic is a good thing. I used the same classes for the game logic.

Disclaimer: Please note, that my intention was to explore some features of the plattforms. There is still room for improvement as I've not implemented a timer, a highscore table etc..
Attached you can find the Eclipse (3.4) projects of both implementations: The following sources are extracts from the Android implementation:

TrapMain.java

package at.lacherstorfer.trap.android;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

public class TrapMain extends Activity {

  private TrapView trapView;
  private static int EDIT_OPTIONS = 1;

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle icicle) {
    super.onCreate(icicle);

    setContentView(R.layout.trap_main_layout);
    trapView = (TrapView) findViewById(R.id.trap);

    if (icicle != null) {
      // We are being restored
      Bundle map = icicle.getBundle("trapView");
      if (map != null) {
        trapView.restoreState(map);
      }
    }

    trapView.doStart();

  }

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // store game state
    outState.putBundle("trapView", trapView.saveState());
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    MenuItem menuItem = menu.add(0, 0, 0, "Start");
    menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {

		public boolean onMenuItemClick(MenuItem item) {
			 trapView.doStart();
			 return true;
		}
    	
    });
    
    menuItem = menu.add(0, 0, 0, "Options");
    menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {

		public boolean onMenuItemClick(MenuItem item) {
	        Intent optionsIntent = new Intent(TrapMain.this, TrapOptions.class);
	        Bundle extras = new Bundle();
	        extras.putInt("numberRows", trapView.getNumberRows());
	        extras.putInt("numberCols", trapView.getNumberCols());
	        extras.putInt("numberTraps", trapView.getNumberTraps());
	        optionsIntent.putExtras(extras);
	        startActivityForResult(optionsIntent, EDIT_OPTIONS);
            return true;
		}
	});
    return true;
  }
  
  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode == RESULT_OK) {
      trapView.doStart(data.getIntExtra("numberRows", 8),
          data.getIntExtra("numberCols", 8), data.getIntExtra("numberTraps", 8));
    }
  }

}


TrapView.java

package at.lacherstorfer.trap.android;

import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import at.lacherstorfer.trap.shared.Cell;
import at.lacherstorfer.trap.shared.Field;

public class TrapView extends View {

	private Field field;

	private int numberRows = 8;
	private int numberCols = 8;
	private int numberTraps = 8;

	private int cursorX;
	private int cursorY;

	private Drawable cellClosedImage;
	private Drawable cell0Image;
	private Drawable cell1Image;
	private Drawable cell2Image;
	private Drawable cell3Image;
	private Drawable cell4Image;
	private Drawable cellBombImage;
	private Drawable cellExplodedImage;
	private Drawable cellFlaggedImage;
	private Drawable cursorImage;

	public TrapView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		loadImages(context);
	}

	public TrapView(Context context, AttributeSet attrs) {
		super(context, attrs);
		loadImages(context);
	}

	public TrapView(Context context) {
		super(context);
		loadImages(context);
	}

	public void loadImages(Context context) {
		cellClosedImage = context.getResources().getDrawable(
				R.drawable.cellclosed);
		cell0Image = context.getResources().getDrawable(R.drawable.cell0);
		cell1Image = context.getResources().getDrawable(R.drawable.cell1);
		cell2Image = context.getResources().getDrawable(R.drawable.cell2);
		cell3Image = context.getResources().getDrawable(R.drawable.cell3);
		cell4Image = context.getResources().getDrawable(R.drawable.cell4);
		cellBombImage = context.getResources().getDrawable(R.drawable.cellbomb);
		cellExplodedImage = context.getResources().getDrawable(
				R.drawable.cellexploded);
		cellFlaggedImage = context.getResources().getDrawable(
				R.drawable.cellflagged);
		cursorImage = context.getResources().getDrawable(R.drawable.cursor);
		setFocusable(true);
	}

	public Bundle saveState() {
		Bundle map = new Bundle();
		map.putInt("numberRows", Integer.valueOf(numberRows));
		map.putInt("numberCols", Integer.valueOf(numberCols));
		map.putInt("numberTraps", Integer.valueOf(numberTraps));
		map.putInt("cursorX", Integer.valueOf(cursorX));
		map.putInt("cursorY", Integer.valueOf(cursorY));
		int[] fieldState = field.getState();
		for (int i = 0; i < fieldState.length; i++) {
			map.putInt("field-" + i, fieldState[i]);
		}
		return map;
	}

	public void restoreState(Bundle icicle) {
		numberRows = icicle.getInt("numberRows");
		numberCols = icicle.getInt("numberCols");
		numberTraps = icicle.getInt("numberTraps");
		cursorX = icicle.getInt("cursorX");
		cursorY = icicle.getInt("cursorY");
		int[] fieldState = new int[icicle.size() - 5];
		for (int i = 0; i < fieldState.length; i++) {
			fieldState[i] = icicle.getInt("field-" + i);
		}
		field = new Field(fieldState);
	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		Log.i("TrapView", "pressed key=" + keyCode);
		boolean handled = false;
		if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
			cursorX -= cursorX > 0 ? 1 : 0;
			handled = true;
		} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
			cursorX += cursorX < numberCols - 1 ? 1 : 0;
			handled = true;
		} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
			cursorY -= cursorY > 0 ? 1 : 0;
			handled = true;
		} else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
			cursorY += cursorY < numberRows - 1 ? 1 : 0;
			handled = true;
		} else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
				|| keyCode == KeyEvent.KEYCODE_ENTER) {
			fire();
			handled = true;
		} else if (keyCode == KeyEvent.KEYCODE_SPACE) {
			flag();
			handled = true;
		}
		if (handled) {
			postInvalidate();
		}
		return handled;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		boolean handled = false;
		if (event.getAction() == MotionEvent.ACTION_DOWN) {
			cursorX = (int) (event.getX() / 15);
			cursorX = cursorX > numberCols ? numberCols - 1 : cursorX;
			cursorY = (int) (event.getY() / 15);
			cursorY = cursorY > numberRows ? numberRows - 1 : cursorY;
			handled = true;
		}
		postInvalidate();
		return handled;
	}

	private void fire() {
		if (field == null)
			return; // game not started yet
		int cellValue = field.fireAt(cursorY, cursorX);
		if (cellValue == Cell.TRAP) {
			field.reveal();
			Builder b = new AlertDialog.Builder(this.getContext());
			b.setTitle("Trap");
			b.setIcon(0);
			b.setMessage("Game Over");
			b.show();
		} else if (field.isSolved()) {
			Builder b = new AlertDialog.Builder(this.getContext());
			b.setTitle("Trap");
			b.setIcon(0);
			b.setMessage("Congratulation, You Win!");
			b.show();
		}
	}

	private void flag() {
		if (field == null)
			return; // game not started yet
		field.flagAt(cursorY, cursorX);
		if (field.isSolved()) {
			Builder b = new AlertDialog.Builder(this.getContext());
			b.setTitle("Trap");
			b.setIcon(0);
			b.setMessage("Congratulation, You Win!");
			b.show();
		}
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		if (field == null)
			return; // game not started yet

		// draw the field
		for (int row = 0; row < numberRows; row++) {
			for (int col = 0; col < numberCols; col++) {
				Cell cell = field.getCellAt(row, col);
				Drawable cellImage = getDrawable(cell);
				cellImage.setBounds(/* left */col * 15, /* top */row * 15, /* right */
				col * 15 + 15, /* bottom */row * 15 + 15);
				cellImage.draw(canvas);
			}
		}

		// draw the cursor
		cursorImage.setBounds(cursorX * 15, cursorY * 15, cursorX * 15 + 15,
				cursorY * 15 + 15);
		cursorImage.draw(canvas);
	}

	public void doStart(int numberRows, int numberCols, int numberTraps) {
		this.numberRows = numberRows;
		this.numberCols = numberCols;
		this.numberTraps = numberTraps;
		field = new Field(numberRows, numberCols, numberTraps);
		postInvalidate();
	}

	public void doStart() {
		if (field == null) {
			field = new Field(numberRows, numberCols, numberTraps);
		} else {
			field.restart();
		}
		postInvalidate();
	}

	private Drawable getDrawable(Cell cell) {
		if (cell.isOpen()) {
			switch (cell.getValue()) {
			case Cell.TRAP:
				return cellBombImage;
			case 0:
				return cell0Image;
			case 1:
				return cell1Image;
			case 2:
				return cell2Image;
			case 3:
				return cell3Image;
			case 4:
				return cell4Image;
			}
		} else if (cell.isClosed()) {
			return cellClosedImage;
		} else if (cell.isFlagged()) {
			return cellFlaggedImage;
		} else if (cell.isExploded()) {
			return cellExplodedImage;
		} else {
			throw new RuntimeException("Internal Error");
		}
		return null;
	}

	public int getNumberRows() {
		return numberRows;
	}

	public int getNumberCols() {
		return numberCols;
	}

	public int getNumberTraps() {
		return numberTraps;
	}

}


TrapOptions.java

package at.lacherstorfer.trap.android;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;

public class TrapOptions extends Activity {
	
	private EditText etNumberCols;
	private EditText etNumberRows;
	private EditText etNumberTraps;
	
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle icicle) {
		super.onCreate(icicle);
		setContentView(R.layout.trap_options_layout);
		etNumberRows = (EditText) findViewById(R.id.number_rows);
		etNumberCols = (EditText) findViewById(R.id.number_cols);
		etNumberTraps = (EditText) findViewById(R.id.number_traps);
		Bundle extras = getIntent().getExtras();
		etNumberRows.setText(""+extras.getInt("numberRows"));
		etNumberCols.setText(""+extras.getInt("numberCols"));
		etNumberTraps.setText(""+extras.getInt("numberTraps"));
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		super.onCreateOptionsMenu(menu);
		
	    MenuItem menuItem = menu.add(0, 0, 0, "Ok");
	    menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
			public boolean onMenuItemClick(MenuItem item) {
				int numberRows = Integer.parseInt(etNumberRows.getText().toString());
				int numberCols = Integer.parseInt(etNumberCols.getText().toString());
				int numberTraps = Integer.parseInt(etNumberTraps.getText().toString());
				Bundle b = new Bundle();
				b.putInt("numberRows", numberRows);
				b.putInt("numberCols", numberCols);
				b.putInt("numberTraps", numberTraps);
				Intent resultIntent = new Intent();
				resultIntent.putExtras(b);
				setResult(RESULT_OK, resultIntent);
				finish();
				return true;
			}
	    });
		
	    menuItem = menu.add(0, 0, 0, "Cancel");
	    menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
			public boolean onMenuItemClick(MenuItem item) {
				setResult(RESULT_CANCELED);
				finish();
				return true;
			}
	    });
		return true;
	}

}


trap_main_layout.xml

<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <at.lacherstorfer.trap.android.TrapView
      android:id="@+id/trap"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent" />
      
</FrameLayout>


trap_options_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    
    <TableRow>
    	<TextView android:layout_width="wrap_content" 
              android:layout_height="wrap_content" 
              android:text="Number Columns"/>
    	<EditText android:id="@+id/number_cols"
    			  android:layout_width="fill_parent" 
            	  android:layout_height="wrap_content"
            	  android:maxLength="2" />
    </TableRow>
    <TableRow>
    	<TextView android:layout_width="wrap_content" 
              android:layout_height="wrap_content" 
              android:text="Number Rows"/>
    	<EditText android:id="@+id/number_rows"
    			  android:layout_width="wrap_content" 
            	  android:layout_height="wrap_content"
            	  android:maxLength="2" />
    </TableRow>
    <TableRow>
    	<TextView android:layout_width="wrap_content" 
              android:layout_height="wrap_content" 
              android:text="Number Traps"/>
    	<EditText android:id="@+id/number_traps"
    			  android:layout_width="fill_parent" 
            	  android:layout_height="wrap_content"
            	  android:maxLength="2"/>
    </TableRow>  
</TableLayout>


AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="at.lacherstorfer.trap.android">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name="TrapMain" android:label="@string/app_name">
            <intent-filter>
                <action android:value="android.intent.action.MAIN"
                  android:name="android.intent.action.MAIN"/>
                <category android:value="android.intent.category.LAUNCHER"
                  android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="TrapOptions"
          android:label="@string/app_name"/>
    </application>
</manifest> 

Leave a comment

About this Entry

This page contains a single entry by Harald Lacherstorfer published on March 18, 2008 9:26 PM.

Android How-To: Create a new Contact is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.