Java2游戏编程读书笔记(9-1)

来源:互联网 发布:excel 数据有效性 编辑:程序博客网 时间:2024/04/29 12:48

第三篇.    面向大众的Java游戏

“道存在于电子游戏中吗?”初学者接着问。

“是的,它也存在于电子游戏中。”大师说。

——摘自Geoffrey James著《编程之道》

在第2篇中,已经看到了Java 2-DJava AWT的强大功能。本篇中将进一步学习如何在游戏编程中应用这些技术,还会看到如何运用面向对象原则来为游戏创建可重用的游戏对象。在接下来的几章中,我们将专注于2-D游戏的开发。

那么2-D游戏究竟是什么呢?本书把2-D游戏定义为图像不保持其物体真实几何形状的游戏。一般说来,2-D是把在一个理想的(或者甚至还不理想的)世界“平放”在一个二维平面上。2-D游戏可以包含“看起来”像3-D的图形,但是不存在让图像显示它的所有表面的操作。

以下是本篇简要描述:

q  2-D动画技术。包含Java MediaTracker类、为动画使用图像条、帧同步以及使用加速图像进行屏外绘制的详细研究。

q  创建自定义的角色类。对自定义Actor2D类的第一手研究,以及增强其功能的工具类。

q  实现自定义的场景管理类。关于如何在游戏中使用场景管理器的讨论,还会学习到如何使用四方树来进行场景管理,并学习如何让游戏进入全屏独占模式。

q  创建可视化的控件和菜单。由于原始的AWT对于游戏而言可能太“繁琐”,我们将研究如何实现自定义的可视化控件系统。首先学习如何在游戏中创建自定义菜单系统。

q  创建客户端/服务器应用。很多时候,通过网络与两个或者以上的人一起玩游戏会更有趣,下面将看到如何在游戏中应用基于连接的网络协议和无连接的网络协议。

q  Nodez!游戏一瞥。这里我们将研究一个真实的,生动的Java游戏Nodez!Nodez!游戏将包含本书讨论的所有主题。

在开始制作Nodez!游戏前,必须开始创建和管理游戏场景所必需的工具。下面开始第9章的学习,首先看看在Java中如何制作2-D动画。

第9章   2-D动画技术

在本章中,我们将专注于基于帧的动画,或者说由静态绘制循环形成的动画。使用基于帧的动画,画面上的物体随着时间独立更新和绘制。在连接的事件循环中包含用来更新画面的代码。

在本章中,我们将研究以下主题:

*  使用MediaTracker类远程加载图像。

*  创建使用线程来产生动画系列的applet

*  使用图像条生成角色动画和位图字体。

*  实现双缓冲的屏外绘制机制。

*  使用帧同步完善动画计时。

9.1      使用MediaTracker类来下载内容

假设想下载一系列的图像,然后在它们上面执行一些图像增强操作,怎样才能保证在应用图像操作之前图像已经加载完毕呢?实现一个同步延时机制可能会有效,但是设置的延时值很可能比实际完全加载图像数据所需的时间长或者短。我们需要一种方法来正确判断什么时候图像完全加载完毕。

使用位于java.awt包中的MediaTracker类,可以跟踪图像的加载进程。MediaTracker类暂时只支持图像的跟踪,不过将来很可能会包括对声音文件的支持。要创建一个MediaTracker对象,只需在其构造函数中给它传递一个Component对象(通常applet或者窗体的引用)。所传入的Component将代表最终绘制图像的对象。

可以附加任意数量的图像到MediaTracker对象上,然后获取特定图像的加载状态或者所有被附加的图像全体的状态。还可以对每一个图像组设置一个ID数字,把它们彼此区分开来,较小数字的ID比较大数字的ID有更高的优先级。所以,如果希望加载一组想进行开来,较小数字的ID比较大数字的ID有更高的优先级。所以,如果希望加载一组想进行加强处理的图像,应该对这些图像设置零。在零优先级的图像结束下载后,可以执行图像增强操作,而同时,较低优先级的图像继续在下载。

