Java 动画制作

来源:互联网 发布:淘宝沉香线香是真的吗, 编辑:程序博客网 时间:2024/05/06 13:48
原文地址:动画制作">Java 动画制作作者:星雨
教学纲要
 
Java世界里最激动人心的时刻终于到来了,在这一章里你将学到在主页里播放动画的技巧,掌握了本章所讲授的这些技巧,再加上你脑子里各种希奇古怪的念头,你就有能力创造出你自己的真正活灵活现、有声有色的活动Web主页来。
学习动画原理了解多线程的工作过程利用多线程制作在动画里加入背景音乐  想像一下,当别人正在浏览你的主页时,突然有个小丑向他做了个鬼脸,跑出一只甲虫在你的主页上载歌载舞,或者不知道从哪里冒出点儿声儿来吓他一跳,那将是多么让人欣喜的事情。这也正是Java的迷人之处,它能在你不注意的时候给你一个惊喜。如今我们到处都能听到或看到所谓活动Web主页(LiveWebPage),是不是很吸引人呢?
孙悟空的筋斗是怎么翻出来的?
  我们知道电影是一张张电影胶片在放映机前滚过,在银幕上投影出动态的画面,动画片是由一组差异很小的图片组合在一起,快速地切换这些图片,你所看到的就是一个在翻跟头的孙悟空。那么计算机上的动画也是这样做出来的吗?基本原理差不多。首先要有一组连续动作的图片,然后只需要快速地用这一组图片顺序刷新显示器上的画面就可以看到一张张静态的画面变成一段可爱的动画片段了。
  所以用Java来实现动画实在是再简单不过了,你肯定马上就想到了前面已经学过的加载和显示图片。既然制作动画最基本的两个步骤你都会了,那还有什么能难得住你呢?现在你已经会用getImage把一个图形文件加载到Java程序中,并用drawImage把加载进来的图片输出到你的Applet窗口中。(如果getImage和drawImage这两个方法你忘了的话,请参阅第十二章。)问题是在这一章中我们将加载的不是单张的图片,而是一组图片,并依次每隔一段时间就显示一张。我们怎么控制它什么时候该换掉旧的显示新的呢?问得好!这就是下面要讲的问题。
  动画效果的好坏不仅仅取决于画面是否漂亮,而且还取决于动画中动作的平滑程度。我们当然不希望看到自己主页里的动画动起来像僵硬的木偶,要让你的动画看上去动作连贯平滑就需要控制切换图片的速度。可想而知,相同时间内放映的图片张数越多,动画中人物的动作看上去就越平滑。
  那么Java到底靠什么来决定何时显示图片、何时更换图片呢?当然是时钟。每台计算机内部都有一个很精确的时钟,计算机依赖它才能有条不紊地进行工作。我们可以对显示的每一帧图片计时,一到规定时间就把它撤换掉,定时地顺序地替换组成动画的每一帧画面,画不就动起来了吗?那我们用什么来计时呢?线程!这是最好的解决办法。支持多线程也是Java的一大特征。
 
什么是线程?
  "线程”这个词似乎听说过。其实它的确也没有什么神秘的,线程就是执行中的一段程序。举个生活中的例子:生活中很多事情都可以看作是线程,比如吃饭、睡觉、看书、工作……有时候为了提高效率,你可能会同时做几件事,每一件事叫做一个线程,那么你就工作在多线程状态下了。假设你正在准备晚餐,微波炉里正烤着面包,咖啡壶里正煮着咖啡,你也许正在煤气灶边忙着煎鸡蛋。你得不时地关照着微波炉和咖啡壶,还得注意不要把蛋煎糊了,如果电话铃响了,你还得把电话夹在腋窝下接电话。那我要说你简直是个效率专家,你正在完成多线程任务。
  其实Java就是一种能让CPU忙得团团转的语言,因为它支持多线程。它可以让CPU同时从从网络上读取数据、等待用户的输入、在屏幕上显示动画,并且向打印机发出打印指令。你看Java通过它的多线程机制大大地提高了CPU的工作效率。
  既然多线程是这么一个好东西,我们何不利用它呢。听起来似乎挺复杂,其实使用多线程的过程很简单。只要遵循固定的步骤,一步步地在程序里加入我们想做的事情就可以了。  1.用你所编的Applet去实现Runnable接口。
  2.在方法start中产生一个新的工作进程。
  3.在方法stop中编写结束线程要处理的事情。
  4.加一个run方法,在这个方法中写你想让新产生的那个线程做的事情,其实就是给新线程分配任务。
  哦!我又糊涂了,又是线程,又是方法,还有什么接口。我们下面就一步一步地讲。
第一步:让你的Applet实现Runnable接口。
  这并不难理解,我们可以把每个程序看作一个线程,它从开始到结束只能干一件事。如果你想让它同时干两件事,就必须在原来那个程序里加上Runnable接口以便让连贯线程能够同时工作。举个例子来说,一个电源插座上只能插一个电器,如果你正在看电视,就不能使用录像机,可是你想把好看的节目录下来,非用录像机不可,怎么办呢?聪明的你马上就想到加一个插线板问题就解决了。在这里Runnable接口就是线程的插线板,只要你的Applet实现了Runnable接口,它就可以成为多线程程序了,而且实现Runnable接口也的确和加一个插线板一样简单,你只需要这样写:  publicclasscartoonextendsjava.applet.Applet implementsRunnable{……}
这句话的前面一部分,我们都已经很熟悉了,cartoon是你的Applet的名字,implementsRunnable就是咱们的插线板。
第二步:在Start方法里产生一个新的线程。
  在Java里线程也被当作一个类。类Thread封装了所有有关线程的控制,用来控制线程的运行、睡眠、挂起和中止。Thread类是唯一可以用来控制线程的手段。start的实现很简单。
  public void start()
  {
     if(thread1==null)
    { thread1=new Thread(this);
     thread1.start();
    }
  }
方法start里产生了一个新的进程,当start()执行完以后,程序里就有了两个线程在同时工作,一个是原来的程序,另一个则是由start()产生的,start()中产生的这个线程完成下面将谈到的方法run中指定的任务。
第三步:在方法stop()里把start()产生的一个线程关掉,中止线程的工作,并释放线程,用stop中止的线程将不能再启动,也就是说这个线程已经死了,所以应该释放它。
Stop方法应该这样写:
  public void stop()
  {
    thread1.stop();
   thread1=null;  
  }
第四步:在run()方法里给新产生的进程分配任务。
  run是线程的核心,在这个方法里,我们将写入新线程工作的程序代码,告诉这位新来的线程先生该做什么。事实上你可以在方法run里做任何你想做的事情,原来的那个线程并不会因为它而受到什么影响,因为它们是两个完全独立的线程,互不干扰。这里还要提一句,除了stop可以中止线程的工作外,run中的程序代码一旦执行结束,线程也会自动中止。
  好啦!讲了这么多,不知道你清楚了没有?如果你还是觉得云山雾罩,没有关系,只要你照着这四个步骤一步步来,你仍然能很容易写出一个像模像样的多线程程序来。下面就让我们来试试吧。
翻筋斗的小企鹅
  这一章不是讲动画吗?怎么又讲这么一大堆线程的东西?这是因为要编出一个好的具有动画效果的Java程序,就必须利用线程。所以前面我们是在为这一节打基础,现在我们已经知道怎样启动和中止一个线程,怎样在方法run中指定线程工作。那么就把它用到咱们的动画程序里来吧!
