Java游戏编程

本文详细介绍了Java游戏开发中的关键概念和技术,包括AWT和Swing组件的使用,游戏图形界面的构建,动画的实现原理,如帧频、视觉暂留现象。讲解了如何通过线程控制动画,消除闪烁的双缓冲技术,以及游戏中的碰撞检测和图像绘制。此外,还探讨了游戏角色的移动、动画效果,以及游戏声音的播放。通过对推箱子游戏的分析,阐述了游戏逻辑和地图数据的设计,展示了如何通过键盘控制角色移动并实现游戏规则的判断。
摘要由CSDN通过智能技术生成

游戏图形界面开发基础

AWT简介

AWT(Abstract Window Toolkit,抽象窗口工具集) 它为用户提供基础的界面构件 组件类(Component)
容器类(Container) 图形类(Graphics) 布局管理器类(LayoutManager)
在AWT中存在缺少剪切板,打印支持等缺陷,甚至没有弹出式菜单和滚动窗口,因此Swing的产生也就成为必然
Swing是纯Java语言实现的轻量级组件,它不依赖系统的支持

Swing基础
与AWT组件不同,Swing组件不能直接添加到顶层容器中,它必须添加到一个与Swing顶层容器相关联的内容面板(content pane)容器上。内容面板它是一个轻量级组件
按钮JButton
它是javax.swing,AbstracButton类的子类,Swing中的按钮可以显示图像,并且可以将按钮设置为窗口的默认图标,而且还可以将多个图像指定给一个按钮
构造方法
JButton(Icon icon):在按钮上显示图标
JButton(String text):按钮上显示字符
JButton(String text,Icon icon):按钮上既显示图标又显示字符
方法
setText(String text):设置按钮的标签文本
setIcon(Icon defaultIcon):设置按钮在默认状态下显示地图片
setRolloverIcon(Icon rolloverIcon):设置当光标移动到按钮上方时显示的图片
setPressedIcon(Icon pressedIcon):设置当按钮被按下时显示地图片
setContentAreaFilled(boolean b):设置按钮的背景为透明,当设为false时表示不绘制,默认绘制
setBorderPainted(boolean b):设置为不绘制按钮的边框,当设为false时表示不绘制,默认为绘制
单选按钮JRadioButton
JRadioButton类可以单独使用,也可以与ButtonGroup类联合使用,当单独使用时,该单选按钮可以被选定和取消选定;当与ButtonGroup类联合使用时,需要使用add()方法将JRadioButton添加到ButtonGroup中并组成一个单选按钮组,此时用户只能选定按钮组中打的一个单选按钮

Java游戏程序的基本框架

1.动画类型及帧频

动画的制作是游戏设计的基础,几乎所有的游戏都是在动画的基础上添加人机交互功能,以及增加剧情功能等延伸出来的。因此动画的制作是游戏开发人员必须了解的知识,也是游戏制作的基本元素。

  • 动画主要分为影视动画游戏动画两种
    • 影视动画就是使用专业动画软件编辑出来的效果很好的动画视频
    • 游戏动画则不同于影视动画,是在屏幕上显示一系列连续动画画面的第一帧图形,然后在每隔很短的时间显示下一帧图像,如此反复,利用人眼的视觉暂留现象而感觉好像画面的物体在运动。显示的图形不一定是图片,也可能是其他绘图元素(例:正方体等)
  • 视觉暂留现象:
    • 当投影机以每秒24格的速度投射在银幕上
    • 或者录像机以每秒30格的扫描方式在电视荧光屏上呈现影像时
    • 它会把每格不同的画面连接起来,从而在我们脑海中产生物体在“运动”的印象,这就是“视觉暂留”现象
  • 设置合理的帧频
    • FPS: 顾名思义,就是每秒钟的帧数。
    • 帧: 每一帧就是一幅静态图像,电影的播放速度是24FPS,但是通常游戏速度达到10FPS就能明显感觉到动画的效果了。
    • 由于屏幕上显示的图像越大,占用的内存越多,处理的速度就越慢,尤其是那些需要大量动画的游戏,因此如果想使用较高的FPS,就必须在显示大小上面做出牺牲。目前PC游戏往往分为640×480、1024×768等多种分辨率就是这个道理。设计一款良好的手机游戏,要充分考虑到设备本身的限制,在游戏中设置好最佳的FPS。

2.游戏动画的制作

  • 动画就是将一连串的图像快速地循环播放,因此就需要使用循环语句控制图像的连续播放。因为动画需要一定的播放速度,所以需要连续播放动画的同时能够控制动画的播放速度,最好使用线程中的暂停函数来实现。
  • 无论你希望动画播放几次都可以通过一个循环语句来实现
    //例:希望动画无限制地播放
    whiletrue{
    处理游戏功能;
    使用repaint()函数要求重画屏幕;
    暂停一小段时间;//帧频FPS控制
    }
    
  • 说明:
  • 在Java游戏程序中,通过repaint()函数请求屏幕的重画,可以请求重画全部屏幕,也可以请求重画部分屏幕

例:使用线程和循环实现一个简单的自由落体小球动画

  • 由于自由落体的基本动画就是从屏幕的顶端往下自由降落小球,一个复杂的动画总是由一些基本的元素组成的,因此实现自由落体动画,首先设计一个自由降落的小球,同时控制降落的速度。
  • 控制速度就需要实现一个继承了 Runnable 线程接口和继承了 JPanel 类的TetrisPanel面板类
  • 继承JPanel类是为了使用JPanel的Paint()方法来实现小球在屏幕上的绘制,继承Runnable线程接口可以实现动画的暂停控制。
import java.awt.Graphics;

import javax.swing.JPanel;

public class TetrisPanel extends JPanel implements Runnable{
   public TetrisPanel(){
       
   }
   public void paint(Graphics g){

   }
   @Override
   public void run() {

   }
}
  • 在 TetrisPanel()构造方法函数中创建线程,并启动该线程。
  • 一旦做好这些准备工作以后,当TetrisPanel对象第一次被显示时,就会创建线程对象的一个实例,并把“this”对象作为建构方法的参数,相当于开启一个本地的线程,之后就可以启动动画了。
 public TetrisPanel(){
        // 创建一个线程
        Thread t = new Thread(this);
        // 启动线程
        t.start();
    }
  • 现在来实现线程的run()方法,它使用无限循环while(true)语句中每隔30ms重画动画场景。
  • Thread.sleep()这个方法很重要,如果在run()方法的循环语句中没有这部分,小球的重画动作将执行得很快,其他一些功能则不能完全执行,屏幕上完全看不出动画的效果,也即在屏幕上将看不到小球的显示。
    @Override
    public void run() {
        // 线程中的无限循环
        while(true){
            try {
                // 线程休眠
                Thread.sleep(24);
            }catch (InterruptedException e){}
            // 修改小球左上角的纵坐标
            ypos += 5;
            // 小球离开窗口后重设左上角的纵坐标
            if(ypos > 300) {
                ypos = -80;
            }
            // 窗口重绘
            repaint();
        }
    }
  • 小球的重画将在paint()方法中实现,通过不断更改小球要显示的y坐标,同时清除上一次显示的小球,就可以看到小球的自由落体动画
  • 由于是演示程序,并没有实现局部清除上一次显示的小球的方法,使用了清除全屏的简单方法,让读者把重点放在动画控制的程序流程中
  public void paint(Graphics g){
        super.paint(g);
        //先清屏,否则原来画的东西仍在
        g.clearRect(0,0,this.getWidth(),this.getHeight());
        //设置小球颜色
        g.setColor(Color.red);
        //绘制小球
        g.fillOval(90,ypos,80,80);
        //绘制小球
    }