下面来看一个完整的例子,以便对MediaTracker类如何工作有一个初步的了解。下面的TrackerTest applet,从文件中加载6个不同的动画帧,然后使用一个Thread来处理动画更新(如果需要复习Java中线程的使用,可以参考第4)TrackerTest appletpaint方法绘制动画系列中的6个实例,每一个是动画的不同的帧。

import java.applet.*;

import java.awt.*;

import java.awt.geom.*;

 

public class TrackerTest extends Applet implements Runnable{

       //动画线程

       private Thread animation;

      

       //Image对象数组,还有第一个图像的索引

       private Image images[];

       private int firstIndex;

      

       //要加载的图像数

       private final int NUM_IMAGES=6;

      

       //一帧图像的宽和高

       private int imageWidth;

       private int imageHeight;

      

       public void init(){

              images=new Image[NUM_IMAGES];

              firstIndex=0;

             

              //为这个Component创建新的MediaTracker对象

              MediaTracker mt=new MediaTracker(this);

              java.net.URL baseURL=getDocumentBase();

             

              //加载图像帧,并以优先级0添加到MediaTracker

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

                     images[i]=getImage(baseURL,"fire"+i+".gif");

                     mt.addImage(images[i],0);

              }

             

              try{

                     //等待,直到图像完全加载再继续

                     mt.waitForID(0);

              }catch(InterruptedException e){

                     //do Nothing

              }

             

              //现在可以肯定图像已经加载,获取它们的宽和高

              imageWidth=images[0].getWidth(this);

              imageHeight=images[0].getHeight(this);

             

              setBackground(Color.BLACK);

       }//init

      

       public void start(){

              //启动动画线程

              animation=new Thread(this);

              animation.start();

       }

      

       public void stop(){

              animation=null;

       }

      

       public void run(){

              Thread t=Thread.currentThread();

              while(t==animation){

                     try{

                            Thread.sleep(100);

                     }catch(InterruptedException e){

                            break;

                     }

                    

                     //动画索引递增,如果需要则循环

                     if(++firstIndex>=images.length){

                            firstIndex=0;

                     }

                    

                     repaint();

              }

       }//run

      

       public void paint(Graphics g){

              Graphics2D g2d=(Graphics2D)g;

             

              AffineTransform at=AffineTransform.getTranslateInstance(20,20);

             

              //对数组中的每一个图像绘制一帧

              int currFrame;//帧绘制

              for(int i=0;i<images.length;i++){

                     currFrame=(firstIndex+i)%images.length;

                    

                     g2d.setTransform(at);

                     g2d.drawImage(images[currFrame],null,this);

                    

                     g2d.setPaint(Color.WHITE);

                     g2d.drawString(""+currFrame,imageWidth/2,imageHeight+20);

                     at.translate(100,0);

              }

       }//paint

}

注意在等待媒体下载时try块的使用,就是捕获在等待体下载过程中发生的任何中断异常。如果这个applet的输出图形有些闪烁,不要担心,在本章后面将学习如何消除它。

最后我们要讨论的特性是MediaTracker类的状态检查和错误检测能力。使用statusIDstatusAll方法,可以检测MediaTracker对象所关联的图像的状态。statusID方法以一个整数ID作为输入,它代表我们感兴趣的特定优先级。statusIDstatusAll方法都还有一个boolean型输入参数,如果这个值被设为true,那么这个方法将开始下载任何还没有开始加载的媒体。这两个方法返回值是当前被跟踪的所有媒体状态按位或(OR)的结果,如果返回值为零,那么相关的媒体还没有开始加载。

下面的代码显示了如何访问被一个MediaTracker对象跟踪的所有媒体的状态。由于状态包含一系列被OR操作的属性,在状态和MediaTreaker类的4个静态int(ABORTED,COMPLETE,ERRORED,LOADING)之一之间使用按位与(AND)会得到多个状态值。然后可以编写代码来处理跟踪器的每一种可能状态。

