目 录

J2ME RPG游戏边学边做(一)

笔者以前是做j2ee的,一个月前由于兴趣所致开始利用业余时间学习j2me游戏开发。在网上看了一通教程后,便准备动手一边学一边做一个简单的rpg游戏。虽然起点比较高,但感觉游戏难度越大,遇到的问题也就越多。这样待全部解决后,也就学的差不多了。另外只有做这种游戏才有学下去的兴趣。以后我会每次把写成的源码及思路发上来,供大家指正。希望各位高人可以无私的帮助我。

下面是游戏中所需要的部分图片,还有的图片下次写到了再上传:

hero_left.png:

hero_right.png:

hero_up.png:

hero_down.png:


整个程序的主体在BraveCanvas类中完成,该类继承于GameCanvas实现Runnable接口,由BraveMIDlet类启动他, BraveMIDlet继承MIDlet实现CommandListener接口。BraveMIDlet类代码如下:(由于只是一个简单的rpg,开头画面和菜单全部省略了,改为由一个按扭启动游戏)

 

BraveMIDlet.java

package brave;

import javax.microedition.lcdui.*;

import javax.microedition.midlet.*;

public class BraveMIDlet extends MIDlet implements CommandListener

{

 private Display d ;

 private Command exitCommand ;

 private Command startCommand ;

    private BraveCanvas braveCanvas;

 public BraveMIDlet()

 {

 d = Display.getDisplay(this);

 exitCommand = new Command("退出",Command.EXIT,1);

 startCommand = new Command("开始",Command.SCREEN,1);

 }

 public void startApp()

 {

 //创建BraveCanvas

 braveCanvas = new BraveCanvas();

 braveCanvas.addCommand(exitCommand);

 braveCanvas.addCommand(startCommand);

 braveCanvas.setCommandListener(this);

 //装载BraveCanvas

 d.setCurrent(braveCanvas);

 }

 public void pauseApp()

 {

 }

 public void destroyApp(boolean unconditional)

 {

 }

 public void commandAction(Command c , Displayable dpa)

 {

 String str_co = c.getLabel();

 if(str_co.equals("开始"))

 {

  //运行BraveCanvas中的线程(启动游戏)

  braveCanvas.startup();

 }

 else if(str_co.equals("退出"))

 {

  destroyApp(false);

  notifyDestroyed();

 }

 }

}

BraveCanvas.java

package brave;

import javax.microedition.lcdui.game.GameCanvas;

import javax.microedition.lcdui.Graphics;

import java.io.IOException;

public class BraveCanvas extends GameCanvas implements Runnable

{

 private boolean sign;

 private Graphics g;

 //设置@符号的当前位置

 private int x,y;

 public BraveCanvas()

 {

 super(true);

 //初始化@位置

 x = getWidth()/2;

  y = getHeight()/2;

 }

 

 public void startup()

 {

 this.sign = true;

 Thread thread = new Thread(this);

 //启动线程

 thread.start();

 }

 public void run()

 {

 g = getGraphics();

 while(sign)

 {

  try

  {

    //@符号的移动

    input(g);

    //@符号的显示

    paint(g);

    //这里应该有详细的计算,方便为上,置为15

    Thread.sleep(15);

  }

  catch(Exception e)

  {

    System.out.println("2:"+e);

  }

 }

 }

 public void input(Graphics g) throws IOException

 {

 int keystates = getKeyStates();

 switch(keystates)

 {

  case UP_PRESSED:

    y = Math.max(0, y - 1);

    break;

  case DOWN_PRESSED:

    y = Math.min(getHeight(), y + 1);

    break;

  case LEFT_PRESSED:

    x = Math.max(0, x - 1);

    break;

  case RIGHT_PRESSED:

    x = Math.min(getWidth(), x + 1);

    break;

 }

 }

 public void paint(Graphics g)

 {

 //设置画布的背景色

 g.setColor(0x000000);

 //把背景色画满画布

 g.fillRect(0, 0, getWidth(), getHeight());

 //设置画布的前景色

 g.setColor(0xffffff);

 //在画布上写上@

 g.drawString("@", x, y, Graphics.TOP|Graphics.LEFT);

 //刷新画布

 flushGraphics();

 }

}

上面代码运行后,画面上会出现一个"@"字符随着键盘的左右而移动。虽然这离rpg游戏相差很远,但这是一个游戏的基本框架。下面我们要做的就是把"@"换成我们的英雄。为了程序清晰,我们还要设计一个英雄类Hero,该类继承于Sprite,这个类里面要用到上面的图片。

Hero.java


package brave;
import javax.microedition.lcdui.game.Sprite;
import javax.microedition.lcdui.Image;
import java.io.IOException;
public class Hero extends Sprite
{
 //设置英雄的当前位置
 private int x, y;
 private BraveCanvas braveCanvas;
 public Hero(Image image, int frameWidth, int frameHeight)
 {
  //英雄的初始化
  super(image, frameWidth, frameHeight);
 }
 public void setBraveCanvas(BraveCanvas braveCanvas)
 {
  this.braveCanvas = braveCanvas;
 }
 public void init(int x, int y)
 {
  //英雄位置的初始化
  this.x = x;
  this.y = y;
 }
 public void afresh()
 {
  //刷新英雄的位置
  setPosition(this.x, this.y);
 }
 public void moveUp() throws IOException
 {
  //英雄上移,并改为相应的图片
  setImage(Image.createImage("/hero_up.png"), 17, 26);
  nextFrame();
  this.y = Math.max(0, y - 1);
 }
 public void moveDown() throws IOException
 {
  //英雄下移,并改为相应的图片
  setImage(Image.createImage("/hero_down.png"), 17, 26);
  nextFrame();
  this.y = Math.min(braveCanvas.getHeight(), y + 1);
 }
 public void moveLeft() throws IOException
 {
  //英雄左移,并改为相应的图片
  setImage(Image.createImage("/hero_left.png"), 17, 26);
  nextFrame();
  this.x = Math.max(0, x - 1);
 }
 public void moveRight() throws IOException
 {
  //英雄右移,并改为相应的图片
  setImage(Image.createImage("/hero_right.png"), 17, 26);
  nextFrame();
  this.x = Math.min(braveCanvas.getWidth(), x + 1);
 }
}
然后把BraveCanvas类稍做修改,把"@"换成英雄
BraveCanvas.java
package brave;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.Graphics;
import java.io.IOException;
import javax.microedition.lcdui.Image;
public class BraveCanvas extends GameCanvas implements Runnable
{
 private boolean sign;
 private Graphics g;
 private Hero hero;
 public BraveCanvas()
 {
  super(true);
 }
 
 public void startup()
 {
  this.sign = true;
  try
  {
   Image heroimage = Image.createImage("/hero_up.png");
   hero = new Hero(heroimage, 17, 26);
   hero.setBraveCanvas(this);
   hero.init(40,40);
  }
  catch(Exception e)
  {
   e.printStackTrace();
  }
  Thread thread = new Thread(this);
  thread.start();
 }
 public void run()
 {
  g = getGraphics();
  while(sign)
  {
   try
   {
    input(g);
    paint(g);
    Thread.sleep(15);
   }
   catch(Exception e)
   {
    System.out.println("2:"+e);
   }
  }
 }
 public void input(Graphics g) throws IOException
 {
  int keystates = getKeyStates();
  switch(keystates)
  {
   case UP_PRESSED:
    hero.moveUp();
    break;
   case DOWN_PRESSED:
    hero.moveDown();
    break;
   case LEFT_PRESSED:
    hero.moveLeft();
    break;
   case RIGHT_PRESSED:
    hero.moveRight();
    break;
  }
  hero.afresh();
 }
 public void paint(Graphics g)
 {
  g.setColor(0x000000);
  g.fillRect(0, 0, getWidth(), getHeight());
  g.setColor(0xffffff);
  hero.paint(g);
  flushGraphics();
 }
}


这样,我们的英雄就可以在画面上移动了,不过英雄比较简单,没有hp,mp等等,只是一个图片。而且如果大家感觉英雄的脚步过快,可以在BraveCanvas类中的Hero初始化时设置:
hero.setFrameSequence(new int[]{0, 0, 1, 1, 0, 0, 2, 2 });

J2ME RPG游戏边学边做(二)
1、
public void moveDown() throws IOException
{
  //英雄下移,并改为相应的图片
  setImage(Image.createImage("/hero_down.png"), 17, 26);
  nextFrame();
  this.y = Math.min(braveCanvas.getHeight(), y + 1);
}

在io包中的读取是非常耗内存的,所以Image.createImage("/hero_down.png")放在线程的循环中确实不好,现在已经改成图象在BraveCanvas类中创建(下面的代码已更改),然后由moveDown(Image image)方法接收传递进来的图象。

2、RPG是个大工程,一个人的力量是很难完成的,我只想实现基本的RPG功能,比如人物的对话,场景的转换,战斗等等,希望大家多多给予帮助。

这一篇我将给游戏加入地图,以下是该篇所需要的图片:

background.png

foreground.png


一个RPG中的游戏地图是非常大而且多的,为了方便以后的维护,我创建了一个Scene类,该类主要是产生游戏所需要的地图。

Scene.java


package brave;
import javax.microedition.lcdui.game.TiledLayer;
import javax.microedition.lcdui.Image;
public class Scene
{
 public static TiledLayer createTiledLayerByBackground(Image image)
 {
  //使用TiledLayer创建地图
  TiledLayer tiledLayer = new TiledLayer(10, 8, image, 48, 64);
  tiledLayer.fillCells(0, 0, 10, 8, 2);
  return tiledLayer;
 }
}

修改BraveCanvas类,加入地图
BraveCanvas.java
package brave;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.Graphics;
import java.io.IOException;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.TiledLayer;

public class BraveCanvas extends GameCanvas implements Runnable
{
 private boolean sign;
 private Graphics g;
 private Hero hero;
 private Image upimage;
 private Image downimage;
 private Image leftimage;
 private Image rightimage;
 private TiledLayer backgroundMap;
 public BraveCanvas()
 {
  super(true);
 }
 public void startup()
 {
  this.sign = true;
  try
  {
   //产生地图
   backgroundMap = Scene.createTiledLayerByBackground(
    Image.createImage("/background.png"));
   
   //改正上一篇的错误
   upimage = Image.createImage("/hero_up.png");
   downimage = Image.createImage("/hero_down.png");
   leftimage = Image.createImage("/hero_left.png");
   rightimage = Image.createImage("/hero_right.png");
   hero = new Hero(upimage, 17, 26);
   hero.setFrameSequence(new int[]{1, 1, 0, 0, 1, 1, 2, 2});
   hero.setBraveCanvas(this);
   hero.init(40,40);
  }
  catch(Exception e)
  {
   e.printStackTrace();
  }
  Thread thread = new Thread(this);
  thread.start();
 }
 public void run()
 {
  g = getGraphics();
  while(sign)
  {
   try
   {
    input(g);
    paint(g);
    Thread.sleep(15);
   }
   catch(Exception e)
   {
    e.printStackTrace();
   }
  }
 }
 public void input(Graphics g) throws IOException
 {
  int keystates = getKeyStates();
  switch(keystates)
  {
   case UP_PRESSED:
    //由这里传入需要改变的图片
    hero.moveUp(upimage);
    break;
   case DOWN_PRESSED:
    hero.moveDown(downimage);
    break;
   case LEFT_PRESSED:
    hero.moveLeft(leftimage);
    break;
   case RIGHT_PRESSED:
    hero.moveRight(rightimage);
    break;
  }
  hero.afresh();
 }
 public void paint(Graphics g)
 {
  g.setColor(0x000000);
  g.fillRect(0, 0, getWidth(), getHeight());
  g.setColor(0xffffff);
  //显示地图
  backgroundMap.paint(g);
  hero.paint(g);
  flushGraphics();
 }
}

现在我们的英雄虽然能在草地上行走了,但感觉给他移动的空间太小了。而且我们在创建背景地图的时候,地图大小明明是512*480的。好,接下来我们要做的就是让英雄可以在更大的天地中活动。这里就需要一个BraveManager类来管理这些屏幕上的Sprite和TiledLayer,该类继承 LayerManager。

BraveManager.java

package brave;

import javax.microedition.lcdui.game.LayerManager;

public class BraveManager extends LayerManager

{

 private BraveCanvas braveCanvas;

 public void setBraveCanvas(BraveCanvas braveCanvas)

 {

 this.braveCanvas = braveCanvas;

 }