小球的下落则通过不断改变小球的显示 y 坐标达到目的

import java.awt.*;

import javax.swing.JPanel;

public class TetrisPanel extends JPanel implements Runnable{
    public int ypos =- 80;//小球左上角的纵坐标
    private Image iBuffer;
    private Graphics gBuffer;
    public TetrisPanel(){
        // 创建一个线程
        Thread t = new Thread(this);
        // 启动线程
        t.start();
    }
    @Override
    public void run() {
        // 线程中的无限循环
        while(true){
            try {
                // 线程休眠
                Thread.sleep(24);
            }catch (InterruptedException e){}
            // 修改小球左上角的纵坐标
            ypos += 5;
            // 小球离开窗口后重设左上角的纵坐标
            if(ypos > 300) {
                ypos = -80;
            }
            // 窗口重绘
            repaint();
        }
    }
    public void paint(Graphics g){
        super.paint(g);
        //先清屏,否则原来画的东西仍在
        g.clearRect(0,0,this.getWidth(),this.getHeight());
        //设置小球颜色
        g.setColor(Color.red);
        //绘制小球
        g.fillOval(90,ypos,80,80);
        //绘制小球
    } }
  • 通过不断改变坐标系统的y坐标ypos达到红色小球向下移动的效果。
  • 屏幕的每次刷新都会根据改变后的y坐标ypos重新绘制红色小球。
    运行TetrisPanel类的主程序并没有添加其他的控制,仅仅为动画屏幕添加了一个窗口关闭处理方法,用来关闭程序
import javax.swing.*;
import java.awt.*; 
import java.awt.event.WindowAdapter; 
import java.awt.event.WindowEvent;

public class MyWindow extends JFrame {
    public MyWindow(){
        this.setTitle("测试窗口");
        //获取面板容器
        Container c = this.getContentPane();
        c.add(new TetrisPanel());
        //设置窗口开始显示时距离屏幕左边400个像素点,距屏幕上边200个像素点,窗口宽300个像素点,高300个像素点
        this.setBounds(400,200,300,300);
        //设置窗口大小不会改变
        this.setResizable(false);
        this.setVisible(true);
    }
    public static void main(String[] args){
        //创建该窗口的实例DB,开始整个程序
        MyWindow DB = new MyWindow();
        //添加窗口关闭出处理方法
        DB.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
               System.exit(0);
            }
        });
    } }

程序运行效果如图所示
在这里插入图片描述
在这里插入图片描述

  • 一个红色小球从上往下以一定的速度慢慢掉下来,当黑色小球移动到屏幕底端的时候,将循环从上开始继续下落,只要不关闭程序,将会无限制地重复下落的动画。

3.消除动画闪烁现象——双缓冲技术

  • 一个动画在运行的时候,如果图像的切换是在屏幕上完成的,则可能会造成屏幕的闪烁,消除动画闪烁现象的最佳方法是使用双缓冲技术
  • 双缓冲技术在屏幕外做一个图像缓冲区,事先在这个缓冲区内绘制图像,然后再将这个图像送到屏幕上,虽然动画中的图像切换很频繁,但是双缓冲技术很好地避免了在屏幕上进行消除和刷新时候的处理工作所带来的屏幕闪烁情况。但是在屏幕外的缓冲区需要占用一部分的内存资源,特别是图像比较大的时候,内存占用非常严重,因此一般需要考虑动画的质量和运行速度之间的重要性,有选择性地进行开发。
  • 1.屏幕产生闪烁的原因
    • 在Java游戏编程和动画编程中最常见的就是对于屏幕闪烁的处理。屏幕产生闪烁的原因是先用背景色覆盖组件再重绘图像的方式造成的。即使时间很短,如果重绘的面积较大则花费的时间也是比较可观的,这个时间甚至可以大到足以让闪烁严重到让人无法忍受的地步。
    • 就像以前课堂上老师用的旧式幻灯机,放完一张胶片,老师会将它拿下来,这个时候屏幕上一片空白,直到放上第二张,中间时间间隔较长。当然,这不是在放动画,但上述闪烁的产生原因与这很类似。
    • 运行简单的小球下落动画程序后,我们会看到窗体中有一个从上至下匀速运动的小球,但仔细观察,你会发现小球会不时地被白色的不规则横纹隔开,即所谓的屏幕闪烁,这不是我们预期的结果。
    • 这种闪烁是如何出现的呢?
      首先我们分析一下这段代码。MyWindow类的对象DB建立后,将显示窗口,程序首先自动调用重载后的 paint(Graphics g)方法,在窗口上绘制了一个小球,绘图线程启动后,该线程每隔24ms修改一下小球的位置,然后调用repaint()方法。
    • 注意,这个 repaint()方法并不是我们重载的,而是从 JPanel 类继承而来的。它先调用update(Graphics g)方法,update(Graphics g)方法再调用 paint(Graphics g)方法。问题就出在update(Graphics g)方法上,我们来看看这个方法的源代码:
      public void update(Graphics g){
      	if(isShowing){
      		if(!(peer instanceof LightweightPeer)){
         			g.clearRect(0,0,width,height);
         		}
      	paint(g);
      	}
      }
      
    • 以上代码的意思是:(假设该组件是轻量组件)先用背景色覆盖整个组件,然后再调用paint(Graphics g)方法重新绘制小球。这样,我们每次看到的都是一个在新的位置绘制的小球,前面的小球都被背景色覆盖了。这就像一帧一帧的画面匀速地切换,以此来实现动画的效果。
    • 但是,正是这种先用背景色覆盖组件再重绘图像的方式导致了闪烁。在两次看到不同位置小球的中间时刻,总是存在一个在短时间内被绘制出来的空白画面(颜色取背景色)。正如前面所述:即使时间很短,如果重绘的面积较大则花费的时间也是比较可观的,这个时间甚至可以大到足以让闪烁严重到让人无法忍受的地步。
    • 另外,用paint(Graphics g)方法在屏幕上直接绘图的时候,由于执行的语句比较多,程序不断地改变窗体中正在被绘制的图像,会造成绘制的缓慢,这也从一定程度上加剧了闪烁。知道了闪烁产生的原因,我们就有了更具针对性的解决闪烁的方案。