int status=mt.statusAll(false);

       if(status==0){

              System.out.println("媒体尚未开始加载...");

              //等待代码

       }else{

              if(status & MediaTracker.AORTED!=0){

                     System.out.println("媒体加载失败!");

                     //放弃处理代码...

              }

              if(status & MediaTracker.COMPLETE!=0){

                     System.out.println("媒体加载完成!");

                     //很好...

              }

              if(status & MediaTracker.ERRORED!=0){

                     System.out.println("媒体加载错误!");

                     //错误处理代码

              }

              if(status & MediaTracker.LOADING!=0){

                     System.out.println("媒体加载中...");

                     //等待代码

              }

       }

利用状态检测的一个方式是只绘制已经被完全加载的图像。作者更倾向于在程序初始化阶段检测图像的状态,以确保updatepaint方法被调用时已经完成了图像的加载。

如果使用MediaTracker对象的应用程序遇到一个错误,看起来好像所有的图像都丢失了,那么,至少创建一点东西来绘制是一个很好的主意。一个跟图像尺寸一样的红色方块可以成为真实图像的替代物,给图像创建一个没有属性需要加载的替代物可以充当一个占位标志,这样游戏至少还可以玩。

下面的代码片段演示了如何为未加载的图像提供占位标志。如果MediaTracker报告一个错误,它会对每一个没有被加载的Image提供一个填充着随机颜色的BufferedImage

//检查错误

       if((mt.status(0,false) & MediaTracker.ERRORED)!=0){

              BufferedImage bi;//"替代"图像

              java.util.Random random=new java.util.Random();

             

              //BufferedImage的边界

              Rectangle rect=new Rectangle(imageWidth,imageHeight);

             

              //我们将通过检测宽小于或等于0来检测"坏的"图像

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

                     if(image[i].getWidth(this)<=0){

                            //创建一个imageWidth*imageHeight的缓冲图像

                            bi=new BufferedImage(imageWidth,imageHeight,BufferedImage.TYPE_INT_RGB);

                           

                            //用随机颜色填充图像

                            Graphics2D g2d=bi.createGraphics();

                            g2d.setPaint(new Color(random.nextInt()));

                            g2d.fill(rect);

                           

                            //设置新图像

                            images[i]=bi;

                     }

              }

       }

还有,在媒体错误检测中可以自由使用MediaTracker类的isErrorAnyisErrorIDgetErrorsAnygetErrorsID方法。这些方法可以跟踪在加载过程中遇到错误的图像。