 public void afresh()

 {

 //确定当前试图的坐标

 //这里用一个比较简单的算法来使英雄永远在屏幕的中央

 int viewX = Math.max(0, getLayerAt(0).getX() - braveCanvas.getWidth()/2);

 int viewY = Math.max(0, getLayerAt(0).getY() - braveCanvas.getHeight()/2);

 viewX = Math.min(viewX, getLayerAt(1).getWidth() - braveCanvas.getWidth());

 viewY = Math.min(viewY, getLayerAt(1).getHeight() - braveCanvas.getHeight());

 setViewWindow(viewX, viewY, braveCanvas.getWidth(), braveCanvas.getHeight());

 }

}

修改BraveCanvas

BraveCanvas.java

package brave;

import javax.microedition.lcdui.game.GameCanvas;

import javax.microedition.lcdui.Graphics;

import java.io.IOException;

import javax.microedition.lcdui.Image;

import javax.microedition.lcdui.game.TiledLayer;

 

public class BraveCanvas extends GameCanvas implements Runnable

{

 private boolean sign;

 private Graphics g;

 private Hero hero;

 private Image upimage;

 private Image downimage;

 private Image leftimage;

 private Image rightimage;

 private TiledLayer backgroundMap;

 //创建Layer管理视图类

 private BraveManager braveManager;

 public BraveCanvas()

 {

 super(true);

 }

 public void startup()

 {

 this.sign = true;

 try

 {

  backgroundMap = Scene.createTiledLayerByBackground(

   Image.createImage("/background.png"));

  upimage = Image.createImage("/hero_up.png");

  downimage = Image.createImage("/hero_down.png");

  leftimage = Image.createImage("/hero_left.png");

  rightimage = Image.createImage("/hero_right.png");

  //创建Layer管理视图类

  braveManager = new BraveManager();

  braveManager.setBraveCanvas(this);

  hero = new Hero(upimage, 17, 26);

  hero.setFrameSequence(new int[]{1, 1, 0, 0, 1, 1, 2, 2});

  hero.setBraveCanvas(this);

  hero.setBraveManager(braveManager);

  hero.init(0,0);

 }

 catch(Exception e)

 {

  e.printStackTrace();

 }

 Thread thread = new Thread(this);

 thread.start();

 }

 public void run()

 {

 g = getGraphics();

 //插入图层

 braveManager.insert(hero, 0);

 braveManager.insert(backgroundMap, 1);

 while(sign)

 {

  try

  {

    input(g);

    paint(g);

    Thread.sleep(15);

  }

  catch(Exception e)

  {

    e.printStackTrace();

  }

 }

 }

 public void input(Graphics g) throws IOException

 {

 int keystates = getKeyStates();

 switch(keystates)

 {

  case UP_PRESSED:

    hero.moveUp(upimage);

    break;

  case DOWN_PRESSED:

    hero.moveDown(downimage);

    break;

  case LEFT_PRESSED:

    hero.moveLeft(leftimage);

    break;

  case RIGHT_PRESSED:

    hero.moveRight(rightimage);

    break;

 }

 hero.afresh();

 //刷新视图的位置

 braveManager.afresh();

 }

 public void paint(Graphics g)

 {

 g.setColor(0x000000);

 g.fillRect(0, 0, getWidth(), getHeight());

 g.setColor(0xffffff);

 //显示视图

 braveManager.paint(g, 0, 0);

 flushGraphics();

 }

}

 


这样英雄就可以在地图上任何地方行动了,不过还得改一个小地方:不知道大家还记的不,在Hero类中,我们定义英雄移动的范围最大为屏幕的尺寸。
这显然是不行的,最大的移动范围应改成地图的大小:

this.y = Math.min(braveManager.getLayerAt(1).getHeight(), y + 1);

this.x = Math.min(braveManager.getLayerAt(1).getWidth(), x + 1);


代码如下:

Hero.java


package brave;
import javax.microedition.lcdui.game.Sprite;
import javax.microedition.lcdui.Image;
import java.io.IOException;
import javax.microedition.lcdui.Graphics;
public class Hero extends Sprite
{
 private int x;
 private int y;
 private BraveCanvas braveCanvas;
 private BraveManager braveManager;
 public Hero(Image image, int frameWidth, int frameHeight)
 {
  super(image, frameWidth, frameHeight);
 }
 public void setBraveCanvas(BraveCanvas braveCanvas)
 {
  this.braveCanvas = braveCanvas;
 }
 public void setBraveManager(BraveManager braveManager)
 {
  this.braveManager = braveManager;
 }
 public void setManager(BraveCanvas braveCanvas)
 {
  this.braveCanvas = braveCanvas;
 }
 public void init(int x, int y)
 {
  this.x = x;
  this.y = y;
 }
 public void afresh()
 {
  setPosition(this.x, this.y);
 }
 public void moveUp(Image image) throws IOException
 {
  setImage(image, 17, 26);
  nextFrame();
  this.y = Math.max(0, y - 1);
 }
 public void moveDown(Image image) throws IOException
 {
  setImage(image, 17, 26);
  nextFrame();
  this.y = Math.min(braveManager.getLayerAt(1).getHeight(), y + 1);
 }
 public void moveLeft(Image image) throws IOException
 {
  setImage(image, 17, 26);
  nextFrame();
  this.x = Math.max(0, x - 1);
 }
 public void moveRight(Image image) throws IOException
 {
  setImage(image, 17, 26);
  nextFrame();
  this.x = Math.min(braveManager.getLayerAt(1).getWidth(), x + 1);
 }
}

现在英雄的移动范围大了,但只英雄一人,也太孤单了,我们给他创造一个小镇吧。

修改Scene类如下:
Scene.java
package brave;
import javax.microedition.lcdui.game.TiledLayer;
import javax.microedition.lcdui.Image;
public class Scene
{
 public static TiledLayer createTiledLayerByBackground(Image image)
 {
  TiledLayer tiledLayer = new TiledLayer(10, 8, image, 48, 64);
  tiledLayer.fillCells(0, 0, 10, 8, 2);
  return tiledLayer;
 }
 public static TiledLayer createTiledLayerByForeground(Image image)
 {
  TiledLayer tiledLayer = new TiledLayer(30, 32, image, 16, 16);
  // 30 * 32
  int[] maplist =
  {
    //0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
26 27 28 29
   0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
,0 ,0 ,0 ,0 ,0 ,//0
   0
,28,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,
30,0 ,//1
   0 ,34,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
,0 ,0 ,0 ,36,0 ,//2
   0 ,34,0 ,1 ,2 ,3 ,4 ,5 ,0 ,0 ,0 ,1 ,2 ,3 ,3 ,26,3 ,3 ,4 ,5 ,0 ,0 ,1 ,2 ,3
,4 ,5 ,0 ,36,0 ,//3
   0 ,34,0 ,7 ,8 ,46,10,11,0 ,0 ,0 ,7 ,8 ,47,31,32,33,47,10,11,0 ,0 ,7 ,8
,46,10,11,0 ,36,0 ,//4
   0 ,34,0 ,13,14,15,16,17,0 ,0 ,0 ,13,14,14,37,38,39,14,16,17,0 ,0
,13,14,15,16,17,0 ,36,0 ,//5
   0 ,34,0 ,19,20,21,22,23,6 ,0 ,0 ,19,20,20,43,44,45,20,20,23,0 ,0
,19,20,21,22,23,0 ,36,0 ,//6
   0 ,34,0 ,0 ,0 ,12,0 ,0 ,0 ,0 ,0 ,24,24,24,13,15,17,24,24,24,0 ,0 ,0 ,0
,12,0 ,0 ,0 ,36,0 ,//7
   0 ,34,0 ,0 ,0 ,12,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,19,21,23,0 ,0 ,0 ,0 ,0 ,0 ,0
,12,0 ,0 ,0 ,36,0 ,//8
   0 ,34,0 ,0 ,0 ,12,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,12,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
,12,0 ,0 ,0 ,36,0 ,//9
   0 ,34,0 ,0 ,0
,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,0 ,0 ,0 ,36,0 ,//10
   0 ,34,0 ,0 ,0 ,0 ,0 ,0 ,0 ,12,25,25,25,25,25,25,25,25,25,25,12,0 ,0 ,0 ,0
,0 ,0 ,0 ,36,0 ,//11
   0 ,34,0 ,1 ,2 ,3 ,4 ,5 ,0 ,12,25,25,25,25,25,25,25,25,25,25,12,0 ,1 ,2 ,3
,4 ,5 ,0 ,36,0 ,//12
   0 ,34,0 ,7 ,8 ,46,10,11,0 ,12,25,25,25,25,25,25,25,25,25,25,12,0 ,7 ,8
,46,10,11,0 ,36,0 ,//13
   0 ,34,0 ,13,14,15,16,17,0 ,12,25,25,25,25,25,25,25,25,25,25,12,0
,13,14,15,16,17,0 ,36,0 ,//14
   0 ,34,0 ,19,20,21,22,23,0 ,12,25,25,25,25,25,25,25,25,25,25,12,0
,19,20,21,22,23,0 ,36,0 ,//15
   0 ,34,0 ,0 ,0 ,12,0 ,0 ,0 ,12,25,25,25,25,25,25,25,25,25,25,12,0 ,0 ,0
,12,0 ,0 ,0 ,36,0 ,//16
   0 ,34,0 ,0 ,0 ,12,0 ,0 ,0 ,12,25,25,25,25,25,25,25,25,25,25,12,0 ,0 ,0
,12,0 ,0 ,0 ,36,0 ,//17
   0 ,34,0 ,0 ,0 ,12,0 ,0 ,0 ,12,25,25,25,25,25,25,25,25,25,25,12,0 ,0 ,0
,12,0 ,0 ,0 ,36,0 ,//18
   0 ,34,0 ,0 ,0
,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,0 ,0 ,0 ,36,0 ,//19
   0 ,34,0 ,0 ,0 ,0 ,0 ,0 ,0 ,12,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,12,0 ,0 ,0 ,0
,0 ,0 ,0 ,36,0 ,//20
   0 ,34,0 ,0 ,0 ,0 ,0 ,0 ,0 ,12,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,12,0 ,0 ,0 ,0
,0 ,0 ,0 ,36,0 ,//21
   0 ,34,0 ,1 ,2 ,3 ,4 ,5 ,0 ,12,0 ,0 ,0 ,0 ,1 ,3 ,5 ,0 ,0 ,0 ,12,0 ,1 ,2 ,3
,4 ,5 ,0 ,36,0 ,//22
   0 ,34,0 ,7 ,8 ,46,10,11,0 ,12,0 ,0 ,0 ,0 ,7 ,48,11,0 ,0 ,0 ,12,0 ,7 ,8
,46,10,11,0 ,36,0 ,//23
   0 ,34,0 ,13,14,15,16,17,0 ,12,0 ,0 ,0 ,6 ,13,15,17,0 ,0 ,0 ,12,0
,13,14,15,16,17,0 ,36,0 ,//24
   0 ,34,0 ,19,20,21,22,23,0 ,12,0 ,0 ,0 ,6 ,19,21,23,0 ,0 ,0 ,12,0
,19,20,21,22,23,0 ,36,0 ,//25
   0 ,34,0 ,0 ,0 ,12,0 ,0 ,0 ,12,18,0 ,0 ,0 ,0 ,12,0 ,0 ,0 ,0 ,12,0 ,0 ,0
,12,0 ,0 ,0 ,36,0 ,//26
   0 ,34,0 ,0 ,0
,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,0 ,0 ,0 ,36,0 ,//27
   0 ,34,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,12,12,12,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
,0 ,0 ,0 ,36,0 ,//28
   0 ,34,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,12,12,12,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
,0 ,0 ,0 ,36,0 ,//29
   0
,40,29,29,29,29,29,29,29,29,29,29,29,29,12,12,12,29,29,29,29,29,29,29,29,29,29,29,
42,0 ,
//30
   0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
,0 ,0 ,0 ,0 ,0  //31
  };
  for(int i = 0 ; i < maplist.length ; i++)
  {
   int col = i % 30;
   int row = (i - col) / 30;
   tiledLayer.setCell(col, row, maplist[i]);
  }
  return tiledLayer;
 }
}
修改BraveCanvas类如下:
BraveCanvas.java
package brave;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.Graphics;
import java.io.IOException;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.TiledLayer;

