Java 游戏编程导学

落落的月

贡献于2013-03-20

字数:0 关键词: Java开发 Java

Java 游戏编程导学 陆光义 宋现锋 编著 清 华 大 学 出 版 社 北 京 前 言 对于一个软件开发人员来说,当前发展最快的主流计算机语言就是由Sun公司开发的 Java语言。Java语言,因为它的简单易用、平台无关性和健壮性,在短短10年时间之内,很 快就发展成为当前最流行的编程语言。同时,为了适应企业应用和嵌入式系统开发,Sun 公司推出了相应的J2EE和J2ME开发工具包和响应规范。目前,它们已经成为各自相关领域 内的主流产品。在当今的网络时代,学习Java,掌握Java编程,对程序员来说不再是可有可 无,Java已经成为开发人员必备的工具了。 本书讲解Java语言的基本知识和应用,这同时也是J2EE和J2ME应用开发的基础。 本书的内容 本书首先介绍了Java基础知识,之后每章将介绍一个或多个精心制作的趣味游戏,它 们各自侧重于应用Java语言的某些特性,循序渐进,详细讲述了Java语言,并给出大量示例 和非常有价值的编程方法。 第1章介绍了Java特性和一些基本语法。在这一章,读者可以了解到Java的历史、现状 及其发展前景,同时也将学会如何编写和运行一个简单的“Hello World”Java程序。 第2章先讲解Java面向对象编程的一些基本概念,然后通过“幸运52”模拟游戏的编写, 让读者对Java的面向对象编程有更深的理解。 第3章讲解了持有对象和异常处理的知识,同时编写了“球迷必答”和“速算24”游戏。 在“球迷必答”游戏中,用户被询问5道问题,前一道问题必须答对,方可进入下一道问题。 在“速算24”游戏中,用户随意抽出4张扑克牌,用加、减、乘、除的方法将它们连接起来, 使得结果等于24。这一章通过这两个游戏来加深读者对数据结构、异常处理等知识的理解。 第4章通过“精彩闹钟”和“模拟钢琴”游戏,重点讲解Java在图形和多媒体方面的一 些简单应用,AWT的组件编程,也涉及到事件处理及简单的动画处理等内容。 第5章的“拼图”游戏和文曲星里的拼图游戏类似。这一章接着上一章,继续讲述Java 的图形和多媒体应用,以及Applet的知识和多线程技术等内容。 第6章通过编写“记事本”和“弹球”游戏,来介绍Java语言编程中的Swing包和I/O知 识。Swing包基本已经取代AWT包,成为目前最流行的GUI编程技术;I/O知识是程序本地 化必不可少的条件。 第7章编写了一个“俄罗斯方块”游戏,我们要编写的俄罗斯方块是一个应用程序(Java Application),不再是嵌到网页里的那种小应用程序(Applet)。游戏设计过程中涉及到游 戏框架、游戏界面的编写,菜单处理、变量的定义、算法设计以及预览功能的实现。通过 这一章,读者可以领略到Java的整体编程风格。同时,我们还为游戏添加了其他功能,如 设计About对话,实现游戏分数的存档,为游戏添加状态栏等。在此我们可以学到如何定义 类和类成员及其方法,如何使用JBuilder来创建一个对话框,并将对话框和主应用程序联系 起来。 在第8章中,我们将把这个游戏改编为网络俄罗斯方块游戏,并使用了最新的Java 2标 准Swing组件,使得界面更具有专业效果。本章主要讲解游戏的网络实现,使游戏能够实现 多用户网络对战,其中涉及到网络模块的编写,ServerSocket的使用,如何实现服务器/客户 间即时通信,NetRead接口的定义,聊天界面的设计,网络协议的设计,网络连接的实现及 如何将游戏打包并发布。通过这些内容让读者领略Java强大的网络功能,学习Java的网络编 程技巧。 本书在选题、策划及编写时,努力做到以下几点: · 趣味性 · 直观性 · 可操作性 · 循序渐进 我们将电脑游戏和程序设计这两个精彩的世界嫁接在一起,希望读者能在充满趣味的 学习过程中轻松地入门,尝试编程的乐趣,以便尽早掌握这一现代编程工具。 本书所附光盘的使用说明 随本书带有一张光盘。光盘中含有本书中涉及到的全部示例的源代码及各种资源文件, 以方便读者在学习过程中查阅、参考。 编 者 2004年10月 内 容 提 要 本书通过编写趣味游戏程序来引导读者学习 Java 编程的方法和技巧,形式新颖活泼,别具一格。 全书从 Java 语言基础知识和编制简单的程序入手,将 Java 编程的知识点有机地分散在“幸运 52”,“速 算 24”,“俄罗斯方块”等多个趣味游戏的程序设计示例中,使得 Java 语言中类、对象、属性、方法、接 口、继承等抽象概念变得具体形象,通俗易懂;并引导读者掌握 Java 中数组、字符串、事件处理、异常 处理、图形和多媒体应用、Swing 组件和网络等知识的运用和技巧。 本书以示例教学方式来组织内容,集趣味性、直观性、可操作性于一体,适合于 Java 初学者及对游 戏程序感兴趣的电脑爱好者阅读参考。 版权所有,翻印必究。举报电话:010-62782989 13901104297 13801310933 本书封面贴有清华大学出版社激光防伪标签,无标签者不得销售。 图书在版编目(CIP)数据 Java 游戏编程导学/陆光义,宋现锋编著. —北京:清华大学出版社,2004 ISBN 7-302-09776-3 Ⅰ. J… Ⅱ. ①陆… ②宋… Ⅲ. JAVA 语言—程序设计 Ⅳ. TP312 中国版本图书馆 CIP 数据核字(2004)第 108635 号 出 版 者:清华大学出版社 地 址:北京清华大学学研大厦 http://www.tup.com.cn 邮 编:100084 社 总 机:010-62770175 客户服务:010-62776969 组稿编辑:科海 文稿编辑:洪英 封面设计:林陶 版式设计:科海 印 刷 者:北京市耀华印刷有限公司 发 行 者:新华书店总店北京发行所 开 本:787×1092 1/16 印张:23.5 字数:572 千字 版 次:2004 年 11 月第 1 版 2004 年 11 月第 11 次印刷 书 号:ISBN 7-302-09776-3/TP·6750 印 数:1~5000 定 价:35.00 元(1CD) 本书如存在文字不清、漏印以及缺页、倒角、脱页等印装质量问题,请与清华大学出版社出版部联系 调换。联系电话:82896445 目 录 第1章 Java基础 ............................................................................................................. 1 1.1 Java简介 ................................................................................................................................................ 1 1.1.1 Java的历史.................................................................................................................................... 1 1.1.2 Java的特性.................................................................................................................................... 2 1.1.3 Java的应用.................................................................................................................................... 4 1.1.4 J2SE 1.5的新特性......................................................................................................................... 4 1.2 Java语言基本概念 ................................................................................................................................6 1.2.1 基本数据类型............................................................................................................................... 6 1.2.2 数组............................................................................................................................................. 11 1.2.3 运算符和表达式......................................................................................................................... 13 1.2.4 基本控制语句............................................................................................................................. 17 1.3 编写和运行Java程序 .......................................................................................................................... 25 1.3.1 Java开发工具简介...................................................................................................................... 25 1.3.2 Hello World................................................................................................................................. 26 1.3.3 编译和运行................................................................................................................................. 26 1.4 本章知识点回顾 ................................................................................................................................. 27 第2章 面向对象编程起步 ............................................................................................. 32 2.1 类和对象 ............................................................................................................................................. 32 2.1.1 类................................................................................................................................................. 32 2.1.2 对象............................................................................................................................................. 34 2.1.3 一个小问题——static................................................................................................................. 35 2.2 类的继承和多态 ................................................................................................................................. 36 2.2.1 Java的继承.................................................................................................................................. 36 2.2.2 abstract类和接口 ........................................................................................................................ 38 2.2.3 多态............................................................................................................................................. 40 2.3 包......................................................................................................................................................... 41 2.3.1 包的定义..................................................................................................................................... 41 2.3.2 包的使用..................................................................................................................................... 41 2.3.3 对包内类的访问权限................................................................................................................. 42 2.4 “幸运52”游戏 ................................................................................................................................. 43 2.4.1 游戏效果说明............................................................................................................................. 43 2.4.2 编写HTML页面.......................................................................................................................... 44 2.4.3 编写Java代码.............................................................................................................................. 45 2.4.4 程序在JBuilder中的编写 ........................................................................................................... 55 2.5 进一步实践 ......................................................................................................................................... 60 2 目 录 2.6 本章知识点回顾 ................................................................................................................................. 60 第3章 持有对象与异常处理.......................................................................................... 64 3.1 持有你的对象 ..................................................................................................................................... 64 3.1.1 Array(数组) ........................................................................................................................... 64 3.1.2 Collection(集合) .................................................................................................................... 64 3.1.3 Mapping(映射) ...................................................................................................................... 68 3.2 “球迷必答” ..................................................................................................................................... 70 3.2.1 游戏规则..................................................................................................................................... 70 3.2.2 游戏注意点................................................................................................................................. 70 3.2.3 建立工程..................................................................................................................................... 71 3.2.4 编写问题封装文件QuestionBean............................................................................................... 72 3.2.5 编写游戏界面............................................................................................................................. 73 3.2.6 处理事件..................................................................................................................................... 76 3.2.7 使用UI编辑器来编写消息对话框............................................................................................. 77 3.3 异常处理机制 ..................................................................................................................................... 81 3.3.1 基本异常..................................................................................................................................... 81 3.3.2 捕获异常..................................................................................................................................... 82 3.3.3 重新抛出异常............................................................................................................................. 85 3.3.4 标准Java异常.............................................................................................................................. 85 3.3.5 创建自己的异常......................................................................................................................... 86 3.4 “速算24”游戏 ................................................................................................................................. 87 3.4.1 游戏效果说明............................................................................................................................. 87 3.4.2 编写游戏规则............................................................................................................................. 88 3.4.3 创建工程和Applet...................................................................................................................... 88 3.4.4 设计游戏界面............................................................................................................................. 90 3.4.5 增加对表达式处理的方法......................................................................................................... 97 3.4.6 添加对Applet中按钮的事件处理 .............................................................................................. 99 3.4.7 进一步实践............................................................................................................................... 101 3.5 本章知识点回顾 ............................................................................................................................... 101 第4章 Java编程深入——图像与多媒体...................................................................... 105 4.1 AWT简介 .......................................................................................................................................... 105 4.2 布局管理器 ....................................................................................................................................... 106 4.2.1 FlowLayout ............................................................................................................................... 106 4.2.2 BorderLayout ............................................................................................................................ 107 4.2.3 GridLayout ................................................................................................................................ 107 4.2.4 CardLayout................................................................................................................................ 107 4.3 图像简介 ........................................................................................................................................... 108 4.3.1 文件格式................................................................................................................................... 108 4.3.2 图像的创建、加载和显示....................................................................................................... 108 4.3.3 ImageObserver .......................................................................................................................... 109 目 录 3 4.3.4 MediaTracker ............................................................................................................................ 111 4.3.5 Graphics类 ................................................................................................................................ 112 4.4 事件处理 ........................................................................................................................................... 114 4.4.1 事件处理机制........................................................................................................................... 114 4.4.2 事件类....................................................................................................................................... 115 4.4.3 事件监听器接口....................................................................................................................... 115 4.5 “精彩闹钟” ................................................................................................................................... 117 4.5.1 程序效果说明........................................................................................................................... 117 4.5.2 实现简单的界面....................................................................................................................... 117 4.5.3 画出表盘和表针....................................................................................................................... 123 4.5.4 让闹钟动起来........................................................................................................................... 125 4.5.5 给闹钟加上声音....................................................................................................................... 126 4.6 “模拟钢琴”游戏 ........................................................................................................................... 128 4.6.1 游戏效果说明........................................................................................................................... 128 4.6.2 实现简单的界面....................................................................................................................... 129 4.6.3 添加事件处理........................................................................................................................... 133 4.6.4 继续完善这个游戏................................................................................................................... 137 4.6.5 加上音响效果........................................................................................................................... 142 4.6.6 鼠标拖动时实现琴键的自动按下和释放 ............................................................................... 144 4.6.7 动画效果的改进....................................................................................................................... 148 4.6.8 Java的局限................................................................................................................................ 149 4.7 本章知识点回顾 ............................................................................................................................... 149 第5章 拼图游戏——Applet和线程.............................................................................. 156 5.1 Applet基础 ........................................................................................................................................ 156 5.1.1 Applet简介................................................................................................................................ 156 5.1.2 Applet体系结构........................................................................................................................ 156 5.1.3 Applet框架................................................................................................................................ 157 5.1.4 其他一些有用的方法............................................................................................................... 158 5.1.5 AppletContext接口的主要方法................................................................................................ 159 5.2 线程技术 ........................................................................................................................................... 159 5.2.1 继承线程................................................................................................................................... 160 5.2.2 Thread和Runnable .................................................................................................................... 161 5.2.3 线程的优先级........................................................................................................................... 162 5.2.4 线程同步................................................................................................................................... 164 5.2.5 多线程技术............................................................................................................................... 164 5.3 “拼图”游戏 ................................................................................................................................... 165 5.3.1 游戏的简单设计....................................................................................................................... 166 5.3.2 实现简单的界面....................................................................................................................... 166 5.3.3 事件处理................................................................................................................................... 172 5.3.4 让游戏能够判断游戏当前状态,并能重新开始.................................................................... 180 5.3.5 让游戏的每次初始化状态都不一样 ....................................................................................... 182 4 目 录 5.3.6 消除闪烁问题........................................................................................................................... 183 5.3.7 让游戏记录玩家所用的时间,并计算出分数 ....................................................................... 183 5.3.8 利用多线程技术来实现计时器,记录玩家玩的时间............................................................ 184 5.3.9 用F1键重新开始游戏............................................................................................................... 186 5.3.10 利用HTML的param标记来改变不同的图片 ........................................................................ 187 5.3.11 增加预览的功能..................................................................................................................... 193 5.3.12 加入音响效果......................................................................................................................... 196 5.3.13 CGI程序——进一步实践 ...................................................................................................... 197 5.4 本章知识点回顾 ............................................................................................................................... 200 第6章 Swing和I/O简介............................................................................................... 203 6.1 Swing组件简介 ................................................................................................................................. 203 6.1.1 JApplet ...................................................................................................................................... 203 6.1.2 按钮类....................................................................................................................................... 203 6.1.3 JComboBox............................................................................................................................... 210 6.1.4 滚动窗格................................................................................................................................... 210 6.1.5 树............................................................................................................................................... 210 6.1.6 表格(JTable) ........................................................................................................................ 211 6.2 I/O系统.............................................................................................................................................. 213 6.2.1 输入和输出............................................................................................................................... 213 6.2.2 FilterInputStream和FilterOutputStream.................................................................................... 215 6.2.3 File ............................................................................................................................................ 216 6.2.4 Java1.1的I/O流 ......................................................................................................................... 218 6.2.5 几个比较重要的类................................................................................................................... 218 6.3 “记事本”程序示例........................................................................................................................ 222 6.3.1 建立工程和框架文件............................................................................................................... 223 6.3.2 完成界面的设计....................................................................................................................... 224 6.3.3 添加“文件”主菜单事件响应 ............................................................................................... 229 6.3.4 添加“编辑”和“关于”主菜单的事件响应 ....................................................................... 232 6.3.5 添加按钮的事件....................................................................................................................... 233 6.4 “弹球”游戏 ................................................................................................................................... 234 6.4.1 游戏的简单设计....................................................................................................................... 234 6.4.2 实现简单的界面....................................................................................................................... 234 6.4.3 让小球运动起来....................................................................................................................... 238 6.4.4 事件处理——让游戏能够玩起来 ........................................................................................... 242 6.4.5 让游戏能够判断当前状态,并能重新开始 ........................................................................... 243 6.4.6 让游戏记录玩家的生命,并计算出分数 ............................................................................... 244 6.4.7 加入音响效果........................................................................................................................... 245 6.5 本章知识点回顾 ............................................................................................................................... 246 第7章 俄罗斯方块游戏——综合应用示例 .................................................................. 254 7.1 游戏效果说明 ................................................................................................................................... 254 目 录 5 7.2 游戏的简单设计 ............................................................................................................................... 255 7.3 编写游戏框架 ................................................................................................................................... 255 7.3.1 编写游戏框架........................................................................................................................... 255 7.3.2 为游戏编写菜单项................................................................................................................... 258 7.3.3 为菜单增加事件处理............................................................................................................... 261 7.4 开始编写游戏界面 ........................................................................................................................... 264 7.4.1 在Panel中加入几个必要的常量和变量 .................................................................................. 264 7.4.2 在Panel上画出游戏界面 .......................................................................................................... 265 7.4.3 将Panel加到框架里去.............................................................................................................. 267 7.5 开始编写游戏内容 ........................................................................................................................... 267 7.5.1 定义一个方块类....................................................................................................................... 267 7.5.2 定义描述游戏状态的变量....................................................................................................... 268 7.5.3 初始化游戏状态....................................................................................................................... 269 7.5.4 编写产生新的方块组的算法 ................................................................................................... 269 7.5.5 编写方块组移动的算法........................................................................................................... 271 7.5.6 编写将一行消去的算法........................................................................................................... 273 7.5.7 在游戏里实现一个线程........................................................................................................... 273 7.5.8 控制游戏开始........................................................................................................................... 274 7.5.9 让游戏暂停............................................................................................................................... 275 7.5.10 让游戏结束............................................................................................................................. 276 7.5.11 判断游戏是否结束................................................................................................................. 276 7.5.12 让玩家控制游戏——添加事件处理 ..................................................................................... 276 7.5.13 重新编写画图的功能代码..................................................................................................... 278 7.5.14 类的同步方法......................................................................................................................... 279 7.5.15 计算游戏的得分和当前游戏级别 ......................................................................................... 280 7.6 添加游戏的其他功能........................................................................................................................ 280 7.6.1 设计About对话框..................................................................................................................... 280 7.6.2 设计设定游戏等级的对话框 ................................................................................................... 285 7.6.3 为游戏添加状态栏................................................................................................................... 289 7.7 封装得分情况 ................................................................................................................................... 290 7.7.1 定义Score类和类成员.............................................................................................................. 290 7.7.2 定义方法writeToFile() ............................................................................................................. 291 7.7.3 定义方法readFromFile()........................................................................................................... 293 7.7.4 定义方法sortScore() ................................................................................................................. 294 7.7.5 定义方法isScoreTop(int nScore).............................................................................................. 294 7.7.6 定义方法insertNameScore(String sName,int nScore) .............................................................. 295 7.7.7 定义方法getScore()和getName() ............................................................................................. 295 7.8 编写对话框,让玩家输入名字........................................................................................................ 296 7.8.1 输入玩家名字的对话框........................................................................................................... 296 7.8.2 查看排名榜的对话框............................................................................................................... 298 7.9 本章知识点回顾 ............................................................................................................................... 301 6 目 录 第8章 网络俄罗斯方块游戏——Swing组件与网络功能.............................................. 304 8.1 游戏效果说明 ................................................................................................................................... 304 8.2 游戏的简单设计 ............................................................................................................................... 306 8.3 编写简单的网络模块........................................................................................................................ 307 8.3.1 网络的基本概念....................................................................................................................... 307 8.3.2 ServerSocket简介...................................................................................................................... 309 8.3.3 Socket简介................................................................................................................................ 310 8.3.4 InetAddress类方法简介............................................................................................................ 310 8.3.5 简单的服务器端实现............................................................................................................... 311 8.3.6 简单的客户端实现................................................................................................................... 318 8.3.7 用两个端口实现即时通信....................................................................................................... 319 8.3.8 定义接口NetRead..................................................................................................................... 325 8.3.9 如何使用这个模块——server包和client包............................................................................. 326 8.4 改编游戏框架 ................................................................................................................................... 326 8.4.1 让界面画出两个游戏区域....................................................................................................... 327 8.4.2 增加菜单及其事件处理功能 ................................................................................................... 329 8.4.3 连接对方的对话框设计........................................................................................................... 331 8.4.4 设计聊天界面........................................................................................................................... 333 8.4.5 编写显示双方分数的对话框 ................................................................................................... 335 8.4.6 编写警告对方不能运行某个命令的提示框 ........................................................................... 337 8.5 把网络模块加入到游戏之中............................................................................................................ 340 8.5.1 网络协议的设计....................................................................................................................... 340 8.5.2 实现网络连接........................................................................................................................... 341 8.5.3 实现聊天功能........................................................................................................................... 346 8.5.4 能够显示对方玩的状态........................................................................................................... 347 8.5.5 当一方消去一行时,对方随机增加一行 ............................................................................... 348 8.5.6 游戏开始、停止、暂停........................................................................................................... 350 8.5.7 一方结束时,使对方结束,并弹出游戏得分状况的对话框................................................ 351 8.5.8 在游戏关闭的时候,将所有网络资源释放 ........................................................................... 351 8.6 将游戏打包,发布我们的游戏........................................................................................................ 352 8.6.1 打包的好处............................................................................................................................... 352 8.6.2 如何打包................................................................................................................................... 352 8.7 Java代码风格和编写文档................................................................................................................. 356 8.7.1 Java的代码风格........................................................................................................................ 356 8.7.2 变量命名规则........................................................................................................................... 357 8.7.3 编写文档................................................................................................................................... 358 8.8 进一步实践 ....................................................................................................................................... 361 8.8.1 游戏还存在的问题................................................................................................................... 361 8.8.2 使游戏界面变得更漂亮........................................................................................................... 362 8.9 本章知识点回顾 ............................................................................................................................... 362 第 1 章 Java 基础 Java是一门发展很快的计算机语言,现在许多大型项目开始逐渐考虑使用Java来编写。 Java以其独特的可跨平台运行和开发速度快、维护方便等特性受到许多开发者的青睐。这 一章我们将要介绍一些Java的基础知识,从中读者可以了解到Java的历史、现状和前景,同 时也会了解到如何编写和运行一个简单的Java程序。 1.1 Java简介 Java是由Sun公司开发的新一代编程语言,使用它可在各种不同机器、不同操作平台的 网络环境中开发软件和使用软件。不论你使用的是哪一种网络浏览器,哪一种计算机,哪 一种操作系统,只要机器上已经安装了Java运行环境(Java Runtime Environment)或者系 统已经注明了“支持Java”,你就可以看到嵌有Java小应用程序(Applet)的生动活泼的网 页,你也可以运行别人在任何机器、任何操作系统上编写和编译的Java应用程序。Java正在 逐步成为Internet上主要的开发语言,也正在成为许多大型网络系统的首选开发语言。它彻 底改变了应用软件的开发模式,带来了自PC机面世以来的又一次技术革命,为迅速发展的 信息世界增添了新的活力。 1.1.1 Java的历史 Sun的Java语言开发小组成立于1991年,其目的是开拓消费类电子产品市场,例如,交 互式电视、烤面包机等。Sun内部人员把这个项目称为Green,那时World Wide Web还没有 发展呢!该小组的领导人James Gosling是一位非常杰出的程序员。他出生于1957年,于1984 年加盟Sun Microsystem公司,之前在一家IBM研究机构工作。他是Sun NeWs窗口系统的总 设计师,也是第一个用C实现的EMACS的文本编辑器COS MACS的开发者。 由于消费类电子产品追求的是可靠性、标准化、使用简单、费用低等特性,对采用什 么处理器不感兴趣。所以,迫切需要一种语言能够使所开发的系统与应用平台(例如:无 论是什么型号的交互式电视)无关,使得在一种平台上开发的控制系统能够很方便地移植 到各种类型的平台上。 为了实现系统与平台无关的目标,Gosling决定改写C编译器。但是,Gosling马上就发 现仅仅靠改写是无法满足需要的,于是在1991年6月决定自己开发一门新的语言。他为这门 语言起名为Oak,这就是Java语言的前身。因为Oak是Sun公司另一种语言的注册商标,所 以Java是后来才改的名。 Gosling在开始写Java时,不仅仅扩充了语言机制本身,更注重于语言所运行的软硬件 2 Java 游戏编程导学 环境。Gosling在设计中采用了虚机器码(Virtual Machine Code)方式,即Java语言编译后 产生的是虚机器码,虚机器码由Java虚拟机解释执行,Java虚拟机中有一个解释器。每一个 操作系统都可以创建一个Java虚拟机以解释执行Java的虚机器码。这样一来,Java就成了与 平台无关语言。 到了1994年,WWW已如火如荼地发展起来。Gosling意识到WWW需要一个中性的浏 览器,它不依赖于任何硬件平台和软件平台,它应是一种实时性较高、安全可靠、有交互 功能的浏览器。于是Gosling决定用Java开发一个新的Web浏览器,这就是Hot Java浏览器。 它虽然没有获得大的发展,但是却展示了Java可能带来的广阔市场前景。Hot Java在1995年 5月23日发布,在业界引起了巨大的轰动,Java的地位也随之得到肯定。又经过一年的试用 和改进,Java 1.0版终于在1996年初正式发布。 Hot Java浏览器的出现,也导致了几个主流浏览器开始支持Java小应用程序(Applet)。 现在,IE、NS都能够支持Java Applet,使得页面更加丰富多彩了。 1997年11月,国际标准化组织正式批准了Sun等公司提出的Java标准,Java标准化促进 了它的进一步发展,也标志着Java语言的成熟。 目前Java已经发展了多个版本,从1.02到1.1、1.2 到目前的1.5 版本。Java 1.2以后的版 本统称为Java 2。每次新版本发布,都会给Java注入新的生命力。 1.1.2 Java的特性 Java具有两大特性:可跨平台运行,面向对象。这两个特性是Java得以迅速发展的重要 原因。 跨平台运行机制 Java程序具有可跨平台运行的特性,这个特性取决于它的应用程序的运行机制。通常 计算机语言的运行机制如图1.1所示。 图 1.1 通常计算机语言的运行机制 而Java的运行机制则如图1.2所示。 源代码 编译器Pentium 可运行的Binary码 编译器PowerPC 编译器SPARC 可运行的Binary码 可运行的Binary码 第 1 章 Java基础 3 图 1.2 Java 运行机制 由图1.1、图1.2可以得知,普通的计算机编程语言大都是将源代码编译成机器码,对不 同的平台其机器码是不同的。由于不同平台的特性和应用程序接口(API)的不同,其源代 码往往也是不同的。这就造成了普通编程语言在移植上的困难。而Java引进的虚机器码的 概念解决了这个问题。因为所有的Java解释器都是按照同一个规则来解释执行Java虚机器码 的,所以所有的Java源代码最终生成的虚机器码对不同平台都是一样的。同时所有的虚机 器码都是运行在相同的Java虚拟机上,所以,Java对于不同的平台却可以有相同的源代码。 这就是Java可跨平台运行的特性。 对于运行于Internet上的网络程序,一个最大的要求是可以跨平台运行。这是因为网络 上会存在许多不同的机器、不同的操作系统。而Java的跨平台特性使之成为最理想的网络 编程语言。 面向对象 面向对象其实是现实世界模型的自然延伸。现实世界中任何实体都可以看作是对象。 对象之间通过信息相互作用。另外,现实世界中任何实体都可归属于某类事物,任何对象 都是某一类事物的实例。如果说传统的过程式编程语言是以过程为中心、以算法为驱动的 话,面向对象的编程语言则是以对象为中心、以信息为驱动。用公式表示,过程式编程语 言为:程序=算法+数据;而面向对象编程语言为:程序=对象+信息。 所有面向对象编程语言都支持3个概念:封装、多态性和继承,Java也不例外。Java提 供了类机制和接口。通过类和接口,我们就可以实现对方法和数据的封装,同时通过类的 继承以及继承时的多态性,我们实现了代码复用。Java程序是由一个一个的类组成,这决 定了Java是一种纯粹的面向对象的编程语言。Java的这个特性决定了使用Java语言开发产品 的高速度和产品的可维护性。 其他特性 Java还有一些特性,比如分布性、健壮性、安全性、支持多线程等等,这些特性都反 映了Java语言设计的精妙。同时,Java也是一门简单易学的语言,相信读者很快就会认识到 这一点。 源代码 不同平台下的编译器 虚机器码 不同平台下的解释器 4 Java 游戏编程导学 1.1.3 Java的应用 Applet Applet是嵌入到网页里用于装饰网页或者完成某种特殊功能的小应用程序。Applet由浏 览器中的Java解释器负责解释执行。 Application Java Application就是独立运行在Java虚拟机上的一系列的应用程序。 servlet servlet是运行在服务器端的小程序,它负责处理客户端传来的请求(request),然后传 给客户端一个响应(response)。 J2SE、J2EE 和 J2ME J2SE(Java 2 Standard Edition)就是我们印象中的普通的Java语言,这是一个标准的API 和工具包,是J2EE和J2ME的基础。 J2EE(Java 2 Enterprise Edition),顾名思义,它向企业级用户提供服务,使得企业可 以把自身的信息通过Internet的形式发布,也就是电子商务技术。同时可以用于解决电子政 务等一系列问题。 电子商务和信息技术的快速发展及对它的需求给应用程序开发人员带来了新的压力。 为了降低成本并加快企业应用程序的设计和开发,J2EE平台提供了一个基于组件的方法, 来设计、开发、装配及部署企业应用程序。J2EE平台提供了多层分布式应用模型、组件重 用、一致化的安全模型以及灵活的事务控制。 J2ME(Java 2 Micro Edition)类似于J2EE的一个平台。其实Java最早的目的就是满足 一些嵌入式装置的开发需要。在Java 1.0.2以前的几个版本,这项功能一度被忽略,但是在 1.0.2版本的时候进行了重新开发。当发展到Java 2的时候,Sun推出了一个专门的软件包 J2MEWDK,支持进行嵌入式开发。目前大家关注最多的是手机游戏的开发,但是J2ME能 做的远远不止这些。 如果读者对J2EE或者是J2ME有兴趣的话,请到http://java.sun.com/上寻找更多的信息。 1.1.4 J2SE 1.5的新特性 J2SE 1.5(开发代号Tiger)是Java平台和语言上的一个重要修改,目前主要包括了15 个JSR的请求和其他一些类似的更新。这次的Release主要关注于几个关键的主题:品质、 监视和管理、性能和可扩展性、轻松的开发以及桌面客户端。 轻松的开发(Ease of Development) 你可能已经听到过关于减轻开发难度而做的Java语言上的修改。这次J2SE根据JSR的要 求实现以下几个JSR:JSR 201 包含了4个修改,JSR 175的核心是支持元数据(metadata), 第 1 章 Java基础 5 而JSR 14则规范了泛型。元数据功能提供了声明式的开发,并且取代了一些工具的代码生 成和维护功能。泛型提升了无需手动转型(manual casting)的代码复用,通常manual casting 都会带来类型安全性上的一些问题。 另外的4个修改分别是: (1)用for循环来遍历容器(Collection 类型),而不需要显式的声明容器的迭代器 (Iterator)。 (2)枚举类型提供了超越类似final static int的、增强的类型安全性。 (3)在泛型中使用基本数据类型(primitive types)的时候提供自动装箱(autoboxing) 功能(原来的泛型中是不能使用基本类型的)。 (4)引入了静态常量(static constants)类,改进显式的共享一个静态数据。 为了实现轻松的开发,除了语言上的修改之外,还有一些额外的东西,比如一些怀念 printf函数的人会发现它又回来了;一个新的并发工具(在JSR 166中提及)将使得多线程编 程变得更加简单轻松。 可扩展性和性能(Scalability and Performance) J2SE 1.5版本承诺改进软件的可扩展性和性能,尤其是在启动时间和内存印记 (memory footprint)上,将使得用户能以最快的速度发布一个应用程序。从JSR 163上将实 现内建的性能工程(performance ergonomics)和一个功能强劲的API profile工具。 监视和管理(Monitoring and Manageability) 监视和管理是Tiger中的一个主要特性。那些在J2EE平台上使用JMX的开发者看到这样 的特性能在J2SE中实现将会非常开心。通过对JVM的监视,将允许对已发布的应用程序健 康性的完全检查,包括对底层内存泄漏检测,错误处理甚至是API 堆栈跟踪(stack trace) 的监视。 核心的 XML 支持(Core XML Support) J2SE 1.5的介绍被修改为XML的核心平台,表示Java的核心API将包括XML1.1、SAX 2.0 和DOM Level3。Web service方面的API、JAX-RPC和JAXB将继续出现在Web Service的包中, 在以后的新版本中这些API将被添加到核心API中去。 桌面客户端(Desktop Client) 最后一个重点的新特性是桌面客户端。这将带来几个内建的新的Look & Feels支持,并 且增强了对皮肤(skins)的支持。除了启动速度和内存印记的增强,桌面开发者又多了几 个值得期待的新特性。 对 Unicode 3.1 的支持(Unicode 3.1 Support) 32位的代理字符(surrogate character)支持将会很谨慎地添加到新的版本中,所以1.5 版本将仍然使用16位的char类型。 6 Java 游戏编程导学 新的 IO 支持(New IO Support) 新版本将提供对异步IO的支持,并且支持在平台中适当的地方开拓更深入的使用这些 API。 1.2 Java语言基本概念 前一节已经简要介绍了Java语言,现在就开始深入到Java语言内部,来介绍Java语言的 基本数据类型、控制语句等重要组成部分。学完这一节,我们就可以编写一个非常简单的 小程序。 1.2.1 基本数据类型 数据类型指明了变量和表达式的状态和行为。本节我们将讲述Java的简单数据类型。 常量与变量 (1)常量 Java有几种类型的常量,如整型常量345,浮点型常量33.2等,所有常量都用final做修 饰符。 下面是几个常量的定义: final int a=345; final float b=33.2; final double c=33.2d; final String d='ddddd'; (2)变量 变量是Java程序中的基本存储单元,它的定义包括变量名、变量类型和作用域。 变量名应该是一个合法的标识符,它允许包括字母、数字、下划线、$字符,不能以数 字开头,而且不能为保留字。 变量的声明格式为: type identifier [=value][,identifier[=value]…]; 多个变量之间用“,”分隔,结束用“;”。 下面是几个合法的变量声明: int a1,b2,c3; int a5=6,b9=8; float tr_$=33.; double sxf$; String _s44=’dddd’; 第 1 章 Java基础 7 下面是非法的变量声明: int 1a; //数字打头 float String=33.; //String是保留字 double aaa#; //#是特殊字符 String ddd-ddd=’dddd’; //-是特殊字符 变量的作用域可以分为:局部变量、类变量、方法参数、异常处理参数等等。局部变 量在方法或方法的一段代码中声明,它的作用域为它所在的代码块(整个方法或方法中的 某段代码)。类变量在类中声明,而不是在类的某个方法中声明,它的作用域为整个类。 方法参数传递给方法,它的作用域就是这个方法。异常处理参数传递给异常处理代码,它 的作用域就是异常处理部分。在一个确定的域中,变量名应该是惟一的。通常一个域可以 使用大括号{}来划定。 如下例在编译的时候将会由于重复对a定义而出错: int a=23; a=45; int a=67; 而如果加上域定界符{}就不会出错了: { int a=23; a=45; } int a=67; 整型数据 (1)整型常量 Java中的整型常量有3种表示方法: · 十进制整数,如24、-36、33。 · 八进制整数,以0开头,如0121表示十进制数81,-012表示十进制数-10。 · 十六进制整数,以0x或0X开头,如0x121表示十进制数290,-0X11表示十进制数-17。 整型常量在机器中占32位,具有int型的值。对于long型值,则要在数字后加L或l,如 123L表示一个长整数,它在机器中占64位。 (2)整型变量 Java的整型数据类型有4种:byte、short、int和long。这4种整数类型所占有的位数如表 1.1所示。 8 Java 游戏编程导学 表1.1 4种整数类型 类型 占有字节数 占有位数 byte 1 8 short 2 16 int 4 32 long 8 64 这4种类型都是有符号数,都可正可负。Java中没有无符号数,这减少了数据类型的复 杂度。 由于不同的机器对于多字节数据的存储方式不同,可能是从低字节向高字节存储,也 可能是从高字节向低字节存储,这样,在分析网络协议或文件格式时,为了解决不同机器 上的字节存储顺序问题,用byte类型来表示数据是合适的。而通常情况下,由于byte表示的 数据范围很小,容易造成溢出,应避免使用。 short类型则很少使用,它限制数据的存储为先高字节后低字节,这样在某些机器中会 出错。 下面是整型变量定义的例子: byte a=3; //指定变量a为byte型 short b=04; //指定变量b为short型 int c=0x5; //指定变量c为int型 long l=61; //指定变量l为long型 浮点型(实型)数据 (1)浮点型常量 · 十进制数形式,由数字和小数点组成,且必须有小数点,如0.123、.123、123.或123.0。 · 科学记数法形式,如1.23e3或123E3,其中e或E之前必须有数,且e或E后面的指数 必须为整数。 浮点型常量在机器中占64位,具有double型的值。对于float型的值,要在数字后加f或F, 如12.3F,它在机器中占32位,且表示精度较低。 (2)浮点型变量 实型变量的类型有float和double两种,表1.2列出了这两种类型所占有的位数。 表1.2 两种浮点类型 类型 所占字节数 所占位数 表示范围 float 4 32 4e-038~3.4e+038 double 8 64 1.7e-308~1.7e308 双精度类型double比单精度类型float具有更高的精度和更大的表示范围,常常使用。 下面是浮点型数据类型定义示例: 第 1 章 Java基础 9 float a=33.3f;//指定变量a为float型 double b=44.4;//指定变量b为double型 字符型数据 (1)字符型常量 字符型常量是用单引号括起来的一个字符,如'a'、'A'。 此外,Java提供的转义字符,以反斜杠(\)开头,将其后的字符转变为另外的含义, 下面列出了Java中的转义字符。Java中的字符型数据是16位无符号型数据,它表示Unicode 集,而不仅仅是ASCII集,例如\u0061表示ISO拉丁码的'a'。 转义字符描述如下: \ddd 1到3位8进制数据所表示的字符(ddd) \uxxxx 1到4位16进制数所表示的字符(xxxx) \' 单引号字符 \\ 反斜杠字符 \r 回车 \n 换行 \f 走纸换页 \t 横向跳格 \b 退格 (2)字符型变量 字符型变量的类型为char,它在机器中占16位,其范围为0~65535。 字符型变量的定义如: char c='a';//指定变量c为char型,且赋初值为'a' Java中的字符型数据不能用作整数,因为Java不提供无符号整数类型,但是同样可以把 它当作整数数据来操作。 例如: int a=3; char b='1'; char c=(char)(a+b);//c='4' 上例中,在计算加法时,字符型变量b被转化为整数,进行相加,最后把结果又转化为 字符型。 (3)字符串常量 Java的字符串常量是用双引号括起来的一串字符,如"Hello world!\n"。但不同的是,Java 中的字符串常量是作为String类的一个对象来处理,而不是一个数据。String类提供了很多 方便的对字符串操作的方法。 下例可以申请一个字符串常量: String str="Hello world!\n"; 10 Java 游戏编程导学 布尔型数据 布尔型数据有两个值:true和false,且它们不对应于任何整数值,在流控制中常用到布 尔型数据。 布尔型变量的定义如: boolean b=false;//定义b为布尔型变量,且初值为false 各类数值型数据间的混合运算 (1)运算过程中自动类型转换 整型、实型、字符型数据可以混合运算。运算中,不同类型的数据先转化为同一类型, 然后进行运算。转换从低级到高级,转换规则为: (byte或short) op int→int (byte或short或int) op long→long (byte或short或int或long) op float→float (byte或short或int或long或float) op double→double char op int→int 其中,箭头左边表示参与运算的数据类型,op为运算符(如加、减、乘、除等),右 边表示转换后进行运算的数据类型。 下面举两个例子。 例1: int a=10; float b=0.8; double c=3.0; int d=a+b+c; 则d=? 答:d=13。 例2: float a=0.8; int b=a; float c=b*a; 则c=? 答:c=0。因为第二句的执行结果为b=0。 (2)强制类型转换 高级数据要转换成低级数据,需用到强制类型转换,如: int a; byte b=(byte)a;//把int型变量a强制转换为byte型 这种使用可能会导致溢出或精度的下降,最好不要使用。 第 1 章 Java基础 11 示例如下: float a=0.8; float b=3.0; int c=2; int d=(int)(a*b)/c; 则d=? 答:d=1。在int d=(int)(a*b)/c这一句代码中,a*b得到的是float类型的结果,为2.4,然 后进行强制转换,得到2,然后除以2,就会得到1这个结果。 1.2.2 数组 数组是有序数据的集合,数组中的每个元素具有相同的数据类型,数组名和下标可惟 一地确定数组中的元素。数组分为一维数组和多维数组,下面分别进行介绍。 一维数组 (1)一维数组的定义 一维数组的定义方式为: type arrayName[]; 其中,类型(type)可以为Java中任意的数据类型,包括简单类型、组合类型,数组名 arrayName为一个合法的标识符,[]指明该变量是一个数组类型变量。 例如: int intArray[]; 声明了一个整型数组,数组中的每个元素均为整型数据。Java在数组的定义中并不为数组 元素分配内存,因此[]中不用指出数组长度,而且对于如上定义的一个数组是不能访问它 的任何元素的。我们必须为它分配内存空间,这时要用到运算符new,其格式如下: arrayName=new type[arraySize]; 其中,arraySize指明数组的长度。如: intArray=new int[3]; 为一个整型数组分配3个int型整数所占据的内存空间。 通常,这两部分可以合在一起,格式如下: type arrayName=new type[arraySize]; 示例: int intArray[]=new int[3]; 对数组元素可以逐个进行赋值,也可以在定义数组的同时进行初始化。 例如: 12 Java 游戏编程导学 int a[]={1,2,3,4,5}; 用逗号(,)分隔数组的各个元素,系统自动为数组分配空间。 (2)一维数组元素的引用 定义了一个数组,并用运算符new为它分配了内存空间后,就可以引用数组中的每一 个元素了。数组元素的引用方式为: arrayName[index] 其中,index为数组下标,它可以为整型常数或表达式。如a[3]、b[i](i为整型)、c[6*I] 等。下标从0开始,一直到数组的长度减1。对于上面例子中的intArray数组来说,它有3个 元素,分别为intArray[0]、intArray[1]、intArray[2]。注意:没有intArray[3]。当强行访问 intArray[3]的时候,就会抛出数组越界的异常。 数组都有一个属性length指明它的长度,例如:intArray.length指明数组intArray的长度。 多维数组 Java中多维数组被看作数组的数组。例如二维数组为一个特殊的一维数组,其每个元 素又是一个一维数组。下面主要以二维数组为例来进行说明。 (1)二维数组的定义 二维数组的定义方式为: type arrayName[][]; 例如: int intArray[][]; 与一维数组一样,这时对数组元素也没有分配内存空间,同样要使用运算符new来分 配内存,然后才可以访问每个元素。 对多维数组来说,分配内存空间有下面几种方法: · 直接为每一维分配空间,如: int a[][]=new int[2][3]; · 从最高维开始,分别为每一维分配空间,如: int a[][]=new int[2][]; a[0]=new int[3]; a[1]=new int[3]; · 在定义数组的时候进行初始化,如: int a[][]={{1,2},{3,4}}; (2)二维数组元素的引用 对二维数组中的每个元素,引用方式为:arrayName[index1][index2],其中index1、index2 第 1 章 Java基础 13 为下标,可为整型常数或表达式,如a[2][3]等。同样,每一维的下标都从0开始。 1.2.3 运算符和表达式 运算符指明对运算数所进行的运算。按运算数的数目可分为一元运算符(如++,--)、 二元运算符(如+、>)和三元运算符(如?:)。对于一元运算符来说,可以有前缀表达式 (如++i)和后缀表达式(i++)。中缀表达式为a+b。按照运算符功能来分,基本的运算符 有下面几类: (1)算术运算符(+,-,*,/,%,++,--) (2)关系运算符(>,<,>=,<=,==,!=) (3)布尔逻辑运算符(!,&&,||) (4)位运算符(>>,<<,>>>,&,|,^,~) (5)赋值运算符(=,及其扩展赋值运算符如+=) (6)条件运算符(?:) (7)其他(包括分量运算符·,下标运算符[],实例运算符instanceof,内存分配运算符 new,强制类型转换运算符(类型),方法调用运算符()等) 下面主要讲述前6类运算符。 算术运算符 算术运算符作用于整型或浮点型数据,完成算术运算。 (1)二元算术运算符(见表1.3) 表1.3 二元算术运算符 运算符 用法 描述 + op1+op2 加 - op1-op2 减 * op1*op2 乘 / op1/op2 除 % op1%op2 取模(求余) Java对加运算符进行了扩展,使它能够进行字符串的连接,如"abcde"+"fgh",得到字 符串"abcdefgh"。 对取余运算符%来说,其运算数可以为浮点数,37.2%10=7.2。 (2)一元算术运算符(见表1.4) 表1.4 一元算术运算符 运算符 用法 描述 + +op 正 ++ ++op或op++ 加1 14 Java 游戏编程导学 (续表) 运算符 用法 描述 - -op 负 -- --op或op-- 减1 i++与++i,i--与--i是有区别的,i++和i--表示先使用i而后进行加1或减1,而++i和--i 表示先进行加1或减1然后再取得i的值。 如下例: int a=3,b=3,c=3,d=3; int dest1=a++; int dest2=++b; int dest3=c--; int dest4=--c; 其最后结果为: dest1=3;a=4; dest2=4;b=4; dest3=3;c=2; dest4=2;c=2; 关系运算符 关系运算符用来比较两个值,返回布尔类型的值true或false。 关系运算符都是二元运算符,如表1.5所示。 表1.5 关系运算符 运算符 用法 返回true的时候 > op1>op2 op1大于op2 >= op1>=op2 op1大于或等于op2 < op12; 则a为true。 布尔逻辑运算符 布尔逻辑运算符进行布尔逻辑运算,包括&&、||、!3个运算符。前两个是二元运算符, 第 1 章 Java基础 15 后一个是一元运算符。 &&:逻辑与,当两者都为真的时候,结果才为真。 ||:逻辑或,当两者中有一个为真或者都为真的时候,结果为真。 !:逻辑否。 对于布尔逻辑运算,先求出运算符左边的表达式的值。对逻辑或运算,如果左边的表 达式为true,则直接返回结果为true。对逻辑与运算,如果左边的表达式为false,则直接返 回结果为fasle。 布尔逻辑运算的规则如表1.6所示。 表1.6 布尔逻辑运算规则 op1 op2 op1&&op2 op1||op2 !op1 true true true true false true false false true false false true false true true false false false false true 下面的例子说明了关系运算符和布尔逻辑运算符的混合使用。 boolean a=true; boolean b=a&&3<2; boolean c=a||b; boolean d=!b; 结果为: a=true,b=false,c=true,d=true 位运算符 位运算符用来对二进制位进行操作,Java中提供了如表1.7所示的位运算符。 表1.7 位运算符 运算符 用法 描述 ~ ~op 按位取反 & op1&op2 按位与 | op1|op2 按位或 ^ op1^op2 按位异或 >> op1>>op2 op1右移op2位 << op1<>> op1>>>op2 op1无符号右移op2位 位运算符中,除~以外,其余均为二元运算符,运算数只能为整型和字符型数据。 Java使用补码来表示二进制数,在补码表示中,最高位为符号位,正数的符号位为0, 负数为1。补码的规定如下: 16 Java 游戏编程导学 · 对正数来说,最高位为0,其余各位代表数值本身(以二进制表示),如+42的补码 为00101010。 · 对负数而言,把该数绝对值的补码按位取反,然后对整个数加1,即得该数的补码。 如-42的补码为11010110(00101010按位取反后加1,即11010101+1=11010110)。 · 用补码来表示数,0的补码是惟一的,都为00000000(而在原码、反码表示中,+0 和-0的表示是不惟一的,可参见相关书籍),而且可以用111111表示-1的补码(这 也是补码与原码和反码的区别)。 赋值运算符 赋值运算符=把一个数据赋给一个变量,在赋值运算符两侧的数据类型不一致的情况 下,如果左侧变量的数据类型的级别高,则右侧的数据会转化成与左侧相同的数据类型, 然后赋给左侧变量;否则,需要使用强制类型转换运算符,例如: byte a=100;//自动转换 int b=a; int c=100;//强制类型转换 byte d=(byte)c; 在赋值运算符=之前加上其他运算符,即构成扩展赋值运算符,例如,a+=10等价于 a=a+10。也就是对于如下赋值表达式: var=var op expression 用扩展赋值运算符可表达为: var op=expression 条件运算符 条件运算符?和:为三元运算符,它的一般形式为: 表达式?语句1:语句2 其中表达式语句的值应为一个布尔值,如果这个布尔值为ture,则执行语句1,否则执 行语句2,而且语句1和语句2需要返回相同的数据类型,且该类型不能为void。 例如: int a=(3>2)?3:2; 则执行结果为: a=3; 这个语句可以用if-else语句来表示。 表达式与运算符的优先级 对一个表达式进行运算的时候,要按照运算符的优先顺序从高向低进行,同级的运算 规则按照从左到右的方向进行。 第 1 章 Java基础 17 Java中运算符的优先级由高到低排列如下: [] () ++ -- !~ instanceof new(type) * / % + - >> >>> << <> <= >= == != & ^ | && || ?: = += -= *= /+ %= ^= &= |= <<= >>= >>>= 一般来说,我们不需要搞清楚这个复杂的优先级顺序,在编写程序的时候,只要尽量 多使用()来界定表达式计算的顺序就行。 1.2.4 基本控制语句 Java程序通过流控制来执行程序语句,完成一定的功能。语句可以是单一的一条语句 (如c=a+b;),也可以是复合语句。 下面分别介绍Java中的流控制语句,包括: · 分支语句 if-else,break,switch,return · 循环语句 while,do-while,for,continue · 异常处理语句 try-catch-finally,throw 最后我们简单介绍一下注释语句。 分支语句 分支语句提供了一种控制机制,使得程序的执行可以跳过这些语句(不执行这些语句), 而转去执行特定的语句。 (1)条件语句if-else if-else语句根据判定条件的真假来执行两种操作中的一种,它的格式为: if(布尔表达式) 语句1; 18 Java 游戏编程导学 else 语句2; 简要说明如下: · 布尔表达式是任意一个返回布尔型数据的表达式。 · 每个单一的语句后都必须有分号。 · 语句1、语句2可以为复合语句,这时要用大括号{}括起来,建议对单一的语句也用 大括号括起,这样程序的可读性强,而且有利于程序的扩充(可以在其中填加新的 语句)。{}外面不加分号。 · else子句是任选的。 · 若布尔表达式的值为true,则程序执行语句1,否则执行语句2。 if-else语句的一种特殊形式为: if(表达式1) { 语句1 } else if(表达式2) { 语句2 } … else if(表达式M) { 语句M } else { 语句N } else子句不能单独作为语句使用,它必须和if 配对使用。else总是与离它最近的if 配对。 可以通过使用大括号{}来改变配对关系。 举例:比较两个数的大小。 int a=10; int b=20; String str=""; if(a>b) str="a>b"; else str="ab" 举例:判断一个数是不是既能被4整除又能被3整除。 第 1 章 Java基础 19 int a=24; String str=""; if(a%4==0) { if(a%3==0) str="a能被4和3整除"; else str="a能被4但不能被3整除"; } else { if(a%3==0) str="a能被3但不能被4整除"; else str="a不能被4整除,也不能被3整除"; } 运行结果为: str为"a能被4和3整除" (2)多分支语句switch switch语句根据表达式的值来执行多个操作中的一个,它的一般格式如下: switch(表达式) { case 值1: 语句1; break; case 值2: 语句2; break; … case 值N: 语句N; break; default: defaultStatement; } 简要说明如下: · 表达式可以返回任一简单类型的值(如整型、实型、字符型),多分支语句把表达 式返回的值与每个case子句中的值相比。如果匹配成功,则执行该case子句后的语 句序列。 · break语句用来在执行完一个case分支后,使程序跳出switch语句,即终止switch语 句的执行。因为case子句只是起到一个标号的作用,用来查找匹配的入口,从此处 开始执行,对后面的case子句不再进行匹配,而是直接执行其后的语句序列,因此 在每个case分支后,要用break来终止后面的case分支语句的执行。在一些特殊情况 20 Java 游戏编程导学 下,多个不同的case值要执行一组相同的操作,这时可以不用break。case分支中包 括多个执行语句时,可以不用大括号{}括起。 · default子句是任选的。当表达式的值与任一case子句中的值都不匹配时,程序执行 default后面的语句。如果表达式的值与任一case子句中的值都不匹配且没有default 子句,则程序不执行任何操作,而是直接跳出switch语句。 · case子句中的值必须是常量,而且所有case子句中的值是不同的。 · switch语句的功能可以用if-else来实现,但在某些情况下,使用switch语句更简练, 可读性更强,而且程序的执行效率更高。 举例: int a=100; switch(a) { case 100: case 10: str="a等于10或者100"; break; default: str="a不等于10或者100"; } 运行结果为: str为"a等于10或者100"; 从该例中我们可以看到break语句的作用。 (3)return语句 return语句是从当前方法中退出,返回到调用该方法的语句处,并从紧跟着该语句的下 一条语句继续程序的执行。 return语句有两种格式,第一种为: 返回表达式 返回一个值给调用该方法的语句,返回值的数据类型必须和方法声明中的返回值类型 一致。可以使用强制类型转换来使类型一致。 第二种形式为: 返回值; 当一个方法的返回值为void的时候,使用这种形式,它不返回任何值。 return语句可以用在方法中的任何地方,但是一旦执行了return语句,就会从方法中跳 出去,后面的语句就不会再执行了。 示例如下: int returnBig(int a,int b) 第 1 章 Java基础 21 ( if(a>b) 返回值 a; else 返回值 b; } 这个方法返回a与b中较大的那个数值。 循环语句 循环语句的作用就是反复执行一段代码,直到满足终止循环的条件为止,一个循环一 般应包括4部分内容: · 初始化部分(initialization) 用来设置循环的一些初始条件,如设置计数器为0。 · 循环体部分(body) 这是反复循环的一段代码,可以是单行代码,也可以是多行 代码,但是必须用大括号括起来。 · 循环中用于改变计数器的部分(iteration) 这是用来控制计数器,从而控制循环 次数的语句。 · 终止部分(termination) 这是一个逻辑表达式,每一次循环都要对该表达式求值, 以验证是否满足循环终止条件。 Java中提供的循环语句有:for语句、while语句和do-while语句。下面分别加以介绍。 (1)for语句 for语句的一般形式为: for(初始化;终止条件;增量) { 循环体; } 下面对这个语句简要的说明一下: · for语句执行时,首先执行初始化操作,然后判断终止条件是否满足,如果满足, 则执行循环体中的语句,最后执行增量部分。完成一次循环后,重新判断终止条件。 · 可以在for语句的初始化部分声明一个变量,它的作用域为这个for语句。 · for语句通常用来执行循环次数确定的情况(如对数组元素执行操作),也可以根 据循环结束条件执行循环次数不确定的情况。 · 在初始化部分和增量部分可以使用逗号来进行多个操作。例如: for(i=0,j=10;i<j;i++,j--=) { … } · 初始化、终止以及增量部分都可以为空语句(但分号不能丢掉),三者均为空的时 候,相当于一个无限循环。 22 Java 游戏编程导学 举例: int sum=0; for(int i=0;i<10;i++) { sum+=i; } 执行结果为: i=55 (2)while语句 while语句的一般格式为: [初始化] while(终止条件) { 循环体; [增量;] } 简要说明如下: · 当布尔表达式(termination)的值为true时,循环执行大括号中的语句。 · while语句首先计算终止条件,当条件满足时,才去执行循环中的语句。 举例: int i=0; int sum=0; while(i<=10) { sum+=i; i++; } 执行结果为: sum=55,i=10 (3)do-while语句 do-while语句实现“直到型”循环,它的一般格式为: [初始化] do { 循环体; [增量;] }while(终止条件); 第 1 章 Java基础 23 简要说明如下: · do-while语句首先执行循环体,然后计算终止条件,若结果为true,则循环执行大括 号中的语句,直到布尔表达式的结果为false。 · 与while语句不同的是,do-while语句的循环体至少执行一次。 举例: int sum=0; int i=0; do { sum+=i; i++; }while(i<=10) 执行结果为: sum=55,i=11。 (4)continue语句 continue语句用来结束本次循环,跳过循环体中下面尚未执行的语句,接着进行终止条 件的判断,以决定是否继续循环。它的格式为: continue; continue语句也可以用作跳转到括号指明的外层循环中,这时的格式为: continue outerLable; 例如: outer: for(int i=0;i<10;i++= { 外层循环 for(int j=0;j<10;j++= { 内层循环 if(j>i) { … continue outer; } … } … } 该例中,当满足j>i的条件时,程序执行完相应的语句后跳转到外层循环,执行外层循 24 Java 游戏编程导学 环i++,然后开始下一次循环。 举例: int i=0,sum=0; for(int i=0;i<100;i++) { if(i>10) continue; sum+=i; } 执行结果为: sum=55 异常处理语句 异常处理语句包括try…catch…以及throw语句。这些语句都是Java特有的。 try…catch…语句的格式为: try { … } catch(SomeExceptionvar) { //to do something to handle the exception } throw语句的格式为: void someFunction throw someException { … } 注释语句 Java可以采用的注释与C++类似,主要有以下3种: · //用于单行注释。 · /*…*/用于多行注释。注释从/*开始,到*/结束。 · /**…*/是Java所特有的doc注释。注释从/**开始,到*/结束。在JDK中有javadoc的 工具,通过这个工具可以由这些注释生成HTML形式的帮助文件。详见最后一章的 说明。 第 1 章 Java基础 25 1.3 编写和运行Java程序 本节我们来熟悉一下Java程序的开发环境,通过编写、编译、运行一个小程序,来了 解Java程序的编写和运行过程。 1.3.1 Java开发工具简介 JDK Java最基本的开发工具就是JDK(Java Development Kit),这 是 Sun公司发布的最权威、 最基本的开发工具包。任何人都可以从Sun的网站上免费得到JDK。 目前JDK存在着多个版本,从1.02到1.1、1.2到目前的1.4版本。每次新版本发布,都会 给Java注入新的生命力。 JDK软件包是基于文本的一个开发包。这个开发包中提供了几个非常重要的工具: · javac 可以将Java源代码编译生成class文件(Java的执行字节码)。 · java 用来执行class文件。 此外还有javadoc、javah、javap等重要的工具。关于这些工具的使用可以参看JDK附带 的文档,其中有非常详细的说明。 常用的 Java 集成开发环境 几乎所有的Java集成开发环境都是基于JDK的。要想使这些开发环境正常工作,必须先 安装JDK(有些集成环境在安装的时候,会自动给用户安装JDK)。下面介绍现在最常见 的Java集成开发环境。 (1)JBuilder 这是Barland Inprise公司推出的一个工具包,是与Braland Inprise公司推出的Delphi、 C++Builder配套的开发工具。 JBuilder是笔者最喜欢使用的一个集成开发环境,它的可视化设计做得不错,对Java各 方面的支持非常友好,而且它使用的几乎都是标准类库里的类,所以用它开发的程序,不 存在移植上的困难。目前Jbuider 9.0版之后的最新版已经发布。 (2)Visual Age for Java 这是IBM开发的一个Java集成开发工具。笔者没有用过,只是在IBM工作的时候看到同 事们用过,据说不错。 (3)JCreator 这是一个小巧玲珑的工具,用起来非常方便,它与JDK联系得特别紧密,读者可以到 www.jcreator.com网站去获得这个软件的最新版本。 26 Java 游戏编程导学 (4)Visual J++ 这是Microsoft公司的MSDEV系列开发工具中的一个,与普遍使用的开发工具Visual C++有相同的前端编辑和编译环境。但是,由于它过多地使用了它自己开发的与平台有关 的类,所以,使用Visual J++开发的东西在移植到非Windows系列操作系统上的时候就会出 现问题。建议读者不要使用这个开发环境。 (5)Eclipse 这是由Eclipse组织(网址为http://www.eclipse.org)开发的一个开源的集成编译环境。 更确切地说,它是一个平台,支持任何符合插件规范的软件在这个平台上运行。Eclispe和 Visual Age for Java是一脉相承的,这样说不仅仅是由于Eclipse组织得到IBM的大力支持, 更是因为Eclipse的开发小组原本是IBM公司开发Visual Age for Java的人员。由于它的开放 性和一贯提倡的敏捷开发原则,这个工具深受程序员的青睐。这个工具也是笔者目前使用 最多的工具,现在的最新版本是2.1.3,还有一个里程碑版本3.0M9。读者有兴趣的话,可以 去Eclispe的官方网址下载使用。 1.3.2 Hello World 我们接下来编写一个简单的Hello World程序,这个程序在控制台上打印出“Hello World!”的字样。 要新建一个文件,在文件里键入如下代码: public class HelloWorld { public static void main(String[] args) { System.out.println("Hello world! "); } } 把这个文件存成HelloWorld.java。 在以上代码里,我们在第一行中新建了一个类。关于类的知识,将在下一章中详细讲 解。类中的main方法是程序在运行时执行的主方法。 System.out.println()是用来在控制台打印一个字符串的方法。 1.3.3 编译和运行 我们在此使用JDK来编译并运行上面编写的程序。 在文件所在的目录键入javac HelloWorld.java,如图1.3所示。 第 1 章 Java基础 27 图 1.3 运行 javac 进行编译 运行javac之后就会生成一个HelloWorld.class文件。这个文件就是Java的可执行文件, 我们可以使用java HelloWorld来执行它。执行结果如图1.4所示。 图 1.4 Hello World 程序的运行结果 1.4 本章知识点回顾 Java的应用 Applet:Applet是嵌入到网页中用于装饰网页或者完成某种特殊功能的小应 用程序。Applet由浏览器中的Java解释器负责解释执行 Application:Java Application就是独立运行在Java虚拟机上的一系列应用程序 servlet:servlet是运行在服务器端的小程序,它负责处理客户端传来的请求, 然后传给客户端一个响应 J2EE,J2ME 28 Java 游戏编程导学 (续表) 基本数据类型 整型、浮点型、字符型、布尔型 整型数据 byte,int,short,long 浮点型(实型)数据 float,double 字符常量 char 转义字符 \ddd 1 到3位八进制数据所表示的字符(ddd) \uxxxx 1 到4位十六进制数所表示的字符(xxxx) \' 单引号字符 \\ 反斜杠字符 \r 回车 \n 换行 \f 走纸换页 \t 横向跳格 \b 退格 布尔型数据 Boolean 只有两种取值:true或false 一维数组的定义方式为: type arrayName[]; 一维数组 一维数组元素的引用方式为: arrayName[index] 其中index为数组下标,它可以为整型常数或表达式。如a[3]、b[i](i为整型)、 c[6*I]等。下标从0开始,一直到数组的长度减1 多维数组 二维数组的定义方式为: ype arrayName[][]; 对二维数组中每个元素,引用方式为: arrayName[index1][index2] 其中,index1、index2为下标,可为整型常数或表达式,如a[2][3]等。同样, 每一维的下标都从0开始 算术运算符 二元算术运算符+,-,*,/,% 一元算术运算符+,-,++,-- 关系运算符 >,>=,<,<=,==,!= 布尔逻辑运算符 布尔逻辑运算符进行布尔逻辑运算,包括&&,||,!三个运算符。前两个是二元 运算符,后一个是一元运算符 位运算符 ~,&,|,^,>>,<<,>>> 赋值运算符 =,+=,-=,*=,/= 条件运算符 条件运算符?:为三元运算符,它的一般形式为: 表达式?语句1: 语句2 第 1 章 Java基础 29 (续表) []() ++ -- ! ~ instanceof Java中运算符的优先次 序(从上到下优先级逐渐 下降) new(type) * / % + - >> >>> << <> <= >= == != & ^ | && || ?: = += -= *= /+ %= ^= &= |= <<= >>= >>>= 条件语句:if-else if-else语句根据判定条件的真假来执行两种操作中的一种,它的格式为: if(布尔表达式) 语句1; else 语句2; 分支语句 多分支语句switch switch语句根据表达式的值来执行多个操作中的一个,它的一般格式如下: switch(表达式) { case 值1: 语句1; break; case 值2: 语句2; break; … case 值N: 语句N; break; [default: defaultStatement;] } 30 Java 游戏编程导学 (续表) return语句 return语句是从当前方法中退出,返回到调用该方法的语句处,并从紧跟着该 语句的下一条语句继续执行程序 返回语句有两种格式,第一种为: 返回表达式 返回一个值给调用该方法的语句,返回值的数据类型必须和方法声明中的返 回值类型一致。可以使用强制类型转换来使类型一致 第二种形式为: 返回值; 当一个方法的返回值为void的时候,使用这种形式,它不返回任何值 for语句的一般形式为: for(初始化;终止条件;增量) { 循环体; } while语句的一般格式为: [初始化] while(终止条件) { 循环体; [增量;] } do-while语句实现“直到型”循环,它的一般格式为: [初始化] do { 循环体; [增量;] }while(终止条件); 循环语句 continue语句用来结束本次循环,跳过循环体中下面尚未执行的语句,接着进 行终止条件的判断,以决定是否继续循环。它的格式为: continue; continue语句也可以用作跳转到括号指明的外层循环中,这时的格式为: continue outerLable; 第 1 章 Java基础 31 (续表) try…catch…语句的格式为: try { … } catch(Some Exception var) { …//to do something to handle the exception } 异常处理语句 throw语句的格式为: void someFunction throw someException { … } 注释语句 Java可以采用的注释与C++类似,主要有以下3种: //,用于单行注释 /*…*/,用于多行注释。注释从/*开始,到*/结束 /**…*/,这是Java所特有的doc注释。它从/**开始,到*/结束 第 2 章 面向对象编程起步 上一章简单介绍了Java语言,本章先来讲解Java面向对象编程的一些基本概念,然后通 过“幸运52”模拟游戏的编写,让读者对Java面向对象编程有更深的理解。 2.1 类 和 对 象 本节开始讲解Java的类以及对象,这些概念是面向对象编程的最基本的概念。要想学 会Java,必须要弄明白Java的类和对象。 2.1.1 类 类是组成Java程序的基本要素。通过使用类,可以把一个东西包装起来,提供给用户 一些有用的方法、变量和常量,而将其内部实现的细节隐藏。这样的封装有利于代码的复 用和改写,我们可以很方便地修改类中方法实现的细节,而不改变这个类的使用;同时可 以降低程序的模块之间的耦合程度,使得程序的修改升级变得更加容易。 类的声明 类是通过class关键字来声明的,格式如下: class myClass { //类名 class-body //类的主体 } 在第1行声明类的名字,在类体中声明类的成员变量和成员方法。在下面我们声明了一 个房子类,这个类抽象出了房子的一些性质,代码如下: class House{//房子类的类名 //成员变量 Window theWindow; boolean windowOpen; //构造函数 public void House(){ } public void House(Window window){ theWindow = window; windowOpen = false; } //成员函数 public void openWindow(){ 第 2 章 面向对象编程起步 33 windowOpen=true; } public void closeWindow(){ windowOpen=false; } public void printStatus(){ if(windowOpen){ System.out.println("窗口已经开了"); }else{ System.out.println("窗口已经关了"); } } } 在声明类的时候可以使用public和private来修饰。这些修饰词用来表示类在包外的访问 权限,相关内容将在2.3节中详细讲解。 成员变量 成员变量通常也称为属性。成员变量声明的格式为: type varName; 成员变量的类型可以是任何类型,包括简单类型、数组、类和接口。在一个类中,成 员变量的名字是惟一的,但是成员变量的名字可以和方法的名字相同。 在声明成员变量的时候,一般要使用public、private和protected等关键词,这些关键词 用来指明这个成员变量在类外的访问权限,如果不加以声明,Java默认声明为private。相 关 内容将在2.2.2节中详细讲解。 函数/方法 函数通常也称为方法。函数的声明格式为: return_type method_name(para_type paraname,[……, para_type paraname]){ //method body } 函数的名字可以为任意名字。它通常用来表述类所代表的事物可以完成的动作。以房 子为例,房子的窗户应该是可以开和关的,于是相应地具有动作openWindow()和 closeWindow()。 每个方法前也可以有public、private和protected关键字,用来指明这个方法在类外的访 问权限,相关内容将在2.2.2节中详细讲解。 构造函数 构造函数是一种特殊的函数,通常也称为构造子。构造函数的名字必须与它所在的类 完全相同,它表示类在初始化的时候应该完成的工作。构造函数的定义格式如下: class_name(para_type pare_name){ //method body 34 Java 游戏编程导学 } 在我们的例子中有两个构造函数public void House()和public void House(Window window),这两个函数完全同名而引数不同,这种函数名相同而引数不同的技术叫做重载, 我们将在后面进行介绍。 每一个类都应该有一个构造函数。一个类如果没有定义构造方法,则编译的时候自动 采用默认的构造方法,构造方法为不执行任何操作。如果这个类是继承得来的类,则采用 父类的构造方法。 2.1.2 对象 对象是由类生成的实例,如下方式可以生成一个类的实例: House hs=new House(); House是一个表示“屋子”的类,它抽象出了屋子的性质,而hs表示一个具体的屋子。 生成了一个对象之后,我们就可以通过它来调用对象的成员变量和方法。通过点运算符(.) 可以实现对变量的访问和方法的调用。 下面我们再写两个类,来简单运行一下这个小程序。House类中有一个类型为Window 类的成员变量,我们先给出Window类的代码,这里仅仅是举例说明,所以Window类什么 也不做。 public class Window(){ } 在下面的Manager类中我们来使用House类的对象。 public class Manager { public static void main(String[] args) { House hs=new House(); hs.closeWindow(); hs.printStatus(); hs.openWindow(); hs.printStatus(); } } 运行结果如图2.1所示。 Java运行时自动通过垃圾回收的方式周期性地释放无用对象所占用的内存,完成对象 的清除。当不存在对一个对象的引用时,该对象就会成为一个无用对象。这时,垃圾回收 机制就会把它当作一个垃圾,在回收的时候就会把它的内存清除。这样,我们就不需要去 跟踪每一个生成的对象,不必担心内存中有过多的垃圾,这是Java的一大优点。 第 2 章 面向对象编程起步 35 图 2.1 运行结果 2.1.3 一个小问题——static 细心的读者可能已经发现,在2.1.2节的例子中有这样一个声明: public static void main(String[] args) 这个main()函数的声明语句中,比我们过去见过的函数声明多加了一个关键字static, 这个关键字有什么用处呢?下面我们就来解释。 通常,我们创建类时会指出那个类的对象的外观与行为。除非用new 创建那个类的一 个对象,否则实际上并未得到任何东西。只有执行了new 后,才会正式生成数据存储空间, 并可使用相应的方法,除了两种特殊的情况。 一种情况是只想用一个存储区域来保存一个特定的数据——无论要创建多少个对象, 甚至根本不创建对象。另一种情况是需要一个特殊的方法,它没有与这个类的任何对象关 联。也就是说,即使没有创建对象,也需要一个能调用的方法。为满足这两方面的要求, Java中设立了static(静态)关键字。一旦将什么东西设为static,数据或方法就不会同那个 类的任何对象实例联系到一起。所以尽管从未创建那个类的一个对象,仍能调用一个static 方法,或访问一些static 数据。 在2.1.2节的例子中,main()函数声明为static,所以我们就可以用如下格式调用main() 函数而不用产生这个类的对象: Manager.Main() 注意:在这里我们只是用这个例子来说明静态成员的调用。一般情况下不可 能出现上面例子中的语句,因为Main()函数是由编译器自行调用的。 0 36 Java 游戏编程导学 2.2 类的继承和多态 2.2.1 Java的继承 Java语言中,继承是一个很重要的概念。通过继承可以实现代码的复用。一个类继承 另一个类,则这两个类就是父类与子类的关系。子类可以拥有父类的特征,同时还可以拥 有自己的特征。通过继承可以创建一个类的子类,其格式如下: public class sonClass extends fatherClass{ } 这样声明,表明sonClass是fatherClass的子类。如果一个类没有显式的声明extends,那 么它就是从基本类java.lang.Object中继承出来的。在Java中,所有的类都是从java.lang.Object 类继承出来的。 子类可以访问父类中通过public、protected关键词修饰的方法和成员变量。如果一个方 法和成员变量在父类中没有用任何关键词修饰,它就是friendly成员,也能够被这个类的子 类访问。而用private修饰的方法和成员变量是不能够被子类所访问的。 我们先声明一个类: public class Animal{ public final int MALE=1; public final int FEMALE=2; private int gender; protected Color color; public House()//构造方法{ } public void setGender(int g){ gender=g; } public int getGender(){ return gender; } public void printStatus(){ if(gender==MALE) System.out.println("Its gender is male"); else if(gender==FEMALE) System.out.println("Its gender is female"); } } 我们可以在子类中重载父类中已经定义了的方法,来给自己定义的子类增加一些新的 第 2 章 面向对象编程起步 37 特性。 我们在子类中定义一个和父类完全相同的方法,那么当这个子类的对象调用这个方法 的时候,它调用的是子类新定义的方法,而不是父类的方法。 如下例,我们定义子类: public class Dog extends Animal { public void printStatus() { if(gender==MALE) System.out.println("The dog's gender is male"); else if(gender==FEMALE) System.out.println("The dog's gender is female"); } } 那么当我们在其他类中这样调用时: Dog myDog=new Dog(); myDog.setGender(myDog.MALE); myDog.printStatus(); 运行结果为: The dog's gender is male 我们调用的setGender()方法仍然是在父类中定义的方法,而printStatus()方法却是我们 重新定义的方法。 super 关键字 如果我们重载了父类中的一个方法,但是仍然想调用父类中的方法,则可以使用super 关键字。 · super() 调用父类中的构造方法。 · super.methodName(paraType paraName) 调用父类中的methodName()方法。 如下例所示: public class Dog extends Animal{ protected boolean canBark; public Dog(){ super(); canBark=true; } public void printStatus(){ System.out.println("This is a dog"); super.printStatus(); } } 38 Java 游戏编程导学 那么在其他类中可用如下代码来调用: Dog myDog=new Dog(); myDog.setGender(myDog.MALE); myDog.printStatus(); 运行结果为: This is a dog Its gender is male 且这个时候myDog的canBark属性是true。 在构造Dog的时候,首先调用Animal的构造方法,然后给canBark赋值为true。 在重载printStatus()时,首先打印出“This is a dog”的消息,然后再调用父类中的 printStatus()方法,打印出这个动物的性别信息。 final 类和方法 由于安全性的原因,有时候需要限制某些类的继承和某些方法的重载。例如在Java基 本类库中的String类,它对编译器和解释器的正常运行有很重要的作用,不能够轻易改变, 因此要把它限定为final类,使它不能够被继承,这就保证了String类型的惟一性。同时,如 果你认为一个类定义得已经很完美,不需要再生成它的子类,这时也应把它限定为final类。 定义一个final类的格式如下: final class finalClass{ … } 同样,由于以上原因,有些方法应该限定不能被重载,我们就需要定义final()方法,final() 方法的定义格式如下: final return_type method_name(para_type para_name){ … } 2.2.2 abstract类和接口 abstract 类 与final类和方法相反,由于abstract类不能直接生成实例,所以它必须要被继承。abstract 的方法也是必须要被继承的。如果一个类声明了abstract的方法,那么它必须要被显式的声 明为abstract。 定义一个抽象类的格式如下: abstract class abstractClass{ … } 抽象方法的定义格式如下: 第 2 章 面向对象编程起步 39 abstract return_type method_name(para_type para_name){ … } 抽象类和抽象方法的作用在于给所有子类提供一个统一的接口。在Java中,还有专门 的接口的概念,下面我们就来介绍接口。 接口 Java通过接口使得处于不同层次,甚至互不关联的类可以具有相同的行为。接口就是 方法定义和常量值的集合。它的用处主要体现在下面几个方面: · 通过接口可以实现不相关类的相关行为,而不需要考虑这些类之间的层次关系。 · 通过接口可以指明多个类需要实现的方法。 · 通过接口可以了解对象的交互,而不需了解对象所对应的类。 接口的定义格式如下: interface myInterface{ interface body; } 在接口体中,声明的是多个方法的原型,定义格式如下: return_type method_name(para_type para_name,[……, para_type para_name]); 在方法体中也可以定义常量。定义常量的格式如下: type NAME=value; type可以为任意类型,常量的名字一般为大写。 接口也可以继承,如果在子接口中声明了和父接口相同的常量和方法,则父接口中的 常量被隐藏,方法被重载。 在类的声明中用implements子句来表示一个类使用某个接口,在这个类中就可以使用 接口中定义的常量,而且必须实现这个接口中定义的所有方法。如果不全部实现,则会出 现编译错误。一个类可以实现多个接口,声明的格式如下: class a implements interface1[,interface2,…,interfacen]{ //必须实现所有接口里定义的所有方法 } 接口的定义很简单,实现也很简单,但是能够在设计和编码的时候用好接口,并不是 一件容易的事情。接口是Java中最难理解的概念之一。下面通过一个例子来讲述使用接口 的好处。 Java不支持多重继承,而是用接口实现比多重继承更强的功能。多重继承指一个类可 以为多个类的子类,它使得类的层次关系不清楚。而且当多个父类同时拥有相同的成员变 量和方法时,子类的行为是不容易确定的。单一继承则清楚地表明了类的层次关系,指明 子类和父类各自的行为。接口则把方法的定义和类的层次区分开来,通过它可以在运行时 动态地定位所调用的方法。通过接口可以实现“多重继承”,且一个类可以实现多个接口。 40 Java 游戏编程导学 所有这些特性可以使得接口比多重继承更灵活,具有更强的功能。 2.2.3 多态 在Java编程时常用到多态,理解了多态,可以使我们的程序编写得更漂亮。多态是Java 中的一个重要概念。 什么叫多态 对于重载或继承的方法,Java运行时系统根据调用该方法的实例的类型来决定选择哪 个方法来调用,这就是多态。 如果一个父类有两个子类,在两个子类中都重载了一个相同的方法,如果在一个对象 中调用这个重载的方法,那么编译器就会自动判断这个对象是哪个子类的对象或者就是那 个父类的对象,然后再决定调用哪个方法:是调用重载的还是没有被重载的方法。 多态示例 下面通过一个示例来说明什么叫做多态。例子中的基类是一个abstract类,通过这个示 例,我们也可以明白abstract类的作用。 我们定义基类如下: abstract class DrawTool{ public abstract void paint(Graphics g){ } } 下面来定义两个子类,一个是用来画圆,一个是用来画方。 class CircleTool extends DrawTool{ public void paint(Graphics g){ g.drawCircle(1,1,10); } } class RectTool extends DrawTool{ public void paint(Graphics g){ g.drawRect(1,1,10,10); } } 可以这样来使用这些类: DrawTool tool1=new RectTool(); DrawTool tool2=new CircleTool(); tool1.paint(g); tool2.paint(g); 可以看到tool1和tool2都是DrawTool对象,但是它们又都是DrawTool子类的对象,所以 在调用paint()方法的时候,调用的是DrawTool子类中定义的方法。如:tool1.paint(g)将在g 第 2 章 面向对象编程起步 41 上面画一个矩形,而tool2.paint(g)将在g上面画一个圆。 这里DrawTool类是一个abstract类,它的作用就是定义一个统一的paint()方法,可以使 得所有的Tool都使用这个方法来画图形。这样可以便于我们使用所有的DrawTool,同时在 使用DrawTool的时候,不必去判断它是一个什么样的Tool(是画圆的工具还是画方的工具) 就可以拿来使用了。 2.3 包 Java中的包用来组织类之间的联系。通过包,可以控制类之间的可见性。同时,通过 包,我们解决了两个类重名的问题。包提供了一种命名机制和可见性限制机制。 2.3.1 包的定义 package是包的定义语句,放在Java源代码的第一条语句,指明该文件中定义的类所在 的包。如果没有用package指明包,则表明这个文件没有包名。 package语句的格式如下: package pakage1.[pakage2.….]; 如果一个类位于包pkg1里的包pkg2里,那么应该用下面的语句: package pkg1.pkg2; 包名一般是小写的。 Java中的包对应于文件管理系统中的目录。如果一个文件位于包java.awt.color里,则它 应该放在目录java/awt/color下面。 2.3.2 包的使用 如果使用一个包中的类,我们必须把这个类导入进来。导入语句为import,它的格式 如下: import pakage1.pakage2.class_name; 或者: import pakage1.pakage2.*; 前者是只导入包pakage1.pakage2中的class_name类,而后者则把包pakage1.pakage2中的类全 部导入。 注意:如果pakage2下面还有包,import的第二种格式并不导入pakage2下面的 包中的类。 在源程序中,如果需要使用某个类,一般情况下,我们只需要直接用类名就可以了。 0 42 Java 游戏编程导学 而仅当在我们导入的类列表中,有两个相同名字的类,例如在包pkg1中有一个名字为Date 的类,在包pkg2也同样存在一个名字为Date的类,那么如果我们在源码开头使用如下import 语句: import pkg1.*; import pkg2.*; 则在源码中使用pkg1包中的Date类的时候,必须使用pkg1.Date来明确指定我们使用的是 pkg1包中的类。 2.3.3 对包内类的访问权限 我们可以用public、protected、friendly(默认)、private来限制类之间的互访。 Java中类之间的访问可以分为以下5种: · 同一个类中 指在类的方法中访问自身的变量和方法。 · 同一个包中子类 指在包中一个类的方法里访问所在包内不同类的方法和变量,且 被访问类是访问类的父类。 · 同一个包中非子类 指在包中一个类的方法里访问所在包内不同类的方法和变量, 且被访问类不是访问类的父类。 · 不同包中的子类 指在包中一个类的方法里访问非所在包内类的方法和变量,且被 访问类是访问类的父类。 · 不同包中的非子类 指在包中一个类的方法里访问非所在包内类的方法和变量,且 被访问类不是访问类的父类。 明白了这5种访问过程,我们通过表2.1来区别public、protected、friendly(默认)、private 这4个关键字对类互访的限定。 表2.1 访问权限 同一个类中 同一个包中子类 同一个包中非子类 不同包中子类 不同包中非子类 private Y protected Y Y Y friendly Y Y Y public Y Y Y Y Y Y表示可以访问。例如:如果一个类成员的访问权限为protected,则处在同一个包中的 子类和不同包中的子类都可以访问到它,而同一包中的非子类和不同包中的非子类都不能 够访问到它。 第 2 章 面向对象编程起步 43 2.4 “幸运52”游戏 2.4.1 游戏效果说明 这个游戏是一个简单的Applet。 “幸运52”游戏是让用户对物品的价格进行估测。在对价格进行估测的时候,系统会 给出用户估测的价格是高还是低的信息。用户根据这些信息重新调整所估测的价格。 游戏的初始界面如图2.2所示。 图 2.2 游戏开始前的界面 当点击“开始游戏”按钮开始游戏后,网页就会自动调出商品的图像和显示商品的名 称,图2.3为刚刚开始的一盘游戏,其中的商品是N3310的手机。 图 2.3 开始游戏 在文本框中输入对该商品的价格的估测,然后点击“确定”按钮,这时就会弹出一个 对话框,用来告诉你所猜的价格是高了还是低了,如图2.4所示。 44 Java 游戏编程导学 图 2.4 玩家猜不中时的界面 如果你猜中了,则对话框会显示“你猜对了,恭喜你!”的消息,如图2.5所示。 图 2.5 玩家猜中后的界面 2.4.2 编写HTML页面 我们还需要编写一个用来嵌入这个Applet的HTML页面。相信大家都对HTML有一定了 解,下面先给出HTML代码,然后对用于嵌入Applet的标记进行详细讲解: HTML Test Page
第 2 章 面向对象编程起步 45 游戏规则:
(1)点击"开始游戏"的按钮,游戏开始,系统将会把物品调入
(2)对物品的价格进行估测,直到猜对为止
我们通过如下的代码,将Applet嵌入到网页中去: 这里的嵌入Applet操作是通过一个html标签完成的。在上面的这行代码中,我 们规定了标签的3个属性:code、width和height。 · code 存放Applet的class文件相对于这个HTML页面的路径和文件名。如果你的 Applet的class文件放在了\applet目录中,名字是My.class,则应该写成applet code= "applet\My.class"。 · width Applet的宽度。 · height Applet的高度。 浏览这个HTML文件,可以看到如图2.6所示的结果。 图 2.6 HTML 页面 可以看到,这个页面的Applet部分还是灰色的一片,这是因为我们还没有编译生成 Applet类文件的缘故。要是你用的浏览器是Netscape的话,那么Applet处将是白色的。 2.4.3 编写Java代码 这个游戏是一个Applet,我们将循序渐进地讲解这个游戏,以增强读者对Java面向对象 编程的理解。首先我们编写的游戏就应该继承包java.applet里的Applet类。通过继承,就不 用考虑Applet如何能够嵌在网页中,如何和浏览器接口了。 46 Java 游戏编程导学 这个游戏有一个主界面,界面上有几个按钮,有一个文本框,用来输入用户所估测的 价格。还有一个是用来显示物品的面板。这里我们将编写几个类,分别用来组成整个游戏。 我们在此先不处理任何事件,仅仅编写界面。 创建一个 Applet 创建一个Applet的类,需要从java.applet.Applet类继承。代码如下: import java.applet.*; public class Lucky52 extends Applet{ } 我们把这个Applet类称为Lucky52,表示“幸运52”这个游戏。 编写一个用来显示图片的 Panel 我们需要用到java.awt包中的一些类。Awt(Abstract window toolkit)是 Java的抽象窗口 类,这个包中有许多组件,比如按钮、面板等等。 Java中不提供直接用来显示图片的Panel,所以我们要编写一个用来显示图片的类。这 个类从Panel类中继承,它具有Panel的一切特征,还能够用来显示图片。 代码如下: class PicPanel extends Panel{ String fileName="N3310.gif"; Image m_img; public void initImg() { URL url=null; try{ url=Class.forName("Lucky").getResource("N3310.gif"); }catch(Exception e){ } m_img=getToolkit().getImage(url); MediaTracker mt=new MediaTracker(this); mt.addImage(m_img,1); try{ mt.wait(); mt.checkAll(); }catch(Exception e){} } public void paint(Graphics g){ g.drawImage(m_img,125,0,50,160,this); } } 这个新定义的PicPanel类就能够用来显示图片。它有两个成员变量和两个方法。两个成 员变量为: · fileName 为要显示的图片名字。 · m_img 为要显示的图片对象。 第 2 章 面向对象编程起步 47 两个方法为: · initImg 用来装载图片,把图片从文件中装载到m_img对象中去。 · paint 这是重载Panel的一个方法,当在Panel上进行重画的时候,会自动调用这个 方法。我们重载这个方法,使得在调用这个方法的时候,能够把m_img对象画到这 个Panel上面,这样这个Panel就能够显示图片了。 在initImg方法中,我们使用了MediaTracker类,这个类的用法在后面将会详细讲解。这 是一个用来跟踪图片装载的类。在Java中,图片的装载是异步的,这儿我们使用MediaTracker 强制它进行同步安装。这样在图片文件不存在的时候,便可以捕获到这个异常。 编写主界面 主界面就是刚才编写的那个Applet类,这一节将在类中添加成员变量和方法。我们需 要在Applet中加上按钮和文本框。按钮和文本框等在Awt中都已经定义好了,只要拿来使用 就可以了。首先设置这个Applet采用BorderLayout的布局。这里只需要知道BorderLayout可 以给界面划分几个固定的区域,可以控制将我们的控件放到哪个位置。下面的代码为没有 添加事件处理时的Applet代码。 public class Lucky extends Applet{ public Lucky(){ super(); this.setLayout(new BorderLayout()); Panel pNorth=new Panel(); Label label=new Label("请输入你对商品价格的估计:"); TextField txField=new TextField(10); pNorth.add(label); pNorth.add(txField); add(pNorth,BorderLayout.NORTH); PicPanel pCenter=new PicPanel(); pCenter.initImg(); add(pCenter,BorderLayout.CENTER); Panel pBottom=new Panel(); Button btStart=new Button("开始游戏"); Button btOk=new Button("确定"); Button btCancel=new Button("取消"); pBottom.add(btStart); pBottom.add(btOk); pBottom.add(btCancel); add(pBottom,BorderLayout.SOUTH); setBackground(Color.white); } } 这时候的用户界面如图2.7所示。 48 Java 游戏编程导学 图 2.7 未添加事件处理的用户界面 编写告诉用户所猜价格与物品价格之间关系的对话框 当用户猜过一个价格,然后点击“确定”按钮的时候,游戏应该告诉玩家所猜的价格 是否正确;如果不正确,告诉玩家是高了还是低了。我们准备使用一个对话框来告诉玩家, 那么就需要编制我们自己的对话框类。在java.awt包中有Frame类,通过对这个类的继承来 实现我们自己的对话框。 这个对话框要求能够显示所猜价格与实际价格的关系。然后,我们还准备在这个对话 框中添加一个按钮,点击这个按钮就可以隐藏该对话框。 代码如下: class MsgDlg extends Frame{ Label label=new Label(); public MsgDlg(String strMsg){ super(); setTitle("猜的结果"); Panel p=new Panel(); add(p); p.add(label); label.setText(strMsg); setSize(150,100); setLocation(300,200); Button btOk=new Button("确定"); p.add(btOk); show(); } } · setSize(int,int) 设置Frame的大小。 · setLocation(int,int) 设置Frame显示时所在的位置。 · show()方法 是用来显示这个对话框的,它是Frame中的方法。 这个对话框的样式如图2.8所示。 第 2 章 面向对象编程起步 49 图 2.8 显示猜测结果的对话框 增加判断所猜价格与标准价格关系的方法 这个方法很容易写,只要做一些简单的判断即可,判断之后返回所猜价格和标准价格 之间关系的字符串。 方法定义在Lucky类中,代码如下: public String comparePrice(int guessPrice){ if(guessPrice==nPrice){ return "你猜对了,恭喜你!"; }else if(guessPrice>nPrice){ return "你猜的价格过高,请重新猜!"; }else if(guessPriceTitle:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable 52 Java 游戏编程导学 * @version 1.0 */ public class lucky extends Applet implements ActionListener { Panel pNorth = new Panel(); PicPanel pCenter = new PicPanel(); Panel pBottom = new Panel(); TextField txField = new TextField(10); Label label = new Label("请输入你对商品价格的估计:"); private int nPrice = 1100; public lucky() { super(); this.setLayout(new BorderLayout()); pNorth.add(label); pNorth.add(txField); add(pNorth, BorderLayout.NORTH); add(pCenter, BorderLayout.CENTER); Button btStart = new Button("开始游戏"); Button btOk = new Button("确定"); Button btCancel = new Button("取消"); btStart.setActionCommand("start"); btStart.addActionListener(this); btOk.setActionCommand("ok"); btOk.addActionListener(this); btCancel.setActionCommand("cancel"); btCancel.addActionListener(this); pBottom.add(btStart); pBottom.add(btOk); pBottom.add(btCancel); add(pBottom, BorderLayout.SOUTH); setBackground(Color.white); } public void actionPerformed(ActionEvent evt) { if (evt.getActionCommand().equals("start")) { pCenter.initImg(); label.setText("请输入你对商品价格的估计:"); pCenter.repaint(); } else if (evt.getActionCommand().equals("ok")) { int guessPrice = 0; try { guessPrice = Integer.parseInt(txField.getText().trim()); String guess = comparePrice(guessPrice); new MsgDlg(guess); } catch (Exception e) { e.printStackTrace(); 第 2 章 面向对象编程起步 53 } } else if (evt.getActionCommand().equals("cancel")) { txField.setText(""); } } public String comparePrice(int guessPrice) { if (guessPrice == nPrice) { return "你猜对了,恭喜你!"; } else if (guessPrice > nPrice) { return "你猜的价格过高,请重新猜!"; } else if (guessPrice < nPrice) { return "你猜的价格过低,请再加价!"; } return "出错了"; } } //PicPanel.java package lucky; import java.awt.*; import java.net.*; import java.awt.image.ImageObserver; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 */ public class PicPanel extends Panel { public PicPanel() { initImg(); } String fileName = "N3310.GIF"; Image m_img; //ImageObserver ob = new ImageObserver(); public void initImg() { URL url = null; try { url = Class.forName("lucky.lucky").getResource("N3310.GIF"); 54 Java 游戏编程导学 } catch (Exception e) {} m_img = getToolkit().getImage(url); MediaTracker mt = new MediaTracker(this); mt.addImage(m_img, 1); try { mt.wait(); mt.checkAll(); } catch (Exception e) {} } public void paint(Graphics g) { g.drawImage(m_img, 125, 0, 50, 120, this); } } //MsgDlg.java package lucky; import java.awt.*; import java.awt.event.*; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 */ class MsgDlg extends Frame implements ActionListener { Label label = new Label(); public MsgDlg(String strMsg) { super(); setTitle("猜的结果"); Panel p = new Panel(); add(p); p.add(label); label.setText(strMsg); setSize(150, 100); setLocation(300, 200); Button btOk = new Button("确定"); btOk.addActionListener(this); p.add(btOk); show(); } 第 2 章 面向对象编程起步 55 public void actionPerformed(ActionEvent evt) { this.dispose(); } } 2.4.4 程序在JBuilder中的编写 我们将带领读者使用现在比较流行的集成编译环境(IDE)编写这个Applet,本书中所 有例子都在Windows 2000 Server Family、JBuilder 9.0 Enterprise环境下编译通过。其实对于 这样小的例程,用JBuilder这样的编辑环境简直是大材小用,我们的目的只是要向读者介绍 这个编辑环境,如果读者将来想在Java方面有所发展,必须要对JBuilder这样的IDE非常熟 悉才行。 下面我们先来认识一下JBuilder的初始界面,如图2.9所示。接着我们就使用这个IDE来 完成我们的工作。 图 2.9 JBuilder 的初始界面 建立一个工程 在JBuilder的File菜单中选择New Project命令,会弹出工程向导(Project Wizard)窗口。 在其中设置工程名称为lucky,设置工程路径为D:\myPro\lucky,模版项为默认设置,如图 2.10所示,然后点击Finish按钮生成工程。 56 Java 游戏编程导学 图 2.10 工程向导窗口 在工程中编写 Applet 在JBuilder的File菜单中选择New命令,弹出对象列(Object Gallery)窗口,如图2.11 所示。选择选项卡Web中的Applet选项,然后点击OK按钮。这时会弹出Applet向导(Applet Wizard)窗口,设置Applet的名称为lucky,如图2.12所示。然后点击Finish按钮,就会生成 一个名为lucky.java的Applet类。 图 2.11 对象列窗口 第 2 章 面向对象编程起步 57 图 2.12 Applet 向导窗口 我们得到的Applet类中有JBuilder自动生成的一些框架代码,这些代码对于本例来说没 有什么意义,直接删除它们,键入上面例程中的代码即可。 键入代码的时候,读者可能会感觉出错了。有的代码行下面出现了红色的波浪线,同 时在代码前的编辑器的左面边缘上出现了红色的×标记。这是因为Jbuilder可以自动识别我 们编写代码的语法错误,如果发现错误,就以这种形式显示出来。在这个例子中我们的错 误是由于缺少了PicPanel类,下面我们就来修正这个错误。 建立 PicPanel 和 MsgDlg 类 在File菜单中选择New命令,出现对象列窗口,选择General选项卡中的Class选项,点 击OK按钮,弹出类向导(Class Wizard)窗口。设置类名为PicPanel,设置基类为java.awt.Panel, 也可以通过点击右侧的下三角按钮来选择,其余选择默认设置,如图2.13所示。完成这些 工作以后,点击OK按钮,就为我们生成了PicPanel类。 图 2.13 类向导窗口 58 Java 游戏编程导学 将例程中的代码填入,结果又发现了错误,这是由于缺少MsgDlg类的原因,生成MsgDlg 类的方法与前面类似。填入代码,注意在这里MsgDlg的基类应该为java.awt.Frame。例子的 编写基本完成了,完成后的窗口如图2.14所示。 图 2.14 代码完成后的窗口 注意:在这里生成Applet类lucky.java文件的时候,JBuilder就为我们生成了一 个同名文件lucky.html,这个文件中嵌入了Applet——lucky.java。但是这个文 件中其他的代码不是我们想要的,所以应将这个文件中的代码用我们的例子 中的HTML代码替换。 加入图片 好像忘记了些什么?是的,没有加入图片,我们需要把图片加入到合适的位置。在这 个例子里需要将图片N3310.GIF拷贝到D:\myPro\Lucky\classes\lucky中。 将图片加入工程还有另外一种简单的方法,在JBuilder的左上角的浏览窗口中右击工程 文件Lucky.jpx,从弹出菜单中选择Add Files/Packages/Classes命令添加文件到工程中即可。 程序的编译运行 接下来读者要完成的工作,就是在JBuilder中对程序进行调试编译。这个过程很简单, 在JBuilder的菜单中选择Run→Run Project命令就可以了,结果如图2.15所示。 0 第 2 章 面向对象编程起步 59 图 2.15 结果窗口 我们也可以在JBuilder中直接观察结果,打开lucky.html就可以了,如图2.16所示。这又 是JBuilder方便的一个地方,给开发人员带来了很大的便利。 图 2.16 在 JBuilder 中显示结果 整个例子的编写演示工作就完成了。在这里我们只对JBuilder的强大功能做了简单介 绍,读者如果有兴趣,请自行查阅JBuilder文献。 60 Java 游戏编程导学 2.5 进一步实践 这个游戏还有两个需要完善的地方: · 现在这个游戏只有一个商品N3310的价格的估测。我们可以在目录中放上多个商品 的图片,然后在玩家开始游戏的时候随机挑选商品来猜测。 · 我们可以给游戏加入可猜次数的限制,每猜一次,就提示玩家已经猜了多少次了。 当猜到指定的次数之后,就告诉玩家已经超过了指定的次数,然后随机选择一个其 他的商品,让玩家重新估价。 这些功能实现起来都很简单,相信读者能够自行完成。 2.6 本章知识点回顾 类的声明格式 类的声明格式为: class myClass //类名 { class-body//类的主体 } 成员变量 成员变量声明的格式为: type varName; 方法 方法的声明格式为: return_type method_name(para_type paraname,[……, para_type paraname]) { //method body } 对象 对象是由类生成的实例,如下方式可以生成一个类的实例: House hs=new House(); 继承 通过继承,可以创建一个类的子类,其格式如下: public class sonClass extends fatherClass { } 这样声明,表明sonClass是fatherClass的子类 第 2 章 面向对象编程起步 61 (续表) 父类和子类的关系 子类可以访问父类中使用public、protected关键词限定的方法和成员变量。如 果一个方法和成员变量在父类中没有用任何关键词,它就是friendly成员,能 被其子类访问。而用private限定的方法和成员变量是不能被子类访问的 方法的重载 在子类中定义一个和父类完全相同的方法,那么当这个子类的对象调用这个 方法的时候,它调用的是这个子类新定义的方法,而不是父类的方法,这就 是方法的重载 super关键字 super():调用父类中的构造方法 super.methodName(paraType paraName):调用父类中的methodName方法 final类 final类不能被继承,定义一个final类的格式如下: final class finalClass { … } final方法 final方法不能被重载,final方法的定义格式如下: final return_type method_name(para_type para_name) { … } abstract类 由于abstract类不能直接生成实例,所以它与final类和方法相反,必须要被继 承。定义一个抽象类的格式如下: abstract class abstractClass { … } abstract方法 abstract方法也是必须要被继承的。如果一个类声明了abstract方法,那么它必 须要被显式地声明为abstract。abstract方法的定义格式如下: abstract return_type method_name(para_type para_name) { … } 多态 对于重载或继承的方法,Java运行时系统根据调用该方法的实例类型来决定 选择哪个方法来调用,这就是多态 62 Java 游戏编程导学 (续表) 接口的定义 接口的定义格式如下: interface myInterface { interface body; } 在接口体中,声明的是多个方法的原型,定义格式如下: return_type method_name(para_type para_name,[……, para_type para_name]); 在方法体中,也可以定义常量。定义常量的格式如下: type NAME=value; type可以为任意类型,常量的名字一般为大写 接口的实现 在类的声明中用implements子句来表示一个类使用某个接口,在这个类中就 可以使用接口中定义的常量,而且必须实现这个接口中定义的所有方法。如 果不全部实现,则会出现编译错误。一个类可以实现多个接口,声明的格式 如下: class a implements interface1[,interface2,….,interfacen] { //必须实现所有接口里定义的所有方法 } 包 Java中的包是用来组织类之间的联系。通过包,可以控制类之间的可见性。 同时,通过包,我们解决了两个类重名的问题。包提供了一种命名机制和可 见性限制机制 包的定义 package是包的定义语句,放在Java源代码的第一条语句,指明该文件中定义 的类所在的包。如果没有用package指明包,则表明这个文件没有包名。 package语句的格式如下: package pakage1.[pakage2…..]; 包的使用 如果使用一个包中的类,我们必须把这个类导入进来。导入语句为import, 它的格式如下: import pakage1.pakage2.class_name; 第 2 章 面向对象编程起步 63 (续表) 对包内类的访问权限 同一个 类中 同一个包 中的子类 同一个包 中的非子类 不同包中 的子类 不同包中的 非子类 private Y protected Y Y Y friendly Y Y Y public Y Y Y Y Y 带包名的类文件的编译 和运行 (1)编译:应该在Java源代码文件所在目录执行javac命令。例如,一个类位 于包test.testPaint中,类名是Paint,那么就应该执行如下命令: javac Paint.java 执行后在源代码所在目录生成Paint.class文件,应该将这个目录放到目录 test\testPaint\下面 (2)运行:应该退到test的上一级目录,执行命令: java test.testPaint.Paint 这样才可以正确运行编写的Java程序 将Applet嵌入到HTML 页面中 示例如下: (1)APPLET CODE:Applet的class文件相对于这个HTML页面的路径和文件 名称。如果Applet的class文件放在\applet目录中,名称为My.class,则应该写成 APPLET CODE="applet\My.class"。 (2)WIDTH:Applet的宽度 (3)HEIGHT:Applet的高度 第 3 章 持有对象与异常处理 这一章我们着重介绍一下对象的持有和对程序中的异常进行处理的问题,然后通过具 体的例子来加深读者的理解。 3.1 持有你的对象 持有对象在编程中是一个十分棘手的问题。一般来说,程序总是会根据某些条件来产 生新的对象,但是不到执行期,无法得知究竟需要多少数量的对象,也无法知道这些对象 的确切型别,这样你就无法只依赖具体的reference来持有对象。为了解决这个问题,Java 提供了几种对象的持有方式,即java.util包中提供的一组容器类。 3.1.1 Array(数组) 在1.2.2节中已经介绍过数组的概念,这里我们关注的是对象的持有方式。Array在持有 对象的方面到底有什么特色,使得它可以在Java的发展过程中得以一直保留呢? 数组最有优势的地方在于它的效率。对于Java来说,为保存和访问一系列对象(实际 是对象的句柄),最有效的方法莫过于。数组实际代表一个简单的线性序列,它使得元素 的访问速度非常快,但在创建一个数组对象时,它的大小是固定的,而且不可在那个数组 对象的“存在时间”内发生改变。然而,由于为这种大小的灵活性要付出较大的代价,那 就使效率大大降低了。 在Java中,无论使用的是数组还是集合,都会进行范围检查——若超过边界,就会产 生一个RuntimeException(运行期异常)错误。同时,对于由基本数据类型构成的数组,它 们的运作方式与对象数组极为相似,只是前者直接包容了基本类型的数据值。 3.1.2 Collection(集合) java.util包中包含了一些在Java 2中新增的最令人兴奋的增强功能:集合。集合使得许 多java.util中的成员在结构上和包的体系结构上发生了根本的改变,同时它也扩展了包可以 被应用的任务范围,这是一项应该被程序员紧密关注的最新型技术。我们将给出一些比较 有代表性的集合来进行说明。首先讨论集合的接口。 Collection 接口 Collection接口是构造集合类的基础。它声明所有集合类都将拥有的核心方法,如表3.1 所示。因为所有的集合类实现Collection接口,所以熟悉它的方法对于清楚地理解框架是很 第 3 章 持有对象与异常处理 65 有必要的,当集合不能被修改的时候,会抛出UnsupportedOperationException异常;而在对 象不兼容的操作时,会产生ClassCastException异常。 表3.1 Collection接口的方法 方法 描述 Boolean add(Object obj) 将对象obj加入到调用集合中。如果加入成功则返回true;如果obj已 经在集合中或者集合不能复制时则返回false Boolean addAll(Collection c) 将集合c中的所有元素都加入到调用集合中,操作成功则返回true,否 则返回false void clear() 从调用集合中删除所有元素 Boolean contain(Object obj) 如果obj是调用集合中的一个元素则返回true,否则返回false Boolean containAll(Collection c) 如果调用集合中包含了集合c中的所有元素则返回true,否则返回false Boolean equals(Object obj) 如果调用集合与obj相等则返回true,否则返回false Int hashCode() 返回调用集合的散列码 Boolean isEmpty() 如果调用集合是空的则返回true,否则返回false Iterator iterator() 返回调用集合的迭代程序 Boolean remove(Object obj) 从调用集合中删除obj的一个实例,如果删除成功则返回true,否则返 回false Boolean removeAll(Collection c) 从调用集合中删除c的所有元素。如果删除成功则返回true,否则返回 false Boolean retainAll(Collection c) 删除调用集合中除了包含c中元素之外的全部元素,如果删除成功则 返回true,否则返回false Int size() 返回调用集合中元素的个数 Object[] toArray() 返回一个数组,该数组包含了所有存储在调用集合中的元素。数组元 素是集合元素的拷贝 Object[] toArray(Object array[]) 返回一个数组,该数组仅仅包含那些类型与数组元素类型匹配的集合 元素 下面我们实际讨论一些集合的标准类。 ArrayList 类 ArrayList类扩展AbstractList并执行List接口。在前面我们已经提到过,Array在创建之 后,它的大小就已经固定,不能进行改变,但是大多数情况下在运行时才可能知道到底需 要多大的数组。ArrayList就是为了解决这个问题而定义的。本质上,ArrayList是对象引用 的一个变长数组。就是说,ArrayList可以动态增加或减少其大小。 注意:动态数组也被以前版本保留的类Vector所支持。 下面的程序展示了ArrayList的一个简单应用。首先创建一个数组列表,然后添加类型 String的对象,接着列表被显示出来。将其中的一些元素删除以后,再一次显示列表。 Import java.util 0 66 Java 游戏编程导学 Class ArrayListDemo{ Public static void main(String arg[]){ //创建一个ArrayList ArrayList a1 = new ArrayList(); System.out.println("Initial size of a1: " + a1.size()); //添加元素 a1.add("a"); a1.add("b"); a1.add("c"); a1.add("d"); a1.add("e"); a1.add("f"); a1.add("g"); a1.add("h"); a1.add(1, "1"); System.out.println("Size of a1 after addtional: "+ a1.size()); //显示ArrayList System.out.println("Contents of a1: " + a1); //删除两个元素 a1.remove("f"); a1.remove(2); System.out.println("size of a1 after deletions: " + a1.size()); System.out.println("Contents of a1: " + a1.size()); } } 注意a1的大小是随着元素的添加而变大的,在删除元素以后,a1又变小了。 在程序中,我们是通过如下的语句给数组添加元素的。 //添加元素 a1.add("a"); …… a1.add(1, "1"); 删除元素的代码如下: //删除两个元素 a1.remove("f"); a1.remove(2); 你可以通过size()函数察看数组的大小: a1.size() 在这个例子中,可以使用从AbstractCollection继承下来的toString()方法,一般来说,开 发人员会提供自己的toString()方法进行重载,不过在这里这个默认的方法已经足够了。 尽管当对象被存储在ArrayList对象中时,其容量会自动增加,但是我们仍然可以通过 调用ensureCapacity()方法来人工增加ArrayList的容量。如果你在开始的时候一次性地增加 它的容量,就可以提高效率,避免再分配花费的不必要的时间。ensureCapacity()方法的特 征如下: void ensureCapacity(int cap) 第 3 章 持有对象与异常处理 67 这里,cap是新的容量。 相反地,如果想要减小在ArrayList对象之下的数组大小,以便它能正好容纳当前项, 可以使用trimToSize()方法,格式如下: void trimToSize() LinkedList 类 LinkedList类扩展AbstractSequentialList并执行List接口。它提供了一个链接列表数据结 构。除了继承的方法之外,它本身还定义了一些有用的方法。这些方法主要用于操作和访 问列表。使用addFirst()方法可以在列表头增加元素,使用addLast()方法可以在列表的尾部 增加元素,它们的形式如下所示,其中的obj是被增加的项: void addFirst(Object obj) void addLast(Object obj) 调用getFirst()方法可以获得第一个元素,调用getLast()方法可以得到最后一个元素,形 式如下: Object getFirst() Object getLast() 为了删除第一个元素可以使用removeFirst()方法,为了删除最后一个元素,可以调用 removeLast()方法。它们的形式如下: Object removeFirst() Object removeLast() 下面的程序示例说明了几个LinkedList支持的方法。 import java.uitl.*; class LinkedListDemo{ public static void main(String args[]){ //创建一个LinkedList LinkedList l1 = new LinkedList(); //添加元素 l1.add("a"); l1.add("b"); l1.add("c"); l1.add("d"); l1.addFirst("A"); l1.addLast("Z"); System.out.println("Original contents of l1: " + l1); //删除元素 l1.remove("c"); l1.remove(2); System.out.println("Contents of l1 after deletion: " + l1); //删除第一个和最后一个元素 l1.removeFirst(); l1.removeLast(); System.out.println("l1 after deleting first and last: " + l1); 68 Java 游戏编程导学 //得到和设置 Object val = l1.get(2); l1.set(2 ,(String) val + " Changed"); System.out.println("l1 after change: " + l1); } } HashSet 类 HashSet类扩展AbstractSet并实现Set接口。它创建一个使用散列表进行存储的集合。散 列表通过散列法的机制存储信息。在散列(Hashing)中,一个关键字的信息内容被用来确 定一个惟一的值,称之为散列码。散列码作为关键字,标志存储数据的存储下标。关键字 到其散列码的转换是自动执行的——看不到散列码本身,程序代码已不能直接索引散列表。 散列表的优点是对于比较大的集合进行基本操作,诸如add()、remove()和size()方法的运行 时间保持不变。 注意:重要的是,散列集合并没有确保其元素的顺序,因为散列法在处理集 合中的元素时,通常不会将这些元素排序。如果需要排序存储,TreeSet优于 HashSet,我们在这里不作过多介绍,读者可以自行查阅J2SE文档。 HashSet的构造函数定义为: HashSet() HashSet(Collection c) HashSet(int capacity) HashSet(int capacity,float fillRatio) 第一种形式构造默认散列集合;第二种形式用c中的元素初始化散列集合;第三种形式 使用整数capacity初始化散列集合的容量;最后一种形式使用capacity初始化散列集合的容 量,同时用引数fillRatio决定填充比,fillRatio必须介于0.0~1.0之间,它决定散列集合向上 调整大小之前,有多少可以充满,其默认值为0.75。 HashSet没有定义任何超过它的超类和接口提供的其他方法。 3.1.3 Mapping(映射) 除了集合,Java 2还在java.util中加入了映射。就好像数据库一样,映射是一个存储关 键字(Key)和值(Value)的关联或者说是关键字/值(key-value)对的对象。给定一个关 键字,可以得到它的值。关键字必须是惟一的,但值可以被复制。有些映射可以接受null 关键字和null值。下面我们讨论一下Map接口和最常用的HashMap类。 Map 接口 Map接口给值映射惟一的关键字。关键字是以后用于检索的对象,给定一个关键字和 一个值,可以存储这个值到一个Map对象中。当这个值被存储后,就可以使用它的关键字 来检索它。方法如表3.2所示。当调用的映射中没有项存在时,有的方法会引起 NoSuchElementException异常;对象与映射中的元素不兼容时,会引发ClassCastException 0 第 3 章 持有对象与异常处理 69 异常;当试图使用映射不允许使用的null对象时,会引发NullPointerException异常;当试图 修改一个不允许修改的映射时,会引发一个UnsupportedOperationException异常。 表3.2 Map接口定义的方法 方法 描述 void clear() 从调用映射中删除所有的关键字/值对 Boolean containsKey(Object k) 如果调用映射中包含了作为关键字的k,则返回true;否 则 返 回false Boolean containsValue(Object v) 如果映射中包含了作为值的v,则返回true;否则返回false Set entrySet() 返回包含了映射中项的Set Boolean equals(Object obj) 如果obj是一个Map,并且包含相应的输入,则返回true;否 则返回false Object get(Object k) 返回与关键字k相关联的值 Int hashCode() 返回调用映射的散列码 Boolean isEmpty() 如果调用映射是空的则返回true;否则返回false Set keySet() 返回一个包含调用中关键字的集合 Object put(Object k,Object v) 将一个输入加入调用映射,覆盖原先与该关键字相关联的 值。关键字和值分别为k和v。如果关键字不存在,则返回null, 否则,返回原先与关键字相关联的值 void putAll(Map m) 将所有来自m的输入加入调用映射 Object remove(Object k) 删除关键字等于k的输入 Int size() 返回映射中关键字/值对的个数 Collection values() 返回一个包含了映射中值的类集 HashMap 类 HashMap类使用散列表实现Map接口。这允许一些基本操作(如get()和put())的运行时 间保持恒定,即使对于大型集合也是这样的。 下面的构造函数定义为: HashMap() HashMap(Map m) HashMap(int capacity) HashMap(int capacity, float fillRatio) 第一种形式构造默认散列映射;第二种用m中的元素初始化散列映射;第三种形式使 用整数capacity初始化散列映射的容量;最后一种形式使用capacity初始化散列映射的容量, 同时用引数fillRatio决定填充比,fillRatio必须介于0.0~1.0之间,它决定散列映射向上调整 大小之前,有多少可以充满,其默认值为0.75。 HashMap实现Map并扩展AbstractMap。它本身并没有增加任何新的方法。散列映射和 散列集合一样不保证它的元素的顺序,因此,元素加入散列映射的顺序并不一定是它们被 迭代函数读出的顺序。 接下来我们介绍一个小例子,这个例子和上一章的例子比较类似,不同的地方在于它 使用数组来控制各个问题,这样就不会单单只是一个问题提给读者了,变化的问题使得游 70 Java 游戏编程导学 戏更有趣。 3.2 “球迷必答” 这里我们提供一个小例子,这是一个关于NBA球星名字的问答。由于只是用来讲解, 我们只选择了5位球星,分别是姚明、乔丹、艾弗森、科比和加内特,答案依次为YaoMing、 Jordan、Iverson、Bryant和Garnett,就是5位球星的英文名字。结果如图3.1所示。 图 3.1 游戏显示结果 3.2.1 游戏规则 这个游戏的规则很简单,当Applet显示的时候,出现姚明的图片,同时要求读者输入 球星的名字,如果读者的回答错误,则弹出一个对话框,显示“您的答案错误,请重试”; 如果读者的回答正确,则显示“恭喜,您答对了,请回答下一题”并显示下一幅图片,如 此继续。 3.2.2 游戏注意点 我们使用JBuilder编写这个小游戏,这个游戏实际上和第2章编写的“幸运52”游戏比 较类似,这次我们增加了问题的数量,这样就需要使用一个数组进行存储和控制。同时我 们使用一个单独的类QuestionBean来封装有关于问题图片链接和问题答案的信息,这样便 于控制。 第 3 章 持有对象与异常处理 71 3.2.3 建立工程 在JBuilder中选择主菜单File→New Project→“新建工程向导”,填写好工程名称 basketball,选择工程目录路径,在这里和第2章一样选择D:\myPro,JBuilder自动把工程名 basketball添加进路径,点击OK按钮,工程就建立好了。 注意:工程建立好以后,如果读者注意查看目的文件夹,也就是使用Windows 的文件系统打开这个工程目录,会发现一个扩展名为.jpx的文件,这是建立工 程时JBuilder生成的工程文件,非常重要,所有关于工程的信息都保存在这里 面,如果关闭工程以后,JBuilder也是使用这个文件来重新打开工程。 接下来要做的就是建立Applet文件,选择主菜单File→New→Web选项卡→Applet,点 击OK按钮,进入Applet向导界面,在文件名称框中填写类名Basketball,注意基类框中一定 是java.awt.Applet,如果不是的话请改正,然后点击Finish按钮。在工程下就生成了文件 Basketball.java和同名的嵌入Applet的文件Basketball.html。 工程编写完毕的结果如图3.2所示,不过大家同样可以看到工程文件下的Basketball.java 和同名的html文件。 图 3.2 结果文件结构 我们还要改写一下生成的HTML文件的框架代码,因为很简单,这里就不再赘述,代 码如下: HTML Test Page 0 72 Java 游戏编程导学
考察你关于球星的知识
3.2.4 编写问题封装文件QuestionBean 在前面已经说过,我们要用一个类来将问题封装,接下来我们首先编写这个类。选择 JBuilder的主菜单File→New→General选项卡→Class选项,点击OK按钮,进入新建类向导 (Class Wizard),在对话框的类名称文本框中填写QuestionBean,其余选择默认设置即可, 点击Finish按钮,就新建好了一个类,在图3.2中我们也能看到QuestionBean.java。 接下来要想一想,在这个类中到底要封装什么?问题的题干是一致的,将直接填写在 主界面上,不用封装。还有就是用作问题的图片和答案,对于图片,我们不打算将它们直 接装载到类中,而是封装一个图片的链接地址;对于答案,就直接作为一个字符串封装在 类中。这样就可以开始动手进行编写了。代码如下: package basketball; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 */ public class QuestionBean { String imageUrl; String answer; public QuestionBean() { } public QuestionBean(String imageUrl, String answer) { this.imageUrl = imageUrl; this.answer = answer; } public String getImageUrl() { 第 3 章 持有对象与异常处理 73 return imageUrl; } public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } public String getAnswer() { return answer; } public void setAnswer(String answer) { this.answer = answer; } } 大家可以看到,这个类比较简单,实际上只是封装了一些要进行操作的数据作为类的 属性,有点类似于C和C++中的结构体,然后提供了get/set方法对每一个类的属性进行存取。 在这里我们封装了两个字符串变量为私有变量,分别是图像路径变量imageUrl和答案变量 answer,然后使用公有的get/set方法提供存取。这样做可以提高安全性,因为只有在这个类 的内部才可以直接对类的属性进行操作。 3.2.5 编写游戏界面 这个游戏有一个主界面,界面上有两个按钮,还有一个文本框,用来输入用户计算用 到的表达式。主界面还有一个用来显示当前图片的面板。和第2章讲解的顺序一样,我们开 始先不处理任何事件,仅仅编写界面。 编写显示图片的界面 在这里我们编写一个用来显示图片的Panel类。这个类从Panel类中继承。在JBuilder的 主菜单中选择File→New→General选项卡→Panel,点击OK按钮,进入Panel向导界面。在这 个对话框中改写Panel的名字为BeanPanel,同时注意基类一定为java.awt.Panel,如果不是的 话请改正,然后点击OK按钮,BeanPanel的代码框架就生成了。然后我们改写代码如下: package basketball; import java.awt.*; import java.net.URL; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 74 Java 游戏编程导学 */ public class BeanPanel extends Panel { QuestionBean q; private Image img; public BeanPanel(){} public BeanPanel(QuestionBean qb){ this.q = qb; } public void setQuestionBean(QuestionBean qb){ this.q = qb; } public void paint(Graphics g){ URL url = null; try { url = Class.forName("basketball.Basketball"). getResource(q.getImageUrl()); } catch (Exception e) {} img = getToolkit().getImage(url); MediaTracker mt = new MediaTracker(this); mt.addImage(img, 1); try { mt.wait(); mt.checkAll(); } catch (Exception e) {} g.drawImage(img, 125, 0, 160, 200, this); } } 我们使用paint()方法中的如下代码绘制图片: g.drawImage(img, 125, 0, 160, 200, this); 这里的q是3.2.4节创建的QueationBean的一个实例。我们通过q变量来提供关于问题的 属性给BeanPanel。q对象是在BeanPanel构造的时候传进去的。 QuestionBean q; …… public BeanPanel(QuestionBean qb){ this.q = qb; } public void setQuestionBean(QuestionBean qb){ 第 3 章 持有对象与异常处理 75 this.q = qb; } 主界面的编写 我们编写主界面的时候仍然使用BorderLayout。这个布局很简单,关于它与其他的布局 的比较在后几章里将会详细讲到。 package basketball; import java.awt.*; import java.awt.event.*; import java.applet.*; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 */ public class Basketball extends Applet { BorderLayout borderLayout1 = new BorderLayout(); Panel panel1 = new Panel(); Panel panel2 = new Panel(); Label label1 = new Label(); Button OK = new Button(); TextField ans = new TextField(); Button reset = new Button(); //静态的数组索引 static int i = 0; //静态的数组元素成员 static QuestionBean q1 = new QuestionBean("Jordan.gif", "Jordan"); static QuestionBean q0 = new QuestionBean("Yao.gif", "Yaoming"); static QuestionBean q2 = new QuestionBean("Iverson.gif","Iverson"); static QuestionBean q3 = new QuestionBean("Bryant.gif","Bryant"); static QuestionBean q4 = new QuestionBean("Garnett.gif","Garnett"); //静态的问题数组对象 //在这里将成员直接添加进去 static QuestionBean[] qb = {q0, q1, q2, q3, q4}; static BeanPanel p = new BeanPanel(qb[Basketball.i]); //Construct the applet public Basketball() { this.setLayout(borderLayout1); 76 Java 游戏编程导学 label1.setText("请输入这个球星的名字:"); label1.setVisible(true); OK.setForeground(Color.black); OK.setLabel("确定"); //OK.addActionListener(new Basketball_OK_actionAdapter(this)); ans.setColumns(10); ans.setLocale(java.util.Locale.getDefault()); ans.setText(""); reset.setLabel("重新开始"); //reset.addActionListener(new Basketball_reset_ actionAdapter(this)); panel2.add(reset, null); panel1.add(label1, null); panel1.add(ans, null); this.add(panel2, BorderLayout.SOUTH); panel2.add(OK, null); this.add(p, BorderLayout.CENTER); this.add(panel1, BorderLayout.NORTH); } } 注意到我们在这里添加了一个静态数组QuestionBean[] qb ,同时建立了5 个 QuestionBean的对象将其添加进数组。这样我们就比较容易进行控制,同时定义了一个静 态的数组索引int i。 对话框的编写 这里暂时不编写对话框,因为我们打算利用这个对话框向读者介绍一些JBuilder的UI 编辑器的用法。 3.2.6 处理事件 这个游戏的逻辑处理比较简单,我们就直接把它和事件处理编写在一起。先添加事件 监听器: OK.addActionListener(new Basketball_OK_actionAdapter(this)); reset.addActionListener(new Basketball_reset_actionAdapter(this)); 接下来是OK按钮的事件处理函数。注意在这个函数体中包含了游戏的逻辑,很简单, 判断答案是否正确,如果正确的话弹出正确对话框,并且继续下一题目;否则,弹出错误 对话框,逻辑停止。 void OK_actionPerformed(ActionEvent e) { String answer = ans.getText(); if (answer.equals(qb[i].getAnswer())) { //next Msg m = new Msg("恭喜,您答对了,请回答下一题"); (Basketball.i)++; this.p.setQuestionBean(qb[Basketball.i]); this.repaint(); 第 3 章 持有对象与异常处理 77 this.setVisible(true); } else { Msg m = new Msg("您的答案错误,请重试"); } } 下面是Reset按钮的对话框,简单地返回第一幅图片。 void reset_actionPerformed(ActionEvent e) { (Basketball.i) = 1; this.p.setQuestionBean(qb[Basketball.i]) this.repaint(); } 下面是添加事件所用的其他类: class Basketball_OK_actionAdapter implements java.awt.event.ActionListener { Basketball adaptee; Basketball_OK_actionAdapter(Basketball adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.OK_actionPerformed(e); } } class Basketball_reset_actionAdapter implements java.awt.event.ActionListener { Basketball adaptee; Basketball_reset_actionAdapter(Basketball adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.reset_actionPerformed(e); } } 3.2.7 使用UI编辑器来编写消息对话框 下面我们来编写对话框界面,这一步采用JBuilder的UI编辑器,这样可以大大提高编写 的效率。 首先选择JBuilder的主菜单File→New→General选项卡→Frame选项,点击OK按钮,进 入Frame向导界面。然后在对话框中填写类名Msg,注意基类一定是java.awt.Frame。然后点 击Finish按钮,生成代码框架。 78 Java 游戏编程导学 在编辑器中点击Design标签,进入UI设计界面,如图3.3所示。 图 3.3 UI 设计界面 下面是实际设计步骤: 1. 在图3.3中鼠标指针所指的那一行工具条中,选择AWT选项卡。 2. 在工具条中选择Panel工具添加进当前编辑器作为控件容器,然后再选择Label控件 添加进这个容器。 3. 在工具条中重新选择Panel工具添加进当前编辑器作为容器,先选择Panel按钮,再 在界面左下角的大纲视图中点击this,就完成了添加。然后我们在窗口最右边的属 性列表中找到Constraints属性,将它改为South。然后用同样的方法给这个Panel添加 一个按钮,再在窗口右边的属性列表中找到label属性,改为“确定”,name属性改 为OK,结果如图3.4所示。 4. 接下来要给按钮添加一个事件,在属性对话框中点击Events 标签,选择 actionPerformed选项,双击鼠标进入代码窗口,就可以编辑事件响应函数了。在这 里我们先什么也不做。 第 3 章 持有对象与异常处理 79 图 3.4 添加结果 5. 最后来改写一下代码,由于JBuilder自动生成的代码不太符合我们的需要,所以在 这个代码的基础上加以改进,下面是完整的源代码: package basketball; import java.awt.*; import java.awt.event.*; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 */ public class Msg extends Frame implements ActionListener { Panel panel1 = new Panel(); Panel panel2 = new Panel(); Label label1 = new Label(); Button ok = new Button(); public Msg() { } 80 Java 游戏编程导学 public Msg(String s) { super(); label1.setText(s); ok.setLabel("确定"); this.add(panel1, BorderLayout.CENTER); panel1.add(label1, null); this.add(panel2, BorderLayout.SOUTH); panel2.add(ok, null); this.setSize(200, 150); this.setVisible(true); setLocation(300, 200); ok.addActionListener(new Msg_ok_actionAdapter(this)); } public void actionPerformed(ActionEvent actionEvent) { this.dispose(); } void ok_actionPerformed(ActionEvent e) { this.dispose(); } } class Msg_ok_actionAdapter implements java.awt.event.ActionListener { Msg adaptee; Msg_ok_actionAdapter(Msg adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.ok_actionPerformed(e); } } 好,到这里这个游戏就编写完毕了,但是我们注意到,在JBuilder自己生成的代码中, 构造函数中经常会出现这样的代码: try{ jbinit() }catch(Exception e){ e.printlnStack() } 这就是Java中的异常处理机制,下面我们就来介绍它。 第 3 章 持有对象与异常处理 81 3.3 异常处理机制 “异常”(Exception)这个词表达的是一种“例外”情况,亦即正常情况之外的一种 “异常”。在问题发生的时候,必须坚决地停下来,并由某人、某地指出发生了什么事情, 以及该采取何种对策。如果当地没有足够多的信息,就需要将问题移交给更高级的负责人, 令其作出正确的决定(类似一个命令链)。异常控制是由Java编译器强行实施的。本节向 大家介绍了正确控制异常所需的代码,以及在某个方法遇到麻烦的时候,该如何生成自己 的异常。 3.3.1 基本异常 “异常条件”表示在出现什么问题的时候应中止方法或作用域的继续。为了将异常问 题与普通问题区分开,异常条件是非常重要的一个因素。在普通问题的情况下,我们在当 地已拥有足够的信息,可在某种程度上解决碰到的问题。而在异常条件的情况下,却无法 继续下去,因为当地没有提供解决问题所需的足够多的信息。此时,我们能做的惟一事情 就是跳出当地环境,将那个问题委托给一个更高级的负责人。 产生一个异常时,会发生几件事情。首先,按照与创建Java对象一样的方法创建异常 对象:在内存“堆”里,使用new来创建。随后,停止当前执行路径(记住不可沿这条路 径继续下去),然后从当前的环境中释放出异常对象的句柄。此时,异常控制机制会接管 一切,并开始查找一个恰当的地方,用于继续程序的执行。这个恰当的地方便是“异常控 制器”,它的职责是从问题中恢复,使程序要么尝试另一条执行路径,要么简单地继续。 产生异常通常也叫做“抛出异常”,它的形式为: if(t == null)//假定的产生异常的条件 throw new NullPointerException(); 异常自变量 和Java的其他任何对象一样,需要用new在内存堆里创建异常,并需要调用一个构建器。 在所有标准异常中,存在着两个构建器:第一个是默认构建器,第二个则需要使用一个字 符串自变量,使我们能在异常里置入相关信息,字符串可用各种方法提取出来,就像稍后 会展示的那样。 if(t == null) throw new NullPointerException("t = null"); 在这里,关键字throw会像变戏法一样做出一系列不可思议的事情。它首先执行new 表 达式,创建一个不在程序常规执行范围之内的对象,而且为那个对象调用构建器。随后, 对象会从方法中返回——尽管对象的类型通常并不是方法设计为返回的类型。通过“抛” 出一个异常,亦可从原来的作用域中退出。但是会先返回一个值,再退出方法或作用域。 但是,与普通方法返回的相似性到此便全部结束了,因为我们返回的地方与从普通方法调 用中返回的地方是迥然不同的。 82 Java 游戏编程导学 注意:可根据需要抛出任何类型的“可抛”对象。典型情况下,我们要为每 种不同类型的错误“抛”出一类不同的异常。我们的思路是在异常对象以及 挑选的异常对象类型中保存信息,使得在更高一级的应用中的使用者可以知 道如何对待异常。 3.3.2 捕获异常 若某个方法产生一个异常,必须保证该异常能被捕获,并得到正确对待。对于Java的 异常控制机制,它的一个好处就是允许我们在一个地方将精力集中在要解决的问题上,然 后在另一个地方对待来自那个代码内部的错误。 try 块 若位于一个方法内部,并“抛”出一个异常(或在这个方法内部调用的另一个方法产 生了异常),那个方法就会在异常产生过程中退出。若不想发现一个异常就立即跳出方法, 可在那个方法内部设置一个特殊的代码块,用它捕获异常。这就叫做“try块”,因为要在 这个地方“尝试”各种方法调用。 try块属于一种普通的作用域,用一个try关键字开头: try { // 可能产生异常的代码 } 若用一种不支持异常控制的编程语言全面检查错误,必须用设置和错误检测代码将每 个方法都包围起来——即便多次调用相同的方法。而在使用了异常控制技术后,可将所有 东西都置入一个try块内,在同一地点捕获所有异常。这样便可极大简化代码,并使其更易 辨读,因为代码本身要达到的目标再也不会与繁复的错误检查混淆。 异常控制模块 当然,生成的异常必须在某个地方中止。这个“地方”便是异常控制器或者异常控制 模块。而且针对想捕获的每种异常类型,都必须有一个相应的异常控制器。异常控制器紧 接在try块后面,并用catch(捕获)关键字标记。如下所示: try { // Code that might generate exceptions } catch(Type1 id1) { // Handle exceptions of Type1 } catch(Type2 id2) { // Handle exceptions of Type2 } catch(Type3 id3) { // Handle exceptions of Type3 } // etc... 每个catch 从句——即异常控制器——都类似一个小型方法,它需要采用一个(而且只 0 第 3 章 持有对象与异常处理 83 有一个)特定类型的自变量,可在控制器内部使用标识符(id1、id2等等),就像一个普通 的方法自变量那样。我们有时并不使用标识符,因为异常类型已提供了足够的信息,可有 效处理异常。但即使不用,标识符也必须就位。 控制器必须“紧接”在try块后面。若“抛”出一个异常,异常控制机制就会搜寻自变 量与异常类型相符的第一个控制器。随后,它会进入那个catch从句,并认为异常已得到控 制(一旦catch从句结束,对控制器的搜索也会停止)。只有相符的catch从句才会得到执行; 它与switch语句不同,后者在每个case后都需要一个break命令,防止误执行其他语句。 在try块内部,请注意大量不同的方法调用可能生成相同的异常,但只需要一个控制器。 异常处理方法 在异常控制理论中,共存在两种基本方法。在“中断”方法中(Java和C++提供了对这 种方法的支持),假定错误非常关键,没有办法返回异常发生的地方。无论谁只要“抛” 出一个异常,就表明没有办法补救错误,而且也不希望再回来。 另一种方法叫做“恢复”。它意味着异常控制器有责任来纠正当前的状况,然后取得 出错的方法,假定下一次会成功执行。若使用恢复,意味着在异常得到控制以后仍然想继 续执行。在这种情况下,异常更像一个方法调用——我们用它在Java 中设置各种各样特殊 的环境,产生类似于“恢复”的行为(换言之,此时不是“抛”出一个异常,而是调用一 个用于解决问题的方法)。另外,也可以将自己的try块置入一个while循环里,用它不断进 入try块,直到结果满意时为止。 注意:介绍“恢复”方法,并不是说我们就推荐使用异常处理机制来做逻辑 控制。正相反,一定要注意,不能使用异常机制来控制逻辑。如果使用异常 机制来控制逻辑的话,会造成错误处理和逻辑控制的代码混杂在一起,容易 出错,更与异常处理机制的本意相悖。 异常规范 在Java中,对那些要调用方法的客户程序员,我们要通知他们可能从自己的方法里“抛” 出异常,只有这样做才能使客户程序员准确地知道要编写什么代码来捕获所有潜在的异常。 为了解决这个问题,Java提供了一种特殊的语法格式,以便告诉客户程序员该方法会“抛” 出什么异常,令对方方便地加以控制。这便是我们在这里要讲述的“异常规范”,它属于 方法声明的一部分,位于自变量(参数)列表的后面。 异常规范采用了一个额外的关键字throws,后面跟随全部潜在的异常类型。 void f() throws Exception1, Exception2, Exception3 { //... 不能完全依赖异常规范——假若方法造成了一个异常,但没有对其进行控制,编译器 会侦测到这个情况,并告诉我们必须控制异常,或者指出应该从方法里“抛”出一个异常 规范。通过坚持从顶部到底部排列异常规范,Java 可在编译期保证异常的正确性。 捕获所有异常 我们可创建一个控制器,令其捕获所有类型的异常。具体的做法是捕获基础类异常类 0 84 Java 游戏编程导学 型Exception(也存在其他类型的基础异常,但Exception 是适用于几乎所有编程活动的基 础),如下所示: catch(Exception e) { System.out.println("caught an exception"); } 这段代码能捕获任何异常,所以在实际使用时最好将其置于控制器列表的末尾,防止 跟随在后面的任何特殊异常控制器失效。 对于程序员常用的所有异常类来说,由于Exception 类是它们的基础,所以我们不会获 得关于异常太多的信息,但可调用来自它的基础类Throwable 的方法: String getMessage() 获得详细的消息。 String toString() 返回对Throwable的一段简要说明,其中包括详细的消息(如果有的话)。 void printStackTrace() void printStackTrace(PrintStream) 打印出Throwable和Throwable的调用堆栈路径。调用堆栈显示出将我们带到异常发生地点 的方法调用的顺序。 第一个版本会打印出标准错误,第二个则打印出我们的选择流程。若在Windows 下工 作,就不能重定向标准错误。因此,我们一般愿意使用第二个版本,并将结果送给System.out, 这样一来,输出就可重定向到我们希望的任何路径。 除此以外,我们还可从Throwable的基础类Object(所有对象的基础类型)获得另外一 些方法。对于异常控制来说,其中一个可能有用的是getClass(),它的作用是返回一个对象, 用它代表这个对象的类。我们可依次用getName()或toString()查询这个Class类的名字。亦可 对Class对象进行一些复杂的操作,尽管那些操作在异常控制中是不必要的。本章稍后还会 详细讲述Class对象。 下面是一个特殊的例子,它展示了Exception 方法的使用: public class ExceptionMethods { public static void main(String[] args) { try { throw new Exception("Here's my Exception"); } catch(Exception e) { System.out.println("Caught Exception"); System.out.println( "e.getMessage(): " + e.getMessage()); System.out.println( "e.toString(): " + e.toString()); System.out.println("e.printStackTrace():"); e.printStackTrace(); } } } 第 3 章 持有对象与异常处理 85 可以看到,该方法连续提供了大量信息——每类信息都是前一类信息的一个子集。 3.3.3 重新抛出异常 在某些情况下,我们想重新抛出刚才产生过的异常,特别是在用Exception 捕获所有可 能的异常时。由于我们已拥有当前异常的句柄,所以只需简单地重新抛出那个句柄即可。 下面是一个例子: catch(Exception e) { System.out.println("一个异常已经产生"); throw e; } 重新“抛”出一个异常导致异常进入更高一级环境的异常控制器中。用于同一个try 块 的任何更进一步的catch从句仍然会被忽略。此外,与异常对象有关的所有东西都会得到保 留,所以,用于捕获特定异常类型的更高一级的控制器可以从那个对象里提取出所有信息。 若只是简单地重新抛出当前异常,我们打印出来的、与printStackTrace()内的那个异常 有关的信息会与异常的起源地对应,而不是与重新抛出它的地点对应。若想安装新的堆栈 跟踪信息,可调用fillInStackTrace(),它会返回一个特殊的异常对象。这个异常的创建过程 如下:将当前堆栈的信息填充到原来的异常对象里。 3.3.4 标准Java异常 Java包含了一个名为Throwable的类,它对可以作为异常“抛”出的所有东西进行描述。 Throwable对象有两种常规类型(亦即“从Throwable继承”)。其中,Error代表编译期和 系统错误,一般不必特意捕获它们(特殊情况除外)。Exception是可以从任何标准Java库 的类方法中“抛”出的基本类型。此外,它们亦可从我们自己的方法以及运行期偶发事件 中“抛”出。 为获得异常的一个综合概念,最好的方法是阅读由http://java.sun.com提供的联机Java 文档。为了对各种异常有一个大概的印象,这个工作是相当有价值的。但大家不久就会发 现,除名字外,一个异常和下一个异常之间并不存在任何特殊的地方。我们最需要掌握的 是基本概念,以及用这些异常能够做什么。 java.lang.Exception是程序能捕获的基本异常。其他异常都是从它衍生的。这里要注意 的是异常的名字代表发生的问题,而且异常名通常都是精心挑选的,可以很清楚地说明到 底发生了什么事情。异常并不全是在java.lang 中定义的;有些是为了提供对其他库的支持, 如util、net以及io等,我们可以从它们的完整类名中看出这一点,或者观察它们从什么继承。 例如,所有IO异常都是从java.io.IOException继承的。 特殊的 RuntimeException 这个类别里含有一系列异常类型。它们全部由Java自动生成,无需我们亲自动手把它 们包含到自己的异常规范里。最方便的做法是,将它们置入单独一个名为RuntimeException 的基础类下面,它们全部组合到一起。这是一个很好的继承例子:它建立了一系列具有某 86 Java 游戏编程导学 种共通性的类型,都具有某些共通的特征与行为。此外,我们没必要专门写一个异常规范, 指出一个方法可能会“抛”出一个RuntimeException,因为已经假定可能出现那种情况。由 于它们用于指出编程中的错误,所以几乎永远不必专门捕获一个“运行期异常”——Runtime Exception——它在默认情况下会自动得到处理。若必须检查RuntimeException,我们的代码 就会变得相当繁复。 3.3.5 创建自己的异常 尽管Java的内置异常可以处理大多数常见错误,但是也许你还是希望建立自己的异常 类型来处理你所应用的特殊情况。这是非常简单的:你只需要定义Exception的一个子类, 因为在前面我们已经提到,Exception是所有异常的父类。你的子类不需要执行任何动作, 只要你在类型系统中对它们进行了声明,就可以作为异常来使用。 Exception类自己没有定义任何方法,当然,由于是从Throwable继承来的,它拥有 Throwable提供的一些方法。因此,所有异常(包括你创建的)都拥有Throwable定义的方 法,如表3.3所示。你还可以在你创建的异常类中覆盖一个或者多个这样的方法。 表3.3 Throwable提供的方法 方法 描述 Throwable fillInStackTrace() 返回一个包含完整堆栈轨迹的Throwable对象,该对象可 能被再次引发 String getLocalizedMessage() 返回一个异常的局部描述 String getMessage() 返回一个异常的描述 Void printStackTrace() 显示堆栈轨迹 Void printStackTrace(PrintReaderstream) 把堆栈轨迹送到指定的输入流 Void printStackTrace(PrintWriterstream) 把堆栈轨迹送到指定的输出流 String toString() 返回一个包含异常描述的String 对象。当输出一个 Throwable对象时,该方法被println()调用 下面的例子自定义了Exception的一个子类,然后把该子类当作方法中出错情形的信号。 它重载了toString()方法,这样可以用println()显示异常的描述。 //这个程序创建了一个异常子类 class MyException extends Exception{ private int detail; MyException(int a){ detail = a; } public String toString(){ Return "MyException [" + detail + "]"; } } //这是主类 第 3 章 持有对象与异常处理 87 class ExceptionDemo{ static void compute(int a) throws MyException{ System.out.println("Called compute (" + a + ")"); if(a>10) throw new MyException(a); System.out.println("Normal exit"); } public static void main(String args[]){ try{ compute(1); compute(20); }catch(MyException e){ System.out.println("Caught " + e); } } } 这个例子定义了Exception的一个子类MyException。这个子类很简单:只含有一个构造 函数和一个重载的显示异常值的toString()方法。ExceptionDemo类定义了一个compute()方 法,该方法引发一个MyException对象。当compute()的整型参数比10大时该异常被引发。 main()方法为MyException设立了一个异常处理程序,然后用一个合法的值和不合法的值调 用compute()来显示执行经过代码的不同路径。下面是程序运行产生的结果清单: Called compute (1) Normal exit Called compute (20) Caught MyException [20] 3.4 “速算24”游戏 这一节我们来编写“速算24”的游戏:在游戏中,用户随意抽出4张扑克牌,用加、减、 乘、除的方法将它们连接起来,使得结果等于24。本节通过“速算24”游戏来加深对数组、 异常处理等的理解。 3.4.1 游戏效果说明 “速算24”是一个锻炼玩家心算和快速反应能力的游戏。在游戏给出4张牌之后,玩家 应该能够很快地给出一个算式,使其计算结果等于24。游戏应该能够在玩家输入表达式之 后判断出玩家输入的表达式是合法还是非法的。如果是合法的,则计算结果是否为24,如 果不是24,则提示用户结果为错。 游戏的初始界面如图3.5所示。 88 Java 游戏编程导学 图 3.5 游戏的初始界面 3.4.2 编写游戏规则 我们首先写出游戏规则,这样就明确了我们编写游戏的目标: (1)点击“开始游戏”按钮,游戏开始,系统将会发出4张牌。 (2)用户将发出的4张牌用+,-,*,/组合起来,并把组合的表达式输入到输入框里。 (3)点击“确定”按钮,游戏将会计算你输入的表达式是否合法,或者结果是否正确, 并且给出提示。 (4)如果输入的表达式不正确,则会让用户重新输入;如果输入的表达式正确,则重 新开始游戏。 目标明确以后,我们的编写就不是无的放矢,这样我们才可以明确自己要做什么,不 要做什么,下面我们就来具体编写游戏。 3.4.3 创建工程和Applet 首先打开JBuilder 9.0,这个游戏和以前的例子一样,也是一个Applet,关于这种游戏的 编写我们已经有很多心得了。我们通过主菜单File→New Project直接创建一个工程,在工程 向导对话框中填写工程目录和工程名称,我们给这个工程起名为Cal24,然后点击Finish按 钮。 接着选择主菜单File→New→Web选项卡→Applet,单击OK按钮,进入Applet向导页面。 在这里我们只改写Applet类的名字为Cal24,然后注意检查基类是不是java.applet.Applet,如 果不是则加以改正,接着点击OK按钮,JBuilder就为我们创建好了Applet的框架。如图3.6 所示。 第 3 章 持有对象与异常处理 89 图 3.6 JBuilder 生成的结果和框架代码 我们可以看到,JBuilder为我们生成了框架代码和嵌入Applet的页面,这里面的代码基 本上对于我们是没有用处的,我们先来改写Cal24.html的代码,如下所示。在页面里我们首 先给出游戏规则,然后把Applet嵌入到一个Table(表单)中。这很简单,这里就不作详细 解释了。 速算24游戏

速算24游戏

游戏规则:
(1)点击"开始游戏"的按钮,游戏开始,系统将会发出四张牌
(2)用户将发出的四张牌用+,-,*,/组合起来,并把组合的表达式输入到输入框里头
(3)点击确定按钮,游戏将会计算你输入的表达式是否合法,或者结果是否正确,并且给出 提示。
(4)如果输入的表达式不正确,则会让用户重新输入;如果输入的表达式正确,则重新开始 游戏。
90 Java 游戏编程导学
改写后,我们可以在JBuilder中直接观察HTML文件的输出结果,和以前一样,Applet 区域是灰色的,如图3.7所示。 图 3.7 改写 Cal24.html 后的结果 3.4.4 设计游戏界面 这个游戏有一个主界面,界面上有几个按钮,还有一个文本框,用来输入用户计算用 到的表达式。主界面还有一个用来显示当前4张扑克牌的面板。这里我们将编写几个类,分 别用来组成整个游戏。和第2章讲解的顺序一样,我们开始先不处理任何事件,仅仅编写界 面。 编写一个用来显示图片的 Panel 我们先编写一个用来显示图片的Panel类。这个类从Panel类中继承。在JBuilder的主菜 单中选择File→New→General选项卡→Panel,点击OK按钮,进入Panel向导界面。在这个对 话框中改写Panel的名字为PicPanel,同时注意基类一定为java.awt.Panel,如果不是则加以改 正,然后点击OK按钮,PicPanel的代码框架就生成了,代码如下: package cal24; 第 3 章 持有对象与异常处理 91 import java.awt.*; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 */ public class PicPanel extends Panel { BorderLayout borderLayout1 = new BorderLayout(); public PicPanel() { try { jbInit(); } catch(Exception ex) { ex.printStackTrace(); } } void jbInit() throws Exception { this.setLayout(borderLayout1); } } 我们要准备14张图片,这些图片是A~K的扑克牌的正面图片和一张背面图片。为了在 这个面板上画出这些图片,我们必须要装载这些图片。装载图片的方法同第2章一样。在 PicPanel中JBuilder自动生成的框架代码基本无用,将它替换为下面给出的代码。 这个类的完整代码如下: package cal24; import java.awt.*; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 */ class PicPanel extends Panel{ final int IMG_SIZE=100; Image[] m_img=new Image[14]; Cal24 m_cal24; 92 Java 游戏编程导学 public PicPanel(Cal24 cal24){ m_cal24=cal24; } public void initImg(){ URL url=null; try{ url=Class.forName("Cal24").getResource("pic/back.JPG"); } catch(Exception e){e.printStackTrace();} m_img[0]=getToolkit().getImage(url); for(int i=1;i<=13;i++){ try{ url=Class.forName("Cal24").getResource("pic/"+i+".JPG"); } catch(Exception e){} m_img[i]=getToolkit().getImage(url); } MediaTracker mt=new MediaTracker(this); for(int i=0;i<=13;i++){ mt.addImage(m_img[i],i); } try{ mt.wait(); mt.checkAll(); }catch(Exception e){} } public void paint(Graphics g){ for(int i=0;i<4;i++){ g.drawImage(m_img[m_cal24.m_nStatus[i]],i*IMG_SIZE+5,5, this); } } } 我们要用这个面板来显示图片,就应该重载它的paint()方法。当panel重画的时候会自 动调用这个paint()方法。只要在paint()方法里画上我们要画的东西就行了。 这里我们要在这个面板上画上4张扑克牌。画图的时候,使用了Graphics类的drawImage() 方法。 重载的paint()方法如下; public void paint(Graphics g){ for(int i=0;i<4;i++){ g.drawImage(m_img[m_cal24.m_nStatus[i]],i*IMG_SIZE+5,5,this); } } 这里的m_cal24是3.4.3节创建的Cal24的一个实例。我们通过m_nStatus数组来保存当前4 张牌的大小。画的时候,由m_nStatus数组的值来决定画哪张扑克牌。 第 3 章 持有对象与异常处理 93 m_cal24对象是在PicImg构造的时候传进去的。 Cal24 m_cal24; public PicPanel(Cal24 cal24){ m_cal24=cal24; } 主界面的设计 我们编写主界面的时候,仍然使用BorderLayout。这个布局很简单,关于它与其他的布 局的比较在后几章里将会详细讲到。主界面设计完成以后的UI设计器如图3.8所示。 图 3.8 UI 设计器 我们来具体讲解一下使用界面编辑器来设计界面的过程。编辑Java界面,首先需要确 定界面的布局管理器,然后在界面中添加合适的组件容器,最后将组件添加到容器里。 1. 确定界面的布局管理器。在前面已经提到,我们使用的是BorderLayout布局管理器。 打开界面,在编辑区域的右方的属性与事件编辑窗口中选择属性(Properties)选项 卡,将其中的layout选项改为BorderLayout,第一步工作就完成了。 2. 完成组件容器的添加。点击编辑区域上方的工具条,在其中选择Swing Containers 选项卡。在工具条中选择javax.swing.JPanel选项添加到界面中。然后在左下角的概 要浏览器中选择刚刚添加好的JPanel对象,在编辑区域右边的属性和事件窗口中, 将属性的第二项——constraints的值改为SOUTH,就会发现它自动移到了界面的下 部。组件容器添加完成。 3. 向组件容器中添加按钮。留心一下我们开始的介绍就会发现,在这里我们需要添加 4个组件,两个按钮“开始游戏”和“确定”、一个标签“请输入表达式”和一个 文本框。选择工具条中的Swing选项卡,在其中选择javax.swing.JButton添加到JPanel 94 Java 游戏编程导学 容器对象JPanel1中,然后在属性和事件编辑窗口选定属性选项卡,将其中的name 选项改为Start,text选项改为“开始游戏”,“开始游戏”按钮就添加好了。同样 的方法添加“确定”按钮,相应的name选项改为OK,text选项改为“确定”即可。 4. 向组件容器中添加文本框和标签。在工具条中选择javax.swing.JLabel选项加入容器, 将它的text属性选项改为“请输入表达式”。然后选择javax.swing.JTextField选项加 入容器,将它的text属性删除即可,如果组件布局和我们想要的布局位置不同,可 使用鼠标将它拖动到合适的位置。打开代码编辑窗口可以发现,JBuilder已经自动 生成了如下代码: package cal24; import java.awt.*; import java.awt.event.*; import java.applet.*; import javax.swing.*; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 */ public class Cal24 extends Applet { private boolean isStandalone = false; JPanel jPanel1 = new JPanel(); BorderLayout borderLayout1 = new BorderLayout(); JButton OK = new JButton(); JButton Start = new JButton(); JTextField jTextField1 = new JTextField(); JLabel jLabel1 = new JLabel(); int[] m_nStatus = new int[4]; //applet的构造函数 public Cal24() { } //初始化applet public void init() { try { jbInit(); } catch (Exception e) { e.printStackTrace(); } } 第 3 章 持有对象与异常处理 95 //组件初始化 private void jbInit() throws Exception { this.setLayout(borderLayout1); OK.setText("确 定"); Start.setText("开始游戏"); jTextField1.setPreferredSize(new Dimension(57, 22)); jTextField1.setText(""); jTextField1.setColumns(15); jLabel1.setText("请输入表达式"); this.add(jPanel1, BorderLayout.SOUTH); jPanel1.add(Start, null); jPanel1.add(jLabel1, null); jPanel1.add(jTextField1, null); jPanel1.add(OK, null); } } 如果这个时候进行编译,结果还不是我们想要的那个样子,因为还没有添加扑克牌的 图片,而且Applet的大小和位置也不是我们想要的,首先要在jbInit()函数中添加如下代码来 控制Applet的大小及是否可见: //设定大小 this.setSize(400, 200); //设定显示为真 this.setVisible(true); 接着要添加已经编辑好的PicPanel对象,首先需要添加全局变量pTop。 PicPanel pTop = new PicPanel(this); 然后要把这个PicPanel的对象添加到界面中,也就是说在jbInit()函数中加入如下代码: //使得pTop对象加载图像 pTop.initImg(); //将pTop加入界面中 this.add(pTop, BorderLayout.CENTER); 我们还需要在游戏初始化的时候,对4张扑克牌的状态进行初始化。Applet在初始化的 时候,会自动调用init()方法,那么我们只要重载Applet的init()方法就可以了。代码如下: for(int i=0;i<4;i++){ m_nStatus[i]=-1; } 这个时候进行程序编译,结果如图3.9所示。此时的界面还不能对我们的输入做出反应, 因为还没有给它添加逻辑代码。 96 Java 游戏编程导学 图 3.9 编辑好的界面 编写告诉用户所输入表达式正确与否的对话框 我们在这里只给出对话框的代码,这个对话框比较简单,和前面的对话框的编写方法 是一样的,在这里就不赘述。 package cal24; import java.awt.*; import java.awt.event.*; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 */ public class MsgDlg extends Frame { Panel panel1 = new Panel(); Panel panel2 = new Panel(); Label label1 = new Label(); Button ok = new Button(); public MsgDlg() { } public MsgDlg(String msg){ label1.setText("msg"); ok.setLabel("确定"); ok.addActionListener(new MsgDlg_ok_actionAdapter(this)); this.add(panel1, BorderLayout.CENTER); 第 3 章 持有对象与异常处理 97 panel1.add(label1, null); this.add(panel2, BorderLayout.SOUTH); panel2.add(ok, null); this.setSize(200,100); this.setLocation(300,200); this.setVisible(true); } void ok_actionPerformed(ActionEvent e) { this.dispose(); } } class MsgDlg_ok_actionAdapter implements java.awt.event.ActionListener { MsgDlg adaptee; MsgDlg_ok_actionAdapter(MsgDlg adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.ok_actionPerformed(e); } } 3.4.5 增加对表达式处理的方法 这个游戏需要用户在求得自己的答案之后,以表达式的形式输入到一个TextField中。 然后对这个表达式进行处理。 下面开始编写判断表达式是否合法且在合法的时候计算出它的值的算法。这个算法比 较复杂,读者可以不必纠缠于它的细节,只要能够弄明白这个方法中用到的一些对字符串 处理和对数组处理的方法和技巧就可以了。 我们先编写两个辅助方法,这两个辅助方法用来判断一个字符串是否是一个数字和是 否是一个运算符(+,-,*,/)。 代码如下: private boolean isNumber(String str){ if(str.equals("0")||str.equals("1")|| str.equals("2")||str.equals("3")|| str.equals("4")||str.equals("5")|| str.equals("6")||str.equals("7")|| str.equals("8")||str.equals("9")){ return true; } else{ return false; } } 98 Java 游戏编程导学 private boolean isOperator(String str){ if(str.equals("+")||str.equals("-")|| str.equals("*")||str.equals("/")){ return true; }else{ return false; } } 下面我们开始编写对表达式处理的方法,这个方法的返回类型为int。当表达式为非法 的时候(如4张扑克牌分别为1,2,3,4,而用户输入表达式1+3+5+7,则表达式为非法表达式), 返回-1。否则返回这个表达式的值。在方法中,有相关的注释说明各代码的作用。 代码如下: public int calString(String str){ /** 判断表达式的合法性 **/ int[] nums=new int[4]; String[] ops=new String[4]; for(int i=0;i<4;i++){ ops[i]=""; } String tempStr,s=""; int numNo=0,opNo=0; for(int i=0;i=4 || opNo>=3)return -1; try{ nums[numNo]=Integer.parseInt(s); }catch(Exception e){} s=""; numNo++; ops[opNo]=tempStr; opNo++; }else{ return -1; } } if(s.length()!=0 && numNo==3){ try{ nums[numNo]=Integer.parseInt(s); }catch(Exception e){ return -1; } 第 3 章 持有对象与异常处理 99 } else return -1; /** *判断表达式的数字就是扑克牌上的数字 *即验证合法性 **/ int tempStatus[]=new int[4]; for(int i=0;i<4;i++){ tempStatus[i]=m_nStatus[i]; } for(int i=0;i<4;i++){ int j=0; boolean existed=false; while(j<4 && !existed){ if(tempStatus[j]==nums[i]){ tempStatus[j]=-1; existed=true; } j++; } if(!existed)return -1; } /** 计算表达式的值 **/ int result=nums[0]; for(int i=0;i<3;i++){ if(ops[i].equals("+")){ result+=nums[i+1]; }else if(ops[i].equals("-")){ result-=nums[i+1]; }else if(ops[i].equals("*")){ result*=nums[i+1]; }else if(ops[i].equals("/")){ result/=nums[i+1]; } } return result; } 3.4.6 添加对Applet中按钮的事件处理 添加对“确定”按钮的事件处理 在点击“开始游戏”按钮的时候,随即产生4张牌,然后发出这4张牌,即在界面上画 出这4张牌。在点击“确定”按钮的时候,对用户输入的表达式进行计算,然后弹出对话框 告诉玩家输入的表达式是否正确。 在这里使用JBuilder中的UI编辑器对事件进行处理,这样可以大大简化我们的工作。切 100 Java 游戏编程导学 换到界面编辑器窗口,在左下角的概要浏览器中选择OK按钮,然后在右边的属性和事件编 辑窗口中进行事件编辑。点击编辑窗口的Events选项卡,双击actionPerformed选项进入代码 编辑器,JBuilder已经为我们做好了一切工作并将光标定位在事件响应函数处,我们只需添 加需要的代码就可以了。 代码如下: try { int result = calString(jTextField1.getText().trim()); if (result == -1) { jTextField1.setText(""); jTextField1.requestFocus(); new Msg("你输入的表达式不合法,请重新输入!"); } else { if (result != 24) { jTextField1.setText(""); jTextField1.requestFocus(); new Msg("你输入的表达式的值为" + result + ",请重新输入!"); } else { if (result == 24) { jTextField1.requestFocus(); new Msg("祝贺你,你的输入正确!"); for (int i = 0; i < 4; i++) { m_nStatus[i] = (int) (Math.random() * 13) + 1; } pTop.repaint(); jTextField1.setText(""); jTextField1.requestFocus(); } } } }catch (Exception ex) { ex.printStackTrace(); } 让我们仔细观察一下代码,看看JBuilder为我们做了哪些工作。首先,JBuilder在组件 构造函数jbInit()里面添加了代码: //添加监听器 OK.addActionListener(new Cal24_OK_actionAdapter(this)); 其次,JBuilder还为我们添加好了事件的适配器代码,就是说定义了上面函数 addActionListener()中的参数的类型。 //事件适配器类 class Cal24_OK_actionAdapter implements java.awt.event.ActionListener { Cal24 adaptee; Cal24_OK_actionAdapter(Cal24 adaptee) { 第 3 章 持有对象与异常处理 101 this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.OK_actionPerformed(e); } } 添加对“开始游戏”按钮的事件响应 和上面提到的方法一样,我们添加对“开始游戏”按钮的事件响应,事件响应函数代 码如下: for (int i = 0; i < 4; i++) { m_nStatus[i] = (int) (Math.random() * 13) + 1; } pTop.repaint(); 这样我们的软件就编写完成了,鉴于篇幅的关系,在这里不给出完全的源代码,具体 的代码请读者参见附录光盘。 3.4.7 进一步实践 读者可以尝试着给这个游戏加上计时器,这样可以给玩家带来一种紧迫感,使游戏变 得更有挑战性。计时器的编写涉及到线程的知识,我们将在后续章节进行讲解。 3.5 本章知识点回顾 Collection接口 Boolean add(Object obj) 将对象obj加入到调用集合中。如果加入成功 则返回true;如果obj已经在集合中或者集合 不能复制时则返回false Boolean addAll(Collection c) 将集合c中的所有元素都加入到调用集合 中,操作成功则返回true;否则返回false void clear() 从调用集合中删除所有元素 Boolean contain(Object obj) 如果obj是调用集合中的一个元素,则返回 true;否则返回false Boolean containAll(Collection c) 如果调用集合中包含了集合c中的所有元 素,则返回true;否则返回false Boolean equals(Object obj) 如果调用集合与obj相等,则返回true;否则 返回false Int hashCode() 返回调用集合的散列码 102 Java 游戏编程导学 (续表) Boolean isEmpty() 如果调用集合是空的,则返回true;否则返 回false Iterator iterator() 返回调用集合的迭代程序 Boolean remove(Object obj) 从调用集合中删除obj的一个实例,如果删除 成功则返回true;否则返回false Boolean removeAll(Collection c) 从调用集合中删除c的所有元素。如果删除 成功则返回true;否则返回false Boolean retainAll(Collection c) 删除调用集合中除了包含c中元素之外的全 部元素,如果删除成功则返回true;否则返 回false Int size() 返回调用集合中元素的个数 Object[] toArray() 返回一个数组,该数组包含了所有存储在调 用集合中的元素。数组元素是集合元素的拷 贝 Object[] toArray(Object array[]) 返回一个数组,该数组仅仅包含了那些类型 与数组元素类型匹配的集合元素 Array类 Array类扩展AbStractList并执行List接口,本 质上,ArrayList是对象引用的一个变长数组 Boolean add(Object obj) 给数组添加元素 Boolean remove(Object obj) 从数组删除元素 void ensureCapacity(int cap) 人工规定数组的大小,这样可以提高效率 void trimToSize() 人工减小数组大小,以便正好容纳当前项 常用的扩展List接口的 类 LinkedList类 LinkedList扩展AbstractSequentialList并执行 List接口,它提供了一个链接列表结构,可 以在头部和尾部增加及删除元素 void addFirst(Object obj) 可以在列表头部增加元素 void addLast(Object obj) 可以在列表尾部增加元素 Object getFirst() 可以获得第一个元素 Object getLast() 可以得到最后一个元素 Object removeFirst() 可以删除第一个元素 Object removeLast() 可以删除最后一个元素 HashSet类 HashSet类扩展AbstractSet并实现Set接口。它 创建一个使用散列表进行存储的集合。可以 对于比较大的集合进行基本操作 HashSet() 构造默认散列集合 常用的扩展Set接口的 类 HashSet(Collection c) 使用集合c中的元素初始化散列集合 第 3 章 持有对象与异常处理 103 (续表) HashSet(int capacity) 使用整数capacity初始化散列集合的容量 HashSet(int capacity, float fillRatio) 使用capacity初始化散列集合的容量,同时使 用fillRatio决定填充比,填充比介于0.0~ 1.0 之间 void clear() 从调用映射中删除所有的关键字/值对 Boolean containsKey(Object k) 如果调用映射中包含了作为关键字的k,则 返回true;否则返回false Boolean containsValue(Object v) 如果映射中包含了作为值的v,则返回true; 否则返回false Set entrySet() 返回包含了映射中项的Set Boolean equals(Object obj) 如果obj是一个Map,并且包含相应的输入, 则返回true;否则返回false Object get(Object k) 返回与关键字k相关联的值 Int hashCode() 返回调用映射的散列码 Boolean isEmpty() 如果调用映射是空的则返回true;否则返回 false Set keySet() 返回一个包含调用中关键字的集合 Object put(Object k,Object v) 将一个输入加入调用映射,覆盖原先与该关 键字相关联的值。关键字和值分别为k和v。 如果关键字不存在,则返回null;否则返回 原先与关键字相关联的值 void putAll(Map m) 将所有来自m的输入加入调用映射 Object remove(Object k) 删除关键字等于k的输入 Int size() 返回映射中关键字/值对的个数 Map接口 Collection values() 返回一个包含了映射中值的类集 HashMap类 HashMap使用散列表实现Map接口,这可以 保证对于一些大型集合的函数运行时间 HashMap() 构造默认散列映射 HashMap(Map m) 使用m中的元素初始化散列映射 HashMap(int capacity) 使用整数capacity初始化散列映射的容量 常用的扩展Map接口 的类 HashMap(int capacity, float fillRatio) 使用整数capacity初始化散列映射的容量, 同时使用fillRatio决定填充比,这个数值介 于0.0~1.0之间,默认值为0.75 104 Java 游戏编程导学 (续表) 异常的产生 if(t == null){ //假定的产生异常的条件 }throws new NullPointer- Exception(); 异常的捕获 try { // 可能产生异常的代码 }catch(Type1 id1) { // 捕获第一种类型的异常 } catch(Type2 id2) { //捕获第二种类型的异常 } catch(Type3 id3) { //捕获第三种类型的异常 } 异常处理机制 “中断”和“恢复”,不推荐使用异常处理 机制来控制程序逻辑 异常处理 异常的重新抛出 catch(Exception e) { System.out.println("一个异常 已经产生"); throw e; } 第 4 章 Java 编程深入——图像与多媒体 这一章我们将简单介绍一下AWT,AWT支持Applet(小应用程序)和独立运行的GUI, 通过它我们可以生成和管理窗口、管理字体、使用控件和使用图像,同时我们还将简单介 绍一下Java事件管理机制。然后通过“精彩闹钟”和“钢琴”两个实例使读者加深对AWT 的理解。 4.1 AWT简介 AWT类定义在java.awt包中,这是Java中最大的包之一。这个包采用的是从上到下的分 层结构,所以理解和使用起来并不难。 AWT根据类的层析定义结构,并在每一层添加了特定的功能。在这些窗口中,用得最 多的是在小应用程序中派生于Panel类的窗口和派生于Frame类的独立窗口。因此,与Panel 和Frame这两个类相关的类结构的描述是我们理解它们的基础。现在分别来看一下这些类。 组件(Component) Component类位于AWT类层次结构的顶部,是一个封装了可视化组件所有属性的抽象 类。在屏幕上显示的所有用语和用户交互的用户界面元素都是Component类的子类。 容器(Container) Container类是Component类的子类。这个类允许别的Component对象嵌套在Container类 的对象中,这就形成了一个多层包容机制。容器通过一些设计管理器来完成布置组件位置 的功能。 面板(Panel) Panel类是Container类的子类,它只是简单地实现了Container类。一个Panel对象可以看 作是一个嵌套递归的具体屏幕组件。当屏幕输出直接传递给一个小应用程序时,它将在一 个Panel对象的表面被画出。实际上,一个Panel对象是一个不包含标题栏、菜单栏的边框的 窗口。你可以调用add()方法将其余组件加入Panel对象中,当加入以后,就可以调用 setLocation()、setSize()和setBound()来改变这些组件的位置和大小。 窗口(Window) Window类产生一个顶级窗口。窗口对象不出现在任何别的对象中,而是直接出现在桌 面上。一般我们会直接使用Window类的子类Frame类。 106 Java 游戏编程导学 框架(Frame) Frame类封装了窗口通常所需要的一切组件,它是Window类的子类,并且拥有标题栏、 菜单栏、边框以及可以调整大小的角。如果在一个小应用程序中创建了一个Frame对象,它 将包含一个警告消息(如Java Applet Window)给用户。当一个Frame窗口被应用程序创建 时,就获得了一个普通窗口。 在这里我们并不打算对AWT的控件进行详细介绍,因为目前这一部分的功能基本上已 经被Swing包取代,下面主要对AWT中的布局管理器(Layout)和图像(Image)包进行详 细介绍,这也是在后面的例子中需要用到的。 4.2 布局管理器 在Java里布局管理的方法是安装一个组件到一个窗体中去,它不同于我们使用过的其 他GUI系统。 1. Java中布局管理的方法是全代码的,没有控制安放组件的“资源”。 2.组件被安放到一个被布局管理器控制的窗体中,根据我们add()这些组件的决定,由 布局管理器来安放组件的大小、形状和组件位置。 3.布局管理器使我们的程序片或者应用程序适合窗口的大小。所以,如果窗口的尺寸 改变,组件的大小、形状和位置都会改变。例如,在HTML页面的程序片指定Applet 的规格,如下: 前面已经介绍过,的标签里面的两个属性width和height指定了Applet在 HTML页面中的窗口尺寸,如果width和height两个属性发生变化,Applet中的组件的大小、 形状和位置就会自动发生变化。读者有兴趣的话可以自己试一试。 4.2.1 FlowLayout 这是Java的默认布局管理器。FlowLayout实现了一种简单的布局风格,类似于在一个 文本编辑器中文字的流动方式,组件从左上角开始,按从左到右,从上到下的方式布置。 FlowLayout的构造函数有以下3种: FlowLayout() FlowLayout(int how) FlowLayout(int how,int horz,int vert) 第一种形式生成默认的布局,第二种形式可以设定每一行组件的对齐方式,how可以 选择FlowLayout.LEFT、FlowLayout.CENTER和FlowLayout.RIGHT。第三种形式还可以设 定组件间的水平距离horz和垂直距离vert。 FlowLayout组件使用组件本来的大小。例如一个按钮将会变得和它的字串符一样的大 第 4 章 Java编程深入——图像与多媒体 107 小。回想一下前面的例子,一个“确定”按钮的大小只比作为它的标签的两个字稍大一点, 这就是采用FlowLayout的结果,所有组件将在FlowLayout 中被压缩为它们的最小尺寸。 4.2.2 BorderLayout 布局管理器有四边和中间区域的概念。它在边缘设定4个狭窄的定宽控件,在中间为一 个大的区域,为北、南、西、东和中央。构造函数有下面两种: BorderLayout() BorderLayout(int horz,int vert) 第一种形式生成默认的BorderLayout管理器,第二种形式允许你设定组件间的水平距离 horz和垂直距离vert。 当我们增加一些事物到使用BorderLayout 的面板上时,必须使用add()方法将一个字符 串对象作为它的第一个自变量,并且字符串必须指定正确的大写BorderLayout.NORTH、 BorderLayout.SOUTH 、 BorderLayout.WEST 、 BorderLayout.EAST 或者BorderLayout. CENTER。下面是一个简单的程序例子片断: setLayout(new BorderLayout()); add( new Button("Button1”),BorderLayout.NORTH); add( new Button("Button2"),BorderLayout.WEST); add( new Button("Button3"),BorderLayout.EAST); …… 除了中央的每一个位置,当元素在其他空间内扩大到最大时,我们会把它压缩到适合 空间的最小尺寸。但是,中央部分扩大后只会占据中心位置。 BorderLayout是应用程序和对话框的默认布局管理器。 4.2.3 GridLayout GridLayout允许我们建立一个组件表,就好像在一个二维的网格布置组件。添加那些 组件时,它们会按从左到右、从上到下的顺序在网格中排列。在构建器里,需要指定自己 希望的行、列数,它们将按正比例展开。 4.2.4 CardLayout CardLayout存储了几个不同的布局管理器,每个布局管理器具有一个索引,就好像一 叠扑克牌中,总有一张牌在给定的时间内位于这叠纸牌的顶层。这样,在用户与组件进行 交互的时候,这些控件可以根据用户的输入而动态启用或者禁用。我们可以创建其他的布 局管理器并隐藏,以便在需要的时候激活。 使用CardLayout比其他的布局管理器需要多做一些工作。布局管理器一般存放在Panel 类的一个对象中,这个面板必须使用卡片布局管理器,形成纸牌的其他布局管理器通常也 是Panel的对象。也就是说,要生成一个包含一副牌的面板,以及向这个面板中加入这副牌 中每张牌的面板组成一个布局管理器。 108 Java 游戏编程导学 这个管理器在我们创建复杂的应用程序时比较有用。 4.3 图 像 简 介 本节我们来介绍AWT的Image类和java.awt.image包,它们为图像的显示和操作提供了 支持。首先我们需要了解一下图像的文件格式,这对于后面的学习非常有帮助。 4.3.1 文件格式 最初,网页图像使用GIF一种格式。这种GIF图像格式使得图像可以在线浏览,非常适 合于Internet。每个GIF图像至多只能有256种颜色,这是一个很受限制的方面,促使主要浏 览器厂商在1995年增加了对JPEG图像的支持。JPEG格式只要被正确生成,就可以比同一源 图像编码生成的GIF图像具有更好的压缩比和精度。Java图像类可以抽象出所有接口之间的 差异,所以一般情况下,你不必关心你在程序中使用了何种格式。 4.3.2 图像的创建、加载和显示 对图像进行的3种最常见的操作为:创建图像、加载图像和显示图像。在Java类中,Image 类用来指向内存中的图像和那些必须从外部资源加载的图像。因此,Java为你提供了创建 新的图像对象并加载它的方法,它也提供了显示图像的方法。 创建一个图像对象 Java.awt的Component类有一个叫做createImage()的方法用来生成图像对象。我们并不 使用Image的构造函数创建图像对象,因为图像最终是要画在窗口中进行查看,而Image类 并没有足够的环境信息来为屏幕生成合适的数据对象。 createImage()方法有如下两种形式: Image createImage(ImageProducer imgProd) Image createImage(int width,int height) 第一种形式返回由imgProd产生的图像,这是一个实现ImageProducer接口的对象。另一 种形式返回指定宽度和高度的空图像,如下例: Canvas c = new Canvas(); Image test = c.createIamge(200,100); 这里首先生成一个Canvas实例,然后调用createImage()方法来实际生成一个Image对象。 在这个例子中,图像是空白的,后面我们会逐步介绍如何写入数据。 加载图像 我们也可以通过加载的方式获得图像。这可以通过使用Applet类定义的getImage()方法 实现,它拥有以下形式: 第 4 章 Java编程深入——图像与多媒体 109 Image getImage(URL url) Image getImage(URL url , String imageName) 这两种形式都通过参数url所设定的路径来寻找图像,将其载入一个Image类的对象并且 返回。后一种形式还将这个返回的图像对象命名为imageName。这种加载的方法在实际中 更为常用。 显示图像 只要你有一个图像,就可以使用drawImage()方法来显示它,drawImage()方法是Graphics 类中的一员。最常用的一种形式如下: boolean drawImage(Image imgObj ,int left,int top,ImageOBserver imgOb) 它显示了由imgObj所传递的图像,其左上角的位置由left和top指定。imgOb是一个实现 了ImageObserver接口的类的引用。这个接口被所有的AWT组件所实现,一个image observer 对象可以在图像加载的时候对其进行监控。 下面的程序片段加载了图像并对其进行显示: public void init(){ …… Image img = getImage(getDocumentBase(),getParameter("img”)); …… } public void paint(Graphics g){ …… g.drawImage(img,0,0,this); …… } init()是程序的初始化参数,一般会在构造函数中调用,我们在这里创建一个Image对象 img并加载图像,然后在paint()函数中将它画出,paint()函数会在屏幕画出窗口的时候调用。 这里我们画出了img对象,图像的左上角的坐标是窗口的(0,0)点。 使用这种方法,只要图像被完整地加载,就可以简单地在屏幕上立即显现。你可以一 边使用其他信息在屏幕上画图,同时使用ImageObserver来监控图像的加载,如果你利用加 载图像的时间去并行完成其他的事情可能会更好,这涉及到线程的知识,我们将在后面的 章节中简要介绍。 4.3.3 ImageObserver ImageObserver是一个接口,当图像被生成时又来接收消息。它仅仅定义了一种方法: imageUpdate()。当图像通过网络加载的时候,这种消息非常有用。下面是ImageUpdate()方 法的一般形式: boolean imageUpdate(Image imgObj,int flags, int left,int top,int width,int height) 这里参数imgObj是被加载的图像,参数flags是表示更新状况的整数。整数left、top、 110 Java 游戏编程导学 width和height代表一个矩形,表示加载图像的位置和大小。如果ImageUpdate()完成了加载, 它将返回false,如果还有图像要处理,将返回true。 表4.1列出了flags参数可以取的、在ImageObserver中定义的静态位标志。 表4.1 flag参数的静态位标志 标识 意义 WIDTH 表示图像的宽度 HEIGHT 表示图像的高度 PROPERTIES 表示与图像相关的属性,可以通过img.getProperty()获得 SOMEBITS 用于画图的像素已经收到,参数left、right、width和height定义了包含新像素的矩形 FRAMEBITS 以前所画的一个多帧图像中的完整一帧已经收到,这个帧可以被显示 ALLBITS 图像已经完成 ERROR 在异步跟踪图像时发生错误,图像未完成,不能显示。设置ABORT,表示图像生成被 异常中止 Applet类中有一个为ImageObserver接口所实现的方法imageUpdate(),用来重画被加载 的图像。你可以在你的类中覆盖此方法来改变它的操作。下面是一个简单的例子片断: public boolean imageUpdate(Image img,int flags,int x,int y,int w,int h){ if((flags & ALLBITS) == 0){ System.out.println("Still Processing the image. "); return true; }else{ System.out.println("Done processing the image. "); return false; } } Applet中imageUpdate()的默认的实现有几处问题。首先,每次新数据到来的时候它都 要重画整个图像,这就造成了背景颜色与图像之间的闪动,看起来屏幕在很频繁地闪烁。 其次,它使用Applet.repaint()方法来保证系统每1/10秒左右重画一次图像,这就造成了一种 急促、不均匀的感觉,图像的色彩看起来也不好。最后,默认的实现对可能正常加载失败 的图像一无所知。即使设定的图像不存在,getImage()方法也会返回成功,所以在 imageUpdate()出现以前,你不会发现缺少图像。如果你使用imageUpdate()的默认实现,那 你决不会知道究竟发生了什么。 下面我们就使用一个程序片段,说明改正imageUpdate()方法的问题。 public boolean imageUpdate(Image img,int flages, int x,int y,int w, int h){ if( ( flags & SOMEBITS ) != 0 ){ //新的实际数据 repaint( x,y,w,h ); //画出新的维度 }else if( ( flags & ABORT ) != 0 ){ error = true; //文件没发现 repaint(); //画出完整的applet }return( flags & ( ALLBITS|ABORT ) ) == 0; 第 4 章 Java编程深入——图像与多媒体 111 } 上面的程序片段解决了出现的3个问题。首先,我们使用了repaint()方法的一个实现, 来确定一个矩形以确定需要重画的范围,这就减少了重画引起的闪烁。其次,它可以消除 引入图像的不均匀的显示。最后,它控制了由于找不到文件而造成的错误,就是为ABORT 位检查flags参数,如果已经设定,error就被设置为true,接着调用repaint(),我们在paint() 方法中加入控制,就可以显示出错信息。下面是paint()方法的示例显示: public void paint(Graphics g){ if(error){ } } 还有一种方法如下,改写imageUpdate(),使得图像完全装入以后,才会重画到屏幕上。 public boolean imageUpdate(Image img,int flags, int x,int y,int w,int h){ if((flags & ALLBITS) != 0){ repaint(); }else if((flags & (ABORT| ERROR)) != 0 ){ error = true; //文件没有发现 repaint(); } return ( flags & (ALLBIT|ABORT|ERROR)) == 0; } 4.3.4 MediaTracker 在此我们再回到getImage()方法。Java的getImage()方法采用异步装入的技术。就是当程 序执行到getImage()方法时,计算机不是在这个方法上停留并装入图像,而是立即去执行 getImage()下面的代码。它只是在图像对象和图像文件之间建立一个关联,直到被特别请求 或者要使用图像对象时,图像的装入才会放到后台去执行。这样会存在一个问题,如果图 像不存在,或者当需要图像对象时图像却没有完全装入,怎么办?Java提供了一个 MediaTracker对象,可以用来监视图像的装入,并能够在图像没有成功装入时抛出异常, 告诉你出现了错误信息。 MediaTracker提供的主要方法如表4.2所示。 表4.2 MediaTracker的主要方法 返回类型 方法 用途 MediaTracker(Component comp) 构造方法,为指定的组件comp 创建一个 MediaTracker对象 void addImage(Image image, int id) 将图片加入到MedialTracker的监视队列中去, image为要被监视的图像对象,id为监视图像 在监视队列中的标识号 112 Java 游戏编程导学 (续表) 返回类型 方法 用途 void AddImage(Image image,int id,int w,int h) 将图片加入到MediaTracker的监视队列中去, w为所监视的图像对象的宽度,h为所监视的 图像对象的高度 boolean CheckAll() 检查所有被监视的图像对象是否已经完成了 装载过程。如果所有图像对象已经完成了装 载,则返回true,否则为false boolean CheckAll(Boolean load) 检查所有被监视的图像对象是否已经完成了 装载过程。如果load为true,则开始装载那些 还没有被装载的图像 boolean CheckId(int id) 检查在监视队列中所有标识号为id的图像对 象是否已经完成了装载过程,如果已经完成了 装载过程,返回true boolean CheckId(int id,Boolean load) 检查在监视队列中所有标识号为id的图像对 象是否已经完成了装载过程,如果load为true, 且在监视队列中的标识号为id的图像没有被 装载的话,则开始装载此图像对象 boolean IsErrorId(int id) 检查在监视队列中标识号为id的图像对象是 否装载有错误,如有错误,则返回true void RemoveImage(Image image) 从监视队列中移去指定的图像对象image void RemoveImage(Image image,int id) 从监视队列中移去指定id的图像对象image void RemoveImage(Image image,int id,int width,int height) 从监视队列中移去指定id的图像对象image, 且这个图像对象指定了大小 void WaitForAll() 开始装载监视队列中所有没有被装载的图像 对象,如果装载不成功,则会抛出一个异常 boolean WaitForAll(long ms) 开始装载监视队列中所有没有被装载的图像 对象,但是如果装载时间超时,则会取消装载, 并返回false void WaitForId(int id) 开始装载监视队列中标识号为id的图像对象, 如果装载不成功,则会抛出一个异常 boolean WaitForId(int id,long ms) 开始装载监视队列中标识号为id的图像对象, 但是如果装载时间超时,则会取消装载,并返 回false 4.3.5 Graphics类 Graphics类在我们画出界面的时候经常用到,Graphics类的主要方法如表4.3所示。 第 4 章 Java编程深入——图像与多媒体 113 表4.3 Graphics类的主要方法 方法 用途 drawLine(int x1, int y1, int x2, int y2) 从坐标(x1,y1)到(x2,y2)画一条直线,返回值类型为void drawOval(int x, int y, int width, int height) 以(x,y)为左上角,画一个宽width、高height的椭圆,返回 值类型为void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) 以xPoints的项为各节点的x坐标,yPoints的项为各节点的y 坐标画多边形。节点数为nPoints,返回值类型为void drawPolygon(Polygon p) 画多边形。多边形的信息由p来描述,返回值类型为void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) 以xPoints的项为各节点的x坐标,yPoints的项为各节点的y 坐标画相互连接的多条线。节点数为nPoints,返回值类型 为void drawRect(int x, int y, int width, int height). 画以坐标(x,y) 为左上角、宽为width、高为height的矩形。 返回值类型为void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) 画以坐标(x,y) 为左上角、宽为width、高为height的矩形, 矩形的角是圆弧,圆弧的宽为arcWidth,高 为 arcHeight。返 回值类型为void drawString(AttributedCharacterIterator iterato r, int x, int y) 在坐标(x,y)处画出字符串。字符串的值和属性在iterator里 定义。返回值类型为void drawString(String str, int x, int y) 在坐标(x,y)处画出字符串str。返回值类型为void fill3DRect(int x, int y, int width, int height, boolean raised) 填充以坐标(x,y) 为左上角、宽为width、高为height的三维 矩形。若raised为true,则矩形是突出显示的,否则为凹下 去的矩形,返回值类型为void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) 填充以坐标(x,y) 为左上角、宽为width、高为height、弧度 为arcAngle的圆弧区域。返回值类型为void fillOval(int x, int y, int width, int height) 填充以(x,y)为左上角、宽为width、高为height的椭圆区域, 返回值类型为void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) 填充以xPoints的项为各节点的x坐标,yPoints的项为各节点 的y坐标的多边形区域。多边形的节点数为nPoints,返回值 类型为void fillPolygon(Polygon p) 填充多边形区域。多边形的信息由p来描述,返回值类型为 void fillRect(int x, int y, int width, int height) 填充以坐标(x,y) 为左上角、宽为width、高为height的矩形 区域。返回值类型为void fillRoundRect(int x, int y, int width, int height,int arcWidth, int arcHeight) 填充以坐标(x,y) 为左上角、宽为width、高为height的矩形 区域,矩形的角是圆弧,圆弧的宽为arcWidth, 高为 arcHeight。返回值类型为void getColor() 得到图的当前颜色。返回值类型为Color getFont() 得到图的当前字体。返回值类型为Font setColor(Color c) 设置图的当前颜色。返回值类型为void setFont(Font font) 设置图的当前字体。返回值类型为void 114 Java 游戏编程导学 4.4 事 件 处 理 本节我们介绍Applet的一个重要方面:事件,因为Java小应用程序是基于事件驱动的。 事件处理识别那些由鼠标、键盘和按钮等各种控件触发的事件,这些事件在java.awt.event 包中提供。下面我们首先介绍一下Java的事件处理机制。 4.4.1 事件处理机制 现在处理事件的方法是基于授权事件模型(delegation event model)的,这些模型定义 了标准一致的机制去产生和处理事件。授权事件模型的概念十分简单:一个源(source)产 生一个事件(event)并把它送到一个或多个监听器(listener)那里。在这种方案中,监听 器简单地等待,直到它收到一个事件,一旦事件被接受,监听器将处理这些事件,然后返 回。但是监听器一定要进行注册,以避免接受不相关的消息。 事件 在授权事件模型中,一个事件是描述一个事件源状态改变的对象,它可以作为一个人 机交互的结果而产生。比如点击一下按钮、点击一下鼠标都会产生一个事件。但事件可能 也不是由于用户接口交互而产生,例如一个计数器超值溢出也可以产生一个事件。你可以 自由定义一些适用于你的应用程序的事件。 事件源 一个事件源是一个产生事件的对象。当这个对象内部的状态以某种方式改变时,事件 就会产生。事件源可能产生不止一种事件。 一个事件源必须注册监听器以便监听器可以接受一个关于一个特定事件的通知。通用 的事件注册方法如下: public void addTypeListener(TypeListener el) 在这里,type是事件的名称,el是一个事件监听器的引用。例如,注册一个键盘事件监 听器的方法叫做addKeyListener()。当一个事件发生时,所有被注册的监听器都被通知并收 到一个事件对象的拷贝。在所有的情况下,事件通知只被送给那些注册接受它们的监听器。 一些事件源可能只允许注册一个监听器,这种方法的通用形式如下所示: public void addTypeListener(TypeListener el) throws java.util.TooManyListenersException 一些事件源必须也提供一个允许监听器注销一个特定事件的方法,通用形式如下: public void removeTypeListener(TypeListener el) 在这两个例子中,type是事件的名字而el是一个事件监听器的引用。这些增加或者删除 监听器的方法由产生事件的事件源提供。 第 4 章 Java编程深入——图像与多媒体 115 事件监听器 一个事件监听器是一个在事件发生时被通知的对象。它有两个要求。第一是必须在事 件源中注册,第二是必须实现接受和处理通知的方法。这种方法在java.awt.event和 javax.swing.event中被定义为一系列的接口。 4.4.2 事件类 Java事件处理机制的核心是代表这些事件的类,因此,我们从浏览事件类开始学习事 件处理,正如你看到的,他们提供一个一致而又易用的封装事件的方法。 在java.util中封装的EventObject类是Java事件类的根节点,它是所有事件类的父类。构 造函数如下,其中参数src是一个可以产生事件的对象。 EventObject(Object src) 此外它还包括两个方法:getSource()和toString(),我们可以使用getSource()方法返回事 件源,一般得到的是一个等价于事件的字符串。 针对EventObject类,AWT和Swing包分别衍生了不同的事件类。因为篇幅有限,这里 只描述java.awt.event包(见表4.4),但是两个事件包的机理是一样的。 表4.4 事件类及其描述 事件类 描述 ActionEvent 通常在点击一个按钮、双击一个列表项或者选中一个菜单项时发生 AdjustmentEvent 当操作一个滚动条时发生 ComponentEvent 当一个组件隐藏、移动、改变大小或者成为可见时发生 ContainerEvent 当一个组件从容器中加入或者删除时发生 FocusEvent 当一个组件获得或失去键盘焦点时发生 InputEvent 所有组件的输入事件的抽象超类 ItemEvent 当一个复选框或者列表框被点击时发生;当一个单选按钮或者菜单项被选择或者取 消时发生 KeyEvent 当输入从键盘获得时发生 MouseEvent 当鼠标被拖动、移动、点击、按下或者释放时产生;或者在鼠标进入或者退出一个 组件时发生 TextEvent 当文本区和文本域的文本改变时发生 WindowEvent 当一个窗口激活、关闭、失效、恢复、最小化、打开或者退出时发生 4.4.3 事件监听器接口 下面我们列表介绍一下对应4.4.2节的事件监听器接口(见表4.5)。当事件产生时,事 件源调用被监听器定义的相应的方法并提供一个事件对象作为参数,同时简要介绍一下它 们定义的方法。 116 Java 游戏编程导学 表4.5 事件监听器接口及其描述 接口 描述 ActionListener 定义了一个接受动作事件的方法 void actionPerformed(ActionEvent ae) AdjustmentListener 定义了一个接受调整事件的方法 void adjustmentValueChanged(AdjustmentEvent ae) ComponentListener 定义4个方法用来识别何时隐藏、移动、改变大小和显示组件 void componentResized(ComponentEvent ce) void componentMoved(ComponentEvent ce) void componentShown(ComponentEvent ce) void componentHidden(ComponentEvent ce) ContainerListener 定义了两个方法来识别何时从容器中加入或除去组件 void componentAdded(ContainerEvent ce) void componentRemoved(ContainerEvent ce) FocusListener 定义了两个方法来识别何时组件获得或失去焦点 void focusGained(FocusEvent fe) void focusLost(FocusEvent fe) ItemListener 定义了一个方法来识别何时项目状态改变 Void itemStateChanged(ItemEvent ie) KeyListener 定义了3个方法来识别何时按键按下、释放和键入字符事件 void KeyPressed(KeyEvent ke) void KeyReleased(KeyEvent ke) void KeyTyped(KeyEvent ke) MouseListener 定义5个方法来识别何时鼠标点击、进入组件、离开组件、按下和释放事 件 void mouseClicked(MouseEvent me) void mouseEntered(MouseEvent me) void mouseExited(MouseEvent me) void mousePressed(MouseEvent me) void mouseReleased(MouseEvent me) MouseMotionListener 定义了两个方法来识别何时鼠标拖动和移动 void mouseDragged(MouseEvent me) void mouseMoved(MouseEvent me) TextListener 定义了一个方法来识别何时文本值改变 void textChanged(TextEvent te) WindowListener 定义了7个方法来识别何时窗口激活、关闭、失效、最小化、还原、打开 和退出 void windowActived(WindowEvent we) void windowClosed(WindowEvent we) void windowClosing(WindowEvent we) void windowDeactivated(WindowEvent we) void windowDeiconified(WindowEvent we) void windowIconified(WindowEvent we) void windowOpened(WindowEvent we) 第 4 章 Java编程深入——图像与多媒体 117 4.5 “精彩闹钟” 这一节我们来设计一个小闹钟。我们总有些事情要在固定的时间去做,为了防止忘记, 我们通常使用一个闹钟来提醒自己。作为一个程序员,为什么我们不能自己编写一个闹钟 的程序呢? 4.5.1 程序效果说明 “精彩闹钟”是一个简单的Applet,是嵌在网页里并可以通过浏览器装入直接运行的 一种程序。 闹钟的初始界面如图4.1所示。在闹钟的左上角有一个表盘,表盘中的表针随时间走动; 在表盘的下方是日期和时间的字符显示。在Applet的下方是一个对话框,用户可以在界面 的对话框中输入定时的时间,点击“确定”按钮来定时,到了规定的时间,闹钟就会报警 一秒钟。 图 4.1 “闹钟”初始界面 4.5.2 实现简单的界面 建立工程和 Applet 框架 我们首先来建立一个工程。选择JBuilder的File主菜单→New Project。工程名称为 118 Java 游戏编程导学 AClock,目录为D:\myPro。 接下来我们建立一个Applet。选择JBuilder的File主菜单→New→Web选项卡→Applet选 项,点击OK按钮。将这个Applet命名为ClockApplet,基类为java.applet.applet,其余均为默 认设置,点击Finish按钮。我们就建立好了Applet框架。 添加组件 我们使用JBuilder的UI设计器来完成这项工作。UI设计器的界面如图4.2所示。 图 4.2 UI 设计完成后的 UI 设计器 我们在这里使用UI设计器添加组件,其余的部分(表盘表针等)需要我们画出。在这 里需要添加8个组件,1个Panel,1个按钮,3个Label和3个TextField。 首先添加Panel,这是其余组件的容器。点击Panel加入界面,将它的Constaints属性改 为South,Layout属性改为FlowLayout。注意,在这里我们的this组件的layout一定要为 BorderLayout。这样Panel就添加好了。 接下来我们把其余7个组件依次加入到Panel中,按图中的顺序排好。需要修改的属性 如表4.6所示: 表4.6 组件需要修改的属性 label1 Text——时 label2 Text——分 label3 Text——秒 textField1 Name——inHour,删除Text属性中的值 textField2 Name——inMin,删除Text属性中的值 textField3 Name——inSec,删除Text属性中的值 button1 Text——确 定 第 4 章 Java编程深入——图像与多媒体 119 这样,组件就添加完毕了。添加完组件的Applet代码如下: package aclock; import java.awt.*; import java.awt.event.*; import java.applet.*; import java.util.Date; import java.net.URL; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 */ public class CApplet extends Applet { MediaTracker mediaTracker = new MediaTracker(this); Panel panel1 = new Panel(); BorderLayout borderLayout1 = new BorderLayout(); Button button1 = new Button(); Label label1 = new Label(); Label label2 = new Label(); Label label3 = new Label(); TextField inHour = new TextField(); TextField inMin = new TextField(); TextField inSec = new TextField(); //Construct the applet public CApplet() { } //Initialize the applet public void init() { try { jbInit(); } catch (Exception e) { e.printStackTrace(); } } //Component initialization private void jbInit() throws Exception { this.setLayout(borderLayout1); 120 Java 游戏编程导学 button1.setLabel("确 定"); button1.setLocale(java.util.Locale.getDefault()); button1.addActionListener(new CApplet_button1_ actionAdapter(this)); label1.setText("时"); label2.setText("分"); label3.setText("秒"); inHour.setColumns(2); //textField2.setCaretPosition(10); inMin.setColumns(2); inMin.setText(""); inSec.setColumns(2); inSec.setText(""); this.add(panel1, BorderLayout.SOUTH); panel1.add(label1, null); panel1.add(inHour, null); panel1.add(label2, null); panel1.add(inMin, null); panel1.add(label3, null); panel1.add(inSec, null); panel1.add(button1, null); } } 装载图片 这一步我们要把图片装入Applet。像前面装载的步骤一样,首先添加一个Image对象。 Image img; 然后写一个初始加入图片的函数。代码如下: void imgInit() { img = getImage(getDocumentBase(), "img/clock.jpg"); //装载总的大图片 mediaTracker.addImage(img, 0); try { mediaTracker.waitForAll(); } catch (Exception e) { System.out.println("图片装载出错"); } } 这样我们就把图片加入到程序里了。然后我们要在程序中画出这幅图片。这需要我们 重载paint()函数。 public void paint(Graphics g) { g.drawImage(img, 0, 0, this); } 第 4 章 Java编程深入——图像与多媒体 121 现在进行编译,结果如图4.3所示。 图 4.3 程序运行后的结果 我们发现图片并没有正常显示,原来是我们的运行Applet的参数设置有问题,在JBuider 的工具条中点击Run工具旁边的下三角按钮,从下拉列表中选择Configuration,得到如图4.4 所示的对话框。 图 4.4 工程属性对话框 在对话框里面点击Edit按钮,修改Applet的参数Width为500,Height为400。如图4.5所 示。连续两次点击OK按钮保存设置。 122 Java 游戏编程导学 图 4.5 运行时设置属性对话框 然后我们再运行程序就得到了想要的结果,如图4.6所示。 图 4.6 修改好的结果 这样,我们的初始界面编写工作就算完成了。 第 4 章 Java编程深入——图像与多媒体 123 4.5.3 画出表盘和表针 前面提到,我们的表盘和表针都是直接画在界面上的,下面我们就来完成这一步。 添加常量 首先我们需要向程序中添加一些会用到的常量,常量的意义请参见程序注释。 int lxS, lyS, lxM, lyM, lxH, lyH; int xCenter = 100, yCenter = 100; //表盘中心的位置 int radius = 80; //表盘的半径 int xToday = 45, yToday = 220; //表盘下方时间的位置 画出表盘 我们编写一个画出表盘的函数drawScale(),代码如下: public void drawScale(Graphics g) { int xHour, yHour; g.setColor(Color.yellow); for (int i = 0; i < 12; i++) { //表盘上时间刻度的位置 xHour = (int) (Math.cos( (i * 30) * 3.14f / 180 - 3.14f / 2) * radius + xCenter); yHour = (int) (Math.sin( (i * 30) * 3.14f / 180 - 3.14f / 2) * radius + yCenter); //画出刻度 g.fill3DRect(xHour - 3, yHour - 3, 6, 6, true); } } 在这里面我们用到了函数fill3Drect(),这是Graphics中的一个函数,作用是画出实心的 具有3D效果的矩形。然后我们将drawScale()函数添加到paint()函数中。 public void paint(Graphics g) { …… g.drawImage(img, 0, 0, this); this.drawScale(g); …… } 结果如图4.7所示。 124 Java 游戏编程导学 图 4.7 画出表盘后的结果 画出表针 下一步我们就是要把表针也画出来,我们需要在paint()函数中添加如下代码。 …… Date date = new Date(); second = date.getSeconds(); minute = date.getMinutes(); hour = date.getHours(); today = date.toLocaleString(); //确定秒针的位置 xSecond = (int) (Math.cos(second * 3.14f / 30 - 3.14f / 2) * radius + xCenter); ySecond = (int) (Math.sin(second * 3.14f / 30 - 3.14f / 2) * radius + yCenter); //确定分针的位置 xMinute = (int) (Math.cos(minute * 3.14f / 30 - 3.14f / 2) * (radius - 10) + xCenter); yMinute = (int) (Math.sin(minute * 3.14f / 30 - 3.14f / 2) * (radius - 10) + yCenter); //确定时针的位置 xHour = (int) (Math.cos( (hour * 30 + minute/ 2) * 3.14f / 180 - 3.14f / 2) * (radius - 30) + xCenter); yHour = (int) (Math.sin( (hour * 30 + minute / 2) * 3.14f / 180 - 3.14f / 2) * (radius - 30) + yCenter); //画出字符显示的日期和时间 第 4 章 Java编程深入——图像与多媒体 125 g.setColor(Color.white); g.drawString(today, xToday, yToday); //画出秒针 g.setColor(Color.red); g.drawLine(xCenter, yCenter, xSecond, ySecond); //画出分针 g.setColor(Color.green); g.drawLine(xCenter, yCenter - 2, xMinute, yMinute); g.drawLine(xCenter - 2, yCenter, xMinute, yMinute); //画出时针 g.setColor(Color.blue); g.drawLine(xCenter, yCenter - 3, xHour, yHour); g.drawLine(xCenter - 3, yCenter, xHour, yHour); …… 这里面分别用到了Graphics的函数drawString()和drawLine(),这些函数在前面已经有过 介绍。 现在的程序运行结果如图4.8所示。 图 4.8 画出表针以后的情况 4.5.4 让闹钟动起来 程序写到这里读者一定发现了一个大问题,就是我们的闹钟不会动,需要进行刷新这 个表才会走。在程序中加入一个线程作为定时器就可以解决这个问题。 线程的知识我们在这里不作过多的解释,因为在后面还会详细讨论,读者只需要知道 在这里怎么做就足够了。 首先我们需要使这个程序实现Runnable接口,这是加入线程的第一步,如下所示: 126 Java 游戏编程导学 public class CApplet extends Applet implements Runnable { …… 然后我们加入一个线程变量作为定时器。 Thread timer = null; 这个时候程序会报错,这是由于我们没有实现Runnable接口中的run()方法所致。加入 如下代码: public void run() { while (true) { try { timer.sleep(100); } catch (Exception e) {} this.repaint(); } } 然后我们还要添加线程启动和结束的代码。 public void start() { if (timer == null) { timer = new Thread(this); timer.start(); } } public void stop() { timer = null; } 接着运行程序,你会很高兴地发现,闹钟开始走了。 4.5.5 给闹钟加上声音 谁都不想买一个哑巴闹钟,可是我们的闹钟到现在为止一直不会闹。这就是下面我们 要解决的问题。 加入声音文件 首先我们加入一个音频变量。 AudioClip au; 然后我们在ImgInit()函数中将它初始化。 void imgInit() { au = getAudioClip(getCodeBase(), "au/alarm.au"); …… } 第 4 章 Java编程深入——图像与多媒体 127 现在这个声音文件就可以使用了,我们需要将它定位到需要的地方去。 给按钮加入事件响应函数 下一步我们要完成闹钟的定时功能,闹钟是应该在预定的时间响的,而不是一直吵个 不停,不是吗?我们要让程序可以接受读者规定的时间。 为了完成这个功能,我们要加入一些变量。 int AHour, //预定的小时数 AMin, //预定的分钟数 ASec; //预定的秒数 然后我们打开UI设计器,双击按钮,JBuider为我们建立好了事件响应函数,在函数中 键入代码: void button1_actionPerformed(ActionEvent e) { if(inHour.getText() != null) //获得读者输入 AHour = Integer.parseInt(inHour.getText()); //如果输入为空,则默认为0 else AHour = 0; if(inMin.getText() != null) AMin = Integer.parseInt(inMin.getText()); else AMin = 0; if(inSec.getText() != null) ASec = Integer.parseInt(inSec.getText()); else ASec = 0; } 这里我们分别从3个文本框中获得读者的输入,如图4.9所示,如果读者没有输入,则 认为输入为0。这样闹钟的预定时间就已经设置好了。我们需要定时让它报警。 在paint()函数中输入如下代码: //判断当前时间是否和预定时间相等 //如果相等的话,则报警。时间为一秒钟 if(AHour == hour && AMin == minute && ASec == second){ au.play(); } 现在运行这个程序,设定时间输入,闹钟定时报警,我们的程序也就编写完成了。 128 Java 游戏编程导学 图 4.9 定时的闹钟 4.6 “模拟钢琴”游戏 “模拟钢琴”是在计算机上模拟钢琴键盘,可以让用户通过鼠标和键盘来弹奏出美妙 乐曲的一个小游戏。本节通过“模拟钢琴”游戏,重点讲解Java在图形和多媒体方面的一 些简单应用,也涉及到其他部分的内容。本节采用最新的Java2的消息处理机制来处理鼠标 和键盘的消息。 4.6.1 游戏效果说明 “模拟钢琴”游戏是一个简单的Applet,是嵌在网页里并可以通过浏览器装入直接运 行的一种程序。 模拟钢琴的初始界面如图4.10所示。用户可以用鼠标或者键盘来模拟钢琴里的击键, 程序便会发出钢琴的相应键的声音。由于本节是通过这个游戏来讲解Java的图形和多媒体 的应用,所以这个游戏只是简单的实现了低高两个全音级的发音,对于钢琴的半音、音的 长短等等其他的控制,这个游戏暂不涉及。笔者在进一步实践里提供了实现这些方法的思 路,读者可以自己去尝试。 第 4 章 Java编程深入——图像与多媒体 129 图 4.10 初始界面 4.6.2 实现简单的界面 因为HTML文件比较简单,请读者自行编写,或者是参考本书附录光盘。 准备图片 这里需要两张图片,一张是琴键按下时的图片,一张是琴键释放时的图片。这两张图 片便可以产生当琴键按下或者释放时需要的动画效果。由多个琴键便可以组成一个简单的 钢琴界面,如图4.11所示。 图 4.11 多个琴键组成的一个简单的钢琴界面 建立工程 我们先来建立一个工程。和前面一样,在JBuilder的主菜单中,选择File→New Project, 进入工程向导对话框,在工程名称中填写Piano,目录选用D:\myPro,其余均为默认设置, 点击Finish按钮,就建好了我们需要的工程。 接着建立一个Applet,在Jbuider的主菜单中,选择File→New→Web选项卡→Applet选 项,点击OK按钮,进入Applet向导对话框。我们将这个Applet命名为Piano,注意基类是 java.applet.Applet,如果不是的话请更正,其余均采用默认设置。点击Finish按钮,就完成 了Applet类的建立。 JBuilder会为我们生成一个带有框架代码的Piano类和同名的html文件,这些框架代码对 我们来说用处不大,将Piano.java中的代码用下面的代码框架替换。我们将在后面填充它的 各个方法,来实现本节所要求的基本功能: 130 Java 游戏编程导学 package piano; import java.awt.*; import java.awt.event.*; import java.applet.*; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

* @author not attributable * @version 1.0 */ public class Piano extends Applet { //构建Applet public Piano() { } //初始化Applet public void init() { try { jbInit(); }catch(Exception e) { e.printStackTrace(); } } //组件初始化 private void jbInit() throws Exception { } //重写paint方法,画出界面 public void paint(Graphics g){} } 这个框架列出了Applet的3个基本方法:一个是Piano()方法,一个是Applet的init()方法, 一个是Applet的paint()方法。Piano()方法是Applet的构造函数。init()方法用于将Applet中的 一些参数进行初始化,jbInit()方法是JBuider加入的初始化方法,我们可以将它看作是init() 方法的一部分,主要作用是初始化组件参数,这样可以使程序结构更清晰。我们还要重写 Applet的paint()方法,以获得我们想要得到的界面。Applet的其他函数将在后面详细讲到。 要实现钢琴的简单界面,应该在init()方法里将所需要的图片装入,在paint方法里将这 些图片按一定顺序绘制成一个完整的钢琴界面。 装入图片 在Applet的init()方法里,我们来装入图片。 首先我们定义Piano类的几个Image成员,用来存放装载进去的图片,这些成员变量和 常量都应该定义在类的开始作为全局变量: 第 4 章 Java编程深入——图像与多媒体 131 //键盘、鼠标释放时显示的键盘图片 Image m_ImgUp; //键盘、鼠标按下时显示的键盘图片 Image m_ImgDown; 定义两个常量来记录图片的宽度和大小: final int IMG_WIDTH=17; final int IMG_HEIGHT=85; 然后我们就可以在init()里将键盘、鼠标释放时显示的键盘图片装入到m_ImgUp里,将 键盘、鼠标按下时显示的键盘图片装入到m_ImgDown里。代码如下: try { URL url = Class.forName("piano.Piano").getResource("img/up.gif"); m_ImgUp = getToolkit().getImage(url); url = Class.forName("piano.Piano").getResource("img/down.gif"); m_ImgDown = getToolkit().getImage(url); }catch (Exception e) { e.printStackTrace(); } 下面我们编写代码监视m_ImgUp、m_ImgDown两个图像对象的装载。 (1)先建立一个MediaTracker对象: MediaTracker mediaTracker=new MediaTracker(this); (2)将m_ImgUp、m_ImgDown两个图像对象加入到mediaTracker的监视队列里: mediaTracker.addImage(m_ImgUp,0); //将m_ImgUp加入到监视队列中,标识号为0 mediaTracker.addImage(m_ImgDown,1); //将m_ImgDown加入到监视队列中,标识号为1 (3)装载图像,并返回图像对象的装载情况: try{ mediaTracker.waitForID(0); }catch(Exception e){ System.out.println("m_ImgUp is not loaded right"); } 这段代码是用mediaTracker.waitForId(int id)方法来执行图片的装载,如果装载不成功, 则会抛出一个异常。在抛出异常时,打印出相应的出错信息。 画出界面 现在Applet已经将需要的两张图片装载到了m_ImgUp和m_ImgDown里去,下面我们应 该将图片按顺序画出来,组织成一个钢琴的图像。 首先我们用一个数组来记载键盘的状态,比如说哪个键处于按下的状态,哪个键处于 132 Java 游戏编程导学 释放的状态。这里我们使用一个整型数组。数组应该定义为Applet的成员: int[] m_nState=new int[14]; 再定义两个常量,来表示释放或按下的状态: final int STATE_UP=0; final int STATE_DOWN=1; 当数组里某一项的数据为STATE_UP时,则相应的琴键处于释放的状态。反之,当数 组里某一项的数据为STATE_DOWN时,则相应的琴键处于按下的状态。 下面我们就可以在Applet里的paint()方法里编写画图的代码了。 遍历m_nState数组,当相应的项为STATE_UP时,则在相应的位置上贴上键盘、鼠标 释放时显示的键盘图片。当相应的项为STATE_DOWN时,则在相应的位置上贴上键盘、 鼠标按下时显示的键盘图片。 代码如下: public void paint(Graphics g){ for(int i=0;i<14;i++){ switch(m_nState[i]){ case 0: g.drawImage(m_ImgUp,i*IMG_WIDTH,0,this); break; case 1: g.drawImage(m_ImgDown,i*IMG_WIDTH,0,this); break; } } } g是一个Graphics对象,这个对象指代的是Applet的界面对象。对g进行操作,就相当于 对Applet的界面进行操作。对于drawImage()函数的介绍,请见4.3.2节。 现在画图部分的程序就编写完了,但是程序还不能运行,我们需要为m_nState数组初 始化一个值,来决定界面的琴键的初始状态。我们先初始化m_nState数组的值如下: for(int i=0;i<14;i++) { m_nState[i]=STATE_UP; } m_nState[10]=STATE_DOWN; //将第11个琴键初始化为按下的状态 这样,整个程序就可以编译了。编译以后在JBuilder中预览如图4.12所示。 第 4 章 Java编程深入——图像与多媒体 133 图 4.12 第 11 个琴键按下时的界面 这样整个程序的界面就编写好了。 4.6.3 添加事件处理 我们已经将小应用程序的界面做好了,但是,这时候鼠标按下时没有任何响应。这是 因为我们还没有添加对鼠标事件的处理。本节我们要着重讲一下对鼠标事件的处理以及由 此产生的动画效果。 如何加入鼠标接口和键盘接口 如果想要实现对鼠标和键盘事件的处理,需要首先在类中导入java.awt.event.*,在此我 们用到的MouseListener()和KeyListener()就是定义在这个包里的。但是这个包已经在Applet 生成的时候导入,我们就不必自己来做这个工作了。 我们要在类的声明中加入implements语句: import java.applet.Applet; import java.awt.event.*; public class Piano extends Applet implements MouseListener,KeyListener{ … } 此外,我们还必须要在init()方法中使用addListener()方法。addListener()相当于在消息 循环里加入一个监测鼠标或键盘事件的方法,方法如下: 134 Java 游戏编程导学 addMouseListener(myListener); //myListener是一个实现了mouseListener()方法的类 addKeyListener(myListener); //myListener是一个实现了keyListener()方法的类 下面就是我们的Applet框架: public class Piano extends Applet implements MouseListener,KeyListener{ public void init(){ … addKeyListener(this); //监听键盘消息 addMouseListener(this); //监听鼠标消息 } //实现MouseListener中的方法 public void mouseClicked(MouseEvent e){ //Invoked when the mouse has been clicked on a component. } public void mouseEntered(MouseEvent e){ //Invoked when the mouse enters a component. } public void mouseExited(MouseEvent e){ //Invoked when the mouse exits a component. } public void mousePressed(MouseEvent e){ //Invoked when a mouse button has been pressed on a component. } public void mouseReleased(MouseEvent e){ } // 实现KeyListener中的方法 public void keyPressed(KeyEvent e){ //Invoked when a key has been pressed. } public void keyReleased(KeyEvent e){ //Invoked when a key has been released. } public void keyTyped(KeyEvent e){ //Invoked when a key has been typed. } } 我们可以直接添加这些方法,也可以通过JBuilder的界面设计器。在编辑器的下方有几 个标签,点击Design标签,就打开了JBuilder的界面设计器。 我们观察窗口右边的属性和事件窗口。在这个窗口的下方也有两个标签,当前默认的 是Porperties(属性)标签,点击events(事件)标签,在这个标签显示的列表中查找鼠标事 件。先找到mouseClicked,在它后面的空白栏目中点击鼠标,出现默认的事件响应函数名 称this_mouseClicked,将这个函数的名称改为mouseClicked,然后双击该函数,JBuilder自 第 4 章 Java编程深入——图像与多媒体 135 动切换到编辑页面,并将编辑光标定位在已经自动添加好的空的事件响应函数体 mouseClicked()上。 用如上方法添加所有的5 个鼠标事件处理函数代码框架。鼠标事件分别是 mouseClicked、mouseEntered、mouseExited、mousePressed和mouseReleased,处理函数的名 称对应为mouseClicked()、mouseEntered()、mouseExited()、mousePressed()和mouseReleased()。 如果读者细心的话就会发现,JBuilder已经为我们添加好了必要的适配器类(在代码的 最后)。将所有的事件添加完毕以后的代码如下: class Piano_this_mouseAdapter extends java.awt.event.MouseAdapter { Piano adaptee; Piano_this_mouseAdapter(Piano adaptee) { this.adaptee = adaptee; } public void mouseClicked(MouseEvent e) { adaptee.mouseClicked(e); } public void mouseEntered(MouseEvent e) { adaptee.mouseEntered(e); } public void mouseExited(MouseEvent e) { adaptee.mouseExited(e); } public void mousePressed(MouseEvent e) { adaptee.mousePressed(e); } public void mouseReleased(MouseEvent e) { adaptee.mouseReleased(e); } } 处理鼠标消息,实现 MouseListener()方法 这样我们就可以在所需要的方法里加入实现方法就行了。比如我们如果要想让鼠标按 下和释放时,让Applet都打印出来一个字符串告诉我们鼠标被按下或释放,则可以这样写: public void mousePressed(MouseEvent e){ //Invoked when a mouse button has been pressed on a component. System.out.println("你的鼠标按钮已被按下"); } public void mouseReleased(MouseEvent e){ //Invoked when a mouse button has been released on a component. System.out.println("你的鼠标按钮已被释放"); } 136 Java 游戏编程导学 编译这个Applet,在琴键上按下鼠标按钮或者释放鼠标按钮,Applet将会在控制台上打 印出如图4.13所示的信息。 图 4.13 控制台打印出来的信息 处理键盘消息,实现 KeyListener()方法 和前面添加鼠标的方法相似,这次我们添加所有的3个键盘事件处理方法的框架。在 event列表中找到keyPressed、keyPeleased和keyClicked 3个事件,采用和添加鼠标事件相同 的方法,3个事件处理函数的名称分别为keyPressed()、keyPeleased()和keyClicked()。 我们先实现简单的功能,在键盘上任意一个键按下和释放时,都打印出相应的信息。 这样我们要在相应的keyPressed()方法和keyReleased()方法里加入如下代码: public void keyClicked(KeyEvent e) { //Invoked when a key has been pressed. System.out.println("你的键盘已经按下"); } public void keyReleased(KeyEvent e) { //Invoked when a key has been released. System.out.println("你的键盘已经松开"); } 这个程序编译之后运行,在按下和释放某键时,控制台上将会打印出如图4.14所示的 信息。 第 4 章 Java编程深入——图像与多媒体 137 图 4.14 控制台打印出来的信息 4.6.4 继续完善这个游戏 现在我们来分析一下这个程序。 鼠标按下琴键时的处理 在鼠标按下时,我们应该如何处理呢?我们应该要获得鼠标按下时的位置,以用来计 算我们按在了哪个键上,然后,我们应该把这个键的状态设为按下状态。 我们应该这样来实现这些功能。 (1)获得鼠标按下时的位置: int nX=e.getX(); (2)计算鼠标按在了哪个键上。 由于我们是将图片一个接着一个的贴在Applet上面组成一个钢琴图像的,所以计算方 法应该是用鼠标的X坐标除以每个键的宽度: int nCount=nX/IMG_WIDTH; //to record which button is pressed (3)将这个键的状态改为按下的状态。 这只要将m_nState数组的第nCount个元素的值变为STATE_DOWN就行了: m_nState[nCount]=STATE_DOWN; 138 Java 游戏编程导学 (4)重画图像: repaint(); //重画图像 这样我们就可以在鼠标按下琴键时使其响应是被按下的状态。 完整的代码如下: public void mousePressed(MouseEvent e){ //Invoked when a mouse button has been pressed on a component. int nX=e.getX(); int nCount=nX/IMG_WIDTH; //to record which button is pressed m_nState[nCount]=STATE_DOWN; repaint(); } 鼠标释放琴键时的处理 鼠标释放时,我们应该将所有的琴键都置于释放的状态。 由于后面我们还会用到要将所有的琴键状态都置于释放状态,这里我们先写一个方法 将所有琴键的状态都置于STATE_UP状态下: void setAllPianoKeyUp(){ for(int i=0;i<14;i++){ m_nState[i]=STATE_UP; } } 我们应该在mouseReleased()方法里调用上面这个方法,并调用重画方法强制画面重画, 使所有的琴键置于释放的状态: public void mouseReleased(MouseEvent e){ //Invoked when a mouse button has been released on a component. setAllPianoKeyUp(); repaint(); } 这样,当我们将按下的鼠标键释放时,所有的琴键都置于释放的状态。 键盘按下时的处理 键盘键按下时要进行的处理和鼠标键按下时类似,我们需要将按下的键对应的琴键的 状态置于按下状态。当我们同时按下数字键和Ctrl键时,其相应的琴键应该是数字键的值加 上7。 (1)定义一些变量: boolean bControlDown; 用来记录Ctrl键是否按下,默认为没有按下。 第 4 章 Java编程深入——图像与多媒体 139 int nCount=-1; 用来记录这是第几个相关的琴键。 (2)判断哪个键按下。 应该用KeyEvent.getKeyCode()方法来判断是哪个键按下并得到按下键的键值: int nKeyCode=e.getKeyCode(); 判断Ctrl键是否按下: bControlDown =e.isControlDown(); (3)计算按下的键对应于钢琴上哪个琴键 当我们没有按下Ctrl键只按下数字键时,应该赋给nCount键盘减1的值。因为Java数组 的第一个元素的编号是0,也就是说m_nState[0]代表第1个琴键的状态。 编写代码如下: switch(nKeyCode){ case KeyEvent.VK_1: nCount=0; break; case KeyEvent.VK_2: nCount=1; break; case KeyEvent.VK_3: nCount=2; break; case KeyEvent.VK_4: nCount=3; break; case KeyEvent.VK_5: nCount=4; break; case KeyEvent.VK_6: nCount=5; break; case KeyEvent.VK_7: nCount=6; break; case KeyEvent.VK_8: nCount=7; break; case KeyEvent.VK_9: nCount=8; break; default: return; //按下其他键时,返回,不执行任何操作。 } 当我们按下Ctrl键时,应该将nCount加7: 140 Java 游戏编程导学 if(bControlDown)nCount=nCount+7; 但是如果我们同时按下8和Ctrl键时,就会出现nCount=14,超过了琴键的总数。所以, 有必要再写一些代码,避免这个错误: if(nCount>14)return; 如果nCount超过琴键的总数减1的话,就返回,不执行任何操作。 (4)将按下的琴键对应的状态改为按下的状态: m_nState[nCount]=STATE_DOWN; (5)强制重画: repaint(); 这一部分的完整代码见附录光盘。 键盘释放时的处理 键盘释放时要实现的功能与鼠标释放时稍有不同。我们这次要将释放的键盘所对应的 琴键释放,而不是将所有的琴键都置于释放的状态。 编写的步骤如下: (1)定义一些变量。 这里定义用到的变量同mousePressed()方法。关键代码如下: boolean bControlDown; 用来记录Ctrl键是否按下。默认为没有按下。 int nCount=-1; 用来记录这时相关的琴键是第几个。 (2)判断哪个键按下了。 应该用KeyEvent.getKeyCode()方法来判断是哪个键按下并得到按下键的键值: int nKeyCode=e.getKeyCode(); 判断Ctrl键是否按下: bControlDown =e.isControlDown(); (3)计算键盘释放的键对应哪个琴键。 同mousePressed()里的部分: switch(nKeyCode){ case KeyEvent.VK_1: nCount=0; break; case KeyEvent.VK_2: nCount=1; 第 4 章 Java编程深入——图像与多媒体 141 break; case KeyEvent.VK_3: nCount=2; break; case KeyEvent.VK_4: nCount=3; break; case KeyEvent.VK_5: nCount=4; break; case KeyEvent.VK_6: nCount=5; break; case KeyEvent.VK_7: nCount=6; break; case KeyEvent.VK_8: nCount=7; break; case KeyEvent.VK_9: nCount=8; break; default: return; } if(bControlDown)nCount=nCount+7; if(nCount>14)return; (4)将按下的琴键对应的状态改为释放的状态: m_nState[nCount]=STATE_UP; (5)强制重画: repaint(); 释放键盘时处理代码见附录光盘。 现在的效果 添加完以上的事件处理代码,已经能够简单地弹钢琴了,虽然还没有实现音响效果。 当鼠标或键盘按下时,相应的琴键也处在按下状态了;当鼠标或键盘释放时,相应的琴键 也释放。 图4.15和图4.16分别为鼠标按下和释放时的界面。 142 Java 游戏编程导学 图 4.15 鼠标按下时的界面 图 4.16 鼠标释放时的界面 4.6.5 加上音响效果 上面实现了简单的动画,下面我们开始让钢琴发出声音。否则,一架钢琴就成了哑巴, 中看不中用。 准备声音文件 首先我们应该先准备一些声音文件。Java支持.au的声音文件。我们需要14个分别代表 低音(do)到高音(xi)的声音文件。当我们弹到相应的琴键时,便让它播放相应音级的 声音文件。虽然这架钢琴还不能弹奏长音和短音,但是在短短的时间里,我们能创建出这 么一个简单的钢琴,也足以让人兴奋了。 这些声音文件不太好找。有条件的读者不妨到Internet上去碰碰运气。如果您真的找不 到这14个音级的声音文件,用一些很有趣的声音来取代它们也是一件很有意思的事情。你 可以随意选择这14个声音文件,让不同的琴键代表不同的声音。如果找到了其他格式的声 音文件,可以用一些转换工具将其格式转换成.au格式。这样的转换工具很多。 第 4 章 Java编程深入——图像与多媒体 143 装载声音文件 下面我们就要将声音文件加入程序,首先应该在Applet里新建一个AudioClip的数组对 象: AudioClip[] m_AudioClip=new AudioClip[14]; 在Applet的init()方法里应该将声音文件装入到这个对象数组里,在此我们采用 getAudioClip()方法。 getAudioClip()有如下两种方法: · getAudioClip(URL url,String name) 其中url是网络上的一个绝对URL,name是声音 文件相对于url的路径。 · getAudioClip(URL url) url是声音文件在网络上的一个绝对URL。 getAudioClip方法实现了对声音文件的装载,它在声音文件和AudioClip对象之间建立 了一个关联。 由上可知,我们可以在Applet的init方法里加上一些代码,来实现对声音文件的装载: for(int i=0;i<14;i++){ try { URL url = Class.forName("piano.Piano").getResource("au/" + i + ".au"); m_AudioClip[i] = getAudioClip(url); }catch (Exception e) { e.printStackTrace(); } } 在这里,我们将声音放在了Applet的.class文件所在目录的au子目录里,声音文件名称 为1.au~14.au。 这样我们就装载了14个声音文件。下面我们将讲解这些声音文件的使用。 播放声音文件 AudioClip是一个接口。在这个接口里定义了3个方法: · void loop() 开始循环播放声音文件。 · void play() 开始播放声音文件,但只播放一遍。 · void stop() 停止播放声音文件。 有了这些方法,我们就可以在需要播放声音文件的地方调用以上方法来播放了。 下面这个方法的作用是播放指定音级的声音文件: void showSound(int nCount){ m_AudioClip[nCount].play(); } nCount指代音级。当音级为低7时,nCount为6。这也是因为Java数组的第一个元素的 144 Java 游戏编程导学 下标是0。这与C语言和C++语言是一致的。 有了以上的方法,我们就可以在鼠标和键盘事件处理中直接调用它来实现音响效果。 (1)鼠标按下时加上音响效果。 前面已经得到了鼠标按下时相对应的琴键的序号,那么我们就可以很方便地调用 showSound(int nCount)方法。 代码如下: public void mousePressed(MouseEvent e) { //Invoked when a mouse button has been pressed on a component. int nX=e.getX(); int nCount=nX/IMG_WIDTH; m_nState[nCount]=STATE_DOWN; showSound(nCount); //添加相应的音响效果 repaint(); } 这样在鼠标按下时,就可以发出相应的琴键所代表的声音。 (2)键盘按下时加上音响效果。 前面也已经得到了键盘按下时相对应的琴键的序号。那么我们直接调用showSound(int nCount)方法就可以了。 代码如下: public void keyPressed(KeyEvent e) { //Invoked when a key has been pressed. … //以上省略的内容是用来计算键盘按下时相对应的琴键的序号的 //琴键的序号存在了变量nCount里 m_nState[nCount]=STATE_DOWN; showSound(nCount); repaint(); } 这样我们就得到一个功能比较完备的钢琴,有按键与松键的动画,也有键盘按下时的 音响效果。这时候,编译并运行程序,自己去玩一玩,心里一定很高兴。但是,你总觉得 还不完美,为什么呢?动画闪烁太厉害,鼠标拖动时琴键却不动。下面我们就来继续完善 它。 4.6.6 鼠标拖动时实现琴键的自动按下和释放 我们弹钢琴时,有时会用手在琴键上拨动,连续触动许多琴键,相应的发出连续的多 个声音。我们用鼠标拖动来模拟这个动作,这需要增加对鼠标移动(或拖动)等事件的处 理。下面我们使用MouseMotionListener来实现对这些事件的处理。 第 4 章 Java编程深入——图像与多媒体 145 MouseMotionListener 的使用 我们可以在mouseDragged()和mouseMoved()中加入打印当前鼠标位置的处理。像前面 一样,我们添加这两个事件的处理函数框架代码。 public void mouseDragged(MouseEvent e){ //Invoked when a mouse button is pressed on a component and then dragged. } public void mouseMoved(MouseEvent e){ //Invoked when the mouse button has been moved on a component //(with no buttons no down) } 此时你便实现了MousemotionListener里所定义的那两个方法,虽然都是一些空方法。 注意:虽然在有些方法中你可能无需添加任何功能代码,但是,这个方法必 须要在程序体中实现。这是接口的概念所要求的。 在mouseMoved()方法和mouseDragged()方法中加入功能代码。先简单地让鼠标在移动 或拖动时打印出鼠标的位置和是否拖动或移动的信息。代码如下: public void mouseDragged(MouseEvent e){ //Invoked when a mouse button is pressed on a component and then dragged. int nX=e.getX(); int nY=e.getY(); System.out.println("鼠标拖动到:x="+nX+" y="+nY); } public void mouseMoved(MouseEvent e){ //Invoked when the mouse button has been moved on a component //(with no buttons no down) int nX=e.getX(); int nY=e.getY(); System.out.println("鼠标移动到:x="+nX+" y="+nY); } 编译并运行这个Applet,当鼠标在这个Applet上移动或拖动时,Applet将会在控制台打 印出如图4.17所示的结果。 添加功能代码 下面我们要添加一些功能代码,让程序在鼠标拖动时能够达到用手来抚弄钢琴琴键的 效果。 功能应该加在mouseDragged()方法里。 (1)首先应该定义一个Applet的成员来记录鼠标拖动前按下的是哪个键。并使这个变 量的初始值为-1,这表示鼠标还没有被按下或拖动: int m_nOldDownCount=-1; 0 146 Java 游戏编程导学 图 4.17 控制台打印的鼠标位置信息 (2)在鼠标按下时,将鼠标按下的琴键的序号存到上边定义的变量里: public void mousePressed(MouseEvent e) { //Invoked when a mouse button has been pressed on a component. … //获得按下的琴键的序号,并存到nCount变量里 m_nOldDownCount=nCount; repaint(); } (3)鼠标释放时,应该将m_nOldDownCount变量仍置于-1: public void mouseReleased(MouseEvent e) { //Invoked when a mouse button has been released on a component. … //获得鼠标释放时所对应的琴键的序号,并存到nCount变量里 m_nOldDownCount=-1; repaint(); } (4)在鼠标拖动时,应该先判断鼠标是否已经移到另一个琴键上了,如果是,则将原 先的琴键置于释放状态,将鼠标当前按下的琴键置于按下状态。否则,不做任何处理: public void mouseDragged(MouseEvent e) { //Invoked when a mouse button is pressed on a component and then dragged. int nX=e.getX(); 第 4 章 Java编程深入——图像与多媒体 147 //得到鼠标当前位置的X坐标 int m_nTempCount=nX/IMG_WIDTH; //获得鼠标位置所对应的琴键序号 if(m_nTempCount==m_nOldDownCount)return; //如果鼠标在一个琴键上移动,则返回 m_nState[m_nTempCount]=STATE_DOWN; //将刚刚移进的琴键置于按下状态 m_nState[m_nOldDownCount]=STATE_UP; //将鼠标刚刚移出的琴键置于释放状态 showSound(m_nTempCount); m_nOldDownCount=m_nTempCount; //存下这次所按的琴键 } 进一步完善 上面已经将功能实现了。但是,如果你自己玩的时间长了,就会发现一个问题,当鼠 标从边上的一个琴键拖动到Applet外头去,然后才释放,你就会发现边上的琴键仍然处于 按下的状态,如图4.18所示。这是怎么回事呢? 图 4.18 现在的界面 这是因为当鼠标移到Applet外头时,鼠标的任何操作都不会在Applet里引发鼠标消息, 也就不会再调用mouseDragged()、mousePressed()、mouseReleased()等方法。那么我们如何 避免上面所说的情况呢? 在MouseListener接口里,我们讲到了mouseExited()方法。这个方法是在鼠标离开Applet 时调用,那么,我们在这个方法里加入一些功能代码,使鼠标离开时原来被鼠标按下的那 个琴键能够释放: public void mouseExited(MouseEvent e){ //Invoked when the mouse exits a component. m_nState[m_nOldDownCount]=STATE_UP; //将原来被鼠标按下的那个琴键释放 148 Java 游戏编程导学 showSound(m_nTempCount); m_nOldDownCount=-1; //将状态还原到没有拖动鼠标时的状态 } 这样,上面所说的问题就解决了。无论你怎么拖动鼠标在Applet内外移动,都不会再 出现什么错误了。 4.6.7 动画效果的改进 钢琴的主要功能和动画已经完全实现了。但是,我们会发现动画效果不好,闪烁特别 厉害。我们可以让刷新的范围变得很小,只局限于图形要变化的那一部分。这样一来动画 效果就好多了,但是在重画时仍然有闪烁,这是因为用背景颜色填充的缘故。改进的办法 如下。 (1)我们先定义一个Graphics类型的变量: Graphics g; (2)将Applet界面图形类的引用传给这个变量: g=getGraphics() (3)然后可以在g上进行一些操作,也相当于在Applet的界面上进行一些操作,如: g.drawImage(m_ImgUp,nCount*IMG_WIDTH,0,this); 我们可以使用这个方法去修改弹钢琴程序。 对程序的修改 我们采用上面讲的方法来改进我们的程序,使其彻底消除闪烁问题。我们需要修改原 先的事件处理时调用repaint的部分代码。 (1)鼠标按下时,我们将相应的重画部分改写一下,以减少闪烁: public void mousePressed(MouseEvent e){ …… Graphics g=getGraphics(); //得到一个Applet的窗口的图形对象 g.drawImage(m_ImgDown,nCount*IMG_WIDTH,0,this); //将变化的部分在原来的图上直接画上改变后的图像 } (2)鼠标释放时,我们将相应的重画部分改写一下,以减少闪烁: public void mouseReleased(MouseEvent e){ …… Graphics g=getGraphics(); //得到一个Applet的窗口的图形对象 g.drawImage(m_ImgUp,nCount*IMG_WIDTH,0,this); //将变化的部分在原来的图上直接画上改变后的图像 第 4 章 Java编程深入——图像与多媒体 149 } (3)键盘按下时,我们将相应的重画部分改写一下,以减少闪烁: public void keyPressed(KeyEvent e){ …… Graphics g=getGraphics(); //得到一个Applet的窗口的图形对象 g.drawImage(m_ImgDown,nCount*IMG_WIDTH,0,this); //将变化的部分在原来的图上直接画上改变后的图像 } (4)键盘释放时,我们将相应的重画部分改写一下,以减少闪烁: public void keyReleased(KeyEvent e){ …… Graphics g=getGraphics(); //得到一个Applet的窗口的图形对象 g.drawImage(m_ImgUp,nCount*IMG_WIDTH,0,this); //将变化的部分在原来的图上直接画上改变后的图像 } 到现在,我们这个小程序已经得到了充分的完善。最后的界面和开始的界面还一样。 但是玩起来有了音响效果,而且动画也不再闪烁。效果好多了。 4.6.8 Java的局限 这里有必要提一下使用Java编写这个游戏的局限性。因为Java不能够直接操纵硬件,它 虽然提供一些包来支持对硬件的操作,但是要让Java控制计算机来发出一些固定的音,而 不是播放声音文件,则还是一件相当麻烦的事情。特别是对Applet,这件事情根本不可能 做到。而这在其他高级语言里(如C和C++)却是一件很简单的事情。但是,这也是Java的 一个优点,Java访问硬件、内存的受限也保证了它的安全性。 4.7 本章知识点回顾 组件(Component)组件类位于AWT类层次结构的顶部, 是一个封装了可视化组件所有属性的抽象类。在屏幕 上显示的所有用语和用户交互的用户界面元素都是 Component类的子类 AWT组件简介 容器(Container)是Component类的子类。这个类允许 别的Component对象嵌套在Container类的对象中,这就 形成了一个多层包容机制。容器通过一些设计管理器 来完成布置组件位置的功能 150 Java 游戏编程导学 (续表) 面板(Panel)类是Container类的一个子类,它只是简 单地实现了Container类 窗口(Window)产生一个顶级窗口。窗口对象不出现 在任何别的对象中,而是直接出现在桌面上。一般我们 会直接使用Window类的子类Frame类 框架(Frame)类封装了窗口通常所需要的一切组件, 它是Window类的子类,并且拥有标题栏、菜单栏、边 框以及可以调整大小的角 FlowLayout,这是Java的默认布局管理器。FlowLayout 实现了一种简单的布局风格,类似于在一个文本编辑器 中文字的流动方式,组件从左上角开始,按从左到右, 从上到下的方式布置 FlowLayout() FlowLayout(int how) FlowLayout(int how,int horz,int vert) BorderLayout布局管理器有四边和中间区域的概念。它 在边缘设定4个狭窄的定宽控件,在中间为一个大的区 域,为北、南、西、东和中央 BorderLayout() BorderLayout(int horz,int vert) GridLayout 允许我们建立一个组件表,就好像在一个 二维的网格布置组件。添加那些组件时,它们会按从左 到右、从上到下的顺序在网格中排列。在构建器里,需 要指定自己希望的行、列数,它们将按正比例展开 布局管理器知识 CardLayout存储了几个不同的布局管理器,每个布局管 理器具有一个索引,就好像一叠扑克牌,总有一张牌在 给定的时间内位于这叠纸牌的顶层。这样,在用户与组 件进行交互的时候,这些控件可以根据用户的输入而动 态启用或者禁用。我们可以创建其他的布局管理器并隐 藏,以便在需要的时候激活 图像的加载和显示 我们也可以通过加载的方式获得图像。这通过使用 Applet类定义的getImage()方法实现,它拥有以下形式: Image getImage(URL url) Image getImage(URL url , String imageName) 第 4 章 Java编程深入——图像与多媒体 151 (续表) 只要你有一个图像,就可以使用drawImage()方法来显 示它,drawImage()方法是Graphics类中的一员。最常用 的一种形式如下: boolean drawImage(Image imgObj,int left,int top, ImageOBserver imgOb) ImageObserver是一个接口,当图像被生成时又来接收 消息。它仅仅定义了一种方法:imageUpdate()。当图 像通过网络加载的时候,这种消息非常有用。下面是 ImageUpdate()方法的一般形式: boolean imageUpdate(Image imgObj,int flags, int left,int top,int width,int height) ImageObserver中定义flag参数的静态位标志 标识 意义 WIDTH 表示图像的宽度 HEIGHT 表示图像的高度 PROPERTIES 表示与图像相关的属性,可以通过img.getProperty()获 得 SOMEBITS 用于画图的像素已经收到,参数left、right、width和 height定义了包含新像素的矩形 FRAMEBITS 以前所画的一个多帧图像中的完整一帧已经收到,这 个帧可以被显示 ALLBITS 图像已经完成 MediaTracker的主要方法 MediaTracker(Component comp) 构造方法,为指定的组件comp创建一个MediaTracker 对象 addImage(Image image, int id) 将图片加入到MedialTracker的监视队列中去,image为 要被监视的图像对象,id为监视图像在监视队列中的标 识号 AddImage(Image image,int id,int w,int h) 将图片加入到MediaTracker的监视队列中去,w为所监 视的图像对象的宽度,h为所监视的图像对象的高度 CheckAll() 检查所有被监视的图像对象是否已经完成了装载过 程。如果所有图像对象已经完成了装载,则返回true, 否则为false CheckAll(Boolean load) 检查所有被监视的图像对象是否已经完成了装载过 程。如果load为true,则开始装载那些还没有被装载的 图像 152 Java 游戏编程导学 (续表) CheckId(int id) 检查在监视队列中所有标识号为id的图像对象是否已 经完成了装载过程,如果已经完成了装载过程,返回 true CheckId(int id,Boolean load) 检查在监视队列中所有标识号为id的图像对象是否已 经完成了装载过程,如果load为true,且在监视队列中 的标识号为id的图像没有被装载,则开始装载此图像对 象 IsErrorId(int id) 检查在监视队列中标识号为id的图像对象是否装载有 错误,如有错误,则返回true RemoveImage(Image image) 从监视队列中移去指定的图像对象image RemoveImage(Image image,int id) 从监视队列中移去指定id的图像对象image RemoveImage(Image image,int id,int width,int height) 从监视队列中移去指定id的图像对象image,且这个图 像对象指定了大小 WaitForAll() 开始装载监视队列中所有没有被装载的图像对象,如 果装载不成功,则会抛出一个异常 WaitForAll(long ms) 开始装载监视队列中所有没有被装载的图像对象,但 是如果装载时间超时,则会取消装载,并返回false WaitForId(int id) 开始装载监视队列中标识号为id的图像对象,如果装载 不成功,则会抛出一个异常 WaitForId(int id,long ms) 开始装载监视队列中标识号为id的图像对象,但是如果 装载时间超时,则会取消装载,并返回false Graphics类的主要方法 drawLine(int x1, int y1, int x2, int y2) 从坐标(x1,y1)到(x2,y2)画一条直线,返回值类型为void drawOval(int x, int y, int width, int height) 以(x,y)为左上角,画一个宽width、高height的椭圆,返 回值类型为void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) 以xPoints的项为各节点的x坐标,yPoints的项为各节点 的y坐标画多边形。节点数为nPoints,返回值类型为void drawPolygon(Polygon p) 画多边形。多边形的信息由p来描述,返回值类型为void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) 以xPoints的项为各节点的x坐标,yPoints的项为各节点 的y坐标画相互连接的多条线。节点数为nPoints,返回 值类型为void drawRect(int x, int y, int width, int height). 画以坐标(x,y) 为左上角、宽为width、高为height的矩 形。返回值类型为void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) 画以坐标(x,y) 为左上角、宽为width、高为height的矩 形,矩形的角是圆弧,圆弧的宽为arcWidth,高为 arcHeight。返回值类型为void 第 4 章 Java编程深入——图像与多媒体 153 (续表) drawString(AttributedCharacterIterator iterator, int x, int y) 在坐标(x,y)处画出字符串。字符串的值和属性在iterator 里定义。返回值类型为void drawString(String str, int x, int y) 在坐标(x,y)处画出字符串str。返回值类型为void fill3DRect(int x, int y, int width, int height, boolean raised) 填充以坐标(x,y) 为左上角、宽为width、高为height的三 维矩形。若raised为true,则矩形是突出显示的,否则为 凹下去的矩形,返回值类型为void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) 填充以坐标(x,y) 为左上角、宽为width、高为height、弧 度为arcAngle的圆弧区域。返回值类型为void fillOval(int x, int y, int width, int height) 填充以(x,y)为左上角、宽为width、高为height的椭圆区 域,返回值类型为void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) 填充以xPoints的项为各节点的x坐标,yPoints的项为各节 点的y坐标的多边形区域。多边形的节点数为nPoints, 返回值类型为void fillPolygon(Polygon p) 填充多边形区域。多边形的信息由p来描述,返回值类型 为void fillRect(int x, int y, int width, int height) 填充以坐标(x,y) 为左上角、宽为width、高为height的矩 形区域。返回值类型为void fillRoundRect(int x, int y, int width, int height,int arcWidth, int arcHeight) 填充以坐标(x,y) 为左上角、宽为width、高为height的矩 形区域,矩形的角是圆弧,圆弧的宽为arcWidth,高为 arcHeight。返回值类型为void getColor() 得到图的当前颜色。返回值类型为Color getFont() 得到图的当前字体。返回值类型为Font setColor(Color c) 设置图的当前颜色。返回值类型为void setFont(Font font) 设置图的当前字体。返回值类型为void 事件类及其描述 ActionEvent 通常在点击一个按钮、双击一个列表项或者选中一个菜 单项时发生 AdjustmentEvent 当操作一个滚动条时发生 ComponentEvent 当一个组件隐藏、移动、改变大小或者成为可见时发生 ContainerEvent 当一个组件从容器中加入或者删除时发生 FocusEvent 当一个组件获得或失去键盘焦点时发生 InputEvent 所有组件的输入事件的抽象超类 ItemEvent 当一个复选框或者列表框被点击时发生;当一个单选按 钮或者菜单项被选择或者取消时发生 154 Java 游戏编程导学 (续表) KeyEvent 当输入从键盘获得时发生 MouseEvent 当鼠标被拖动、移动、点击、按下或者释放时产生;或 者在鼠标进入或者退出一个组件时发生 TextEvent 当文本区和文本域的文本改变时发生 WindowEvent 当一个窗口激活、关闭、失效、恢复、最小化、打开或 者退出时发生 事件监听器接口及其描述 ActionListener 定义了一个接受动作事件的方法 void actionPerformed(ActionEvent ae) AdjustmentListener 定义了一个接受调整事件的方法 void adjustmentValueChanged(AdjustmentEvent ae) ComponentListener 定义4个方法用来识别何时隐藏、移动、改变大小和显 示组件 void componentResized(ComponentEvent ce) void componentMoved(ComponentEvent ce) void componentShown(ComponentEvent ce) void componentHidden(ComponentEvent ce) ContainerListener 定义了两个方法来识别何时从容器中加入或除去组件 void componentAdded(ContainerEvent ce) void componentRemoved(ContainerEvent ce) FocusListener 定义了两个方法来识别何时组件获得或失去焦点 void focusGained(FocusEvent fe) void focusLost(FocusEvent fe) ItemListener 定义了一个方法来识别何时项目状态改变 Void itemStateChanged(ItemEvent ie) KeyListener 定义了3个方法来识别何时按键按下、释放和键入字符 事件 void KeyPressed(KeyEvent ke) void KeyReleased(KeyEvent ke) void KeyTyped(KeyEvent ke) MouseListener 定义5个方法来识别何时鼠标点击、进入组件、离开组 件、按下和释放事件 void mouseClicked(MouseEvent me) void mouseEntered(MouseEvent me) void mouseExited(MouseEvent me) void mousePressed(MouseEvent me) void mouseReleased(MouseEvent me) 第 4 章 Java编程深入——图像与多媒体 155 (续表) MouseMotionListener 定义了两个方法来识别何时鼠标拖动和移动 void mouseDragged(MouseEvent me) void mouseMoved(MouseEvent me) TextListener 定义了一个方法来识别何时文本值改变 void textChanged(TextEvent te) WindowListener 定义了7个方法来识别何时窗口激活、关闭、失效、最 小化、还原、打开和退出 void windowActived(WindowEvent we) void windowClosed(WindowEvent we) void windowClosing(WindowEvent we) void windowDeactivated(WindowEvent we) void windowDeiconified(WindowEvent we) void windowIconified(WindowEvent we) void windowOpened(WindowEvent we) 第 5 章 拼图游戏——Applet 和线程 这一章我们来讲解一下Applet和线程方面的知识。通过对Applet的讲解,读者会对前面 几章的例子有一个更加深入的了解;线程方面的知识,可以使读者在编程的技巧方面更进 一步。 5.1 Applet基础 这一节将介绍Applet类的概念,它为运行小应用程序提供了必不可少的支持。在前面 的章节中我们的历程都是以小应用程序的形式出现,通过这一节的讲解,大家可以对Applet 有一个比较深入的认识。现在我们先来回顾一下组成一个小应用程序的基本元素和生成并 测试的必要步骤。 5.1.1 Applet简介 所有的小应用程序都是Applet类的子类,所有的小应用程序都必须引用java.applet类 库。在Swing中的JApplet类,也是从这里派生的。小应用程序是一个窗口程序,所以就需 要引入AWT或者Swing。另外,Applet不是由控制台的Java运行环境来解释,而是由Web浏 览器或者Applet阅读器(AppletViewer,JDK提供的一个工具)执行。 与大多数程序不同,Applet的执行不是从main()开始。事实上,Applet使用一种与一般 应用程序的执行完全不同的机制启动和控制。Applet的输入输出也和一般的应用程序不同。 小应用程序一般是使用标记包含在HTML文件中。当浏览器遇到HTML文件中 的applet标记时,小应用程序就可以通过嵌入在浏览器中的Java运行环境来执行。为了更方 便地观察和测试Applet,只需在你编写的Java源程序代码的头部加入一个包含applet标记的 注释即可。这样,你的代码就能用你的小应用程序所需的HTML语言记述下来,启动 AppletViewer并指定你的Java源文件为目标文件以后,就可以测试经过编译的Applet了。加 入注释的方式如下例: /* */ 5.1.2 Applet体系结构 我们先来讲解几个比较重要的概念。如果读者有C++编程经验的话,这些概念会比较 第 5 章 拼图游戏——Applet 和线程 157 易于理解。 首先,Applet是由事件驱动的。一个Applet类似于系列提供中断服务的子程序的集合。 在事件发生前,Applet一直处于等待状态,一旦事件发生,Applet就会采取相应措施并将控 制权交给Applet窗口。在大部分时间里,Applet都不具有控制权,但是,它必须针对特定的 事件作出相应的动作,获得控制权并将其转发给内部的窗口组件。这样的话,Applet可能 会完成一些独立的作业,或者会启动额外的线程。 其次,用户可以按照自己的意愿来和Applet进行交互。这些交互作为事件送至Applet。 在前面的程序我们已经看到,Applet包含了各种控件,当用户和某一个控件进行交互时, 就会产生一个事件。这个事件发送到相应的监听器中,由监听器调用相应的操作,产生一 系列动作。 5.1.3 Applet框架 几乎所有的Applet中都会重载一套方法,这些方法提供了浏览器或者AppletViewer与 Applet的接口和控制机制。它们是Applet定义的init()、start()、stop()和destory(),另外一般 还需要AWT中定义的方法paint(),这些方法同时也已经提供了默认的具体实现。 Applet 的初始化 当Applet开始运行的时候,首先执行的就是init()方法。一般情况下,所有的初始化工 作都会在这里进行。回想一下,在前面的编程工作中,当我们使用JBuilder创建一个Applet 时,在每一个Applet的构造函数中都存在如下代码: public MyApplet(){ try{ jbInit() }catch(Exception e){ …… } } public void jbInit(){ //初始化代码,省略 …… } 这里的jbInit()方法就是JBuilder重新定义过的init()方法,在Applet执行的时候首先调用 这个方法对Applet进行初始化。 init()方法也是一样的,它在Applet被执行的时候首先调用来初始化变量,它在程序运 行期间只调用一次。 start() 这个方法在init()之后调用,或者是在Applet重新启动的时候调用。与init()不同,start() 在每一次Applet的HTML文档被显示在屏幕上时都被调用。因此,如果用户离开一个网页之 后重新进入的时候,Applet就会从start()开始重新执行。 158 Java 游戏编程导学 paint() 在每一次Applet的输出必须重画窗口时,paint()方法就会被调用。Applet开始执行的时 候也会调用paint()。paint()有一个Graphics类型的参数,这个参数包含了图像的上下文,描 述小应用程序运行的环境,在需要对Applet进行输出时,这个上下文将会用到。 stop() 在Web浏览器离开包含Applet的HTML文件的时候,stop()方法就会被调用。如果调用 的时候Applet正在运行,这样就会挂起一些Applet不可见时不需要运行的线程。当用户回到 此页面的时候,就可以重新启动它们。 destory() 当你的Applet需要完全移出内存的时候,就会调用这个方法,这时所有Applet占用的资 源就应该被释放。stop()方法总是在destroy()方法之前调用。 从一个init()方法到一个destroy()方法,称为Applet的一个生命周期。 5.1.4 其他一些有用的方法 这一节我们来讨论另外一些对Applet来说很有意义的方法,包括update()和repaint()。 重载 update() 在很多情况下,Applet中都需要重载一个AWT定义的方法update(),这个方法在Applet 要求窗口的一部分被重画时调用。默认的update()是先使用默认的背景颜色填充Applet窗口, 再调用paint()方法,这样窗口在重画时,用户常常会感觉到闪烁。避免闪烁的比较好的方 法就是重载update(),使它完成所有必要的显示功能,然后使paint()简单调用update()。如 下 : public void update(Graphics g){ //重新显示窗口代码 } public void paint(Graphics g){ update(g); } repaint() repaint()方法是AWT定义的,它使得AWT的运行时环境执行对Applet的update()方法的 调用,默认情况下,update()会调用repaint()方法。 repaint()方法有以下几种形式: void repaint() void repaint(int left,int top,int width,int height) void repaint(long maxDelay) void repaint(long maxDelay,int x,int y,int width,int height) 第一种形式最简单,它重画了整个窗口,这种方法的效率最低。第二种形式指定了窗 第 5 章 拼图游戏——Applet 和线程 159 口中需要重画的区域,left和top指定了重画区域的左上角,而width和height指定了区域的宽 和高。第三种和第四种形式中的参数maxDelay指定了最大延迟毫秒数。 5.1.5 AppletContext接口的主要方法 下面介绍一个Applet的上下文接口——AppletContext接口。这个接口在Applet里已经得 到了实现,我们要在后面使用。AppletContext接口的主要方法如表5.1所示。 表5.1 AppletContext接口的主要方法 返回类型 方法 用途 Applet getApplet(String name) 返回这个网页上名为name的Applet,用于同一 网页上的两个Applet之间的通信 Enumeration getApplets() 返回这个网页上所有的Applet,用于同一网页 上的几个Applet之间的通信 AudioClip getAudioClip(URL url) 从网络地址url处取得声音文件,并创建一个 AudioClip实例,与这个声音文件建立关联 Image getImage(URL url) 从网络地址url处取得图片文件,并创建一个 Image实例,与这个图片文件建立关联 void showDocument(URL url) 重新装载页面,使页面的地址为给定的url void showDocument(URL url, String target) 同上 void showStatus(String status) 与前面用到的方法功能类似,用于在状态栏上 显示信息status 5.2 线 程 技 术 利用对象,可将一个程序分割成相互独立的区域。我们通常也需要将一个程序转换成 多个独立运行的子任务。像这样的每个子任务都称为一个“线程”(Thread)。线程有好 几种状态:可以正在运行(running);运行的线程可以挂起(suspend),就是说临时中断 它的执行;挂起的线程可以被恢复(resume),从停止的地方继续运行;一个线程在等待 资源的时候可以被阻塞(block);在任何时候,线程都可以终止(terminate)。 “进程”是指一种“自包容”的运行程序,有自己的地址空间。“多任务”操作系统 能同时运行多个进程(程序)——但实际是由于CPU 分时机制的作用,使每个进程都能循 环获得自己的CPU 时间片。但由于轮换速度非常快,使得所有程序好象是在“同时”运行 一样。“线程”是进程内部单一的一个顺序控制流。因此,一个进程可能容纳了多个同时 执行的线程。 多线程的应用范围很广。但在一般情况下,程序的一些部分同特定的事件或资源联系 在一起,同时又不想为它而暂停程序其他部分的执行,这样一来,就可考虑创建一个线程, 令其与那个事件或资源关联到一起,并让它独立于主程序运行。 160 Java 游戏编程导学 5.2.1 继承线程 为了创建一个线程,最简单的方法就是从Thread 类继承。这个类包含了创建和运行线 程所需的一切东西。Thread 最重要的方法是run()。但为了使用run(),必须对其进行过载或 者覆盖,使其能充分按自己的吩咐行事。因此,run()属于那些会与程序中的其他线程“并 发”或“同时”执行的代码。 下面这个例子可创建任意数量的线程,并通过为每个线程分配一个独一无二的编号, 从而对不同的线程进行跟踪。Thread 的run()方法在这里得到了重载,每通过一次循环,计 数就减1——计数为0时则完成循环(此时一旦返回run(),线程就中止运行)。 public class SimpleThread extends Thread { private int countDown = 5; private int threadNumber; private static int threadCount = 0; public SimpleThread() { threadNumber = ++threadCount; System.out.println("Making " + threadNumber); } /* *run()方法必须在类中实现 *这是继承Thread的要求 */ public void run() { while(true) { System.out.println("Thread " + threadNumber + "(" + countDown + ")"); if(--countDown == 0) return; } } public static void main(String[] args) { for(int i = 0; i < 5; i++) new SimpleThread().start(); System.out.println("All Threads Started"); } } run()方法几乎肯定含有某种形式的循环——它们会一直持续到线程不再需要为止。因 此,我们必须规定特定的条件,以便中断并退出这个循环(或者在上述的例子中,简单地 从run()返回即可)。run()通常采用一种无限循环的形式。也就是说,通过阻止外部发出对 线程的stop()或者destroy()调用,它会永远运行下去,直到程序完成。 在main()中,可以看到创建并运行了大量线程。Thread 包含了一个特殊的方法start(), 它的作用是对线程进行特殊的初始化,然后调用run()。所以整个步骤包括:调用构建器来 构建对象,然后用start()配置线程,再调用run()。如果不调用start(),线程便永远不会启动。 第 5 章 拼图游戏——Applet 和线程 161 5.2.2 Thread和Runnable Java的多线程体系建立于Thread类和它的接口Runnable的基础上。Thread类封装了线程 的执行。为了创建一个新的线程,你必须扩展Thread或者实现Runnable接口。 实现 Runnable 接口 创建线程最简单的方法就是创建一个实现Runnable接口的类。Runnable抽象了一个执 行代码单元。可以通过实现Runnable接口的方法创建每一个对象的线程。为实现Runnable 接口,一个类仅需实现一个run()的简单方法如下: public void run() 在run()中可以定义代码来构造新的线程。run()方法能够调用其他方法,引用其他类和 声明变量,同时,run()在程序中确立了一个并发的线程执行入口。当run()返回时,该线程 结束。 在实现了Runnable接口以后,还应该在类内部实例化一个Thread类的对象。代码片断 如下: class NewThread implements Runnable{ Thread t; NewThread(){ t = new Thread(this, "new thread"); } …… } 建立新的线程以后,它不直接运行调用了它的start()方法,该方法在Thread类中定义。 本质上,start()执行的是一个对run()的调用。 扩展 Thread 创建线程的另一个途径就是扩展Thread类,然后创建该类的实例。如果一个类继承 Thread类,它必须重载run()方法,这个方法是新线程的入口。它也必须调用start()方法去启 动新线程。 Thread类定义了好几种方法来帮助管理线程,主要方法如表5.2所示。 表5.2 Thread类中主要方法 返回类型 方法 用途 Thread(Runnable target) 构造方法,构造一个新的Thread实例,实例的run方法在 target里定义 Thread currentThread() 静态方法,返回当前执行的Thread对象 void yield() 静态方法,使当前线程暂停 void sleep(long millis) 静态方法,使当前线程暂停millis毫秒 void sleep(long millis,int nanos) 静态方法,使当前线程暂停millis毫秒加上nanos纳秒 162 Java 游戏编程导学 (续表) 返回类型 方法 用途 void start() 开始一个线程 void run() Runnable接口里的run方法 void interrupt() 中断一个线程 boolean interrupted() 判断线程是否已经中断,如果线程已经中断,返回true,并 将线程的中断标志取消 boolean isInterrupted() 判断线程是否已经中断,如果线程已经中断,返回true,不 会影响线程的中断标志 void destroy() 除去线程 boolean isAlive() 线程是否被激活 void setPriority(int priority) 设置线程的优先级,priority越大优先级越高 void setName() 设置线程的名字 int getPriority() 取得线程的优先级 String getName() 取得线程的名字 int aliveCount() 静态方法,取得当前存活的线程总数 5.2.3 线程的优先级 线程优先级被线程调度用来判定每个程序何时允许运行。理论上,优先级高的线程比 优先级低的线程获得更多的CPU时间。例如,当低优先级线程正在运行,此时一个高优先 级的线程被恢复,则它将抢占低优先级线程所使用的CPU。 理论上,等优先级的线程以同等的权利使用CPU。但是,在某些环境中,为安全起见。 等优先级线程偶尔也会受控制。这保证了所有线程在无优先级的操作系统下都有机会运行。 实际上,无优先级的情况下,多数线程仍然有机会运行,因为很多线程不可避免地会遭遇 阻塞,例如等待输入,这时阻塞线程挂起,运行其他线程。但是一般不建议采用这种方法。 我们可以通过如下语句来设置线程优先级: final void setPriority(int level) 这里level指定对调用的线程的新的优先权的设置。level的值在MIN_PRIORITY和 MAX_PRIORITY之间,通常它们分别为1和10。一个线程默认的优先级是NORM_ PRIOPITY,值为5,这些优先级在Thread中都被定义为final型常量。 同样,我们可以使用这条命令来获得当前的优先级设置: final int getPriority() 当涉及调度时,不同的操作系统中,Java的执行可能有本质上不同的行为。大多数矛 盾发生在使用有优先权的线程情况下,而不是协同地腾出CPU的时间。最安全的办法就是 获得可预先性的优先权,对于Java来说,就是自动放弃对CPU的控制。 下面我们举出一个例子,通过运行结果就可以很清楚地看到了: 第 5 章 拼图游戏——Applet 和线程 163 //Clicker.java public class Clicker implements Runnable { int clicking = 0; Thread t; volatile boolean running = true; public Clicker(int p){ t = new Thread(this); t.setPriority(p); } public void run() { while(running){ clicking++; } } public void stop(){ running = false; } public void start(){ t.start(); } } //Test.java public class Test { public static void main(String args[]){ Thread.currentThread().setPriority(Thread.MAX_PRIORITY); Clicker hi = new Clicker(Thread.NORM_PRIORITY+2); Clicker lo = new Clicker(Thread.NORM_PRIORITY-2); lo.start(); hi.start(); try{ Thread.sleep(9000); }catch(InterruptedException e){ System.out.println("Main thread interrupted."); } lo.stop(); hi.stop(); try{ hi.t.join(); lo.t.join(); }catch(InterruptedException e){ System.out.println("InterruptedException caught"); } 164 Java 游戏编程导学 System.out.println("Low priority thread : " + lo.clicking); System.out.println("High priority thread : " + hi.clicking); } } 结果为: Low priority thread : 90635979 High priority thread : 2035004773 这个程序的精确的输出结果依赖于CPU的运算速度和运行其他任务的数量,但这些结 果都可以表示,线程却是上下转换,优先级高的线程获得CPU时间较多。 5.2.4 线程同步 当两个或者两个以上的线程需要共享资源时,它们之间就需要一种方法来确定资源在 某一时刻仅仅被一个线程占用。达到此目的的过程叫做同步(Synchronization)。 下面列出简单的synchronized 方法: synchronized void a() { …… } synchronized void b() { …… } 每个对象都包含了一把锁,也叫作“监视器”,它自动成为对象的一部分。调用任何 synchronized 方法时,对象就会被锁定,不可再调用那个对象的其他任何synchronized 方 法,除非第一个方法完成了自己的工作,并解除锁定。在上面的例子中,如果为一个对象 调用a(),便不能再为同样的对象调用b(),除非a()完成并解除锁定。因此,一个特定对象的 所有synchronized 方法都共享着一把锁,而且这把锁能防止多个方法对通用内存同时进行 写操作(比如同时有多个线程)。 每个类也有自己的一把锁,作为类的Class 对象的一部分,所以synchronized static()方 法可在一个类的范围内被相互间锁定起来,防止与static 数据的接触。注意如果想保护其他 某些资源不被多个线程同时访问,可以强制通过synchronized方法访问那些资源。 但是,由于要为同样的数据编写两个方法,所以无论如何效率都不会很高。如果将所 有方法都设为自动同步,就可以完全消除synchronized 关键字。但实际上获取一把锁并非 一种“廉价”方案——为一次方法调用付出的代价至少要累加到4倍,而且根据我们的具体 方案,这一代价还有可能变得更高。所以假如已知一个方法不会造成冲突,最明智的做法 便是撤消其中的synchronized 关键字。 5.2.5 多线程技术 多线程是存在于一个内存空间里的,当其中的一个线程崩溃之后,会影响其他的线程。 第 5 章 拼图游戏——Applet 和线程 165 而多进程则不是。一个进程崩溃后,其他进程仍然不受影响地运行。在另一方面,跟踪一 个线程所需的开销要少于跟踪一个进程所需的开销。 Java中实现线程的方式有两种,一是生成Thread类的子类,并定义该子类自己的run方 法,线程的操作在run方法中实现。但我们定义的类一般是其他类的子类,而Java又不允许 多重继承,因此第二种实现线程的方法是实现Runnable接口。通过覆盖Runnable接口中的run 方法实现该线程的功能。 Runnable接口里定义了一个run方法。当一个对象声明了要实现Runnable接口,可以用 来创建一个Thread对象。当通过这个Thread的start方法来启动这个线程后,这个线程的run 方法就会自动被调用,且在一个独立的线程里,不占用现有的线程。 5.3 “拼图”游戏 拼图游戏也是一个简单的Applet,它的游戏规则和文曲星里的拼图游戏是一样的。这 个游戏将一张大图打散成9张小图,然后在游戏里任意挑8张图,贴在9个位置中的任意位置。 通过鼠标或键盘的方向键移动打乱的8张图片,让其复原成原来的顺序,玩家就胜利了,游 戏就结束了。在游戏结束之后,会算出玩家的得分,并自动将游戏得分发送给服务器(当 然游戏应该放在服务器上,后面会详细讲)。 游戏初始界面如图5.1所示。 图 5.1 初始界面 166 Java 游戏编程导学 5.3.1 游戏的简单设计 我们只设计游戏主要要实现的功能。下面简述游戏要实现的功能,及这些功能应该放 到哪儿去实现。这是一个简单的游戏,“设计”这一部分看起来不是很必要。但是要是做 一些大游戏、大项目,是一定要有这一步的。这在软件工程里叫做需求分析报告。这会让 我们明白我们的游戏要做成什么样子,要满足什么样的要求,然后,我们才能提出合适的 游戏设计方案。 游戏的设计大致如下: · 给出游戏规则的说明。由于游戏是嵌在网页里的,所以我们可以在网页上来说明游 戏的规则,无需在游戏里用Java代码来实现。 · 游戏界面。尽可能美观,这一部分应该在界面设计部分完善。 · 开始或结束游戏的菜单和按钮。这个也不需要,我们只要让玩家在打开页面时就开 始玩,关掉网页时就关掉好了。但是,应该设置一个按键,当用户按这个键时,游 戏会重新开始。这一部分应该放到处理键盘和鼠标事件部分来实现。 · 游戏能够玩起来,就是在玩家按动鼠标和键盘时,游戏能做相应的动作,并且判断 游戏是否已经结束。这应该放到鼠标和键盘事件处理部分来完成。 · 在游戏结束时报出玩家的成绩。这个功能相对比较独立,在做完事件处理后游戏能 够玩时再加上这个功能,不会对前面的代码有任何影响。 · 将玩家的成绩传到服务器。这个功能相对来说较独立,放到最后去做。 整个计划大体是这样。这里仅仅是罗列一下游戏要实现的功能,以及明确一下各个功 能应该在何处完成。这个计划并不是必须要这么写,各人有各人的风格。只要你能明确你 应该将游戏做成什么样就行了。 写这个计划书,主要是为了避免开始设计时的盲目性(这样不利于后面功能的实现)。 它主要是促使我们在一开始设计游戏时,就要考虑到这种设计是否适合其他功能的实现。 对于复杂的项目,还应该给出每一个功能实现的大体方案。 5.3.2 实现简单的界面 HTML 代码 这部分HTML代码比较简单,在这里只给出完整的代码,就不再详细解释了。 拼图游戏 拼图游戏 第 5 章 拼图游戏——Applet 和线程 167
这是一个拼图游戏。玩家应该将打散的小图拼成一张大图。
玩家可以通过鼠标和键盘来移动小图,移动的次数和拼成
一张大图所花费的时间作为游戏得分的依据。
按Y键可以预览整幅图。

编写 Java 代码 下面编写Applet的代码,来实现基本的界面部分。 首先找一张漂亮的图片,用作拼图游戏的画面,如图5.2所示,大小为360*360像素。 记住这个大小,在程序里要用到。 图 5.2 全图 我们向前面讲过的方法一样,建立工程pintu,工程路径这里选择为D:\myPro。然后再 在这个工程中创建一个Applet,注意工程基类为java.applet.Applet,命名为Pintu。框架代码 如下: package pintu; import java.awt.*; import java.awt.event.*; import java.applet.*; /** *

Title:

*

Description:

*

Copyright: Copyright (c) 2004

*

Company:

168 Java 游戏编程导学 * @author not attributable * @version 1.0 */ public class Pintu1 extends Applet { //Construct the applet public Pintu1() { } //Initialize the applet public void init() { try { jbInit(); } catch(Exception e) { e.printStackTrace(); } } //Component initialization private void jbInit() throws Exception { } public void paint(Graphics g) { } } 其中paint()方法是我们添加上去的重载方法,具体作用前面已经介绍过,这里不再赘 述。 装载图片 首先定义两个常量,来记录每块拼图的大小。在此将大图片分成3*3的拼图,则每张拼 图的大小为120*120: final int IMAGE_WIDTH=120; //每张拼图的宽 final int IMAGE_HEIGHT=120; //每张拼图的高 再定义10个图片对象。其中1个用来装入总的大图片,9个用来装入每个拼图的图片: Image[] m_Image=new Image[9]; //总的大图片 Image m_ImgAll; //9个用来装入每个拼图的图片对象 在init()里填写装入图片的代码: MediaTracker mediaTracker=new MediaTracker(this); //建立一个监视器 m_ImgAll=getImage(getDocumentBase(),"img/pintu.jpg"); 第 5 章 拼图游戏——Applet 和线程 169 //装载总的大图片 mediaTracker.addImage(m_ImgAll,1); try { mediaTracker.waitForAll(); } catch(Exception e) { System.out.println("图片装载出错"); } 上述代码装入了总的大图片,并用MediaTracker跟踪它的装入,在装入出错的时候打 印出出错信息。下面我们来装载9个小拼图的对象: for (int i = 0; i < 9; i++) { //遍历9个小拼图对象,用来装载其中的每个 m_Image[i] = getImage(getDocumentBase(), "img/" + i + ".jpg"); mediaTracker.addImage(m_Image[i],1); try { mediaTracker.waitForAll(); } catch (Exception e) { System.out.println("图片装载出错"); } //m_Image[0] } 这里我们是通过图像处理工具来把整个图像分成了9个小图像,分别命名为0.jpg~9.jpg。 然后分别加载,当然我们也可通过下面的方法加载: for(int i=0;i<9;i++) { //遍历9个小拼图对象,用来装载其中的每个 m_Image[i]=createImage(IMAGE_WIDTH,IMAGE_HEIGHT); //创建实例,也就是为每个图片对象分配内存空间 Graphics g=m_Image[i].getGraphics(); //获得Graphics对象 int nRow=i%3; int nCol=i/3; //算出这个小图片对象应对应总图的那一块区域 g.drawImage(m_ImgAll,0,0,IMAGE_WIDTH,IMAGE_HEIGHT, nRow*IMAGE_WIDTH,nCol*IMAGE_HEIGHT, (nRow+1)*IMAGE_WIDTH,(nCol+1)*IMAGE_HEIGHT, this); //往小拼图上画 } 画出界面 图片装载完毕,下一步我们就来设计界面,并画出来。 我们准备让界面在左边留下一定的区域用来显示游戏的一些信息,如玩家走了多少步 170 Java 游戏编程导学 了,玩家是否已经赢了等等;然后在右边将拼图的图片画出。可以定义一个常量来标识左 边提示信息区域的宽度(高度为整个Applet的高度)。 final int DELTAX=120; //标志提示信息区的宽度 为了标志现在各个拼图的排列情况,我们定义一个二维数组: int m_nImageNo[][]=new int[3][3]; //标志现在各个拼图的排列情况 数组下标表明了这个地方为哪张拼图,如项m_nImageNo[i][j]表明第(j+1)行第(i+1) 列上的拼图为第m_nImageNo[i][j]张拼图。 如果在某一位置没有拼图,怎么存储呢? 我们定义一个常量,当m_nImageNo的某一项等于这个常量时,就表明这个位置的拼图 为空: final int NO_IMAGE=-1; //此位置没有拼图 我们还必须定义一个变量,用于存储当前玩家所走的步数和已经玩的时间: int nStep=0; //已经走的步数 int nTime=0; //已经玩过的时间,以秒为单位 下面我们定义一个方法,用来初始化各个拼图的排列情况。我们先简单的写这个方法, 将拼图按顺序排列在各个位置上。然后将第3行第2列的拼图初始化为第8个拼图。将第2行 第2列的拼图初始化为无。 public void initData() { for(int j=0;j<3;j++) { for(int i=0;i<3;i++) { m_nImageNo[i][j]=j*3+i; } } m_nImageNo[1][2]=NO_IMAGE; m_nImageNo[1][1]=7; } 在init()方法的最后调用initData()方法,来初始化界面的拼图状态。 我们应该遍历整个m_nImageNo,按照这个数组所提供的信息来画出界面。下面这些代 码都是写在Applet的paint()方法里的。 (1)画左边的提示信息区域 将当前颜色置为白色,用白色来填充左边的提示信息区域: 第 5 章 拼图游戏——Applet 和线程 171 g.setColor(Color.white); //将当前颜色置为白色 g.fillRect(0,0,DELTAX,IMAGE_HEIGHT*3); //填充左边的提示信息区域 g.setFont(new Font("宋体",Font.PLAIN,15)); //设置字体 g.setColor(Color.blue); //设置颜色 g.drawString("步数:"+nStep,10,20); //在坐标(10,20)画出字符串,来显示现在走了多少步 (2)画拼图区域 遍历m_nImageNo数组,按数组里所表述的拼图的排列顺序画出拼图区域: g.setColor(Color.white); for(int i=0;i<3;i++) { for(int j=0;j<3;j++) { int x=i*IMAGE_WIDTH+DELTAX; int y=j*IMAGE_HEIGHT; //计算要画的拼图的区域 if(m_nImageNo[i][j]==NO_IMAGE) //用空白来填充 g.fill3DRect(x,y,IMAGE_WIDTH,IMAGE_HEIGHT,true); else { g.drawImage(m_Image[m_nImageNo[i][j]],x,y,this); g.drawRect(x,y,IMAGE_WIDTH,IMAGE_HEIGHT); } } } 这样一来画图区域就画好了。整个画图部分的代码如下: public void paint(Graphics g) { g.setColor(Color.white); //将当前颜色变为白色 g.fillRect(0,0,DELTAX,IMAGE_HEIGHT*3); //填充左边的提示信息区域 g.setFont(new Font("宋体",Font.PLAIN,15)); //设置字体 g.setColor(Color.blue); //设置颜色 g.drawString("步数:"+nStep,10,20); //在坐标(10,20)画出字符串,来显示现在走了多少步 172 Java 游戏编程导学 g.setColor(Color.white); for(int i=0;i<3;i++) { for(int j=0;j<3;j++) { int x=i*IMAGE_WIDTH+DELTAX; int y=j*IMAGE_HEIGHT; if(m_nImageNo[i][j]==NO_IMAGE) g.fill3DRect(x,y,IMAGE_WIDTH,IMAGE_HEIGHT,true); else { g.drawImage(m_Image[m_nImageNo[i][j]],x,y,this); g.drawRect(x,y,IMAGE_WIDTH,IMAGE_HEIGHT); } } } } 现在运行Applet,结果如图5.3所示。 图 5.3 现在的运行界面 5.3.3 事件处理 本节我们将增加对鼠标和键盘事件的处理,使游戏能够玩起来,能够判断游戏是否结 束,并计算出玩家的得分。我们仍然采用Java 2最新的消息处理机制,这种消息机制比较接 近于通用的监听器的概念,是笔者强烈建议大家使用的一种消息机制。 第 5 章 拼图游戏——Applet 和线程 173 鼠标事件处理 游戏应该让玩家在用鼠标点击能够移动的拼图时,移动拼图到周围空格的地方。 当我们点击左下角的拼图时,它就会移动到周围的空格中,如图5.4所示。 图 5.4 点击鼠标后 参考第4章的鼠标和键盘消息处理,加上鼠标Listener,实现它的接口。 1. 首先导入包含MouseListener接口的包,这一步JBuilder已经为我们完成了。 import java.awt.event.*; 2. 声明实现MouseListener接口: public class pintu extends Applet implements MouseListener 这个时候在JBuilder的代码编辑器边缘的左边会出现错误提示,一个红色的X,因为我 们还没有在代码中实现接口所需的方法。这里有一个添加代码框架的简便方法,点击这个 错误标记,就会弹出一个改正方法窗口,里面显示了JBuilder提供给读者的几种修改建议, 如图5.5所示。 174 Java 游戏编程导学 图 5.5 JBuilder 的错误提示 在这个修改建议中我们选择Implement Methods(实现方法),因为我们知道错误是由 于没有实现方法造成的,生成代码如下: 注意:这里的注释是我们添加的,是为了可以让读者看得更清楚一些。 public void mouseClicked(MouseEvent e){ //当鼠标在组件上点击时被调用 } public void mouseEntered(MouseEvent e){ //当鼠标移进组件范围时被调用 } public void mouseExited(MouseEvent e){ //当鼠标移出组件范围时被调用 } public void mousePressed(MouseEvent e){ //当鼠标在组件上按下时被调用 } public void mouseReleased(MouseEvent e){ //当鼠标在组件上释放时被调用 } 3. 给这个Applet添加MouseListener: 0 第 5 章 拼图游戏——Applet 和线程 175 public void init() { … addMouseListener(this); } 4. 在mouseClicked()方法里加入功能代码 (1)首先判断出鼠标点击的是哪个拼图。 int nX=e.getX()-DELTAX; int nY=e.getY(); int nCol=nY/IMAGE_HEIGHT; int nRow=nX/IMAGE_WIDTH; (2)然后判断这个拼图可以往哪个方向移动。 这一部分的算法稍微长了点,我们为它建立一个独立的方法directionCanMove(int nCol,int nRow),返回整型值。这里用一个整型来代表这个拼图可以移动的方向。 定义Applet的常量如下: final int DIRECTION_UP=1; //代表移动方向为向上 final int DIRECTION_DOWN=2; //代表移动方向为向下 final int DIRECTION_LEFT=3; //代表移动方向为向左 final int DIRECTION_RIGHT=4; //代表移动方向为向右 final int DIRECTION_NONE=-1; //代表不能移动 该方法的代码如下: public int directionCanMove(int nCol,int nRow) { if((nCol-1)>=0) if(m_nImageNo[nRow][nCol-1]==NO_IMAGE) return DIRECTION_UP; if((nCol+1)<=2) if(m_nImageNo[nRow][nCol+1]==NO_IMAGE) return DIRECTION_DOWN; if((nRow-1)>=0) if(m_nImageNo[nRow-1][nCol]==NO_IMAGE) return DIRECTION_LEFT; if((nRow+1)<=2) if(m_nImageNo[nRow+1][nCol]==NO_IMAGE) return DIRECTION_RIGHT; return DIRECTION_NONE; } 算法的简要说明:4个方向上依次判断有没有拼图,若没有,则返回这个方向值。若4 176 Java 游戏编程导学 个方向都有拼图存在,则返回一个整型值标志不能移动。 (3)若可以移动则移动该拼图。 public void move(int nCol,int nRow,int nDirection) { //nCol和nRow为要移动的拼图的位置 switch(nDirection) { case DIRECTION_UP: m_nImageNo[nRow][nCol-1]=m_nImageNo[nRow][nCol]; m_nImageNo[nRow][nCol]=NO_IMAGE; break; case DIRECTION_DOWN: m_nImageNo[nRow][nCol+1]=m_nImageNo[nRow][nCol]; m_nImageNo[nRow][nCol]=NO_IMAGE; break; case DIRECTION_LEFT: m_nImageNo[nRow-1][nCol]=m_nImageNo[nRow][nCol]; m_nImageNo[nRow][nCol]=NO_IMAGE; break; case DIRECTION_RIGHT: m_nImageNo[nRow+1][nCol]=m_nImageNo[nRow][nCol]; m_nImageNo[nRow][nCol]=NO_IMAGE; break; } } 算法的简要说明:依次判断是哪个方向,然后按照不同的方向对m_nImageNo进行不同 的操作。 (4)写好了上面的两个方法,就可以在mouseClicked()方法里添加如下代码了: int nDirection=directionCanMove(nCol,nRow); if(nDirection!=DIRECTION_NONE) { move(nCol,nRow,nDirection); nStep++; } 代码说明:先判断可以往哪个方向移动,若可以移动(即可以移动的方向不是 DIRECTION_NONE),就移动拼图,并将玩家所走的步数加1。 (5)重画。 repaint(); 这样就完成了鼠标的处理,我们可以用鼠标来玩这个游戏了。下面我们继续加入键盘 控制。 键盘事件处理 用键盘来玩游戏和用鼠标玩起来在控制方法上有点不一样。用鼠标玩,操纵的就是我 第 5 章 拼图游戏——Applet 和线程 177 们点击的那张拼图。但是,用键盘怎么玩呢?我们这样来规定,用键盘的方向键移动拼图。 图5.6为游戏的任一状态,能往下移动的拼图有且仅有一个,那就是最右边中间的那张拼图。 图 5.6 方向键按下前 当我们按向下移动方向键的时候,游戏就会变成如图5.7所示的状态。 图 5.7 按向下移动方向键后 参考上一章的鼠标和键盘消息处理,加上键盘Listener,并实现它的接口,步骤如下: 1. MouseListener和KeyListener都放在包java.awt.event里,前面已导入该包。 2. 声明实现KeyListener接口: public class pintu extends Applet implements MouseListener,KeyListener { … } 178 Java 游戏编程导学 3. 实现接口方法: public void keyPressed(KeyEvent e) { //Invoked when a key has been pressed. } public void keyReleased(KeyEvent e) { //Invoked when a key has been pressed. } public void keyTyped(KeyEvent e) { //Invoked when a key has been pressed. } 4. 给这个Applet添加KeyListener: public void init() { … addKeyListener(this); } 5. 在mouseClicked方法里加入功能代码。 (1)首先判断按下了哪个方向键: int nDirection=Dire switch(e.getKeyCode()) { case KeyEvent.VK_DOWN: nDirection=DIRECTION_DOWN; break; case KeyEvent.VK_UP: nDirection=DIRECTION_UP; break; case KeyEvent.VK_LEFT: nDirection=DIRECTION_LEFT; break; case KeyEvent.VK_RIGHT: nDirection=DIRECTION_RIGHT; break; default: return; } 算法说明:如果按下方向键,则将方向存到nDirection里,否则返回。 (2)往某个方向移动拼图。 这个算法比较繁琐一点,我们定义方法move(int nDirection)来完成。在这个方法内部首 第 5 章 拼图游戏——Applet 和线程 179 先要判断哪个拼图是可以往方向nDirection移动的,然后就移动这个拼图。 public void move(int nDirection) { //往某个方向移动拼图 int nNoImageCol=-1; int nNoImageRow=-1; int i=0; int j=0; while (i<3 && nNoImageRow==-1) { while (j<3 && nNoImageCol==-1) { if(m_nImageNo[i][j]==NO_IMAGE) { nNoImageRow=i; nNoImageCol=j; } j++; } j=0; i++; } //以上判断哪个拼图可以往方向nDirection移动 //可以移动的拼图的位置为第nNoImageCol列,第nNoImageRow行 switch(nDirection) { case DIRECTION_UP: if(nNoImageCol==3)return; m_nImageNo[nNoImageRow][nNoImageCol]= m_nImageNo[nNoImageRow][nNoImageCol+1]; m_nImageNo[nNoImageRow][nNoImageCol+1]=NO_IMAGE; break; case DIRECTION_DOWN: if(nNoImageCol==0)return; m_nImageNo[nNoImageRow][nNoImageCol]= m_nImageNo[nNoImageRow][nNoImageCol-1]; m_nImageNo[nNoImageRow][nNoImageCol-1]=NO_IMAGE; break; case DIRECTION_LEFT: if(nNoImageRow==3)return; m_nImageNo[nNoImageRow][nNoImageCol]= m_nImageNo[nNoImageRow+1][nNoImageCol]; m_nImageNo[nNoImageRow+1][nNoImageCol]=NO_IMAGE; break; case DIRECTION_RIGHT: 180 Java 游戏编程导学 if(nNoImageRow==0)return; m_nImageNo[nNoImageRow][nNoImageCol]= m_nImageNo[nNoImageRow-1][nNoImageCol]; m_nImageNo[nNoImageRow-1][nNoImageCol]=NO_IMAGE; break; } //以上往某个方向移动拼图 } (3)在mouseClick里调用move(int nDirection)方法: move(nDirection); (4)将玩家移动的步数加1: nStep++; (5)重画界面: repaint(); 加入了以上代码之后,游戏就可以玩了。但是游戏还没有判断出什么时候赢呢!下面 我们来实现这个功能。 5.3.4 让游戏能够判断游戏当前状态,并能重新开始 下面我们应该先写一个方法,用来判断此时由数组m_nImageNo记录的状态是否已经赢 了。 判断游戏当前状态的方法 我们只要判断8张拼图是否都已经放到了正确的位置上就行了。如果是,游戏结束并给 出游戏的分数,并让玩家按任意键可以重新开始游戏。 先定义一个Applet的成员bWantStartNewGame,用来标志游戏是否结束,是否需要重新 开始新游戏: boolean bWantStartNewGame=false; 方法如下: public void checkStatus() { boolean bWin=true; //定义成员,默认值为真 int nCorrectNum=0; for(int j=0;j<3;j++) { for(int i=0;i<3;i++) { if(m_nImageNo[i][j]!=nCorrectNum && 第 5 章 拼图游戏——Applet 和线程 181 m_nImageNo[i][j]!=NO_IMAGE) bWin=false; nCorrectNum++; } } //比较拼图是否都放到了正确的位置上 //如果有一个没有放到正确位置上,则游戏就不能结束 if(bWin)bWantStartNewGame=true; } 调用这个方法后,会将当前的游戏状态存在bWantStartNewGame里,通过查看 bWantStartNewGame的值,就可以得知当前的游戏状态:是还没有结束,还是玩家已经赢 得了这盘游戏。 调用判断游戏状态的方法 判断当前游戏状态的方法写好了,我们应该在玩家移动了某一个拼图之后调用。由于 每次移动拼图之后,都要调用重画方法,所以,我们将这个方法的调用放到paint()方法里。 在paint()里我们增加如下代码,让游戏能判断当前状态,并能够在游戏可以结束时打 印出有关信息,如“你赢了”之类的信息: public void paint(Graphics g) { //画图 … //此处省略了画游戏界面的代码 checkStatus(); //检查当前状态 if(bWantStartNewGame) { g.setColor(Color.blue); g.drawString("请按任意键重新开始",0,60); g.setColor(Color.red); g.setFont(new Font("宋体",Font.PLAIN,40)); g.drawString("你赢了,祝贺你!",30+DELTAX,180); //在界面上显示出玩家赢了的信息 } } 加入上面的代码后,游戏就可以判断出拼图是否已经拼好。拼好后,则会显示相应信 息,如图5.8所示。 182 Java 游戏编程导学 图 5.8 游戏结束后的界面 5.3.5 让游戏的每次初始化状态都不一样 我们在前面简单地写了一下初始化游戏状态的方法,但是这个初始化方法过于简单, 使得每次游戏开始时的游戏状态都是一样的,玩家只要玩过一次后,就没有什么可玩的了。 这里我们用Math包里的random()方法来产生随机数,用这个随机数来初始化游戏的状 态。这样一来,玩家每次开始玩游戏时,游戏状态都不一样,玩家玩得才会有兴致。 random():返回一个double类型的值,这个值是正的,大于等于0,小于1。 使用这个方法必须要注意,每个拼图只能在游戏界面里出现一次,也就是说我们应该 在9张拼图里随机挑出其中的8张,随机分配到9个位置中的8个位置。 代码如下: public void initData() { for(int i=0;i<9;i++)nHasDistrib[i]=0; //记录每个拼图的使用情况 for(int j=0;j<3;j++) { for(int i=0;i<3;i++) { int nCount=j*3+i; int nImgNo=-1; do { nImgNo=(int)(Math.random()*9); }while(nHasDistrib[nImgNo]==1); //1代表已经分配了这张拼图 //若此拼图已经被分配,则给这个位置从新分配拼图 m_nImageNo[i][j]=nImgNo; 第 5 章 拼图游戏——Applet 和线程 183 nHasDistrib[nImgNo]=1; System.out.println("test.."); } } m_nImageNo[(int)(Math.random()*3)][(int)(Math.random()*3)] =NO_IMAGE; nStep=0; //初始化玩家走的步数 } 5.3.6 消除闪烁问题 我们又遇到了动画闪烁的问题。这次我们采用第4章讲过但没有使用的一种方法,就是 重载Applet的默认update()方法。这种方法我们不提倡使用,但是它用起来很方便。在这种 小游戏里,它引起的负面效应看不出来。但是在大游戏里或做一些大项目的时候,笔者建 议使用第4章用到的方法。 我们知道,默认的update()方法是先清除小应用程序窗口(用背景颜色重新绘制该窗 口),然后再调用paint()方法来重新画图。下面我们来重载这个方法,使其不清除小应用 程序窗口(用背景颜色重新绘制该窗口),而直接调用paint()方法来重新画图。 我们将下面的代码加入到程序中去: public void update(Graphics g) { paint(g); } 我们的程序在调用repaint()方法时,就会调用我们定义的update()方法,而不是Applet 默认的update()方法。我们的update()方法实现在重画的时候,不让屏幕用背景色清除,这 样就会大大的减轻闪烁效果。 到现在为止,我们终于可以舒舒服服地来玩这个游戏了。下面我们还要再给这个游戏 加上一些其他的功能,使它更完善。 5.3.7 让游戏记录玩家所用的时间,并计算出分数 这里我们通过实现Runnable接口来实现多线程。一般的过程如下: (1)首先我们声明要实现Runnable接口: public class pintu extends Applet implements MouseListener,KeyListener,Runnable { … } (2)实现接口里的方法: public void run() 184 Java 游戏编程导学 { //这个线程里我们要做的事情 } (3)要启动这个线程,可以这样写: Thread thThread=new Thread(this); //创建一个Thread对象来跟踪这个线程 thThread.start(); //开始这个线程 5.3.8 利用多线程技术来实现计时器,记录玩家玩的时间 下面是实现的步骤。 1. 首先定义Applet的变量来记录当前玩的时间和计时器线程的对象: int nTime=0; //已经玩过的时间,以秒为单位 Thread thTimer; //计时器线程 2. 声明要实现Runnable接口: public class pintu extends Applet implements MouseListener,KeyListener,Runnable { … } 3. 实现接口里的方法。 我们可以编写功能代码,使得程序能在玩家开始游戏后,隔一秒钟在浏览器的状态栏 里打印出游戏已经玩过的时间: public void run() { //这个线程里我们要做的事情 //我们将当前用户玩得时间显示出来 while(true) { try { thTimer.sleep(990); String sTemp="你玩了"+nTime+"秒的时间,"; if(nTime>200)sTemp=sTemp+"时间用的很长了,你可要加油啦!"; else sTemp=sTemp+"别紧张,慢慢来。"; showStatus(sTemp); if(!bWantStartNewGame)nTime++; //如果游戏已经结束,则不将时间累加 } 第 5 章 拼图游戏——Applet 和线程 185 catch(Exception e) { } } } 4. 在init()方法里新建一个线程: thTimer=new Thread(this); //为线程分配内存空间 thTimer.start(); //开始线程 5. 在initData里将nTime置为0: nTime=0; //清空计时器 6. 计算玩家的分数,并将结果显示出来。 这里我们采用(1000-步数*10-时间)来记分。分数记录在Applet的成员变量nScore里。 (1)定义Score: int nScore=0; //玩家所得的分数 (2)在paint()方法里将玩家所得的分数画出: if(bWantStartNewGame) { //如果游戏结束,即玩家将拼图的顺序排对之后 nScore=1000-nStep*10-nTime; g.setColor(Color.blue); g.drawString("请按任意键重新开始",0,60); g.setColor(Color.red); g.setFont(new Font("宋体",Font.PLAIN,40)); g.drawString("你赢了"+nScore+"分",70+DELTAX,160); g.drawString("祝贺你!",110+DELTAX,210); } 这样一来,我们便实现了计数器,并能够对玩家玩的时间计时,最后将玩家的分数计 算出来。游戏的界面如图5.9所示。 186 Java 游戏编程导学 图 5.9 游戏加上了计时器后的界面 5.3.9 用F1键重新开始游戏 我们应该在keyPress里加入如下代码,使程序能够在按下F1键时,游戏执行initData() 方法,开始一盘新的游戏。 public void keyPressed(KeyEvent e) { //Invoked when a key has been pressed. if(bWantStartNewGame) { initData(); bWantStartNewGame=false; repaint(); return; } int nDirection=DIRECTION_NONE; switch(e.getKeyCode()) { case KeyEvent.VK_DOWN: nDirection=DIRECTION_DOWN; break; case KeyEvent.VK_UP: nDirection=DIRECTION_UP; break; case KeyEvent.VK_LEFT: System.out.println("left111..."); nDirection=DIRECTION_LEFT; 第 5 章 拼图游戏——Applet 和线程 187 break; case KeyEvent.VK_RIGHT: System.out.println("left..."); nDirection=DIRECTION_RIGHT; break; //the Code Added here case KeyEvent.VK_F1: //F1键按下,重新开始游戏 initData(); repaint(); return; default: return; } move(nDirection); nStep++; repaint(); } 这样,在按下F1键时就可以重新开始游戏了。 5.3.10 利用HTML的param标记来改变不同的图片 我们上面讲的只是使用一张图片,将这张图片分成9张拼图,让玩家来拼。假如玩家对 图的美观表示怀疑的话,怎么办?当然是换一张图了。 下面用html标记来实现可以让玩家随便改变图片的功能。 HTML 的 param 标记 param标记是嵌入在applet标记之间的一种标记。 如下例: param有两个参数:一个是name参数,后跟着param的名字;一个是value,跟着这个标 记的值。对上面这个例子,我们可以说:font参数标记的值是字符串“宋体”,style参数标 记的值是字符串PLAIN,size参数标记的值是数字12。 在 Applet 里取得 param 标记的值 我们可以在Applet里使用getParameter()方法来取得param标记的值: getParameter(String name) 188 Java 游戏编程导学 返回名字为name的param标记的值,返回值类型为String。 对于上例,可以写下面的代码来取得param标记的值: String font=getParameter("font"); String style=getParameter("style"); String size=getParameter("size"); System.out.println("font="+font+" style="+style+" size"); 使用 param 标记来增加玩家对图片的选择 我们这样写HTML代码: 拼图游戏 拼图游戏
这是一个拼图游戏。玩家应该将打散的小图拼成一张大图。
玩家可以通过鼠标和键盘来移动小图,移动的次数和拼成
一张大图所化费的时间作为游戏得分的依据。
成绩=1000-时间(秒)-移动步数*10
按F1键重新开始该游戏
param标记中,名字为NumOfImg的param的标记表示有多少张图片,剩下的param标记 为每张图的名字。有多少张图片,就有多少个param标记来表示图的名字。 我们设计用数字键来重新装入新的图片。若有3张图片,则我们就应该在程序里做到用 户可以用数字键1、2、3来选择是用哪张图片,而对于其他的数字键则不响应,并会在浏览 器的状态栏里告诉玩家没有这张图片。最多可以改变9张图片。 下面我们来实现这个功能。 1. 定义一个Applet的成员变量用来存储一共有多少个可以使用的图: int m_nNumOfImg=0; 第 5 章 拼图游戏——Applet 和线程 189 //拼图底图所使用的图片的个数 2. 定义一个Applet的成员数组用来存储一共有多少个可以使用的图的名字: String m_sImgName[]=new String[9]; //记录拼图底图的名字 3. 首先在init()方法里取得各个param参数的值。代码如下: public void init() { String param=getParameter("NumOfImg"); try { m_nNumOfImg=Integer.parseInt(param); } catch(Exception e) { m_nNumOfImg=1; System.out.println("Can't convert the param's name to int."); } for(int i=0;im_nNumOfImg) { //当nImgNo大于可以使用的图片总数 showStatus("你要的图片不存在!!"); return; } System.out.println(param); MediaTracker mediaTracker=new MediaTracker(this); //建立一个监视器 m_ImgAll=getImage(getDocumentBase(),"img/"+m_sImgName[0]); //装载总的大图片 mediaTracker.addImage(m_ImgAll,1); try { mediaTracker.waitForAll(); } catch(Exception e) { System.out.println("图片装载出错"); } for(int i=0;i<9;i++) { m_Image[i]=createImage(IMAGE_WIDTH,IMAGE_HEIGHT); Graphics g=m_Image[i].getGraphics(); int nRow=i%3; int nCol=i/3; g.drawImage(m_ImgAll,0,0,IMAGE_WIDTH,IMAGE_HEIGHT, nRow*IMAGE_WIDTH,nCol*IMAGE_HEIGHT, (nRow+1)*IMAGE_WIDTH ,(nCol+1)*IMAGE_HEIGHT,this); } } 第 5 章 拼图游戏——Applet 和线程 191 5. 在键盘事件里处理按下键盘的消息。我们需要增加功能代码,使得程序能够在按下 1~9的数字键时判断是否存在序号为按下键的数字的图片,若有,则重新装载图片,重新开 始游戏。 public void keyPressed(KeyEvent e) { … //省略了一些代码 switch(e.getKeyCode()) { case KeyEvent.VK_DOWN: nDirection=DIRECTION_DOWN; break; case KeyEvent.VK_UP: nDirection=DIRECTION_UP; break; case KeyEvent.VK_LEFT: System.out.println("left111..."); nDirection=DIRECTION_LEFT; break; case KeyEvent.VK_RIGHT: System.out.println("left..."); nDirection=DIRECTION_RIGHT; break; case KeyEvent.VK_F1: //F1键按下,重新开始游戏 initData(); repaint(); return; //下面是增加的代码 case KeyEvent.VK_1: case KeyEvent.VK_2: case KeyEvent.VK_3: case KeyEvent.VK_4: case KeyEvent.VK_5: case KeyEvent.VK_6: case KeyEvent.VK_7: case KeyEvent.VK_8: case KeyEvent.VK_9: int nImgNo=e.getKeyCode()-KeyEvent.VK_1; if(nImgNo


这么一个简单的表单可以通过页面将数据传给服务器的核心语句是:
第 5 章 拼图游戏——Applet 和线程 199 这句话告诉网页表单里填写的东西要通过HTTP的put()方法发回服务器。这个put()方法 的实质是什么呢? 可以看一下图5.13。 图 5.13 查询后的结果 详细看一下这张图的地址栏,地址栏里在要接受信息的CGI地址之后又附加了一些信 息,这些信息恰好就是我们要发送的信息。这样我们就可以明白put()方法是通过在请求页 的后面添加参数来实现往服务器传递信息的目的的。 下面我们也用这种办法来实现将分数传递给服务器的目的。 具体实现 前面我们讲到了HTTP的put请求的实质就是在要接受信息的页面(CGI)的地址后面添 加参数。下面我们也用这种办法,在Applet里打开一个页面,页面的地址为CGI的地址加上 参数,如http://162.105.101.107/a.php3?score=1222,这样就可以将分数传递给服务器了。 我们先写一个transferScore的方法,在这个方法里实现了将分数传递给服务器的功能。 我们可以使用showDocument方法,使得页面重新打开到一个CGI上,并在这个CGI的打开 地址后面附上一个score的参数,这里我们将分数进行了简单的加密。 在使用showDocument()方法之前我们使用方法getAppletContext()取得这个Applet的 AppletContext的接口。代码如下: public void transferScore(int nScore) { String sLocation="http://162.105.101.107/saveScore.php3?score="; nScore=(nScore/2)*10+nScore%4; //加密分数 sLocation=sLocation+nScore; URL url=null; try { url=new URL(sLocation); } catch(Exception e){} getAppletContext().showDocument(url); } 我们还要在游戏结束时调用transferScore()方法将分数传递给服务器: public void paint(Graphics g) 200 Java 游戏编程导学 { … //省略代码 if(bWantStartNewGame) { //如果游戏结束,玩家将拼图的顺序排对之后 nScore=1000-nStep*10-nTime; g.setColor(Color.blue); g.drawString("请按任意键重新开始",5,140); g.setColor(Color.red); g.setFont(new Font("宋体",Font.PLAIN,40)); g.drawString("你赢了"+nScore+"分",70+DELTAX,160); g.drawString("祝贺你!",110+DELTAX,210); //增加的代码 transferScore(nScore); //增加的代码结束 } } 这样当我们结束游戏时,游戏就会自动装载CGI的那个页面,将分数传递给服务器。 如图5.14所示。 图 5.14 将分数传到服务器上 5.4 本章知识点回顾 init()方法,在Applet被执行的时候首先调用来初始化变量,程序运行期 间只调用一次 start()方法在init()之后调用,或者是在Applet重新启动时调用 在每一次Applet的输出必须重画窗口时,paint()方法会被调用 在Web浏览器离开包含Applet的HTML文件的时候,stop()方法会被调用 Applet框架 当Applet需要完全移出内存时,会调用destory()方法,这时所有Applet 占用的资源就应该被释放 第 5 章 拼图游戏——Applet 和线程 201 (续表) 从一个init()方法到一个destroy()方法,称为Applet的一个生命周期 在很多情况下,Applet中都需要重载一个AWT定义的方法update(),这 个方法在Applet要求窗口的一部分被重画时调用,重载这个方法可以避 免画面闪烁 repaint()方法是AWT定义的,它使得AWT的运行时环境执行对Applet的 update()方法的调用,默认情况下,update()会调用repaint()方法 AppletContext接口的主要方法 getApplet(String name) 返回这个网页上名为name的Applet,用于同一网页上的两个Applet之间 的通信 getApplets() 返回这个网页上所有的Applet,用于同一网页上的几个Applet之间的通 信 getAudioClip(URL url) 从网络地址url处取得声音文件,并创建一个AudioClip实例,与这个声 音文件建立关联 getImage(URL url) 从网络地址url处取得图片文件,并创建一个Image实例,与这个图片文 件建立关联 showDocument(URL url) 重新装载页面,使页面的地址为给定的url showDocument(URL url, String target) 同上 showStatus(String status) 与前面用到的方法功能类似,用于在状态栏上显示信息status 为了创建一个新的线程,必须扩展Thread或者实现Runnable接口 线程具有优先级,级别在MIN_PRIORITY和MAX_PRIORITY之间,通 常它们分别为1和10。一个线程默认的优先级是NORM_PRIOPITY,值 为5,这些优先级在Thread中都被定义为final型常量 当两个或两个以上的线程需要共享资源时,它们之间就需要一种方法来 确定资源在某一时刻仅仅被一个线程占用。达到此目的的过程叫做同步 (Synchronization) 线程技术 多线程存在于一个内存空间里,当其中的一个线程崩溃之后,会影响其 他的线程。而多进程则不是,一个进程崩溃后,其他进程仍然不受影响 地运行。在另一方面,跟踪一个线程所需的开销要少于跟踪一个进程所 需的开销 Thread类的几个主要方法 Thread(Runnable target) 构造方法,构造一个新的Thread实例,实例的run()方法在target里定义 currentThread() 静态方法,返回当前执行的Thread对象,返回值类型为Thread yield() 静态方法,使当前线程暂停,返回值类型为void sleep(long millis) 静态方法,使当前线程暂停millis毫秒,返回值类型为void 202 Java 游戏编程导学 (续表) sleep(long millis, int nanos) 静态方法,使当前线程暂停millis毫秒加上nanos纳秒,返回值类型为void start() 开始一个线程,返回值类型为void run() Runnable接口里的run方法,返回值类型为void interrupt() 中断一个线程,返回值类型为void interrupted() 判断线程是否已经中断,如果线程已经中断,返回true,并将线程的中 断标志取消。返回值类型为boolean isInterrupted() 判断线程是否已经中断,如果线程已经中断,返回true,不会影响线程 的中断标志。返回值类型为boolean destroy() 撤消线程,返回值类型为void isAlive() 判断线程是否被激活,返回值类型为boolean setPriority(int priority) 设置线程的优先级,priority越大优先级越高。返回值类型为void setName() 设置线程的名字,返回值类型为void getPriority() 取得线程的优先级,返回值类型为int getName() 取得线程的名字,返回值类型为string aliveCount() 静态方法,取得当前存活的线程总数,返回值类型为int 第 6 章 Swing 和 I/O 简介 这一章我们来介绍Swing。javax.swing包是SUN公司推出的针对AWT的一些缺点做出 改进的轻量级纯粹Java的GUI开发包。它是AWT的扩展,提供更强大和更灵活的组建集合。 Swing不但包括AWT中已经有的组件(如按钮、复选框和标签等),还包括许多新的组件, 如选项板、滚动窗格、树和表格。 6.1 Swing组件简介 在Swing包中的类和接口的数量众多,本章只简单介绍其中的一部分。 6.1.1 JApplet Swing的基础是JApplet类,JApplet是从Applet类扩展而来的。使用Swing的Applet必须 是JApplet类的子类。除了保持原有的框架函数外,JApplet还增加了许多Applet没有的功能。 例如JApplet支持多种窗格,如内容窗格、透明窗格和根窗格。 注意:Applet和JApplet的不同之处是,在JApplet实例中增加一个组件,要先 调用add()方法增加一个内容窗格,再使用容器的add()方法在内容窗格中增加 一个组件。 6.1.2 按钮类 Swing的按钮比较AWT的按钮增加了更多功能,如可以用一个图标来修饰。JComponent 衍生AbstractButton类,而JButton是AbstractButton类的子类,AbstractButton类包含了多种控 制按钮行为的方法: void setDisabledIcon(Icon di) void setPressedIcon(Icon pi) void setSelectedIcon(Icon si) void setRolloverIcon(Icon ri) 这几种方法分别用来控制按钮在禁止、按下或者选择的时候,显示的不同图标。 和AWT中的按钮一样,AbstractButton在按钮按下时生成行为事件,我们可以按如下方 法注册和注销这些事件的监听器: void addActionListener(ActionListener a) void removeActionListener(ActionListener a) 0 204 Java 游戏编程导学 JButton 下面我们再来介绍一下JButton类,这是我们在实际中经常用到的,由于衍生自 AbstrctButton类,所以同前面一样可以注册和注销时间的监听器。JButton类的构造函数如 下: JButton(Icon i) JButton(String s) JButton(String s , Icon i) 下面我们简单编写一个例子说明一下JButton的用途: package test; import java.awt.*; import java.awt.event.*; import java.applet.*; import javax.swing.*; public class JbuttonDemo extends JApplet { private boolean isStandalone = false; JPanel jPanel1 = new JPanel(); JButton jButton1 = new JButton(); ImageIcon i1; JPanel jPanel2 = new JPanel(); JLabel jLabel1 = new JLabel(); //Construct the applet public JbuttonDemo() { } //Initialize the applet public void init() { try { jbInit(); } catch(Exception e) { e.printStackTrace(); } } //Component initialization private void jbInit() throws Exception { i1 = new ImageIcon( test.JbuttonDemo.class.getResource("icon.gif")); this.setSize(new Dimension(200,150)); jButton1.setIcon(i1); jButton1.addActionListener(new JbuttonDemo_jButton1_actionAdapter(this)); jLabel1.setText("jLabel1"); this.getContentPane().add(jPanel1, BorderLayout.CENTER); jPanel1.add(jButton1, null); this.getContentPane().add(jPanel2, BorderLayout.SOUTH); 第 6 章 Swing和 I/O 简介 205 jPanel2.add(jLabel1, null); this.setVisible(true); } //事件响应函数 void jButton1_actionPerformed(ActionEvent e) { jLabel1.setText("按钮已经按下"); this.repaint(); } } //实现事件监听器接口 class JbuttonDemo_jButton1_actionAdapter implements java.awt.event.ActionListener { JbuttonDemo adaptee; JbuttonDemo_jButton1_actionAdapter(JbuttonDemo adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.jButton1_actionPerformed(e); } } 这个例子简单生成了一个JApplet对象,显示一个带有图标的按钮jButton1和一个标签 jLabel1,当按钮按下以后,标签上的字变为“按钮已经按下”,结果如图6.1所示。 图 6.1 JButton 程序输出 JRadioButton 除了JButton以外,AbstractButton的子类中还有JRadioButton类,它提供了单选按钮的 功能。构造函数如下: JRadioButton(Icon i) JRadioButton(Icon i,boolean state) JRadioButton(String s) 206 Java 游戏编程导学 JRadioButton(String s,boolean state) JRadioButton(String s,Icon i) JRadioButton(String s,Icon I,boolean state) 其中,i是按钮的图标,按钮上的标签由s指定,如果state为true,复选框在初始状态下 显示为被选中,否则相反。 单选按钮必须配置成组,每次一个组内只能选中一个按钮。这样我们需要先实例化一 个ButtonGroup,然后调用add()方法把元素加进按钮组。 void add(AbstractButton ab) 单选按钮产生的行为由actionPreformed()处理。 JCheckBox AbstractButton类中另外一个比较重要的子类是JCheckBox类,它用来提供复选框的功 能,构造函数如下: JCheckBox(Icon i) JCheckBox(Icon i , boolean state) JCheckBox(String s) JCheckBox(String s,boolean state) JCheckBox(String s,Icon i) JCheckBox(String s,Icon i,boolean state) 其中,i是按钮的图标,按钮上的标签由s指定,如果state为true,复选框在初始状态下 显示为被选中,否则相反。 当选中或者取消复选框时,生成一个项目事件。这个事件由itemStateChanged()处理。 在itemStateChanged()中,getItem()方法获取产生时间的JCheckBox对象。 下面是一个关于按钮类的例子。在这个例子中有两组按钮和两个文本框,jTextField1 对应单选按钮组buttonGroup1,当选中其中一个的时候,文本框jTextFiled1中显示对应的单 选按钮标签;jTextField2对应jCheckBox1,选中jCheckBox1时,文本框jTextField2显示对应 标签: package test; import java.awt.*; import java.awt.event.*; import java.applet.*; import javax.swing.*; public class RnCDemo extends JApplet { private boolean isStandalone = false; JPanel jPanel1 = new JPanel(); JPanel jPanel2 = new JPanel(); ButtonGroup buttonGroup1 = new ButtonGroup();//实例化按钮组 BorderLayout borderLayout1 = new BorderLayout(); JPanel jPanel3 = new JPanel(); //实例化单选按钮 JRadioButton jRadioButton1 = new JRadioButton(); 第 6 章 Swing和 I/O 简介 207 JRadioButton jRadioButton2 = new JRadioButton(); JRadioButton jRadioButton3 = new JRadioButton(); //实例化复选框 JTextField jTextField1 = new JTextField(); JPanel jPanel4 = new JPanel(); JCheckBox jCheckBox1 = new JCheckBox(); JTextField jTextField2 = new JTextField(); //applet构造函数 public RnCDemo() { } //初始化applet public void init() { try { jbInit(); } catch (Exception e) { e.printStackTrace(); } } //组件初始化 private void jbInit() throws Exception { this.setSize(new Dimension(400, 300)); jPanel1.setLayout(borderLayout1); jRadioButton1.setText("jRadioButton1"); jRadioButton1.addActionListener(new RnCDemo_jRadioButton1_actionAdapter(this)); jRadioButton2.setText("jRadioButton2"); jRadioButton2.addActionListener(new RnCDemo_jRadioButton2_actionAdapter(this)); jRadioButton3.setText("jRadioButton3"); jRadioButton3.addActionListener(new RnCDemo_jRadioButton3_actionAdapter(this)); jTextField1.setText("jTextField1"); jCheckBox1.setText("jCheckBox1"); jCheckBox1.addItemListener(new RnCDemo_jCheckBox1_itemAdapter(this)); jTextField2.setText("jTextField2"); this.getContentPane().add(jPanel1, BorderLayout.CENTER); jPanel1.add(jPanel3, BorderLayout.NORTH); jPanel3.add(jRadioButton1, null); jPanel3.add(jRadioButton2, null); jPanel3.add(jRadioButton3, null); jPanel1.add(jPanel4, BorderLayout.CENTER); jPanel4.add(jCheckBox1, null); this.getContentPane().add(jPanel2, BorderLayout.SOUTH); jPanel2.add(jTextField1, null); jPanel2.add(jTextField2, null); //向按钮组中添加JradioButton实例 buttonGroup1.add(jRadioButton3); 208 Java 游戏编程导学 buttonGroup1.add(jRadioButton2); buttonGroup1.add(jRadioButton1); this.setVisible(true); } //下面是各按钮的事件响应函数代码 void jRadioButton3_actionPerformed(ActionEvent e) { String s = jRadioButton3.getText(); jTextField1.setText(s); this.repaint(); } void jRadioButton2_actionPerformed(ActionEvent e) { String s = jRadioButton2.getText(); jTextField1.setText(s); this.repaint(); } void jRadioButton1_actionPerformed(ActionEvent e) { String s = jRadioButton1.getText(); jTextField1.setText(s); this.repaint(); } void jCheckBox1_itemStateChanged(ItemEvent e) { String s = jCheckBox1.getText(); jTextField2.setText(s); this.repaint(); } } class RnCDemo_jRadioButton3_actionAdapter implements java.awt.event.ActionListener { RnCDemo adaptee; RnCDemo_jRadioButton3_actionAdapter(RnCDemo adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.jRadioButton3_actionPerformed(e); } } class RnCDemo_jRadioButton2_actionAdapter implements java.awt.event.ActionListener { RnCDemo adaptee; RnCDemo_jRadioButton2_actionAdapter(RnCDemo adaptee) { this.adaptee = adaptee; 第 6 章 Swing和 I/O 简介 209 } public void actionPerformed(ActionEvent e) { adaptee.jRadioButton2_actionPerformed(e); } } class RnCDemo_jRadioButton1_actionAdapter implements java.awt.event.ActionListener { RnCDemo adaptee; RnCDemo_jRadioButton1_actionAdapter(RnCDemo adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.jRadioButton1_actionPerformed(e); } } class RnCDemo_jCheckBox1_itemAdapter implements java.awt.event.ItemListener { RnCDemo adaptee; RnCDemo_jCheckBox1_itemAdapter(RnCDemo adaptee) { this.adaptee = adaptee; } public void itemStateChanged(ItemEvent e) { adaptee.jCheckBox1_itemStateChanged(e); } } 程序的结果如图6.2所示。 图 6.2 单选按钮和复选框示例结果 210 Java 游戏编程导学 6.1.3 JComboBox JComboBox支持组合框,一个文本域和下拉列表的组合。JcomboBox是JComponent的 子类。组合框通常用来显示一个可选条目,即允许用户在一个下拉列表中选择不同条目。 构造函数如下,其中v是初始化选择框的矢量: JComboBox() JComboBox(Vector v) 我们通常使用addItem()方法在列表中增加选项,obj是加入组合框的对象。 Void addItem(Object obj) 当读者选中选项的时候,JComboBox对象产生一个事件,这个事件由itemStateChanged() 函数处理,同前面介绍过的JCheckBox类,在这里我们就不举例介绍了。 6.1.4 滚动窗格 滚动窗格组件是一个可以容纳其他组件的矩形区域,在必要的时候提供水平和垂直滚 动条。JScrollPane实现了这个功能。JScrollPane扩展了JComponent类。构造函数如下: JScrollPane(Component comp) JScrollPane(int vsb,int hsb) JScrollPane(Component comp,int vsb,int hsb) 其中,comp是加入滚动窗格的组件。vsb和hsb是整型常数,定义滚动窗口的水平和垂 直滚动条,它们可以取ScrollPaneConstants接口中定义的整型常数,如表6.1所示。 表6.1 ScrollPaneConstants接口中定义的整形常数 常数 描述 HORIZONTAL_SCROLLBAR_ALWAYS 总是提供水平滚动条 HORIZONTAL_SCROLLBAR_AS_NEEDED 在需要时提供水平滚动条 VERTICAL_SCROLLBAR_ALWAYS 总是提供垂直滚动条 VERTICAL_SCROLLBAR_AS_NEEDED 在需要时提供垂直滚动条 我们常常通过以下步骤在程序中增加滚动窗口: 1. 创建JComponent对象。 2. 创建JScrollPane对象,将JComponent对象加入。 3. 将滚动窗格对象加入到程序窗口中。 6.1.5 树 树对象提供了用树形结构分层显示数据的视图。用户可以扩展或收缩视图中的单个子 树。在Swing中,JComponent类的子类JTree实现了树结构。构造函数如下: JTree(Hashtable ht) 第 6 章 Swing和 I/O 简介 211 JTree(Object obj[]) JTree(TreeNode tn) JTree(Vector v) 第一种模式中,哈希表的每个元素是树的一个子节点。第二种模式中对象数组的每一 个元素都是树的一个子节点。第三种模式中,tn是树的根节点。最后一种模式向量v中的元 素是树的子节点。 当节点扩展或者收缩时,JTree对象生成事件,addTreeExpansionListener()和removeTree ExpansionListener()方法注册或者注销这些事件的监听器,如下所示: void addTreeExpansionListener(TreeExpansionListener tel) void removeTreeExpansionListener(TreeExpansionListener tel) 其中,tel是监听器TreeExpansionListener的对象。TreeExpansionListener是一个接口, 提供下列两个方法,参数tee是树的扩展事件: void treeCollapsed(TreeExpansionEvent tee) void treeExpanded(TreeExpansionEvent tee) 下面是在程序中使用树组件时应遵循的步骤: 1. 创建一个树对象。 2. 创建一个JScrollPane对象。 3. 将JTree对象加入滚动窗口。 4. 将滚动窗口加入应用程序的面板。 6.1.6 表格(JTable) Swing中的表格类由JComponent的子类JTable类实现。可以在表格的列边界上拖曳鼠标 来改变列的大小,也可将列拖放到新的位置。JTable类的构造函数如下: JTable(Object data[][],Object colHeads[]) 其中,data是一个二维数组,包含要显示的信息,colHeads是一个一维数组,包含列标 头。 下面我们举一个例子来说明滚动窗格和表格的用法: package test; import java.awt.*; import java.awt.event.*; import java.applet.*; import javax.swing.*; import javax.swing.table.*; public class TDemo extends JApplet { private boolean isStandalone = false; JPanel jPanel2 = new JPanel(); JScrollPane jScrollPane2 = new JScrollPane(); 212 Java 游戏编程导学 //创建TabelModel TableModel dataModel = new AbstractTableModel() { public int getColumnCount() { return 10; } public int getRowCount() { return 10; } public Object getValueAt(int row, int col) { return new Integer(row * col); } }; //创建JTable JTable jTable1 = new JTable(dataModel); //applet构造函数 public TDemo() {} //初始化applet public void init() { try { jbInit(); } catch (Exception e) { e.printStackTrace(); } } //组件初始化 private void jbInit() throws Exception { this.setSize(new Dimension(408, 210)); jPanel2.setBorder(BorderFactory.createLoweredBevelBorder()); jPanel2.setPreferredSize(new Dimension(220, 220)); jPanel2.setRequestFocusEnabled(true); jScrollPane2.setHorizontalScrollBarPolicy(JScrollPane. HORIZONTAL_SCROLLBAR_ALWAYS); jScrollPane2.setVerticalScrollBarPolicy(JScrollPane. VERTICAL_SCROLLBAR_ALWAYS); jScrollPane2.getViewport().setBackground(new Color(212, 208, 230)); jScrollPane2.setAlignmentX( (float) 0.5); jScrollPane2.setAutoscrolls(false); jScrollPane2.setBorder(BorderFactory.createLoweredBevel Border()); jScrollPane2.setMaximumSize(new Dimension(32767, 32767)); jScrollPane2.setMinimumSize(new Dimension(24, 28)); jScrollPane2.setOpaque(true); jScrollPane2.setPreferredSize(new Dimension(300, 200)); jScrollPane2.setRequestFocusEnabled(true); 第 6 章 Swing和 I/O 简介 213 this.getContentPane().add(jPanel2, BorderLayout.CENTER); jPanel2.add(jScrollPane2, null); jScrollPane2.getViewport().add(jTable1, null); } } 程序的结果如图6.3所示。 图 6.3 JTable 示例 6.2 I/O系统 本节将帮助大家理解标准Java 库内的各种I/O类,并学习如何使用它们。本节的第一部 分将介绍“旧”的Java 1.0 I/O流库,因为现在有大量代码仍在使用它。本节剩下的部分将 为大家引入Java 1.1 I/O库的一些新特性。我们这样做是有价值的,因为可以更清楚地认识 老方法与新方法之间的一些差异,从而加深我们的理解。 6.2.1 输入和输出 可将Java库的I/O类分割为输入与输出两个部分。通过继承,从InputStream(输入流) 衍生的所有类都拥有名为read()的基本方法,用于读取单个字节或者字节数组。类似地,从 OutputStream衍生的所有类都拥有基本方法write(),用于写入单个字节或者字节数组。然而, 我们通常不会用到这些方法;它们之所以存在,是因为更复杂的类可以利用它们,以便提 供一个更有用的接口。我们很少用单个类创建自己的系统对象。一般情况下,我们都是将 多个对象重叠在一起,提供自己期望的功能。我们之所以感到Java 的流库(Stream Library) 异常复杂,正是由于为了创建单独一个结果流,却需要创建多个对象的缘故。 214 Java 游戏编程导学 对于我们来说,很有必要按照功能对类进行分类。库的设计者首先决定与输入有关的 所有类都从InputStream继承,而与输出有关的所有类都从OutputStream 继承。 InputStream InputStream 的作用是标志那些从不同起源地产生输入的类。这些起源地包括(每个都 有一个相关的InputStream的子类,见表6.2): · 字节数组。 · String对象。 · 文件。 · “管道”。 · 其他起源地,如Internet连接等。 表6.2 InputStream的子类 类 功能 ByteArrayInputStream 允许内存中的一个缓冲区作为InputStream 使用 ByteArrayInputStream(byte[] buf) ByteArrayInputStream(byte[] buf, int offset, int length) StringBufferInputStream 将一个String(字符串)转换为InputStream StringBufferInputStream(String s) FileInputStream 用于从文件读取信息 FileInputStream(File file) FileInputStream(String name) FileInputStream(FileDescriptor fdobj) PipedInputStream 产生为相关的PipedOutputStream 写的数据 PipedInputStream() PipedInputStream(PipedOutputStream src) SequenceInputStream 将两个或更多的InputStream 对象转换成单个InputStream 使用 SequenceInputStream(Enumeration e) SequenceInputStream(InputStream i1,InputStream i2) FilterInputStream 对作为过滤器接口使用的类进行抽象;那个过滤器为其他InputStream 类提 供了有用的功能 FilterInputStream(InputStream in) 除此以外,FilterInputStream 也属于InputStream 的一种类型,它可以方便地将属性或 者有用的接口同输入流连接到一起。这将在以后讨论。 OutputStream 这一类别包括的类决定了我们的输入往何处去:一个字节数组(但没有String;假定我 们可用字节数组创建一个)、一个文件还是一个“管道”,如表6.3所示。 第 6 章 Swing和 I/O 简介 215 表6.3 OutputStream的子类 类 功能 ByteArrayOutputStream 在内存中创建一个缓冲区。我们发送给流的所有数据都会置入这个缓冲区 ByteArrayOutputStream() ByteArrayOutputStream(int size) FileOutputStream 将信息发给一个文件用一个String 代表文件名,或选用一个File或FileDescri- ptor对象 FileOutputStream(OutputStream out) PipedOutputStream 我们写给它的任何信息都会自动成为相关的PipedInputStream 的输出。实现 了“管道化”的概念 PipedOutputStream() PipedOutputStream(PipedInputStream snk) 6.2.2 FilterInputStream和FilterOutputStream FilterInputStream(其子类见表6.4)和FilterOutputStream(其子类见表6.5)提供了相应 的接口,用于控制一个特定的输入流(InputStream)或输出流(OutputStream)。它们分别 是从InputStream 和OutputStream 衍生出来的。此外,它们都属于抽象类,在理论上为我们 与一个流的不同通信手段都提供了一个接口。 表6.4 FilterInputStream的子类 类 功能 DataInputStream 与DataOutputStream 联合使用,使自己能以机动方式读取一个流中的基本数 据类型,比如int、char、long 等等 DataInputStream(InputStream in) BufferedInputStream 避免每次想要更多数据时都进行物理性的读取,要求先在缓冲区里找到 InputStream BufferedInputStream(InputStream in) BufferedInputStream(InputStream in,int size) LineNumberInputStream 可以跟踪输入流中的行号;可调用getLineNumber()以及setLineNumber(int)。 只是添加对数据行编号的能力,所以需要同一个真正的接口对象连接 LineNumberInputStream(InputStream in) 表6.5 FilterOutputStream的子类 类 功能 DataOutputStream 与DataInputStream 配合使用,以便采用方便的形式将基本数据类型int、char、 long等写入一个数据流 DataOutputStream(OutputStream out) 216 Java 游戏编程导学 (续表) 类 功能 PrintStream 用于产生格式化输出。DataOutputStream 控制的是数据的“存储”,而 PrintStream 控制的是“显示” PrintStream(OutputStream out) PrintStream(OutputStream out, boolean autoFlush) PrintStream(OutputStream out, boolean autoFlush,String encode) BufferedOutputStream 用于避免每次发出数据的时候都要进行物理性的写入 BufferedOutputStream(OutputStream out) BufferedOutputStream(OutputStream out,int size) FilterInputStream派生的DataInputStream允许读取不同的基本类型数据以及String 对象 (所有方法都以read开头,如readByte()、readFloat()等等)。伴随对应的DataOutputStream, 我们可通过数据“流”将基本类型的数据从一个地方搬到另一个地方。若读取块内的数据 并自己进行解析,就不需要用到DataInputStream。 其他的类用于修改InputStream 的内部行为方式:是否进行缓冲,是否跟踪自己读入的 数据行,以及是否能够推回一个字符等等。 与DataInputStream对应的是DataOutputStream,后者对各个基本数据类型以及String 对 象进行格式化,并将其置入一个数据“流”中,以便任何机器上的DataInputStream 都能正 常地读取它们。所有方法都以wirte开头,例如writeByte()、writeFloat()等等。 若想进行一些真正的格式化输出(如输出到控制台),则使用PrintStream。利用它可 以打印出所有基本数据类型以及String 对象,并可采用一种易于查看的格式。这与 DataOutputStream 正好相反,后者的目标是将那些数据置入一个数据流中,以便 DataInputStream能够方便地重新构造它们。很明显的一个例子就是System.out静态对象,它 是一个PrintStream。 PrintStream内两个重要的方法是print()和println()。它们已进行了覆盖处理,可打印出 所有数据类型。print()和println()之间的差异是后者在操作完毕后会自动添加一个新行。 BufferedOutputStream 属于一种“修改器”,用于指示数据流使用缓冲技术,使自己 不必每次都向流内物理性地写入数据。通常都应将它应用于文件处理和控制器I/O。 6.2.3 File File类主要提供一些判断文件属性和文件是否存在、目录里有多少文件等的方法。主要 方法如表6.6所示。 表6.6 File类的主要方法 返回类型 方法 用途 File(String pathname) 构造方法 boolean canRead() 返回这个文件是否能读 第 6 章 Swing和 I/O 简介 217 (续表) 返回类型 方法 用途 boolean canWrite() 返回这个文件是否可写 int compareTo(File pathname) 仅从字面上与pathname比较,若文件的路径名大于pathname, 则返回正数,相等则为零,小于为负数 boolean createNewFile() 如果文件不存在,则创建一个新的文件,否则返回假 File createTempFile(String prefix,String suffix) 在当前默认的临时目录里创建名字为prefix.suffix的文件 File createTempFile(String prefix, String suffix, File directory) 在directory目录里创建名字为prefix.suffix的文件 boolean delete() 删除这个文件或目录(若File的实例代表一个目录),删除 成功返回真 void deleteOnExit() 当虚拟机关掉的时候,删除此文件 boolean exists() 判断文件是否已经存在 File getAbsoluteFile() 返回一个File实例,这个File实例的路径是绝对的 String getName() 返回一个文件(或目录)的名字 String getParent() 返回一个文件(或目录)所属的目录名字 File getParentFile() 返回由这个文件的父目录定义的File实例 String getPath() 得到这个文件的路径 boolean isAbsolute() 判断定义这个File实例的路径是不是绝对路径 boolean isDirectory() 判断定义这个File实例的路径是不是一个目录(而不是文件) boolean isFile() 判断定义这个File实例的路径是不是一个文件 boolean isHidden() 判断定义这个File实例的路径是不是隐藏的 long lastModified() 返回最后一次修改的时间 long length() 返回文件的大小 String[] list() 返回定义这个File实例的目录下的文件或目录名字 String[] list(FilenameFilter filter) 返回定义这个File实例的目录下的文件或目录名字(经过筛 选之后) File[] listFiles() 返回定义这个File实例目录下的文件或目录所定义的File File[] listFiles(FileFilter filter) 返回定义这个File实例目录下的文件或目录所定义的File(经 过筛选之后) File[] listRoots() 返回文件系统的根目录 boolean mkdir() 在File定义的目录下创建一个目录,若成功创建则返回真 boolean renameTo(File dest) 更改文件的名字为dest boolean setLastModified(long time) 设定一个文件最后的修改时间 boolean setReadOnly() 将文件标志为只读 218 Java 游戏编程导学 6.2.4 Java1.1的I/O流 事实上,Java 1.1对I/O流库进行了一些重大改进。但Reader和Writer类并不能完全替换 原来的InputStream 和OutputStream 类。尽管不建议使用原始数据流库的某些功能,但原来 的数据流依然得到了保留,以便维持向后兼容。 在许多情况下,我们需要与新结构中的类联合使用老结构中的类。为达到这个目的, 需要使用一些“桥”类:InputStreamReader将一个InputStream转换成Reader,OutputStream Writer将一个OutputStream转换成Writer。所以与原来的I/O流库相比,通常都要对新I/O流进 行层次更多的封装。这就是为得到额外的灵活性付出的代价。 在Java 1.1里添加了Reader和Writer层次,最重要的原因便是国际化的需求。老式I/O流 层次结构只支持8位字节流,不能很好地控制16位Unicode字符。由于Unicode主要面向的是 国际化支持(Java内含的char是16位的Unicode),所以添加了Reader和Writer层次,以提供 对所有I/O操作中的Unicode的支持。除此之外,新库也对速度进行了优化,可比旧库更快 地运行。 表6.7显示了新旧I/O类库的对应关系。 表6.7 新旧I/O类库的对比 Java 1.0类库 Java 1.1类库 InputStream Reader OutputStream Writer FileInputStream FileReader FileOutputStream FileWriter StrIngBufferInputStream StringReader 没有相应的类 StringWriter ByteArrayInputStream CharArrayReader ByteArrayOutputStream CharArrayWriter PipedInputStream PipedReader PIpedOutputStream PipedWriter 6.2.5 几个比较重要的类 InputStreamReader是一个从字节流转换成字符流的桥梁。它读出字节然后通过一定的 编码将它转化成字符流。这个编码可以用名字来指定,也可以使用系统默认的编码。每次 运行InputStreamReader对象的read()方法都会导致从这个对象所连接的输入流里读入一个 或多个字节。一般的情况下我们都将InputStreamReader封装到BufferedReader里来使用。 InputStreamReader的主要方法如表6.8所示。 第 6 章 Swing和 I/O 简介 219 表6.8 InputStreamReader的主要方法 返回类型 方法 用途 InputStreamReader(InputStream in) 用默认的编码来构造一个InputStreamReader对象 InputStreamReader(InputStream in, String enc) 用名字为enc 的编码来构造一个InputStream- Reader对象 void Close() 关闭这个输入流 String getEncoding() 返回这个InputStreamReader使用编码的规范名 int Read() 读取一个单独的字符 int read(char[] cbuf, int off, int len) 把字符读进一个数组里。off为要放进去的字符在 数组里的起始位置 boolean ready() 判断缓冲区里是不是有字符可以读 OutputStreamWriter所做的事情与InputStreamReader正好相反。它的功能是把字符流通 过指定的编码转换成字节流。一般我们都将OutputStreamWriter封装到BufferedWriter里来使 用。outputStreamWriter的主要方法如表6.9所示。 表6.9 OutputStreamWriter的主要方法 返回类型 方法 用途 OutputStreamWriter(OutputStream out) 用默认的编码来构造一个OutputStreamWriter 对象 OutputStreamWriter(OutputStream out, String enc) 用名字为enc的编码来构造一个OutputStream Writer对象 void close() 关闭这个输出流 void flush() 将缓冲区里的所有字符均发送出去 String getEncoding() 返回这个OutputStreamWriter使用编码的规范 名 void write(char[] cbuf, int off, int len) 将数组的一部分写出。off为起始位置,len为要 写的字符长度 void write(int c) 写单个字符 void write(String str, int off, int len) 将一个字符串的一部分写出。off为起始位置, len为要写的字符串长度 BufferedReader通过从缓冲区读取内容,给用户提供了更方便的方法。这些方法比 InputStreamReader的方法要更加实用。BufferedReader的主要方法如表6.10所示。 表6.10 BufferedReader的主要方法 返回类型 方法 用途 BufferedReader(Reader in) 构造方法,为字符输入流创建一个Buffered Reader对象,使用的缓冲区的大小为默认大小 220 Java 游戏编程导学 (续表) 返回类型 方法 用途 BufferedReader(Reader in, int sz) 构造方法,为字符输入流创建一个Buffered Reader对象,使用的缓冲区的大小为sz void close() 关掉这个流 void mark(int readAheadLimit) 在当前位置做一个标志。在此后调用reset()方法 时可以回到这个地方。ReadAheadLimit为失效时 所读的字符数 boolean markSupported() 返回这个流是否支持mark()的方法 int Read() 读一个单独的字符 int read(char[] cbuf, int off, int len) 把字符读进一个数组的一部分。返回所读字符 数。off为字符读进数组的首位置 String readLine() 读一行字符 boolean ready() 告诉这个流是否可以读 void reset() 返回上一次mark()的位置 long skip(long n) 在流里跳过n个字符,返回实际跳过的字符数 PrintWriter通过自己的缓冲区来缓存要写出的字符,以便于能够给使用者提供更方便的 方法。这些方法比OutputStreamWriter的方法要更加实用。PrintWriter的主要方法如表6.11 所示。 表6.11 PrintWriter的主要方法 返回类型 方法 用途 PrintWriter(OutputStream out) 构造方法,为输出流创建一个PrintWriter对象,使用缓 冲区的默认大小。不采用自动flush的方法 PrintWriter(OutputStream out, boolean autoFlush) 构造方法,为输出流创建一个PrintWriter对象,使用缓 冲区的默认大小。是否自动flush依参数autoFlush的值而 定 PrintWriter(Writer out) 构造方法,为Writer对象创建一个PrintWriter对象,使用 缓冲区的默认大小。不采用自动flush的方法 PrintWriter(Writer out, boolean autoFlush) 构造方法,为Writer对象创建一个PrintWriter对象,使用 缓冲区的默认大小。是否自动flush依参数autoFlush的值 而定 boolean checkError() 将缓冲区内的字符写出,并检查是否有错 void close() 关掉这个流 void flush() 将缓冲区内的字符写出,清空缓冲区 void print(boolean b) 打印一个布尔值 void print(char c) 打印单个字符 void print(double d) 打印double类型的数 第 6 章 Swing和 I/O 简介 221 (续表) 返回类型 方法 用途 void print(float f) 打印float类型的数 void print(int i) 打印int类型的数 void print(long l) 打印long类型的数 void print(Object obj) 打印一个Object对象 void print(String s) 打印字符串 void println() 打印一个行分隔符 void println(char x) 打印一个布尔值,然后用行分隔符结束这一行 void println(char[] x) 打印字符数组,并结束这一行 void println(double x) 打印double型数,并结束这一行 void println(float x) 打印float型数,并结束这一行 void println(long x) 打印long型数,并结束这一行 void println(Object x) 打印Object对象,并结束这一行 void println(String x) 打印字符串,并结束这一行 void write(char[] buf) 写字符数组 void write(char[] buf, int off, int len) 写字符数组的一部分 void write(int c) 写单个字符 void write(String s) 写一个字符串 void write(String s, int off, int len) 写一个字符串的一部分 FileReader是一个很方便的用于读文件的类。这个类提供一些读取文件的方法。它的主 要方法如表6.12所示。 表6.12 FileReader的主要方法 返回类型 方法 用途 FileReader(File file) 构造方法 FileReader(String fileName) 构造方法 int read() 读取单个字符 int read(char[] cbuf, int off, int len) 读取多个字符到cbuf里,返回读取的字符数 boolean ready() 判断这个流是不是已经可以读了,也就是这个 流的缓冲区里是不是存在可读的字符 void close() 关掉这个I/O流 FileWriter类提供了用于往文件里写入的方法。这个类的主要方法如表6.13所示。 222 Java 游戏编程导学 表6.13 FileWriter的主要方法 返回类型 方法 用途 FileWriter(String fileName) 构造方法 FileWriter(String fileName, boolean append) 构造方法,append标志用于判断在文件内容后追 加还是直接改写 FileWriter(File file) 构造方法 void write(char[] cbuf, int off, int len) 把数组的一部分写入文件 void write(int c) 把单独的c写入文件 void write(String str, int off, int len) 把一个字符串的一部分写入文件 void flush() 清除缓冲区,并将缓冲区里的东西写入文件 void close() 关闭这个I/O流 由FileWriter,我们可以很方便地往一个文件里写入内容。 有了上面这些方法,我们就可以读取文件了。但是,读完文件,把内容放在String里之 后,我们还要对这个String进行处理,以便能够将存档的信息提取出来。下面再介绍一个很 重要的类,它放在java.util包里。这个类是StringTokenizer,是一个字符串的分析器,它能 够将字符串分隔开来。StringTokerizer的主要方法如表6.14所示。 表6.14 StringTokenizer的主要方法 返回类型 方法 用途 string StringTokenizer(String str) 构造方法,使用默认的分隔符,如回车、Tab string StringTokenizer(String str, String delim) 构造方法 string StringTokenizer(String str,String delim, boolean returnDelims) 构造方法,returnDelims表征是否要返回分隔符 int countTokens() 返回含有多少个节点(用分隔符分隔) boolean hasMoreElements() 返回是否还有其他的节点 boolean hasMoreTokens() 同上,只适用于字符串的分隔 Object nextElement() 返回下一个元素 String nextToken() 返回下一个节点 String nextToken(String delim) 返回下一个节点,分隔符为delim 6.3 “记事本”程序示例 下面我们通过一个常见的小型应用程序——“记事本”程序的编写,再进一步介绍一 下在JBuilder中如何进行Swing应用程序的开发。程序的运行结果如图6.4所示。 第 6 章 Swing和 I/O 简介 223 图 6.4 记事本程序运行结果 6.3.1 建立工程和框架文件 首先我们需要建立一个工程,命名为notebook。在JBuilder的主菜单中选择File→New Project,进入工程向导对话框。在这里我们仍然将工程目录设为D:\myPro,工程名称为 NoteBook,点击Finish按钮,就完成了工程的建立。 下面我们建立框架文件NoteBook,我们计划使用一个Frame类来实现它,在JBuilder的 主菜单中选择File→New→General选项卡→Frame选项,点击OK按钮,进入Frame向导对话 框。在这里我们将类名称命名为NoteBook ,注意这时我们的基类应该选择 javax.swing.Jframe,如果不是的话请改正,然后点击Finish按钮,就建立好了主类文件。 为了让我们的工程可以运行,需要在NoteBook类中加入如下代码: public static void main(String[] args) { NoteBook noteBook = new NoteBook(); } Main()函数是Java应用程序运行的默认的入口,如果没有这个类的话,就会抛出一个异 常: java.lang.NoSuchMethodError: main Exception in thread "main" 为了让程序看起来感觉更正式,需要给它加上一个版本信息的对话框。我们使用JDialog 类来完成这项工作。 在JBuilder的主菜单中选择File→New→General选项卡→Dialog选项,点击OK按钮,进 入Dialog 向导对话框。我们将对话框的名称命名为AboutDialog,注意它的基类是 Java.Swing.JDialog,如果不是的话请更正,然后点击OK按钮,就生成了我们需要的对话框 224 Java 游戏编程导学 框架文件。完整的工程框架如图6.5所示。 图 6.5 notebook 工程框架 6.3.2 完成界面的设计 我们仍然使用UI设计器来完成这个程序的界面设计工作,工程完成后的界面如图6.6所 示,请注意观察界面左边的概要浏览器,看一下在这个工程中应用了哪些组件。 图 6.6 界面设计完成的样子 第 6 章 Swing和 I/O 简介 225 主界面菜单设计 首先给工程添加菜单。在界面编辑器上方的工具条中选择Swing Containers选项卡,选 择菜单条Javax.Swing.JmenuBar,添加到工程中,在左侧的概要浏览器中就会把它显示在 Menu子目录下,然后双击Menu子目录下的jMenuBar1,编辑窗口中便显示菜单编辑界面, 我们就可以进行菜单项的编辑了,图6.7是编辑完成的界面。 图 6.7 菜单设计编辑器 我们选中第一项进行编辑,这个菜单编辑器使用很方便,点击要进行编辑的菜单,直 接键入菜单名称就可以了。在编辑器的顶部有几个工具条按钮选项,分别是插入菜单、插 入分隔符、插入推出菜单、删除菜单项、可见/不可见选项、可查/不可查选项和添加/去除 单选按钮选项,这些工具可以协助我们更好地完成工作。 我们在这里编写3个主菜单选项“文件”、“编辑”和“关于”。“文件”主菜单项下, 设5个子菜单项“新建”、“打开”、“保存”、“另存为”和“退出”;“编辑”主菜单 项下,设立子菜单项“改变背景”;“关于”主菜单项下,设立子菜单项“关于”,菜单 项基本就设计好了。 主界面文本编辑区域设计 既然是记事本程序,我们需要一个文本编辑器和滚动条。在编辑器上方的工具条中点 击Swing Containers标签,选择滚动窗口选项javax.swing.JScrollPane,然后在概要浏览器窗 口中的UI子目录下点击this选项,将它添加到工程中。然后我们在工具条中点击Swing标签, 在其中选择组件javax.swing.JEditorPane,然后在概要浏览器中的UI子目录下点击jScroll Pane1选项,这样就将它加入到了滚动窗口对象jScrollPane1里面。至此,我们的主要界面设 计工作就完成了。 226 Java 游戏编程导学 现在的程序代码如下: package notebook; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.*; public class NoteBook extends JFrame { JMenuBar jMenuBar1 = new JMenuBar(); JMenu jMenu1 = new JMenu(); JMenuItem jMenuItem1 = new JMenuItem(); JMenuItem jMenuItem2 = new JMenuItem(); JMenuItem jMenuItem3 = new JMenuItem(); JMenuItem jMenuItem4 = new JMenuItem(); JMenu jMenu2 = new JMenu(); JMenuItem jMenuItem6 = new JMenuItem(); JMenu jMenu3 = new JMenu(); JScrollPane jScrollPane1 = new JScrollPane(); JEditorPane jEditorPane1 = new JEditorPane(); JMenuItem jMenuItem7 = new JMenuItem(); JMenuItem jMenuItem5 = new JMenuItem(); private boolean dirty; private String currFileName; BorderLayout borderLayout1 = new BorderLayout(); public NoteBook() { try { jbInit(); } catch(Exception e) { e.printStackTrace(); } } public static void main(String[] args) { NoteBook noteBook = new NoteBook(); } private void jbInit() throws Exception { jMenu1.setText("文 件"); jMenuItem1.setText("新 建"); jMenuItem2.setText("打 开"); jMenuItem3.setText("保 存"); jMenuItem4.setText("另存为"); jMenu2.setText("编 辑"); jMenuItem6.setText("改变背景"); jMenu3.setText("关 于"); jEditorPane1.setText(""); jScrollPane1.setHorizontalScrollBarPolicy( JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); jMenuItem7.setActionCommand("关 于"); jMenuItem7.setText("关 于"); jMenuItem5.setText("退 出"); 第 6 章 Swing和 I/O 简介 227 this.getContentPane().setLayout(borderLayout1); jMenuBar1.add(jMenu1); jMenuBar1.add(jMenu2); jMenuBar1.add(jMenu3); jMenu1.add(jMenuItem1); jMenu1.addSeparator(); jMenu1.add(jMenuItem2); jMenu1.add(jMenuItem3); jMenu1.add(jMenuItem4); jMenu1.addSeparator(); jMenu1.add(jMenuItem5); jMenu2.add(jMenuItem6); this.getContentPane().add(jScrollPane1, BorderLayout.CENTER); this.getContentPane().add(jMenuBar1, BorderLayout.NORTH); jScrollPane1.getViewport().add(jEditorPane1, null); jMenu3.add(jMenuItem7); } } 对话框界面编辑 对话框界面的编辑相对简单,我们将这个界面的布局管理器设为BorderLayout。在界面 编辑器上方的工具条中点击Swing Containers标签,选择javax.swing.JPanel添加到界面中, 将constraits属性设置为South。在原有的JPanel对象Panel1中添加组件javax.swing.JLabel,在 添加好的JPanel对象中添加按钮组件javax.swing.JButton,并将按钮的text属性设置为“确 定”。这样对话框界面就设计好了。 这时的对话框代码如下: package notebook; import java.awt.*; import javax.swing.*; import java.awt.event.*; public class AboutDialog extends JDialog { JPanel panel1 = new JPanel(); JPanel jPanel1 = new JPanel(); JLabel jLabel1 = new JLabel(); JButton jButton1 = new JButton(); public AboutDialog(Frame frame, String title, boolean modal) { super(frame, title, modal); try { jbInit(); pack(); } catch(Exception ex) { ex.printStackTrace(); } } public AboutDialog() { this(null, "", false); 228 Java 游戏编程导学 } private void jbInit() throws Exception { jLabel1.setText("Jlabel1"); jButton1.setText("确 定"); getContentPane().add(panel1); panel1.add(jLabel1, null); this.getContentPane().add(jPanel1, BorderLayout.SOUTH); jPanel1.add(jButton1, null); } } 进一步修改 如果这个时候运行程序就会发现,我们什么也看不到,看来还需要对代码做一些修改。 在主界面的jbInit()函数中添加如下代码: this.setLocation(300,200); this.setSize(400,300); this.setVisible(true); this.setTitle("NoteBook"); 在对话框中的jbInit()函数中添加如下代码: this.setLocation(400,300); this.setSize(150,100); this.setVisible(true); this.setTitle("About"); 设定好这个窗口的出现位置、窗口大小、是否可见和窗口标题,然后再进行调试,效 果如图6.8所示。 图 6.8 设计完成后的界面 第 6 章 Swing和 I/O 简介 229 6.3.3 添加“文件”主菜单事件响应 接下来就要添加菜单事件了。让我们来仔细想一想,菜单该具有什么动作呢?很明显, 举例来说,选择“打开”菜单项,就应该出现对话框来显示文件结构,使得我们可以选择 想要打开的文件,如图6.9所示。 图 6.9 “打开”对话框 那么,我们是不是要去直接编写这个看起来比较复杂、结构繁琐的对话框呢?答案是 否定的,Swing已经为我们做好了准备,这就是javax.swing. JFileChooser类,它封装了文件 选择对话框,给我们的工作带来了很大的便利。 “打开”事件的添加 首先我们还是回到界面编辑器,进行添加JFileChooser类的工作。在界面编辑器上方的 工具条中点击Swing Containers标签,选择javax.swing.JFileChooser添加到界面中。然后选择 “打开”菜单项,在界面编辑器右边的属性和事件编辑器中点击events标签,进行菜单项的 事件编辑。双击其中的action_Performed属性,JBuilder会自动添加事件并将你带入到事件响 应函数的编辑界面。在响应函数中添加如下代码: jFileChooser1.showOpenDialog(this); 然后调试运行,“打开”对话框就显示出来了,但是我们会发现,怎么也打不开文件。这 是因为对话框只是为我们封装了一个框架,详细的打开文件的过程还是要我们自己来编写。 那么下一步我们就来编写打开文件的函数。代码如下: // 打开已知名字的文件 void openFile(String fileName) { try{ // 打开一个给定名字为fileName的文件 File file = new File(fileName); // 获得已经打开文件的大小 int size = (int)file.length(); //将已经读入的字符数标记设置为零 int chars_read = 0; 230 Java 游戏编程导学 // 创建一个读入文件流 FileReader in = new FileReader(file); //创建一个文件大小的字符数组 char[] data = new char[size]; //读入缓冲区中所有的字符 while(in.ready()) { //每个字符读入时计数,将它们的数目从缓冲区中减去 chars_read += in.read(data, chars_read, size - chars_read); } //关闭文件流 in.close(); // 显示文件 jEditorPane1.setText(new String(data, 0, chars_read)); //获得当前文件名称用来保存文件 this.currFileName = fileName; //标记更改标志 this.dirty = false; } catch (IOException e){e.printStackTrace();} } 然后我们在事件响应函数中再加入一条语句: openFile(jFileChooser1.getSelectedFile().getPath()); 保存文件,再次进行编译,我们发现对话框可以使用了。 “保存”事件的添加 添加“保存”事件和添加“打开”事件是极为相似的,在这里就不做过多解释,只给 出代码,首先需要添加一个用来保存文件的函数: boolean saveFile() { // 检查是否已经有当前文件名称, // 如果没有就调用“另存为”函数 if (currFileName == null) { return saveAsFile(); } try { // 打开一个文件的当前文件名 File file = new File (currFileName); // 创建一个输出流来写入文件 FileWriter out = new FileWriter(file); String text = jEditorPane1.getText(); out.write(text); out.close(); this.dirty = false; return true; } catch (IOException e) { e.printStackTrace(); 第 6 章 Swing和 I/O 简介 231 } return false; } 同前面的方法添加菜单事件响应函数,在函数体中加入代码: saveFile(); “另存为”事件的添加 这个事件的添加和前面相似,我们也不在这里赘述,先写好一个saveAsFile()函数: // 保存当前文件 // 要求用户输入文件名 boolean saveAsFile() { this.repaint(); // 使用对话框的save版本 if (JFileChooser.APPROVE_OPTION == jFileChooser1.showSaveDialog(this)) { // 设置当前文件名称为用户选择的文件名 // 接着调用一般的“保存文件”函数 currFileName = jFileChooser1.getSelectedFile().getPath(); //选择完毕以后重画菜单 this.repaint(); return saveFile(); } else { this.repaint(); return false; } } 然后在“保存为”菜单项的事件响应函数中添加代码: saveAsFile(); “保存”对话框如图6.10所示。 图 6.10 “保存”对话框 232 Java 游戏编程导学 添加“退出”事件 “退出”事件的添加很简单,只需在事件响应函数体中写如下一行代码: this.dispose(); 到现在为止,我们就把主菜单“文件”部分的编写工作完成了。 6.3.4 添加“编辑”和“关于”主菜单的事件响应 接下来我们为另外两个主菜单项中添加事件响应。 添加“改变背景”菜单事件 在这个主菜单项中只有一个子菜单项,就是“改变背景”。我们希望能够通过这一菜 单项的调用,可以改变对话框的背景颜色。 很明显这需要一个改变背景颜色的对话框。同前面的选定文件对话框一样,这个对话 框也已经由Swing包提供,这就是java.swing.JColorChooser类。它产生的对话框如图6.11所 示。 图 6.11 改变背景颜色对话框 这一过程比较简单,首先我们需要在界面编辑器上方的工具条中单击Swing Containers 标签,然后选择java.swing.JColorChooser组件添加到界面中。 接着像前面章节介绍的一样产生改变背景菜单的事件响应函数,然后向里面添加如下 代码: //控制“改变背景”菜单项 Color color = JColorChooser.showDialog(this,"Background Color", jEditorPane1.getBackground()); 第 6 章 Swing和 I/O 简介 233 if (color != null) { jEditorPane1.setBackground(color); } //在选择后重画菜单 this.repaint(); 我们使用JColorChooser中的静态成员函数showDialog()来改变背景颜色。在这个函数体 中,如果颜色选择为空,就表示什么也不选,仍然会使用当前背景颜色。改变颜色后的 NoteBook如图6.12所示。 图 6.12 改变背景颜色后的 NoteBook 添加“关于”事件 下面就进行到了最后一个菜单项——“关于”,就是调出关于版本信息的对话框,这 个对话框的界面我们已经在前面编辑好了,下面只需要简单的调用。 在“关于”菜单的事件响应函数体中,添加如下的代码: AboutDialog ad = new AboutDialog(); 这样就一切OK了。 6.3.5 添加按钮的事件 现在我们的小记事本还有一点点的缺憾,就是在“关于”对话框弹出以后,我们不能 将它关闭,也就是说,“确定”按钮还没有派上用场。接下来我们就实现这一步。 和前面添加菜单的事件响应函数类似,打开AboutDialog类的界面编辑器。在这个编辑 器右边的属性和事件对话框中,点击events标签。在出现的列表中双击actionPerform事件, JBuilder就会为我们自动编写好事件响应函数的框架代码,并且将我们定位到代码编辑器 中。我们只需添加如下一行: this.dispose(); 234 Java 游戏编程导学 到现在为止,我们的NoteBook(记事本)程序就编写完毕了,这个记事本程序还不甚 完善,读者可以自行进一步添加工具条,状态条等等,这些都比较简单而且和前面的工作 来说比较重复,我们在这里就不一一赘述。 6.4 “弹球”游戏 下面我们来编写一个简单的“弹球”游戏,这个游戏是一个Applet ,使用 javax.swing.JApplet类作为基类。 6.4.1 游戏的简单设计 这个游戏是一个Applet,规则很简单。玩家使用a键和d键控制一个球拍,来接空间中 四处反弹的小球击打砖块,打掉的砖块即为玩家获得的分数,小球掉到下方则失败。 游戏界面如图6.13所示。 图 6.13 “弹球”游戏界面 6.4.2 实现简单的界面 建立工程和 Applet 我们的第一步还是需要建立一个工程。像前面一样建立工程,命名为BreakOut,路径 仍然选择D:\myPro。 接下来建立一个Applet,命名为BallApplet,注意基类这次选择为javax.swing.JApplet。 JBuider为我们生成了一个Applet和同名的HTML文件。仔细看一下JBuilder自动为我们生成 第 6 章 Swing和 I/O 简介 235 的代码,发现在JApplet里面有一个main()函数,这是和Applet的很大的一个区别。 实现界面代码 下面我们就要编写代码实现游戏界面。首先加入几个常量: //设置字体 Font largefont = new Font("Helvetica",Font.BOLD,24); Font smallfont = new Font("Helvetica",Font.BOLD,14); Dimension d; FontMetrics fmsmall,fmlarge;//设置字符缓冲 Graphics goff; Image img; 这个游戏的界面比较简单,我们就在paint()函数里面直接画出。 public void paint(Graphics g){ String s g.setFont(smallfont); fmsmall = g.getFontMetrics(); g.setFont(largefont); fmlarge = g.getFontMetrics(); if(goff == null && d.width>0 && d.height>0){ img = createImage(d.width,d.height); goff = img.getGraphics(); } if(goff == null || img == null)return ; goff.setColor(new Color(backcol)); goff.fillRect(0,0,d.width,d.height); g.drawImage(img,0,0,this); } 下一步是要画出击打的对象——砖块,首先我们定义一些与砖块有关的常量。 boolean[] showbrick; //显示砖块 int bricksperline; //每行的砖块数 final int borderwidth = 5, brickwidth =15, //砖块的厚度 brickheight = 8, //砖块的宽度 brickspace = 2, backcol = 0x102040, //背景颜色 numlines = 4 //砖块的行数 startline = 32; //开始的行 我们使用一个函数DrawBricks()画出砖块。 //画出砖块 public void DrawBricks() { int i,j; boolean nobricks = true; //有无砖块标志 236 Java 游戏编程导学 int colordelta = 255/(numlines - 1); //颜色变化 for(j=0;j=(d.height-ballsize-scoreheight)){ if(ingame){ ballsleft--; if(ballsleft <= 0)ingame = false; } ballx = batpos + (batwidth - ballsize)/2; bally = startline + numlines*(brickheight+brickspace); balldy = dxval; balldx = 0; } if(ballx >= (d.width-borderwidth-ballsize)){ balldx = -balldx; ballx = d.width-borderwidth-ballsize; } if(ballx<= borderwidth){ balldx = -balldx; ballx = borderwidth; } } 最后,别忘记将MoveBall()函数加入paint()方法中。 if (ingame) { } else { 240 Java 游戏编程导学 DrawPlayField(); DrawBricks(); MoveBall(); } 现在的界面如图6.16所示。 图 6.16 小球可以运动的界面 编写小球的碰撞代码 如果读者和我们一起进行到这一步,就会发现一个有趣的现象。小球直接穿过砖块和 球拍,并不碰撞反弹。这种现象是正确的,是因为我们还没有加入相应的代码来产生碰撞 的结果,下面我们就来完善这一步。 如何才能确定小球和其他物体发生了碰撞呢?我们在程序中不停检测小球的位置,如 果小球的位置的其他物体发生了重叠,就可以判断是小球撞上了别的物体。原理就是这么 简单,下面来看看具体的实现。 //小球和砖块碰撞的函数 public void CheckBricks() { int i,j,x,y; int xspeed = balldx; if(xspeed <0) xspeed =-xspeed; int ydir = balldy; //判断小球是否相撞,如果没有碰撞则返回空 if(bally <(startline - ballsize)|| bally > (startline+numlines*(brickspace + brickheight))) return; //碰撞后的反弹代码 for(j=0;j= (y - ballsize) && bally<(y+ brickheight)&& ballx >= (x - ballsize) && ballx<(x+ brickwidth)){ showbrick[j*bricksperline + i] =false; score +=(numlines-j); if(ballx >=(x-ballsize) && ballx<=(x-ballsize +3)){ //左边 balldx = -xspeed; }else if(ballx <= (x+brickwidth -1) && ballx >= (x+ brickwidth -4)){ //右边 balldx = xspeed; } balldy = -ydir; } } } } } 小球和球拍碰撞的代码略有不同,在这里我们也不赘述,代码如下: //控制小球和球拍碰撞后运动的函数 public void CheckBat() { batpos += batdpos; if(batpos < borderwidth)batpos = borderwidth; else if (batpos >(d.width-borderwidth-batwidth)) batpos = (d.width-borderwidth - batwidth); if(bally >= (d.height-scoreheight-2*borderwidth-ballsize) && bally<(d.height-scoreheight-2*borderwidth) && (ballx+ballsize) >= batpos && ballx <= (batpos + batwidth)){ bally = d.height-scoreheight-ballsize-borderwidth*2; balldy = -dxval; balldx = CheckBatBounce(balldx,ballx - batpos); } } //控制小球和球拍碰撞以后,水平方向运动的函数 public int CheckBatBounce(int dy, int delta) { int sign; int stepsize,i = -ballsize,j =0; stepsize=(ballsize + batwidth)/8; if(dy>0)sign = 1; else sign = -1; while(i< batwidth && delta>i){ i += stepsize; 242 Java 游戏编程导学 j++; } switch(j){ case 0: case 1: return -4; case 2: return -3; case 7: return 3; case 3: case 6: return sign * 2; case 4: case 5: return sign * 1; default: return 4; } } 完成后编译运行,发现小球就可以击打砖块了;同时掉在球拍上的小球会在拍子上反 弹(见图6.17)。 图 6.17 可以反弹的小球 6.4.4 事件处理——让游戏能够玩起来 前面我们做了很多努力,可是直到现在我们还是没有动过键盘。不能控制球拍的运行 来接住小球,只能眼睁睁的看它掉下去。别泄气,下一步工作我们就把这个漏洞补好。 第 6 章 Swing和 I/O 简介 243 这里我们采用一种新的方法,不扩展keyListener接口,而是利用Applet本身的事件响应 函数processKeyEvent(KeyEvent e)。这个函数使用起来很简单,它会自动接收键盘事件,我 们只需要在其中加入必要的代码。 函数代码如下: public void processKeyEvent(KeyEvent e){ int nKeyCode = e.getKeyChar(); switch (nKeyCode) { case 97: batdpos = -3; repaint(); break; case 100: batdpos = 3; repaint(); break; } } 其中的常数97和100分别是a键和d键的键码,我们使用这两个键来控制球拍的运动。 6.4.5 让游戏能够判断当前状态,并能重新开始 如果想判断游戏状态的话,就需要给游戏定义一个布尔形常量,来记录这个游戏是否 运行。需要在游戏中加入如下代码: boolean ingame = false; //判断游戏是否在玩 然后我们需要在键盘事件函数中定义相应的键。向其中加入如下代码: case 119: stop(); break; case 115: start(); break; 然后我们需要补充stop()和start()两个方法的代码。 public void start(){ requestFocus(); if(theThread == null){ theThread = new Thread(this); theThread.start(); } } public void stop(){ if(theThread != null){ theThread.stop(); theThread = null; } 244 Java 游戏编程导学 } 保存运行,就一切都可以了。 6.4.6 让游戏记录玩家的生命,并计算出分数 我们首先在游戏中加入记录生命和分数的的变量。 int score, //游戏得分 ballsleft; //玩家生命 游戏开始的时候将其初始化,也就是说,在GameInit()方法中加入如下代码: …… score = 0; ballsleft = 3; …… 接下来我们编写一个函数ShowScore()来完成这个逻辑。 public void ShowScore() { String s; goff.setFont(smallfont); goff.setColor(Color.white); s ="得分:"+score; goff.drawString(s,40,d.height-5); s ="生命:"+ballsleft; goff.drawString(s,d.width-40-fmsmall.stringWidth(s),d.height-5); } 现在我们在paint()函数中要添加的函数已经很多,所以就将其添加到两个函数上,根 据游戏状态的不同,编写两个函数。 if (ingame) { playGame(); } else { ShowIntroScreen(); } 下面是要编写的两个函数的代码: private void playGame() { MoveBall(); CheckBat(); CheckBricks(); DrawPlayField(); DrawBricks(); ShowScore(); } private void ShowIntroScreen() { MoveBall(); 第 6 章 Swing和 I/O 简介 245 CheckBat(); CheckBricks(); BatDummyMove(); DrawPlayField(); DrawBricks(); ShowScore(); goff.setFont(largefont); goff.setColor(new Color(96,128,255)); count--; if(count <=0){ count =screendelay;showtitle =!showtitle; } } 6.4.7 加入音响效果 像前面一样,我们首先需要装载声音文件。在Init()中输入如下代码: try { URL url = Class.forName("breakout.BallApplet").getResource("move.au"); au = getAudioClip(url); }catch (Exception e) { e.printStackTrace(); } 这样我们就把声音文件加载入程序中,注意这个时候在程序包的classes目录下一定要 有一个声音文件move.au,否则程序会报错。 然后在合适的位置加入声音文件代码。在小球碰撞砖块的时候发出声音,需要在 CheckBricks()函数中加入如下播放代码: public void CheckBricks() { int i,j,x,y; int xspeed = balldx; if(xspeed <0) xspeed =-xspeed; int ydir = balldy; if(bally <(startline - ballsize)|| bally > (startline+numlines*(brickspace + brickheight))) return; for(j=0;j= (y - ballsize) && bally<(y+ brickheight)&& ballx >= (x - ballsize) && ballx<(x+ brickwidth)){ showbrick[j*bricksperline + i] =false; score +=(numlines-j); //播放声音文件 246 Java 游戏编程导学 au.play(); if(ballx >=(x-ballsize) && ballx<=(x-ballsize +3)){ //左边 balldx = -xspeed; }else if(ballx <= (x+brickwidth -1) && ballx >= (x+ brickwidth -4)){ //右边 balldx = xspeed; } balldy = -ydir; } } } } } 到现在为止,整个程序就已经完成了。 6.5 本章知识点回顾 JApplet类 Applet和JApplet的不同之处是,在JApplet实例中增加一个组 件,要先调用add()方法增加一个内容窗格,再使用容器的add() 方法在内容窗格中增加一个组件 JButton类 JButton(Icon i) JButton(String s) JButton(String s , Icon i) 可以显示图标 void addActionListener(ActionListener a) void removeActionListener(ActionListener a) 添加和删除事件监视器 JRadioButton类 JRadioButton(Icon i) JRadioButton(Icon i,boolean state) JRadioButton(String s) JRadioButton(String s,boolean state) JRadioButton(String s,Icon i) JRadioButton(String s,Icon I,boolean state) 单选按钮必须配置成组 void add(AbstractButton ab) 第 6 章 Swing和 I/O 简介 247 (续表) JCheckButton类 JCheckBox(Icon i) JCheckBox(Icon i , boolean state) JCheckBox(String s) JCheckBox(String s,boolean state) JCheckBox(String s,Icon i) JCheckBox(String s,Icon i,boolean state) 当选中或者取消复选框时,生成一个项目事件。这个事件由 itemStateChanged()处理 JComboBox类 JComboBox() JComboBox(Vector v) 我们通常使用addItem()方法在列表中增加选项,obj是加入组 合框的对象 Void addItem(Object obj) JScrollPane(Component comp) JScrollPane(int vsb,int hsb) JScrollPane(Component comp,int vsb,int hsb) HORIZONTAL_SCROLLBAR_ALWAYS 总是提供水平滚动条 HORIZONTAL_SCROLLBAR_AS_NEEDED 在需要时提供水平滚动条 VERTICAL_SCROLLBAR_ALWAYS 总是提供垂直滚动条 JScrollPane类 VERTICAL_SCROLLBAR_AS_NEEDED 在需要时提供垂直滚动条 JTree类 JTree(Hashtable ht) JTree(Object obj[]) JTree(TreeNode tn) JTree(Vector v) 当节点扩展或者收缩时,JTree对象生成事件,addTree ExpansionListener()和removeTreeExpansionListener()方法注册 或者注销这些事件的监听器,如下: void addTreeExpansionListener(TreeExpansionListener tel) void removeTreeExpansionListener(TreeExpansionListener tel) JTable类 JTable(Object data[][],Object colHeads[]) ByteArrayInputStream 允许内存中的一个缓冲区作为InputStream 使用 ByteArrayInputStream(byte[] buf) ByteArrayInputStream(byte[] buf, int offset, int length) 248 Java 游戏编程导学 (续表) StringBufferInputStream 将一个String(字符串)转换为InputStream StringBufferInputStream(String s) FileInputStream 用于从文件读取信息 FileInputStream(File file) FileInputStream(String name) FileInputStream(FileDescriptor fdobj) PipedInputStream 产生为相关的PipedOutputStream写的数据 PipedInputStream() PipedInputStream(PipedOutputStream src) SequenceInputStream 将两个或更多的InputStream对象转换成单个InputStream使用 SequenceInputStream(Enumeration e) SequenceInputStream(InputStream i1,InputStream i2) FilterInputStream 对作为过滤器接口使用的类进行抽象;那个过滤器为其他 InputStream 类提供了有用的功能 FilterInputStream(InputStream in) ByteArrayOutputStream 在内存中创建一个缓冲区,发送给流的所有数据都会置入这 个缓冲区 ByteArrayOutputStream() ByteArrayOutputStream(int size) FileOutputStream 将信息发给一个文件,用一个String代表文件名,或选用一个 File或FileDescriptor对象 FileOutputStream(OutputStream out) PipedOutputStream 写给它的任何信息都会自动成为相关的PipedInputStream 的 输出。实现了“管道化”的概念 PipedOutputStream() PipedOutputStream(PipedInputStream snk) DataInputStream 与DataOutputStream 联合使用,使自己能以机动方式读取一 个流中的基本数据类型,比如int、char、long等等 DataInputStream(InputStream in) BufferedInputStream 避免每次想要更多数据时都进行物理性的读取,要求先在缓 冲区里找到InputStream BufferedInputStream(InputStream in) BufferedInputStream(InputStream in,int size) 第 6 章 Swing和 I/O 简介 249 (续表) LineNumberInputStream 可以跟踪输入流中的行号;可调用getLineNumber()以及 setLine Number(int)。只是添加对数据行编号的能力,所以需 要同一个真正的接口对象连接 LineNumberInputStream(InputStream in) DataOutputStream 与DataInputStream 配合使用,以便采用方便的形式将基本数 据类型int、char、long等写入一个数据流 DataOutputStream(OutputStream out) PrintStream 用于产生格式化输出。DataOutputStream 控制的是数据的“存 储”,而PrintStream 控制的是“显示” PrintStream(OutputStream out) PrintStream(OutputStream out, boolean autoFlush) PrintStream(OutputStream out, boolean autoFlush,String encode) BufferedOutputStream 用它避免每次发出数据的时候都要进行物理性的写入 BufferedOutputStream(OutputStream out) BufferedOutputStream(OutputStream out,int size) Java 1.0类库对比Java 1.1类库 InputStream——Reader OutputStream——Writer FileInputStream——FileReader FileOutputStream——FileWriter StrIngBufferInputStream——StringReader 没有相应的类——StringWriter ByteArrayInputStream——CharArrayReader ByteArrayOutputStream——CharArrayWriter PipedInputStream——PipedReader PIpedOutputStream——PipedWriter File类的主要方法 exists() 判断文件是否已经存在 getAbsoluteFile() 返回一个File实例,这个File实例的路径是绝对的 getName() 返回一个文件(或目录)的名字 getParent() 返回一个文件(或目录)所属的目录名字 getParentFile() 返回由这个文件的父目录定义的File实例 getPath() 得到这个文件的路径 isAbsolute() 判断定义这个File实例的路径是不是绝对路径 isDirectory() 判断定义这个File实例的路径是不是一个目录(而不是文件) isFile() 判断定义这个File实例的路径是不是一个文件 isHidden() 判断定义这个File实例的路径是不是隐藏的 250 Java 游戏编程导学 (续表) lastModified() 返回最后一次修改的时间 length() 返回文件的大小 list() 返回定义这个File实例的目录下的文件或目录名字 list(FilenameFilter filter) 返回定义这个File实例的目录下的文件或目录名字(经过筛选 之后) listFiles() 返回定义这个File实例目录下的文件或目录所定义的File listFiles(FileFilter filter) 返回定义这个File实例目录下的文件或目录所定义的File(经 过筛选之后) listRoots() 返回文件系统的根目录 mkdir() 在File定义的目录下创建一个目录,若成功创建则返回真 renameTo(File dest) 更改文件的名字为dest setLastModified(long time) 设定一个文件最后的修改时间 setReadOnly() 将文件标志为只读 InputStreamReader类的主要方法 InputStreamReader(InputStream in) 用默认的编码来构造一个InputStreamReader对象 InputStreamReader(InputStream in, String enc) 用名字为enc的编码来构造一个InputStreamReader对象 close() 关闭这个输入流 getEncoding() 返回这个InputStreamReader使用编码的规范名 Read() 读取一个单独的字符 read(char[] cbuf, int off, int len) 把字符读进一个数组里。off为要放进去的字符在数组里的起 始位置 ready() 判断缓冲区里是不是有字符可以读 OutputStreamWriter类的主要方法 OutputStreamWriter(OutputStream out) 用默认的编码来构造一个OutputStreamWriter对象 OutputStreamWriter(OutputStream out, String enc) 用名字为enc的编码来构造一个OutputStreamWriter对象 close() 关闭这个输出流 flush() 将缓冲区里的所有字符均发送出去 getEncoding() 返回这个OutputStreamWriter使用编码的规范名 write(char[] cbuf, int off, int len) 将数组的一部分写出。off为起始位置,len为要写的字符长度 write(int c) 写单个字符 write(String str, int off, int len) 将一个字符串的一部分写出。off为起始位置,len为要写的字 符串长度 第 6 章 Swing和 I/O 简介 251 (续表) BufferedReader的主要方法 BufferedReader(Reader in) 构造方法,为字符输入流创建一个BufferedReader对象,使用 的缓冲区的大小为默认大小 BufferedReader(Reader in, int sz) 构造方法,为字符输入流创建一个BufferedReader对象,使用 的缓冲区的大小为sz close() 关掉这个流 mark(int readAheadLimit) 在当前位置做一个标志。在此后调用reset()方法时可以回到这 个地方。ReadAheadLimit为失效时所读的字符数 markSupported() 返回这个流是否支持mark()的方法 Read() 读一个单独的字符 read(char[] cbuf, int off, int len) 把字符读进一个数组的一部分。返回所读字符数。off为字符 读进数组的首位置 readLine() 读一行字符 ready() 告诉这个流是否可以读 reset() 返回上一次mark()的位置 skip(long n) 在流里跳过n个字符,返回实际跳过的字符数 PrintWriter的主要方法 PrintWriter(OutputStream out) 构造方法,为输出流创建一个PrintWriter对象,使用缓冲区的 默认大小。不采用自动flush的方法 PrintWriter(OutputStream out, boolean autoFlush) 构造方法,为输出流创建一个PrintWriter对象,使用缓冲区的 默认大小。是否自动flush依参数autoFlush的值而定 PrintWriter(Writer out) 构造方法,为Writer对象创建一个PrintWriter对象,使用缓冲 区的默认大小。不采用自动flush的方法 PrintWriter(Writer out, boolean autoFlush) 构造方法,为Writer对象创建一个PrintWriter对象,使用缓冲 区的默认大小。是否自动flush依参数autoFlush的值而定 checkError() 将缓冲区内的字符写出,并检查是否有错 close() 关掉这个流 flush() 将缓冲区内的字符写出,清空缓冲区 print(boolean b) 打印一个布尔值 print(char c) 打印单个字符 print(double d) 打印double类型的数 print(float f) 打印float类型的数 print(int i) 打印int类型的数 print(long l) 打印long类型的数 print(Object obj) 打印一个Object对象 252 Java 游戏编程导学 (续表) print(String s) 打印字符串 println() 打印一个行分隔符 println(char x) 打印一个布尔值,然后用行分隔符结束这一行 println(char[] x) 打印字符数组,并结束这一行 println(double x) 打印double型数,并结束这一行 println(float x) 打印float型数,并结束这一行 println(long x) 打印long型数,并结束这一行 println(Object x) 打印Object对象,并结束这一行 println(String x) 打印字符串,并结束这一行 write(char[] buf) 写字符数组 write(char[] buf, int off, int len) 写字符数组的一部分 write(int c) 写单个字符 write(String s) 写一个字符串 write(String s, int off, int len) 写一个字符串的一部分 FileReader的主要方法 FileReader(File file) 构造方法 FileReader(String fileName) 构造方法 read() 读取单个字符 read(char[] cbuf, int off, int len). 读取多个字符到cbuf里,返回读取的字符数 ready() 判断这个流是不是已经可以读了,也就是这个流的缓冲区里 是不是存在可读的字符 close() 关掉这个I/O流 FileWriter的主要方法 FileWriter(String fileName) 构造方法 FileWriter(String fileName, boolean append) 构造方法,append标志用于判断在文件内容后追加还是直接 改写 FileWriter(File file) 构造方法 write(char[] cbuf, int off, int len) 把数组的一部分写入文件 write(int c) 把单独的c写入文件 write(String str, int off, int len) 把一个字符串的一部分写入文件 flush() 清除缓冲区,并将缓冲区里的东西写入文件 close() 关闭这个I/O流 StringTokenizer的主要方法 StringTokenizer(String str) 构造方法,使用默认的分隔符,如回车,Tab 第 6 章 Swing和 I/O 简介 253 (续表) StringTokenizer(String str, String delim) 构造方法 StringTokenizer(String str,String delim,b oolean returnDelims) 构造方法,returnDelims表征是否要返回分隔符 countTokens() 返回含有多少个节点(用分隔符分隔) hasMoreElements() 返回是否还有其他的节点 hasMoreTokens() 同上,只适用于字符串的分隔 nextElement() 返回下一个元素 nextToken() 返回下一个节点 nextToken(String delim) 返回下一个节点,分隔符为delim 第 7 章 俄罗斯方块游戏——综合应用示例 本章通过编写一个俄罗斯方块游戏,来介绍Java语言的综合应用。我们要编写的俄罗 斯方块是一个应用程序(Java Application),不再是嵌到网页里的那种Applet。通过本章内 容,读者可以领略到Java的整体编程风格。 7.1 游戏效果说明 俄罗斯方块是一个传统的游戏,这个游戏是考验玩家的反应灵敏度和对几何图形的一 种直觉,想必大家一定都玩过。关于游戏规则,在此就不做介绍了。下面只简单介绍我们 要编写的这个游戏的功能。 游戏的界面如图7.1所示。 图 7.1 游戏开始玩时的界面 游戏的主要功能如下: · 游戏当然可以让玩家操纵方块来玩,这里设计用方向键来控制方块,向上为转动方 块。 · 游戏有着预览的功能,就是告诉玩家下一个将出现什么类型方块。 · 每种类型的方块都有各自不同的颜色。 · 游戏能够在玩的过程中,给出玩家的分数。 第 7 章 俄罗斯方块游戏——综合应用示例 255 · 游戏有暂停、开始和结束等控制。 · 可以让玩家来调整游戏的级别。并且当玩家玩过30个方块之后,游戏还没有结束, 则游戏级别就会增加1。 · 在游戏结束时,如果玩家的分数进入了前10名,则提示玩家输入名字,并将玩家的 名字存入档案之中。 7.2 游戏的简单设计 由于我们要将这个游戏写成一个应用程序(Application),所以首先要设计一下游戏 的框架和菜单。 游戏的框架采用标准的Windows框架,在上面有标题栏、菜单栏、工具栏,以方便玩 家控制游戏。我们应该先做好这一部分。菜单栏和工具栏分别放菜单和工具条,这里应包 括游戏的开始、暂停、重新开始、结束等控制菜单和工具。在框架的底部还应该放进去一 个状态栏,用来显示游戏的当前状态。 我们需要对菜单和工具栏的操作进行响应,并相应地来控制游戏。这时我们需要添加 对菜单和工具栏的事件响应方法。 然后,应该在一个面板上构建游戏的界面,将这个面板嵌入到游戏的大框架里。这样 整个游戏的界面就出来了。 在游戏面板构建好之后,我们应该让游戏能够玩起来,这一部分主要是编写一些算法, 用来控制游戏。这里我们还应该增加对键盘事件的响应,让玩家能够控制游戏。 最后,我们再来改进这个游戏,使得在游戏结束时能够判断玩家的分数在存档里是不 是前10名,如果是,则将玩家的分数存档。 7.3 编写游戏框架 AWT最开始的设计目标是让程序员构建一个通用的图形用户接口(GUI)。但是,这 个目标并没有达到。在Java 1.0的抽象窗口工具包产生的是在各个系统看来都欠佳的图形用 户接口。除此之外,它还限制只能使用4种字体,限制我们不能够访问操作系统中现有的高 级GUI元素。在Java 1.1里,对抽象窗口工具包进行了很好的改进,更加清晰、面向对象的 编程使得我们使用起来更加方便。在Java 1.2里,又增添了Java的基础类(JFC),这是一 个被称为Swing的GUI的一部分。Swing的使用方法很简单,类似AWT包里类的使用方法。 7.3.1 编写游戏框架 先使用JBuilder新建一个项目,这次我们将使用最简单的方法建立应用程序框架。先建 立一个工程,将工程的名字命名为Tetrics。 256 Java 游戏编程导学 在JBuilder的主菜单中,选择File主菜单→New子菜单项→General选项卡→Application 选项,点击OK按钮,进入应用程序向导对话框。在对话框中规定应用程序主类的名称为 TetricsApplication,如图7.2所示。 图 7.2 应用程序向导第一步 接下来我们要命名程序界面框架的名称。在当前的对话框中点击Next按钮,进入应用 程序向导第二步,在这里命名Frame类的名称和标题分别为TetricsFrame和Tetrics,如图7.3 所示,接着点击Finish按钮,JBuilder就为我们建立好了一个应用程序框架。这个应用程序 框架中有一个主类TetricsApplication和一个界面框架类TetricsFrame。 图 7.3 应用程序向导第二步 框架代码如下: //TetricsApplication.java 第 7 章 俄罗斯方块游戏——综合应用示例 257 package tetrics; import javax.swing.UIManager; import java.awt.*; public class TetricsApplication { boolean packFrame = false; //Construct the application public TetricsApplication() { TetricsFrame frame = new TetricsFrame(); //Validate frames that have preset sizes //Pack frames that have useful preferred size info, e.g. from their layout if (packFrame) { frame.pack(); } else { frame.validate(); } //Center the window Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); Dimension frameSize = frame.getSize(); if (frameSize.height > screenSize.height) { frameSize.height = screenSize.height; } if (frameSize.width > screenSize.width) { frameSize.width = screenSize.width; } frame.setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2); frame.setVisible(true); } //Main method public static void main(String[] args) { try { UIManager.setLookAndFeel(UIManager. getSystemLookAndFeelClassName()); } catch(Exception e) { e.printStackTrace(); } new TetricsApplication(); } } //TetricsFrame.java package tetrics; 258 Java 游戏编程导学 import java.awt.*; import java.awt.event.*; import javax.swing.*; public class TetricsFrame extends JFrame { JPanel contentPane; BorderLayout borderLayout1 = new BorderLayout(); //Construct the frame public TetricsFrame() { enableEvents(AWTEvent.WINDOW_EVENT_MASK); try { jbInit(); } catch(Exception e) { e.printStackTrace(); } } //Component initialization private void jbInit() throws Exception { contentPane = (JPanel) this.getContentPane(); contentPane.setLayout(borderLayout1); this.setSize(new Dimension(400, 300)); this.setTitle("Tetrics"); } //Overridden so we can exit when window is closed protected void processWindowEvent(WindowEvent e) { super.processWindowEvent(e); if (e.getID() == WindowEvent.WINDOW_CLOSING) { System.exit(0); } } } 我们还要写一个方法,用来初始化窗口的大小和位置,代码如下: private void myInit() { setSize(450,500); setLocation(500,400); } 我们应该在构造方法里调用这个方法。关掉系统可以调用System.exit()方法,关掉整个 窗口可以使用Frame.dispose()方法。 7.3.2 为游戏编写菜单项 图7.4是一个标准的Java菜单。 第 7 章 俄罗斯方块游戏——综合应用示例 259 图 7.4 标准的 Java 菜单 游戏的菜单包含“游戏”、“控制”、“关于”3个主菜单。在“游戏”主菜单里有“开 始游戏”、“暂停游戏”、“结束游戏”、“关闭”4个菜单项,在“控制”菜单里有“设 置级别”菜单项;在“关于”主菜单里有“关于”菜单项。 我们使用UI设计器中的菜单设计器来进行菜单项的编辑。我们应该对界面类 TetricsFrame进行编辑,因为这个类用于界面的显示,所有的组件都要添加在这个类中。 下面打开TetricsFrame类,然后进入UI编辑器。根据前面的知识,组件是添加在组件容 器里的,菜单项组件的容器就是菜单条(MenuBar)。在工具条中单击Swing Containers标 签,在其中选择组件javax.swing.JMenuBar,添加到界面上。然后在屏幕左下角的概要浏览 器中打开Menu目录,就会看到添加好的菜单条对象jMenuBar1。双击对象jMenuBar1,进入 菜单编辑界面,如图7.5所示。 图 7.5 菜单编辑器 菜单项的编辑十分简单,点击空白的菜单项,然后直接键入菜单项的名称即可。如果 260 Java 游戏编程导学 想要插入分隔符,可以使用上方菜单中的“插入分隔符”选项。每一个子菜单项我们都确 定了对应的名字,如表7.1所示。 表7.1 菜单项的设置 主菜单项 子菜单项 名称 开始游戏 start 暂停游戏 pause 结束游戏 end 游戏 关闭 quit 控制 设置级别 level 关于 关于 about 添加完菜单项的代码如下: //在Frame类的开始定义的全局变量 JMenuBar jMenuBar1 = new JMenuBar(); JMenu jMenu1 = new JMenu(); JMenuItem start = new JMenuItem(); JMenuItem pause = new JMenuItem(); JMenuItem end = new JMenuItem(); JMenuItem quit = new JMenuItem(); JMenu jMenu2 = new JMenu(); JMenuItem level = new JMenuItem(); JMenu jMenu3 = new JMenu(); JMenuItem about = new JMenuItem(); …… //组件初始化函数 private void jbInit() throws Exception { contentPane = (JPanel) this.getContentPane(); contentPane.setLayout(borderLayout1); this.setTitle("Tetrics"); //开始设计菜单的代码 jMenu1.setText("游 戏"); start.setText("开始游戏"); pause.setText("暂停游戏"); end.setText("结束游戏"); quit.setText("关 闭"); jMenu2.setText("控 制"); level.setText("设置级别"); jMenu3.setText(" 关 于"); about.setText("关 于"); jMenuBar1.add(jMenu1); jMenuBar1.add(jMenu2); jMenuBar1.add(jMenu3); jMenu1.add(start); jMenu1.add(pause); jMenu1.add(end); 第 7 章 俄罗斯方块游戏——综合应用示例 261 jMenu1.addSeparator(); jMenu1.add(quit); jMenu2.add(level); jMenu3.add(about); contentPane.add(jMenuBar1, BorderLayout.NORTH); } 然后还要把菜单项加入到界面中: this.setJMenuBar(jMenuBar1); 上面的代码产生如图7.6所示的效果。 图 7.6 游戏的菜单 7.3.3 为菜单增加事件处理 上面已经做好了菜单,下面要为每个菜单都增加事件响应。 我们用ActionListener接口来做菜单的事件响应。ActionListener是用来接受一个动作事 件的。用来处理这些动作事件的类要实现这个接口里的方法,这个类要被发出动作的事件 的类用addActionListener()方法来注册。当动作事件发生后,用来处理这些动作事件的类里 实现的actionPerformed()方法将被调用。 下面添加菜单的事件响应函数代码。我们使用UI编辑器来完成这项工作。完整的代码 如下: void start_actionPerformed(ActionEvent e) { //控制开始游戏的代码加到这里 } void pause_actionPerformed(ActionEvent e) { 262 Java 游戏编程导学 //控制暂停游戏的代码加到这里 } void end_actionPerformed(ActionEvent e) { //控制结束游戏的代码加到这里 } void quit_actionPerformed(ActionEvent e) { //控制退出的代码加到这里 } void about_actionPerformed(ActionEvent e) { //关于事件的代码加到这里 } void level_actionPerformed(ActionEvent e) { //设置级别事件的代码加到这里 } 下面是适配器的代码: class TetricsFrame_start_actionAdapter implements java.awt.event.ActionListener { TetricsFrame adaptee; TetricsFrame_start_actionAdapter(TetricsFrame adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.start_actionPerformed(e); } } class TetricsFrame_pause_actionAdapter implements java.awt.event.ActionListener { TetricsFrame adaptee; TetricsFrame_pause_actionAdapter(TetricsFrame adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.pause_actionPerformed(e); } } class TetricsFrame_end_actionAdapter implements java.awt.event.ActionListener { TetricsFrame adaptee; TetricsFrame_end_actionAdapter(TetricsFrame adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.end_actionPerformed(e); } } 第 7 章 俄罗斯方块游戏——综合应用示例 263 class TetricsFrame_quit_actionAdapter implements java.awt.event.ActionListener { TetricsFrame adaptee; TetricsFrame_quit_actionAdapter(TetricsFrame adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.quit_actionPerformed(e); } } class TetricsFrame_about_actionAdapter implements java.awt.event.ActionListener { TetricsFrame adaptee; TetricsFrame_about_actionAdapter(TetricsFrame adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.about_actionPerformed(e); } } class TetricsFrame_level_actionAdapter implements java.awt.event.ActionListener { TetricsFrame adaptee; TetricsFrame_level_actionAdapter(TetricsFrame adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.level_actionPerformed(e); } } 这里是加入监听器的代码: private void jbInit() throws Exception { …… start.addActionListener(new TetricsFrame_start_actionAdapter(this)); pause.addActionListener(new TetricsFrame_pause_actionAdapter(this)); end.addActionListener(new TetricsFrame_end_actionAdapter(this)); quit.addActionListener(new TetricsFrame_quit_actionAdapter(this)); level.addActionListener(new TetricsFrame_level_actionAdapter(this)); about.addActionListener(new TetricsFrame_about_actionAdapter(this)); …… } 这样就建立了整个的事件驱动过程。只需加入相应的控制代码就可以了。 264 Java 游戏编程导学 7.4 开始编写游戏界面 上一节我们已经将游戏的框架编写好了,下面我们将开始写游戏的界面。我们设计将 游戏写在一个Panel上面。 我们再新建一个类,让它从java.awt.panel里继承,将它命名为Tetrics,游戏的主要功能 都要在这个类里实现。Tetrics类的代码如下: package tetrics; import java.awt.*; import javax.swing.*; import java.awt.event.KeyListener; import java.awt.event.KeyEvent; public class Tetrics extends JPanel implements Runnable, KeyListener { public Tetrics() { } } 7.4.1 在Panel中加入几个必要的常量和变量 定义颜色常量 我们需要定义两个常量,一个定义Panel的背景颜色,另一个定义Panel的前景颜色。当 我们画图的时候,如果要用背景色来填充的话,就可以直接调用这个常量。 下面简单介绍一下Color类。 在Color中定义了一些颜色常量,这些颜色是我们经常用到的。需要时,我们可以直接 使用这些常量。这些颜色常量都是静态的,可以通过Color来直接调用,如表7.2所示。 表7.2 Color类中的一些常量 返回类型 方法 用途 Color blue 蓝色 Color black 黑色 Color cyan 青色 Color darkGray 黑灰色 Color gray 灰色 Color green 绿色 Color lightGray 浅灰色 Color magenta 洋红色 第 7 章 俄罗斯方块游戏——综合应用示例 265 (续表) 返回类型 方法 用途 Color orange 桔红色 Color pink 粉红色 Color red 红色 Color white 白色 Color yellow 黄色 Color提供了Color(int r, int g, int b)方法,用来创建一个新的颜色,r为颜色中的红色成 份,g为颜色中的绿色成份,b为蓝色成份。r、g、b都是整型数,从0~255。例如,new Color(0,0,0) 为黑色,new Color(255,255,255)为白色。 由上面的方法,我们可以定义上面两个颜色常量: final Color BAKCOLOR= new Color(80,123,166); final Color FORCOLOR=Color.black; 定义坐标常量 我们应该定义左边用来显示预览分数、走的步数的区域的宽度。见下面这行代码: final int XOFFSET=200; 定义必要的变量 下面这行代码定义了每个小方块的宽度: int m_nSqLength; 我们还应该定义俄罗斯方块应该有多少行、多少列: static int m_nCols; static int m_nRows; 7.4.2 在Panel上画出游戏界面 下面我们使用Panel的paint()方法,画出一个游戏的简单界面。 初始化变量 我们应该在Panel的构造方法里将Panel的背景颜色设为背景色常量,并且为必要的变量 赋上一个初始值。 关键代码如下: public Tetrics() { super(); try { jbInit(); } catch (Exception ex) { 266 Java 游戏编程导学 ex.printStackTrace(); } } //重载构造方法,拥有TetricsFrame变量 //方便传递函数 public Tetrics(TetricsFrame tFrame) { super(); m_tFrame = tFrame; try { jbInit(); } catch (Exception ex) { ex.printStackTrace(); } } void jbInit() throws Exception{ //设置初始参数 m_nSqLength=20; m_nCols=10; m_nRows=20; …… } 画出界面来 下面我们就应该在paint()方法里写一些功能代码,让整个Panel能够画出一个初始的界 面来。关键代码如下: public void paint(Graphics g) { g.setFont(new Font("宋体",0,18)); int gx=m_nSqLength; int gy=m_nSqLength*m_nRows/4; //打印Score和Level的位置 g.drawString("Score:"+m_nTheScore,gx,gy); gy+=30; g.drawString("Level:5",gx,gy); //画预览的方块 int middle=m_nCols/2; int top=m_nRows; gy+=30; g.setColor(Color.black); g.fillRect(gx,gy,m_nSqLength*4,m_nSqLength*4); //画游戏区 for(int i=0;i=0 && m_nColumn=0 && m_nRow 10) t=75; else t=m_nDelayMap[m_nPlayLevel]; Thread.sleep(t); }catch(InterruptedException e){e.printStackTrace();} if(m_bNeedNewPiece) { if(m_nPieceValue>0) { //计算分数 m_nTheScore+=m_nPieceValue; m_nTotalPieces+=1; if(m_nTotalPieces%30==0)m_nPlayLevel++; } //消去一行 removelines(); transferPreToCur(); newPrePiece(); m_bNeedNewPiece=false; } else { m_bNeedNewPiece=!moveCurPiece(0,-1,false); if(!m_bNeedNewPiece) m_nPieceValue-=5; } this.update(this.getGraphics()); } m_theThread=null; } 7.5.8 控制游戏开始 这很简单,只要在方法里先定义一个线程,然后让这个线程开始运行就可以了。当然 在这里还应该注意一点,就是要重新给一些变量赋值,如游戏的分数。我们还要考虑这么 一种情况,就是当我们点击“暂停”按钮之后的开始,不是用来重新开始的,而是接着刚 才的状态往下玩。那么我们应该在Start()方法的一开始来判断游戏是否是暂停,如果是暂停 的,就不将游戏的状态重新刷新一次。代码如下: 第 7 章 俄罗斯方块游戏——综合应用示例 275 public synchronized void start() { if(m_theThread!=null) //游戏是被暂停,而不是重新开始 { m_theThread.resume(); m_bPaused=false; return; } //重新开始赋上游戏的状态 for(int i=0;i0) { m_nTheScore+=m_nPieceValue; m_nTotalPieces+=1; if(m_nTotalPieces%30==0)m_nPlayLevel++; } 到此,这个游戏暂告一段落。在下一章,我们还将对它进行完善。 本章在上一章的基础上来为游戏添加其他功能,并实现游戏分数的存档。存档这一部 分是这样来设计的:让读者在游戏结束时判断它的分数是否已经进入了存档中的前10名, 如果是,则弹出对话框让玩家输入他的名字,并让玩家在点击“确定”按钮后存入档案。 然后,我们还要做一个对话框,并在菜单栏中加上一个查看档案的菜单,让玩家可以查看 到档案里的分数。 7.6 添加游戏的其他功能 7.6.1 设计About对话框 一个完整的应用程序都是应该有“关于”对话框的。在这个对话框里,一般要放上一 张标志性的图片,然后再有一段说明,最后再有一个“确定”按钮。我们用JFrame类来产 生这个对话框。 设计装载图片的面板 首先我们要在About里装载图片。 先定义一个类PicPanel,这个类是用来装载图片的。在这个方法中,我们仍然采用和前 几章一样的方法来装载图片。 我们用Class接口来定位这个URL。 我们调用了Class类的两个方法: 第 7 章 俄罗斯方块游戏——综合应用示例 281 · forName(String s) 取得s类的Class接口对象,是Class的静态方法。 · getResource(String name) 得到一个名为name的资源,返回值为这个资源的URL。 要是在应用程序里取得资源a.txt的URL,这个资源和TFrame类同在一个目录里,就可 以这样来写: URL url=Class.forName("TFrame").getResource("a.txt"); 不过,由于Class.forName()方法在出错时会抛出异常,所以我们要捕获这个异常。 在此有必要使用Toolkit(工具箱),它提供了很多有用的方法,如表7.3所示。 表7.3 Toolkit里的主要方法 返回类型 方法 用途 void addAWTEventListener(AWTEvent- Listener listener, long eventMask) 加上一个AWTEventListener,用来监听所有的 AWTEvent void addPropertyChangeListener(Stringname, PropertyChangeListener pcl) 加上一个PropertyChangeListener用来监听名字 为name的属性 void Beep 发出beep的声音 int checkImage(Image image, int width, int height, ImageObserver observer) 检查一个图片是否已经装载好 Image createImage(byte[] imagedata, int imageoffset, int imagelength) 用存在于byte[]中的图像数据构造Image对象的 一个实例 Image createImage(String filename) 从filename文件中取出图像中每个点的像素信 息,用来构造一个Image对象的实例 Image createImage(URL url) 从url取出图像中每个点的像素信息用来构造一 个Image对象的实例 ColorModel getColorModel() 取得这个Toolkit的颜色模式 Toolkit getDefaultToolkit() 取得默认的Toolkit,是Toolkit的一个静态方法 Object getDesktopProperty(StringpropertyName) 取得桌面的属性值 String[] getFontList() 取得所有能支持的字体 Image getImage(String filename) 从文件filename取出图片 Image getImage(URL url) 从url处取出图片 int getMaximumCursorColors() 取得当前的Toolkit所支持的光标颜色的最大数 目 String getProperty(String key, String defaultValue) 取得名字为key的属性值,defaultValue为默认值 Dimension getScreenSize() 取得桌面的大小 Clipboard getSystemClipboard() 取得系统剪贴板 用上面的一些方法,我们可以取得一个图片资源的URL,并将它装入,代码如下: void initImage() 282 Java 游戏编程导学 { URL url=null; try{ url=Class.forName("tetrics.Tetrics"). getResource("about.gif"); } catch(Exception e){} m_img=getToolkit().getImage(url); MediaTracker mt=new MediaTracker(this); mt.addImage(m_img,1); try { mt.wait(); mt.checkAll(); } catch(Exception e){} } 我们应该在构造方法中调用这个方法以装载图片,然后还应该在paint()方法中将它画 到Panel上面。paint()方法的关键代码如下: public void paint(Graphics g){ g.drawImage(img,0,0,this); g.setFont(new Font("宋体", 3, 18)); g.drawString("版本1.0",220,200); } 这样,我们就可以将装载的图片画在About对话框中了,同时还写入了版本信息。 编写 About 对话框 我们要用JBuilder的Designer来设计这个对话框。操作步骤如下: (1)打开我们要设计的类即AboutDialog类。 (2)打开Designer属性页,如图7.9所示。 (3)将this的Layout属性设置成BorderLayout。 (4)在设计窗口上方有“选择包”这一项,我们选择Swing Containers包。 (5)放上一个JPanel,如图7.10所示。 (6)将JPanel的Layout设置成FlowLayout。 (7)在JPanel中放上一个Button。将Button的Label设置为“确定”。 (8)在代码中还需要加入PicPanel对象,来显示版本信息。 this.getContentPane().add(img,BorderLayout.CENTER); 这样一来,我们就将AboutP的界面部分编好了。但是我们还要对Button编写一些代码, 让我们点击Button之后能够关闭对话框。 第 7 章 俄罗斯方块游戏——综合应用示例 283 图 7.9 Designer 的属性框 图 7.10 放上一个 JPanel 点击“确定”按钮,然后在右边的属性对话框中点击Events标签。点击actionPerformed, 如图7.11所示。JBuilder自动生成一个事件响应函数jButton1_actionPerformed。取这个默认 的名字,双击进入代码编辑器,JBuilder将光标自动转移到事件响应函数的函数体上。 284 Java 游戏编程导学 图 7.11 事件页和事件响应函数 然后我们增加对Button点击的事件处理。在光标的当前位置添加如下代码即可: m_dlg.dispose(); 在菜单里打开“关于”对话框 此时AboutP的全部代码就写完了。我们还应该使在游戏菜单中选择“关于”时,能够 弹出对话框。在MenuListener类中的actionPerformed方法里添加如下代码: String sCommand=e.getActionCommand(); if(sCommand.equals("开始游戏")) { … //省略部分代码 } else if(sCommand.equals("关于")) { Dialog d=new Dialog(m_tFrame,"关于"); d.add(new AboutP(d)); d.setSize(400,320); d.setLocation(400,300); d.show(); //控制显示游戏的"关于"对话框的代码加到这儿 } 这样,整个对话框就做好了。界面如图7.12所示。 第 7 章 俄罗斯方块游戏——综合应用示例 285 图 7.12 “关于”对话框 7.6.2 设计设定游戏等级的对话框 上一节我们已经用JBuilder的设计器设计了一个简单的“关于”对话框。这一节我们再 来设计一个对话框,用来设定游戏的等级。 Dialog 类 Dialog是一种特殊的Frame,它只能放到应用程序的最高层,它一般是用来给用户提供 某种信息或者获得某种信息而创建的Frame,我们把它叫做对话框。 对话框的方法和Frame的方法几乎一样,它也有setSize()、setLocation()等方法。我们使 用这些方法来创建“关于”对话框。创建一个Dialog的关键代码如下: Dialog d=new Dialog(m_tFrame,"关于"); d.setSize(200,200); d.setLocation(400,300); d.show(); 现在显示出来的只是一个空的Dialog,上面什么都没有。我们应该再新建一个JPanel, 把它命名为levelDialog,然后在它的上面放置一个About对话框必须有的东西。然后将这个 Panel加到这个对话框里。 用JBuilder来新建这么一个JPanel的类。在levelDialog中定义一个成员对象,用来保存 levelDialog所属于的Dialog对象: Dialog m_dlg; 286 Java 游戏编程导学 然后,修改JBuilder中的levelDialog的构造方法。因为在这个Panel中要用到它所属的 Dialog的引用。修改后的构造方法为: public AboutP(Dialog d) { m_dlg=d; super(); } 在创建对话框的时候,可以同时创建一个levelDialog的对象,并将它加入到对话框里。 代码如下: Dialog d=new Dialog(m_tFrame,"关于"); d.add(new AboutP(d)); d.setSize(200,200); d.setLocation(400,300); d.show(); 这样这个对话框就不是一个简单的框架了,里面还有一个Panel存在。但是现在的Panel 是空的,所以现在的对话框的样式仍然没有什么改观。 用 JBuilder 的 Designer 来设计对话框 新建一个类,我们把它叫做levelDialog,这是一个用来选择游戏等级的对话框。我们 使用滚动条来设定游戏的级别。设计步骤如下: (1)打开我们要设计的类,即levelDialog类。 (2)打开Designer属性页。 (3)将要编辑的Panel的Layout设置成nullLayout。 (4)在设计窗口上方有选择包这一项。我们选择AWT包。 (5)在包里选择组件JSilder,放到Panel上面。 (6)在JSilder的上面放一个Label,将Label的text属性改为“你选择的等级:5”。 (7)在最下面放一个Button。将Button的Label属性改为“确定”。 (8)将Panel的布局管理器LayoutManager改为GridBagLayout。 这样设计好的界面如图7.13所示。 使设定游戏级别对话框生效 上面我们已经将游戏的界面设计好了,接下来我们增加代码,使在这个窗口里能够修 改游戏的级别。 第 7 章 俄罗斯方块游戏——综合应用示例 287 图 7.13 设计好的 levelDialog 要使这个对话框能够控制游戏,我们必须能够获得游戏的主体Tetrics对象。我们应该 在构造对话框中的Panel时传入这个对象。关键代码如下: TFrame m_tFrame; Dialog m_dialog; public levelDialog(TFrame tFrame,Dialog d) { m_tFrame=tFrame; m_dialog=d; try { //装载对话框里的组件 jbInit(); } catch(Exception e) { e.printStackTrace(); } } 然后我们就可以编写代码处理对话框的各个事件。 (1)使点击Button能够关闭窗口。 双击Button,然后进入代码栏,添加下面的代码: m_dialog.dispose(); 288 Java 游戏编程导学 (2)增加滚动条的changeListener,使得滚动条的值改变时,能够同时改变游戏的级别。 我们应该在设计框里选定滚动条组件,这时可以看到它的属性框。在属性框下面,点 击Events,进入事件页。双击AdjustmentValueChanged即可进入代码里。在光标的当前位置 加入当滚动条的值改变的时候要执行的代码。这里我们要让Label中显示更改后的游戏级 别,然后在游戏中确实地改变它的级别。代码如下: label1.setText("你选择的等级:"+scrollbar1.getValue()); int nLevel=scrollbar1.getValue(); m_tFrame.m_tetrics.setPlayLevel(nLevel); (3)在对话框刚刚打开的时候,使得对话框显示的就是游戏当前的级别。 为此,我们应该在构造selectLevelP时就能够取得游戏当前的级别,然后设定滚动条的 当前值和Label上要显示的值。加入这个功能以后的构造方法的代码如下: public levelDialog(TFrame tFrame,Dialog d) { m_tFrame=tFrame; m_dialog=d; try { jbInit(); scrollbar1.setValue(tFrame.m_tetrics.getPlayLevel()); label1.setText("你选择的等级:"+scrollbar1.getValue()); } catch(Exception e) { e.printStackTrace(); } } 设定游戏级别的对话框界面如图7.14所示。 图 7.14 设计游戏级别的对话框 第 7 章 俄罗斯方块游戏——综合应用示例 289 7.6.3 为游戏添加状态栏 border 布局 我们知道跨平台特性是Java很突出的一个特性。对于Java的界面管理,也要保证在各个 运行平台上都能保持一致。这儿我们要用到BorderLayout,这是一个简单的Layout,它有5 个位置:South,North,West,East,Center。 用 JPanel 来实现 StatusBar 我们为TFrame安装Border布局,然后在Center处装入游戏,在South处装入游戏状态栏。 StatusBar通过在JPanel中加入一个JLabel来实现。我们现在在TFrame中定义两个类成 员,一个是表示StatusBar的那个JPanel对象,另一个是JLabel对象。代码如下: JPanel jPanel1 = new JPanel(); JLabel jLabel1 = new JLabel(); 下面我们要在myInit()方法(在构造方法里调用)中加入状态栏代码,如下所示: private void jbInit() throws Exception { …… jLabel1.setText("游戏装载完毕"); contentPane.add(jPanel1, BorderLayout.SOUTH); jPanel1.add(jLabel1, null); …… } 然后在改变状态信息的地方直接调用m_lStatus.setText(String s)就可以了。s为要显示的 信息。如下所示: if(sCommand.equals("开始游戏")) { //控制开始玩游戏的代码加到这里 m_tetrics.start(); jLabel1.setText("开始游戏"); this.repaint(); } else if(sCommand.equals("结束游戏")) { //控制暂停游戏的代码加到这里 m_tetrics.pause(); jLabel1.setText("暂停游戏"); this.repaint(); } else if(sCommand.equals("暂停游戏")) { //控制暂停游戏的代码加到这里 m_tetrics.pause(); 290 Java 游戏编程导学 jLable1.setText("暂停游戏"); } else if(sCommand.equals("关闭游戏")) { //控制关闭游戏的代码加到这里 dispose(); jLabel1.setText("关闭游戏"); } 这样,游戏的状态栏也设计完成了,界面如图7.15所示。 图 7.15 加入状态栏后的游戏界面 7.7 封装得分情况 下面我们要编写一个类,用来从文件(档案)中取得原来玩家的得分,也提供方法将 现在的玩家的分数写入到文件(档案)中。在编写的过程中笔者会逐步讲解对这个类的封 装。 7.7.1 定义Score类和类成员 这个类应该提供3个方面的服务: · 取得现在存档里的玩家的名字和分数,定义方法名为getScore()和getName()。 · 判断一个分数是不是进入了前10名,定义方法名为isScoreTop(int nScore)。 第 7 章 俄罗斯方块游戏——综合应用示例 291 · 将一个玩家的名字和分数插入到存档中,定义方法名为insertNameScore(String sName,int nScore)。 这3个服务应该是这个类的公有方法,对于这些方法所要实现的底层细节(下面要调用 的方法)我们应该给予保护。比如我们需要从文件里取得玩家的名字和得分,以及往文件 里写等等细节,我们都要封装起来,不让类外的对象来调用。这样,就会让这个类使用起 来很方便,整个程序也很容易读懂。 对于类的成员,我们要定义一个String数组(用来保存存档里的玩家名字),一个整型 的数组(用来保存玩家的分数)。 在类的构造方法里,我们还应该给这些信息初始化。类的结构即关键代码如下: public class Score { private int[] m_nScore=new int[10]; private String[] m_sName=new String[10]; public Score() { for(int i=0;i<10;i++) { m_nScore[i]=0; m_sName[i]="None"; } } 7.7.2 定义方法writeToFile() 下面我们要实现这个方法中的一个私有方法。因为这个方法只服务于这个类中的方法, 应该用private关键字保护起来。 对于private、public、protected关键字的说明如下: · private 用这个关键字限制的资源只能被这个类中的方法访问。 · public 用这个关键字限制的资源能被所有看到这个类的对象使用。 · protected 能被这个类本身和它的子类使用。 下面来讲述这个方法的实现细节。 首先我们应该定义存档里信息的组织方式。我们准备在文件中存储10个玩家和相应的 10个分数。现在来定义信息的组织方式。我们将每个用户的信息用“|”隔开,每个用户的 名字和分数用“@”隔开,所有这些信息都连续地放在文件之中。也就是说,假如是如表 7.4所示的这种情况: 292 Java 游戏编程导学 表7.4 得分情况示意图 玩家的名字 玩家的得分 A 1000 B 990 C 900 D 800 E 700 F 600 G 500 H 400 I 300 J 200 那么应该在文件里存有如下内容: A@1000|B@990|C@900|D@800|E@700|F@600|G@500|H@400|I@300|J@200 这样,我们写的方法只要先根据m_nScore和m_sName的情况来生成这个字段,然后将 这个字段写进文件就行了。 算法是这样的,我们先遍历玩家的名字数组和分数数组,按我们规定的方式来生成一 个字符串,最后我们将字符串写入到文件之中。代码如下: private void saveToFile() { //first to create a string with some order String sStr=""; for(int i=0;i<10;i++) { sStr+=m_sName[i].trim()+"@"+m_nScore[i]+"|"; System.out.println(sStr); } try { File f=new File("res/a.txt"); //先判断文件是否存在,如果不存在,则创建新的文件 if(!f.exists())f.createNewFile(); Writer wr=new FileWriter(f); wr.write(sStr); wr.flush(); } catch(Exception e){e.printStackTrace();} } 第 7 章 俄罗斯方块游戏——综合应用示例 293 7.7.3 定义方法readFromFile() 上一节已经实现了向文件中写入存档的方法,这一节我们来写从文件中读出存档的方 法。使用表6.12的FileReader类中的方法来读取文件,代码如下: Reader in = new FileReader("res/a.txt"); 读完文件,把内容放在String里之后,我们还要对这个String进行处理,以便能够将存 档的信息提取出来。下面再介绍一个很重要的类,它放在java.util包里。这个类是 StringTokenizer,它是一个字符串的分析器,能够将字符串分隔开来。它的主要方法请参见 表6.14。 算法是这样的,我们不断地从文件里取字符数组,并将字符数组添加到一个String的后 面,一直到文件被读完。这样这个文件的所有内容都被放到了这个String里。然后按我们保 存存档的格式来分析这个字符串,将分析的结果放到玩家名字的数组和玩家分数的数组里。 这儿考虑到,如果我们读取文件失败,或者文件里的记录数少于10个,那么我们就会把后 面的要装入的记录都设为默认值。代码如下: private void readFromFile() { String ObjStr=""; try{ Reader in = new FileReader("res/a.txt"); char[] buff = new char[4096]; int nch; while ((nch = in.read(buff, 0, buff.length)) != -1) { ObjStr=ObjStr+(new String(buff, 0, nch)); } } catch(Exception e){e.printStackTrace();} parseStr(ObjStr); } private void parseStr(String s) { StringTokenizer st=new StringTokenizer(s,"|"); for(int i=0;i<10;i++) { if(st.hasMoreTokens()) { String sStr=st.nextToken(); StringTokenizer stt=new StringTokenizer(sStr,"@"); m_sName[i]=stt.nextToken(); try{ m_nScore[i]=Integer.parseInt(stt.nextToken()); }catch(Exception e) { m_nScore[i]=0; } 294 Java 游戏编程导学 } else { m_sName[i]="None"; m_nScore[i]=0; } } } 上面的代码将文件读出,解析后放到m_sName和m_nScore里。 7.7.4 定义方法sortScore() 我们还要写一个对分数进行排序的算法。因为我们要将显示出来的分数从高到低排序, 也就是,我们第一个看到的分数应该是第一名。这是游戏排行榜的要求。 我们可以采用任何排序算法来对分数进行排序,因为我们对排序算法的效率要求不是 很高,所以可采用冒泡法来进行排序。 冒泡法的算法是这样的,我们先遍历所有的分数,将最高的分数放到第一个。然后, 再遍历其他分数,再将最高的放到第二个。这样,一直到最后一个。 关于排序算法,读者可以参考任何一本算法与数据结构的书,就可以找到许多种的方 法。读者可以尝试用其他办法。代码如下: private void sortScore() { int nTempScore=0; String sTempName="None"; for(int i=0;i<10;i++) { for(int j=i;j<10;j++) { if(m_nScore[i]nMyScore)label3.setText("你输了!!!"); else label3.setText("你赢了!!!"); 第 8 章 网络俄罗斯方块游戏——Swing 组件与网络功能 337 } catch(Exception e) { e.printStackTrace(); } } 我们还要编写一个JDialog类(类名我们定义为GameOverD)来封装这个JPanel,这会 使得我们调用这个对话框的时候,不用再创建新的JDialog对象,而直接调用JPanel类即可。 这个类是从JDialog里派生出来的,我们在这个类的构造方法里完成对话框的创建、加入上 面新建的JPanel、将对话框显示出来等功能。我们在构造方法的参数里还应该增加玩家自己 的分数和对手的分数,以使得它封装的JPanel也能够得到这些内容。 编写完毕的GameOverD的代码如下: package dialog; import javax.swing.*; import TFrame; public class GameOverD extends JDialog { public GameOverD(TFrame tframe,int nMyScore,int nRivalScore) { super(tframe); setTitle("分数报告"); setSize(251,223); setLocation(400,300); getContenPane().add(new GameOverP(this,nMyScore,nRivalScore)); show(); } } 这样在游戏结束时要显示的分数报告对话框就编写好了,如图8.15所示。 图 8.15 显示双方分数的对话框的实际运行效果 8.4.6 编写警告对方不能运行某个命令的提示框 我们的游戏规则里指定了这么一条,当游戏处在客户端的时候,我们限制它来控制游 338 Java 游戏编程导学 戏的开始和结束、暂停等功能。这时,在玩家点击这些菜单的时候,应该弹出一个提示框, 来提示玩家正处在客户端,没有权利来运行这些命令,只能等服务器端的游戏来控制这些 命令。 这个提示框的编写模式同8.4.6节一样,我们要编写两个类,一个是对话框的内容,继 承JPanel类,一个是对话框,从JDialog继承的类,用来封装JPanel。 继承JPanel的类我们命名为WarningP。它用JBuilder设计的最终结果如图8.16所示。 图 8.16 警告对方不能运行某个命令的提示框设计 下面我们为WarningP加入功能代码。 (1)增加一个类成员,用来保存这个JPanel所属的对话框,也就是从JDialog继承出来 的类(叫做WarningD)。代码如下: WarningD m_wd; (2)我们应该改变构造方法的参数,使得在JPanel里能够获得它所属的对话框的 WarningD对象。代码如下: public WarningP(WarningD wd) { m_wd=wd; try { jbInit(); } catch(Exception e) { e.printStackTrace(); } } (3)添加“确定”按钮的功能代码。我们要使用户点击“确定”按钮时,该对话框关 闭。 第 8 章 网络俄罗斯方块游戏——Swing 组件与网络功能 339 在设计器里双击“确定”按钮,进入代码框,添加“确定”按钮的功能代码: Void jButton1_actionPerformed(ActionEvent e) { m_wd.dispose(); } 下面我们来编写封装上面这个JPanel的JDialog类,这个类很简单,只要在构造方法里 调用super()方法创建一个新的JDialog对象,然后设定它的大小、标题,最后还要新建一个 新的WarningP对象,并将这个对象加入到这个对话框里。代码如下: package dialog; import javax.swing.*; import Tframe; public class GameOverD extends Jdialog { public GameOverD(Tframe tframe,int nMyScore,int nRivalScore) { super(tframe); setTitle("分数报告"); setSize(251,223); setLocation(400,300); getContentPane().add(new GameOverP(this,nMyScore,nRivalScore)); show(); } } 这样整个提示对话框就编写好了。我们只要在Tframe里调用new WarningD(this),就可 以将这个提示框显示出来,界面如图8.17所示。 图 8.17 警告对方不能运行某个命令提示框的实际运行效果 340 Java 游戏编程导学 8.5 把网络模块加入到游戏之中 8.5.1 网络协议的设计 要实现两台机器之间的通信,就应该指定一个通信协议。通过遵守这个协议,可以让 双方的计算机都能够明白互相在说些什么东西。下面先对双方要进行互传的信息分类。 (1)互传的数据为当时游戏界面的方块状态。 (2)告诉对方自己已经消去一行,对方的游戏应该随机增加一行(游戏规则)。 (3)从服务器发出的开始游戏的请求,使得双方同时开始,或者是暂停后的开始。 (4)从服务器发出的暂停游戏的请求,我们限制客户端进行这样的控制。 (5)从服务器发出的结束游戏的请求,我们限制客户端进行这样的控制。 (6)从服务器发出的设置级别的请求,我们限制客户端进行这样的控制。 (7)双方发送相互之间聊天的内容。 (8)任何一方游戏结束的时候,告诉对方自己这一方的游戏已经结束,并且将自己的 分数发送给对方。 这是双方要实现的所有通信内容。我们需要让计算机能够识别出这8种不同的信息,并 对这8种不同的信息做不同的处理。这样我们必须为每条不同的信息都加上不同的标志,双 方所加的标志必须是双方都已经协议好的,这就是双方的通信协议。 笔者设计的协议如表8.8所示。这个协议不是惟一的,你可以任意规定,但是必须保证 计算机能够识别出来这8种不同的信息。 表8.8 设计的通信协议 信息类别 标志(加在信息开头) 当时游戏界面的方块状态 “Status:”+每个方块的状态(每个方块的状态之间用|符号隔开。 如Status:1|1|0|…表示的信息是当时游戏界面的方块状态) 告诉对方自己已经消去一行 “RemoveLine” 从服务器发出的开始游戏的请求 “StartGame” 从服务器发出的暂停游戏的请求 “PauseGame” 从服务器发出的结束游戏的请求 “StopGame” 从服务器发出的设置级别的请求 “Level:”+级别 双方发送相互之间聊天的内容 “Talk:”+谈话的内容 任何一方游戏结束的时候,告诉对 方自己这一方的游戏已经结束 “GameOver:”+游戏结束的时候游戏得分 我们设计好这个协议,就应该在编写游戏的时候遵守这个协议。比如在发送我们互相 之间聊天内容的时候,就应该在相互之间聊天的内容前头加上“Talk:”的标志。在读到对 方发来的数据的时候,也应该按照这个协议进行分析识别。 第 8 章 网络俄罗斯方块游戏——Swing 组件与网络功能 341 8.5.2 实现网络连接 我们使用8.3节编写的即时通信的网络模块来实现网络连接。 首先要实现NetRead接口。代码如下: public class Tframe extends Jframe implements NetRead 然后在Tframe类里定义readStr(String str)、showMessage(String str)及sendStr(String str) 方法。 (1)sendStr(String str)方法的实现。 在这个方法里我们应该首先判断当前的网络是什么状态。如果是没有连接的状态,则 直接返回;如果是SERVER状态,则通过m_myServer(MyServer的对象)来发送数据;如 果是CLIENT状态,则通过m_myClient(MyServer的对象)来发送数据。 代码如下: public void sendStr(String str) { switch(m_nNetStatus) { case SERVER: if(m_server!=null) m_server.writeStr(str+"\n"); break; case CLIENT: if(m_client!=null) m_client.writeStr(str+"\n"); break; } } (2)showMessage(String str)方法的实现。 这个方法实现起来很简单。它是当网络模块传来消息的时候调用的一个方法,str为传 来的消息。我们要在这个方法中将传来的消息在StatusBar(聊天工具栏)里的JTextArea中 显示出来。我们可以直接调用StatusBar类里的appendStr(String str)方法。代码如下: public void showMessage(String str) { m_pStatus.appendStr(str+"\n"); } 其中m_pStatus为StatusBar类的一个对象。 (3)readStr(String str)方法的实现。 这是一个相当复杂的方法,它是当网络模块读出对方发来的数据的时候才调用的。我 们需要在这个方法里判断这个读出来的str是属于哪种类型的消息,然后取出消息的主体, 并相应地执行某种操作。 判断读出的字符串是属于什么类别的,需要用到String类的一些方法。String中给我们 342 Java 游戏编程导学 提供了许多操作字符串的简便方法。在前几章我们也用到了很多String提供的方法。这里将 String类的方法总结一下,如表8.9所示。 表8.9 String类的主要方法 返回类型 方法 用途 char charAt(int index) 返回在index位置处的字符 int compareTo(Object o) 与一个Object对象o比较,如果这个Object对象o的类 型是String ,则比较两个字符串,返回值同 compareTo(String str)一致。若o不是String类型,则 抛出ClassCastException异常 int compareTo(String anotherString) 比较两个字符串,如果相等则返回0,如果第一个字 符串大于第二个字符串(从字面上),则返回正数, 否则返回负数 int compareToIgnoreCase(String str) 同上,但是忽略由于字符大小写而带来的区别 String concat(String str) 连接两个字符串,参数str在后面 String copyValueOf(char[] data) 将字符数组data转化为字符串,静态方法 String copyValueOf(char[] data, int offset, int count) 将字符数组data从off处开始count个字符转化为字 符串,静态方法 boolean endsWith(String suffix) 判断字符串是否以suffix字符串结束 boolean equals(Object anObject) 判断一个字符串是否等于一个Object对象 boolean equalsIgnoreCase(String anotherString) 判断一个字符串是否等于另一个字符串 byte[] getBytes() 将一个字符串转化为字节数组,使用系统的默认编 码 byte[] getBytes(String enc) 将一个字符串转化为字节数组,使用名字为enc的编 码 void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) 从一个字符串的srcBegin到srcEnd位置之间的子字 符串转化到一个字符数组里,这个字符数组为dst, 转化的字符在字符数组的初始位置为dstBegin int indexOf(int ch) 返回字符ch在字符串里第一次出现的位置,如果字 符串里不存在字符ch,则返回-1 int indexOf(int ch, int fromIndex) 返回字符ch在字符串里从fromIndex位置开始第一 次出现的位置。如果字符串里从fromIndex位置处不 再存在字符ch,则返回-1 int indexOf(String str) 返回子字符串str在字符串里第一次出现的位置,如 果字符串里不存在子字符串str,则返回-1 int indexOf(String str, int fromIndex) 返回子字符串str在字符串里从fromIndex位置开始 第一次出现的位置。如果字符串里从fromIndex位置 处不再存在子字符串str,则返回-1 第 8 章 网络俄罗斯方块游戏——Swing 组件与网络功能 343 (续表) 返回类型 方法 用途 String intern() 返回一个字符串的正统名称,这个名称是惟一的, 只有两个字符串相等的时候,它们的返回值才相等 int lastIndexOf(int ch) 返回字符ch在字符串里最后一次出现的位置,如果 字符串里不存在字符ch,则返回-1 int lastIndexOf(int ch, int fromIndex) 返回字符ch在字符串里从fromIndex位置开始最后 一次出现的位置。如果字符串里从fromIndex位置处 不再存在字符ch,则返回-1 int length() 返回子串的长度 boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) 比较两个字符串的子字符串是否相等。这个子串对 象的子字符串是从toffset开始的,而other的子字符 串是从ooffset开始的,长度len。如果ignoreCase是真, 则不考虑字符大小写 boolean regionMatches(int toffset, String other, int ooffset, int len) 同上,但是都考虑字符的大小写 String replace(char oldChar, char newChar) 返回一个新的字符串,新的字符串是将老字符串里 的所有oldChar字符都替换成newChar而成的 boolean startsWith(String prefix) 判断一个字符串是否以子字符串prefix开头 boolean startsWith(String prefix, int toffset) 判断一个字符串在toffset处是否以子字符串prefix开 头 String substring(int beginIndex) 返回一个新的字符串,这个新的字符串是原来字符 串从beginIndex开始的子字符串 String substring(int beginIndex, int endIndex) 返回在位置beginIndex和endIndex之间的子字符串 char[] toCharArray() 将这个字符串转化成字符数组 String toLowerCase() 返回一个将字符串里的字符都变成小写的字符串 String toUpperCase() 返回一个将字符串里的字符都变成大写的字符串 String trim() 将开头和结尾的空格都去掉 String valueOf(boolean b) 返回一个相应于b的字符串,如果b为真,则返回 “true”,否则返回“false”,静态方法 String valueOf(char c) 返回与字符c相应的字符串,静态方法 String valueOf(char[] data) 将字符数组转化成字符串,返回这个字符串 String valueOf(char[] data, int offset, int count) 将字符数组从offset位置开始,长度为count的部分转 化成字符串,返回这个字符串 String valueOf(double d) 返回一个与double类型的数d相对应的字符串,静态 方法 String valueOf(float f) 返回一个与float类型的数f相对应的字符串,静态方 法 344 Java 游戏编程导学 (续表) 返回类型 方法 用途 String valueOf(int i) 返回一个与整型数i相对应的字符串,静态方法 String valueOf(long l) 返回一个与long类型的数l相对应的字符串,静态方 法 String valueOf(Object obj) 返回一个与对象obj相对应的字符串,静态方法 由上面讲到的String()方法,我们很容易分析出读出的数据是属于什么类型的信息。 在这个方法里我们逐步判断读出的数据是什么类型的信息,然后分别处理。 (1)首先判断读出的数据是否是对方的状态信息。我们知道对方的状态信息是以 “Status:”开头的,所以我们用String对象的StartWith来判断。如果是对方的状态信息,我 们就逐步分解,将对方的状态信息从字符串里提取出来(参看8.5.1节中网络协议的设计)。 代码如下: //如果读出来的数据是对方的状态信息 if(str.startsWith("Status:")) { int[] nRivalField=new int[Tetrics.m_nCols*(Tetrics.m_nRows+4)]; str=str.substring(7,str.length()); StringTokenizer st=new StringTokenizer(str, "|"); int i=0; try { while(st.hasMoreTokens()) { nRivalField[i]=Integer.parseInt(st.nextToken()); i++; } }catch(Exception e){} i=0; for(int col=0;col0;nRow--) { m_nField[nCol][nRow]=m_nField[nCol][nRow-1]; } for(int Col=0;nCol一般缩进8个空格或一个Tab键,不过也可以缩进4个空格。 (2)BSD风格 又叫做Allman Style,这种编码风格源自UnixBSD程序员Eric Allman,他经常在编写的 BSD应用程序里采用这种风格。笔者比较喜欢这种风格。 if(sth) { sth } (3)Whitesmith风格 if(sth) { sth } (4)GNU风格 if(sth) { sth } 那么在Java里用哪种好呢?建议你采用K&R风格或BSD风格。 在编写代码的时候,还要注意使我们的行宽不要超过80列,因为一般编辑器的宽度大 都是80列。当代码很长的时候,想办法把它分成两行或多行。 8.7.2 变量命名规则 本书采用的是匈牙利命名法则。匈牙利命名法是在我们要命名的变量前面加上前缀, 表征这个变量的类型。表8.10是本书的匈牙利法则的前缀规则。 表8.10 前缀规则 前缀 表征的变量类型 n int b byte c char d double f float l long sor str string m_ 类成员 如一个变量名为nBits,表示这是一个整型变量。若还有一个变量为m_nX,表示这是一 358 Java 游戏编程导学 个整型的类成员变量。 对常量,我们用全大写来表示。如: final int WIDTH=23; 8.7.3 编写文档 在 Java 里如何注释文档 Java里有两种类型的注释。第一种是传统的、C语言风格的注释,是从C++继承而来的。 这些注释用一个“/*”起头,随后是注释内容,并可跨越多行,最后用一个“*/”结束。注 意许多程序员在连续注释内容的每一行都用一个“*”开头,所以经常能看到像下面这样的 内容: /* 这是一段注释, 它跨越了多个行 */ 但请记住,进行编译时,/*和*/之间的所有东西都会被忽略,所以上述注释与下面这段 注释并没有什么不同。 /* 这是一段注释, 它跨越了多个行 */ 第二种类型的注释也起源于C++。这种注释叫作“单行注释”,以一个“//”起头,表 示这一行的所有内容都是注释。这种类型的注释更常用,因为它书写时更方便。没有必要 在键盘上寻找“/”,再寻找“*”(只需按同样的键两次),而且不必在注释结尾时加一 个结束标记。下面便是这类注释的一个例子: // 这是一条单行注释 使用 javadoc 来创建文档 在Java语言里,最体贴的一项设计就是它提供一个用来把代码的注释分离出来,创建 一个文档文件的工具:javadoc。当然我们必须遵守一定的编写注释的规则。用这个工具, 输出的是HTML文档。 javadoc的使用方法为: javadoc 要分离的java文件.java 这样javadoc就自动为这个文件生成HTML文档。 具体语法 javadoc能够识别的注释语言必须是以“/**”开头,以“*/”结尾的注释。 /** 一个类注释 */ public class Test 第 8 章 网络俄罗斯方块游戏——Swing 组件与网络功能 359 { /**一个变量注释 */ public int m_nHaha; /** 一个方法注释 */ public void someFunction() { } } javadoc只对public(公共)和protected(受保护)成员的注释文档处理。而private(私 有)和friendly(友好)成员的注释会被忽略。 各种标记 我们可以在注释里加入各种标记,分别在HTML文件里产生相应的格式。 (1)@see标记。引用其他类。有3种格式: @see 类名 @see 完整类名 @see 完整类名#方法名 每一种格式都会在生成的文档里自动加入一个超链接的See Also(参见)条目。注意 javadoc不会检查我们指定的超链接,不会验证它们是否有效。 (2)@version。格式如下: @version 版本信息 这里的“版本信息”是代表任何适合作为版本说明的资料。若在javadoc命令行使用了 “-version”标记,就会从生成的HTML文档里提取出版本信息。 (3)@author。格式如下: @author 作者信息 这儿的“作者信息”包括您的姓名、电子邮件地址或者其他任何相关的资料。若在javadoc 命令行使用了“-author”标记,就会专门从生成的HTML文档里提取出作者信息。 (4)@param。格式如下: @param 参数名 说明 这是对一个方法的标记,这里的“参数名”是指参数列表内的标识符,而“说明”代 表一些可延续到后续行内的说明文字。一旦遇到一个新文档标记,就认为前一个说明结束。 可使用任意数量的说明,每个参数一个。 (5)@return。格式如下: @return 说明 360 Java 游戏编程导学 这也是对一个方法的标记,这里“说明”是指返回值的含义。它可延续到后面的行内。 (6)@exception。格式如下: @exception 完整类名 说明 这也是对一个方法的标记,是有关“异常”(Exception)的详细情况。 其中,“完整类名”明确指定了一个异常类的名字,它是在其他某个地方定义好的。 而“说明”(同样可以延续到下面的行)告诉我们为什么这种特殊类型的异常会在方法调 用中出现。 (7)@deprecated。 这是对一个方法的标记,用户不用使用这种特定的功能,因为未来改版时可能摒弃这 一功能。若将一个方法标记为@deprecated,则使用该方法时会收到编译器的警告。 一个简单的文档实例 这是一段加了注释后的java代码: /** *@author 宋现锋 *@version 1.0 */ public class test { /**一个成员变量**/ int m_nScore=0; /** *将现有分数加上一个值 *@param a 一个实例 *@return 一个实例 *@see m_nScore */ public int addScore(int nAdd) { m_nScore+=nAdd; } } 我们对这个Java文件运行了以下命令之后,会在test.java所在的目录看到index.htm文件。 javadoc test.java –version –author 打开这个文件,可以看到如图8.26所示的文档。 第 8 章 网络俄罗斯方块游戏——Swing 组件与网络功能 361 图 8.26 用 javadoc 输出的文档 8.8 进一步实践 8.8.1 游戏还存在的问题 游戏已经编好了,但是,要是作为一个商业版本来发行的话,还远远不够。这个游戏 现在还存在着一些问题。 游戏还有很多功能上的缺陷,这里我列举一下,并提出一些解决的办法,希望有兴趣 的读者可以进一步改进。 (1)我们希望在连网之后,还能取消连网状态,这个功能的实现是很简单的,只要我 们再加一个菜单,在这个菜单的时间处理中,使得游戏的连网状态置为NOCONNECT即可。 (2)现在对键盘的响应有一种迟钝的感觉。这种感觉是由于重画而引起的。因为我们 采用了双缓冲的机制,双缓冲的区域是两个游戏区域。由于双缓冲的区域过大,造成重画 速度受到了影响。解决办法有两种: · 摒弃双缓冲机制,直接采用在屏幕上重画的方法。 · 使双缓冲的区域变小一点。 当然上面两种办法我们可以都采用,用来优化整个重画的效率。具体算法,留待读者 自己去考虑吧。 362 Java 游戏编程导学 8.8.2 使游戏界面变得更漂亮 本章我们使用了Swing组件来构建用户界面,但是由于我们一直专注于游戏功能的实 现,从而忽略了游戏界面的美观。大家可以查看有关Swing的文档(Sun公司免费提供), 尝试着使用Swing组件的一些新特性,我想大家一定会很快地掌握Swing组件,使界面变得 更专业化。 8.9 本章知识点回顾 ServerSocket 的主 要方法 ServerSocket(int port) 创建一个ServerSocket对象,将它与port端口 绑定。当port为零的时候,它将从空闲的端口 里任取一个端口绑定 ServerSocket(int port,int backlog) 同上,但是多了一个参数。参数backlog指它 能建立连接的最大个数 ServerSocket(int port,int backlog, InetAddress bindAddr) 同上,但是只监听来自bindAddr的请求 accept() 这是一个很重要的方法,它返回与它要建立 连接的机器的套接字。通过这个套接字我们 才能够与对方的机器进行通信,返回类型为 Socket close() 关闭这个套接字,返回类型为void getInetAddress() 返回本地的机器标识,返回类型为 InetAddress getLocalPort() 返回这个ServerSocket对象所绑定的端口数。 返回类型为整型 Socket的主要方法 Socket() 创建一个无连接的套接字对象 Socket(InetAddress address, int port) 创建一个套接字对象,使它与指定端口port 和指定的地址进行连接 Socket(InetAddress address, int port, InetAddress localAddr, int localPort) 同上,localAddr指的是本机地址 第 8 章 网络俄罗斯方块游戏——Swing 组件与网络功能 363 (续表) Socket(String host, int port) 创建一个套接字对象,使它与指定端口port 和指定的地址进行连接,host为要连接的机器 的名字 Socket(String host, int port, InetAddress localAddr, int localPort) 创建一个套接字对象,使它与指定端口port 和指定的地址进行连接,host为要连接的机器 的名字。localAddr为本机地址,localPort为本 机的这个Socket对象所绑定的端口号 close() 关闭这个套接字,返回类型为void getInetAddress() 返回这个套接字所连接的机器的地址,返回 类型为InetAddress getInputStream() 得到这个套接字的输入流,返回类型为 InputStream getLocalAddress() 得到本机的地址,返回类型为InetAddress getLocalPort() 得到这个套接字与本机绑定的端口,返回类 型为int getOutputStream() 得到这个套接字的输出流,返回类型为 OutputStream getPort() 得到这个套接字在所连接的机器(远程)所 绑定的端口,返回类型为int InetAddress的主要 方法 getAddress() 返回这个InetAddress对象的IP地址。类型为 byte[],我们可以通过getAddress()[0]来取得最 高位的IP,返回类型为byte[] getAllByName(String host) 得到一个机器的所有IP地址。因为一台机器 可以有多个IP,这个方法可以取得这个机器的 所有的IP地址,这是一个静态方法,返回类型 为InetAddress[] getByName(String host) 返回机器名为host的IP地址,返回类型为 InetAddress getHostAddress() 返回这个InetAddress所表征的IP地址,这个IP 地址用字符串来表示。如 “162.105.l01.107”, 返回类型为String InetAddress 的主 要方法 getHostName() 返回这个InetAddress对象所表征的机器的名 称。返回类型为String getLocalHost() 返回本机的InetAddress对象,返回类型为 InetAddress isMulticastAddress() 判断一个IP地址是不是D级地址,返回类型为 boolean 364 Java 游戏编程导学 (续表) String类的主要方 法 charAt(int index) 返回在index位置处的字符,返回类型为char compareTo(Object o) 与一个Object对象o比较,如果这个Object对象 o的类型是String,则比较两个字符串,返回值 同compareTo(String str)一致。若o不是String类 型,则抛出ClassCastException异常,返回类型 为int compareTo(String anotherString) 比较两个字符串,如果相等则返回0,如果第 一个字符串大于第二个字符串(从字面上), 则返回正数,否则返回负数,返回类型为int compareToIgnoreCase(String str) 同上,但是忽略由于字符大小写而带来的区 别,返回类型为int concat(String str) 连接两个字符串,参数str在后面,返回类型为 String copyValueOf(char[] data) 静态方法,将字符数组data转化为字符串,返 回类型为String copyValueOf(char[] data, int offset, int count) 将字符数组data从off处开始的count个字符转 化为字符串,静态方法,返回类型为String endsWith(String suffix) 判断字符串是否以suffix字符串结束,返回类 型为boolean equals(Object anObject) 判断一个字符串是否等于一个Object对象,返 回类型为boolean equalsIgnoreCase(String anotherStri ng) 判断一个字符串是否等于另一个字符串,返回 类型为boolean getBytes() 将一个字符串转化为字节数组,使用系统的默 认编码,返回类型为byte[] getBytes(String enc) 将一个字符串转化为字节数组,使用名字为 enc的编码,返回类型为byte[] getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) 从一个字符串的srcBegin到srcEnd位置之间的 子字符串转化到一个字符数组里,这个字符数 组为dst,转化的字符在字符数组的初始位置为 dstBegin,返回类型为void String类的主要方 法 indexOf(int ch) 返回字符ch在字符串里第一次出现的位置,如 果字符串里不存在字符ch,则返回-1,返回类 型为int 第 8 章 网络俄罗斯方块游戏——Swing 组件与网络功能 365 (续表) indexOf(int ch, int fromIndex) 返回字符ch在字符串里从fromIndex位置开始 第一次出现的位置。如果字符串里从 fromIndex位置处不再存在字符ch,则返回-1, 返回类型为int indexOf(String str) 返回子字符串str在字符串里第一次出现的位 置,如果字符串里不存在子字符串str,则返回 -1,返回类型为int indexOf(String str, int fromIndex) 返回子字符串str在字符串里从fromIndex位置 开始第一次出现的位置。如果字符串里从 fromIndex位置处不再存在子字符串str,则返 回-1,返回类型为int intern() 返回一个字符串的正统名称,这个名称是惟一 的,只有两个字符串相等的时候,这两个返回 值才相等,返回类型为String lastIndexOf(int ch) 返回字符ch在字符串里最后一次出现的位置, 如果字符串里不存在字符ch,则返回-1,返回 类型为int lastIndexOf(int ch, int fromIndex) 返回字符ch在字符串里从fromIndex位置开始 最后一次出现的位置。如果字符串里从 fromIndex位置处不再存在字符ch,则返回-1, 返回类型为int String类的主要方 法 length() 返回子串的长度,返回类型为int regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) 比较两个字符串的子字符串是否相等。这个子 串对象的子字符串是从toffset开始的,而other 的子字符串是从ooffset开始的,长度len。如 果 ignoreCase是真,则不考虑字符大小写,返回 类型为boolean regionMatches(int toffset, String other, int ooffset, int len) 同上,但是都考虑字符的大小写,返回类型为 boolean replace(char oldChar, char newChar) 返回一个新的字符串,新的字符串是将老字符 串里的所有oldChar字符都替换成newChar而 成的,返回类型为String startsWith(String prefix) 判断一个字符串是否以子字符串prefix开头, 返回类型为boolean startsWith(String prefix, int toffset) 判断一个字符串在toffset处是否以子字符串 prefix开头,返回类型为boolean 366 Java 游戏编程导学 (续表) substring(int beginIndex) 返回一个新的字符串,这个新的字符串是原来 字符串从beginIndex开始的子字符串,返回类 型为String substring(int beginIndex, int endIndex) 返回在位置beginIndex和endIndex之间的子字 符串String,返回类型为Stirng toCharArray() 将这个字符串转化成字符数组,返回类型为 char[] toLowerCase() 返回一个将字符串里的字符都变成小写的字 符串,返回类型为String toUpperCase() 返回一个将字符串里的字符都变成大写的字 符串,返回类型为String trim() 将开头和结尾的空格都去掉,返回类型为 String valueOf(boolean b) 静态方法,返回一个相应于b的字符串,如果b 为真,则返回“true”,否则返回“false”, 返回类型为String valueOf(char c) 静态方法,返回与字符c相应的字符串,返回 类型为String valueOf(char[] data) 将字符数组转化成字符串,返回这个字符串。 返回类型为String valueOf(char[] data, int offset, int count) 将字符数组从offset位置开始,长度为count的 部分转化成字符串,返回这个字符串,返回类 型为String valueOf(double d) 静态方法,返回一个与double类型的数d相对 应的字符串,返回类型为String valueOf(float f) 静态方法,返回一个与float类型的数f相对应 的字符串,返回类型为String valueOf(int i) 静态方法,返回一个与整型数i相对应的字符 串,返回类型为String valueOf(long l) 静态方法,返回一个与long类型的数l相对应 的字符串,返回类型为String String类的主要方 法 valueOf(Object obj) 静态方法,返回一个与对象obj相对应的字符 串,返回类型为String

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享文档获得金币 ] 1 人已下载

下载文档

相关文档