下面是TrackerErrorTest applet的代码,其中就在init方法中增加了测试代码。

        import java.applet.*;

     import java.awt.*;

     import java.awt.image.*;

     import java.awt.geom.*;

 

     public class TrackerErrorTest extends Applet

                                   implements Runnable

     {

        // 动画线程

        private Thread animation;

 

        // Image对象数组以及第一帧图像的索引

        private Image images[];

        private int firstIndex;

 

        // 要加载的图像数目

        private final int NUM_IMAGES = 6;

 

        // 一帧图像的宽和高

        private int imageWidth = 90;

        private int imageHeight = 90;

 

        public void init()

        {

            images = new Image[NUM_IMAGES];

            firstIndex = 0;

 

            // 为这个Component创建一个新的MediaTracker对象

            MediaTracker mt = new MediaTracker(this);

            java.net.URL baseURL = getDocumentBase();

 

            // 加载图像帧,并以0优先级添加到MediaTracker中去

            for(int i = 0; i < NUM_IMAGES; i++)

            {

                 images[i] = getImage(baseURL, "bot" + i + ".gif");

                 mt.addImage(images[i], 0);

            }

            

            try

            {   

                // 等待,直到图像完全加载再继续

                mt.waitForID(0);

            }

            catch(InterruptedException e) { /* 什么也不做*/ }

 

            // 检查错误        

            if((mt.statusID(0, false) & MediaTracker.ERRORED) != 0)

            {             

                 BufferedImage bi;     // 我们的"备用"图像

                 java.util.Random random = new java.util.Random();

                

                 // BufferedImage的边界

                 Rectangle rect = new Rectangle(imageWidth, imageHeight);                 

 

                 // 通过检查宽度是否小于或者等于0来检测""的图像

                 for(int i = 0; i < NUM_IMAGES; i++)

                 {

                      if(images[i].getWidth(this) <= 0)

                      {    

                           // 创建大小为(imageWidth x imageHeight)的缓冲图像

                           bi = new BufferedImage(imageWidth,imageHeight,BufferedImage.TYPE_INT_RGB);

 

                           // 用随机的颜色填充图像                

                           Graphics2D g2d = bi.createGraphics();

                           g2d.setPaint(new Color(random.nextInt()));                    

                           g2d.fill(rect);

 

                           // 设置新的图像

                           images[i] = bi;

                      }

                  }

             }

 

            setBackground(Color.BLACK);

        }   // init

       

        public void start()

        {

            // 启动动画线程

            animation = new Thread(this);

            animation.start();

        }

 

        public void stop()

        {

            animation = null;

        }

 

        public void run()

        {

             Thread t = Thread.currentThread();

            while (t == animation)

             {

                  try

                  {     Thread.sleep(100);

                  }

                  catch(InterruptedException e)

                  {     break;

                  }

 

                  // 动画索引递增,如果需要的话循环

                  if(++firstIndex >= images.length)

                  {     firstIndex = 0;

                  }

 

                  repaint();

              }             

          }   // run

 

          public void paint(Graphics g)

          {

               Graphics2D g2d = (Graphics2D)g;

 

               AffineTransform at = AffineTransform.getTranslateInstance(20, 20);

 

               // 绘制图像数组中的每一帧               

               int currFrame;     // 实际绘制的帧

               for(int i = 0; i < images.length; i++)

               {

                    currFrame = (firstIndex+i)%images.length;

 

                    g2d.setTransform(at);

                    g2d.drawImage(images[currFrame], null, this);

 

                    g2d.setPaint(Color.WHITE);

                    g2d.drawString("" + currFrame, imageWidth/2, imageHeight + 20);

                    at.translate(100, 0);

               }

          }    // paint

     }

 

9.2      使用图像条进行更快的下载

很多时候下载速度决定了游戏在Web上是否可玩,大多数Internet用户在等待网页下载2.5秒就会变得不耐烦。为了防止用户在游戏还没有被完全下载时就单击了Web浏览器的“后退”按钮,就需要来弥补网络慢的缺陷,把图像压缩为图像条就是一种达到这个目的的方法。

事实上,使用图像条比较好有两个原因:首先,它使得文件和项目变得有组织。我们可以简单地在单个文件里生成完整图像,然后把它们复制到一个个狭小的单元中,这使我们可以在一个文件中放置所有的信息,同时尽可能地使目录变小。图像条对于减少下载时间也是很有帮助的,因为请求远程文件需要时间,实现图像条当然会节省时间。请求8个图像文件,每个包含8 帧动画,相对每帧图像请求一次,会节省56个更新请求。图9.3所示为在一个图像条例子中的一部分机器人动画。

9.3图像条中的动画帧

我们已经注意到在图9.3中围绕每一个单独的图像帧都有分隔线,合起来,这些分隔线就组成了一系列可视化分隔单个图像帧的网格。文件中的网格数据可以包含任意数目的行和列。而且,由于是通过Internet传输数据,尽量不要有空的位置或者在图像文件的底部填充其他东西,文件越简短越好。

花一些时间来创建分隔线分帮助我们准确确定在父图像文件的什么位置放置图像帧,本书建议使用与图像明显不同的一种明亮的颜色,这样它们把自己真实的图像分离开来。当然,用户需要找到一种方法来避免在从父图像中解析帧图像时包含分隔线。

下面考虑一下从文件加载图像条所需要的代码。我们将编写的代码应该可以直接放入applet中或者被加到一个工具类中。在这个类中我们会有一个loadImageStrip方法。首先,来看看loadImageStrip方法的头部:

//从给定的文件名中加载一个图像数组

public Image[] loadImageStrip(

       String filename,                          //要加载的文件

       int numImages,                           //要加载的图像数目

       int cellWidth,                              //每一个单元的宽和高

       int cellHeight,

       int cellBorder                              //单元框的宽

){

方法头是自明的,目标是给定文件名和加载的图像数产生一个Image对象数组,还提供了每一个单元的宽和高,以及单元框的宽。如果对图像不使用分隔线,那么只需用0作为cellBorder参数。

方法体的第一部分,自然是加载图像了。我们都知道如何使用MediaTracker加载图像了,下面是加载图像的完整代码:

//作为动画帧的图像数组

Image[] images=new Image[numImages];

 

//为这个组件创建一个新的MediaTracker对象

MediaTracker mt=new MediaTracker(this);

 

//加载主图像条

Image img=getImage(getDocumentBase(),filename);

mt.addImage(img,0);

try{

       //等待主图像加载

       mt.waitForID(0);

}catch(InterruptedException e){

       /*Nothing*/

}

 

//计算列的数目有助于分离每一个单元

int numCols=img.getWidth(this)/cellWidth;

下面是将每个图像单元分离出来的代码。

//为主图像获取ImageProducer

ImageProducer sourceProducer=img.getSource();

 

//加载到单元格!

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

       images[i]=loadCell(sourceProducer,

                                   ((i%numCols)*cellWidth)+cellBorder,

                                   ((i/numCols)*cellHeight)+cellBorder,

                                   cellWidth-cellBorder,

                                   cellHeight-cellBorder);

}

return images;

}

这显然是loadImageStrip方法中最复杂的部分了。首先,必须从源图像中获得一个ImageProducer对象,ImageProducer接口包含从图像中重新构造数据的方法。一旦有了ImageProducer对象,就可以用loadCell方法加载单个的单元格,这个方法用很短的时间就可以定义出来。loadCell的第二个和第三个参数是开始复制图像数据的xy位置,第四个和第五个参数是想提取的单元格的宽和高。为什么需要cellBorder偏转量很清楚了——不想让分隔线出现在最终的Image数组中。此外,上面对每一个单元格是从左上角确定分隔线的。

下面是loadCell方法的代码:

//给定区域,从ImageProcer中加载一个单元

public Image loadCell(ImageProducer ip,int x,int y,int width,int height){

       return createImage(new FilteredImageSouce(ip,new ImageFilter(x,y,width,height)));

}

要创建单元格,只需简单地调用createImage方法并提供另一个ImageProducer对象。这里,ImageProducer是一个FilteredImageSource对象的形式,它以源ImageProducer和一个ImageFilter作为输入,ImageFilter实现了源图像的过滤,所以,只要对图像提供正确的区域,就可以让这些Java类来为你工作。

默然:上面这段代码原书是错误的,我已经对它进行了修改。说实话,这段错误的代码让我着实头疼了一段时间。如果你暂时看不太明白,没关系,继续往后看,下面的LoadImageToolkit抽象类的代码,是我把上面的代码进行合并后的结果,也是我对上面代码的理解<_>!

import java.awt.*;

import java.awt.image.*;

import java.awt.geom.*;

public abstract class LoadImageToolkit{

       //从给定的文件名中加载一个图像数组

public static Image[] loadImageStrip(

       String filename,                          //要加载的文件

       int numImages,                           //要加载的图像数目

       int cellWidth,                              //每一个单元的宽和高

       int cellHeight,

       int cellBorder                              //单元框的宽

){

       //作为动画帧的图像数组

       Image[] images=new Image[numImages];

      

       //为这个组件创建一个新的MediaTracker对象

       MediaTracker mt=new MediaTracker(this);

      

       //加载主图像条

       Image img=getImage(getDocumentBase(),filename);

       mt.addImage(img,0);

       try{

              //等待主图像加载

              mt.waitForID(0);

       }catch(InterruptedException e){

              /*Nothing*/

       }

      

       //计算列的数目有助于分离每一个单元

       int numCols=img.getWidth(this)/cellWidth;

       //为主图像获取ImageProducer

       ImageProducer sourceProducer=img.getSource();

      

       //加载到单元格!

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

              images[i]=loadCell(sourceProducer,

                                          ((i%numCols)*cellWidth)+cellBorder,

                                          ((i/numCols)*cellHeight)+cellBorder,

                                          cellWidth-cellBorder,

                                          cellHeight-cellBorder);

       }