public class BraveCanvas extends GameCanvas implements Runnable
{
 private boolean sign;
 private Graphics g;
 private Hero hero;
 private Image upimage;
 private Image downimage;
 private Image leftimage;
 private Image rightimage;
 private TiledLayer backgroundMap;
 private TiledLayer foregroundMap;
 private BraveManager braveManager;
 public BraveCanvas()
 {
  super(true);
 }
 public void startup()
 {
  this.sign = true;
  try
  {
   backgroundMap = Scene.createTiledLayerByBackground(
    Image.createImage("/background.png"));
   //生成小镇地图
   foregroundMap = Scene.createTiledLayerByForeground(
    Image.createImage("/foreground.png"));
   upimage = Image.createImage("/hero_up.png");
   downimage = Image.createImage("/hero_down.png");
   leftimage = Image.createImage("/hero_left.png");
   rightimage = Image.createImage("/hero_right.png");
   braveManager = new BraveManager();
   braveManager.setBraveCanvas(this);
   hero = new Hero(upimage, 17, 26);
   hero.setFrameSequence(new int[]{1, 1, 0, 0, 1, 1, 2, 2});
   hero.setBraveCanvas(this);
   hero.setBraveManager(braveManager);
   hero.init(0,0);
  }
  catch(Exception e)
  {
   e.printStackTrace();
  }
  Thread thread = new Thread(this);
  thread.start();
 }
 public void run()
 {
  g = getGraphics();
  braveManager.insert(hero, 0);
  //插入小镇地图
  braveManager.insert(foregroundMap, 1);
  braveManager.insert(backgroundMap, 2);
  while(sign)
  {
   try
   {
    input(g);
    paint(g);
    Thread.sleep(15);
   }
   catch(Exception e)
   {
    e.printStackTrace();
   }
  }
 }
 public void input(Graphics g) throws IOException
 {
  int keystates = getKeyStates();
  switch(keystates)
  {
   case UP_PRESSED:
    hero.moveUp(upimage);
    break;
   case DOWN_PRESSED:
    hero.moveDown(downimage);
    break;
   case LEFT_PRESSED:
    hero.moveLeft(leftimage);
    break;
   case RIGHT_PRESSED:
    hero.moveRight(rightimage);
    break;
  }
  hero.afresh();
  braveManager.afresh();
 }
 public void paint(Graphics g)
 {
  g.setColor(0x000000);
  g.fillRect(0, 0, getWidth(), getHeight());
  g.setColor(0xffffff);
  braveManager.paint(g, 0, 0);
  flushGraphics();
 }
}

运行结果如图:


J2ME RPG游戏边学边做(三)--游戏基本元素类

    虽然我们有了midp2.0的支持,但是有时还是需要一些辅助工具,方便我们使用。这怕是在进行真正的游戏设计之前最有趣的了。

    1,首先是一个ImageTools工具类,提供一个方法帮助调用Image


public class ImageTools {
  protected ImageTools() {
  }
  public static Image getImage(String str){
    Image img=null;
    try {
      img = Image.createImage(str);
    }
    catch (Exception ex) {
      System.out.println(ex);
    }
    finally{
      return img;
    }
  }
}
2.GameObject,提供一个通用的游戏对象。

       有了Sprite类,为什么还要GameObject呢?其实我们一般是将Sprite,看作成一个高级的Image,往往一个Sprite要被多个游戏对象调用,GameObject其实就是Sprite的状态类。GameObject提供简单的生命周期概念,动画更新速度;

public class GameObject {
  public Sprite sprite;//内置的Sprite
  public boolean alive;//存活标记
  private int lifecount=0;//生命周期计数器
  public int lifetime=0;//生命周期,以桢为单位
  public int speed=0;//动画桢更新速度,(0至无穷,0代表每一桢跟新一个画面)
  private int animcount=0;// /动画桢更新计数器
   public GameObject(Image img,int width,int height){
    sprite=new Sprite(img,width,height);
    reset();
  }
   public void move(int dx,int dy){//相对移动
    sprite.move(dx,dy);
  }
   public void moveto(int x,int y){//绝对移动
    sprite.setPosition(x,y);
  }
   public void update(){//更新状态,动画桢更新,生命周期更新
    if(!alive)
      return;
    if(++animcount>speed){
      animcount=0;
      sprite.nextFrame();
      if(lifetime!=0 && ++lifecount>lifetime)
        alive=false;
    }
  }
   public void paint(Graphics g){//Paint
    if(!alive)
      return;
    sprite.paint(g);
  }
  public void reset(){//复位
    alive=true;
    lifecount=0;
    animcount=0;
    sprite.setFrame(0);
  }
}

     3.封装字体类,你需要漂亮的字体吗?

    我们经常需要用图片来输出文字,一个方便的字体类是必须的。我们希望仅仅提供一个图片,一个图片所描述的字符的数组,来初始化一个字体类。字体类提供一个类似Textout的方法,方便得在一个位置输出信息。先封装一个简单的版本,只支持英文和数字,并且输出不能自动换行。可能你有一个简单的思路,就是简单的保存字符数组,当打印的时候遍历数组,来查找每个字符在sprite的frameseq中的index,但当我们打印一个字符串的时候就会发现,太多的遍历操作耽误了宝贵时间,这里我们使用一个小技巧用容量换取速度,我们知道Character. hashCode()可以返回字符的ascii编码,常用字符将返回1-127;利用这一点,我们开辟一个128的数组charhash,将输入的字符c 所在图片index存入charhash[c. hashCode()]中。以后也用这种映射方法来读取字符。charhash的元素初值为-1,以后只要数值大于0就是有效值。


public class Font {
  Sprite sprite;       //Sprite
  int width,height;  //每个char的尺寸
  int[] charhash;    //储存1-127个常见字符在sprite的frameseq中的位置
  Graphics g;
   public Font(Graphics g,Image img, int width,  int height, char[] chars) {
    this.g=g;
    sprite=new Sprite(img,width,height);
    this.width=width;
    this.height=height;
    charhash=new int[128];
    for (int i = 0; i < charhash.length; i++) {
      charhash[i]=-1;//没有代表此字符的图片
    }
    Character c;
    for (int i = 0; i < chars.length; i++) {
      c=new Character(chars[i]);
      charhash[c.hashCode()]=i;
    }
  }
   public void drawChar(char ch, int x, int y){
    Character c=new Character(ch);
    int hashcode=c.hashCode();
    sprite.setPosition(x,y);
    if(hashcode>=0){
      sprite.setFrame(charhash[hashcode]);
      sprite.paint(g);
    }
  }
   public void drawString(String str, int x, int y){
    int length=str.length();
    for (int i = 0; i < length; i++) {
      drawChar(str.charAt(i),x+width*i,y);
    }
  }
}


这样只要有一个实例font,就可以调用font.drawString(“hello”,0,0);

在0,0位置输出漂亮的图片字符串。怎么样还挺好使的吧:)

J2ME RPG游戏边学边做(四)--爆炸效果

大多数游戏都有着丰富的效果类,在精灵移动类游戏中曾一度以此为一个重要的卖点。光光是一些丰富的特效是不能够产生一个好的游戏的,但是一个好的游戏是万万不能缺少好的效果的。

很多人认为游戏的效果层有时和跟游戏逻辑本身并没有太大的关系,往往就是在最终屏幕上再画上一层效果层。但是游戏逻辑和效果层之间的通信是很重要的。这种通信往往体现在延时与等待上。比如飞机爆炸时,不接受任何用户输入,并且爆炸效果还要继续跟随飞机坠落,甚至爆炸的范围会影响周围的物体,要等待爆炸结果结束了才继续进行游戏。游戏逻辑和效果层之间的通信是很复杂的问题。在这里我突然有了罪恶感,我们没有对游戏进行任何的分析就起步了,游戏完全是基于硬编码的,我想到那儿,大家跟着看到那儿。飞机类仅仅是一个sprite,没有设计成一个状态机,这也就使得我们的效果层和逻辑层的通信有些卡通了。也许本文给了你编写自己第一个游戏的喜悦,也带给了你对游戏扩展性与复杂性的一丝担忧。或许这比便一个硬编码的游戏更有意义呢?谁说得好呢,现还是以为那些扩展性良好的游戏是伟大游戏构架师的杰作吧,相信你有了一两个好的想法后会重新设计这个游戏的,使之稍微有一些像个“系统”。然而好的技术不一定产生好的游戏。

有扯远了,会到现实吧,boys and girls!goon.

描述一下我们的爆炸效果,在子弹击中飞机后,子弹要迅速消失,飞机图像保持不变,此时将爆炸效果至于飞机图像之上,然后开始显示boom动画,在此期间,飞机不接受任何移动指示,因为他lose control。在爆炸效果后飞机消失。

我们的爆炸效果类:


GameObject explosion;

初始化once:

img=ImageTools.getImage("/pic/explosion.png");
explosion=new GameObject(img,32,32);

初始化:

explosion.reset();
explosion.lifetime=3;//生命周期定位三桢

逻辑处理:

if (gameover) {//如果游戏结束,显示效果类
    explosion.paint(g);
    explosion.update();
    if(!explosion.alive){//当生命周期结束了
        plane.alive=false;//关闭plane
        g.setColor(255,255,255);
        g.drawString(StringTools.timeOpinion(gametime),5,22,g.LEFT|g.TOP);
        g.drawString("fly 0.1 ver by favo yang",2,100,g.LEFT|g.TOP);
        g.drawString("E-mail : 该邮件地址已受到反垃圾邮件插件保护。要显示它需要在浏览器中启用 JavaScript。",2,115,g.LEFT|g.TOP);
        g.drawString("simulate from:",2,130,g.LEFT|g.TOP);
        g.drawString("Mr.tony 's <hold on 20sec 1.20> ",2,145,g.LEFT|g.TOP);
        g.drawString("hello tony, just funny.",2,160,g.LEFT|g.TOP);
    }
}

现在你看我是如何解决效果层与逻辑层之间的通信的,我使用的是全局变量gameover,在简单游戏中使用大量的全局状态变量也是一种常见的方法,可以避免动脑劲。不过缺点明显,游戏硬编码,结构既不清晰也不漂亮,几乎没有扩展性。所以说最好还是将飞机基于状态机设计,并将效果类设计成含有回调函数的抽象类,然后继承效果类实现回调函数来实现通信。至于总体层次上可以用堆栈将绘画单元串起来。还有分层处理等等…给你个思考的起点…

导弹的是实现,是不是你已经有个想法了呢,其实就是利用Bullets.killbullets。


逻辑处理

J2ME RPG游戏边学边做
if(bomb.alive){
bomb.moveto(plane.sprite.getX()-20,plane.sprite.getY()-20);
bomb.paint(g);
bomb.update();
bullets.killbullets(plane.sprite,32);
}

    在这里我不得不提一句,将生命概念封装在GameObject中是很好的(其实我们只是将其用作显示关键字),但将生命周期安排在GameObject中有欠妥当,生命周期也不一定就是基于桢的,有时基于时间,有时还有别的什么。我是说她足够复杂到交给另一个独立类处理,在这里实际需要的是一个足够强大的显示方法,其支持以桢数为参数显示罢了。


J2ME RPG游戏边学边做(五)--游戏灵魂(计时器和奖惩与评价)

    我们该加入我们的计时器了,我要从游戏开始时刻开始计时,并不断的更新到屏幕上,在游戏结束后计时器要停止工作。

     首先在初始化的时候将当前时刻记录下来:

gametime=0;
gametimeoffset=System.currentTimeMillis();

以后只要游戏不gameover就在每个更新周期都进行一次计算:
gametime=(System.currentTimeMillis()-gametimeoffset)/1000;
//转换为秒

下面要做的就是将它显示出来,还记得我们曾经实现的字体类了吗,这下子有用了,首先是实例化一个字体类:
img=ImageTools.getImage("/pic/b_number.png");
fontbig=new   Font(g,img,10,15,
new char[]{'0','1','2','3','4','5','6','7','8','9'});

然后就是显示:
fontbig.drawString(String.valueOf(gametime),screenwidth/2-15,10);

还真是方便yeah!

接着是奖励系统,我们规定每过20s就加一个bomb给玩家。
int awardindex=(int)gametime/20;//计算奖励时间
    if(awardindex>bombawardtop)
      awardindex=bombawardtop;
    if(bombaward[awardindex]!=0){//如果本20s没有奖励
      bombnum+=bombaward[awardindex];
      bombaward[awardindex]=0;//奖励过了
}

