MIDP2.0 Game API入门
一、 Game API结构体系
五个类构成:
GameCanvas继承自Canvas,具有Canvas提供的所有的功能,在Canvas基础上增加了便于游戏设计的功能:
1、 键盘事件处理方面:
过去要等keyPressed()/keyRelease()/keyRepeated()被调用之后才能知道按键被按下的状态。而在GameCanvas中提供了getKeyStates()方法,可以在同一个线程中自己检测按键的状态。某些设备,getKeyStates()可以检测到很多按钮同时间被按下的情形。
2、 图形绘制方法:
提供flushGraphics()方法,相当于过去repaint()再调用serviceRepaints(),而且还带有双缓冲区的概念,但flushGraphics并不会产生重绘事件,而是直接将Off-Screen的内容显示到屏幕上,所以在GameCanvas中,paint()的地位就不像过去那样重要了。
3、 图层管理:
利用LayerManager可以实现管理许多图层的功能,可以方便的将前景与背景混合在同一个画面之后再输出到屏幕上。LayerManager中可以有多个Layer子类。
二、 使用GameCanvas
每产生一个GameCanvas子类,其内部就会产生一块Off-Screen,大小与全屏幕模式的宽高相同。所以,除非必要,不要产生太多的GameCanvas,这样会占用太多的内存空间。
CameCanvas的paint()方法默认情况下就是绘出Off-Screen的内容:
public void paint(Graphics g)
{
g.drawImage(offscreen_buffer, 0, 0, 0);
}
所以,一般我们不需要在我们编写的类中重写paint()方法。
GameCanvas唯一的构造方法有一个参数,该boolean型参数的意义是:是否抑制键盘事件,true抑制,false不抑制。传入true,系统抑制大多数键盘事件的产生,keyPressed()/keyRelease()/keyRepeated()将不会被调用。传入false,则用户按下按钮,就会产生键盘事件。
可见,构造方法的第一件事就是super(true)或者super(false)。
GameCanvas中之所以可以选择抑制键盘事件的发生,是因为我们可以通过getKeyStates()取得按键被按下的状态。
注意:抑制键盘事件,只在当前画面有效。
GameCanvas中,图形都被绘制到Off-Screen上,而不是直接被绘在屏幕上。程序中调用getGraphics取得的Graphics对象,是属于Off-Screen的。
绘制好Off-Screen后,可以调用flushGraphics()将Off-Screen的内容绘制到屏幕上。flushGaphics()会等到Off-Screen真正被绘制到屏幕上才会返回。相当于过去调用repaint()再调用serviceRepaints()的功能,并且带有双缓冲概念。但flushGraphics()并不会产生重绘事件,而是直接将Off-Screen的内容显示到屏幕上,所以,调用flushGraphics()时,并不会调用paint()方法。
如果希望只是重绘Off-Screen的某些部分,可以调用flushGraphics()具有四个参数的重载方法,给定x、y、宽度、高度即可。
使用GameCanvas基本步骤:
1、 import javax.microedition.lcdui.game.* ;
import javax.microedition.lcdui.* ;
2、 extends GameCanvas;
3、 构造方法中调用super(true)或者super(false)选择抑制或者不抑制键盘事件;
4、 获得Off-Screen的Graphics实例g;
5、 利用Off-Screen的g绘图至Off-Screen并调用flushGraphics将其显示。
例:
import javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
//1.extends GameCanvas
public class BaseGameCanvas extends GameCanvas
{
private Graphics g;
public BaseGameCanvas()
{
//2.调用super()选择是否抑制键盘事件
super(true);
//3.获得Off-Screen的Graphics
g = getGraphics();
//绘图
render(g);
}
public void render(Graphics g)
{
//4.利用获取的Off-Screen在其表面绘制图形
g.setColor(127, 127, 127);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(255, 0, 0);
g.drawString("Hello the world!", 10, 50, 0);
//5.将Off-Screen输出到屏幕
flushGraphics();
}
}
三、 取得键盘状态
GameCanvas中改变了以往等待键盘事件发生再决定动作的做法,使用getKeyStates()方法主动查询键盘状态。
GameCanvas提供的键盘码常量有9个:
常量
功能
UP_PRESSED
向上方向键
DOWN_PRESSED
向下
LEFT_PRESSED
向左
RIGHT_PRESSED
向右
FIRE_PRESSED
发射
GAME_A_PRESSED
游戏A键,不是所有设备都支持
GAME_B_PRESSED
游戏B键,不是所有设备都支持
GAME_C_PRESSED
游戏C键,不是所有设备都支持
GAME_D_PRESSED
游戏D键,不是所有设备都支持
其中,UP_PRESSED、DOWN_PRESSED、LEFT_PRESSED、RIGHT_PRESSED及FIRE_PRESSED对应手机键盘上的方向键及Select键或者有的手机对应2、8、4、6
及5键。
GAME_A_PRESSED、GAME_B_PRESSED、GAME_C_PRESSED、
GAME_D_PRESSED分别对应键盘的1、3、7、9键。
在GameCanvas中获得手机键盘码并进行验证的方法如下例所示,验证了键盘是否被按下上键和下键:
int keystate = getKeyStates();
if ((keystate & GameCanvas.UP_PRESSED) != 0)
{
y = y - 2;
}
else if ((keystate & GameCanvas.DOWN_PRESSED) != 0)
{
y = y + 2;
}
由于GameCanvas中键盘状态的获得并没有提供键盘监听的方法,仅仅调用getKeyStates()不能够保证一定能够监听到键盘是否已经被按下,因此需要一个无限循环语句来监听键盘事件。
while (true)
{
int keystate = getKeyStates();
… …
}
通常该循环会被写在一个线程中。
例如:
public void run()
{
long startTime = 0;
long endTime = 0;
while (loop)
{
startTime = System.currentTimeMillis();
input();
render(g);
endTime = System.currentTimeMillis();
if ((endTime-startTime) < rate)
{
try
{
Thread.sleep(rate - (endTime-startTime));
}
catch (java.lang.InterruptedException e)
{
}
}
}
}
public void input()
{
int keystate = getKeyStates();
if ((keystate & GameCanvas.UP_PRESSED) != 0)
{
y = y - 2;
}
else if ((keystate & GameCanvas.DOWN_PRESSED) != 0)
{
y = y + 2;
}
}
四、 Sprite
Sprite就是画面上能够独立移动的图形,是为了实现游戏中角色动画、移动和碰撞检测而设计的。Game API中提供了Sprite类用来方便的建立Sprite。
Sprite类先根据读入的图像在其建立一个Raw Frame数组,另外一个Frame Sequence数组的内容都是画面的索引值。Current Frame指的是目前屏幕上显示的画面。
Sprite开发基础
分割图片:为了便于动画图片资源的管理和内存的合理使用,往往把完整动画的图像的每一帧都绘制在同一完整的图片中,所以在游戏开发的时候需要分割图片。
Sprite分割图片的规则:从左到到右,从上到下。
序号的分配:按照分割的顺序分配的。
分割的要求:既可以按照正方形来分块,也可以按照长方形分块。
Sprite的创建和使用
3个构造方法:
public Sprite(Image image)
public Sprite(Image image, int frameWidth, int frameHeight)
public Sprite(Sprite s)
其中,参数image为要分割的原始图像;参数frameWidth,frameHeight分别制定了将以什么样的宽度和高度分割原始图像。
使用步骤:
1) 创建一个用于读取图像资源的Image对象;
private Image spriteImage;
…
try
{
spriteImage = Image.createImage(“/picName.png”);
}
catch (Exception e)
{
}
2) 构造Sprite,可以指定以什么样的宽高分割图像:
private Sprite sprite;
…
sprite = new Sprite(spriteImage, 32, 32);
3) 成功创建Sprite之后,可以设置图片分割后动画播放的顺序:
可以使用setFrameSequence()方法设置动画播放的序列,该播放序列存放在一个一维数组中。
例,指定播放序号为0,1,2的图片:
private int seq[] = {0, 1, 2}
sprite.setFrameSequence(seq);
注意:数组索引是从0开始的。
4) Sprite动画播放数组设置好后,使用paint()方法可以把Sprite的一帧图像显示
在屏幕上了,例:
Graphics g;
g = this.getGraphics();
sprite.paint(g);
flushGraphics();
5) 使用sprite.nextFrame()方法可以显示下一帧图像,也可以使用setFrame(int index)指定需要播放的图片。
6) 使用sprite.setPosition(int x, int y)可以改变精灵图片在屏幕上显示位置的坐标。
五、 封装Sprite
移动坦克使其在指定区域内移动。
例:
import javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
/**
*
* @author Administrator
*/
public class TankSpriteCanvas extends GameCanvas implements Runnable //3.
{
private Graphics g;
private int rate = 50;
private LayerManager lm;
private Sprite tank;
private int x = 10;
private int y = 10;
private boolean isStopped = false;
public TankSpriteCanvas()
{
super(true);
g = getGraphics();
lm = new LayerManager();
tank = createTank("/res/tank.png");
lm.append(tank);
//render(g);
new Thread(this).start();
}
public Sprite createTank(String pic)
{
try
{
Image tankImg = Image.createImage(pic);
tank = new Sprite(tankImg, 32, 32);
}
catch (Exception e)
{
e.printStackTrace();
}
return tank;
}
public void render(Graphics g)
{
g.setColor(0x00FFFFFF);
g.fillRect(0, 0, getWidth(), getHeight());
//1.
tank.setPosition(x, y);
lm.paint(g, 0, 0);
//2.
g.setColor(0, 0, 0);
g.drawRect(10, 10, 128, 128);
flushGraphics();
}
public void run()
{
long startTime = 0;
long endTime = 0;
while (!isStopped)
{
startTime = System.currentTimeMillis();
input();
render(g);
endTime = System.currentTimeMillis();
if ((endTime-startTime) < rate)
{
try
{
Thread.sleep(rate - (endTime - startTime));
}
catch (Exception e)
{
}
}
}
}
public void input()
{
int keystate = getKeyStates();
if ((keystate & UP_PRESSED) != 0)
{
y = y - 2;
if (y <= 10)
{
y = 10;
}
}
else if ((keystate & GameCanvas.DOWN_PRESSED) != 0)
{
y = y + 2;
if (y >= 138 - tank.getHeight())
{
y = 138 - tank.getHeight();
}
}
else if ((keystate & GameCanvas.LEFT_PRESSED) != 0)
{
x = x - 2;
if (x <= 10)
{
x = 10;
}
}
else if ((keystate & GameCanvas.RIGHT_PRESSED) != 0)
{
x = x + 2;
if (x >= 138 - tank.getWidth())
{
x = 138 - tank.getWidth();
}
}
}
}
可以看到,程序虽然可以运行,但是不够结构化,许多代码纠缠在一起,为了让程序看起来更加清晰,将程序重构如下:
//TankSprite
import javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
/**
*
* @author Administrator
*/
public class TankSprite extends Sprite
{
private int bx = 0;
private int by = 0;
private int speed = 2;
public TankSprite(Image img, int w, int h, int bx, int by)
{
super(img, w, h);
this.bx = bx;
this.by = by;
}
public void moveUp()
{
move(0, -speed);
if (getY() <= 0)
{
setPosition(getX(), 0);
}
}
public void moveDown()
{
move(0, speed);
if (getY() >= (by - getHeight()))
{
setPosition(getX(), by - getHeight());
}
}
public void moveLeft()
{
move(-speed, 0);
if (getX() <= 0)
{
setPosition(0, getY());
}
}
public void moveRight()
{
move(speed, 0);
if (getX() >= (bx - getWidth()))
{
setPosition((bx - getWidth()), getY());
}
System.out.println("********");
}
}
上述代码将角色(坦克)封装成一个独立的类。
//
import javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
public class TankSpriteCanvas extends GameCanvas implements Runnable //3.
{
private Graphics g;
private int rate = 50;
private LayerManager lm;
private TankSprite tank;
private boolean isStopped = false;
public TankSpriteCanvas()
{
super(true);
g = getGraphics();
lm = new LayerManager();
tank = createTank("/res/tank.png");
lm.append(tank);
//render(g);
new Thread(this).start();
}
public TankSprite createTank(String pic)
{
try
{
Image tankImg = Image.createImage(pic);
tank = new TankSprite(tankImg, 32, 32, 128, 128);
}
catch (Exception e)
{
e.printStackTrace();
}
return tank;
}
public void render(Graphics g)
{
g.setColor(0x00FFFFFF);
g.fillRect(0, 0, getWidth(), getHeight());
//1.
//tank.setPosition(x, y);
lm.paint(g, 10, 10);
//2.
g.setColor(0, 0, 0);
g.drawRect(10, 10, 128, 128);
flushGraphics();
}
public void run()
{
long startTime = 0;
long endTime = 0;
while (!isStopped)
{
startTime = System.currentTimeMillis();
input();
render(g);
endTime = System.currentTimeMillis();
if ((endTime-startTime) < rate)
{
try
{
Thread.sleep(rate - (endTime - startTime));
}
catch (Exception e)
{
}
}
}
}
public void input()
{
int keystate = getKeyStates();
if ((keystate & UP_PRESSED) != 0)
{
tank.moveUp();
}
else if ((keystate & GameCanvas.DOWN_PRESSED) != 0)
{
tank.moveDown();
}
else if ((keystate & GameCanvas.LEFT_PRESSED) != 0)
{
tank.moveLeft();
}
else if ((keystate & GameCanvas.RIGHT_PRESSED) != 0)
{
tank.moveRight();
}
}
}
从修改结果看来,现在的程序有条理多了。
六、 Sprite的绘制
默认情况下,Layer的绘制起点(即Layer左上角的点)为相对于LayerManager起点(0, 0)的位置,可以用getX()或者getY()取得当前Sprite的绘制起点位置。
Sprite之中引入了一个称作Reference Pixel的概念,而且每个Sprite默认的Reference Pixel为坐标(0, 0)的位置。可以利用defineReferencePixel()方法来设置Reference Pixel的坐标。
Reference Pixel除了可以用来做为setRefPixelPosition()的参考位置之外,也可以当作setTransform()的参考位置。
可以利用getRefPixelX()/getRefPixelY()来得到参考点实际在LayerManager上的位置。
七、 Sprite的旋转
我们可以借助Sprite提供的名为setTransform()的方法转动Sprite。转动的时候以Reference Pixel为转动中心。如果我们希望以整张图片的中心转动,通常会把Reference Pixel设定在Sprite的中心点(getWidth()/2, getHeight()/2)。
setTransform()方法可以接受的参数有:
?? TRANS_NONE,不转动坐标系;
?? TRANS_ROT90,以Reference Pixel为转动中心,坐标系顺时针转动90度;
?? TRANS_ROT180, 以Reference Pixel为转动中心,坐标系顺时针转动180度;
?? TRANS_ROT270, 以Reference Pixel为转动中心,坐标系顺时针转动270度;
?? TRANS_MIRROR,以Reference Pixel所在的X坐标为准,镜像坐标系;
?? TRANS_MIRROR_ROT90, 以Reference Pixel所在的X坐标为准,镜像坐标系,再顺时针转动坐标系90度;
?? TRANS_MIRROR_ROT180
?? TRANS_MIRROR_ROT270
需要注意的是,这些值并没有叠加效果,也就是说,不会因为前一次转动了90度,然后再要求转动90度,而使得最后屏幕上呈现的是转动180度之后的效果。
例子:
import javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
/**
*
* @author Administrator
*/
public class TankSprite extends Sprite
{
private int bx = 0;
private int by = 0;
private int speed = 2;
private int state = 0;
public TankSprite(Image img, int w, int h, int bx, int by)
{
super(img, w, h);
this.bx = bx;
this.by = by;
defineReferencePixel(w/2, h/2);
}
public void moveUp()
{
move(0, -speed);
if (getY() <= 0)
{
setPosition(getX(), 0);
}
}
public void moveDown()
{
move(0, speed);
if (getY() >= (by - getHeight()))
{
setPosition(getX(), by - getHeight());
}
}
public void moveLeft()
{
move(-speed, 0);
if (getX() <= 0)
{
setPosition(0, getY());
}
}
public void moveRight()
{
move(speed, 0);
if (getX() >= (bx - getWidth()))
{
setPosition((bx - getWidth()), getY());
}
System.out.println("********");
}
public void rotate()
{
state++;
if (state > 3)
{
state = 0;
}
setTankDirection(state);
}
public void setTankDirection(int state)
{
switch (state)
{
case 0:
setTransform(Sprite.TRANS_NONE);
setFrame(0);
break;
case 1:
setTransform(Sprite.TRANS_ROT90);
setFrame(0);
break;
case 2:
setTransform(Sprite.TRANS_ROT180);
setFrame(0);
break;
case 3:
setTransform(Sprite.TRANS_ROT270);
setFrame(0);
break;
}
}
}
在GameCanvas中按钮控制Sprite旋转:
public void input()
{
int keystate = getKeyStates();
if ((keystate & UP_PRESSED) != 0)
{
tank.moveUp();
}
else if ((keystate & GameCanvas.DOWN_PRESSED) != 0)
{
tank.moveDown();
}
else if ((keystate & GameCanvas.LEFT_PRESSED) != 0)
{
tank.moveLeft();
}
else if ((keystate & GameCanvas.RIGHT_PRESSED) != 0)
{
tank.moveRight();
}
else if ((keystate & GameCanvas.FIRE_PRESSED) != 0)
{
tank.rotate();
}
}
八、 精灵图片的参照点
Sprite中的方法,都是通过图片参照点的坐标来计算的,例如setPosition(x, y)方法、getX()、getY()都是通过参照点来计算坐标的。这些方法的一个共同特点就是参照点都是默认为图片或者屏幕的左上角,但是这个参照点是可以更改的。
Sprite类提供了一个defineReferencePixel(x, y)方法可以更改参照点。
参照点可以是动画的一帧图片中的某个位置,需要的话也可以定义在图片区域的外面。
定义了参照点之后,以后图片的移动或者放置于屏幕都将按照参照点的位置进行计算。
九、 Sprite的碰撞检测
Sprite可用于下列情况的碰撞检测:
?? Sprite与Sprite ;
?? Sprite与Image;
?? Sprite与TiledLayer。
每种碰撞模式的检测都有两种:
?? 像素检测:只有在两个不透明的像素交会时,才被判定为发生碰撞。如果是透明像素和透明像素相交会或者透明像素与非透明像素相交会,则不会判定为发生碰撞。
?? 矩形检测:只要两个Sprite默认的碰撞边界相交,就被判定为发生碰撞。
(注意:Sprite的默认的碰撞边界与Sprite本身一样大,可以利用
defineCollisionRectangle()设置Sprite的碰撞边界)
像素检测的优、缺点:图片的矩形范围发生交会,并不会发生碰撞,从视觉效果上看更真实。缺点是计算复杂,浪费大量的系统资源。因此,如果没有特殊需求,我们都尽量使用矩形检测的方法。
Sprite类提供的检测碰撞的方法:
public final boolean collidesWith(Sprite s, boolean pixelLevel)
public final boolean collidesWith(TiledLayer t, boolean pixelLevel)
public final boolean collidesWith(Image image, int x, int y, Boolean pixelLevel)
方法一是检测精灵(Sprite)是否与其他精灵发生碰撞。当pixelLevel为true,使用像素检测;当pixelLevel为false时,使用矩形检测。要求两个精灵都处于可见状态。
方法二是检测精灵是否与其他TiledLayer元素发生碰撞,要求精灵与TiledLayer都处于可见状态。
方法三检测精灵是否与其他非透明的图片区域发生了相交。碰撞的判断是把精灵放置到坐标(x, y)中,然后再判断在这个坐标下精灵是否与图片发生了相交。
在进行矩形检测时,默认检测的区域就是图片的大小范围,但是可以通过使用defineCollisionRectangle方法设置精灵图片检测碰撞的区域,其原形如下:
public void defineCollisionRectangle(int x, int y, int width, int height)
十、 TiledLayer类的开发
Tiled中文译为平铺,TiledLayer类就是用图片来平铺屏幕背景的类,即常说的贴图。游戏的背景通常很大,为了显示人物移动,背景往往需要不断的滚动显示。如果背景使用一整幅很大的图片,制作以及程序的处理都将是低效的。
由于背景的很多局部画面是相同的,所以我们可以使用很多小的图片平铺成完整的背景图片。
创建TiledLayer对象,需要使用TiledLayer类的构造方法:
public TiledLayer(int columns, int rows, Image image, int tileWidth, int tileHeight)
参数:
columns和rows分别指定了图的列数和行数;
image指定了包括贴图在内所需要的所有分块的图像;
tileWidth和tileHeight指定以怎样的宽高分割图像。
例,一幅名为”tiled.png”的图片,宽192像素,高96像素
使用这张图片来创建一个游戏的背景。
1. 首先创建一个拥有3列,6行的TiledLayer对象,同时指定图片”/res/tiled.png”按照宽64像素,高32像素来分割,代码如下:
TiledLayer background;
Image tiledImage = Image.createImage(“/res/tiled.png”);
background = new TiledLayer(3, 6, titleImage, 64, 32);
(可以理解为将titleImage,以宽64,高32分割,然后放到3,6列的TiledLayer中)
由于分配规则是按照64x32的大小来分割,那么一个192x96大小的图片
被分割成9块。分割情况是3行3列,每个分割后的图片将被分配一个序号,
具体分配情况如下:
2. 假设我们要显示的地图是6行3列,那么我们定义一个相应的地图数组,在数组中指定显示图片的序号:
private byte[] titledMap = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
{7, 7, 7},
{7, 7, 7},
{7, 7, 7},
};
如果数组中的数值使用0表示不显示(与Sprite不同)。如果使用的数值超过分配序号的最大数值,将会抛出异常。
3. 显示分割后的小图片需要使用setCell(int col, int row, int tileIndex)方法。其中col指定列,row指定行,tileIndex指定小图片索引。即,将在TiledLayer上的第col列,row行的图片贴为tileIndex指定的图片。
Graphics g = getGraphics();
background.setCell(2, 3, 3);
background.paint(g);
flushGraphics();
范例:
import javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
/**
*
* @author Allan
*/
public class TiledLayerCanvas extends GameCanvas
{
private Graphics g;
private Image tiledImage;
private TiledLayer background;
//定义地图数组
private byte[][] tiledMap = {
{1, 2, 3},
{4, 1, 6},
{7, 8, 9},
{7, 7, 7},
{7, 7, 7},
{7, 7, 7}
};
public TiledLayerCanvas()
{
super(true);
g = getGraphics();
try
{
tiledImage = Image.createImage("/res/tiled.png");
}
catch (Exception e)
{
}
background = new TiledLayer(3, 6, tiledImage, 64, 32);
render(g);
}
public void render(Graphics g)
{
g.setColor(0x00FFFFFF);
g.fillRect(0, 0, getWidth(), getHeight());
for (int i=0; i<6; i++)
{
for (int j=0; j<3; j++)
{
//TiledLayer的图片索引从1开始,0不显示
background.setCell(j, i, tiledMap[i][j]);
}
}
background.paint(g);
flushGraphics();
}
}
现在我们只有一个背景,可以加入行走的精灵。
//MamSprite.java
import javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
/**
*
* @author Allan
*/
public class ManSprite extends Sprite
{
private int bx;
private int by;
private int speed = 2;
public ManSprite(Image img, int w, int h, int bx, int by)
{
super(img, w, h);
this.bx = bx;
this.by = by;
}
public void moveDown()
{
move(0, speed);
}
public void walk(int x, int y)
{
setPosition(x, y);
}
… …
}
//GameCanvas.java
import javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
/**
*
* @author Allan
*/
public class TiledLayerCanvas extends GameCanvas implements Runnable
{
private Graphics g;
private int rate = 150;
private boolean exit = false;
private Image tiledImage;
private TiledLayer background;
//定义地图数组
private byte[][] tiledMap = {
{1, 2, 3},
{4, 1, 6},
{7, 8, 9},
{7, 7, 7},
{7, 7, 7},
{7, 7, 7}
};
private ManSprite man;
private int[] down_seq = { 0, 1, 2};
public TiledLayerCanvas()
{
super(true);
g = getGraphics();
try
{
tiledImage = Image.createImage("/res/tiled.png");
}
catch (Exception e)
{
}
background = new TiledLayer(3, 6, tiledImage, 64, 32);
//建立精灵
man = createManSprite("/res/man.png");
man.setFrameSequence(down_seq);
new Thread(this).start();
}
public ManSprite createManSprite(String pic)
{
Image manImg = null;
try
{
manImg = Image.createImage(pic);
}
catch (Exception e)
{
e.printStackTrace();
}
return new ManSprite(manImg, 32, 48, getWidth(), getHeight());
}
public void render(Graphics g)
{
g.setColor(0x00FFFFFF);
g.fillRect(0, 0, getWidth(), getHeight());
//绘制背景
for (int i=0; i<6; i++)
{
for (int j=0; j<3; j++)
{
//TiledLayer的图片索引从1开始
background.setCell(j, i, tiledMap[i][j]);
}
}
background.paint(g);
//绘制人物
man.nextFrame();
man.walk(getWidth()/2, getHeight()/2);
man.paint(g);
//绘制Off-Screen
flushGraphics();
}
public void run()
{
long startTime = 0;
long endTime = 0;
while (!exit)
{
startTime = System.currentTimeMillis();
render(g);
endTime = System.currentTimeMillis();
if ((endTime-startTime) < rate)
{
try
{
Thread.sleep(rate-(endTime-startTime));
}
catch (Exception e)
{
}
}
}
}
}
运行效果:
十一、 LayerManager类的开发
一款游戏的背景通常不是仅由一个图片的贴图构成,可以使用多个图层构成复杂的背景。
LayerManager类可以用来管理多个图层,可将这些图层组合起来显示于屏幕上。LayerManage不仅可以组合图层,还可以控制图层是否显示于屏幕;设置具体显示窗口,可以仅在LayerManager定义的窗口上显示背景的一部分。
可以使用LayerManager的append()方法添加Layer。每个Layer都会有个索引值,从0开始,索引值越小表示距离屏幕越近,越大距离屏幕越远。
若加入时,该Layer已经存在于LayerManager之中,那么该Layer会先从LayerManager中删除,然后再加入。
可以利用remove()将某个Layer从LayerManager中删除,索引值会被重新索引。
可以利用insert()在任意位置插入Layer。
若插入时,该Layer已经存在于LayerManager之中,那么该Layer会先从LayerManager中删除,然后再进行插入。
可以利用getLayer()取得制定索引的Layer,getSize()取得LayerManager中Layer的数量。
LayerManager内部有一个坐标系,该坐标系与外界无关。可以把LayerManager本身当作一个虚拟屏幕来考虑,不管外部的干扰。
LayerManager类的使用:
?? 使用LayerManager类的构造方法创建LayerManager对象;
?? 使用append()方法添加图层,先添加的图层将显示在屏幕的最前面。
例:
LayerManager lm;
lm = new LayerManager();
//添加图层3,最先加入,最先显示
lm.append(layer_3);
lm.append(layer_2);
lm.append(layer_1);
实例:将下面2幅图片分割后构成2个背景图层,然后利用LayerManager将这2个图层结合起来。
图片1 图片2
第一个图层由图片1分割为3x3共9个小图片贴图而成,每个小图片宽64,高32,图层在屏幕上的坐标为(0,0)。数据表示如下:
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
{7, 7, 7},
{7, 7, 7},
{7, 7, 7}
}
第二个图层由图片2分割为1x2共2个小图片贴图而成,每个小图片宽28,高59。该图层在屏幕上的坐标为(0, 屏幕高度减去59)。其数据表示:
{
{2, 2, 2, 2},
{2, 2, 2, 2}
}
例:
import javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
/**
*
* @author Allan
*/
public class MyGameCanvas extends GameCanvas
{
private Graphics g;
//准备背景TiledLayer
private Image bgImg1;
private Image bgImg2;
private TiledLayer bg1;
private TiledLayer bg2;
private LayerManager lm;
//背景1数组
private byte[][] bg1Map = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
{7, 7, 7},
{7, 7, 7},
{7, 7, 7}
};
//背景2数组
private byte[][] bg2Map = {
{2, 2, 2, 2},
{2, 2, 2, 2}
};
public MyGameCanvas()
{
super(true);
g = getGraphics();
//准备背景TiledLayer
bgImg1 = loadImage("/res/tiled1.png");
bgImg2 = loadImage("/res/tiled2.png");
bg1 = new TiledLayer(3, 6, bgImg1, 64, 32);
bg2 = new TiledLayer(4, 2, bgImg2, 59, 28);
//图层管理
lm = new LayerManager();
//移动背景2到屏幕下方
bg2.move(0, this.getHeight()-59);
//先将背景2加入LayerManager,则背景2最先加入,最先显示
lm.append(bg2);
lm.append(bg1);
render(g);
}
//获取图片的方法
public Image loadImage(String pic)
{
Image tmp = null;
try
{
tmp = Image.createImage(pic);
}
catch (Exception e)
{
}
return tmp;
}
//绘制屏幕的方法
public void render(Graphics g)
{
//绘出背景1
for (int i=0; i<6; i++)
{
for (int j=0; j<3; j++)
{
bg1.setCell(j, i, bg1Map[i][j]);
}
}
//绘出背景2
for (int i=0; i<2; i++)
{
for (int j=0; j<4; j++)
{
bg2.setCell(j, i, bg2Map[i][j]);
}
}
//绘出LayerManager
lm.paint(g, 0, 0);
//绘出Off-Screen
flushGraphics();
}
}
结果如下:
控制可视区域:
LayerManager类除了提供了图层合并的功能以外,还提供了控制图层显示区域大小的功能。
显示区域(View Window):就是相对于LayerManager默认坐标系统位置的一个可视区域, 即LayerManager让屏幕显示出来的部分。例如,背景图片很大,但限于手机屏幕的大小,只能在屏幕上显示一部分图像,这个显示的部分就是显示区域。
通过更改显示区域的坐标,可以达到背景移动的效果。
使用setViewWindow()方法设置View Window的大小:
layerManger.setViewWindow(30, 40, 50, 50);
显示区域的优点,每次设置新的显示区域后,它会自动清除前一次显示的图像,不需要开发人员编写自己的清除功能的代码。
如图,显示区域为85x85像素,相对于LayerManager坐标系统的坐标为(52, 11)。而LayerManager添加了一个背景图层,相对于LayerManager坐标系统的坐标为(18, 37)。另外添加了一个精灵的图层,其相对于LayerManager坐标系统的坐标为(75, 25)。
LayerManager坐标原点的设置:
LayerManager坐标系统原点默认为屏幕左上角,即屏幕坐标原点为(0,0)。可以更改LayerManager坐标系统的原点在屏幕上的位置,更改相对坐标可以在使用paint()方法把图层绘制在屏幕上的时候指定。paint()方法的原型:
public void paint(Graphics g, int x, int y)
参数x,y指定相对坐标。
指定相对坐标为(17, 17)
例子:
//ManSprite.java
import javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
/**
*
* @author Administrator
*/
public class ManSprite extends Sprite
{
private int speed = 5;
public ManSprite(Image img, int w, int h)
{
super(img, w, h);
}
public void moveRight()
{
move(speed, 0);
}
public void moveTo(int x, int y)
{
this.setPosition(x, y);
}
}
//MyGameCanvas.java
import javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
/**
*
* @author Allan
*/
public class MyGameCanvas extends GameCanvas implements Runnable
{
private Graphics g;
private int rate = 150;
private boolean exit = false;
//准备精灵
private Image manImg;
private ManSprite man;
//准备背景TiledLayer
private Image bgImg;
private TiledLayer background;
private LayerManager lm;
private int[] right_seq = {9, 10, 11};
private int drawX = 0;
private int drawY = 0;
private int dx = 0;
private int dy = 0;
public MyGameCanvas()
{
super(true);
g = getGraphics();
//准备精灵
manImg = loadImage("/res/man.png");
man = new ManSprite(manImg, 32, 48);
man.setFrameSequence(right_seq);
drawX = getWidth()/2;
drawY = getHeight() - manImg.getHeight()/3;
//准备背景TiledLayer
bgImg = loadImage("/res/bg.png");
background = new TiledLayer(1, 1, bgImg, 586, 121);
background.setCell(0, 0, 1);
//图层管理
lm = new LayerManager();
//先将man加入LayerManager,则man最先加入,最先显示
man.setPosition(drawX, drawY);
lm.append(man);
//移动背景到屏幕下方
background.move(0, this.getHeight()-bgImg.getHeight());
lm.append(background);
new Thread(this).start();
}
//获取图片的方法
public Image loadImage(String pic)
{
Image tmp = null;
try
{
tmp = Image.createImage(pic);
}
catch (Exception e)
{
System.out.println("************");
}
return tmp;
}
//绘制屏幕的方法
public void render(Graphics g)
{
//绘出LayerManager
lm.paint(g, 0, 0);
//绘出Off-Screen
flushGraphics();
}
public void run()
{
long startTime = 0;
long endTime = 0;
while (!exit)
{
startTime = System.currentTimeMillis();
man.nextFrame();
man.moveTo(drawX + dx, drawY);
lm.setViewWindow(dx, 0,
getWidth(), getHeight());
render(g);
//当显示区域已经移动到差一个屏幕宽度
if (dx < 586 - getWidth())
{
dx = dx + 5;
}
else
{
dx = 0;
}
endTime = System.currentTimeMillis();
if ((endTime-startTime) < rate)
{
try
{
Thread.sleep(rate - (endTime-startTime));
}
catch (Exception e)
{
}
}
}
}
}
运行效果: