Java 2游戏编程读书笔记(7-1)

来源:互联网 发布:c语言质数 编辑:程序博客网 时间:2024/05/16 12:39

第7章   Java 2-D来绘制图形、文字和图像(第一部分)

对于游戏来说,用户最先注意到的是总体的视觉外观,这是决定成败的关键。

后文将使用Java 2-D API中的类作为填充图形组件的设备。Java 2-D所包含的组件可又在java.awtjava.awt.image包中找到,这使得Java 2D成为Java AWT的一部分。

Java 2-D包含绘制几何图形,图像又及文本的类和方法。如果你已经比较熟悉颜色混和,几何叠加和冲突检测等主题,那么尽可又放心,Java 2-D提供了对这些操作和其他很多方面的本地化支持。此外,1.4版的Java 2还支持硬件加速,这将允许游戏获得闪电一般快的帧刷新率(frame rate)。

Java 2-D的一个重要元素是Graphics2D类,在研究这个类的实现细节之前,我们先来看看将要用到的坐标系统。

7.1      坐标空间

Java 2-D系统定义了两个坐标系统:用户空间和设备空间。用户空间是一个被所有Java 2-D应用程序使用的抽象的,逻辑上的坐标系统。用户空间独立于用来绘画的任何设备,所又不管是把一个图像画到单显示器的系统中的窗体上,双显示器的系统中的窗体上,还是打印到打印机,都可又独占使用这个坐标系统。用户空间有一个坐标原点(00),它位于绘制目标的左上角。

设备坐标指的是一个特定设备内的坐标系统,这个设备可又是从显示器到打印机的任何事物。Java 2-D支持把用户空间内的坐标转换为实际设备空间坐标的三层体系。对于我们,需要做的只是在用户坐标中编程,其他的会自动处理。

7.2      Graphics2D

Java的早期实现是使用Graphics类来进行一般组件的绘制,比如线条和图像的绘制。除了绘制组件,Graphics类还保持绘制的状态属性,比如用来绘制的当前颜色和当前的变换状态。所有这些状态变量的快照被称为图形环境。

Java的设计者通过开发Graphics2D类扩展了Graphics类。Graphics2D类中包含了很多Graphics类不具备的增强特性,为了向后兼容,所有像Applet这样的容器类仍然传递原始的Graphics到它的方法中。由于Graphics2D类直接继承自Graphics类,所又直接进行类型转换就可又得到一个有用的Graphics2D环境,如下所示:

public void methodX(Graphics g){

       Graphics2D g2d=(Graphics2D)g;

       //在这里做一些很酷的填充

}

Graphics2D类可又做什么?下文将探讨下列特征:

q  变换

q  颜色模型

q  绘制和填充操作

q  绘制图像

q  绘制文本

q  使用几何图形

q  设置绘制提示

7.3      使用仿射变换

在图形中,我们使用变换来操纵几何图形并把它放入到一个画面中。变换可又有很多形式,比如平移(顺着x,y轴移动),旋转(相对一个定点角度移动),翻转(相对一个给定的轴或者线作对称图形),缩放(均衡放大或者收缩)和剪裁(顺着一个轴线失真)。

Java 2-D中,所创建的所有变换都是仿射变换。

仿射变换指的是任何平移和旋转等操作的组合,它们会保持直线和线段之间的平行关系。所以虽然我们可又操纵任何给定的图形,但是图形本身不会受到影响。

仿射变换可是惟一的,因为它们不会参照原点,而矢量变换参照原点。在这种方式下,同样的仿射变换可又用来在用户空间或者一个给定的设备空间内映射坐标。

产生仿射变换所需要的操作包含在AffineTransform类中。它使用右手坐标系统(关于坐标系统,请参考其他专业书籍),它还使用一个3×3矩阵来保存变换信息。这是令很多没有很好线性几何基础的初学者感到迷惑的地方。幸运的是,Java是面向对象的,而且事实上所有工作都做好了。AffineTransform类高度抽象,因此,无须知道它如何工作,只须知道怎么使用它即可。

当使用默认构造函数创建一个AffineTransform对象时,它代表恒等变换。恒等变换不影响画面,也就是说,它不影响任何所应用的几何形状。当需要清除一个变换时总是调用AffineTransformsetToIdentity方法。

正如前面所提过的,仿射变换是任何独立变换的组合。那么,怎样来组合这些变换呢?答案是串接。要串接变换,需要做的只是按顺序来定义这些变换。Java 2-D将会给出最后的变换。记住无须知道它是怎样做的,只需要知道使用即可。

这里有一个简单的例子。假设有一个矩形,想让它绕原点旋转45度,然后顺着X轴和Y轴各移动100个单位。

事实上设置这些变换相当简单,但要知道一件事:恒等变换的规则是最后指定的变换被最先应用,用栈的方式分析它就是:最后进栈的元素最先被去掉。因此,如果想对一个物体先缩放,然后旋转,接着平移,应按下列顺序操作

设置平移。

设置旋转。

设置绽放。

简言之:按照和逻辑上应用的相反顺序来指定变换,实际的代码可能如下所示:

Graphics2D g2d=(Graphics2D)g;

AffineTransform at=new AffineTransform();

 

at.setToIdentity();

at.translate(100,100);

at.rotate(radians);

at.sale(10,10);

g2d.setTransform(at);

指定转换的顺序真的要紧吗?答案是肯定的。例如,假设有一个物体位于原点(00),把它旋转30度后平移(2040),与平移(2040)后再旋转30度的结果是完全不同的,下面的程序AffineTest将证明这一点。

import java.applet.*;

import java.awt.*;

import java.awt.event.*;

import java.awt.geom.*;

import java.util.*;

 

public class AffineTest extends Applet implements ItemListener{

       //要绘制的矩形

       private Rectangle2D rect;

      

       //选择应用变换的顺序的两个单选按钮

       private Checkbox rotateFirst;

       private Checkbox translateFirst;

      

       public void init(){

             

              setLayout(new BorderLayout());

             

              //创建一个CheckboxGroup来容纳这两个单选按钮

              CheckboxGroup cbg=new CheckboxGroup();

              Panel p=new Panel();

             

              rotateFirst=new Checkbox("旋转,平移",cbg,true);

              rotateFirst.addItemListener(this);

              p.add(rotateFirst);

              translateFirst=new Checkbox("平移,旋转",cbg,true);

              translateFirst.addItemListener(this);

              p.add(translateFirst);

              add(p,BorderLayout.SOUTH);

             

              //基于原点来构建矩形

              rect=new Rectangle2D.Float(-0.5f,-0.5f,1.0f,1.0f);

       }

      

       public void paint(Graphics g){

              //把传入的Graphics容器转换为一个可用的Graphics2D对象

              Graphics2D g2d=(Graphics2D)g;

             

              //保存一个恒等变换来清除Graphics2D容器

              final AffineTransform identity=new AffineTransform();

             

              //如果先旋转则为真

              boolean rotate=rotateFirst.getState();

              System.out.println(rotate);

              Random r=new Random();

             

              final double oneRadians=Math.toRadians(1.0);

              for(double radians=0.0;radians<2.0*Math.PI;radians+=oneRadians){

                     //清除Graphics2D的变换

                     g2d.setTransform(identity);

                    

                     //记住,操作的顺序和逻辑顺序刚好相反

                     if(rotate){

                            g2d.translate(100,100);

                            g2d.rotate(radians);

                     }else{

                            g2d.rotate(radians);

                            g2d.translate(100,100);

                     }

                    

                     g2d.scale(10,10);

                    

                     g2d.setColor(new Color(r.nextInt()));

                     g2d.fill(rect);

              }

       }

      

       public void itemStateChanged(ItemEvent e){

              //新的单选按钮被中时,最好重绘

              repaint();

       }

}

7.4      绘制形状

Graphics2D使用一个单一的draw和单一的fill方法代替了Graphics类的一系列drawfill方法,而且他们都以一个Shape对象作为输入参数,还Graphics2D环境中包含的当前AffineTransform,使用这种方式,所有的点都为自己的数据结构所容纳,这使得可以很简单地改变对象的几何形状,所需要做的只是更改这些对象的声明,而这样做了之后,drawfill方法仍然可以工作。

究竟什么是Shape对象呢?Shape是一个接口,由想要代表某种几何形状的类实现。Shape接包含访问形状的边界和探测冲突的方法。到后面谈到动画和冲突检测时,我们会更具体地来研究Shape中的方法。

                                          Shape

                                         

————————————————————

|—Polygon                                                   |—Area

|—RectangularShape*                                    |—Line2D*

|—Arc2D *                                                       |—Line2D.Float

       |—Arc2D.Float                                          |—Line2D.Double

                  |—Arc2D.Float                        |—GeneralPath

       |—Ellipse2D*                                        |—QuadCurve2D*

                  |—Ellipse2D.Float                               |—QuadCurve2D.Float

                  |—Ellipse2D.Double                            |—QuadCurve2D.Double

       |—Rectangle2D*                                    |—CubicCurve2D*

                  |—Rectangle                                                 |—CubicCurve2D.Float

                  |—Rectangle2D.Float                              |—CubiCurve2D.Double

                  |—Recanle2D.Double

       |—RoundRectangle2D*

                     |-RoundRectangle2D.Float

                     |-RoundRectangle2D.Double