2.双缓冲技术

  • 所谓双缓冲,就是在内存中开辟一片区域,作为后台图像,程序对它进行更新、修改,绘制完成后再显示到屏幕上。
  • 双缓冲技术的工作原理:先在内存中分配一个与我们动画窗口一样大的空间(在内存中的空间我们是看不到的),然后利用getGraphics()方法来获得双缓冲画笔,接着利用双缓冲画笔给空间描绘出我们想画的东西,最后将它全部一次性地显示到屏幕上。这样在我门的动画窗口上面显示出来就非常流畅了,避免了上面的闪烁效果。

3.双缓冲的使用

  • 一般采用重载 paint(Graphics g)方法实现双缓冲。这种方法要求我们将双缓冲的处理放在paint(Graphics g)方法中,那么具体该怎么实现呢?先看下面的代码(基于前面代码段修改):
    在TetrisPanel 类中添加如下两个私有成员:
private Image iBuffer;
private Graphics gBuffer;

重载paint(Graphics g)方法:

public void paint(Graphics g){
	if(iBuffer == null){
		iBuffer = createImage(this.getSize().width,this.getSize.height);
		gBuffer = iBuffer.getGraphics();
	}
	gBuffer.setColor(getBackground());
	gBuffer.fillRect(0,0,this.getSize().width,this.getSize().height);
	gBuffer.setColor(Color.red);
	gBuffer.fillOval(90,ypos,80,80);
}

再将此缓冲图像一次性绘制到代表屏幕的Graphics对象上,即该方法传入的g上

g.drawImage(iBuffer,0,0,this);
  • 分析上述代码:我们首先添加了两个成员变量iBuffer和gBuffer作为缓冲(这就是所谓的双缓冲名称的来历)。在paint(Graphics g)方法中,首先检测如果iBuffer为null,则创建一个与屏幕上的绘图区域大小一样的缓冲图像;再取得iBuffer的Graphics类型的对象的引用,并将其赋值给gBuffer;然后对gBuffer这个内存中的后台图像先用fillRect(int,int,int,int)清屏,再进行绘制操作,完成后将iBuffer直接绘制到屏幕上。
  • 这段代码看似可以完美地完成双缓冲,但是运行之后我们看到的还是严重的闪烁。什么原因呢?问题还是出现在update(Graphics g)方法上。这段修改后的程序中的update(Graphics g)方法还是我们从父类继承的。在(Graphics g)方法中,clearRect(int,int,int,int)对前端屏幕进行了清屏操作,而在paint(Graphics g)方法中,对后台图像又进行了清屏操作。那么如果保留后台清屏,去掉多余的前台清屏应该就会消除闪烁。因此,我们只要重载update(Graphics g)方法即可:
public void update(Graphics g){
	paint(g);
}

这样就避开了对前端图像的清屏操作,避免了屏幕的闪烁。

  • 运行上述修改后的程序,我们会看到完美的消除闪烁后的动画效果。就像在电影院看电影,每张胶片都是在后台准备好的,播放完一张胶片之后,下一张很快就被播放到前台,自然不会出现闪烁的情形。
    为了让读者能对双缓冲技术有个全面的认识,现将上述双缓冲的实现原理概括如下。
  • 1.定义一个Graphics对象gBuffer和一个Image对象iBuffer。按屏幕大小建立一个缓冲对象给iBuffer。然后取得iBuffer的Graphics赋给gBuffer。此处可以把gBuffer理解为逻辑上的缓冲屏幕,而把iBuffer理解为缓冲屏幕上的图像。
  • 2.在gBuffer(逻辑上的屏幕)上绘制图像。
  • 3.将后台图像iBuffer全部一次性地绘制到我们的前台窗口。
  • 以上就是一次双缓冲的过程。注意,将这个过程联系起来的是repaint()方法。paint(Graphics g)方法是一个系统调用语句,不能由程序员手工调用,只能通过repaint()方法调用。
  • 上文提到的双缓冲的实现方法只是消除闪烁的方法中的一种。如果在Swing中,组件本身就提供了双缓冲的功能,我们只需要进行简单的方法调用就可以实现组件的双缓冲,在awt中却没有提供此功能。另外,一些硬件设备也可以实现双缓冲,每次都是先把图像画在缓冲中,然后再绘制在屏幕上,而不是直接绘制在屏幕上,基本原理与文中介绍的类似。还有其他用软件实现消除闪烁的方法,但双缓冲是个简单的、值得推荐的方法。

4.关于双缓冲的补充

  • 双缓冲技术是编写Java游戏的关键技术之一。双缓冲付出的代价是较大的额外内存消耗,但现在节省内存已经不再是程序员考虑的最首要的问题了,游戏的画面在游戏制作中是至关重要的,因此以额外的内存消耗换取程序质量的提高还是值得肯定的。
  • 有时动画中相邻的两幅画面只是有很少部分的不同,这就没必要每次对整个绘图区进行清屏。我们可以对文中的程序进行修改,使之每次只对部分屏幕清屏,这样既能节省内存,又能减少绘制图像的时间,使动画更加连贯。

