Sprite 与 LayerManager
上一讲对Sprite类的简单的使用方法进行了说明。本讲再进一步介绍Sprite便利的方法。 | |
上一讲学习了替换frame图像显示动画,这一讲学什么内容啊? | |
Sprite类里面,除了可以替换显示用frame图像之外,还有通过转动、翻转等进行显示的功能。 |
1.使用 Sprite 类使图像转动
那就让我们开始吧。在Sprite类里面存在「 void setTransform(int transform)」方法。利用它就可以容易的使图像转动或者翻转。 | |
参量transform中要指定什么样的值呢? | |
SetTransform的参量中可以指定的值是,Sprite类预先作为常量定义的下表中的 8 类。 根据指定值的不同其结果也各异。表中的“结果举例”的原图像如下所示。 |
值 | 说明 | 结果举例 |
TRANS_NONE | 没有进行转动和翻转 | |
TRANS_ROT90 | 90 度转动 | |
TRANS_ROT180 | 180 度转动 | |
TRANS_ROT270 | 270 度转动 | |
TRANS_MIRROR | 左右翻转 | |
TRANS_MIRROR_ROT90 | 左右翻转 后 90 度转动 | |
TRANS_MIRROR_ROT180 | 左右翻转后 180 度转动 | |
TRANS_MIRROR_ROT270 | 左右翻转后 270 度转动 |
博士您骨碌骨碌的转动起来肯定不好受吧。(笑)不过看了这个表,参量和结果的关系就很容易理解了。 | |
TRANS_NONE没有进行转动和翻转,就显示原始的图像。实际在进行转动和翻转时,实际指定了其他的值,具体参见下面的代码,这样就可以简单的进行 90 度的转动了。 |
Image image = Image.createImage("/spriteImage.png"); // 作成 Image 对象 Sprite mySprite = new Sprite(image, 32, 32); // 作成 Sprite 对象 mySprite.setTransform(Sprite.TRANS_ROT90); // 将 Sprite 对象的 Image 进行 90 度转动 |
然而,指定图像显示位置的是Sprite类的setPosition(int x, int y) 方法。注意这个方法指定的坐标在图像的左上角。 |
(在图像的左上角 (20, 10) 处描绘)
mySprite.setPosition(20,10);
那么,博士手中拿的指挥棒的尖端要移动到特定的位置的话需要计算一下吧。 | |
在 Sprite 类里面,有一个叫defineReferencePixel(int x, int y) 的方法, 使用它可以指定参照点的位置。另外使用setRefPixelPosition(int x, int y) 方法可以将参照点与目的坐标合并。这些在Sprite的转动进行之后也有效。 |
( 指定参照点为图像右端的 (24, 0) )
mySprite.defineReferencePixel(24, 0);
(描绘图像,使转动后参照点移动到 (60, 40) 的位置)
mySprite.setTransform(Sprite.TRANS_ROT90);
mySprite.setRefPixelPosition(60, 40);
恩,真方便啊。 | |
通常设定图像的中央为参照点。明白这点就很方便了。接下来,就使用刚才介绍的内容,试一试上一讲最后制作的应用程序吧。 | |
上一讲的程序是火箭升空的动画对吧。 | |
恩,这一讲我们来改变火箭移动的方向。 转动图像就可以改变火箭的方向了。代码如下所示。 |
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 | import javax.microedition.lcdui.game.*; | ||
3 | import java.io.IOException; | ||
4 | |||
5 | public class MyGameCanvas extends GameCanvas implements Runnable { | ||
6 | private boolean running; // 根据thread显示有无loop处理的flag | ||
7 | private Sprite rocket; // 火箭的Sprite | ||
8 | private int imageIndex; // 动画使用的图像的index | ||
9 | private int dx; // 火箭的x坐标位移 | ||
10 | private int dy; // 火箭的y坐标位移 | ||
11 | private final int MOVE_STEP_SIZE = 5; // 1step的火箭的移动量 | ||
12 | |||
13 | /** | ||
14 | * MyGameCanvas的constructor | ||
15 | */ | ||
16 | public MyGameCanvas() throws IOException { | ||
17 | super(true); | ||
18 | Image image = Image.createImage("/rocket.png"); | ||
19 | rocket = new Sprite(image, 32, 32); | ||
20 | rocket.defineReferencePixel(16, 16); // 指定图像的中央为参照点 | ||
21 | } | ||
22 | |||
23 | /** | ||
24 | * thread的开始 | ||
25 | */ | ||
26 | public void start() { | ||
27 | rocket.setRefPixelPosition(getWidth() / 2, getHeight() / 2); | ||
28 | dx = 0; | ||
29 | dy = -MOVE_STEP_SIZE; | ||
30 | running = true; | ||
31 | imageIndex = 0; | ||
32 | Thread t = new Thread(this); | ||
33 | t.start(); | ||
34 | } | ||
35 | |||
36 | /** | ||
37 | * 由thread执行的方法 | ||
38 | */ | ||
39 | public void run() { | ||
40 | Graphics g = getGraphics(); | ||
41 | |||
42 | while(running) { | ||
43 | tick(); | ||
44 | keyEvent(getKeyStates()); | ||
45 | draw(g); | ||
46 | |||
47 | try { | ||
48 | Thread.sleep(100); | ||
49 | } | ||
50 | catch (InterruptedException e) { | ||
51 | stop(); | ||
52 | } | ||
53 | } | ||
54 | } | ||
55 | |||
56 | /** | ||
57 | * 每经过一定时间的处理 | ||
58 | */ | ||
59 | private void tick() { | ||
60 | // 火箭的移动(到达画面一端后从相反端出来) | ||
61 | int x = (rocket.getRefPixelX() + dx + getWidth()) % getWidth(); | ||
62 | int y = (rocket.getRefPixelY() + dy + getHeight()) % getHeight(); | ||
63 | |||
64 | // 以参照点为基准指定显示位置 | ||
65 | rocket.setRefPixelPosition(x, y); | ||
66 | |||
67 | // frame的index的更新 | ||
68 | imageIndex = (imageIndex + 1) % 4; | ||
69 | |||
70 | // 指定frame的index | ||
71 | rocket.setFrame(imageIndex); | ||
72 | } | ||
73 | |||
74 | /** | ||
75 | * 根据按键状态切换处理 | ||
76 | */ | ||
77 | private void keyEvent(int keyStates) { | ||
78 | if ((keyStates & LEFT_PRESSED) != 0) { | ||
79 | // 左键被按下时 | ||
80 | dx = -MOVE_STEP_SIZE; | ||
81 | dy = 0; | ||
82 | rocket.setTransform(Sprite.TRANS_ROT270); | ||
83 | } else if ((keyStates & RIGHT_PRESSED) != 0) { | ||
84 | // 右键被按下时 | ||
85 | dx = MOVE_STEP_SIZE; | ||
86 | dy = 0; | ||
87 | rocket.setTransform(Sprite.TRANS_ROT90); | ||
88 | } else if ((keyStates & UP_PRESSED) != 0) { | ||
89 | // 上键被按下时 | ||
90 | dx = 0; | ||
91 | dy = -MOVE_STEP_SIZE; | ||
92 | rocket.setTransform(Sprite.TRANS_NONE); | ||
93 | } else if ((keyStates & DOWN_PRESSED) != 0) { | ||
94 | // 下键被按下时 | ||
95 | dx = 0; | ||
96 | dy = MOVE_STEP_SIZE; | ||
97 | rocket.setTransform(Sprite.TRANS_ROT180); | ||
98 | } | ||
99 | } | ||
100 | |||
101 | /** | ||
102 | * 描绘的更新 | ||
103 | */ | ||
104 | private void draw(Graphics g) { | ||
105 | g.setColor(0xffffff); | ||
106 | g.fillRect(0, 0, getWidth(), getHeight()); | ||
107 | rocket.paint(g); | ||
108 | flushGraphics(); | ||
109 | } | ||
110 | |||
111 | /** | ||
112 | * thread的停止 | ||
113 | */ | ||
114 | public void stop() { | ||
115 | running = false; | ||
116 | } | ||
117 | } | ||
下面,就对上面的代码进行编译,执行一下试试看吧。使用按键改变火箭的移动方向啦 。 | |
好的 。与之前做的程序很像,但这次可以用按键改变火箭的方向了。 改变方向之后,火箭仍然可以继续移动呢。 |
以上一讲的编码为基础,根据刚才介绍的内容,可以进行Sprite对象的转动以及以参照点为基准指定描绘位置等。 | |
是的 。使用 78 行到 98 行的setTransform方法,就可以通过按键转动图像了。 | |
恩,这样通过按键切换处理的功能是手机程序基本的功能。 另外,还要注意的是, 20 行指定图像的参照点为中心, 65 行指定位置。 |
指定参照点(20行 )
rocket.defineReferencePixel(16, 16); // 指定参照点为图像的中心 |
指定参照点到指定的坐标(65行 )
rocket.setRefPixelPosition(x, y); |
2. 管理复数的Sprite对象
接下来,复数的对象开始登场了! | |
画面上只有一个火箭,孤零零的显得很单调啊。 | |
火箭的话,可以相像是躲避陨石的游戏。 |
画面里显示的陨石增多的话,就需要单独生成陨石的Sprite对象。 本讲为了在屏幕上显示复数的Sprite对象,要使用LayerManager 。LayerManager 顾名思义,就是管理复数的Layer的类。之前的Stripe类也是Layer的一种。 | |
陨石和火箭的Sprite类显示是一样的啊。 | |
恩,使用 Sprite类就可以了。本讲为了简单的处理复数的角色动作,以 Sprite类为基础,作成了MyStripe类。 | |
对 Sprite 进行扩展形成了新的类?添加了什么新功能呢? | |
在 Sprite 类里面,只有现在的坐标值,而在MySprite类里面,还可以保存移动量和图像的转动状态。 |
MySprite.java
1 | import javax.microedition.lcdui.*; | ||
2 | import javax.microedition.lcdui.game.*; | ||
3 | |||
4 | /** | ||
5 | * 为显示动作对象,扩展Sprite类之后的类 | ||
6 | */ | ||
7 | public class MySprite extends Sprite { | ||
8 | private int dx; // x轴方向位移 | ||
9 | private int dy; // y轴方向位移 | ||
10 | private int screenWidth; // 屏幕的宽度 | ||
11 | private int screenHeight; // 屏幕的高度 | ||
12 | public int transformIndex; // 转动状态的index | ||
13 | |||
14 | // 表示转动状态的排列 | ||
15 | private static final int[] transformArray = { | ||
16 | Sprite.TRANS_NONE, | ||
17 | Sprite.TRANS_ROT90, | ||
18 | Sprite.TRANS_ROT180, | ||
19 | Sprite.TRANS_ROT270 | ||
20 | }; | ||
21 | |||
22 | /** | ||
23 | * MySprite的constructor | ||
24 | */ | ||
25 | public MySprite(Image image, int width, int height, | ||
26 | int screenWidth, int screenHeight) { | ||
27 | super(image, width, height); | ||
28 | defineReferencePixel(width / 2, height / 2); | ||
29 | dx = 0; | ||
30 | dy = 0; | ||
31 | transformIndex = 0; | ||
32 | this.screenWidth = screenWidth; | ||
33 | this.screenHeight = screenHeight; | ||
34 | } | ||
35 | |||
36 | /** | ||
37 | * 设定移动方向 | ||
38 | */ | ||
39 | public void setDirection(int dx, int dy) { | ||
40 | this.dx = dx; | ||
41 | this.dy = dy; | ||
42 | } | ||
43 | |||
44 | /** | ||
45 | * 移动 (到达画面一端后从相反端出来) | ||
46 | */ | ||
47 | public void move() { | ||
48 | int x = (getRefPixelX() + dx + screenWidth) % screenWidth; | ||
49 | int y = (getRefPixelY() + dy + screenHeight) % screenHeight; | ||
50 | setRefPixelPosition(x, y); | ||
51 | } | ||
52 | |||
53 | /** | ||
54 | * 转动状态的更新 | ||
55 | */ | ||
56 | public void rotate() { | ||
57 | transformIndex = (transformIndex + 1) % 4; | ||
58 | setTransform(transformArray[transformIndex]); | ||
59 | } | ||
60 | } | ||
MyGameCanvas.java
1 | import javax.microedition.lcdui.*; | ||
2 | import javax.microedition.lcdui.game.*; | ||
3 | import java.io.IOException; | ||
4 | import java.util.*; | ||
5 | |||
6 | public class MyGameCanvas extends GameCanvas implements Runnable { | ||
7 | |||
8 | private LayerManager layerManager; // Layer Manager | ||
9 | private boolean running; // 根据thread显示有无loop处理的flag | ||
10 | private MySprite rocket; // 火箭的Sprite | ||
11 | private MySprite[] stones; // 火箭的Sprite | ||
12 | private int imageIndex; // 火箭动画使用的图像的index | ||
13 | private final int MOVE_STEP_SIZE = 5; // 1step的火箭的移动量 | ||
14 | private final int STONE_NUM = 10; // 画面一次显示的陨石的个数 | ||
15 | |||
16 | /** | ||
17 | * MyGameCanvas的constructor | ||
18 | */ | ||
19 | public MyGameCanvas() throws IOException { | ||
20 | super(true); | ||
21 | layerManager = new LayerManager(); | ||
22 | |||
23 | // 作成火箭用的MySprite对象 | ||
24 | Image rocketImage = Image.createImage("/rocket.png"); | ||
25 | rocket = new MySprite(rocketImage, 32, 32, getWidth(), getHeight()); | ||
26 | layerManager.append(rocket); // 登陆LayerManager对象 | ||
27 | |||
28 | // 作成陨石用的MySprite对象 | ||
29 | stones = new MySprite[STONE_NUM]; | ||
30 | Image stoneImage = Image.createImage("/stone.png"); | ||
31 | for(int i = 0; i < STONE_NUM; i++) { | ||
32 | stones[i] = new MySprite(stoneImage, 24, 24, getWidth(), getHeight()); | ||
33 | layerManager.append(stones[i]); // 登陆LayerManager对象 | ||
34 | } | ||
35 | } | ||
36 | |||
37 | /** | ||
38 | * thread的开始 | ||
39 | */ | ||
40 | public void start() { | ||
41 | // 作成Random对象 | ||
42 | Random rand = new Random(System.currentTimeMillis()); | ||
43 | |||
44 | for(int i = 0; i < STONE_NUM; i++) { | ||
45 | // 任意设定陨石的位置 | ||
46 | int x = Math.abs(rand.nextInt()) % getWidth(); | ||
47 | int y = Math.abs(rand.nextInt()) % getHeight(); | ||
48 | stones[i].setRefPixelPosition(x, y); | ||
49 | |||
50 | // 任意设定陨石的移动方向和初始转动量 | ||
51 | int dx = rand.nextInt() % 5; | ||
52 | int dy = rand.nextInt() % 5; | ||
53 | stones[i].setDirection(dx, dy); | ||
54 | stones[i].transformIndex = Math.abs(rand.nextInt()) % 4; | ||
55 | } | ||
56 | |||
57 | // 设定火箭的初始位置与行进方向 | ||
58 | rocket.setRefPixelPosition(getWidth() / 2, getHeight() / 2); | ||
59 | rocket.setDirection(0, -MOVE_STEP_SIZE); | ||
60 | imageIndex = 0; | ||
61 | |||
62 | running = true; | ||
63 | Thread t = new Thread(this); | ||
64 | t.start(); | ||
65 | } | ||
66 | |||
67 | /** | ||
68 | * 由thread执行的方法 | ||
69 | */ | ||
70 | public void run() { | ||
71 | Graphics g = getGraphics(); | ||
72 | |||
73 | while(running) { | ||
74 | long startTime = System.currentTimeMillis(); | ||
75 | tick(); | ||
76 | keyEvent(getKeyStates()); | ||
77 | draw(g); | ||
78 | long endTime = System.currentTimeMillis(); | ||
79 | int duration = (int)(endTime - startTime); | ||
80 | if(duration < 150) { | ||
81 | try { | ||
82 | Thread.sleep(150 - duration); | ||
83 | } | ||
84 | catch (InterruptedException e) { | ||
85 | stop(); | ||
86 | } | ||
87 | } | ||
88 | } | ||
89 | } | ||
90 | |||
91 | /** | ||
92 | * 每经过一定时间的处理 | ||
93 | */ | ||
94 | private void tick() { | ||
95 | // 更新火箭的frame图像的index | ||
96 | imageIndex = (imageIndex + 1) % 4; | ||
97 | rocket.setFrame(imageIndex); | ||
98 | // 移动火箭 | ||
99 | rocket.move(); | ||
100 | |||
101 | // 更新全部陨石的位置与转动状态 | ||
102 | for(int i = 0; i < STONE_NUM; i++) { | ||
103 | stones[i].move(); | ||
104 | stones[i].rotate(); | ||
105 | } | ||
106 | } | ||
107 | |||
108 | /** | ||
109 | * 根据按键状态切换处理 | ||
110 | */ | ||
111 | private void keyEvent(int keyStates) { | ||
112 | if ((keyStates & LEFT_PRESSED) != 0) { | ||
113 | // 左按键被按下时 | ||
114 | rocket.setDirection(-MOVE_STEP_SIZE, 0); | ||
115 | rocket.setTransform(Sprite.TRANS_ROT270); | ||
116 | } else if ((keyStates & RIGHT_PRESSED) != 0) { | ||
117 | // 右按键被按下时 | ||
118 | rocket.setDirection(MOVE_STEP_SIZE, 0); | ||
119 | rocket.setTransform(Sprite.TRANS_ROT90); | ||
120 | } else if ((keyStates & UP_PRESSED) != 0) { | ||
121 | // 上按键被按下时 | ||
122 | rocket.setDirection(0, -MOVE_STEP_SIZE); | ||
123 | rocket.setTransform(Sprite.TRANS_NONE); | ||
124 | } else if ((keyStates & DOWN_PRESSED) != 0) { | ||
125 | // 下按键被按下时 | ||
126 | rocket.setDirection(0, MOVE_STEP_SIZE); | ||
127 | rocket.setTransform(Sprite.TRANS_ROT180); | ||
128 | } | ||
129 | } | ||
130 | |||
131 | /** | ||
132 | * 描绘的更新 | ||
133 | */ | ||
134 | private void draw(Graphics g) { | ||
135 | g.setColor(0xffffff); | ||
136 | g.fillRect(0, 0, getWidth(), getHeight()); | ||
137 | layerManager.paint(g, 0, 0); | ||
138 | flushGraphics(); | ||
139 | } | ||
140 | |||
141 | /** | ||
142 | * thread的停止 | ||
143 | */ | ||
144 | public void stop() { | ||
145 | running = false; | ||
146 | } | ||
147 | } | ||
148 | |||
本讲一共使用了三个文件啊,马上来编译试试吧。 |
这次总共使用了11个MySprite对象,屏幕上显示1个火箭与10个陨石在移动,看代码可以很容易的理解内容吧。 | |
是的。以前使用的Sprite类的处理,被替换成了MySprite类。利用MyGameCanvas 类的constructor生成火箭和陨石的MySprite对象。 | |
没错。MySprite对象分别保存位置和移动量,还有转动的状态等资料。 而MyGameCanvas只需要调出MySprite类的move或者rotate方法就可以了。 | |
但是,因为对于制作新的类还不是很习惯,所以还不能完全理解全部的代码。 | |
呵呵,习惯了就没问题了。但是自己定义新的类对于以后开发复杂的游戏也很重要哦。 | |
那么, LayerManager该怎么使用啊? | |
这次, LayerManager行使了下面两个处理。
| |
找不到使用Stripe对象的paint方法的位置,也可以进行描绘么? | |
恩,只要执行一次LayerManager的paint 方法,登陆LayerManager的Sprite对象就可以被全部描绘下来了。因此,没有必要执行个别的Sprite对象的 paint 方法。 | |
我明白了。 | |
本讲除了这个之外代码里面还有几个重点,总结到一个表里了。请参见下表。 |
行号 | 说明 |
MySprite.java 12 ~ 17 行 | 用排列表示 显示 Sprite 类的转动方法的值。这个值每经过一定时间就会顺次传递到setTransform方法,表现为陨石骨碌骨碌的转动。 |
MySprite.java 45 ~ 46 行 | 将目前的 x 坐标和 y 坐标的值,加上 dx 和 dy ,计算出移动目的地的坐标值。表现为,到达屏幕一端后可以从另一端出来。 |
MyGameCanvas.java 11 行 ,29 ~ 34 行 | 显示陨石的 MySprite 对象被保存在 stones 排列 。 |
MyGameCanvas.java 42 行 | 生成 Random 对象。 Random 对象根据产生随机数的 seed 值决定图形。可以根据指定当前时刻的不同,每次指定不同的图形。 |
MyGameCanvas.java 73 ~ 88 行 | 使用上讲介绍的,每隔一定时间更新描绘的技巧。测量描绘等处理需要的时间,将其值从sleep时间中抽出。 |
MyGameCanvas.java 94 行 | 每隔一定时间进行下面的处理。 |
恩。 参考上面的重点,又仔细的看了一遍代码。 | |
恩,这次只学习了怎么使复数的 Sprite 动起来。目前来说,准确的判断火箭是否与陨石相撞还是个比较难的问题,但是使用MIDP2.0新添的 Sprite 类中的功能,就能轻松的解决此问题了。 | |
哇,真的啊?那么再添加一些新的与陨石相撞的处理,又可以制作一个新的程序了。 | |
呵呵,下一讲我们就将介绍利用Sprite 对象判定冲突的办法。 |