GameCanvas与Sprite
这一讲我们来学习MIDP2.0的新增功能GameAPI。 | |
GameAPI?从名称上面看是游戏用的API么? | |
是啊。手机上面的程序大都是游戏吧。MIDP2.0新添了许多方便游戏开发的类。这些类都被包含在了javax.microedition.lcdui.game package里面。 |
1. 使用GameCanvas类
下面我们先来看看GameCanvas类的使用方法。 | |
还有叫GameCanvas名称的类呢。感觉确实是游戏用的Canvas啊。 | |
恩,没错。是方便游戏开发的Canvas类。它继承了目前一直在使用的 javax.microedition.lcdui.Canvas类,因此Canvas备置的方法可以直接使用。 |
GameCanvas与普通的Canvas有什么不同么? | |
GameCanvas最大的特征,就是支持取得offscreen缓冲和按键的状态。 | |
取得offscreen缓冲和按键的状态?呃,不是很明白。 | |
offscreen缓冲,是指在画面以外的地方描绘,然后将描绘结果发送到实际画面的系统。有了这个,就可以防止显示动画时的画面飘飞现象了。 | |
那么,可以取得按键状态,又是什么意思呢?以前不也是通过按键进行程序操作的么。 | |
在以前的应用中,按键被按下时,JVM调出keyPressed方法,这样很容易明白按键的状态。这种方法被称为event驱动,但此方法在按键被按下后到方法的执行存在时间滞留现象,很难直接体现游戏中的动作。 | |
那么实际使用GameCanvas该如何进行程序的开发呢? | |
一般来说与普通的Canvas比较一下,就明白GameCanvas的特征和使用方法了。 咱们使用GameCanvas和Canvas两个类,来制作同样内容的程序,比较一下代码吧。 | |
好的。 | |
下面,先来执行一下下面的GameCanvasTest.java和MyCanvas.java看看吧。这个是使用普通的Canvas类制作的显示动画的简单用例。 |
GameCanvasTest.java
1 | import javax.microedition.lcdui.*; | ||
2 | import javax.microedition.midlet.MIDlet; | ||
3 | |||
4 | public class GameCanvasTest extends MIDlet implements CommandListener { | ||
5 | private Display display; | ||
6 | private MyCanvas canvas; | ||
7 | |||
8 | public GameCanvasTest() { | ||
9 | display = Display.getDisplay(this); | ||
10 | canvas = new MyCanvas(); | ||
11 | canvas.addCommand(new Command("Exit", Command.EXIT, 0)); | ||
12 | canvas.setCommandListener(this); | ||
13 | } | ||
14 | |||
15 | public void startApp() { | ||
16 | display.setCurrent(canvas); | ||
17 | canvas.start(); | ||
18 | } | ||
19 | |||
20 | public void pauseApp() {} | ||
21 | |||
22 | public void destroyApp(boolean unconditional) { | ||
23 | if (canvas != null) { | ||
24 | canvas.stop(); | ||
25 | } | ||
26 | } | ||
27 | |||
28 | public void commandAction(Command c, Displayable s) { | ||
29 | if (c.getCommandType() == Command.EXIT) { | ||
30 | destroyApp(true); | ||
31 | notifyDestroyed(); | ||
32 | } | ||
33 | } | ||
34 | } | ||
MyCanvas.java
1 | import javax.microedition.lcdui.*; | ||
2 | |||
3 | public class MyCanvas extends Canvas implements Runnable { | ||
4 | |||
5 | private final int BALL_SIZE = 20; // ball的大小 | ||
6 | private int ballX; // ball的x坐标 | ||
7 | private int ballY; // ball的y坐标 | ||
8 | private int dx; // ball在x轴的位移 | ||
9 | private int dy; // ball在y轴的位移 | ||
10 | private boolean running; // 根据thread显示有无loop处理的flag | ||
11 | |||
12 | /** | ||
13 | * MyCanvas的constructor | ||
14 | */ | ||
15 | public MyCanvas() { | ||
16 | ballX = getWidth() / 2; | ||
17 | ballY = getHeight() / 2; | ||
18 | dx = 10; | ||
19 | dy = 0; | ||
20 | } | ||
21 | |||
22 | /** | ||
23 | * thread的开始 | ||
24 | */ | ||
25 | public void start() { | ||
26 | running = true; | ||
27 | Thread t = new Thread(this); | ||
28 | t.start(); | ||
29 | } | ||
30 | |||
31 | /** | ||
32 | * 由thread执行的方法 | ||
33 | */ | ||
34 | public void run() { | ||
35 | while(running) { | ||
36 | // 每经过一定时间的处理 | ||
37 | tick(); | ||
38 | |||
39 | // 再描绘命令 | ||
40 | repaint(); | ||
41 | |||
42 | // 100毫秒待机 | ||
43 | try { | ||
44 | Thread.sleep(100); | ||
45 | } | ||
46 | catch (InterruptedException e) { | ||
47 | stop(); | ||
48 | } | ||
49 | } | ||
50 | } | ||
51 | |||
52 | /** | ||
53 | * thread的停止 | ||
54 | */ | ||
55 | public void stop() { | ||
56 | running = false; | ||
57 | } | ||
58 | |||
59 | /** | ||
60 | * 每经过一定时间的处理 | ||
61 | */ | ||
62 | private void tick() { | ||
63 | ballX += dx; | ||
64 | ballY += dy; | ||
65 | |||
66 | if(ballX > getWidth()) { | ||
67 | ballX = 0; | ||
68 | } else if (ballX < 0) { | ||
69 | ballX = getWidth(); | ||
70 | } | ||
71 | |||
72 | if(ballY > getHeight()) { | ||
73 | ballY = 0; | ||
74 | } else if (ballY < 0) { | ||
75 | ballY = getHeight(); | ||
76 | } | ||
77 | } | ||
78 | |||
79 | /** | ||
80 | * 根据被按下的按键变更移动方向 | ||
81 | */ | ||
82 | protected void keyPressed(int key) { | ||
83 | int gameaction = getGameAction(key); | ||
84 | switch(gameaction){ | ||
85 | case Canvas.RIGHT: | ||
86 | dx = 10; | ||
87 | dy = 0; | ||
88 | break; | ||
89 | case Canvas.LEFT: | ||
90 | dx = -10; | ||
91 | dy = 0; | ||
92 | break; | ||
93 | case Canvas.UP: | ||
94 | dx = 0; | ||
95 | dy = -10; | ||
96 | break; | ||
97 | case Canvas.DOWN: | ||
98 | dx = 0; | ||
99 | dy = 10; | ||
100 | break; | ||
101 | default: | ||
102 | } | ||
103 | } | ||
104 | |||
105 | /** | ||
106 | * 描绘处理 | ||
107 | */ | ||
108 | public void paint(Graphics g) { | ||
109 | // 用白色涂满画面 | ||
110 | g.setColor(0xffffff); | ||
111 | g.fillRect(0, 0, getWidth(), getHeight()); | ||
112 | |||
113 | // 描绘红色的圆形 | ||
114 | g.setColor(0xff0000); | ||
115 | g.fillArc(ballX, ballY, BALL_SIZE, BALL_SIZE, 0, 360); | ||
116 | } | ||
117 | } | ||
完成程序了,开始编译…好了,可以运行了。 画面上一个红色的小球开始动了。 | |
按上下左右键可以改变小球移动的方向哦。 |
上面的程序没有涉及到新的知识,代码的内容理解起来不难吧。 | |
是的。使用thread就可以显示动画了啊。 | |
恩,一开始thread就执行run方法了。主要的处理就是run方法中的内容。 | |
Run方法的内容如下所示。 |
Run方法(第34~50行)
public void run() { while(running) { // 每经过一定时间的处理 tick(); // 再描绘命令 repaint(); // 100毫秒待机 try { Thread.sleep(100); } catch (InterruptedException e) { stop(); } } } |
在while loop中利用tick方法变更ball的位置,调出repaint()方法。之后有100毫秒的待机,就可以大概显示出ball位置变化的动画了。 | |
没错,这个方法是使用Canvas类的动画的基本。 | |
按下按键后执行keyPressed()方法,可以改变ball的移动方向。 | |
恩,是这样的。 | |
这样没有问题的话,那还有使用GameCanvas的必要么? | |
GameCanvas里面有Canvas里没有的大的方法。接下来就使用GameCanvas来实现同样内容看看。 将下面的MyGameCanvas.java添加到程序里,把刚才的GameCanvasTest.java第6行和第10行出现的MyCanvas替换为MyGameCanvas。 这样,就替换成使用GameCanvas类的程序了。 | |
明白,我试一下。 |
MyGameCanvas.java
1 | import javax.microedition.lcdui.*; | ||
2 | import javax.microedition.lcdui.game.*; | ||
3 | |||
4 | public class MyGameCanvas extends GameCanvas implements Runnable { | ||
5 | |||
6 | private final int BALL_SIZE = 20; // ball的大小 | ||
7 | private int ballX; // ball的x坐标 | ||
8 | private int ballY; // ball的y坐标 | ||
9 | private int dx; // ball的x轴位移 | ||
10 | private int dy; // ball的y轴位移 | ||
11 | private boolean running; //根据thread显示有无loop处理的 | ||
12 | |||
13 | /** | ||
14 | * MyGameCanvas的constructor | ||
15 | */ | ||
16 | public MyGameCanvas() { | ||
17 | super(true); | ||
18 | ballX = getWidth() / 2; | ||
19 | ballY = getHeight() / 2; | ||
20 | dx = 10; | ||
21 | dy = 0; | ||
22 | } | ||
23 | |||
24 | /** | ||
25 | * thread的开始 | ||
26 | */ | ||
27 | public void start() { | ||
28 | running = true; | ||
29 | Thread t = new Thread(this); | ||
30 | t.start(); | ||
31 | } | ||
32 | |||
33 | /** | ||
34 | * 由thread执行的方法 | ||
35 | */ | ||
36 | public void run() { | ||
37 | Graphics g = getGraphics(); | ||
38 | |||
39 | while(running) { | ||
40 | // 每经过一定时间的处理 | ||
41 | tick(); | ||
42 | |||
43 | // 取得按键的状态 | ||
44 | int keyStates = getKeyStates(); | ||
45 | |||
46 | // 根据按键状态变更移动方向 | ||
47 | if ((keyStates & LEFT_PRESSED) != 0) { | ||
48 | dx = -10; | ||
49 | dy = 0; | ||
50 | } else if ((keyStates & RIGHT_PRESSED) != 0) { | ||
51 | dx = 10; | ||
52 | dy = 0; | ||
53 | } else if ((keyStates & UP_PRESSED) != 0) { | ||
54 | dx = 0; | ||
55 | dy = -10; | ||
56 | } else if ((keyStates & DOWN_PRESSED) != 0) { | ||
57 | dx = 0; | ||
58 | dy = 10; | ||
59 | } | ||
60 | |||
61 | // 用白色涂满画面 | ||
62 | g.setColor(0xffffff); | ||
63 | g.fillRect(0, 0, getWidth(), getHeight()); | ||
64 | |||
65 | // 描绘红色的圆形 | ||
66 | g.setColor(0xff0000); | ||
67 | g.fillArc(ballX, ballY, BALL_SIZE, BALL_SIZE, 0, 360); | ||
68 | |||
69 | // 更新描绘内容 | ||
70 | flushGraphics(); | ||
71 | |||
72 | // 100毫秒待机 | ||
73 | try { | ||
74 | Thread.sleep(100); | ||
75 | } | ||
76 | catch (InterruptedException e) { | ||
77 | stop(); | ||
78 | } | ||
79 | } | ||
80 | } | ||
81 | |||
82 | /** | ||
83 | * thread的停止 | ||
84 | */ | ||
85 | public void stop() { | ||
86 | running = false; | ||
87 | } | ||
88 | |||
89 | /** | ||
90 | * 每经过一定时间的处理 | ||
91 | */ | ||
92 | private void tick() { | ||
93 | ballX += dx; | ||
94 | ballY += dy; | ||
95 | |||
96 | if(ballX > getWidth()) { | ||
97 | ballX = 0; | ||
98 | } else if (ballX < 0) { | ||
99 | ballX = getWidth(); | ||
100 | } | ||
101 | |||
102 | if(ballY > getHeight()) { | ||
103 | ballY = 0; | ||
104 | } else if (ballY < 0) { | ||
105 | ballY = getHeight(); | ||
106 | } | ||
107 | } | ||
108 | } | ||
运行后发现与之前的结果一样啊。 | |
因为制作的是同样的程序,所以结果一样啊。很没意思吧。 为了学习MyGameCanvas.java和MyCanvas.java的差异,来比较一下代码吧。 | |
好的。在使用thread显示动画这一点上面两者是一样的啊。 但是使用GameCanvas的方法里,缺少了keyPressed()和paint()方法。 | |
是的。取而代之的是run方法中的whileloop内,取得按键状态和更新描绘两种方法。 Canvas类,决定了执行Java程序的JVM(Java virtual machine)按下按键时的通知和再描绘的timing,而GameCanvas不同的是,开发者可以根据自己的timing,更新描绘的命令。 Canvas类与基本的MyCanvas,以及GameCanvas类与基本的MyGameCanvas相比较,不同点如下图所示。 |
看了上面的图觉得MyGameCanvas比较简单啊。 | |
Canvas类,按键被按下时的方法keypressed与再描绘的方法paint通过JVM的判断执行,因此与游戏状态的紧密连接比较难。而GameCanvas类,开发者可以随时查看按键的状态,执行再描绘,因此是方便对游戏状况进行管理的方法。 | |
程序的编码都弄懂了。 | |
另外,为了在while loop中执行全部的处理,可以通过调整待机时间严密的定义每个step的时间。 例如下面的代码可以每隔100毫秒执行一次含有处理和再描绘的一个step。 |
在100毫秒内执行一个step的代码
while(running) { long startTime = System.currentTimeMillis(); /* •每个step的处理 •对应按键状态的处理 •画面的描绘处理 */ long endTime = System.currentTimeMillis(); int duration = (int)(endTime - startTime); if(duration < 100) { try { Thread.sleep(100 - duration); } catch (InterruptedException e) { stop(); } } } |
果然啊。While loop的开始与执行处理之后,分别在startTime、endTime变量里取得时刻,从而从差分得知处理需要的时间。 | |
是的,那个处理需要的时间从100毫秒中分出来进行待机的话,就可以刚好在100毫秒内执行1个step了。 | |
对于游戏的开发来说速度的调整很重要啊。使用GameCanvas,就可以制作出考虑取得按键状态和再描绘时间的程序了。 |
2. 使用Sprite类
接下来,来讲一讲有助于游戏制作的Sprite类的使用方法。 使用Sprite类,可以简单的管理角色动画使用的frame图像了。 | |
frame图像? | |
比如准备显示角色动作的复数图像,将其在一定时间内替换,角色就可以表现出栩栩如生的动作了。像这样动画使用的图像被称为frame图像。 | |
把角色右脚在前和左脚在前的图像交替显示,看起来就像是在走路了。这样,使用两个frame图像就可以显示动画了。 | |
说的没错啊。下面的图就是为本讲程序专门制作的火箭的动画图像。全部4张,按顺序显示,就可以表现边旋转边升空的火箭动画了。 |
也就是说,提前准备4张图像,按顺序替换就可以了。 | |
没错没错。但是,如果使用Sprite类,如下图,只需准备一张图像就可以了,将动画使用的图像按顺序排列好作成一张图片。然后Sprite类就自动的根据frame数对图像进行分割了。 |
下面,我们就使用Sprite类,制作基于上面的rocket.png图像的动画程序吧。将下面的GameCanvasTest2.java与MyGameCanvas.java编译后执行试试吧。 | |
好的。 |
GameCanvasTest2.java
1 | import javax.microedition.lcdui.*; | ||
2 | import javax.microedition.midlet.MIDlet; | ||
3 | |||
4 | public class GameCanvasTest2 extends MIDlet implements CommandListener { | ||
5 | private Display display; | ||
6 | private MyGameCanvas gameCanvas; | ||
7 | |||
8 | public GameCanvasTest2() { | ||
9 | display = Display.getDisplay(this); | ||
10 | } | ||
11 | |||
12 | public void startApp() { | ||
13 | try { | ||
14 | gameCanvas = new MyGameCanvas(); | ||
15 | gameCanvas.addCommand(new Command("Exit", Command.EXIT, 0)); | ||
16 | gameCanvas.setCommandListener(this); | ||
17 | display.setCurrent(gameCanvas); | ||
18 | gameCanvas.start(); | ||
19 | } catch (Exception e) { | ||
20 | } | ||
21 | } | ||
22 | |||
23 | public void pauseApp() {} | ||
24 | |||
25 | public void destroyApp(boolean unconditional) { | ||
26 | if (gameCanvas != null) { | ||
27 | gameCanvas.stop(); | ||
28 | } | ||
29 | } | ||
30 | |||
31 | public void commandAction(Command c, Displayable s) { | ||
32 | if (c.getCommandType() == Command.EXIT) { | ||
33 | destroyApp(true); | ||
34 | notifyDestroyed(); | ||
35 | } | ||
36 | } | ||
37 | } | ||
MyGameCanvas.java
1 | import javax.microedition.lcdui.*; | ||
2 | import javax.microedition.lcdui.game.*; | ||
3 | import java.io.IOException; | ||
4 | |||
5 | public class MyGameCanvas extends GameCanvas implements Runnable { | ||
6 | |||
7 | private Sprite rocket; // 火箭的Sprite | ||
8 | private int imageIndex; // 动画使用的图像的index | ||
9 | private int rocketY; // 火箭的y坐标 | ||
10 | private boolean running; //根据thread显示有无loop处理的flag | ||
11 | |||
12 | /** | ||
13 | * MyGameCanvas的constructor | ||
14 | */ | ||
15 | public MyGameCanvas() throws IOException { | ||
16 | super(true); | ||
17 | Image image = Image.createImage("/rocket.png"); | ||
18 | rocket = new Sprite(image, 32, 32); | ||
19 | rocketY = getHeight(); | ||
20 | } | ||
21 | |||
22 | /** | ||
23 | * thread的开始 | ||
24 | */ | ||
25 | public void start() { | ||
26 | running = true; | ||
27 | imageIndex = 0; | ||
28 | Thread t = new Thread(this); | ||
29 | t.start(); | ||
30 | } | ||
31 | |||
32 | /** | ||
33 | * 由thread执行的方法 | ||
34 | */ | ||
35 | public void run() { | ||
36 | Graphics g = getGraphics(); | ||
37 | |||
38 | while(running) { | ||
39 | tick(); | ||
40 | draw(g); | ||
41 | |||
42 | try { | ||
43 | Thread.sleep(200); | ||
44 | } | ||
45 | catch (InterruptedException e) { | ||
46 | stop(); | ||
47 | } | ||
48 | } | ||
49 | } | ||
50 | |||
51 | /** | ||
52 | * 每经过一定时间的处理 | ||
53 | */ | ||
54 | private void tick() { | ||
55 | imageIndex = (imageIndex + 1) % 4; | ||
56 | rocket.setFrame(imageIndex); | ||
57 | rocketY -= 10; | ||
58 | if(rocketY < 0) { | ||
59 | rocketY = getHeight(); | ||
60 | } | ||
61 | rocket.setPosition(getWidth()/2, rocketY); | ||
62 | } | ||
63 | |||
64 | /** | ||
65 | * 描绘的更新 | ||
66 | */ | ||
67 | private void draw(Graphics g) { | ||
68 | g.setColor(0xffffff); | ||
69 | g.fillRect(0, 0, getWidth(), getHeight()); | ||
70 | rocket.paint(g); | ||
71 | flushGraphics(); | ||
72 | } | ||
73 | |||
74 | /** | ||
75 | * thread的停止 | ||
76 | */ | ||
77 | public void stop() { | ||
78 | running = false; | ||
79 | } | ||
80 | } | ||
这次是使用GameCanvas和thread的例子了。 | |
恩,编译完成后赶快运行一下看看吧。 |
准备的一张图像,被分割后,就变成了动画显示了。 | |
没错。第18、19行,在Sprite类的constructor中,指定Image对象和1个frame的宽度和高度后,Image的图像就被自动分割了。 |
Sprite对象的生成(18、19行)
Image image = Image.createImage("/rocket.png"); rocket = new Sprite(image, 32, 32); |
怎么指定显示的frame啊? | |
利用Sprite类的setFrame(int index)方法可以指定显示哪一个frame。本讲有四种的frame,因此可以在index里面指定0到3的值。这个在loop处理调出的tick方法的第56行进行。 | |
在执行实际描绘的draw方法中没有指定描绘火箭的坐标吗? | |
恩。Sprite类利用setPosition方法指定描绘的坐标。在第61行进行。因此,实际描绘时,只需把Graphics对象传递到paint方法的参量里就可以了。 |
Sprite的坐标指定(第61行)
rocket.setPosition(getWidth()/2, rocketY); |
Sprite的描绘(第70行)
rocket.paint(g); |
原来是这样啊,我终于理解了frame号码的指定和描绘方法了。 | |
呵呵,动画的表示方法我们顺利的讲完了。下一讲,再继续讲一下Sprite与LayerManager类的方便功能吧。 |