       return images;

}// loadImageStrip

 

//给定区域,从ImageProcer中加载一个单元

private Image loadCell(ImageProducer ip,int x,int y,int width,int height){

       return createImage(new FilteredImageSouce(ip,new CropImageFilter(x,y,width,height)));

}

 

}

上面的方法对于图像如何分布有几个假设。首先,它们假设整个图像只包含单元,也就是说,包含空白空间和填充物的图像不能正确加载。第二个假设是,每一个单元采用同一的尺寸。如果不遵循这些规则,那么可以分开调用loadCell来手工加载它们,或者修改loadImageStrip方法,让它以点的数组和尺寸数据为输入,以便让图像条可以正确读取。

注意:记住,虽然使用.gif文件来做动画,但是使用的文件不包含内在的gif动画。读者可能对内部包含多个动画帧.gif文件比较熟悉;Web浏览器根据内在的时钟设置来使这些文件动起来。这些文件在放入Java applet容器中时不会动起来。因此,为了时间不被浪费于下载包含无用数据的文件,应确保.gif文件只包含一个层。

下面创建一个位图字体类,来演示图像条的使用。位图字体和Font字体中的本地图样字体很相似,只是位图字体以图像文件的形式显示。

有几个原因需要实现自己的字体系统。首先,可以完全控制文字的形状和样式,因此,可以绘制空想的字体而不是使用系统提供的字体。其次,由于不是所有的系统都可以提供所有的字体,用位图字体可以保证所有的用户看到同样的文本。最后,由于绘制的是图像,可以避免标准Font对象相关的转换所造成的计算上的浪费。

C++里实现位图字体机制可能是极其麻烦的,而在Java中却相对简单,因为Java提供了相当可观的类来帮助我们完成工作。

JavaHashtable类使用键值对来储存实体,键和值都以Object的类型在表中储存。因而,如果没有使用包装类来封装,则不能储存原始类型。而且,要正确地插入值并从表中重新获取值用来充当键的类必须实现hashCodeequals方法。

下面的FontMap类用Hashtable对象来封装字体图像的集合,它还提供了在所要求的图像在表中找不到时设置一个替换物的功能,它还允许我们使用putImage方法在表上做一些特殊处理,最后,它给了我们一个drawString方法,把字符串和所对应的图像值匹配起来。具体代码如下:

import java.applet.Applet;

import java.awt.*;

import java.awt.image.*;

import java.awt.geom.*;

class LoadImageToolkit extends Applet{

       //从给定的文件名中加载一个图像数组

public Image[] loadImageStrip(

       String filename,                          //要加载的文件

       int numImages,                           //要加载的图像数目

       int cellWidth,                              //每一个单元的宽和高

       int cellHeight,

       int cellBorder                              //单元框的宽

){

       //作为动画帧的图像数组

       Image[] images=new Image[numImages];

      

       //为这个组件创建一个新的MediaTracker对象

       MediaTracker mt=new MediaTracker(this);

      

       //加载主图像条

       Image img=getImage(getDocumentBase(),filename);

       mt.addImage(img,0);

       try{

              //等待主图像加载

              mt.waitForID(0);

       }catch(InterruptedException e){

              /*Nothing*/

       }

      

       //计算列的数目有助于分离每一个单元

       int numCols=img.getWidth(this)/cellWidth;

       //为主图像获取ImageProducer

       ImageProducer sourceProducer=img.getSource();

      

       //加载到单元格!

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

              images[i]=loadCell(sourceProducer,

                                          ((i%numCols)*cellWidth)+cellBorder,

                                          ((i/numCols)*cellHeight)+cellBorder,

                                          cellWidth-cellBorder,

                                          cellHeight-cellBorder);

       }

