主体程序
1、字母下落线程
游戏界面中每一个下落的字母对应一个字母下落线程DropCharThread的实例,这个线程负责将一个随机的字母在指定的画布栏中从上至下落下。在TypeTrainApplet内部定义这个线程类,之所以要将其作为成员内部类来定义,是因为这样可以减少类和类之间的通信,降低调用接口的复杂度。DropCharThread需要访问到TypeTrainApplet的众多成员,作为内部类就可以直接访问TypeTrainApplet类的成员变量了。其代码如下所示:
代码清单 4 DropCharThread字母下落线程
1. … 2. public class TypeTrainApplet extends JApplet { 3. … 4. private class DropCharThread extends Thread { 5. char c; //对应的字符 6. int colIndex; //在哪列下落 7. int x, y; //行列的坐标 8. private static final int ACTION_DRAW_FONT = 1; //画字符 9. private static final int ACTION_CLEAR_FONT = 2; //清字符 10. public DropCharThread(char c, int colIndex) { 11. this.c = c; 12. this.colIndex = colIndex; 13. this.x = (colIndex - 1) * colWidth + colWidth / 2; //所在的横坐标 14. } 15. //线程方法 16. public void run() { 17. draw(ACTION_DRAW_FONT); 18. try { 19. while (c != pressKeyChar && y < canvas.getHeight() && statusCode != 0) { 20. synchronized (canvas) { 21. while (statusCode == 2) { 22. canvas.wait(); 23. } 24. } 25. draw(ACTION_CLEAR_FONT); 26. y += stepLen; 27. draw(ACTION_DRAW_FONT); 28. Thread.sleep(stepInterval); 29. } 30. } catch (InterruptedException ex) { 31. } 32. 33. pressKeyChar = ' '; 34. draw(ACTION_CLEAR_FONT); 35. if (statusCode != 0) {//游戏没有停止 36. totalCount++; //统计总数 37. if (y < canvas.getHeight()) { 38. rightCount++; //击中 39. } else { 40. errorCount++; //打不中 41. } 42. drawResult(); 43. } 44. } 45. 46. /** 47. * 画字母 48. * @param actionType 1:画字母 2: 清除字母 49. */ 50. private void draw(int actionType) { 51. synchronized (canvas) { //必须对资源canvas进行同步,否则会产生线程不安全 52. Graphics g = canvas.getGraphics(); 53. if (actionType == ACTION_CLEAR_FONT) { 54. g.setXORMode(canvas.getBackground()); //清除 55. } 56. g.setFont(new Font("Times New Roman", Font.PLAIN, 12)); 57. g.drawString("" + c, x, y); 58. } 59. } 60. } 61. … 62. } | 由于这个类比较关键,逻辑也比较复杂,为了方便说明,我们将其流程通过一个流程图来描述,如下图所示:
 图 11 字母下落线程流程图 | 1) 首先在栏序号为colIndex的栏的第一个位置画出保存在变量c中的字母(第17行)。
2) 当这个字母未被击中,未到达画布底部且用户未结束游戏进行循环,这步判断对应程序的19行。如果这个判断条件通过进入第3步,即进入循环体,否则转到第5步。
3) 如果被暂停,这个线程进入等待态,直接被通知后才继续运行。需要指明一点的是,所有字母下落线程都用画布对象canvas进行同步,即用canvas进行通讯。线程间要进行通讯时,一定需要通讯线程都可以访问到的对象充当媒介将这些线程"串"起来,通过这个对象的notify()/notifyAll()/wait()在线程间通讯。这个对象好比一个"月下老人",在线程的情人间传递音讯。
4) 当线程被唤醒后,或原来就没有等待,则进入下一个循环的处理过程,在这个过程中,程序将原来位置的字母清除,下移纵坐标,并在新的位置画字母,以达到字母下落的动画效果,然后下落线程睡眠指定的毫秒数,毫秒数值为TypeTrainApplet成员变量stepInterval的值,而这个值可以在网页的<param name = "stepInterval" value = "50">标签中定义,达到控制下落速度的效果。
因为在画布上画字母后,这个字母并不会自动消失,如果直接移动纵坐标并在新位置画字母,原位置的字母依就存在。所以在新位置画字母之前,必须先将旧位置的字母清除。我们用了一个小技巧,即使用Graphics对象的setXORMode()方法,该方法两图像重叠部分的颜色。我们调用这个方法将图像重叠部分的颜色设置为画布的背景色,这样在原来的位置上再次画字母时,因为前后两次画个字母刚好重叠,就达到了清除原位置字母的效果。
画字母和清除字母的程序相似,我们把它抽出到一个方法中draw(int actionType),如第50~59行代码所示,通过入参决定是清除还是画新字母。为增强程序的可读性,我们在第8~9行中定义了两个用于表示清字母和画字母的动作常量。
5) 当程序出了循环体后,进行善后的处理:将用于保存用户按键字母的pressKeyChar变量置为空字符,在画布上清除移到底部的字母。如果游戏没有结束统计数据,并将数据写到界面的JLabel组件中。
2、添加击中音效
击中字母后播放一个短促的声音,将能大大提高游戏的听觉体验,这在节里,我们对字母下落线程稍作更改,以使其支持音效。
首先准备一个声音文件hit.wav,放在TypeTrainApplet.java相同的文件夹下。Applet类中定义了一个getAudioClip(URL url)方法,通过这个方法可以获取AudioClip的声音文件的对象。通过AudioClip的play()即可播放这个音效。
代码清单 5
1. … 2. import java.applet.AudioClip; 3. public class TypeTrainApplet extends JApplet { 4. … 5. AudioClip hitSound;//声明音效对象 6. … 7. public void init() { 8. … 9. hitSound = getAudioClip( (TypeTrainApplet.class).getResource( 10. "hit.wav"));//初始化音效对象 11. } 12. … 13. private class DropCharThread extends Thread { 14. … 15. public void run() { 16. … 17. draw(ACTION_CLEAR_FONT); 18. if (statusCode != 0) { //游戏没有停止 19. totalCount++; //统计总数 20. if (y < canvas.getHeight()) { 21. hitSound.play();//击中时播放音效 22. rightCount++; //击中 23. } else { 24. errorCount++; //打不中 25. } 26. drawResult(); 27. } 28. } 29. } 30. … 31. } | 在第5行定义一个音效的对象,在Applet初始化时获取音效对象,如第9行所示。更改字母下落线程,当击中下落的字母时播放音效,如第21行所示。
3、字母下落线程的产生器线程
指法练习需要"子子孙孙,无穷匮也"地不断产生字母下落线程,以使游戏持续进行,这个工作由产生器线程GenerateDropThread负责。GenerateDropThread线程出于和DropCharThread同样的原因,也作为TypeTrainApplet成员内部类来定义,其代码如下所示:
代码清单 6 GenerateDropThread 产生器线程
1. … 2. public class TypeTrainApplet extends JApplet { 3. … 4. private class GenerateDropThread extends Thread { 5. Random random = new Random(); //随机数 6. public void run() { 7. try { 8. while (statusCode != 0) { //产生下落线程 9. synchronized (canvas) { 10. while (statusCode == 2) { 11. canvas.wait(); 12. } 13. } 14. DropCharThread dropCharThread = new DropCharThread( 15. getRandomChar(), 16. random.nextInt(columnCount) + 1); 17. dropCharThread.start(); 18. Thread.sleep(generateInterval); 19. } 20. } catch (InterruptedException ex) { 21. } 22. } 23. 24. /** 25. * <b>功能说明:</b><br> 26. * 返回一个随机字符 27. * @return 随机字符 28. */ 29. private char getRandomChar() { 30. int temp = 97 + random.nextInt(26); 31. return (char) temp; 32. } 33. } … 34. } | 这个线程很简单:定期创建并启动一个DropCharThread字母下落线程。需要特别说明的是如何为字母下落线程提供一个随机字母和一个随机栏序号。我们通过一个随机对象java.util.Random的nextInt(int range)方法产生一个0~range-1的整数作为随机栏序号,在第29~32行定义了一个随机产生字母的getRandomChar()方法,因为小写字母a~z的ASCII代码是97~112,第30行即得到一个小写字母所对应的ASCII代码,通过第31行强制类型转换就可获取一个随机的小写字母字符。
在每次循环时,都判断游戏是否被暂停,如果暂停,则线程进入睡眠,暂停产生字母下落线程,如第8~13行所示。为了统一游戏总体的控制,所以这个线程也通过canvas对象进行同步,在其他地方调用canvas.notifyAll()方法后,暂停的线程就苏醒出来,继续工作。
在第18行,线程睡眠一小段时间,通过TypeTrainApplet的generateInterval成员变量就可以控制字母下落线程下落的速度,这个参数可以直接通过网页<param name = "generateInterval" value = "500">指定其值。
4、响应用户按键事件
所谓击中下落的字母,即是用户按下键盘中的一个键所对应的字母和某个字母下落线程的字母是一致的,对应的字母下落线程结束并将击中数递增1。
要让游戏自动监测到用户所按的按键,就需要Applet响应键盘按键事件,下面我们来为Applet生成按键事件的处理方法。
打开TypeTrainApplet.java,切换到Design视图页中,在结构窗格的组件树中选择this(BorderLayout)节点,切换属性查看器到Event标签页中,双击keyPressed项,如下图所示:
 图 12 为Applet生成响应按键的事件处理方法 | 此时,JBuilder为Applet生成了一个按键事件监听器,并切换到Source视图页并将光标定位到事件处理方法中,在方法中键入如下粗体的代码。
1. … 2. public class TypeTrainApplet extends JApplet { 3. … 4. /**获取用户点击按键所对应的字符*/ 5. public void this_keyPressed(KeyEvent e) { 6. if (!e.isActionKey()) { 7. pressKeyChar = e.getKeyChar(); 8. } 9. } 10. … 11. } | 第6行判断按键是否字符的按键,如果是在第7行中获取按键所对应的字符。
|
|