*号表示抽象类

在图中列出的类中,绝大部分都可以直接从名字推知用途。下文将探讨其中一些比较独特,比较有趣的类。现在,让我们来看一个绘制和填充形状的例子。下面的MouseShapeTest applet,在鼠标的光标位于一个事先定义好的Shape边界内时,填充这个Shape。如果鼠标不在Shape边界内,则只绘制轮廓。

import java.applet.*;

import java.awt.*;

import java.awt.event.*;

import java.awt.geom.*;

import java.util.*;

 

public class MouseShapeTest extends Applet implements MouseMotionListener{

       //要绘制的Shape

       private Shape shape;

      

       //鼠标光标是否在Shape的标志上

       private boolean mouseOver;

      

       //当前绘制和填充的颜色

       private Color currentColor;

      

       public void init(){

              //Shape的控制点

              int[] x={25,55,60,75,110,130};

              int[] y={65,100,133,20,115,55};

             

              //创建一个新的Polygon来代表我们的Shape

              //记住,PolygonShape,因为它实现了Shape接口

              shape=new Polygon(x,y,x.length);

             

              mouseOver=false;

             

              addMouseMotionListener(this);

       }

      

       public void paint(Graphics g){

              //把传入的Graphics容器转换为一个可用的Graphics2D对象

              Graphics2D g2d=(Graphics2D)g;

             

              g2d.setColor(currentColor);

             

              //如果鼠标光标在上方则填充Shape

              if(mouseOver){

                     g2d.fill(shape);

              }else{

                     g2d.draw(shape);

              }

       }

      

       public void mouseMoved(MouseEvent e){

              //保存先前的值

              boolean prevValue=mouseOver;

             

              //使用Shape.contains方法来更新mouseOver标志

              mouseOver=shape.contains(e.getPoint());

             

              //只是在有理由时重绘

              if(prevValue!=mouseOver){

                     //在可以的时候改变当前颜色

                     currentColor=new Color(new Random().nextInt());

                     repaint();

              }

       }

      

       public void mouseDragged(MouseEvent e){

              //Nothing

       }

}

本书推荐把几何形状声明为Shape对象,这种方法有很大的灵活性,可以把一个Shape放到任何实现了Shape接口的物体上,让多态性来为我们工作。

7.5      实例建模

在上节中的MouseShapeTest applet中,我们定义了组成Polygon对象的特定点,坐标被随意地放到窗体内。虽然图形的几何形状和特殊的例子很吻合,但还是有些缺点。比如,如果想移动对象该怎么办?柯以分别操纵每一个点,但是这将变得单调乏味;也可以使用AffineTransform来移动对象,但这样做的问题在于,如果有很多物体需要处理时,就会很快对画面失去控制。

这个问题的一个合理的解决方案是实例建模。实例建模包括两种原则,首先,几何图形是基于原点建模得到的,或者说是根据原点定义的,这给了所有对象同样的参照框架,如果希望操纵整个场景,这是很有用的。它还允许一个对象就像在AffineTest applet中实现的那样绕它自身旋转。

实例建模的第二个原则是把几何形状定义为单位尺寸,换句话说,应该尽量构造所有的对象,使得它们的所有维度都是一个单元长。构造为单位尺寸的最大好处是对象不用被具体的尺寸所束缚。领先简单地使用AffineTransform来缩放对象,可以创建任何尺寸的对象。单位尺寸允许以线性的方式缩放。

下面是一个使用实例建模的简单而粗糙的例子,如果所用的方法使你迷惑,那么可以试着生成不同的形状或者尝试使用一个简单的形状来构建一个复杂的形状。

7.6      Image

Image就是图像。

图像就是指一个可以画到屏幕上的n*m的像素阵列,这样的一个像素阵列也称为光栅。

图像格式有很多种,由于Java可以在任何平台上绘制.gif图像格式,我们就集中注意力在这种格式上。

除了图像格式问题之外,不同的平台处理和绘制图像的方式也不同,因此,需要一种方法来抽象定义一个图像并允许操作系统来做底层工作。幸运的是,Java提供了一个Image类。Image类是一个抽象类,它包含可以(在其他物体中)创建Graphics容器以及获取图像宽度和高度的抽象方法。

Applet类定义了两个加载Image对象的方法,都以getImage命名。和创建AudioClip对象很相似,我们将主要考虑采用其中两个参数的方法,这两个参数是一个URL对象和一个String对象。URL对象包含applet的互联网位置信息,由于applet设计为在Web浏览器中运行,所以图像的URL是一个必要因素;String参数指的是要加载的图像的实际文件名。getImage方法把这两个参数连接到一起创建一个指向实际图像文件的路径。