尽可能简单和给出些提示是我写本文的原则, 所以评价系统,很easy。
我们建立一个辅助类,提供一个方法,输入游戏时间,返回一个String评语。

public class StringTools {
  protected StringTools() {
  }
  public static String timeOpinion(long gametime){
    if(gametime<10){
      return "Do you play with your foot?";
      //return "i can't belive,your are a game master";
    }else if(gametime<16){
      return "come boy, you can do it!";
    }else if(gametime<20){
      return "what a pity! try again.";
    }else if(gametime<25){
      return "very well, you are a real man.";
    }else if(gametime<30){
      return "i know you have talent of this game.";
    }else if(gametime<40){
      return "i can't belive, your are a game master.";
    }else{
      return "oh my god, are you a human?";
    }
  }
}
之后显示出来就好了,我手头没有合适大小的字体图片,我直接使用
g.drawString(StringTools.timeOpinion(gametime),5,22,g.LEFT|g.TOP);

想在手机小小的屏幕容下那么东西是挺费劲的,其实这也是我对手机上玩游戏没什么兴趣,不过我对手机网络应用冲满了信心

J2ME RPG游戏边学边做(六)--莱克兄弟的第一架飞机

实在是等不及了吧?加把劲,让我们加入游戏的主角飞机吧。

你可以将这个游戏定位在动作游戏,一个动作游戏需要不断的从用户端获得输入,基本上不间断,所以plane需要不断的改变位置。那么这个基于桢的游戏应该定在多少桢合适呢?这需要在手机中测试,在模拟器上,如果定在15-18桢左右,是比较合适的。听csdn的几位xd说有的游戏仅仅定在12桢,所以不同的游戏是不一定的。基本上动作游戏对机子的要求是要高一点。我们的游戏框架是线性的,即输出显示和接受输入是在同一个线程中,15桢以下的游戏处理输入会显得比较苯,高于20桢如果处理器速度较慢,很容易出现输入卡在缓冲区,处理不过来的情况。现阶段尽量让桢数降下来吧,你不能和pc上用dx编程时,统一 30fps的情况相提并论。

在这个以控制精灵移动为主要内容的游戏中,我们不封装任何的游戏事件,我们的程序可能有些稚嫩,没关系,一步一步来吧。

1. 飞机

plane是一个标准的plane。由三桢画面组成,首先在ps中处理,让其透明:


在构造函数中,我们加入对飞机的初始化对象语句:

Image img=ImageTools.getImage("/pic/MyPlaneFrames.png");
plane=new GameObject(img,24,24);
      
在gameInit()中初始化状态,位置居中:
plane.reset();
plane.moveto((screenwidth-plane.sprite.getWidth())/2,
(screenheight-plane.sprite.getHeight())/2);

   在gameMain中加入:
plane.paint(g);
   接下来,让飞机在控制下移动,主要是在gameMain中加入输入处理,如果飞机左移就更新画面到飞机左倾的画面,如果飞机右倾反之,没有输入的时候让飞机处于正常。
if (gameover) {
}else{
       if (keyevent) {
        if(key_up){
          plane.move(0, -3);
          plane.sprite.setFrame(0);
        }
        if(key_down){
          plane.move(0, 3);
            plane.sprite.setFrame(0);
        }
        if(key_left){
          plane.move( -3, 0);
          plane.sprite.setFrame(1);
        }
        if(key_right){
          plane.move(3, 0);
          plane.sprite.setFrame(2);
        }
        if(key_fire){
          }
        }
      }
      else {
        plane.sprite.setFrame(0);
      }
}
   尽管很简单,但这是控制精灵移动的主要方式。可以想象一下,如果往左飞不是简单的一桢画面而是播放动画,该怎么实现呢?我们的GameObject功能有限,没有对spriite的动画序列进行增强,今后我们可以增强GameObject,使其支持多个动画序列,弥补spriite的不足。现在逐渐体会到一个好的游戏引擎是多么重要了吧…
      

2. 背景

   让飞机在海面飞行吧,我们用一个蓝色的背景图片代表海面,我们需要他铺满整个背景。我们使用TiledLayer来绘画背景。



初始化:
img=ImageTools.getImage("/pic/back_water.png");
int backcolumns=screenwidth/img.getWidth()+1;//计算横向
int backrows=screenheight/img.getHeight()+1;
background=new TiledLayer(backcolumns,backrows,img,img.getWidth(),img.getHeight());
int x,y;
for (int i = 0; i < backcolumns*backrows; i++) {
   x=i%backcolumns;
   y=i/backcolumns;
   System.out.println("x="+x+" y="+y);
   background.setCell(x,y,1);
}

接下来在gameMain中加入绘制语句。
background.paint(g);//draw background

注意,要保证背景的绘制在飞机的绘画之前。如果使用Layermanager可以很方便的的控制绘制层,但是我们的接下来的子弹要求多个子弹对象共用一个 sprite的图像,但是Layermanager绘画的时候以Layer为单位,由Layermanager帮助我们调用各个layer的paint, so一次只支持将一个sprite画在一个地方,所以我觉得Layermanager有些鸡肋。一般时候我们还是自己paint吧。当然,层是一个很重要的概念,利用层,屏幕校准将非常方便,震动整个屏幕等特效将很实现。也许你有个好法,可以让我不在Layermanager层与sprite状态对象组之间矛盾…


飞行战机源代码:


J2ME RPG游戏边学边做(七)--丰富人物和剧情

在J2ME RPG游戏边学边做(二)中我们已经成功的完成地图和英雄的编写。这次我们将为英雄加入碰撞检测和人物对话。

在开始前,我们需要确定在地图中哪些地方不允许走动,那些地方可以触发对话,这就需要在地图中事先把这些事件定义好。我们改变先前的Scene类。利用二维数组为地图加入事件。