程序18.1//animate.java
importjava.awt.*;
public class animate extends java.applet.Applet implementsRunnable //实现Runnable接口
{
  Image frame[];  //说明一个Image数组,准备放动画图片
  Thread thd;   //说明一个Thread(线程)对象
  int num;    //设置计算帧数的计数器
  int pause;      //设置每帧显示的时间
  public void init()
  {
    String fps;      //设置每秒切换图片的张数
    frame=newImage[17];  //构造Image数组,共包含17个元素
    thd=null;        //初始化线程
    num=0;      //计数器置0
    for(inti=0;i<frame.length;i++)  //加载图片
    frame[i]=getImage(getCodeBase(),"images/T"+i+".gif");
    fps=getParameter("speed");//从HTML文件读入参数speed,确定放映速度
    if(fps==null)  //如果HTML中未指定参数,则用省缺的放映速度
    fps="10";      //为每秒10帧
    pause=1000/Integer.parseInt(fps);  //计算每帧图片显示的时间
  }
  publicvoidstart()
  {
    if(thd==null)
    {
      thd=new Thread(this); //在start()方法里产生一个新线程,名为thd
      thd.start();    //启动这个新线程
    }
  }
  publicvoidstop()
  {
    if(thd!=null)
    {
      thd.stop();   //在stop()方法里中止线程thd
      thd=null;   //释放线程    }
  }
  public void run()
  {
   while(true)
    {
    try{Thread.sleep(pause);
    } //让线程睡眠每一帧图片的显示的时间
    catch(Interrupted Exceptione){}
    repaint();      //重画窗口
    num=(num+1)%frame.length;  //计数器增1
    }
  }
  public void paint(Graphics g)
  {
    g.drawImage(frame[num],0,0,this);  //显示计数器指定的那帧图片
  }
  public void update(Graphics g)
  {
    paint(g);
  }
}
该程序的HTML文件如下所示:
animate
<APPLETCODE="animate.class"WIDTH=130 HEIGHT=80>
 
先不忙看程序运行结果,我敢打赌这个程序里一定有好几个地方你还不太理解,那就让我们先来看看这几条令人不解的语句:
1.fps=getParameter("speed");
  这条语句的意思是从HTML文件接受一个名叫speed的参数。很多时候我们希望自己控制程序的运行情况,在这个程序里我们希望能够用一种比较容易的方法来改变动画的放映速度,而不是到程序源代码里直接修改程序里的某条语句,然后重新编译程序。的确完成这样的修改是工程浩大的,也不是一般人所能胜任的。于是我们就想到了利用参数。只要给定了参数,程序执行时就会按照给定的参数去执行了。那么Java是从那里得到参数的呢?我们都知道,Java程序是通过编写HomePage的HTML文件加载到WWW浏览器里执行的,自然而然,我们就想到了让Java程序到包含了它的那个HTML文件里去读取参数。为了使Java能够从HTML文件里读到参数,必须满足两个条件:在HTML文件里,必须用标签,指定参数的名称和参数的值。在Java源程序中,必须用方法getParameter来取得参数值。这回你该明白为什么HTML文件里会多出这样一句话来
这句话正是用来告诉Java程序“放映速度为每秒5帧”
2.pause=1000/integer.parseint(fps);
  根据我们的定义,在这条语句中的pause应该是每帧图片显示的时间,在Java里时间都是以毫秒为单位的,所以这里1000毫秒也就是1秒,不难理解
每张图片在屏幕上停留的时间=1秒/1秒内放映的图片张数
  根据这个算式,理解这条语句就很容易了,可是为什么还有Integer.parseInt(fps)呢?要知道HTML里的变量都是字符型的,即使是数字5,它也仅仅是一个写成5这个样子的一个字符,所以fps一开始就被说明成字符型,为的就是在这里赋值时不至于类型不匹配。那么既然fps只是一个字符,我们怎么能把它放到数学算式里来进行计算呢?当然要进行类型转换,把fps这个字符串类型的值转换成能进行数学运算的整数。Integer.parseInt(fps)就是完成这一功能的。
3.try{Thread.sleep(pause);}
 catch(Interrupted Exceptione){}
  这是什么东西?这么复杂。哦!请你千万不要看它复杂就轻易放弃。try和catch只不过是一个防止意外的保险栓,把你要进行保险的东西放在try后面的{}里,然后把一旦发生意外要进行的处理写在catch后面的{}里。这和填一张保险单一样简单,在try里填你投保的东西,在catch里填入发生意外时你希望保险公司为你做的事。
  好了,关于try和catch我们在后面的章节里还要详细地介绍。在这个程序里我们把Thread.sleep(pause);这条语句投了保,不过没有对它进行什么意外处理,因为catch里什么也没做。Thread.sleep(pause);这条语句又是什么意思呢?再容易理解不过了,它就是“让线程睡上一会儿”,睡多久呢?每帧图片显示时间多久,它就睡多久。pause不就决定了每帧图片显示的时间吗?
4.num=(num+1)%frame.length;
  这是图片帧数的计数器,每放完一帧图片,num就加1。%我们在第四章里已经学过,它表示取模运算,再说简单一些,就是计算余数。在这里我们用它来干什么呢?哦!对了,还没有说frame.length呢,frame在前面定义过,它是一个Image数组,包含了动画的一组图片,frame.length就是这个数组的长度,也就是frame所包含的图片张数。明白了每一个参数的意义以后,再来看看算式吧,这个算式的意思就是:显示图片的计数值对图片的总张数取模。为什么要这样做呢?如果不对图片的总张数取模又会发生什么呢?设想一下,当图片显示到了最后一张,再没有图片可供显示了,你想让动画再从头放一遍,也就是从第一张开始再重新来一遍。取模就是完成这个功能的,它让显示的图片可以再次从头开始。在这个程序里我们一共有17张图片,所以frame.length等于17,当计数器num计到16时,已经没有图片可以显示了,(注意数组下标都以0开始)应该显示第0张,所以num=(16+1)%17=0正好符合要求。
  好啦!现在你对程序的理解应该更清楚了。看看程序的运行结果吧!程序运行起来会有一只顽皮的小企鹅在窗口里翻筋斗。当然我们不可能在这里看到那只会动的小企鹅,不过小企鹅翻筋斗的每一个动作都在这里了,我们的动画就是快速地切换这些图片,让它动起来。
动画的每一帧图片
不再闪烁的动画
  看了上面的程序,你可能会觉得有些不尽如人意,小企鹅翻起筋斗来总是一闪一闪地。虽然动起来了,开始一闪一闪的动画实在不能令人满意。为什么我们的小动画会有这种讨厌的闪烁呢?有没有办法让它不闪烁呢?
  不知道你还记不记得前一章讲过的repaint方法的执行过程,当调用repaint时,系统首先调用的是update,省缺的update是用背景色把整个窗口重刷一次,然后再调用paint方法在窗口里画出要显示的内容。为什么会发生画面闪烁?现在就应该比较清楚了,原因就在于我们每次调用repaint重画窗口时,都相当于先调用update清除窗口里的所有画面,然后再调用paint方法在一张干净的纸上显示下一幅画面。这样重复的清除画面,又重绘画面就造成了动画的闪烁。
  其实我们在换图片的时候真的需要清除所有画面再重画它吗?完全没有必要。我们只需要用下一张图片去覆盖上一张图片就可以了。所以在update里,我们不再需要清除画面,而是直接调用paint重绘画面就可以了。你也许要奇怪了,我们的程序里并没有什么update呀?是的,我们上面的程序里的确没有update这个方法,可是正如我们前面提过的,update是在调用repaint的时候,由repaint去自动调用的。如果我们不在程序里重写一个新的update,它就会自己去调用系统省缺的那个,用背景色先清除整个窗口。如果你不想要系统省缺的那个笨笨的update,你只需要在程序里写上自己的update就行了。
  在我们这个程序里只需用下一张图片去覆盖上一张图片,而无需先清除窗口,所以我们的update就可以省去清除窗口这一步,直接调用paint重绘窗口。那么这就在程序里加上我们自己的update方法吧!
  publicvoidupdate(Graphicsg){paint(g);}
  好啦!,闪烁的问题就解决了。不信你试试,把上面一句话加到程序的最后一个}之前,重新编译一次,看看运行结果是不是好多了呢?