5.使用定时器

  • 定时器在游戏开发中是相当重要的,前面提到动画的实现就是通过显示时间的控制达到视觉暂留的效果,除了使用线程的暂停函数sleep()外,还有一个重要的计时工具,那就是Timer组件。
  • Timer 组件可以定时执行任务,这在游戏动画编程上非常有用。Timer组件可以用javax.swing.Timer包中的Timer类来实现,该类的构造方法为: `
Timer(int delay,ActionListener listener);
  • 该构造方法用于建立一个Timer组件对象,参数listener用于指定一个接收该计时器操作事件的侦听器,指定所要触发的事件;
  • 而参数delay用于指定每一次触发事件的时间间隔。也就是说,
  • Timer组件会根据用户所指定的delay时间,周期性地触发ActionEvent事件。如果要处理这个事件,就必须实现ActionListener接口类,以及接口类中的actionPerformed()方法。
  • Timer组件类中的主要方法如下。
    • void start():激活Timer组件对象。
    • void stop():停止Timer组件对象。
    • void restart():重新激活Timer
  • 在游戏编程中,在组件内容更新时经常用到Timer组件,例如JPanel、JLabel等内容的更新。本书中俄罗斯方块游戏就采用定时器Timer实现控制方块的下落。
    例:下面是一个简单的每隔500ms显示时间的swing程序,可以帮助读者加深对Timer组件使用的理解。
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
* 测试swing中Timer的使用,一个显示时间的GUI程序
*/
public class TimerTest extends JFrame implements ActionListener {
   //显示时间的JLabel
   private JLabel jlTime = new JLabel();
   private Timer timer;

   public TimerTest(){
       setTitle("测试题目");
       setSize(180,80);
       setLocation(400,300);
       setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

       //添加标签
       add(jlTime);
       //设置Timer定时器并启动
       timer = new Timer(500,this);
       timer.start();
       setVisible(true);
   }

   /**
    * Timer要执行的部分
    */
   @Override
   public void actionPerformed(ActionEvent e) {
       DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
       Date date = new Date();
       jlTime.setText(format.format(date));
   }

   public static void main(String[] args){
       new TimerTest();
   }
}
  • TimerTest类实现了ActionListener接口,因此可以直接设置timer = new Timer(500,this);使用this初始化计时器。
  • 当计时器启动后(timer.start()执行后),每隔500ms执行一次实现的ActionListener 接口中的actionPerformed的方法体。

6.设置游戏难度

  • 一般来说,一款游戏要增强它的可玩性,就需要有合理的游戏难度,使玩家不容易感到厌烦,同时增加玩家的挑战欲望。例如,智力游戏可以在人工智能方面下工夫,但是由于人工智能在游戏里面很难真正地控制游戏本身的难度,因此往往通过其他手段实现游戏的难度控制,比如增加游戏进行的速度。
  • 例如,在俄罗斯方块游戏中,可以增加方块下落的速度,或者使用增加游戏关数、减少玩家的思考时间等。难度设置的开发需要根据具体的游戏情况而定,很难一概而论。
  • 如果使用速度控制游戏难度,则可以把游戏设计成为具有很多个级别,每个级别游戏的运行速度都不一样,类似的代码如下所示:
public void level(){
	if(level == 3){
		//设置游戏速度为1
		speed = 1;
	}else if(level == 4){
		//设置游戏速度为2
		speed = 2;
	}
}

7.游戏与玩家的交互

  • 对于游戏而言,交互性就是生命,优异的作品总是体现在人物与场景之间的高互动性、人物与人物(NPC)之间的高互动性以及玩家与玩家之间(多人模式)的高互动性上,这是真正能让玩家溶入其中的动力所在,因此一款好的游戏必然拥有一个良好的游戏与玩家之间的互动方式。
  • 游戏与玩家的交互都是通过键盘或者鼠标实现的,具体的方法在第2章中已经详细介绍了。
  • 下面举一个简单的例子,在小球下落时可以通过键盘控制其左右移动。由于需要实现键盘监听事件,因此TetrisPanel加入了KeyListener接口,同时为控制x坐标增加了xpos变量。
package ThirdDemo;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class TetrisPanel extends JPanel implements Runnable, KeyListener {
   public int ypos =- 80;
   public int xpos = 90;

   public TetrisPanel(){
       //创建一个新线程
       Thread t = new Thread(this);
       //启动线程
       t.start();
       //设定焦点在本面板并作为监听对象
       setFocusable(true);
       addKeyListener(this);
   }

   @Override
   public void keyTyped(KeyEvent e) {

   }

   @Override
   public void keyPressed(KeyEvent e) {
       //获得按键编号
       int keyCode = e.getKeyCode();
       //通过keyCode识别用户的按键
       switch (keyCode){
           case KeyEvent.VK_LEFT:  //当触发Left时
               xpos -= 10;
               break;
           case KeyEvent.VK_RIGHT: //当触发Right时
               xpos += 10;
               break;
       }
       if(xpos > 280) {
           xpos = -75;
       }
       if(xpos < -75){
           xpos = 280;
       }
       //重新绘制窗体图像
       repaint();
   }

   @Override
   public void keyReleased(KeyEvent e) {

   }

   @Override
   public void run() {
       while(true){
           try {
               //线程休眠30毫秒
               Thread.sleep(24);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           ypos += 5;
           if(ypos > 300) {
               ypos = -80;
           }
           //窗口重绘
           repaint();
       }
   }

   public void paint(Graphics g){
       super.paint(g);
       //先清屏,否则原来画的东西仍在
       g.clearRect(0,0,this.getWidth(),this.getHeight());
       //设置小球颜色
       g.setColor(Color.red);
       //绘制小球
       g.fillOval(xpos,ypos,80,80);
       //绘制小球
   }
}
  • 这样在游戏过程中,玩家通过键盘就可以控制小球左右移动了。

8.游戏中的碰撞检测

  • 我们在游戏开发中总会遇到这样那样的碰撞,并且会很频繁地去处理这些碰撞,这也是游戏开发中的一种基本算法。
    常见的碰撞算法是(矩形碰撞用得最多)

    • 矩形碰撞
    • 圆形碰撞
    • 像素碰撞
  • 1.矩形碰撞

  • 假如把游戏中的角色统称为一个一个的 Actor,并且把每个 Actor 框成一个与角色大小相等的矩形框,那么在游戏中的每次循环检查就是围绕每个 Actor的矩形框之间是否发生了交错。为了简单起见,我们就拿一个主角与一个Actor来分析,其他情况与此类似。

  • 一个主角与一个Actor的碰撞其实就成了两个矩形的检测,是否发生了交集。

  • 第1种方法:

  • 我们可以通过检测一个矩形的4个顶点是否在另外一个矩形的内部来完成。下面简单地设定一个Actor类:

public class Actor(){
	int x;
	int y;
	int w;
	int h;

	public int getX(){
		return(x);
	}
	public int getY(){
		return(y);
	}

	public int getActorWidth(){
		return  w;
	}
	
	public int getActorHeight(){
		return h;
	}
}

检测的处理为:

public boolean isCollidingWith(int px,int py){
       if(px > getX() && px < getX() + getActorWidth() && px > getY() && px  getY() + getActorHeight()){
           return true;
       }else{
           return false;
       }
   }
   
   public boolean isCollidingWith(Actor another){
       if(isCollidingWith(another.getX(),another.getY())
       || isCollidingWith(another.getX()+another.getActorWidth(),another.getY())
       || isCollidingWith(another.getX(),another.getY()+another.getActorHeight())
       || isCollidingWith(another.getX()+another.getActorWidth(),another.getY()+an>other.getActorHeight())        
       ){
           return true;
       }else{
           return  false;
       }
   }

}
  • 以上处理运行应该是没有什么问题的,但是没有考虑到运行速度,而游戏中需要大量的碰撞检测,因此要求碰撞检测要尽量地快。
  • 第2种方法:
  • 我们从相反的角度考虑,以前是处理什么时候相交,现在我们处理什么时候不会相交。可以处理4条边,左边a矩形的右边界在b矩形的左边界以外,同理,a的上边界需要在b的下边界以外,4条边都判断,则可以知道a是否与b相交
    在这里插入图片描述
    代码如下:
   /**
    *  ax:a矩形左上角x坐标
    *  ay:a矩形左上角y坐标
    *  aw:a矩形宽度
    *  ah:a矩形高度
    *  bx:b矩形左上角x坐标
    *  by:b矩形左上角y坐标
    *  bw:b矩形宽度
    *  bh:b矩形高度
    */
   public boolean isColliding(int ax,int ay,int aw,int ah,int bx,int by,int bw,int bh){
       if(ay > by + bh || by > ay + ah || ax > bx + bw || bx > ax + aw){
           return false;
       }else{
           return true;
       }
   }

此方法比第1种方法简单且运行快。
第3种方法:

  • 这种方法其实可以说是第2种方法的一个变异,我们可以保存两个矩形的左上和右下两个坐标的坐标值,然后对两个坐标的一个对比就可以得出两个矩形是否相交。这种方法应该比第2种方法更优越一点。
/**
* rect1[0]:矩形1左上角x坐标
* rect1[1]:矩形1左上角y坐标
* rect1[2]:矩形1右下角x坐标
*  rect1[3]:矩形1右上角y坐标
*  rect2[0]:矩形2左上角x坐标
*  rect2[1]:矩形2左上角y坐标
*  rect2[2]:矩形2右下角x坐标
*  rect2[3]:矩形2右上角y坐标
*/
static boolean IsRectCrossing(int rect1[],int rect2[]){
   if(rect1[0] > rect2[2]){return false;}
   if(rect1[2] < rect2[0]){return false;}
   if(rect1[1] > rect2[3]){return false;}
   if(rect1[3] < rect2[1]){return false;}
   return true;
}

这种方法的速度应该很快了,推荐使用这种方法。

9.圆形碰撞

  • 下面介绍一种测试两个对象边界是否重叠的方法。可以通过比较两个对象间的距离和两个对象半径的和的大小,很快实现这种检测。如果它们之间的距离小于半径的和,就说明产生了碰撞。
  • 为了计算半径,可以取高度或者宽度的一半作为半径的值。
    代码如下:
     public static boolean isColliding(int ax,int ay,int aw,int ah,int bx,int by,int bw,int bh){
        int r1 = (Math.max(aw,ah)/2+1);
        int r2 = (Math.max(bw,bh)/2+1);
        int rSquard = r1*r1;
        int anrSquard = r2*r2;
        int disX = ax-ax;
        int disY = ay-by;
        if((disX*disY)+(disY*disY) < (rSquard+anrSquard)){
            return true;
        }else{
            return  false;
        }
    }

这种方法类似于圆形碰撞检测,处理两个圆的碰撞时就可以用这种方法。
10像素碰撞

  • 由于游戏中的角色的大小往往是以一个刚好能够将其包围的矩形区域来表示的
  • 虽然两个卡通人物并没有发生真正的碰撞,但是矩形碰撞检查的结果却是它们发生了碰撞。
  • 如果使用像素检查,就通常把精灵的背景颜色设置为相同的颜色而且是最后图片里面很少用到的颜色,然后碰撞检查的时候就只判断两张图片除了背景色外的其他像素区域是否发生了重叠的情况。如图4-4所示,虽然两张图片的矩形发生了碰撞,但是两个卡通人物并没有发生真正的碰撞,这就是像素检查的好处,但缺点是计算复杂,消耗了大量的系统资源,因此一般没有特殊要求,应尽量使用矩形检查碰撞。
  • 以上只是总结了几种简单的方法,当然在游戏中熟练的运用才是最好的。在Java中掌握以上几种方法就可以了,它不需要太精密的算法,当然可能有些时候需要比以上情况更复杂。
  • 例如如果一个对象速度足够得快,可能只经历一步就穿越了一个本该和它发生碰撞的对象,如果要考虑这种情况,就要根据它的运动路径来处理。还有可能碰到不同的边界发生不同行为的情况,这就要对碰撞行为进行具体的解剖,然后再做处理。

11.游戏中图像的绘制

  • Graphics 类中提供了很多绘制图形的方法,但对于复杂图形,大部分都事先利用专用的绘图软件绘制好,或者是用其他截取图像的工具(如扫描仪、视效卡等)获取图像的数据信息,再将它们按一定的格式存入图像文件。Java程序运行时,将它装载到内存,然后在适当的时机将它显示在屏幕上。
  • 1.图像文件的装载
    Java目前所支持的图像文件格式通常有GIFPNGJPEG格式(带有.gif、.jpg、.jpeg后缀名的文件),具体描述如下。
  • GIF(Graphics Interchange Fomat,图像互换格式)图像文件的数据是经过压缩的,其压缩率一般在50%左右。此外,在一个GIF文件中可以存放多幅彩色图像,如果把它们逐幅读出并显示到屏幕上,就可形成一组简单的动画。
  • JPG的全名是JPEG(Joint Photographic Experts Group,联合图
    像专家组),它是与平台无关的格式,支持最高级别的压缩,但是这种压缩是有损耗的。
  • PNG(Portable Network Graphic,流式网络图形) 是一种位图文件存储格式。PNG用来存储灰度图像时,灰度图像的深度可多到16位;存储彩色图像时,彩色图像的深度可多到48位,并且还可存储多到16位的a通道数据。PNG使用无损数据压缩算法,压缩比高,生成文件的容量小。
  • Java 特别提供了 java.awt.Image包来管理与图像文件有关的信息,因此执行与图像文件有关的操作时需要import包。
  • java.awt.image包提供可用于创建、操纵和观察图像的接口和类。每一个图像都用一个java.awt.Image对象表示。除了Image类外,java.awt包还提供了其他基本的图像支持
  • 例如Graphics类的drawImage()方法、Toolkit对象的getImage()方法及MediaTracker类。
  • Toolkit类提供了两个getImage()方法来加载图像。
    • Image getImage(URL url)。
    • Image getImage(String filename)。
  • Toolkit是一个组件类,取得Toolkit的方法如下:
Toolkit tookkit = Tookit.getDefaultToolkit();

对于继承了Frame的类来说,可以直接使用下面的方法取得

Toolkit tookkit = getToolkit();

下面是两个加载图片的实例:

Toolkit toolkit Toolkit.getDefaultToolkit();
Image image1 = toolkit.getImage("imageFile.gif");
Image image2 = toolkit.getImage(new >URL("http://java.sun.con/graphics/people.gif"));

在java中获取一个图像文件,可以调用Toolkit类提供的getImage()方法。但是getImage方法会在调用后马上返回,如果此时马上使用由getImage()方法获取的Image对象,但这时Image对象并没有真正装载或者装载完成。因此我们在使用图像文件时,使用java.awt包中的MediaTracker跟踪一个Image对象的装载。可以确保所有的图片都加载完毕

  • 使用MediaTracker需要如下3个步骤。
  • 1.实例化一个MediaTracker,注意要将显示图片的Component对象作为参参数传入
MediaTracker tracker = new MediaTracker(Jpanel1);
  • 2.将要装载的Image对象加入MediaTracker
Toolkit toolkit = Toolkit.getDefaultToolkit();
Image[] pics = new Image[10];
pics[i] = toolkit.getImage("imageFile" + i + ".jpg");
tracker.addImage(pics[i],0);
  • 3.调用MediaTracker的checkAll()方法,等待装载过程的结束
tracker.checkAll(true);

11.图像文件的显示

  • getImage()方法仅仅是将图像文件装载进来,交由Image对象管理,而把得到的Image对象中的图像显示在屏幕上,则通过传递到paint()方法的Graphics对象可以很容易地显示图像
  • 具体显示过程需要调用Graphics类的drawImage()方法,它能完成将Image对象中的图像显示在屏幕的特定位置上,就像显示文本一样方便
  • drawImage()方法的常见调用格式如下
1.boolean drawImage(Image img,int x,int y,ImageObserver observer);
2.boolean drawImage(Image img,int x,int y,int width,int >height,ImageObserver observer);
3.boolean drawImage(Image img,int x,int y,Color bgcolor,ImageObserver >observer);
4.boolean drawImage(Image img,int x,int y,int width,int height,Color >bgcolor,ImageObserver observer);

这里介绍常用的情况:

boolean drawImage(Image img,int x,int y,ImageObserver observer);
  • 其中,参数img就是要显示的Image对象,参数x和y是该图像左上角的坐标值,参数observer则是一个ImageObserver接口(interface),它用来跟踪图像文件装载是否已经完成的情况,通常我们都将该参数置为this,即传递本对象的引用去实现这个接口
  • 组件可以指定this作为图像观察者的原因是:Component类实现了ImageObserver接口。
  • 当图像数据被加载时,它的实现会调用repaint方法
    例:下面的代码在组件区域的左上角(0,0)以原始大小显示一幅图像
g.drawImage(myImage,0,0,this);

除了将图像文件照原样输出以外,drawImage()方法的另外一种调用格式还能指定图像显示的区域大小:

boolean drawImage(Image img,int x,int y,int width,int height,ImageObserver observer);
  • 这种格式比第一种格式多了两个参数width和height,即表示图像显示的宽度和高度。如果实际图像的宽度和高度与这两个参数值不一样时,Java系统会自动将实际图像进行缩放,以适合我们所设定的矩形区域
  • 下面的代码在坐标(90,0)处显示一个被缩放为宽300像素高62像素的图像:
g.drawImage(myImage,90,0,300,62,this);
  • 有时,我们为了不使图像因缩放而变形失真,可以将原图的宽和高均按相同的比例进行缩小或放大。调用Image类中的两个方法就可以得到原图的宽度和高度,他们的调用格式如下:
int getWidth(ImageObserver observer);
int getHeight(ImageObserver observer);
  • 与drawImage()方法一样,我们通常用this作为observer的参数值
  • 下面的程序段给出一个显示图像文件的例子
import javax.swing.*;
import java.awt.*;

public class ShowImage extends JFrame {
   String fileName;

   public ShowImage(String fileName){
       setSize(1000,800);
       setVisible(true);
       this.fileName = fileName;
   }

   public void paint(Graphics g){
       //获取Image对象,加载图像
       Image img = getToolkit().getImage(fileName);
       //获取图像的宽度
       int w = img.getWidth(this);
       //获取图像的高度
       int h = img.getHeight(this);
       //原图
       g.drawImage(img,20,80,this);
       //缩小一半
       g.drawImage(img,200,80,w/2,h/2,this);
       //宽扁图
       g.drawImage(img,280,80,w*2,h/3,this);
       //瘦高图
       g.drawImage(img,500,80,w/2,h*2,this);
   }

   public static void main(String[] args){
       new ShowImage("F:\\Pictures\\屏幕截图 2021-01-26 160348.png");
   }
}

  • 窗体类继承自JFrame,因此可以使用Toolkit.getDefaultToolkit()方法来取得Toolkit对象,然后使用getImage()方法取得一张本地图片文件,最后在paint()方法中使用Graphics的drawImage()方法即可显示该图像并缩放显示
  • 通过getImage()方法取得的是java.awt.ImageIO类的read()方法取得一幅图像,返回的是BufferedImage()对象
    调用格式如下
BufferedImage ImageIO.read(Url);

BufferedImage是Image的子类,它描述了具有可访问图像数据缓冲区的Image,通过该类即可实现图像的缩放
例:

Image im = ImageIO.read(getClass().getResource());
//图像的绘制方式
g.drawImage(im,0,0,null);

下面的程序首先读入一个图片文件,然后根据Image的getWidth()和getHeight()方法取得图片的宽度和高度,按照该宽度和高度的一半构造新的图片对象BufferedImage。并将原有的图片写入该实例中,即可实现图片的缩小,最后通过JPEG编码保存图片

在这里插入代码片

运行该程序,即可生成缩小后的图片

  • 游戏中的场景图像分为两种,分别为:
    • 卷轴型图像(ribbon)
    • 砖块型图像(tile)
  • 卷轴型图像的特点是内容多,面积大,常常作为远景图,例如设计游戏中的蓝天白云图像,一般不与用户交互
  • 砖块型图像通常面积很小,往往由多块这样的图像共同组成游戏的前景,并作为障碍物与游戏角色进行交互
    绘制卷轴型图像
  • 卷轴型图像通常都要超过程序窗口的尺寸,如果采用上一小节介绍的图像绘制方法来绘制卷轴型图像,则只能显示图像的一部分
  • 事实上,在绘制卷轴型图像时往往都需要让其滚动显示以制造移动效果,让图像的不同部分依次从程序窗口中经过
  • 用程序实现这样的滚动显示效果,则需要将卷轴型图像一段一段地显示在程序中,而这又涉及从图像坐标系到程序窗口坐标系的变换问题
  • 如图所示,左边为程序窗口坐标系,原点在窗口左上角;右边为图像坐标系,原点在卷轴型图像区域的左上角
    在这里插入图片描述
  • 图中的坐标变换可通过调用程序窗口的Graphics对象的另一个drawImage()方法来实现,该方法有10个参数,具体定义如下:
drawImage(im,t dxl,f dyl,dx2,dy2,:sxl,sylr sx2,sy2,observer);
  • 其中,第1个参数表示源图像,第2个至第9个参数的含义如图所示
  • dxl和dyl为目标区域左上角坐标;
  • dx2和dy2为目标区域右下角坐标;
  • sxl和syl为源区域左上角坐标;
  • sx2和sy2为源区域右下角坐标
  • 为了简化起见,这里仅讨论水平方向的场景滚动,因此dy1和dy2,syl和sy2的值无需改变,可依据窗口的尺寸设置为固定值。于是可以另写一个drawRibbon()方法来封装drawImage()方法,代码如下:
Private void drawRibbon(Graphics g,Image im,int dx1,int dx2,int sx1,int sx2){
	//pHeight为窗口的高度
	g.drawImage(im,dx1,0,dx,2,pHeight,sx2,pHeight,null);
}

绘制砖块型图像
砖块型图像的尺寸通常较少,其绘制过程类似于在程序窗口“贴瓷砖”,即将窗口区域按砖块型图像的尺寸划分为许多小方格,然后在相应的方格内绘制图像
在这里插入图片描述

  • 用多个砖块型图像来绘制窗口中的不同区域,需要使用砖块地图(TileMap)
  • 砖块地图可以简单地使用一个文本文件或者二维数组来保存,记录某个位置显示的图像可以通过图像代号来表示。在游戏初始时由程序载入砖块地图文件或者二位数组,并对文件中的信息或者二位数组逐行地进行分析,然后根据不同的图像代号来分别读取不同种类的砖块型图像
  • 例:将推箱子游戏每关的地图信息放入二位数组中,可以和网格对应。
  • 其中存储1代表pic1.jpg砖块型图像,存储2代表pic2.jpg砖块型图像,依次类推,存储0代表此处不绘制砖块型图像
static byte map[][] = {
	{0,0,1,1,1,0,0,0},
	{0,0,1,4,1,0,0,0},
	{0,0,1,9,1,1,1,1},
	{1,1,1,2,9,2,4,1},
	{1,4,9,2,5,1,1,1},
	{1,1,1,1,2,1,0,0},
	{0,0,0,1,4,1,0,0},
	{0,0,0,1,1,1,0,0}
}

游戏角色开发

  • 游戏角色也叫游戏精灵(Sprite),是指游戏中可移动的物体,主要包括玩家控制的角色和电脑控制的角色,如怪物,敌机等
  • 在游戏中精灵通常可以活动,往往需要通过连续地绘制多幅静态图像来表现其运动效果
  • 这里我们设计一个Sprite类,主要用来实现游戏里的人物动画和移动的效果。使用Sprite类可以读取一张张小图片,并把它们按照一定的顺序存储在数组中,然后在屏幕上显示出其中的一张小图片,如果连续地更换显示的小图片,则屏幕上表现为一个完整对的动画效果
  • 假如Sprite类中的人物动画由4帧状态图片构成,在绘制人物时,根据mPlayID的值绘制相应状态的图片
  • Sprite类中的坐标更新主要修改x坐标(水平方向)值,每次15个像素。当然可以修改y坐标(垂直方向)值,这里为了简化起见没修改y坐标值(垂直方向)
  • 下面的例子展示了如何显示一个精灵走动的动画,一个精灵从屏幕的左方往右方走动,显示精灵走动的SpritePanel类的代码如下所示:
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class MyWindow extends JFrame {
    public MyWindow(){
        this.setTitle("测试窗口");
        //获取面板容器
        Container c = this.getContentPane();
//        c.add(new TetrisPanel());
        c.add(new SpritePanel());
        //设置窗口开始显示时距离屏幕左边400个像素点,距屏幕上边200个像素点,窗口宽300个像素点,高300个像素点
        this.setBounds(400,200,300,500);
        //设置窗口大小不会改变
        this.setResizable(false);
        this.setVisible(true);
    }
    public static void main(String[] args){
        //创建该窗口的实例DB,开始整个程序
        MyWindow DB = new MyWindow();
        //添加窗口关闭出处理方法
        DB.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
               System.exit(0);
            }
        });
    }
}

import javax.swing.*;
import java.awt.*;
import java.awt.image.ImageObserver;

public class Sprite {
    public int m_posX = 0;  //Sprite类的x坐标
    public int m_poxY = 0;  //Sprite类的y坐标
    private Image pic[] = null; //Sprite类的图片数组
    private int mPlayID = 0;    //当前帧的ID
    boolean mFacus = true;  //是否更新绘制Sprite

    public Sprite(){
        pic = new Image[4];
        for(int i=0; i<4; i++){
            pic[i] = Toolkit.getDefaultToolkit().getImage("F:\\Pictures\\d" + i + ".jpg");
        }
    }
    /* 初始化坐标*/
    public void init(int x,int y){
        m_posX = x;
        m_poxY = y;
    }
    /*设置坐标*/
    public void set(int x,int y){
        m_posX = x;
        m_poxY = y;
    }
    /*绘制精灵*/
    public void DrawSprite(Graphics g, JPanel i){
            g.drawImage(pic[mPlayID], m_posX, m_poxY, (ImageObserver) i);
            mPlayID++;  //下一帧图像
            if (mPlayID == 4) {
                mPlayID = 0;
            }
    }
    /*更新精灵的坐标点*/
    public void UpdateSprite(){
        if(mFacus == true){
            m_posX += 10;
        }
        if(m_posX == 300){  //如果达到窗口右边缘
            m_posX = 0;
        }
    }
}

import javax.swing.*;
import java.awt.*;

/**
 * 绘图线程类
 */
public class SpritePanel extends JPanel implements Runnable{
    private Sprite player;

    public SpritePanel(){
        player = new Sprite();  //创建角色精灵
        Thread t = new Thread(this);  //创建一个新线程
        t.start();  //启动线程
    }

    @Override
    public void run() {
        while(true){
            player.UpdateSprite();  //更新角色Sprite类的x,y坐标
            try {
                Thread.sleep(100);  //线程休眠50ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            repaint();
        }
    }
    /*重载绘图方法*/
    public void paint(Graphics g){
        super.paint(g); //将面板上原来画的东西擦掉
        g.clearRect(0,0,this.getWidth(),this.getHeight());
        player.DrawSprite(g,this);  //绘制精灵
    }
}

游戏声音效果的设定

  • 游戏中的音乐能够丰富游戏的内涵,同时增强游戏的可玩性。
  • 游戏中的声音效果一般分为两类,分别是动作音效场景音乐。前者用于动作的配音,以便增强游戏角色的真实感;后者用于烘托游戏气氛,通过为不同的背景配备相对应的音乐来表达特定的情感。
  • Java提供了丰富的API用于对声音进行处理和播放,其中最常用的是一个使用数字化样本文件工作的包,专门用于载入声音文件并通过音频混合器进行播放。该包叫作javax.sound.sampled, 其所支持的声音文件格式有以下几种:AIFF、AU 和WAV 3 种。
  • Java中播放声音文件与显示图像文件一样方便,同样只需要先将声音文件装载进来,然后播放即可。
    下面以 WAV文件的播放为例来进行说明。首光需要打开声音文件并读取其中的信息,主要包括如下几个步骤。
    1.新建一个文件对象获取WAV文件数据
File file = new File("sound.wav");

2.将WAV文件转换为音频输入流

AudioInputStream stream = AudioSystem.getAudioInputStream(file);

3.获取音频格式

AudioFormat format = stream.getFormat();
  • 众所周知,Java通过流对象(Stream)来统一处理输入与输出数据,声音数据也不例外,处理声音数据输入的流类叫作 AudiolnputStream,它是具有指定音频格式和长度的输入流。
  • 除此之外,还需要使用AudioSystem类,该类用于充当取样音频系统资源的入口点,提供许多在不同格式间转换音频数据的方法,以及在音频文件和流之间进行转换的方法。
  • AudioFormat 类是在声音流中指定特定数据安排的类。通过检查以音频格式存储的信息,可以发现在二进制声音数据中解释位的方式。
  • 获取声音文件信息之后接下来就能够对声音数据进行播放了,主要包括如下几个步骤。
    1.设置音频行信息
DataLine.Info info = new DataLine.Info(Clip.class,format);

2.建立音频行

Clip clip = (Clip)AudioSystem.getLine(info);

3.将音频数据流读入音频行

clip.open(stream);

4.播放音频行

clip.start();

其中涉及的类包括如下几个。

  • Line:Line接口表示单声道或多声道音频供给。
  • DataLine:包括一些音频传输控制方法,这些方法可以启动、停止、消耗和刷新通过数据行传入的音频数据。
  • DataLine.Info:提供音频数据行的信息。包括受数据行支持的音频格式、其内部缓冲区的最小和最大值等。
  • Clip接口:表示特殊种类的数据行,该数据行的音频数据可以在回放前加载,而不是实时流出。
  • 音频剪辑的回放可以使用start()和stop()方法开始和终止。这些方法不重新设置介质的位置,start()方法的功能是从回放最后停止的位置继续回放。
  • 下面建立SoundPlayer类来播放声音效果,代码如下:
import javax.sound.sampled.*;
import java.io.File;
import java.io.IOException;

public class SoundPlayer {
    File file;
    AudioInputStream stream;
    AudioFormat format;
    DataLine.Info info;
    Clip clip;

    SoundPlayer(){

    }
    /*打开声音文件*/
    public void loadSound(String fileName){
        file = new File(fileName);
        try {
            stream = AudioSystem.getAudioInputStream(file);
        } catch (UnsupportedAudioFileException e){
            e.printStackTrace();
        } catch (IOException e){
            e.printStackTrace();
        }
        format = stream.getFormat();
    }

    /*播放声音*/
    public void playSound(){
        info = new DataLine.Info(Clip.class,format);
        try {
            clip = (Clip)AudioSystem.getLine(info);
            clip.open(stream);
        } catch (LineUnavailableException e) {
            e.printStackTrace();
        } catch(IOException e){
            e.printStackTrace();
        }
        clip.start();
    }
}

给角色动作添加动作音乐,在用户按空格键跳跃时播放音效,代码如下:

public class GamePanel extends Panel implements Runnable,KeyListener{
	SoundPlayer sound = new SoundPlayer();
	public void keyPressed(KeyEvent e){
		int keycode = e.getKeyCode();
		switch(keycode){
		case KeyEvent.VK_DOWN:
		break;
		case KeyEvent.VK_SPACE:
		sound.loadSound("Sound/jump.wav");
		sound.playSound();
		}
	}
}

推箱子游戏

1.推箱子游戏介绍

  • 经典的推箱子游戏是一款来自日本的古老游戏,目的是训练玩家的逻辑思考能力。在一个狭小的仓库中,要求把木箱放到指定的位置,稍不小心就会出现箱子无法移动或者通道被堵住的情况,因此要求玩家巧妙地利用有限的空间和通道,合理安排移动的次序和位置,才能顺利地完成任务。

2.推箱子游戏的资源和功能

  • 运行游戏载入相应的地图,屏幕中出现一名推箱子的工人,其周围是围墙、人可以走的通道、几个可以移动的箱子和箱子放置的目的地。
  • 玩家通过按上下左右键控制工人推箱子,当所有箱子都推到了目的地后出现过关信息,并显示下一关。如果推错了,玩家通过单击鼠标右键可以撤销上次的移动操作,还可以按空格键重新玩这一关,直到通过全部关卡。
    在这里插入图片描述
    在这里插入图片描述
  • 图片资源说明:
    • pic1:墙;pic2:箱子;pic3:箱子在目的地上;pic4:目的地
    • pic5:向下的人;pic6:向左的人;pic7:向右的人;pic8:向上的人
    • pic9:通道;pic10:站在目的地向下的人;pic11:站在目的地向左的人;pic12:站在目的地向右的人;pic13:站在目的地向上的人

3.程序设计的思路

  • 首先我们来确定一下游戏开发的难点。对工人的操作很简单,就是4个方向的移动,工人移动,箱子也随之移动,因此对按键的处理也比较简单。当箱子到达目的地位置时,就会产生游戏过关事件,需要一个逻辑判断。
  • 那么我们仔细想一下,所有这些事件都发生在一张地图中。这张地图包括箱子的初始化位置、箱子最终放置的位置和围墙障碍等。每一关地图都要更换,这些位置也要变。因此我们发现每关的地图数据是最关键的,它决定每关的不同场景和物体位置。那么我们就重点分析一下地图。
  • 我们把地图想象成一个网格,每个格子就是工人每次移动的步长(这里为30像素),也是箱子移动的距离,这样问题就简化多了。
  • 首先我们设计一个mapRow×mapColumn的二维数组map。按照这样的框架来思考,对于格子的(x,y)两个屏幕像素坐标,可以由二维数组下标(i,j)换算得到。
  • 换算公式为:
left× + j × 30, leftY +30
  • 每个格子状态值分别用枚举类型值:
//定义一些常量,对应地图的元素
final byte WALL = 1;	//墙
final byte BOX = 2;	//箱子
final byte BOXONEND = 3;	//放到目的地的箱子
final byte END = 4;	//目的地
final byte MANDOWN = 5;	//向下的人
final byte MANLEFT = 6;	//向左的人
final byte MANRIGHT = 7;	//向右的人
final byte MANUP = 8;	//向上的人
final byte GRASS = 9;	//通道
final byte MANDOWNONEND = 10;	//站在目的地向下的人
final byte MANLEFTONEND = 11;	//站在目的地向左的人
final byte MANRIGHTONEND = 12;	//站在目的地向右的人
final byte MANUPONEND = 13;	//站在目的地向上的人
  • 存储的原始地图中格子的状态值数组采用相应的整数形式存放
  • 在玩家通过键盘控制工人推箱子的过程中,需要按游戏规则进行判断是否响应该按键指示。下面分析一下工人将会遇到什么情况,以便归纳出所有的规则和对应算法。为了描述方便,假设工人移动趋势的方向向右,其他方向原理是一致的。P1、P2分别代表工人移动趋势方向前的两个方格。
    在这里插入图片描述
  • 1.前方P1是围墙(WALL)
    如果工人前方是围墙(即阻挡工人的路线){
    退出规则判断,布局不做任何改变;
    }
    
  • 2.前方P1是通道(GRASS)或目的地(END)
    如果工人前方是通道或目的地{
    工人可以前进到P1方格;修改相关位子格子的状态值
    }
    
  • 3.前方P1是箱子(BOX)
    • 在前面两种情况中,只要根据前方P1处的物体就可以判断出工人是否可以移动;
    • 而在第三种情况中,需要判断箱子前方P2处的物体才能判断出工人是否可以移动
      • 1.P1处为箱子(BOX)或者放到目的地的箱子(BOXONEND),P2处为通道(GRASS);
      工人可以进到P1方格;P2方格状态为箱子。修改相关位置格子的状态值
      
      • 2.P1处为箱子(BOX)或者放到目的地的箱子(BOXONEND),P2处为目的地(END)
      工人可以进到P1方格;P2方格状态为放置好的箱子。修改相关位置格子的状态值
      
      • 3.P1处为箱子(BOX)或者放到目的地的箱子(BOXONEND),P2处为墙(WALL)
      退出规则判断,布局不做任何改变
      
  • 整个游戏的源文件说明:
GameFrame.java:游戏界面视图
Map.java:封装游戏的当前状态
MapFactory.java:提供的地图数据

4.设计地图数据类

  • 地图数据类保持所有关卡的原始地图数据,每关数据为一个二维数组,因此此处map是一个三维数组
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值