Scene.java
//这次的代码和上次有点出入,这次我们利用getMap()方法来读取地图数组,这样方便以后改为
//从外部文件读取
package brave;
import javax.microedition.lcdui.game.TiledLayer;
import javax.microedition.lcdui.Image;
public class Scene
{
public static TiledLayer createTiledLayerByBackground(Image image)
{
  TiledLayer tiledLayer = new TiledLayer(10, 8, image, 48, 64);
  tiledLayer.fillCells(0, 0, 10, 8, 2);
  return tiledLayer;
}
public static int[][] getMap()
{
  //生成地图数组,在原来的每个地图元素后面都加了一个事件。
  //事件id为99是不允许通过
  //事件id为98是激活对话
  //其实在这里定义二维数组并不是很方便,个人感觉还是三维比较直观和方便,
  //这里为了方便,只定义二维数组
  int[][] maplist =
  {
   //30*32
   {0 ,0}, {0 ,0}, {0 ,0}, {0 ,1}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
   {0 ,0}, {28,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99},
{29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99},
{29,99},
{29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99},
{29,99}, {30,0},
{0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {1 ,99}, {2 ,99}, {3 ,99}, {4 ,99}, {5 ,99}, {0
,0}, {0 ,0}, {0 ,0}, {1 ,99}, {2 ,99}, {3 ,99}, {3 ,99}, {26,99}, {3 ,99}, {3 ,99},
{4 ,99}, {5
,99}, {0 ,0}, {0 ,0}, {1 ,0}, {2 ,0}, {3 ,0}, {4 ,0}, {5 ,0}, {0 ,0}, {36,99},
{0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {7 ,99}, {8 ,99}, {46,99}, {10,99}, {11,99}, {0
,0}, {0 ,0}, {0 ,0}, {7 ,99}, {8 ,99}, {47,99}, {31,99}, {32,99}, {33,99}, {47,99},
{10,99},
{11,99}, {0 ,0}, {0 ,0}, {7 ,0}, {8 ,0}, {46,0}, {10,0}, {11,0}, {0 ,0}, {36,99},
{0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {13,99}, {14,99}, {15,99}, {16,99}, {17,99}, {0
,0}, {0 ,0}, {0 ,0}, {13,99}, {14,99}, {14,99}, {37,99}, {38,99}, {39,99}, {14,99},
{16,99},
{17,99}, {0 ,0}, {0 ,0}, {13,0}, {14,0}, {15,0}, {16,0}, {17,0}, {0 ,0}, {36,99},
{0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {19,99}, {20,99}, {21,99}, {22,99}, {23,99}, {6
,99}, {0 ,0}, {0 ,0}, {19,99}, {20,99}, {20,99}, {43,99}, {44,99}, {45,99},
{20,99}, {20,99},
{23,99}, {0 ,0}, {0 ,0}, {19,0}, {20,0}, {21,0}, {22,0}, {23,0}, {0 ,0}, {36,99},
{0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {24,99}, {24,99}, {24,99}, {13,99}, {15,99}, {17,99}, {24,99},
{24,99}, {24,99},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {19,99}, {21,99}, {23,99}, {0 ,0}, {0 ,0},
{0 ,0}, {0
,0}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {12,0}, {12,0}, {12,0},
{12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0},
{12,0}, {12,0},
{12,0}, {12,0}, {12,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{12,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0},
{25,0}, {12,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {1 ,0}, {2 ,0}, {3 ,0}, {4 ,0}, {5 ,0}, {0 ,0},
{12,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0},
{25,0}, {12,0},
{0 ,0}, {1 ,0}, {2 ,0}, {3 ,0}, {4 ,0}, {5 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {7 ,0}, {8 ,0}, {46,0}, {10,0}, {11,0}, {0 ,0},
{12,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0},
{25,0}, {12,0},
{0 ,0}, {7 ,0}, {8 ,0}, {46,0}, {10,0}, {11,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {13,0}, {14,0}, {15,0}, {16,0}, {17,0}, {0 ,0},
{12,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0},
{25,0}, {12,0},
{0 ,0}, {13,0}, {14,0}, {15,0}, {16,0}, {17,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {19,0}, {20,0}, {21,0}, {22,0}, {23,0}, {0 ,0},
{12,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0},
{25,0}, {12,0},
{0 ,0}, {19,0}, {20,0}, {21,0}, {22,0}, {23,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0},
{12,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0},
{25,0}, {12,0},
{0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0},
{12,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0},
{25,0}, {12,0},
{0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0},
{12,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0}, {25,0},
{25,0}, {12,0},
{0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {12,0}, {12,0}, {12,0},
{12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0},
{12,0}, {12,0},
{12,0}, {12,0}, {12,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {12,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {12,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {1 ,0}, {2 ,0}, {3 ,0}, {4 ,0}, {5 ,0}, {0 ,0},
{12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {1 ,0}, {3 ,0}, {5 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {12,0},
{0 ,0}, {1 ,0}, {2 ,0}, {3 ,0}, {4 ,0}, {5 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {7 ,0}, {8 ,0}, {46,0}, {10,0}, {11,0}, {0 ,0},
{12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {7 ,0}, {48,0}, {11,0}, {0 ,0}, {0 ,0},
{0 ,0}, {12,0},
{0 ,0}, {7 ,0}, {8 ,0}, {46,0}, {10,0}, {11,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {13,0}, {14,0}, {15,0}, {16,0}, {17,0}, {0 ,0},
{12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {6 ,0}, {13,0}, {15,0}, {17,0}, {0 ,0}, {0 ,0},
{0 ,0}, {12,0},
{0 ,0}, {13,0}, {14,0}, {15,0}, {16,0}, {17,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {19,0}, {20,0}, {21,0}, {22,0}, {23,0}, {0 ,0},
{12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {6 ,0}, {19,0}, {21,0}, {23,0}, {0 ,0}, {0 ,0},
{0 ,0}, {12,0},
{0 ,0}, {19,0}, {20,0}, {21,0}, {22,0}, {23,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0},
{12,0}, {18,98}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {12,0},
{0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {12,0}, {12,0}, {12,0},
{12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0}, {12,0},
{12,0}, {12,0},
{12,0}, {12,0}, {12,0}, {12,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {12,0}, {12,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {34,99}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {12,0}, {12,0}, {12,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {36,99}, {0 ,0},
   {0 ,0}, {40,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99},
{29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {12,0}, {12,0}, {12,0},
{29,99}, {29,99},
{29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99}, {29,99},
{42,99}, {0 ,0},
   {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0},
{0 ,0}, {0 ,0},
{0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}, {0 ,0}
  };
  return maplist;
}
public static TiledLayer createTiledLayerByForeground(Image image)
{
  //生成地图
  int[][] maplist = Scene.getMap();
  TiledLayer tiledLayer = new TiledLayer(30, 32, image, 16, 16);
  for(int i = 0 ; i < maplist.length ; i++)
  {
   int col = i % 30;
   int row = (i - col) / 30;
   tiledLayer.setCell(col, row, maplist[i][0]);
  }
  return tiledLayer;
}
public static int getEvent(int x, int y)
{
  //根据地图单元格的x和y得到该单元格的事件,这里的30应该根据地图的实际大小来确定
  return Scene.getMap()[x + (y * 30)][1];
}
}

ok,地图完成!下面是检测上面定义的事件。

我们知道在Sprite类中已经有了一个检测Sprite(人物)和TiledLayer(地图)的方法:collidesWith()。可是该检测方法必须在Sprite和TiledLayer交错时才能检测出来,个人感觉使用不是很方便,所以我决定重新写一检测碰撞的方法,该方法可以检测出当前 Sprite在四个方向时下一个将要移动的地图单元是什么。

BraveCanvas类需要增加一个静态变量,这里为了篇幅,暂不给出原码,大家可以在下面看到。

运行后,只有左上角、中间的房子和栅栏,可以正常检测,这是因为其他的单元格为了方便都没有加上事件。

一点缺陷:
其实这里的地图并不是很完善,比如说人物如果在房子上面的话,屋顶应该会把人物遮住一部分,而人物在房子下面的话,人物应该把房子遮住一点。大家如果感兴趣的话可以用图层来解决这问题。

下面说说人物的对话实现。

需要用到的图片:



在实现人物对话的时候,我想实现对话的打字机效果,这就需要一个循环来实现它。循环结束后,打字机的效果结束,对话并没有结束。应该显示对话内容并进入等待状态直到用户再次按键,才真正的结束对话。

流程图如下:



代码如下:

Hero.java

package brave;

import javax.microedition.lcdui.game.Sprite;
import javax.microedition.lcdui.Image;
import java.io.IOException;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.game.TiledLayer;

public class Hero extends Sprite
{
private int x;
private int y;

private BraveCanvas braveCanvas;
private BraveManager braveManager;

public Hero(Image image, int frameWidth, int frameHeight)
{
  super(image, frameWidth, frameHeight);
}

public void setBraveCanvas(BraveCanvas braveCanvas)
{
  this.braveCanvas = braveCanvas;
}

public void setBraveManager(BraveManager braveManager)
{
  this.braveManager = braveManager;
}

public void setManager(BraveCanvas braveCanvas)
{
  this.braveCanvas = braveCanvas;
}

public void init(int x, int y)
{
  this.x = x;
  this.y = y;
}

public void afresh()
{
  setPosition(this.x, this.y);
}

public void moveUp(Image image) throws IOException
{
  setImage(image, 17, 26);
  nextFrame();
  if(!eventActionExist(99) && !eventActionExist(98))
   this.y = Math.max(0, y - 1);
}

public void moveDown(Image image) throws IOException
{
  setImage(image, 17, 26);
  nextFrame();
  if(!eventActionExist(99) && !eventActionExist(98))
   this.y = Math.min(braveManager.getLayerAt(1).getHeight(), y + 1);
}

public void moveLeft(Image image) throws IOException
{
  setImage(image, 17, 26);
  nextFrame();
  if(!eventActionExist(99) && !eventActionExist(98))
   this.x = Math.max(0, x - 1);
}

public void moveRight(Image image) throws IOException
{
  setImage(image, 17, 26);
  nextFrame();
  if(!eventActionExist(99) && !eventActionExist(98))
   this.x = Math.min(braveManager.getLayerAt(1).getWidth(), x + 1);
}

//实现人物的对话
public void talk(String addressor,Image talkImage, String s, Graphics g)
{
  g.drawImage(talkImage, 0, 0, Graphics.TOP|Graphics.LEFT);
  g.drawString(addressor+":", 7, 6, Graphics.TOP|Graphics.LEFT);
  for(int i = 0 ; i < s.length() ; i++)
  {
   g.drawString(s.substring(i, i+1), (i*12)+12, 21,

Graphics.TOP|Graphics.LEFT);
   try
   {
    Thread.sleep(100);
   }
   catch(Exception e)
   {
    e.printStackTrace();
   }
   braveCanvas.flushGraphics();
  }
  //对话的文字形式结束,进入等待状态,直到
  //再次按下对话键
  while(BraveCanvas.isTalk)
  {
   try
   {
    int keystates = braveCanvas.getKeyStates();
    //再次按下对话键
    if(keystates == BraveCanvas.FIRE_PRESSED)
    {
     //是否对话标志位置为false
     BraveCanvas.isTalk = false;
     //是否可以重新开始对话标志位置为false
     //之所以这样做是保证在在下一次检测按键时,不重新开始对话
     BraveCanvas.isTalkSign = false;
     break;
    }
    Thread.sleep(50);
   }
   catch(Exception e)
   {
    e.printStackTrace();
   }
  }
}

public boolean eventActionExist(int eventID)
{
  …………
}

}

修改BraveCanvas.java 如下

BraveCanvas.java

package brave;

import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.Graphics;
import java.io.IOException;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.TiledLayer;


public class BraveCanvas extends GameCanvas implements Runnable
{
private boolean sign;
private Graphics g;
private Hero hero;
private Image upimage;
private Image downimage;
private Image leftimage;
private Image rightimage;
private Image talkImage;
private TiledLayer backgroundMap;
private TiledLayer foregroundMap;
private BraveManager braveManager;
//该标志位判断对话是否开始
public static boolean isTalk;
//该标志为判断对话是否可以重新开始,默认可以重新开始
public static boolean isTalkSign = true;
//当前的人物方向(碰撞检测用)
public static int way = 0;

public BraveCanvas()
{
  super(true);
  try
  {
   backgroundMap = Scene.createTiledLayerByBackground(
    Image.createImage("/background.png"));
   foregroundMap = Scene.createTiledLayerByForeground(
    Image.createImage("/foreground.png"));
   upimage = Image.createImage("/hero_up.png");
   downimage = Image.createImage("/hero_down.png");
   leftimage = Image.createImage("/hero_left.png");
   rightimage = Image.createImage("/hero_right.png");
   talkImage = Image.createImage("/talk.png");
   braveManager = new BraveManager();
   braveManager.setBraveCanvas(this);
   hero = new Hero(upimage, 17, 26);
   //hero.setFrameSequence(new int[]{1, 1, 0, 0, 1, 1, 2, 2});
   hero.setBraveCanvas(this);
   hero.setBraveManager(braveManager);
   hero.init(120, 120);
  }
  catch(Exception e)
  {
   e.printStackTrace();
  }
}

public void startup()
{
  this.sign = true;
  Thread thread = new Thread(this);
  thread.start();
}

public void run()
{
  g = getGraphics();
  braveManager.insert(hero, 0);
  braveManager.insert(foregroundMap, 1);
  braveManager.insert(backgroundMap, 2);
  while(sign)
  {
   try
   {
    input(g);
    BraveCanvas.isTalkSign = true;
    paint(g);
    Thread.sleep(15);
   }
   catch(Exception e)
   {
    e.printStackTrace();
   }
  }
}

public void input(Graphics g) throws IOException
{
  int keystates = getKeyStates();
  switch(keystates)
  {
   case UP_PRESSED:
    BraveCanvas.way = UP_PRESSED;
    hero.moveUp(upimage);
    break;
   case DOWN_PRESSED:
    BraveCanvas.way = DOWN_PRESSED;
    hero.moveDown(downimage);
    break;
   case LEFT_PRESSED:
    BraveCanvas.way = LEFT_PRESSED;
    hero.moveLeft(leftimage);
    break;
   case RIGHT_PRESSED:
    BraveCanvas.way = RIGHT_PRESSED;
    hero.moveRight(rightimage);
    break;
   case FIRE_PRESSED:
    //当用户按下对话键时候,首先判断对话是否可以重新开始
    //只有对话可以重新开始后才能再次开始对话
    if(hero.eventActionExist(98))
    {
     if(BraveCanvas.isTalkSign)
     {
      BraveCanvas.isTalk = true;
     }
    }
    break;
  }
  hero.afresh();
  braveManager.afresh();
}

public void paint(Graphics g)
{
  g.setColor(0x000000);
  g.fillRect(0, 0, getWidth(), getHeight());
  g.setColor(0x000000);
  braveManager.paint(g, 0, 0);
  if(BraveCanvas.isTalk)
  {
   //加入对话,这里只是简单做个例子。应该给对话规定编号,然后从文件中相应的编号中读取
   hero.talk("英雄", talkImage, "这是一个小镇", g);
  }
  else
  {
   flushGraphics();
  }
}

}

运行后,可以在小镇开始的小木牌处按[射击键]实现对话

J2ME RPG游戏边学边做(八)--使用j2meunit进行游戏测试

做一款精彩的j2me游戏是属不易呀。但是后面对游戏的测试更是麻烦,这也是一些手机游戏公司中测试人员与开发人员的比例相当的一个重要原因。。究竟有没有好的途径提高游戏测试的速度那??这也是摆在游戏开发team面前的老问题。。

不过现在就使用j2meunit这一利器,它可是好用的开源的东东呀!

j2meunit简介:
利用JUnit等单元测试框架进行单元测试对于Java程序员并不陌生,利用这些非常有效的工具,使得代码的质量得到有效的监控和维护。然而似乎一切在 J2ME的平台上,都显得略有些不同。由于J2ME环境不能提供反射(Reflection)API,因此很多基于反射的功能都无法使用,例如JUnit 中自动创建并运行test suite的功能。广大的J2ME程序员不能在J2ME平台上使用JUNIT进行单元测试,但谁都知道没有单元测试的程序是多么的脆弱!

J2MEUnit 是由Kent Beck和Erich Gamma设计开发的在J2ME平台上模仿JUnit的单元测试框架,大小17KB。它的运用为编写有保证的J2ME程序代码提供了基础性的支持。 J2MEUnit引入了一些新的机制来解决原有JUnit对反射的依赖。可能在使用中J2MEUnit明显的没有JUnit方便,但现阶段我们也只能利用它了,热烈的期盼着J2ME环境对反射的支持。现有的J2MEUnit的版本是1.1.1。如同JUnit一样,它也是开源的。你可以在sf.net上找到他的下载。相比较JUnit经常升级,J2MEUnit有一段时间没有升级了,一方面投入的力量较小,另外可能是考虑到J2ME环境的特殊性,要保证测试的LIB足够的小。


搭建测试平台:

我们以Eclipse配合EclipseME为例子说明如何使用J2MEUnit。

首先到sf下载J2MEUnit的最新版本:http://j2meunit.sourceforge.net,并解压缩到你的常用目录中。

新建一个Midlet Suite,选择Project…>properties…>Java Build Path…>Libraries…>Add External JARs…选择你需好下载的路径中的j2meunit.jar。




这样就可以使用了。

3。编写测试类:

让我们编写一个TestCase来学习如何使用这套工具。

编写TestCase类
编写测试的类要继承j2meunit.framework.TestCase。如同JUnit中一样,你可以覆写setUp() 和tearDown()方法,虽然这里没有反射机制,但还是推荐你把测试方法以test开头。这样一但J2ME有了反射机制,你也可以快速的移植。还有一点要注意的是,你需要为子类提供一个构造函数(假设你的类叫做TestOne):

public TestOne(String sTestName, TestMethod rTestMethod)
{
      super(sTestName, rTestMethod);
}
稍候解释这是为什么?
接下来编写两个个测试方法,这很熟悉:
public void testOne()
{
      System.out.println("TestOne.testOne()");
      assertTrue("Should be true", false);
}
public void testTwo()
{
      System.out.println("TestOne.testTwo()");
      throw new RuntimeException("Exception");
}

正是缺少反射机制,你需要手动编写suite方法,并一一调用你编写的测试方法,这个步骤多多少少有些烦闷。没办法了,这是理解J2MEUnit框架的关键了,咱连write once debug anywhere都忍了,还有什么困难不能克服呢?

suite 方法要求我们返回一个TestSuite对象,因此,首先建立一个新的TestSuite对象并调用addTest方法,为他添加Test对象。Test 是一个接口,TestSuite、TestCase都实现了他,因此既可以添加测试单元、又可以添加一个测试套件。

根据J2MEUnit的设计思想,一个TestCase在运行时,只能捆绑一个TestMethod对象。TestMethod是一个标准的回调接口,只含有一个回调run(TestCase tc)方法。这个run方法的任务是调用一个,注意,是一个测试方法,那么一旦这个方法出现问题,可以很好的捕捉它,并返回给用户。TestMethod 提供了一组set方法用于捆绑一个TestMethod对象,但实际我们不去使用它,因为效率太低了,为了更快捷的捆绑TestMethod对象,我们要利用构造函数和匿名类来捆绑TestMethod类的实例。这个匿名类很好编写,只要将传入的TestCase tc向上转型到你的TestCase子类,然后调用相关方法就可。我们不得不同时提供一个String作为名称给我们的构造函数(还记得吗?我们添加的那个构造函数,这下,明白她的用处了吧)。

看一下下面这个例子,希望能帮助你理解上面那段总觉得有些拗口的话。如果你理解了“一个TestCase在运行时,只能捆绑一个TestMethod对象” 这句话,那么就理解了J2MEUnit所谓的新机制。千万不要在一个TestMethod中连续调用多个test方法,这样一旦某个方法出了问题,那么整个方法会结束而后续的测试将不能执行。一定要老老实实做人,认认真真写suite(),似乎又回到了剪刀加浆糊的时代。。。[-_-"]


public Test suite()
{
      TestSuite aSuite = new TestSuite();       
      aSuite.addTest(new TestOne("testOne", new TestMethod()
      { public void run(TestCase tc) {((TestOne) tc).testOne(); } }));
      aSuite.addTest(new TestOne("testTwo", new TestMethod()
      { public void run(TestCase tc) {((TestOne) tc).testTwo(); } }));       
      return aSuite;
}
编写测试套件
接下来编写一个测试套件,其实你可能已经明白了,测试套件不过是一个特殊的TestCase,根据惯例,一般这样的类叫做TestAll,只需要将以前添加的TestCase中的suite添加给TestAll的suite就可以了。
public class TestAll extends TestCase
{
public Test suite()
{
      TestSuite suite = new TestSuite();
      suite.addTest(new TestOne().suite());
      suite.addTest(new TestTwo().suite());
      return suite;
}
}
4。调试:
有两个方法运行我们的测试。
使用textui
利用textui,这个大家都熟悉了,不做重点介绍。一般习惯上在TestAll方法中添加一个main方法:
public static void main(String[] args)
{
      String[] runnerArgs = new String[] { "j2meunit.examples.TestAll" };
      j2meunit.textui.TestRunner.main(runnerArgs);
}

要为TestRunner.main传入一个String数组,里面罗列所有要测试的TestCase的完整路径,因为我们编写了TestAll,所以只传入他就可以了。



使用midletui这才是这套框架迷人的地方,正是有了他我们可以在真机上进行Unit Test了,cool,这将节省多少的测试成本呀。所以之前所有的编写suite的工作就认了!

继承j2meunit.midletui.TestRunner,这是一个midlet父类。在startApp中调用如下方法:

protected void startApp()
{
  start(new String[] { "j2meunit.examples.TestAll" });
}

或者,更为灵活的,你可以在jad文件中编写一个J2MEUnitTestClasses属性,写入你要测试的若干个TestCase,这样也可以进行测试而不更改主类。

如下是在模拟上的结果:




在我的MIDP1.0,真机上运行这个例子得到同样的结果,用时401ms。如果你正在使用j2me开发项目,建议把单元测试引入到你的工作当中,正如我们看到单元测试对于别的java平台的影响一样,对于嵌入式开发,它也是大有用武之地的。


J2ME RPG游戏边学边做(九)--模拟真实世界,浅谈游戏中的运动物理学

玩过/见过不少的手机游戏,包括下载率颇高的game。除了精彩的情节、动人的音效外,不可或缺的还要有一点运动学常识,这样人物动作、物体(如子弹、汽车)运动才会惟妙惟肖,给玩家更强的带入感。以我的个人观点:这样的game才能称得上是“game”,才够资格收费。

呵呵,结束了以“反对手机游戏乱收费”的牢骚后,给大家讲讲“运动物理学”在游戏开发上的应用才是不让“人望梅止渴”的好东东。

物体做抛物线运动是游戏中基本运动物理模型之一! 在PC游戏中可以由重力公式轻易模拟,但在手机游戏中 ,由于多数手机不支持浮点运算 因此不能用 sin ,cos, 来分解初速度。 所以只能用近似模拟的方法! 我所采用的是:先放大后缩小的模拟方式,并且为了更精确加入了一定的偏移量。

先用哈希表列出0-90度的正弦值,并且把值放大100000倍,例如:

Hashtable anglevalue;
public void loadAnglevalue()
{
    anglevalue = new Hashtable();
    anglevalue.put(String.valueOf(0),new Integer(0));
    anglevalue.put(String.valueOf(30),newInteger(50000));
    anglevalue.put(String.valueOf(60),new Integer(86603));
    anglevalue.put(String.valueOf(90),new Integer(100000));
    ……


这样就可以得出各种角度的正余弦值
设初速度为V0 物体当前坐标为x=0,y=0; t为时间 g重力=10;
根剧力学公式 
Vx=V0*cos&;
Vy=V0*sin&;
再根据重力公式:
x=Vx*t;
y=Vy*t –5*t*t;

由于cos& sin&都是放大了100000倍的所以 再得到手机屏幕坐标的时候应该缩小100000倍

x=Vx*t/100000;
y=(Vy*t –5*t*t)/100000;

现在公式中除了t之外都解决了! 现在来解决时间t;


我们可以在游戏主循环的 中有不断增加t的值 但是因为主循环非常快!以毫秒计算所以我们应该加入缓冲

while (true){
    thisThread.sleep(10);
    if(bFire){
        tTemp++;
        if (tTemp >10) {
            t+=1;
            tTemp = 0;
        }
    }
}

代码中的if (tTemp >10) 这个值的判断就调整了时间的增长频率!你也可以用if (tTemp >2)来使时间增长加快 或则用其他数值让时间变慢注意的一点就是我们的时间也要放大! 
至于放大多少倍 则要看游戏的节奏!我这里暂且放大20000倍
因此公式为:

x=Vx*t/100000;
y=(Vy*t –5*t*t*20000)/100000;

还有 我们需要把 物体初始位置放在 屏幕的下放那就需要加个初试位置常量
公式变为:

x=Vx*t/100000;
y=(100000*(getHeight()-20))-(Vy*t –5*t*t*20000)/100000;

getHeight()在手机中为得到手机屏幕的高度

好了 来看看用了这个公式后的运行效果(NOKIA 7650模拟器 或则unijava模拟器)
图1




这是45度角情况下的抛物线轨迹,是不是觉得高度不够呢! 运算不够精确! 那么我们在Y上加个偏移量来增加高度

这个代码 是在平抛的时候就不需要加入高度偏移了

现在再看45度角的 抛物线
图2



如果你还不满意 还可以改动偏移数值来让模拟更精确

下面来看一些角度 在不同力度 和风速下的轨迹快照

J2ME RPG游戏边学边做(十)--如何提高j2me游戏移植性

一、编写易于移植的J2ME代码

我写第一个J2ME游戏的时候,根本就没想过移植的问题。所以那个游戏也就很难移植了。反过来,如果你已经计划好要移植了,那么事情就简单的多。这一节说的是代码问题。那就想想,不同手机之间在代码上会有哪些差异。

(1) 屏幕尺寸不同

这儿谈的主要问题,是自适应控件。所谓控件,就是菜单、文本框、列表框、进度条等等。这些控件的大小必须可以根据屏幕大小自适应的调整。按照第一篇说的方法,将屏幕大小作为变量参与到控件尺寸的计算即可得到正确的尺寸(自适应后的)。其次就是得到正确尺寸后怎么把它画出来。

这要看你的GUI是怎么画得了,如果是用线画的,那就很简单;如果使用了图片,那么就可能要更换图片了。我的控件使用了图片平铺和画线结合,所以可以很容易的改变尺寸。如果控件变大了,则绘制时增加平铺的次数即可。

顺便说一下,这些控件我只用了一个类表示,使用参数化的方法区分使用,毕竟咱要尽量少用类吧。

(2) 支持的API不同

如果你的游戏只限于使用Midp1.0,那么移植的时候就不用考虑什么了。实际上由于我们经常要使用图片翻转、象素绘制、全屏等,往往要用到厂商API或Midp2.0。显然移植的时候要考虑到这些API的差异。

我的办法是将这些api封装一层,比如我需要使用创建透明子图的API,于是封装了一个函数createSubImg。这是Nokia版本:

public static Image createSubImg(Image img,int []imgRect)
{
Image subImg = DirectUtils.createImage(imgRect[2],imgRect[3],0) ;
subImg.getGraphics().drawImage(img,-imgRect[0],-imgRect[1],20);
return subImg ;
}
这是Midp2.0版本:
public static Image createSubImg(Image img,int []imgRect)
{     
return Image.createImage(img,imgRect[0],imgRect[1],imgRect[2],imgRect[3],0) ;
}


对于不同机型,该函数的实现不同,但功能相同,因此使用这个函数的代码在移植时无需修改。当然这样做增加了一些间接性,有可能降低性能。

(3) 按键代码不同

我们知道MIDP提供了Game Action,和按键代码无关,但这不够用啊,我们完全可以定义自己的Game Action,但首先让我们定义自己的虚拟按键码吧。我使用位记录每个键的状态,每个位代表一个按键,一个int有32个位所以足够了。

当keyPressed 发生时,我记下哪些键被按下;同样当keyReleased时,将那些被松开的键使用的位清0。某个键,也就是这个键盘状态整数里的某个位,就是我定义的一个虚拟键。当然它的值总是2的n次方了,和key code完全不搭边,所以需要我们用一个映射函数将key code映射到这些虚拟键。

这个函数就是移植的关键,每个机型都要改写这个映射函数,在里面填入正确的key code。你可以在虚拟键的基础上再定义Game Action,支持在游戏中设置按键,这样就更灵活了。

(4) 封装库

如果想不更改一行代码就从MotorolaV600移植到Nokia N-Gage,那么为他们封装不同的库吧。我就这样在1分钟内完成了移植。我的库包含了一个游戏框架类(内含游戏循环和渲染函数,键盘处理,以及若干跨机型的工具函数),一个图形组管理类(管理图片的载入切割旋转绘制和动画等,有点像GameAPI中的Sprite)和一个控件类(包含了所有我需要的控件)。

这3个类封装了不同机型的所有差异,我需要为每种机型改写这三个类,当然大部分代码是相同的了。此外我还写了一个工具支持图形组管理类,所见即所得的编辑动画和管理图片,当然这也对移植有帮助。

总结:

以上几条,总得讲来,无非是拆合而以。主要是要将差异性独立出来,便于更改。但是移植总得来讲还是比较郁闷,主要原因是各种机型有各自的bug,这就需要特殊处理啦。各位写代码时一定要想好移植的问题啊


J2ME RPG游戏边学边做(十一)--游戏中的loading设计

    为什么很多游戏要加入Loading滚动条呢?加入Loading状态并不是为了使软件显得更专业美观,而是为了保证程序的运行内存不溢出。通常计算机/手机的存储系统分为:cup 的缓存,磁盘(或者手机中的存储用的的FLASH RAM或者其他类型的可以持久保存的存储系统),运行内存。我们知道通常NOKIA S40的heap size为200KB大小,而通常我们加入程序和3张128*128的图片之后内存就趋于崩溃了,再加入声音和地图,程序的运算内存就显得太不够了。一般来讲,很多游戏仅仅在运行的时候把所有的资源一次性读入heap memory这样,我们在模拟器看到程序运行的状况就非常接近崩溃的边缘,如果不小心加入了新的图片,可能就没有足够的运算内存了。

    我们如何解决heap size不够的事情呢?手机是不能够改变其heap size的,我们只有想办法控制heap memory的使用。最直观的做法就是:存储内存与运算内存的优化使用,当运算内存需要资源时从存储内存中调用,需要新的资源时,就把不需要的释放掉。下面我就结合一段代码解释我们是如何制作Loading状态的。

    众所周知,Java是内置多线程的,我们可以使用两个线程来解决loading的问题,一个读资源的线程,一个绘制资源的线程。程序代码:


import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
/**
* Loading演示
* @author gaogao
* */
class MainCanvas
extends Canvas
implements Runnable {
//程序状态
static final int LOADING = 0;
static final int GAMEING = 1;
//程序状态控制器
int state = LOADING;
//主线程
Thread thread = null;
//是否loading完毕,
boolean isLoaded = false;
//内部类,新开读取资源的 线程
class Loading
implements Runnable {
//内线程
Thread innerThread = null;
public Loading() {
innerThread = new Thread(this);
innerThread.start();
}
int counter = 100;
public void run() {
//模拟读取资源
//把下面的东西改成读取资源的代码即可
while (counter > 0) {
counter--;
try {
Thread.sleep(20);
}
catch (Exception ex) {}
}
//loading结束
isLoaded = true;
}
}
Loading loading = null;
public MainCanvas() {
loading = new Loading();
thread = new Thread(this);
thread.start();
}
int loadingCounter = 0;
//绘制..
public void paint(Graphics g) {
g.setColor(0);
g.fillRect(0, 0, getWidth(), getHeight());
switch (state) {
case LOADING: {
g.setColor(0XFFFFFF);
g.drawString("LOADING" + ">>>>>".substring(0, loadingCounter),
getWidth() >> 1, getHeight() >> 1,
Graphics.HCENTER | Graphics.TOP);
loadingCounter = ++loadingCounter % 5;
}
break;
case GAMEING: {
g.setColor(0XFFFFFF);
g.drawString("GAME", getWidth() >> 1, getHeight() >> 1,
Graphics.HCENTER | Graphics.TOP);
}
break;
}
}
public void run() {
while (true) {
try {
Thread.sleep(100);
}
catch (Exception ex) {
}
if (isLoaded) {
loading = null;
state = GAMEING;
}
repaint(0, 0, getWidth(), getHeight());
serviceRepaints();
}
}
}
public class Main
extends MIDlet {
MainCanvas mc;
public void startApp() {
if (mc == null) {
mc = new MainCanvas();
Display disp = Display.getDisplay(this);
disp.setCurrent(mc);
}
}
public void destroyApp(boolean bool) {}
public void pauseApp() {}
}

J2ME RPG游戏边学边做(十二)--J2ME 记录管理存储基础

移动信息设备框架(Mobile Information Device Profile)? 移动 Java 应用程序的平台 ?

为 MIDP 应用程序提供一种跨多个调用持久存储数据的机制。这种持久存储机制可以被视为一种简单的面向记录的数据库模型,被称为记录管理系统(record management system(RMS))。在此,Soma Ghosh 说明了您的 J2ME 应用程序怎样能够使用 RMS 来管理和解释数据。通过一个样本电话数据库,您还将了解到关于这个概念的说明。

J2ME 记录管理系统

J2ME 记录管理系统(RMS)提供了一种机制,通过这种机制,MIDlet 能够持久存储数据,并在以后检索数据。在面向记录的方法中,J2ME RMS 由多个记录存储构成。

可以将每个记录存储想像成一个记录集合,它将跨多个 MIDlet 调用持久存在。设备平台负责在平台正常使用的整个过程(包括重新启动、换电池等)中,尽全力维护 MIDlet 的记录存储的完整性。

记录存储在与平台相关的位置(比如非易失性设备存储器)创建,这些位置不直接公开给 MIDlet。RMS 类调用特定于平台的本机代码,这种本机代码使用标准 OS 数据管理器函数来执行实际的数据库操作。

记录存储实现确保所有单个的记录存储操作都是原子的、同步的以及序列化的,因此多个访问将不会出现数据毁坏。记录存储被盖上时间戳来指示它上次被修改的时间。记录存储还维护版本(version),它是一个整数,修改记录存储内容的操作每发生一次,这个数加一。版本和时间戳对于同步目的很有用。

当 MIDlet 使用多个线程访问一个记录存储时,协调该访问是 MIDlet 的责任;如果它不能这样做,可能出现无法意料的结果。同样,如果一个平台使用试图同时访问记录存储的多个线程执行记录存储的同步,那么对 MIDlet 及其同步引擎之间的记录存储实施排外访问是平台的责任。

记录存储中的每个记录是一个字节数组,并且有唯一的整数标识符。

管理设备数据库

javax.microedition.rms.RecordStore 类代表 RMS 记录存储。它提供了几个方法来管理以及插入、更新和删除记录存储中的记录。

管理记录存储

要打开一个记录存储,调用 javax.microedition.rms.RecordStore 的 openRecordStore() 方法。

public static RecordStore openRecordStore(String recordStoreName,
boolean createIfNecessary) 打开具有指定名称 recordStoreName 的记录存储。

如果没有具有这个名称的记录存储,那么调用这个方法来创建一个。

如果记录存储已经打开,这个方法将返回对同一个记录存储对象的引用。

清单 1. 打开一个 RecordStore
RecordStore rs = RecordStore.openRecordStore("MyAppointments",true);
一旦所有操作完成,对 closeRecordStore() 的调用将关闭指定名称的记录存储。当一个记录存储被关闭时,不能进行进一步的操作。

清单 2. 关闭一个 RecordStore
Rs.closeRecordStore();
通过调用 deleteRecordStore() 方法可以删除指定名称的记录存储。

清单 3. 删除一个 RecordStore
RecordStore.deleteRecordStore("MyAppointments");
插入记录
MIDlet 调用 javax.microedition.rms.RecordStore 类的 addRecord() 方法来将一条新记录插入到记录存储中。这是阻塞的原子操作,并返回新记录的 recordId。在这个方法返回之前,记录被写到持久存储中。
public int addRecord(byte[] data, int offset, int numBytes) 插入一条由字节数组 data 代表的记录,这个数组以 offset 作为它的起始索引,numBytes 作为它的长度。

清单 4. 插入一条记录
String appt = "new record";
byte bytes[] = appt.getBytes();
rs.addRecord(bytes,0,bytes.length);

更新记录

更新一条特殊记录包括获取这个记录的句柄以及设置新信息。
public int getRecord(int recordId, byte[] buffer, int offset) 返回存储在由 buffer 代表的字节数组中给定记录的数据。public byte[] getRecord(int recorded) 返回由 recordId 代表的数据的副本。public void setRecord(int recordId, byte[] newData, int offset, int numBytes) 在 recordId 所代表记录的位置设置新信息,新信息是以 offset 作为它的起始索引,并以 numBytes 作为它的长度的字节流(newData)。

清单 5. 更新一条记录
String newappt = "update record";
Byte data = newappt.getBytes();
Rs.setRecord(1, data, 0, data.length());
删除记录
MIDlet 调用 deleteRecord() 方法来从记录存储中删除记录。
public void deleteRecord(int recordId) 删除由 recordId 代表的记录。这个记录的 recordId
接下来不能重用。

清单 6. 删除一条记录
Rs.deleteRecord(1);
数据解释
J2ME API 提供某种接口来解释存储在记录存储中的数据。这个过程包括比较记录来确定它们的相对排序。
它还包括根据给定条件的内容过滤。
比较记录
MIDlet 实现 RecordComparator 接口,并定义 compare (byte[] rec1, byte[] rec2) 方法来比较两个候选记录。这个方法的返回值必须指示这两条记录的顺序。

清单 7. 比较记录并确定相对排序
Int compare (byte[] b1, byte[] b2)
{
String s1 = new String(b1);
String s2 = new String(b2);
If (s1.compareTo(s2) > 0)
Return RecordComparator.FOLLOWS;
Else if (s1.compareTo(s2) == 0)
Return RecordComparator.EQUIVALENT;
Else
Return RecordComparator.PRECEDES;
}
[code]

枚举记录
RecordEnumeration 接口负责枚举记录存储中的记录。它逻辑上维护记录存储中一连串的记录的 recordId。枚举器将以记录比较器确定的顺序迭代所有记录(或者如果提供了一个可选的记录过滤器,那么只是一个子集)。如果既没有指定过滤器又没有指定比较器,枚举将以未定义的顺序遍历记录存储中的所有记录。

清单 8. 枚举记录
[code]
RecordEnumeration re = rs.enumerateRecords(null, null, false);
If (re.hasNextElement())
Byte nextRec[] = re.nextRecord();
过滤记录
MIDlet 实现 RecordFilter 接口,定义检查记录是否满足应用程序定义的标准的过滤器。这个应用程序实现 RecordFilter 的 match() 方法来选择 RecordEnumeration 返回的记录。

清单 9. 过滤记录
Public boolean matches(byte[] candidate)
{
String s1 = new String(candidate);
If (s1.equals("XX"))
Returns true;
Else
Returns false;
}

开发电话约会簿

在这部分,我们将通过构建一个电话约会簿来说明 J2ME RMS 的功能。这个应用程序将允许用户设置某个日期和时间的约会,取消约会或查看已经设置好的约会列表。

构成这个应用程序的各种屏幕以及屏幕元素的用户界面元素的完整列表在与 J2ME Wireless Toolkit 一起提供的 MID 框架 API 文档中可以得到;要获取关于这些元素的更多详细信息,请查阅我早些时候给 developerWorks 写的一篇文章(请参阅下面的参考资料部分以获取这两个链接)。

记录存储可以以字节流形式存储记录。在我们的应用程序中,用户输入的日期和时间被连接成一个字符串,转换成字节,然后被存储。

清单 10. 将一个新的约会添加到数据库中

Public boolean matches(byte[] candidate)

String appt = apptName + " " + apptTime;

byte bytes[] = appt.getBytes();

rs.addRecord(bytes,0,bytes.length);

同样,这个应用程序以字节流形式检索记录,然后将它转换成一个字符串。这个字符串以 ####AAAA 格式,其中 # 表示代表时间信息的数字,AAAA 表示代表约会描述的字符。这个应用程序解析这个字符串来获得日期和时间信息,并以用户所希望的格式显示它们,比如 description - mm/dd/yyyy hh:mm AM_PM

清单 11. 从记录存储检索一条记录

byte b[] = rs.getRecord(j);

String str = new String(b,0,b.length);

清单 12. 解析从记录存储获得的数据,然后以用户所希望的格式显示

 

if (Character.isDigit(str.charAt(i)))

at += str.charAt(i);

else

name += str.charAt(i);

time = Long.parseLong(at);

java.util.Date date = new java.util.Date(time);

java.util.Calendar rightNow = java.util.Calendar.getInstance();

rightNow.setTime(date);

String year = String.valueOf

(rightNow.get(java.util.Calendar.YEAR));

String month = String.valueOf

(rightNow.get(java.util.Calendar.MONTH) + 1);

String day = String.valueOf

(rightNow.get(java.util.Calendar.DATE));

String displayName = name

+ "-" + year + " " + day;

用户被允许从记录存储中选择某种约会以及将它们从记录存储中删除。因为为了维持记录中原始的顺序所删除的 recordId 不能重用,所以这个记录通过特有的字符串模式标记为无效。清单 13. 将一条记录标记为已删除

String deactive = "@";

byte b[] = deactive.getBytes();

rs.setRecord(m+1, b,0,b.length);

 

当这个应用程序显示一个约会列表时,它检测那些无效记录的字符串模式,然后跳过它们。

清单 14. 跳过无效记录

if (!(str.startsWith("@")))

{

// Record is valid

}

else

{

// Record is invalid.

}

 

这个应用程序的一个重要的方面是用户界面。各种屏幕有下面这些:

欢迎表单:欢迎表单显示一个已经设置好的约会列表,如果没有设置约会,则通知用户。它提供继续或退出这个应用程序的各种选项。

菜单表单:菜单表单给用户提供查看约会、设置新约会或取消约会等选项。

显示表单:显示表单显示已经设置好的约会列表。

设置表单:设置表单提供一个日期选择域和一个输入文本域,以提供新约会的详细信息。当用户选择保存时,这条信息被存储到数据库中。

删除表单:删除表单列出一组约会,并提供选择一个或多个的选项。如果用户选择删除,所选择的这组约会在记录存储中被标记为无效。

应用程序实现使自己能够响应各种事件的 CommandListener ItemStateListener 接口。

ItemStateListener 使应用程序能够接收指示下面这些东西的内部状态的改变的事件:

DateField,一个显示日期和时间的可编辑组件

TextField,一个可编辑文本组件

ChoiceGroup,一组可选择的元素

清单 15. 从屏幕获取值

// The date value is set to a variable when the

// DateField item is changed

if (item == apptDate)

{

date = apptDate.getDate();

apptTime = String.valueOf(date.getTime());

}

// The name of appointment is set to a variable

//when the name input field is changed

if (item == apptField)

{

apptName = apptField.getString();

}

// If the ChoiceGroup item state on Delete form is

//changed, it sets an array of appointments selected for deletion

if (item == cg)

{

cg.getSelectedFlags(deleted);

}

清单 16 包含这个样本应用程序的完整清单。请参阅参考资料部分中我以前的一篇关于 J2ME 的文章,以获取关于下载将使您能够在桌面上运行这个程序的设备仿真器的指导。
总结
在本文中,我们讲述了 MID 应用程序持久存储和检索数据的能力;这种机制根据简单的面向记录的数据库建模。J2ME API javax.microedition.rms 包提供了一个开发者的方法和接口宝库,从而可以利用 MID 应用程序的这种独特功能。现在,您应该能够将数据存储集成到您自己的微型 Java 应用程序中了。

J2ME RPG游戏边学边做(十三)--J2ME 记录管理存储增强篇 
 Record Management System是MIDP的子系统,提供了数据的持久性存储功能,本文并非上次讲述Record Management System的基础知识,而是从编程的角度提出高效使用Record Management System的建议。如果您对RMS还不够了解请参考本专题其他的文章。 
    在RecordStore中存储的数据是以字节的形势存在的,MIDP规范中并没有规定什么数据可以存储在RMS中,只要他可以转换成字节数组。那么读取和写入这些字节数据的时候我们应该注意些什么问题呢?

   由于非挥发性内存的存取速度都比较慢,因此我们应该尽量的少对RMS进行写操作,当然这也和设备有关系,有些设备的写操作是非常好费资源的。在读取数据的时候我们应该尽量复用对象,避免大量的创建对象然后丢弃对象,这样会给Heap和GC造成不小的负担。     看下面读取数据的两个不同的代码片断
//片断1
RecordStore rs = ....; // an open record store
try {
int lastID = rs.getNextRecordID();
byte[] data;

for( int i = 0; i < lastid; ++i ){
try {
data = rs.getrecord( i );
.... // do something with the data
}
catch( invalidrecordidexception e ){
continue;
}
}
}
catch( exception e ){
// error
}

//片断2
RecordStore rs = ....; // an open record store
try {
RecordEnumeration enum = rs.enumerateRecords(
null, null, false );
while( enum.hasNextElement() ){
byte[] data = enum.nextRecord();
.... // do something with the data
}
}
catch( Exception e ){
// error
}

上面的代码存在的问题是系统每次读取记录都要创建新的字节数组对象,这显然不够高效。
其实我们可以对字节数组进行复用,并可以适当的调整它的大小。
RecordStore rs = ....; // an open record store
try {
RecordEnumeration enum = rs.enumerateRecords(
null, null, false );
byte[] data = new byte[100];
int len = 0;

while( enum.hasNextElement() ){
int id = enum.nextRecordId();
len = rs.getRecordSize( id );
if( len > data.length ){
// add a growth factor
data = new byte[ len + 40 ];
}
rs.getRecord( id, data, 0 );
// do something with the data
}
}
catch( Exception e ){
// error
}


    在我们读取数据的时候,通常还会用到javaIO,比如ByteArrayInputStream和DataInputStream类,那么在使用他们的时候,我们也应该尽量复用对象。比如当我们从RMS中读取纪录的时候,假设纪录包括一个布尔型和两个整型数据。

RecordStoreEnumeration enum = ...; // get a record enumeration
byte[] data = new byte[9]; // record size
ByteArrayInputStream bin = new ByteArrayInputStream( data );
DataInputStream din = new DataInputStream( bin );
while( enum.hasNextElement() ){
int id = enum.nextRecordId();
getRecord( id, data, 0 );
din.reset(); // move stream back to start

boolean first = din.readBoolean();
int second = din.readInt();
int third = din.readInt();

// do something here
}
下面提供一个封装好的Record类,你可以在使用RMS的时候使用它
import java.io.*;
import javax.microedition.rms.*;
public class Record implements DataInput {
private RecordStore _rs;
private byte[] _data;
private int _length;
private int _id;
private DataInputStream _din;
public Record( RecordStore rs ){
this( rs, 100 );
}
public Record(
RecordStore rs, int initialRecordSize ){
_rs = rs;
_data = new byte[ initialRecordSize ];
_din = new DataInputStream(
new ByteArrayInputStream( _data ) );
_length = -1;
}
public byte[] getByteArray() { return _data; }
public int getLength() { return _length; }
public byte[] moveTo( int id )
throws RecordStoreNotOpenException,
InvalidRecordIDException,
RecordStoreException,
IOException
{
_length = _rs.getRecordSize( id );
if( _length > _data.length ){
_data = new byte[ _length + 40 ];
_din = new DataInputStream(
new ByteArrayInputStream( _data ) );
}
_rs.getRecord( id, _data, 0 );
_id = id;
_din.reset();
return _data;
}
public void readFully(byte b[])
throws IOException {
_din.readFully( b );
}
public void readFully(byte b[], int off, int len)
throws IOException {
_din.readFully( b, off, len );
}
return _din.skipBytes( n );
}
public boolean readBoolean() throws IOException {
return _din.readBoolean();
}
public byte readByte() throws IOException {
return _din.readByte();
}
public int readUnsignedByte()
throws IOException {
return _din.readUnsignedByte();
}
public short readShort() throws IOException {
return _din.readShort();
}
public int readUnsignedShort()
throws IOException {
return _din.readUnsignedShort();
}
public char readChar() throws IOException {
return _din.readChar();
}
public int readInt() throws IOException {
return _din.readInt();
}
public long readLong() throws IOException {
return _din.readLong();
}
public String readUTF() throws IOException {
return _din.readUTF();
}
}
使用起来非常简单
try {
rs = RecordStore.openRecordStore( "mydata", true );

// Write two records to the record store
ByteArrayOutputStream bout =
new ByteArrayOutputStream();
DataOutputStream dout =
new DataOutputStream( bout );
byte[] data;
dout.writeUTF( "this is a test" );
dout.writeInt( 1 );
dout.flush();
data = bout.toByteArray();
rs.addRecord( data, 0, data.length );
bout.reset();
dout.writeUTF( "this is another test" );
dout.writeInt( 99 );
dout.flush();
data = bout.toByteArray();
rs.addRecord( data, 0, data.length );

// Now read through the record store
Record record = new Record( rs );
int lastID = rs.getNextRecordID();
RecordEnumeration enum = rs.enumerateRecords(
null, null,
while( enum.hasNextElement() ){
int id = enum.nextRecordId();
record.moveTo( id );
System.out.println( record.readUTF() + " " +
record.readInt() );
}
rs.closeRecordStore();
}
catch( Exception e ){
// handle error
} J2ME RPG游戏边学边做(十四)--特辑:地图的设计与绘制

在开发很多类型的游戏中,地图系统都需要良好的设计,直观的说,我们需要的地图系统仅仅是一个2D数组,然后用最快的方式将数组影射到屏幕上。

游戏中的地图通常不是由程序员用键盘输入到程序里然后再在程序中修改然后再修改的狂乱过程,而是一般先由程序员做一个地图编辑器,在这个地图编辑器中用鼠标点点点,再保存的过程,或者是从网络上下载的一些成熟编辑器比如:mappy这样的工具生成地图,再用脚本语言为mappy写一个应该保存成什么样格式的程序。通常地图分为45度角,侧视角和俯视角等等,45度角的也有很多种,这种视角相对俯视角和侧视叫较复杂,我们主要讨论俯视角,其实侧视叫和俯视角主要的区别是图片的表现风格不一样,比如雷电这样的空战就是俯视角,mario这样的游戏就是侧视角,可以用相同的地图编辑器做出来。综上,你要知道游戏地图不是程序员用程序写出来的,你喜欢写也可以,修改起来较麻烦,也不能像资源一样动态管理而是一次性读入到内存里,比较不爽。
在这个文章里面,我们假设我们的2D数组是通过,资源读取出来的,内容如下:
public static byte[][] B_MAZE_2D_ARRAY = {
{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
, {
0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
, {
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
, {
0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
, {
0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}
, {
0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}
, {
0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}
, {
0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0}
, {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0}
, {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0}
, {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
这个地图中一共0,1两种byte数,0代表一种图块,1代表一种图块,也可以是动画图块,你绘制的时候区别一下就可以了。因为我们程序里面想把这整个图形绘制出来的话有N多种方案,我给出两个比较合理的方案,当然第二种是比较优化的.我们假设你的主角一直在屏幕中央,当你主角移动的时候,地图相应的变化,就是说主角为参照物,地图动.我们知道地图的大小要超过屏幕的,我们需要设定一个坐标系统,我的方法是,以左上角为0,0也就是和我们常用的 Canvas的坐标系统是相同的,我们的图块大小为:ELEMENT_WIDTH, ELEMENT_HEIGHT,所以我们整个地图的面积(绝对面积)是ELEMENT_WIDTH * 横坐标的块数 * ELEMENT_HEIGHT*纵坐标的块数。因此,我们把这么大的题图画在屏幕上时,需要把需要画的坐标面积(也就是屏幕面积)从这个地图中拿出来,其他地方被切除,这就比较的高效了。
方法一:循环整个2维数组,不需要的地方不绘制只绘需要的部分:
int kI = 0;// 表示
int kJ = 0;
//nEleStartedX, nEleStartedY表示从2D Array哪个位置开始绘制地图
for (int i = nEleStartedY; i < B_MAZE_2D_ARRAY.length; i++) {
kJ = 0;
//是否需要绘制
boolean isDrawed = false;
for (int j = nEleStartedX; j < B_MAZE_2D_ARRAY[i].length; j++) {
//绘制需要的面积,N_MAZE_ELEMENT_WIDTH,N_MAZE_ELEMENT_HEIGHT表示图块宽高
int bX = nMapStartedX + j * N_MAZE_ELEMENT_WIDTH;
int bY = nMapStartedY + i * N_MAZE_ELEMENT_HEIGHT;
//SCREEN_WIDTH,SCREEN_HEIGHT屏幕大小
if (bX <= SCREEN_WIDTH
&&
bY <= SCREEN_HEIGHT
&&
bX >= -N_MAZE_ELEMENT_WIDTH
&&
bY >= -N_MAZE_ELEMENT_HEIGHT
) {
g.drawImage(mapImages[B_MAZE_2D_ARRAY[i][j]], bX,
bY,
Graphics.TOP | Graphics.LEFT);//绘制图块
isDrawed = true;
kJ++;
// N_MAX_MAZE_ITEM_X , N_MAX_MAZE_ITEM_Y屏幕面积内图块的最大值
if (kJ > N_MAX_MAZE_ITEM_X + 2) {
break;
}
}
}
if (isDrawed) {
kI++;
}
if (kI > N_MAX_MAZE_ITEM_Y + 2) {
break;
}
}
方法二:事先找到需要绘制的横坐标纵坐标的图块编号(2DArray的数组下标),循环屏幕面积大小的数组:

// 需要绘制的2DArray左上角位置,nMapStartedX,nMapStartedY在地图绝对面积上的坐标
int nArrayI = ( -N_MAZE_ELEMENT_HEIGHT - nMapStartedY) /
N_MAZE_ELEMENT_HEIGHT;
int nArrayJ = ( -N_MAZE_ELEMENT_WIDTH - nMapStartedX) /
N_MAZE_ELEMENT_WIDTH;
for (int i = nArrayI;
i < SCREEN_HEIGHT / N_MAZE_ELEMENT_HEIGHT + 2; i++) {
for (int j = nArrayJ;
j < SCREEN_WIDTH / N_MAZE_ELEMENT_WIDTH + 2; j++) {
if (i < 0 || j < 0 || i > B_MAZE_2D_ARRAY.length ||
j > B_MAZE_2D_ARRAY[0].length) {
continue;
}
else {
int bX = nMapStartedX + j * N_MAZE_ELEMENT_WIDTH;
int bY = nMapStartedY + i * N_MAZE_ELEMENT_HEIGHT;
g.drawImage(mapImages[B_MAZE_2D_ARRAY[i][j]], bX,
bY,
Graphics.TOP | Graphics.LEFT);
}
}
}
根据我的测试,方法一的地图面积越大fps掉的越为厉害,而方法二基本上不会掉fps,强烈推荐方法二.
地图系统做好了之后,你就可以使用地图做更多的表现力了,只要改变nMapStartedX,nMapStartedY,就可以绘制出地图上的相应部分,代码的复用效率非常的高。RPG, SLG, PUZZLE等游戏类型都可以使用.
欢迎跟我探讨更多的游戏制作技术,我还将写一个关于动画的相关东东,不过最近没什么时间
转自:matrix.org.cn