连续的筋斗
  现在我们的小企鹅已经能把筋斗翻得相当漂亮了,可是如果你是个完美主义者的话,可能还是感到有些美中不足。这只小企鹅怎么只会在原地翻筋斗?能让它连续地翻几个筋斗吗?当然可以。这就是动画常见的另一种效果,在固定的背景上,一边放映主角的动作,一边同时改变主角的位置。听起来似乎挺复杂的,不过实现起来并没有想像的那么难。在这种效果的动画里,除了前面讲过的技巧外,我们所要做的就是加上重画背景和改变主角位置这两个动作的处理。
  制作这样的动画,除了需要一组连续动作的图片外,还应该准备一张背景图让我们的动画人物以此为背景来进行表演。我们想让小企鹅连续地翻上好几个筋斗,就需要在显示图片的同时改变显示的位置,连续不断地在不同位置画小企鹅翻筋斗的图片是不是就可以让它连续地翻筋斗了呢?还不行。每次显示完上一帧图片后,我们还必须用背景图去重画一次背景,然后再按正确的位置显示下一次图片。如果不这样做,可以想像,背景图上就会留下每个动作的残迹。根据前面的分析,我们需要做的修改实际就在run和paint这两个方法里。在run里修改图片显示的位置,在paint里加入重画背景图的动作。那么就来看看修改过的程序是什么样子。