注意:对于.gif有几个必须注意的地方,首先,在.gif文件内可以自由定义透明层,Java2-D会在程序中保留透明性。如果作为一个OpenGL或者DirectX程序员,可能会很感激在图像文件中嵌入透明层的能力,事实上是无须任何工作就可以获得透明的物体。其次,有些人认为由于.gif文件只能包含256种颜色,所有的颜色会映射到同一个颜色检索表中。不要为把.gif文件规格化为一个256色而担心,只要整体桌面分辨率超过8位颜色,图像看起来就会很好。

由于图你文件一般不会存放在离applet很远的地方,所以可以使用Applet类的getCodeBase方法来获取appletURL位置。在我们所有的救命中,都将使用这种方法。如果applet需要加载很图像,则可能需要把applet的位置存到一个URL对象中以避免过多方法调用。加载一个名为ship.gif图像文件的代码可能会如下所示:

private Image ship;

 

public void init(){

       ship=getImage(getCodeBase(),”ship.gif”);

}

默然:想知道getCodeBase()返回什么?执行下面的代码,你就会明白。

public class CodeBaseTest(){

       public void init(){

              System.out.println(getCodeBase());

       }

}

注意:Windows用户必须尊重其他平台的用户。由于很多非Windows平台操作系统对文件名是大小写敏感的,所又在使用文件名时注意不要混淆。比如,如果有一个名为MYFILE.GIF的文件,那么注意在代码中不要写成myfile.gif(小写的),否则你可能会想不通为什么UNIX上的朋友不能运行你的程序。

加载图片完成后,我们就需要进行绘制。Graphics2D类中我们感兴趣的drawImage方法以3个参数作为输入;我们希望绘制的Image对象,包含转换信息的AffineTransform和一个ImageObserver对象。

默然:什么是Observer呢?中文翻译过来,就是观察者,ImageObserver就是图像观察者的意思。Applet实现了ImageObserver接口,它的作用就是观察画在它上面图像有没有发生什么变化,如果有发生,立即将变化绘制出来,而不用我们再去手动调用repaint方法。

下面的ImageTest applet从文件中加载3Image对象,然后绘制100个图像,每一个使用一个随机生成的变换。

import java.applet.*;

import java.awt.*;

import java.awt.event.*;

import java.awt.geom.*;

import java.util.*;

import java.net.URL;

 

public class ImageTest extends Applet{

       //要绘制的Image数组

       private Image[] images;

      

       //图像的文件名

       private final String[] filenames={"simon.gif","tj2gp.gif","blade.gif"};

      

       public void init(){

              //得到基准URL

              URL appletBaseURL=getCodeBase();

             

              //为图像分配内存

              int n=filenames.length;

              images=new Image[n];

              for(int i=0;i<n;i++){

                     images[i]=getImage(appletBaseURL,filenames[i]);

              }

       }

      

       public void paint(Graphics g){

              //把传入的Graphics容器转换为一个可用的Graphics2D对象

              Graphics2D g2d=(Graphics2D)g;

             

              //保存一个恒等变换

              final AffineTransform identity=new AffineTransform();

             

              //用来变换图像

              AffineTransform at=new AffineTransform();

             

              Random r=new Random();

             

              int width=getSize().width;

              int height=getSize().height;

              int numImages=filenames.length;

             

              //绘制100个图像,每一个应用一个随机的变换

              for(int i=0;i<100;i++){

                     //清除变换

                     at.setTransform(identity);

                    

                     //随机设置平移和旋转

                     at.translate(r.nextInt(%width,r.nextInt()%height));

                     at.rotate(Math.toRadians(360*r.nextDouble()));

                    

                     //绘制图像

                     g2d.drawImage(images[i%numImages],at,this);

                    

              }

       }

}

这里设计ImageTest applet来模仿InstanceTest applet,这样就可又讨论绘制图像和绘制图形之间的不同点和相似之处。首先,图像和图形都使用仿射变换来把它们放到场景之中。事实上,任何特定的AffineTransform对象都可以同样应用到图像和图形,另一个显著的差异是图形的绘制依赖于Graphics2D对象本身的AffineTransform对象,而drawImage方法却需要专门的AffineTransform作为参数。此外,把图像存为单位尺寸几乎没有什么好处,这是因为图像很少可以重用,通常总是用希望图像在场景中可见的尺寸来创建图像。然而,如果希望调整一个图像的尺寸,还是可又对AffineTransform对象运用缩放操作的。很快我们将看到如何通过抽象把这两个概念封闭到一个类结构中。 

原创粉丝点击