现在流行的游戏似乎都是用c或c++来开发的。在java平台上几乎没有很大型及可玩的流行游戏。由于java是个新生语言,他的许多特性还有待大家的发掘,但是我们不能否认java在游戏编程方面的强大性。本文将带领大家一步一步学习编写java游戏。最终打造属于自己的java游戏。
在开始之前我们得确认你已经安装了java jdk,并已经安装了浏览器软件如ie。本章是以internet为开发对象,一步一步教大家认识java的thread、applets….以及游戏编程要注意的一些方方面面。并在每一小部分附上了相应的源代码以供大家参考,最后我们还会对我们的游戏程序进行指导性的提示。在文章中我们还穿插了很多建设性的问题,让读者参与到我们的开发中来。但是由于本章指在带领大家进入这个门槛,大部分知识并不会很详细说明,有兴趣的读者可查阅相关的资料补充。在开始之前我们还得确认你已经知道类,继承和java语言的一些基本属性了。
基本applet
applets是一种web浏览器上的小程序,由于applet对系统而言绝对安全,所以它做的事比aaplication有限,但是对于客户端的程序,applets仍然是个很强大的工具。为了浏览和运行方便,我们就以applet为开发对象。
开发applets程序,我们得继承applet类,并覆写必要的函数,下面几个函数控制了web页面上的applet生成与执行。
函数 | 作用 |
init() | 这个函数会被自动调用,执行applet的初始化动作―包括组件在版面上的分配,你一定得覆写它 |
start() | 每当浏览器显示applet内容时,都会调用它,让applet开启其正规工作(尤其是那些被stop()关闭的工作),调用init()之后也会调用这个函数 |
stop() | 每当浏览器不显示内容时,都会调用它。让applet关闭某些耗资源的工作,调用destory()之后也会调用这个函数 |
destroy() | 浏览器将applet自网页移除之际,便会调用它,以执行”applet不再被使用”应该做的最后释放资源等动作 |
paint() | 让你在applet界面上进行相应的绘画动作,每次刷新时都会重画 |
所有的applet文件源文件名和java应用程序一样都是.java为扩展名,编译后的执行文件扩展名为.class,由于在applet中已经没有了main()函数,它是和html自动集成,所以我们要执行applet,要在html源文件中放入一特定的标签(tag),才能告诉网页如何装载并执行这个applet,这里有一点要注意,我们执行的网页必须能执行java程序。
普通html 源码格式
<html> <applet code="helloworld.class" width=300 height=500> </applet> </html |
<applet code ="helloworld.class" width=300 height=500>这行即applet的执行处。
applet 执行文档为 ="helloworld.class" 告诉网页”applet ”扩展文件为helloworld.class
width 和 height 告诉浏览器这个显示的applet的大小
有关标签(tag)的说明,大家可在网上找到很多相关的说明文档。
线程
由于apllet,java应用程序的执行都和线程有关。我们来大概了解一下线程的概念。
线程也称为轻型进程 (lwp)。每个线程只能在单个进程的作用域内活动、协作和数据交换,并且在计算资源方面非常廉价。线程需要操作系统的支持,因此不是所有的机器都提供线程。java 编程语言,作为相当新的一种语言,已将线程支持与语言本身合为一体,这样就对线程提供了强健的支持。
thread 类是一个具体的类,即不是抽象类,该类封装了线程的行为。要创建一个线程,程序员必须创建一个从 thread 类导出的新类。程序员必须覆盖 thread 的 run() 函数来完成有用的工作。用户并不直接调用此函数;而是必须调用 thread 的 start() 函数,该函数再调用 run()。
但是使用thread类实现线程,增加了程序的类层次,所以一般程序员都由另一个java线程接口runnable接口来实现,runnable接口只有一个函数run(),此函数必须由实现了此接口的类实现。
线程中有几个重要的方法是我们得了解:
thread.start(): 启动一个线程
thread.stop(): 停止一个线程
thread.sleep(time in milliseconds): 暂停线程在一个等待时间内。
动画技术
自由降落动画
了解了一些基本概念后,下面我们就开始我们的实质性的工作。我们设计一个球从屏幕顶上降落到屏幕下面,程序实现比较简单,但是这是游戏动画中不可少的一部分。在开始之前我们来看看我们的applet开始语句。
import java.awt.*; public class ball extends applet implements runnable { public void init() { } public void start() { } public void stop() { } public void destroy() { } public void run () { } public void paint (graphics g) { } } |
在开始函数中我们要新建程序的主线程,并启动这个线程。一旦做好这些准备工作以后,当applet第一次被显示时,就会创建线程对象的一个实例,并把this对象作为建构方法的参数,之后就可以启动动画了
public void start () // 定义一个新的线程 } |
现在我们来看看线程的run方法,它在循环while(),中每隔20毫秒重画动画场景。sleep这个方法很重要,如果在run循环中没有这部分,圆的重画动作将执行得很快,其他方法将得不到有效执行,也即我们在屏幕上将看不到球的显示。
public void run () // // 重画applet画面 try // 暂停线程20毫秒 } } } } |
我们接着读下去之前,有几个问题需要回答。你也许会问,浏览器调用java小程序的start和stop方法吗? run 方法是如何被调用的? 情况是这样的,当浏览器启动了一个内部线程时,就相应地启动了applet 的运行。当网页显示时,就启动了applet的start 方法。start方法创建一个线程对象,并把applet自身传送给线程,以实现run方法。
此时,两个线程在运行:由浏览器启动的初始线程,以及处理动画的线程。快速查看applet的start方法,可以知道它创建了线程,并启动了它。类似地,当网页被隐藏后,applet的stop方法就调用了线程的stop方法。
注意:在applets和threads中的 start/stop子程序
在applet 和thread 两个类中都有start和stop方法,但它们的功能不同。一旦applet 显示时,就调用applet的start方法,一旦applet 隐藏时,就调用applet的stop 方法。相反,线程的start方法将调用run方法,线程的stop方法将停止正在执行的线程。
public void paint(graphics g);
paint() 方法所传入的参数―― java.awt.graphics 对象将是一个经裁剪的相关显示区的图像代表(而不会是整个显示区)。我们对圆球图形元素的绘制就是在通过重写 paint()方法,在其中对传入的graphics 对象g进行操作完成的。
当我们应用程序的逻辑要对系统界面进行更新时,调用 repaint() 方法来通知awt线程进行刷新操作。repaint() 方法实际会让 awt线程去调用另外一个方法,update。update方法在默认情况下会做两件事,一是清除当前区域内容,二是调用其 paint()方法完成实际绘制工作。paint、repaint、update 三个方法关系如图所示:
但是如何让我们的圆运动呢?这里我们利用函数graphics 类的filloval函数来设置了圆的起始位置x,y。现在我们只要在线程run方法中每单位时间增大y的值,线程将在每一个单位时间内重画圆的位置。每单位时间y值越大,下降的速度就会越快。在屏幕上我们就将看到这个圆球做自由降落运动。 如下代码所示:
while (true) { // 设置动画移动速度 y +=1; public void paint (graphics g) //设置球的颜色 // 从x,y位置处画一个实心的圆 } |
在这之前我们需要在开始处设置一些变量,定义好x,y的默认位置值。r 在此处是我们画的圆的半径大小。
int x = 100; int y = 20; int r = 10; |
我们的自由降落的动画就完了。是不是很简单,如果还有地方不明白,大家可在此处下载完整的代码及应用程序。看看真实的演示效果和代码。下面每一部分我们也将在最后附上相应的源代码及应用程序下载。如果大家有兴趣,可改变y的值,及x的值,你会得到不同的下降效果。
大家可能注意到了上面例子中的我们下降的圆看起来不是很清晰,带着很严重的闪烁。这种现象在写游戏程序中是普遍存在的现象。这是由于我们的repaint()函数导致的结果,由于它在调用paint()函数前会自动清除屏幕,所以在一个毫秒内我们会看到一个空白的屏幕,在快速的变换操作中就出现了闪烁现象。
解决这种闪烁现象有几种方法,下面是两种方法的列举说明,其他的方式大家可以自己尝试。
第一种:我们始终不清除屏幕显示,但是这个方法会带来个附作用,我们下降的圆不在是一个圆了,而是一条直线,因为它的下降过程中没有了断点,保留了所有的圆球的影象。我们只要在ball.java内加上如下代码update(graphics g) {paint(g)},你就会看到一条很长的线拉出来。有兴趣的朋友可以试试。
第二种:使用双缓冲机制(double buffering)
现在大部分的游戏都是采用双缓冲机制来解决屏幕的闪烁现象,我们就以此为例来进行说明,有关缓冲区及相关缓冲机制的概念,大家可参考附录的缓冲说明。
而我们的程序中简单的说就是在显示我们想要的图画之前,把所有的图画先在后台绘制好并存放到相应的图像变量中去。当需要显示时直接复制到前台屏幕就可以了。
具体实现:
1.首先我们用createimage方法新建一后台图像类变量
2.然后使用getgraphics()方法得到当前图像的图形关联
3.在后台处理所有相关的处理,如清除屏幕,后台绘画等等
当完成所有的后台工作后,复制已经绘制好的图像到前台,并覆盖前台的存在图像。这样我们的所有操作都是在后台前行,在屏幕显示新的图像前,这些内容都已经存在于后台了。所以你也将在任何时刻都看不到空屏幕的存在。也即代表闪烁消除了。
下面我们来看看相关的代码说明:
在开始之前我们得先在程序的开始部分声明两个实例变量用来存储后台图画。如下:
private image bgimage; private graphics bg; |
然后我们利用update()方法来实现双缓冲机制。
update()方法要实现下面三个步骤:
1.清除屏幕上的组件
2.设置相关联组件的前景色
3.调用paint方法重画屏幕
public void update (graphics g) // 初始化buffer bgimage = createimage (this.getsize().width, this.getsize().height); } // 后台清屏,即设置圆球组件和后台一样的颜色,大小 // 绘制相应的元素组件 // 在屏幕上重画已经绘制好的圆 } |
此处g 为屏幕图形,bg为g的后台关联。而bgimage包含了bg图形。请于此处来看看我们的源代码例子及演示效果。
改变运动方向
我们已经解决了动画的两个很重要的问题,移动动画和闪烁消除。但是我们很快会发现一个问题,球从屏幕顶上落下来后,就不见了。这可不是我们所需要的。我们要的是一个生动的画面。如何让我们的球不穿过屏幕而始终在屏幕上活动呢?在开始之前,我建议大家自己想办法解决,如果你能自己处理好了。你的水平将会有一个很大的提高。如果没有想出好办法,没关系,下面我们将很详细的说明球的方向改变的技术。
不知道大家注意了没有,在上面我们说到球的移动时,我们是通过增加y的值,让线程重画新的圆位置和图形。如果改变y的值的大小球的下降速度也会改变。不错,这就是我们的解决方法 ,我们只要用一个变量来存储这个速度的大小而不用固定的值。在线程执行也即run方法处我们用代码改变速度的方向,球的方向也会改变。即设置这个变量”speed”为”-1”。当然在设置值前我们要进行判断,你是想让球穿过屏幕从别一边开始显示,还是来回反弹呢!如果想来回反弹,我们只要不让球的半径值超过applet屏幕显示区域就可以了。此处我们用r/2来表示球的半径。
//反弹下落球 // 改变方向 } // 改变方向 } |
至于如何让球穿过从屏幕顶上重新下降,我们在此没有说明,也不会说明了。留给大家自己去想想,已经很简单了。在下面我们附上了两种方式的源代码和执行文件。如果大家运行程序,大家可能会发现,我们的球的大小和速度有一些改变。这里是为了更好的反应演示效果。
多媒体使用多媒体声音
多媒体功能在游戏中是必不少的一部分,优美的音乐,漂亮的界面往往是一个成功游戏必需具备的条件。
在开始之前我们先了解一下主要的小型声音文件类型:
au - (扩展名为au或snd)适用于短的声音文件,为solaris和下一代机器的通用文件格式,也是java平台的标准的音频格式。au类型文件使用的三种典型音频格式为: 8位μ-law类型(通常采样频率为8khz), 8位线性类型,以及16位线性类型。
wav - (扩展名为wav)由 microsoft和 ibm共同开发,对wav的支持已经被加进windows 95并且被延伸到windows 98. wav文件能存储各种格式包括μ-law,a-law和 pcm (线性)数据。他们几乎能被所有支持声音的windows应用程序播放。
aiff - (扩展名为aif或ief)音频互换文件格式是为macintosh计算机和silicon graphics (sgi)计算机所共用的标准音频文件格式。aiff和 aiff-c几乎是相同的,除了后者支持例如μ-law和 ima adpcm类型的压缩。
midi - (扩展名为mid)乐器数字接口midi是为音乐制造业所认可的标准,主要用于控制诸如合成器和声卡之类的设备。
在jdk1.0上,java只支持*.au格式的声音文件,但是java2的api以及声音包提供了很强大的对声音技术的支持。而此部分为了让大家快速掌握游戏编程的基本知识,我们仅使用了audioclip接口类来实现播放"*.wav"。如果大家有兴趣可参考sun java网站的声音sapmle,上面提供了完备的实例和教程说明。
使用audioclip接口比较简单,我们只要实例对象,加载声音文件后,再在任何地方播放即可。恢复和播放声音最简单的方法是通过applet类的play()方法。
audioclip接口
1.播放 play
2.循环 loop
3.停止 stop
启动和停止声音文件,或循环播放,你必须用 applet的 getaudioclip方法把它装载进入 audioclip对象,getaudioclip方法要用一个或两个参数,当作播放的指示。第一个或唯一的一个参数是 url参数,用来指示声音文件的位置,第二参数是文件夹路径指针。
下列代码行举例说明加载声音文件进入剪贴对象: 下面的"gun.wav"是指当前目录下的声音文件。我们也可用*.au格式的文件代替。
audioclip co = getaudioclip(getcodebase(), "gun.wav"); |
getaudioclip()方法仅仅能被applet内调用。随着java2的引入,应用程序也能用applet类的newaudioclip方法装入声音文件。前一例子可以改写如下以用于java应用程序:
audioclip co = newaudioclip(“gun.wav”) |
我们现在可在任何地方使用方法play()播放我们的声音了。play()一旦被调用立刻开始恢复和播放声音。但这有一点要注意:如果声音文件不能被查找,将不会有出错信息,仅仅是沉默。
源代码及应用程序请于此处下载.
图片处理技术
图片的处理和声音的处理在一样简单。设置图片变量,得到图形,最后绘制图形。我们就直接从代码来分析。在此我们绘制一幅applet的背景图。开始绘制前,我们先要声明图形变量,用来存放图形文件。
image backimage; // 加载图片文件 |
下面在我们的paint()方法中利用函数drawimage绘制我们图形。
g.drawimage (backimage, 0, 0, this); |
drawimage参数中的blackimage即我们得到的图形,而后面的0,0分别代表图形的x坐标和y坐标.this:为图形代表的类,这里指的即picture类。在这里建议大家使用*.gif格式的图片文件。因为如果是internet网上,文件的大小也决定了你的applet加载时的快慢,没有人很愿意等很长时间来玩你的游戏,即使你的游戏比较出色。源代码及演示程序下载.
大家在玩游戏时是不是见过人物图像行走?动物来回跑动的动画?这些都是基于图形技术来实现的。我们只要把上面的代码稍微修改,用数组变量来存储我们得到的图形文件组,再利用drawimage()方法播放出来就可实现动画图片的播放.
image[] backimage; // 加载图片文件 for (int i=4,i<backimage.length,i++) { } |
大家可参考jdk包中的animation例子,它就是一个很好的播放一组图片文件的例子。
鼠标监听技术
玩游戏时,不管是小型的扑克牌和大型的rpg游戏,都要参与者溶入到游戏的角色当中。不错,交互,游戏有了交互的功能才可以说是一个完整的游戏。即使是编程游戏如机器人足球,robocode都要程序员参与编写代码,观察比赛。有两种主流方法可实现游戏的交互:鼠标和键盘。当然还包括手操杆等,但现在大部分pc机上使用的还是鼠标和键盘。我们就以这两项为基础来说明游戏中事件的响应过程。
要判断相应的鼠标所进行的动作:是点击,还是移动。我们必须对我们鼠标进行监听。要监听鼠标事件就必须调用这些接口之一,或扩展一个鼠标适配器(mouse adapters) 类. awt 提供了两种监听接口(listener interface): java.awt.event.mouselistener 和 java.awt.event.mousemotionlistener.
现在我设计一个鼠标事件,当点击applet屏幕时,下降的球向反方向运动。以实现了对游戏的简单控制。
mouselistener一共有5个方法,主要用来实现鼠标的点击事件。这里要注意一点:由于mouselistener是接口我们要在实现的类中重载它的所有方法.
mouse点击事件
? mousepressed() 当用户按下鼠标按钮时发生.
? mousereleased() 当用户松开鼠标按钮时发生.
? mouseclicked() 当用户按下并松开鼠标按钮时发生. 用户在选择或双击图标的时候通常会点击鼠标按钮. 用户如果在松开鼠标之前移动鼠标,点击不会导致鼠标相应事件出现.
? 因为点击鼠标是按下鼠标和松开鼠标的结合, 在事件分配给 mouseclicked() 方法之前, mousepressed() 和 mousereleased() 方法已同时被调用.
鼠标状态处理:
mouseentered() 当鼠标离开当前组件并进入你所监听的组件时激活事件.
mouseexited() 当鼠标离开你所监听的组件时发生.
mouse 移动事件
鼠标移动主要通过接口mousemotionlistener来实现:
mousedragged() 当用户按下鼠标按钮并在松开之前进行移动时发生.在mousedragged() 后松开鼠标不会导致mouseclicked().
mousemoved() 当鼠标在组件上移动而 不时拖动时发生.
依据我们的游戏设计,我们在这要使用到mouselistener接口。实现接口后。我们要在init()函数加入监听器addmouselisener(),来监听对applet的响应事件。
知道了鼠标事件的处理,我们再来回顾一下上面提到的球反弹设计,现在我们要如何处理了球的控制呢?让我们想一想,不错,可能你已经发现了,我们照样可通过改变speed方向来实现回弹控制操作。在mousepressed(){}事件中加入下面的代码,我们的回弹控制就设计完成。
speed = -4
记得在释放applet资源时,我们要释放mouselistener资源。在destory()中加入
removemouselistener(this);
可能有些朋友会使用mousedown()方法,mousedown()在此我建议大家不要再使用这个方法了,它已经是被淘汰的产品。是为了兼容jdk1.0而带到jdk1.4中来的。
键盘监听技术知道了鼠标的操作处理,键盘的操作处理就很简单了。我们只要实现keylistener接口,并在相应的事件中加入我们要实现的代码。
keypressed: 当按键时发生
keyreleased:当翻译键时发生
keytyped:当打击键时发生
由于在后面我们设计的游戏中我们不会使用到键盘操作,键盘事件处理我们就交给大家自己去实现。
现在我们来回顾一下我们能做什么了?移动一个物体,加载声音和图片,用鼠标对游戏进行一定的控制。哦,我的天,我们已经可以做自己的很简单的游戏了。是的,你可以了,我认为在此,大家可以放下教程,把自己小时候一直想玩的游戏,把自己学程序时一直想做的游戏自己进行设计实现,这对你的帮助将是非常大的。对你的编程水平也是一个很大的提高。
当然如果你仍然认为自己认识还不是很深,下面让我们来设计一个完整的游戏。这将是一个很有意思的过程。
第一个游戏-"保卫者"
主线思路:
真正做自己的游戏是总是很兴奋。在开始任何事情之前,我们都要有个好的设计,游戏更不例外。下面我们就以上面的例子为本。设计一个”保卫者”的游戏。游戏思路本身很简单,从屏幕的顶端不断的有炸弹落下来,而我们这些”保卫者” 要在它们着地之前,用鼠标点击让它反弹回去,不让它落到地面上来,但是球在上升过程中我们也要注意不让它撞到顶上。如果撞到顶上或地画,你的生命点数都会减少。每点中一个炸弹你的分数就会增加。当你的生命点数为零。”game over”。
设计结构:
1.模块设计:
游戏的结构很简单,由三个模块组成。
denfen类:denfen类控制整个游戏主线程,初始化炸弹类,并绘制屏幕上的炸弹数量及处理炸弹的移动,并监听鼠标事件
bomb类:主要是判断炸弹的速度,方向,是否撞到地面和点击事件
denfense类:主要用来处理游戏者的记分和生命点数
2.方法实现:
denfen:
init(): 初始化所有对象,包括声音文件的加载,bomb类的生成
run(): 处理炸弹的下降运动
paint(...):绘制炸弹及相关的数据记录显示
update(...): 实现屏幕图像的双缓冲,消除闪烁
mouseprocess (...): 利用mouseevent事件监听来处理鼠标按下事件,并根据鼠标当时的x坐标和y坐标判断是否点中炸弹。
addbomb():利用默认值来动态实现bomb的生成,这里我们利用了数组来记录的。默认值是3,大家可依据自己的爱好增加或减少记录。
denfenser:
score:积分
life:生命点
addscore():增加游戏者的积分
death():减少游戏者的生命点数
getscore():获得当前的积分数
getlife():获得当前的生命点数
bomb:
bomb(...): 构造函数,初始化炸弹的位置,声音,颜色等相关变量的值.
down():处理bomb的下降
isrebound ():反向回弹炸弹的方向,并根据积分来加快炸弹的下降速度
userhit (int x, int y):游戏者是否点中炸弹。
washitearth(): 判断炸弹是否撞击到地面或顶面,如果是生命点将减少。
drawbomb(graphics g): 绘制bomb图象。
3.工作原理:
首先我们在init()方法中加载所有游戏必要的资源,包括声音,鼠标事件的监听、背景等相关设置。利用addbomb()方法增加bomb的数量、初始位置及初始化颜色。再利用start()启动线程。线程调用run()方法,处理炸弹下降运动down()。repaint()会在每一个单位时间调用paint()方法不断的刷新屏幕,paint()调用bomb.addbomb()绘制炸弹。当游戏者按下鼠标,mousepress()事件激活,判断是否点中了炸弹。如果点中addscore()自动加1分。如果没有点中炸弹,炸弹继续下降,当撞到屏幕washitearth()方法激活,其内调用death()方法,减少denfenser.life生命点,同时audio.play()处理声音的播放,用以提示游戏者。当你的生命点数小于0时”game over”。
这个游戏并不是很完善。下面提到一些改进方法,大家可以动手试试。做出适合自己的游戏风格来。具体的源代码及实现过程请大家从这里下载.
4.游戏的改进:
背景的替换,本例的背景用的是函数setbackground(),大家可用相应的图形来代替。
炸弹数量的增加,为了减少复杂度,例子用到的炸弹数量是固定值3,我们可根据积分的多少,在游戏中动态的增加炸弹的数量。
等级的设置,本游戏中没有等级的功能。如果大家在游戏中加入等级,依据不同的等级不断的变换游戏的模式,这将是很有意思的过程。
模式改变。我们可以在游戏中实现自己的模式。如消灭炸弹。点一个炸弹,就让炸弹从屏幕上消灭。
我们还可以增加一个游戏者,加大游戏的可玩性。增加键盘的处理功能。加大游戏的灵活性。
还有很多很多的处理和玩法,这都等着你去发掘。相信java 游戏编程将会是一个很有意思的学习过程。
. 缓冲区 缓冲区用来储存着色的像素(影像)在视频内存中的区域。缓冲区的大小由解析度和色深决定,例如800x600,16bit色的缓冲区就占用800x600x2(16bit=2bytes)的内存区域。
(1) 前置buffer是当前显示在萤幕上的缓冲区,后置buffer是尚未显示在萤幕上的缓冲区。
(2) single buffering使用一个前置缓冲区,在着色的同时影像立即显示在萤幕上。因此当萤幕更新影像时会出现闪烁的现象。single buffering在目前的程序中已很少使用。
(3) double buffering则使用两个缓冲区,一个前置buffer,一个后置buffer。所谓前置和后置是相对而言的。前置缓存的像素在屏幕上显示的同时,显卡正在紧张地着色后置缓存中的像素。
后置缓存的像素上色完毕后是以vsync信号的形式等待。在前置缓存和后置缓存交换后,新一轮的着色工作又重新开始。这正如舞台话剧中前台和后台的演员一般。在前台演员表演时,后台的演员仍在进行最后的排练。前台的演员下场时正是后台演员登场的时间。唯一不同的是前置和后置缓存是循环轮番上阵,而演员表演完毕一般都不再出场。目前大多数游戏内定都使用double buffering。
(4) triple buffering使用一个前置缓存和两个后置缓存。在着色完第一个后置缓冲区的数据后,立即开始处理第二个后置缓冲区。今天,不少新游戏都采用的是triple buffering,trible buffering正逐渐成为发展的趋势,因为它没有vsync(萤幕的垂直刷新频率)等待的时间,游戏也将更加流畅。triple buffering也是3dmark2000测试的内定值设定。