并不是我反应迟钝,只是我接到的是完全无用的指令。
——卡尔文,《卡尔文和霍布斯》
发布游戏有很多方法。我们已经知道,Java应用程序可以在任何支持Java的环境中运行。这些环境不仅包括Solaris和Windows,而且还包括手持设备和“智能”设备,甚至可以在搅拌机上编写游戏。然而,运行游戏最流行的方法还是从Web浏览器或者Sun的appletviewer工具运行Applet。在本篇开头,我们将研究一下Java的Applet类,并且研究一下怎样用很少的代码来开发可视化的程序。
到目前为止,我们应该已经具备了很多制作游戏必不可少的知识。已经掌握了Java语言的基本知识并且已经知道恰当地使用现成的Java类。在本篇中,我们将开始进行关于Java2-D游戏编程的研究。基于此目的,我们将探讨一下Java抽象Window工具包(AWT),及其主要扩展:Java 2-D,特别是以下主题:
q Applet基础:Applet究竟是什么,以及applet的结构和用途
q AWT组件:包括使用按钮,下拉列表框,文本框和布局管理器来提供和用户的交互。
q 事件处理:包括怎样捕获鼠标和键盘事件并且正确响应这些事件。
q Java 2-D:通过两章来纵览图形的建模,着色和填充,从文件加载和绘制图像,到文本的操作和着色。还将讨论几何叠加,冲突测试和图像增强之类的主题。
现在翻到第5章——Applet基础,准备学习如何创建一个交互的可视应用程序框架来构建Java游戏。
在本章,我们将研究applet的构成组件,看看applet和application之间有什么不同,并给出几个applet例子。由于本书的主要部分集中在基于applet的游戏开发上,因此首先搞清楚applet的工作机制是大有裨益的。
我们还将研究怎样把AWT组件添加到applet上。Java AWT包括一系列用来绘制可视化组件的类和接口。它们的根是Component类。在java.awt包中。
Component类是一个抽象类,用来扩展以创建特定的控件。所有的AWT类,包括容器类,都从Component类扩展而来。下面描述了Component类及其子类的层次关系。注意,这幅图并没有描述所有的AWT类,它只是描述了Component的扩展关系。
――――――――Component
| |
|—Button |-Container
|-Canvas |-Panel
|-Checkbox |-Applet
|-Label |-Window
|-List |-Dialog
|-Scrollbar | |-FileDialog
|-TextComponent |-Frame
|-TextArea
|-TextField
简单地说,一个Java applet就是一个嵌入在另一个程序里面运行的应用程序。换句话说,applet是不能独立运行的应用程序。
5.2 Applet和Application的比较
application是能独立于除Java虚拟机之外的所有软件运行。
5.3 Applet的组成和生命周期
Java Applet类中包含了初始化和运行applet所需要的方法,所需要做的工作是根据需要利用继承机制来覆盖Applet类的这些方法。当applet被加载到applet运行环境时,一些方法按照特定的顺序依次被调用。在Applet类范围内,这些方法事实上并不做任何事情。不过,它们并没有被声明为abstract,所以并不是非得覆盖它们。尽管如此,它们只是作为框架以供applet遵循。
当一个applet被加载到它的运行环境中时,它的init方法被调用。在init方法中,应该提供对applet进行初始化的代码,这包括初始化游戏中的对象以及加载游戏可能用到的图像和声音。接着被调用的方法是start,它只是传达applet已经作好执行的准备。可以使用start方法做诸如开始动画系列和线程之类的事情。
在start方法完成之后,paint方法被调用。在这里,applet的可视化内容被提供给窗体。而这个部分将会是我们的重点,因为用户会看到什么,将都在这个方法中被绘制。
stop方法是在Web浏览器被关闭或用户切换到其它网页的时候会被调用的方法,它结束applet的执行。所以如果在start方法中启动了动画,那么可以在这里结束它。在applet中最后被调用的是destroy方法,在这里可以中止活动着的对象,比如在applet的生命周期中所创建的线程。
5.4 一个Applet例子
下面的程序ManyShaps继承了Applet类并且只定义了一个方法:paint方法。这个paint方法在窗体上绘制10000幅图形。在每一次循环中,根据一个随机数选择所要绘制的图形。
import java.applet.*;
import java.awt.*;
import java.util.*;
public class ManyShapes extends Applet{
//覆盖Applet类的paint方法
public void paint(Graphics g){//用来绘制的图形环境
Random r=new Random();
for(int i=0;i<10000;i++){
//设置随机坐标
int x=r.nextInt()%300;
int y=r.nextInt()%300;
//设置随机宽高
int width=r.nextInt()%300;
int height=r.nextInt()%300;
//设置随机颜色
g.setColor(new Color(r.nextInt()));
//产生一个在0与4之间的正数,以确定画什么样的图形
int n=Math.abs(r.nextInt()%5);
//根据n的值绘制一幅图形
switch(n){
case 0:
g.draw3DRect(x,y,width,height,true);
break;
case 1:
g.drawRect(x,y,width,height);
break;
case 2:
g.drawOval(x,y,width,height);
break;
case 3:
g.fillRect(x,y,width,height);
break;
case 4:
g.fillOval(x,y,width,height);
break;
default:
System.out.println("非法类型:"+n);
break;
}
}
}
}
这一次只需要为程序定义一个方法,不需要定义像init那样的方法,因为没有类成员需要初始化。试运行上面的代码。如果需要编译和运行applet,可以参看下面部分。
现在复习一下怎么从命令行编译并运行刚完成的applet。如果还没有动手的话,请在一个命名为ManyShapes.java的文件中输入上面的程序清单。现在,必须创建一个后缀为.html的文件,在这个文件中包含一个用来装载applet的<applet>标签。下面的html代码用宽和高均为300像素的区域加载ManyShapes类。这里还在applet的前后各添加一个水平规则标签(<hr>),把它和这个文件的其他部分分隔开来。如果用户喜欢,也可以包含它。
<html>
<head>
<title>ManyShapes</title>
</head>
<body>
<hr>
<applet code=ManyShapes></applet>(原书这个地方写成了“ManyShapes.java”,个人观点:这样写不太正确,但经测试通过了,HTML真的很奇怪呀。)
<hr>
</body>
</html>
现在可以把这个.html文件以任何文件名保存,这里以一种没什么新意的方式存为ManyShapes.html。
现在可以编译源代码。和往常一样,如下调用javac工具
javac ManyShapes.java
如果出现什么错误请修正后再编译。一旦完成了编译,可以很简单地用自己喜欢的(支持java的)Web浏览器打开那个html文件,或者用appletviewer工具来打开它:
appletviewer ManyShapes.html
恭喜你!你的第一个Java applet——ManyShapes applet运行起来了。
5.6 通用AWT组件
为了加强印象,下面列出在applet中添加AWT组件的几个必需步骤:
q 在applet类中实现EventListener接口。
q 定义实现EventListener接口所必需的方法,可以暂时让这些方法为空。
q 在类中添加private或者protected的类成员。
q 初始化AWT组件、注册并把新创建的AWT组件添加到容器中。
q 填充EventListener接口的那些方法。
关于EventListener接口,个人觉得其实使用匿名类的形式也许会更好。另外,请大家要注意的是:这里说的EventListener接口,指的是所有XXXXXXListener接口,而不仅仅只是说EventListener接口,后面将要提到的ActionListener也是一个EventListener(即ActionListener继承自EventListener)。
5.6.1 按钮
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class ButtonTest extends Applet implements ActionListener{
private Button button;//按钮
//背景色
private final Color[] bgColors={Color.RED,Color.BLUE,Color.GREEN,Color.YELLOW};
//当前背景色索引
private int currentColor;
//重写Applet类的init()方法
public void init(){
button=new Button("点我");//创建一个按钮对象
button.addActionListener(this);//注册监听
this.add(button);//添加按钮到applet中
//初始化背景色的索引
currentColor=-1;
changeWindowColor();
}
public void paint(Graphics g){
//根据当前的索引设置窗体的背景色
setBackground(bgColors[currentColor]);
//设置按钮文本色为窗体背景色
button.setForeground(bgColors[currentColor]);
}
//当前索引递增
public void changeWindowColor(){
currentColor++;
if(currentColor==bgColors.length){
currentColor=0;
}
}
public void actionPerformed(ActionEvent e){
//如果按钮产生事件,改变窗口的背景色
if(button==e.getSource()){
changeWindowColor();
repaint();
}
}
}
这个Applet类为了把自己注册为一个可以接收按钮事件的对象而实现了ActionListener接口。ActionListener接口只定义了惟一的一个方法:actionPerformed,这个方法以ActionEvent对象作为参数。当一个事件发生时(比如单击并松开一个按钮),一个描述这个事件的ActionEvent对象被构建并传送给actionPerformed方法。在actionPerformed方法中可以定义实际发生的动作。
此外,我们还应该知道ActionEvent类的getSource方法的用法,它返回的是触发该事件的对象。当有多个组件并把它们用同样的ActionListener注册时,这是很有用的。
5.6.2 复选按钮
复选按钮和普通按钮不一样的地方在于:与复选按钮的交互不能被ActionListener捕获。取而代之的是,applet必须实现ItemListener接口。ItemListener接口和ActionListener很相似,它也只定义了一个方法:itemStateChanged,这个方法以ItemEvent作为参数。
使用单选按钮都需要把复选按钮放入按钮组中(是的,Java AWT中的复选框和单选按钮是同一个控件的两种表现形式)。按钮组保证在组内只有一个单一的选择。当组内的一个成员被选中时,组内其他的所有成员都被自动设为非选中。当applet要求一组成员中只有一个可以激活时,可以这样做。在Java中,CheckboxGroup类用来装载一组单选按钮。
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class CheckboxTest extends Applet implements ItemListener{
//用来容纳几个单选框的CheckboxGroup
private CheckboxGroup cbg;
//applet采用的选项
private final String[] selections={"百事可乐","可乐","私酿威士忌","Tab"};
private Checkbox createCheckbox(
String label, //复选按钮标签
CheckboxGroup group, //所属的组
boolean enabled //为真则设置这个复选框选中
)
{
Checkbox cb=new Checkbox(label,group,enabled);
cb.addItemListener(this);
return cb;
}
//重写Applet类的init方法
public void init(){
cbg=new CheckboxGroup();
for(int i=0;i<selections.length;i++){
this.add(createCheckbox(selections[i],cbg,false));
}
}
public void itemStateChanged(ItemEvent e){
//打印一条选择信息
System.out.println("是的,我也同意,"+cbg.getSelectedCheckbox().getLabel()+"非常好喝!");
}
}
5.6.3 作出重要选择
另外一个可能很有用的组件是Choice类,它使用起来很简单。Windows下的程序员知道它就是ComboBox,而Choice以类似的方式给用户提供了一个供选择的下拉列表框。里面的选项以String对象的方式来维护。
和Checkbox相类似,Choice对象向ItemListener类注册监听,ItemListener类必须实现ItemStateChanged方法,。
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class ChoiceTest extends Applet implements ItemListener{
//我们骨骼的技术名字
public final String[] BONES={"脚","腿","膝","臀","肋","肩","颈"};
private Choice choice;
public void init(){
this.setBackground(new Color(125,0,125));
//创建Choice并注册监听
choice=new Choice();
for(int i=0;i<BONES.length;i++){
//添加一个字符串来描述每一种选择
choice.add(BONES[i]);
}
choice.addItemListener(this);
this.add(choice);
}
//在已注册的监听器改变时调用
public void itemStateChanged(ItemEvent e){
//产生一个与当前所选的项不同的索引
int index;
do
{
index=(int)(Math.random()*BONES.length);
}while(index==choice.getSelectedIndex());
System.out.println(choice.getSelectedItem()+"骨连接了"+BONES[index]+"骨....");
}
}
5.6.4 循环播放声音文件
虽然用Java applet播放声音是一件微不足道的事情,但这里还是要谈几点。首先,建议所有声音文件采用Sun的.au格式,它可又保证跨平台工作。虽然其他的格式可能在Windows下工作正常,但是在Linux或者Mac之类的系统中则可能失效。
现在,让我们关注一下在applet中加载声音文件的最直接的方式。类Applet提供了集中加载并播放声音的方法。有两种直接播放声音的方法,都又play命名,其中之一用一个URL对象和一个String对象作为输入,另一个只又一个URL对象作为输入。这两种方法创建一个临时的AudioClip对象,然后马上把它的数据以流的方式输出为声音。即使声音文件不存在或者不可用,也并不会产生错误。
因为空闲时也在不断读和播放数据,所又play方法浪费内存并且有潜在使applet运行变慢的可能。处理游戏更好一点的方法是提前缓冲声音数据。在这种方式中,如果一个声音需要一次次重复播放的话,可又把它先加载到内在中的一个地方,又后只需从这个地方读取。Java提供了AudioClip这个接口来播放,循环,停止声音。虽然AudioClip是一个接口,但是Java在各个平台的各个版本都定义了它们自己内部的AudioClip的实现类。由于只是进行一般意义的编程,所以需要考虑的只是这个范围内的AudioClip。
要创建一个AudioClip对象,可又使用Applet类的getAudioClip方法。这个方法和play方法很类似,也是以一个URL对象或者一个URL对象和一个String对象作为输入。当声音内容和applet字节码储存在同一个位置时,建议使用两个参数的形式。
比如下面的代码加载一个名为bang.au的文件:
AudioClip ac=getAudioClip(getCodeBase(),”bang.au”);
getCodeBase方法返回applet字节码所处的位置,所又不管是在远程还是在本地运行这个applet,上面的调用方法都可又正常工作。
第三种加载音频片段的方法是使用Applet类的newAudioClip方法。这个方法只有一种形式,又一个指向声音文件的URL对象作为输入。由于它是一个static的方法,当想加载一个声音却又没有一个指向可用的applet对象的引用时,这是很有用的。
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class AudioChoiceTest extends Applet implements ActionListener{
//程序中声音的名字
public final String[] AUDIO={"ping","pop","return","salvation","shuffle","squish"};
//包含上述字符串数组的下拉列表框
private Choice choice;
//真实的音频片段数据
private AudioClip[] clips;
//控制播放和停止的按钮
private Button playClip;
private Button loopClip;
private Button stopClip;
private Button stopAllClips;
//跟踪当前哪些音频片段在播放
private boolean[] clipsPlaying;
public void init(){
this.setBackground(new Color(48,255,0));
//构建下拉列表框和AudioClip对象
choice=new Choice();
clips=new AudioClip[AUDIO.length];
clipsPlaying=new boolean[AUDIO.length];
for(int i=0;i<AUDIO.length;i++){
//为每一个选项添加一个描述的字符串
choice.add(AUDIO[i]);
//添加音频片段的路径和扩展名
clips[i]=this.getAudioClip(getCodeBase(),"audio/"+AUDIO[i]+".au");
System.out.println(getCodeBase()+"audio/"+AUDIO[i]+".au");
//标志这个片段是否在播放的布尔值
clipsPlaying[i]=false;
}
this.add(choice);
//创建播放和停止按钮
playClip=new Button("播放");
playClip.addActionListener(this);
this.add(playClip);
loopClip=new Button("循环");
loopClip.addActionListener(this);
this.add(loopClip);
stopClip=new Button("停止");
stopClip.addActionListener(this);
this.add(stopClip);
stopAllClips=new Button("全部停止");
stopAllClips.addActionListener(this);
this.add(stopAllClips);
//如果没有什么要停止的话,把"停止"按钮变灰
stopClip.setEnabled(false);
stopAllClips.setEnabled(false);
}
//停止所有音频片段的播放
public void stop(){
for(int i=0;i<AUDIO.length;i++){
if(clipsPlaying[i]){
clips[i].stop();
}
}
}
//允许用户使音频片段播放,循环播放或者停止
public void actionPerformed(ActionEvent e){
int clipIndex=choice.getSelectedIndex();
System.out.println(clipIndex);
AudioClip clip=clips[clipIndex];
//播放被选中的音频片段
if(e.getSource()==playClip){
clip.play();
stopClip.setEnabled(true);
stopAllClips.setEnabled(true);
clipsPlaying[clipIndex]=true;
}else if(e.getSource()==stopClip){//循环播放被选中的音频片段
clip.stop();
stopClip.setEnabled(false);
stopAllClips.setEnabled(false);
clipsPlaying[clipIndex]=false;
//只要有一个音频片段在播放就激活"停止"按钮
for(int i=0;i<AUDIO.length;i++){
if(clipsPlaying[i]){
stopClip.setEnabled(true);
stopAllClips.setEnabled(true);
break;
}
}
}else if(e.getSource()==stopAllClips){
for(int i=0;i<AUDIO.length;i++){
if(clipsPlaying[i]){
clips[i].stop();
clipsPlaying[i]=false;
}
}
stopClip.setEnabled(false);
stopAllClips.setEnabled(false);
}else if(e.getSource()==loopClip){
clip.loop();
stopClip.setEnabled(true);
stopAllClips.setEnabled(true);
clipsPlaying[clipIndex]=true;
}
}
}
//这个声音播放的程序让我感觉到play()方法似乎有一点问题,因为在用play()进行播放时
//如果声音播放时间很短的时候,会听不到声音的播放,而loop()却没有这个问题。
//会不会是Windows下面才有这个问题呢?很值得思考。
Java允许几个声音同时播放(因为每一个AudioClip就是一个线程嘛,这是很符合逻辑的),产生所有声音播放流的混合物,这使得不太费力就可以产生一些美妙的效果。
注意:如果想提供一个让用户能够从同一组中作出多项选择的界面,可以考虑使用List对象。可以查阅Java 2文档,了解更多关于List类的内容。
注意:不仅可以对对象添加监听器,而且还可以删除监听器。Button类有一个用来删除事件监听器的removeActionListener()方法。当applet的stop方法被调用时,删除监听器的方法也会调用。然而,由于一旦父线程被清除,所有的子线程也会被清除,所以当applet停止时并没有必要删除监听器。
5.6.5 文本域(TextField)
5.6.6 标签
5.7 布局管理
Java使用布局管理器来控制组件的摆放,实现LayoutManager接口的类知道如何在窗口内放置组件。在Java.awt包中有几个实现了LayoutManager接口的类,下面从FlowLayout类开始学习。
5.7.1 FlowLayout类
FlowLayout类可能是java.awt布局类中最简单的一个,它简单的按照组件被添加到applet的顺序从左到右摆放组件。这是默认的布局管理器。
如果想直接指定使用FlowLayout类(或其他的布局管理器),一般需要在容器的init方法中调用setLayout()方法。
setLayout(new FlowLayout());
FlowLayout类除了默认构造函数外,还有两个构造函数。第一个以一个描述布局对齐方式的init值作为输入参数,这个参数可以取5个值:LEFT CENTER RIGHT LEADING和TRAILING。下面的代码创建一个左对齐,水平和垂直间距都为10的FlowLayout:
setLayout(new FlowLayotu(FlowLayout.LEFT,10,10));
如果构造函数时没有输入参数,则默认的对齐方式为FlowLayout.CENTER,默认的间距为5像素。
5.7.2 GridLayout类
GirdLayout把组件放在一定行数和列数的网格中,网格中所有的组件尺寸都相同。
创建一个GridLayout类的方式和创建FlowLayout类似,只是它有不同的构造函数。默认函数每列放置一个组件,其他两个构造函数如下所示:
GridLayout(int rows,int cols);
GridLayout(int rows,int cols,int hgap,int vgap);
第一个指定了多行多列的网格,可以放置rows*cols个组件,第二个还指定了水平和垂直间隔(第三个和第四个参数)。如果不指定间隔,它们就默认为零,如果把行数或者列数指定为零,或者添加比行数乘列数还多的组件,这个布局管理器将调整其属性值使得组件仍然在它的网格之中。
import java.applet.*;
import java.awt.*;
import java.util.*;
public class GridLayoutTest extends Applet{
public void init(){
//创建一个字符串和一个解析字符串的StringTokenizer
String string="My Head Is My Only House Unless It Rains";
StringTokenizer st=new StringTokenizer(string);
//创建一个3×3的网格布局,组件之间的间隙为5像素
setLayout(new GridLayout(3,3,5,5));
//为每一个标志字符串创建一个背景为绿色的标签并把它加到panel上
while(st.hasMoreTokens()){
Label label=new Label(st.nextToken(),Label.CENTER);
label.setBackground(Color.green);
this.add(label);
}
}
}
1.7.3 BorderLayout类
BorderLayout类是另外一种布局管理器。这个类按照4个方向(北,南,东,西)的顺序排列组件,还定义了第五个方位:中。
setLayout(new BorderLayout());
add(new Button("North"),BorderLayout.NORTH);
add(new Button("South"),BorderLayout.SOUTH);
add(new Button("East"),BorderLayout.EAST);
add(new Button("West"),BorderLayout.WEST);
add(new Button("Center"),BorderLayout.CENTER);
除了默认构造函数外,还有一个构造函数,以分隔组件的水平和垂直间距为输入参数。
1.7.4 CardLayout类
CardLayout类把组件像一叠卡片那样一个叠一个地放在一起。CardLayout类有两个构造函数:默认构造函数和一个以水平和垂直间距为输入参数的构造函数。
下面的applet以动画的形式切换标着“Card 1”~“Card 10”的10个按钮对象,每一秒钟显现队列中的一个按钮。
import java.applet.*;
import java.awt.*;
public class CardTest extends Applet implements Runnable{
//一个充当定时器的线程
private Thread timer;
public void init(){
//创建一个新的CardLayout
setLayout(new CardLayout());
//在CardLayout中创建10个按钮
for(int i=1;i<=10;i++){
//第二个参数是卡片面板的名字
add(new Button("Card"+i),"Card"+i);
}
//把这个applet注册为Thread
timer=new Thread(this);
}
public void start(){
timer.start();
}
public void stop(){
timer=null;
}
//重写Runnable接口的run方法
public void run(){
//获得当前的布局管理器
CardLayout layout=(CardLayout)this.getLayout();
//得到这个线程的一个引用
Thread t=Thread.currentThread();
//当线程active时循环
while(t==timer){
layout.next(this);
//在更新前等待1秒
try{
timer.sleep(1000);
}catch(InterruptedException e){
return;
}
}
}
}
CardLayout类还有一些值得注意的方法,其中,first和last方法分别显示布局中的第一个和最后一个,还有一个显示布局中先前组件的previous方法。它们都是以一个Container对象作为输入参数。在这里,它是applet本身。
CardLayout类值得关注的第五个方法就是show方法,它以一个Container对象作为它的第一个参数,以一个String对象作为第二个参数,其中的String对象必须和创建布局时的add方法的第二个参数(也是一个String)对应。比如,下面的代码就可以显示标有“Card5”的按钮。
layout.show(this,"Card 5");