       return images;

}// loadImageStrip

 

//给定区域,从ImageProcer中加载一个单元

private Image loadCell(ImageProducer ip,int x,int y,int width,int height){

      

       return createImage(new FilteredImageSource(ip,new CropImageFilter(x,y,width,height)));

}

 

}//FontMap

FontMap的构造函数用一个键的数组和一个值的数组作为参数,把它们放入表中。另一种解决方案是,可以提供一个以Map对象作为输入的构造函数,并直接把这个Map对象放进哈希表中。FontMap的构造函数还为默认的图像提供了null值,因此,这个封装类可以提供键,值和替换图像。结果是FontMap类可以根据一个给定的String对象绘制一系列的位图图像。

注意:选择String类作为表的键只是因为它是我们熟悉的一个类。如果选择Character类作为键也是可行的,因为我们是把单一的字符和字体中的每一个成员进行映射。然而,使用String对象还给了我们把图像和多字符字符串关联起来的能力,对于哪种键类型适合程序,完全取决于我们的选择。

默然:这里所说的字体映射,千万不要理解成为中文字符,如果是中文字符,使用Character类是行不通的,因为原书作者是美国人,所以他所考虑的仅仅只是26个英文字母而已,所以大家一定要注意这些区别。

现在,我们有了一个基本的FontMap类,下面看一个应用它的例子。下面的FontMapTest applet创建了一个只包含数字的FontMap对象,每一个数字的图像和它对应的文字描述关联起来。这个applet-100开始递增一个int计数器,给FontMapdrawString方法发送数字的String描述。由于在这个例子中只实现了数字的字符串,对于理解和尝试应该是很容易的。

import java.applet.*;

import java.awt.*;

import java.awt.image.*;

import java.awt.geom.*;

import java.util.*;

 

public class FontMapTest extends Applet implements Runnable{

       //动画线程

       private Thread animation;

      

       //绘制字符串的FontMap

       private FontMap fontMap;

      

       //要绘制的旧数字

       private int number=-100;

      

       public void init(){

              //FontMap的键将是代表每一个数字的字符串

              Object[] keys=new Object[10];

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

                     keys[i]=String.valueOf(i);

              }

             

              //加载10个图像到图像数组中,每一个大小为20*20像素

              //还有一个1像素的边

              Image[] images=loadImageStrip("fontmap2.gif",10,20,20,1);

             

              //创建FontMap

              fontMap=new FontMap(keys,images);

             

              //创建一个BufferedImage作为FontMap的默认图像

              //由于图像有一个1像素的边,实际图像的大小为19*19像素

              BufferedImage bi=new BufferedImage(19,19,BufferedImage.TYPE_INT_BGR);

              Graphics2D g2d=bi.createGraphics();

              g2d.setPaint(Color.red);

              g2d.fill(new Rectangle(19,19));

              g2d.setPaint(Color.white);

              g2d.draw(new Rectangle(1,1,17,17));

              g2d.draw(new Line2D.Double(1,1,17,17));

              g2d.draw(new Line2D.Double(17,1,1,17));

              fontMap.setDefaultImage(bi);

             