程序18.2//tumble.java
importjava.awt.*;
public class tumble extends java.applet.Applet implementsRunnable
{
  Image frame[];  //说明一个用来存放动画图片的Image数组
  Thread thd;    //说明一个线程对象
  int num;
  int pause;
  Image backgnd;  //说明背景图片
  int x,x_pos;    //说明显示图片的x坐标
public void init()
{
  int i;
  String fps;
  frame=new Image[17];  //初始化Image数组
  thd=null;        //初始化线程
  num=0;          //初始化计数器
  for(i=0;i<frame.length;i++)  //加载动画图片  
   frame[i]=getImage(getCodeBase(),"images/T"+i+".gif");
  backgnd=getImage(getCodeBase(),"images/backgnd.gif"); //加载背景图片
  fps=getParameter("speed");  //设置播放速度
  if(fps==null)
    fps="10";
  pause=1000/Integer.parseInt(fps);  //计算每帧图片显示的时间
}
public void start()
{
  if(thd==null)
  {
    thd=new Thread(this);  //产生一个新线程
    thd.start();  //启动这个新线程
  }
}
public void stop()
{
  if(thd!=null)
  {
    thd.stop();  //终止线程
    thd=null;  //释放线程
  }
}
public void run()
{
  while(true)
  {
    try{Thread.sleep(pause);  //使线程睡眠
  }
    catch(Interrupted Exceptione){};
    repaint();  //重画窗口
    num=(num+1)%frame.length;  //为显示的图片计数
    x=((x+130+10)%(size().width+130+70))-130;//计算下一帧图片显示的位置
    x_pos=size().width-x;  //调整图片显示的位置
  }
}
public void paint(Graphics g)
{
  g.drawImage(backgnd,0,0,this);  //显示背景图
  g.drawImage(frame[num],x_pos,0,this);  //显示动画图片
  }
  public void update(Graphics g)
  {
    paint(g);
  }
}
这个程序的HTML文件如下所示:tumble

<APPLETCODE="tumble.class"WIDTH=500HEIGHT=80>
<paramname="speed"value="5">
  看看程序运行结果,一只会连续翻筋斗的小企鹅高高兴兴地在你的主页上玩开了。
  让它自由自在地玩吧,我们再来看看程序。程序18.2比程序18.1增加了几句话:
1.方法run中的
  x=((x+130+10)%(size().width+130+70))-130;
  一看就知道这是用来改变图片显示位置的,在这个长算式中10是每次小企鹅前进的步长,size().width指的是运行这个Applet窗口的宽度,此外我们还考虑了每一帧图片的宽度和窗口显示预留量,为的是让小企鹅从画面右侧切入,以免造成突然出现的那种突兀的感觉。
2.方法paint中的
  g.drawImage(backgnd,0,0,this);
  大家对这句话的含义应该再清楚不过了,它在这里的作用就是重画背景图。
0 0