              setBackground(Color.black);

       }//init

      

       //从给定的文件名中加载一个图像数组

       public Image[] loadImageStrip(

                                                 String filename,                          //要加载的文件

                                                 int numImages,                                  //要加载的图像数目

                                                 int cellWidth,                              //每一个单元的宽和高

                                                 int cellHeight,

                                                 int cellBorder                              //单元框的宽

                                                        ){

              //作为动画帧的图像数组

              Image[] images=new Image[numImages];

             

              //为这个组件创建一个新的MediaTracker对象

              MediaTracker mt=new MediaTracker(this);

             

              //加载主图像条

              Image img=getImage(getDocumentBase(),filename);

              mt.addImage(img,0);

              try{

                     //等待主图像加载

                     mt.waitForID(0);

              }catch(InterruptedException e){

                     /*Nothing*/

              }

      

              //计算列的数目有助于分离每一个单元

              int numCols=img.getWidth(this)/cellWidth;

              //为主图像获取ImageProducer

              ImageProducer sourceProducer=img.getSource();

             

              //加载到单元格!

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

                     images[i]=loadCell(sourceProducer,

                                                 ((i%numCols)*cellWidth)+cellBorder,

                                                 ((i/numCols)*cellHeight)+cellBorder,

                                                 cellWidth-cellBorder,

                                                 cellHeight-cellBorder);

              }

              return images;

       }// loadImageStrip

 

       //给定区域,从ImageProcer中加载一个单元

       private Image loadCell(ImageProducer ip,int x,int y,int width,int height){

             

              return createImage(new FilteredImageSource(ip,new CropImageFilter(x,y,width,height)));

       }

      

       public void start(){

              //启动动画线程

              animation=new Thread(this);

              animation.start();

       }

      

       public void stop(){

              animation=null;

       }

      

       public void run(){

              Thread t=Thread.currentThread();

              while(t==animation){

                     try{

                            Thread.sleep(100);

                     }catch(InterruptedException e){

                            break;

                     }

                     repaint();

              }

       }//run

      

       public void paint(Graphics g){

              Graphics2D g2d=(Graphics2D)g;

             

              //(30,30)处绘制数字

              fontMap.drawString(String.valueOf(number++),30,30,g2d);

       }//paint

 

}

这个例子具体介绍了loadImageStrip方法和loadCell方法的具体使用。(默然:你可以象上面这个例子一样的使用这两个方法,也可以把它们独立出来形成一个类,然后在上面这个例子中实例化,然后调用方法进行使用,其实效果是一样的,不过默然是极力推荐后一种使用的,因为这样你可以增强loadImageStriploadCell的可重用性。)

现在,我们来解决负号的问题,并使用美国数字格式来显示数字(从右到左,每3个数字之间用逗号隔开)。首先,可以创建一个私有的NumberFormat对象(这里称为formatter)并像下面这样在init方法中将其初始化:

formatternew java.text.DecimalFormat(“###,###”);

这会创建一个使用指定的String模式的数字格式来以3个一组分开数字,我们还需要像下面这样在FontMap中添加逗号和负号的图像:

fontMap.putImage(“,”,getImage(getDocumentBase(),”comma.gif”);

fontMap.putImage(“-”,getImage(getDocumentBase(),”minus.gif”));

这样从文件中加了另外的两个图像到FontMap中。为了简单起见,这里没有把这些图片加到MediaTracker对象中。

最后,必须调整drawingString方法来传入代表数字的正确格式化过的String

fontMap.drawString(formatter.format(number++),30,30,g2d);

现在,数字字体可以正确绘制负数和整数了。DecimalFormat类还允许把数字格式化为百分数,科学计数甚至货币形式。对于DecimalFormat类的更多内容,可以参考Java文档。

对于位图字体有几点最后的说明:首先,建议对于图像使用等宽字体,这允许更简单地填充每一个图像单元的区域,因为对于任何等宽字体而言,小写的“i”和大写的“W”宽度一致。在这种方式下,可以使用一个常数值来对字符串中的每一个字符增加水平的绘制位置。另外一个节省时间的主意(只考虑时间)可能是先把静态的信息画到一个BufferedImage对象上,然后可以不使用FontMapdrawString方法而绘制单独的图像,这会节省对表所进行的查询。静态信息是那些很少随时间变化的信息,比如文字标签,玩家姓名,故事情节等。可以做很多改进来增强位图字体。可以实现一个完整的字母表(包括数字和标点符号),允许变宽字体以及创建可以跨越多行的字体。可以花时间来考虑位图字体应该具备的能力。

原创粉丝点击