Java游戏编程不完全详解-2(1万2千字吐血推荐)

Java游戏编程不完全详解-2(1万2千字吐血推荐)

封面11 拷贝.png

作者:老九—技术大黍

原文:Developing Games in Java

社交:知乎

公众号:老九学堂(新人有惊喜)

特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系笔者授权

前言

代码演示环境

  • 软件环境:Windows 10
  • 开发工具:Visual Studio Code
  • JDK版本:OpenJDK 15

虽然这些代码是10几年前的写的,但是仍然能够在现代操作系统和Java最新开源版本中正常运行。

三种Java游戏类型

使用Java我们可以创建三种类型的游戏:applet游戏,窗体游戏和全屏幕游戏。

  • applet游戏—是运行在浏览器中的应用。它的好处理用户不需要安装应用。但是用户必须安装JRE并且必须在web浏览器中运行。另外,applet小程序还有安全限制,以保证它不恶意破坏本地代码。比如applet程序不能把游戏保存到用户机的硬盘中去。它只能通过网络连接一个服务器(我在“Java多线程第2版不完全详解”一文中提到的国内最早的MMORPG的页游《倾城》的客户端就是使用applet写的)
  • 窗体游戏—该类型的游戏没有applet流程的安全限制,它与普通的应用一样,有标量栏、关闭按钮等。但是它不吸收用户,特别是当我们沉浸在游戏中时。
  • 全屏幕游戏—没有桌面元素,比如标题栏、任务栏和菜单栏,这样玩家可完全沉浸在游戏情节当中。

全屏幕绘图

在计算机中有两部分显示硬件:显卡和显示器。显卡保存屏幕的内容,这些内容是在显卡的内存中存在的,它会呼叫一些函数来修改显示内容,另外显卡在显示器背后工作,它把内存中的内容push到显示器来呈现。而显示器只是简单的呈现显卡告诉它的内容。

屏幕布局—显示器屏幕被分解大小相等的像素(color pixel)。像素来自术语图片元素(picture element)的概念,它是由显卡显示的单个亮点。水平和垂直的像素组成了屏幕(screen)布局。

屏幕的原点是屏幕的左上角,像素存贮在显卡的内存中,它从左上角开始从左到右读,从上到下读取。屏幕中的位置表示 (x,y)座标,x表示从原点开始的水平方向的像素个数,y表示从原点开始的垂直方向的像素个数。

image-20210326153901996.png

屏幕显示的效果依赖于显卡和显示器的能力,一般解决方案有640x480, 800x600, 1024x768, 和 1280x1024布局方式。

一般显示器的尺寸比率是4:3,这表示高度显示是宽度的四分之三。一般宽屏使用16:9的比率。老式的CRT显示器可完成实现以上策略,因为它使用电子光栅来表示像素。现代的LCD显示器,因为它比电子管亮,所以它有自己的显示策略,它的失真比较明显,所以我们的游戏必须保证两种或者三种显示策略中能够运行。

像素颜色和位层次(Bit Depth)

我们都知道三种基本颜色:红、黄和兰色。黄色+兰色=绿色,三种颜色的不同组合会产生自己想要的颜色,去掉所有的色值就是白色。同样显示也使用这三种基色来产生需要的颜色,显示器发亮,所以RGB颜色模型是一个加法模型,也就是添加所有的色值就会产生白色。显示器的颜色值依赖于位层次(bit depth)来表示色值,一般位层次分为8, 15, 16, 24和32位。

  • 8位颜色有2的8次方为256颜色,也就是一次只能显示256种颜色,这些颜色基于颜色面板。

    刷新率(Refresh Rate)—虽然我们的显示器看起来像是显示一个固定的图片,每个像素实际上会在几毫秒中消失。所以显示器会不间断的刷新以弥补像素消失效果。那么刷新的频率就是刷新率,单位是Hertz—即一秒钟中之内重复的次数。

    Now that you know all about resolutions, bit depths, and refresh rates

    不幸的是,当前的Java版本(它指的是JDK 1.4版本)不能修改调色板(我没有试过更高版本的,因为我开游戏客户端是使用C++来实现的,因为实际开发中不用的话,我就不研究image-20210325194659853.png),也不能描述这些是什么。Java运行时可以使用一个web-safe调色板来表示颜色:对于红绿兰色每种都6个色值(6x6x6=216 )。

  • 15位的红绿兰有2的15次方值32,768颜色

  • 16位的红绿兰有2的15次方值65,536颜色

  • 24位红绿兰有2的15次方值16,777,216颜色

  • 32位颜色与24一样,但是有8位的填充像素

大多数现代的显卡支持8位、16位和32位模型,因为人类的眼睛可以看到1千万种颜色,24位是理想的,16位会比24位显示速度快,但是质量不好。

控制全屏幕显示模型

Window对象—Window对象是被显示屏幕的抽象。我们可以把它想像成一个画布,在Java的API中是使用JFrame来抽象表示的,该类是Window类(Window 对象是一个没有边界和菜单栏的顶层窗口。窗口的默认布局是 BorderLayout)。JFrame是Window类的子类,它可以被使用在窗体应用中。

  • DisplayMode对象—显示模型对象指定屏幕显示策略、位层次和显示的刷新率
  • GraphicsDevice对象—图形设备对象可以修改显示模型,我们可把它当成显卡来使用。该对象从GraphicsEnvironment对象获取。

SimpleScreenManager类

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

/**
	功能:书写一个实现全屏幕显示的类
	作者:技术大黍
	*/
public class SimpleScreenManager{
	//声明一个显卡对象--该类是描述了特定图形环境中使用的图形设备。这些设备包括屏幕和打印设备。
	//因为GraphicsEnviornment实例中可以有许多屏幕和打印机,所以每个图形设备有一个或者多个与之
	//关联的GraphicsConfiguration对象。这些配置对象可以指定GraphicsDevice对象的不同配置。
	//在多屏幕环境中,GraphicsConfiguration对象可以用于多个屏幕上的组件呈现。
	private GraphicsDevice device;
	
	/**
		在构造方法中初始化成员变量
		*/
	public SimpleScreenManager(){
		//GraphicsEnvironment类描述了应用程序在特定平台上可以的GraphicsDevice对象和Font对象集合
		//因此该资源可以是本地资源,也可以位于远程机器上的资源。GraphicsDevice对象可以屏幕、打印
		//机或者图像缓冲区,并且都是Graphics2D对象的绘制目标。每个GraphicsDevice都有许多与之相关的
		//GraphicsConfiguration对象,这些配置对象指定了GraphicsDevice所需的不同配置。
		GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
		device = environment.getDefaultScreenDevice();
	}
	
	/**
		功能:公开一个设置指定窗体的全屏幕显示模型的方法--该是方法是一个控制是否全屏幕核心方法
			  不使用双缓存显示策略来显示图片。
		参数:DisplayMode displayMode是显示模型
			  JFrame window是被设置的窗体对象
		*/
	public void setFullScreen(DisplayMode displayMode, JFrame window){
		window.setUndecorated(true);//不需要装饰区域
		window.setResizable(false); //不允许缩放
		//把window对象作为参数传给显卡对象--告诉显卡在显示时使用全屏幕模型
		//注意:GraphicsDevice的setFullScreenWindow是执行全屏幕显示的语句
		//当GraphicsDevice设置完了window对象之后,它会呼叫window的paint()方法
		//setFullScreenWindow方法会让当前屏幕显示进入全屏幕模型,或者返回容器化模型状态。
		//进入全屏幕模型是独占的,也可以模拟的。只有isFullScreentSupported返回true值,
		//这时电脑屏幕才会进入独占模型。独占模型意味着:(1)Windows无法重叠全屏幕窗口,因此
		//当已存在全屏幕窗口时,再调用此方法会导致现面的全屏幕窗口返回窗口化模型!(2)禁用
		//输入方法窗体,只能呼叫Component.enableInputMethods(false)方法可以让一个非客户组件
		//作为输入方法窗体使用。如果w作为全屏幕窗口,那么当设置w为null时返回窗口化模型。如果
		//一些平台希望全屏幕窗口成为顶层组件(Frame),那么最好使用java.awt.Frame类,而不JFrame类。
		device.setFullScreenWindow(window);
		//如果显示模型不为null,并且显卡可以支持显示该屏幕
		if(displayMode != null && device.isDisplayChangeSupported()){
			try{
				device.setDisplayMode(displayMode);//那么设置显示模型
			}catch(IllegalArgumentException ex){
				ex.printStackTrace();//否则显示异常信息
			}
		}
	}
	
	/**
		功能:返回当前的全屏幕的窗体模型对象
		*/
	public Window getFullScreenWindow(){
		return device.getFullScreenWindow();
	}
	
	/**
		功能:恢复屏幕的显示模型
		*/
	public void restoreScreen(){
		Window window = device.getFullScreenWindow();
		//如果window不为null
		if(window != null){
			window.dispose(); //那么让窗体释放占有的资源
		}
		device.setFullScreenWindow(null);//把窗体设置为窗体模型null
	}
	
	//使用Graphics2D来渲染显示文字
	public Graphics getAntiAliasing(Graphics g){
		if(g instanceof Graphics2D){
			Graphics2D g2 = (Graphics2D)g;
			g2.setRenderingHint(
				RenderingHints.KEY_TEXT_ANTIALIASING,
				RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		}
		return g;
	}
}

SimpleScreenManagerTest类

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

/**
	功能:书写一个实现全屏幕显示的类
	作者:技术大黍
	*/
	
public class SimpleScreenManagerTest extends JFrame{
	private DisplayMode displayMode;
	private static final long DEMO_TIME = 5000;
	
	public SimpleScreenManagerTest(String[] args){
		//设置指定的显示模型
		if(args.length == 3){
			displayMode = new DisplayMode(
				Integer.parseInt(args[0]),
				Integer.parseInt(args[1]),
				Integer.parseInt(args[2]),
				DisplayMode.REFRESH_RATE_UNKNOWN
			);
		}else{//否则使用默认显示模式
			displayMode = new DisplayMode(800,600,16,DisplayMode.REFRESH_RATE_UNKNOWN);
		}
		//运行全屏幕显示
		run(displayMode);
	}
	
	private void run(DisplayMode displayMode){
		setBackground(Color.red);
		setForeground(Color.white);
		setFont(new Font("Dialog",Font.PLAIN, 24));
		//使用全屏幕管理器
		SimpleScreenManager screen = new SimpleScreenManager();
		try{
			//呼叫它的setFullScreen方法,让它把当前的屏幕变成全屏幕--核心代码就是这里
			screen.setFullScreen(displayMode, this);
			//getDefaultWindow();
			try{
				Thread.sleep(DEMO_TIME);
			}catch(InterruptedException ex){
				ex.printStackTrace();
			}
		}finally{
			screen.restoreScreen();
		}
	}
	
	/**
		功能:重写paint方法--绘制容器。该方法将 paint 转发给任意一个此容器子组件的轻量级组件
		在窗体中显示字符串。在显示全屏幕之后,在屏幕中绘制文字!
		Graphics类是图形上下文的抽象基类,它允许应用程序组件,以及闭屏图像上进行绘制。该类封装了
		Java支持的基本呈现操作所需要的状态信息:
		1、需要在其上绘制的Component对象
		2、呈现和剪贴坐标的转换原点
		3、当前的剪贴区
		4、当前的颜色
		5、当前的字体
		6、当前的逻辑像素操作函数(XOR或者Paint)
		7、当前XOR交替颜色
		坐标是无限细分的,并且位于输出设备的像素之间。绘制图形轮廓的操作是通过使用像素大小的画笔
		遍历像素间无限细分路径的操作,画笔从路径上的锚点向下和向右绘制,填充图形的操作是填充图形
		内部区域无限细分路径操作。呈现水平文本的操作是呈现字符字形完全位于基线坐标之后的上升部分。
		图形画笔从要遍历的路径向下和向右绘制的含义如下:
		1、如果绘制一个覆盖给定矩形的图形,那么该图形与填充被相同矩形所限定的图形相比,在右和底边
		   多占一和像素
		2、如果沿着与一行文本基线相同的y坐标绘制一条水平线,那么除了文字的所有下降部分外,该线完全
		   在文本的下面。
		所有作为此Graphics对象方法的参数而出现的坐标,都是相对于调用该方法前的此Graphics对象转换原
		点的。所有呈现操作仅修改当前剪贴区域内的像素,此剪贴区域是由空间中的shape指定的,并且通过使
		用Gaphics对象的程序来控制。此用户剪贴区被转换到设备空间中,并与设备剪贴区组合。后者是通过窗
		口可见性和设备范围定义的。用户剪贴区和设备剪贴区的组合定义为复合剪贴区,复合剪贴区确定最终
		的剪贴区域。用户剪贴区不由呈现系统修改,所以得由复合剪贴区来修改。在用户剪贴区只通过setClip
		和clipREct方法修改。所有的绘制或写入都以当前的颜色、当前绘图模型和当前字体完成。
	*/
	public void paint(Graphics g){
		g = new SimpleScreenManager().getAntiAliasing(g);
		g.drawString("Hello World!(你好: Java!) ^_^", 20, 50);
		repaint();
	}
	
	private void getDefaultWindow(){
		//使用JFrame封装的行为来显示窗体
		setSize(200,200);
		Container container = getContentPane();
		container.add(new JButton("测试第二个"),BorderLayout.SOUTH);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setVisible(true);
	}
	
	public static void main(String[] args){
		new SimpleScreenManagerTest(args);
	}
}

SimpleFullScreentTest类是使用try/finally块来完成全屏幕的显示,在finally语句块中恢复窗体显示模型,如果本地没有显卡没有恰当的显示模型支持,那么抛出异常。

另外,在Graphics对象在paint方法中使用,该对象提供所有功能:绘制文本、线条、矩形、椭圆、多边形、图形等。大多数方法都的自明的(self-explanatory),所以,如果需要使用,可以查看Java API规范来使用。

那么paint方法是怎样被呼叫呢?当JFrame被显示时,Java的Abstratct Window Toolkit会呼叫组件的paint方法。如果需要强制呼叫paint方法,那么需要我们呼叫repaint方法即可,因为该方法会给AWT一个信号,然后让AWT来呼叫paint方法。AWT会发送paint事件在两个线程,所以如果我们需要发送repaint事件,并且等等绘制完成,那么应该这样书写:

public class MyComponent extends SomeComponent{
   …
   public synchronized void repaintAndWait(){
	repaint();
	try{
		wait();
	}catch(InterruptedException ex){}
    }
   public synchronized void paint(){
	//do painting here
         …
        notifyAll();//通知等待的线程
        ...
   }
}

在VS Code中的运行效果

image-20210326163122084.png

在SimpleFullScreenTest中的字样有锯齿状态。

如果希望显示字符比较平滑,那么需要使用Graphics2D对象来处理。

image-20210326163719686.png

image-20210326163801910.png

图片

除了在屏幕绘制文本以外,我们还会在屏幕中绘制图片,绘制图片需要我们知道两个基本概念:透明度类型和文件格式。 图片的背景依赖于图片的透明度来表示,我们可以使用三种图片透明度:不透明(opaque)、透明(transparent)和半透明(translucent):

  • opapque—图片中的每个像素都是可见的
  • transparent—图片中像素中完成可见或者可以看穿透的。对于白色背景在透明时,可以从它看到它下面的像素
  • translucent—半透明,它用于一个图片的边缘和Anti-aliasing图片

文件格式

图片格式有两种基本类型:raster(光栅)和vector(矢量)。光栅类型使用像素来描述图片;矢量图片格式描述几何图形,它可以缩放后不会变形。 Java运行时有三种内置的光栅格式:GIF, PNG和JPEG.

  • GIF—该格式图片可以不透明或者透明,它是8位颜色
  • PNG—该格式可以有不透明、透明和半透明,它支持24位颜色
  • JPEG—只能是不透明,它是24位图片

读取图片

读取图片我们使用ToolKit类的getImage()方法,它解析文件,然后返回Image对象:

Toolkit toolkit = Toolkit.getDefaultToolkit();
Image image = toolkit.getImage(fileName);

上面的代码并不是实际装载该图片,该图片只是被装载另外一个线程中去了。我们可以使用MediaTracker对象来检查该图片,并且等待它装载完毕,但是我们还有更简单化的解决方案—使用ImageIcon类,该类使用MediaTracker来帮助我们装载图片。

ImageIcon icon = new ImageIcon(fileName);
Image image = icon.getImage();

ImageFrameTest类

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

/**
	功能:书写一个测试类用来说明怎样装载图片
	作者:技术大黍
	*/
public class ImageFrameTest extends JFrame{
	/*
		DisplayMode类封装了GraphicsDevice的位深、高度、宽度和刷新频率。修改图形设置的显示模式
		的能力是与平台和配置有关的。可能并不总是可用的。
		*/
	private DisplayMode displayMode;
	private static final int FONT_SIZE = 24;
	private static final long DEMO_TIME = 20000;
	private SimpleScreenManager screen; //使用图片管理器
	private Image bgImage; //表示背景图片--其中抽象类Image是表示图形图像
	//的所有类的超类,必须特定于平台的方式获取图像
	private Image opaqueImage; //表示不透明图片
	private Image transparentImage; //表示透明图片
	private Image translucentImage; //表示半透明图片
	private Image antiAliasedImage; //表示反锯齿状图片
	private boolean imagesLoaded; //图片是否装载完成
	
	
	private void run(DisplayMode displayMode){
		setBackground(Color.blue);
		setForeground(Color.white);
		setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE));
		imagesLoaded = false;
		
		//使用屏幕管理器
		screen = new SimpleScreenManager();
		try{
			screen.setFullScreen(displayMode, this);
			//注意这里是关键代码
			loadImages();//装载图片
			try{
				Thread.sleep(DEMO_TIME);//让主线程睡觉2分钟
			}catch(InterruptedException ex){
				ex.printStackTrace();
			}
		}finally{
			screen.restoreScreen();
		}
	}
	
	/*
		装载图片之后,呼叫repaint方法,让swing框架呼叫paint方法,以便显示出来。
		*/
	private void loadImages(){
		bgImage = loadImage("images/background.jpg");
		opaqueImage = loadImage("images/opaque.png");
		transparentImage = loadImage("images/transparent.png");
		translucentImage = loadImage("images/translucent.png");
		antiAliasedImage = loadImage("images/antialiased.png");
		imagesLoaded = true;
		//装载完成之后让AWT刷新画面重新paint一次当前画面
		repaint();
	}
	
	/**
		绘制该容器(JFrame对象)。该方法将paint转发给任意一个此容器组件的轻量级组件,如果重新实现此
		方法,那么JVM应该调用super.paint(g)方法,从而正确呈现轻量级组件。如果通过g中的当前剪切设置
		完全剪切某个子组件,那么不会将paint转换给这个子组件。
		*/
	public void paint(Graphics g){
		g = screen.getAntiAliasing(g);
		//绘制图片,如果装载完毕
		if(imagesLoaded){
			//那么在屏幕中绘制出来
			g.drawImage(bgImage,0,0,null);
			drawImage(g,opaqueImage,0,0,"不透明");
			drawImage(g,transparentImage,320,0,"透明");
			drawImage(g,translucentImage,0,300,"半透明");
			drawImage(g,antiAliasedImage,320,300,"半透明(Anti-Aliased)");
		}else{
			//否则显示等信息
			g.drawString("装载图片...", 5, FONT_SIZE);
		}
	}
	
	/*
		具体呼叫Graphics的drawImage方法来绘制图形到屏幕中。其中Graphics的drawImage方法是用来
		绘制指定图像中当前可用的图像,图像的左上角位于该图形上下文坐标空间的(x,y)。图像中的透明
		像素不处已存在的像素,此方法在任何情况下都立刻返回,甚至在图像尚未完整加载的情况,并且
		还没有针对当前输出设备完成抖动和转换的情况也是如此。如果图像已经完整加载,并且其像素不
		再发生更改,那么drawImage返回true值;否则drawImage返回false值,并且随着更多的图像可以用
		或者到了绘制动画另一帧的时候,加载图像的进程将通知指定的图像观察者。
		*/
	private void drawImage(Graphics g, Image image, int x, int y, String caption){
		g.drawImage(image,x,y,null);
		g.drawString(caption,x + 5, y + FONT_SIZE + image.getHeight(null));
	}
	
	/*
		呼叫ImageIcon对象的getImage方法初始化Image对象。因为ImageIcon是一个Icon接口的实现类,它
		根据Image来绘制Icon对象。我们可以使用MediaTracker对象来跟踪该图像的加载状态。注意:该类
		序列化对象与以后的Swing版本不兼容。
		*/
	private Image loadImage(String fileName){
		return new ImageIcon(fileName).getImage();
	}
	
	/**
		在构造方法初始化成员变量
		*/
	public ImageFrameTest(String[] args){
		if(args.length == 3){
			displayMode = new DisplayMode(
				Integer.parseInt(args[0]),
				Integer.parseInt(args[1]),
				Integer.parseInt(args[2]),
				DisplayMode.REFRESH_RATE_UNKNOWN
				);
		}else{
			displayMode = new DisplayMode(1024,768,16,DisplayMode.REFRESH_RATE_UNKNOWN);
		}
		//执行显示方法
		run(displayMode);
	}
	
	public static void main(String[] args){
		new ImageFrameTest(args);
	}
}

运行效果

image-20210326164640642.png

关键代码

关键代码是让主线程等待2分钟,系统去装载图片,并且在装载之后重新刷新画面,让图片显示出来。

image-20210326164727777.png

硬件加速图片显示

硬件加速图片显示(hardware-accelerated image)是图片被存贮在显示内存,而不是系统内存中,所以使用硬件加速的图片显示速度非常快。使用Toolkit的getImage()方法,Java使用主动使用硬件加速图片功能,所以我们不必担心图片的硬件加速问题。

只是使用我们需要注意:

  • 如果我们连续不断的修改图片显示内容,那么Java不使用加速功能
  • JDK 1.4.1不加速半透明图片显示,只加速不透明和透明
  • 不是每个操作系统都具有硬件加速功能

如果我们需要强制使用硬件的图形加速显示功能,那么我们需要使用VolatileImage类来完成。因为VolatileImage是被存贮在显存中的图形。该类的对象使用Component的createVolatileImage方法创建,或者使用GraphicsConfiguration的createCompatibleVolatileImage方法来创建。这种类型的图片只能是不透明的,因为它是volatile的图片,所以需要我们经常重绘这种类型的图片。

我们可以使用validate()和contentsLost()方法来判断显示的图片内容是否有丢失。前者方法可以判断图片是否与当前的显示模型匹配;后者返回显示的图片内容是否有丢失。下面是示例代码:

import static java.lang.System.*;
import java.awt.*;
import javax.swing.*;

/**
	功能:书写一个类用来演示怎样使用硬件加速
	作者:技术大黍
	备注:
		  没有使用强制模型来实现硬件加速显示。
	*/
public class ImageSpeedFrameTest extends JFrame{
	private DisplayMode displayMode;
	private static final int FONT_SIZE = 24;
	private static final long TIME_PER_IMAGE = 2500;
	private SimpleScreenManager screen;
	private Image bgImage;
	private Image oqaqueImage;
	private Image transparentImage;
	private Image translucentImage;
	private Image antiAliasedImage;
	private boolean imagesLoaded;
	
	private void run(DisplayMode displayMode){
		setBackground(Color.blue);
		setForeground(Color.white);
		setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE));
		imagesLoaded = false; //标识该图片是否已经被装载
		
		//使用SimpleScreenManager类封装过的方法--实现全屏幕显示的功能
		screen = new SimpleScreenManager();
		try{
			screen.setFullScreen(displayMode,this);
			synchronized(this){//注意这里需要同步
				loadImages(); //装载图片
				try{
					wait(); //让主线程等待IO操作完毕
				}catch(InterruptedException ex){
					ex.printStackTrace();
				}
			}
		}finally{
			screen.restoreScreen();//恢复显示状态
		}
	}
	
	//让系统装载需要的图片
	private void loadImages(){
		bgImage = loadImage("images/background.jpg");
		oqaqueImage = loadImage("images/funfreebird.png");
		transparentImage = loadImage("images/cat.png");
		translucentImage = loadImage("images/books.png");
		antiAliasedImage = loadImage("images/apple.png");
		imagesLoaded = true;
		//让AWT重绘窗体
		repaint();//让系统呼叫paint方法来显示图形
	}
	
	private final Image loadImage(String fileName){
		return new ImageIcon(fileName).getImage();
	}
	
	public void paint(Graphics g){
		//要求显示时反锯齿功能显示
		g = screen.getAntiAliasing(g);
		if(imagesLoaded){//如果图片装载到内存完毕
			//那么才能绘制该图片到屏幕上
			drawImage(g,oqaqueImage,"不透明");
			drawImage(g,transparentImage,"透明");
			drawImage(g,translucentImage,"半透明");
			drawImage(g,antiAliasedImage,"半透明(anti-aliased)");
			
			//绘制完成之后,通知其它线程显示完毕,然后让CPU唤醒这些线程。
			synchronized(this){
				notify();
			}
		}else{
			g.drawString("装载图片中。。。", 5, FONT_SIZE);
		}
	}
	
	/*
		快速显示图形的核心代码。在2.5秒之内快速绘制图片在屏幕中
		*/
	private void drawImage(Graphics g, Image image, String name){
		int width = screen.getFullScreenWindow().getWidth() - image.getWidth(null);
		int height = screen.getFullScreenWindow().getHeight() - image.getHeight(null);
		int numImages = 0;
		g.drawImage(bgImage,0,0,null);
		
		long startTime = currentTimeMillis();
		//如果有限定的时间长度2.5秒之内
		while(currentTimeMillis() - startTime < TIME_PER_IMAGE){
			int x = Math.round((float)Math.random() * width);
			int y = Math.round((float)Math.random() * height);
			out.println("当前x = " + x + ", y = " + y);
			//随机绘制图形
			g.drawImage(image,x,y,null); 
			numImages++;
		}
		long time = currentTimeMillis() - startTime;
		float speed = numImages * 1000f / time;
		out.println(name + ": " + speed + " 图片/秒");
	}
	
	public ImageSpeedFrameTest(String[] args){
		if(args.length == 3){
			displayMode = new DisplayMode(
				Integer.parseInt(args[0]),
				Integer.parseInt(args[1]),
				Integer.parseInt(args[2]),
				DisplayMode.REFRESH_RATE_UNKNOWN
			);
		}else{
			displayMode = new DisplayMode(800,600,16,DisplayMode.REFRESH_RATE_UNKNOWN);
		}
		//运行
		run(displayMode);
	}
	
	public static void main(String[] args){
		new ImageSpeedFrameTest(args);
	}
}

不过以上代码OpenJDK 15运行失败了

image-20210326170356075.png

之前使用JDK 1.4版本运行成功的。

image-20210326170415279.png

各位可以换成JDK 8来试一下。

动画

动画中的图片可以被看成帧(frame),每一帧在一个确定的时间中显示,但是在相同的时间内部中帧不需要显示。比如第一帧可能显示200毫秒,第二帧显示75毫秒等。

image-20210326170447836.png

  • mage--图片
  • Time--时间
  • millisecond--毫秒

动画循环

动画效果是由循环显示图片呈现出来的,这种循环就是动画循环。动画循环遵守步骤如下:

  • Updates any animations--更新动画
  • Draws to the screen--绘制到屏幕
  • Optionally sleeps for a short period--确定每个动作的睡觉时间
  • Starts over with step 1--回到第一步循环

我们整个代码实现的结构图片如下:

image-20210326171534535.png

无双缓存实现动画

import static java.lang.System.*;
import java.awt.*;
import javax.swing.*;
/**
	功能:书写一个测试动画循环的类
	作者:技术大黍
	备注:
		  没有实现双缓存技术
	*/

public class AnimationFrameTestOne extends JFrame{
	private DisplayMode displayMode;
	private static final long DEMO_TIME = 5000;
	private SimpleScreenManager screen;
	private Image bgImage;
	private Animation animation;
	
	
	public AnimationFrameTestOne(String[] args){
		if(args.length == 3){
			displayMode = new DisplayMode(
				Integer.parseInt(args[0]),
				Integer.parseInt(args[1]),
				Integer.parseInt(args[2]),
				DisplayMode.REFRESH_RATE_UNKNOWN
			);
		}else{
			displayMode = new DisplayMode(800,600,16,DisplayMode.REFRESH_RATE_UNKNOWN);
		}
		run(displayMode);
	}
	
	private void run(DisplayMode displayMode){
		screen = new SimpleScreenManager();
		try{
			screen.setFullScreen(displayMode, new JFrame());
			loadImages();//装载图片
			animationLoop();//实现动画
		}finally{
			screen.restoreScreen();
		}
	}
	
	private void loadImages(){
		bgImage = loadImage("images/background.jpg");
		Image player1 = loadImage("images/player1.png");
		Image player2 = loadImage("images/player2.png");
		Image player3 = loadImage("images/player3.png");
		//创建动画--一张图片使用一个ArrayList中的AnimationFrame类,该类
		//使用Image属性和long来保存图片对象,以及图片显示结束时间。
		animation = new Animation();
		animation.addFrame(player1,250);
		animation.addFrame(player2,150);
		animation.addFrame(player1,150);
		animation.addFrame(player2,150);
		animation.addFrame(player3,200);
		animation.addFrame(player2,150);
	}
	
	//读取图片
	private Image loadImage(String fileName){
		return new ImageIcon(fileName).getImage();
	}
	
	/**
		功能实现动画效果:
		1、更新动画
		2、绘制到屏幕
		3、确定睡觉的时间
		4、回到第一步循环
		*/
	private void animationLoop(){
		//获取动画开始时间
		long startTime = currentTimeMillis();
		//把它设置为当前时间
		long currentTime = startTime;
		//如果流逝的时间小于指定的时间
		while(currentTime - startTime < DEMO_TIME){//在指定的总体动画时间中
			long elapsedTime = currentTimeMillis() - currentTime;
			currentTime += elapsedTime;
			//1. 根据指定的elapsedTime来决定更换显示显示的图片,从而达到更新动画的效果。
			animation.update(elapsedTime);
			//2. 绘制到屏幕
			Graphics g = screen.getFullScreenWindow().getGraphics();
			draw(g);//这里是核心代码,把图片绘制在屏幕中。
			g.dispose();//释放资源
			//3. 暂停一下
			try{
				Thread.sleep(20);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
	}
	
	/*
		功能:该方法是核心方法,它主要完成两个功能:
		1、绘制背景图片
		2、绘制动画图片,在指定的(0,0)坐标处理绘制
		注意:核心方法的呼叫--呼叫Animation对象的getImage方法
		*/
	private void draw(Graphics g){
		//绘制背景
		g.drawImage(bgImage,0,0,null);
		//绘制动画--这是实现动画的关键方法
		g.drawImage(animation.getImage(),0,0,null);
	}
	
	public static void main(String[] args){
		new AnimationFrameTestOne(args);
	}
}

双缓存实现动画

import static java.lang.System.*;
import java.awt.*;
import javax.swing.*;

/**
	功能:书写一个使用双缓存技术实现的动画效果
	作者:技术大黍
	*/
public class AnimationTestTwo{
	
	public AnimationTestTwo(){
		run();
	}
	
	public static void main(String[] args){
		new AnimationTestTwo();
	}
	
	private static final DisplayMode POSSIBLE_MODES[] = {
		new DisplayMode(1280,800,32,0),
		new DisplayMode(1280,800,24,0),
		new DisplayMode(1280,800,16,0),
		new DisplayMode(1024,768,32,0),
		new DisplayMode(1024,768,24,0),
		new DisplayMode(1024,768,16,0),
		new DisplayMode(800,600,32,0),
		new DisplayMode(800,600,24,0),
		new DisplayMode(800,600,16,0)
	};
	
	private static final long DEMO_TIME = 10000;
	private ScreenManager screen;
	private Image bgImage;
	private Animation animation;
	
	public void loadImages(){
		//装载图片
		bgImage = loadImage("images/background.jpg");
		Image player1 = loadImage("images/player1.png");
		Image player2 = loadImage("images/player2.png");
		Image player3 = loadImage("images/player3.png");
		//创建动画
		animation = new Animation();
		animation.addFrame(player1,250);
		animation.addFrame(player2,150);
		animation.addFrame(player1,150);
		animation.addFrame(player2,150);
		animation.addFrame(player3,200);
		animation.addFrame(player2,150);
		/*
		animation = new Animation();
		Image player1 = loadImage("images/01.png");
		Image player2 = loadImage("images/02.png");
		Image player3 = loadImage("images/03.png");
		Image player4 = loadImage("images/04.png");
		Image player5 = loadImage("images/05.png");
		Image player6 = loadImage("images/06.png");
		
		animation.addFrame(player1,150);
		animation.addFrame(player2,150);
		animation.addFrame(player3,150);
		animation.addFrame(player4,150);
		animation.addFrame(player5,150);
		animation.addFrame(player6,150);*/
	}
	
	private Image loadImage(String fileName){
		return new ImageIcon(fileName).getImage();
	}
	
	private void run(){
		screen = new ScreenManager();
		try{
			DisplayMode displayMode = screen.findFirstCompatibleMode(POSSIBLE_MODES);
			//呼叫screen对象的setFullScreen方法是实现双缓存技术显示的关键代码!
			screen.setFullScreen(displayMode);
			loadImages();
			animationLoop();
		}finally{
			screen.restoreScreen();
		}
	}
	
	/*
		实现双缓存动画的核心方法。
		*/
	private void animationLoop(){
		long startTime = currentTimeMillis();
		long currentTime = startTime;
		while(currentTime - startTime < DEMO_TIME){
			long elapsedTime = currentTimeMillis() - currentTime;
			currentTime += elapsedTime;
			//更新动画--确定需要显示的图形
			animation.update(elapsedTime);
			//绘制和刷新屏幕--绘制图形在缓存对象中--关键是使用Graphics2D类来实现双缓存显示!
			Graphics2D g = screen.getGraphics();
			//在屏幕中绘制背景和动画--在屏幕中绘制出现(离屏绘制)
			draw(g);
			g.dispose();
			//然后在缓存中绘制图形--实现双缓存的关键代码--绘制到屏幕中
			screen.update();
			//停顿一下
			try{
				Thread.sleep(20);
			}catch(InterruptedException ex){
				ex.printStackTrace();
			}
		}
	}
	
	/**
		注意:g.drawImage(animation.getImage(),0,0,null)方法是在固定的坐标绘制双缓存的图形
			  所以只有动画效果,但是没有小怪的移动动作。
		*/
	public void draw(Graphics g){
		g.drawImage(bgImage, 0,0, null);//绘制背景
		g.drawImage(animation.getImage(),620,350,null);//绘制图片
	}
}

主动呈现

主动呈现(active rendering)是一个实现动画的术语。我们必须使用一种有效方式来不断刷新屏幕,从而产生动画。前面的例子是使用paint方法来呈现,我们呼叫repaint方法,向AWT事件分发线程发送信号,让它重新刷新屏幕,但是这种做法会产生延迟,因为AWT的线程可能会在忙于别的事情。而解决方法就是使用主动呈现的方式,该方式是在主线程中直接绘制图片!这样我们让屏幕直接绘制,并且代码简单:

Graphics g = screen.getFullScreenWindow().getGraphics();
draw(g); //主线程中直接绘制
g.dispose();

去掉闪烁

在AnimationFrameTestOne示例我们看动画闪烁—很烦!这是因为我们不断刷新屏幕,这样的结果是我们擦除背景,然后重新绘制它。这样的结果会有时候出现闪烁,有时候不出现闪烁现象。这个类似于,我们在屏幕使用笔画图,然后用户会看见怎么画图的全过程。

image-20210326172448859.png

我们使用双缓存(Double Buffering)技术解决这个问题。所谓buffer就是一个在内存中绘制图片,也就是当我们使用双缓存技术时,需要我们不能直接把图形画到屏幕中去,需要我们先画到buffer中,然后把整个画面拷贝到屏幕中去。这样整个屏幕只刷新一次。

image-20210326172515910.png

  • back buffer--备份缓存
  • copy--复制
  • screen--屏幕呈现

Back buffer可以是普通的Java图形,所以我们使用Component的createImage()方法来创建缓存图片,然后在update方法中呼叫paint方法呈现:

image-20210326172823110.png

完整代码参见ScreenManger类。

ScreenManager类

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

/**
	功能:书写一个屏幕管理器,用来初始化和全屏幕显示功能
	作者:技术大黍
	*/
public class ScreenManager{
	private GraphicsDevice device; //指定显卡
	
	/**
		在构造方法中初始化成员变量, 其中GraphicsEnvironment抽象类,它是抽象了平台上可以使用的
		GraphicsDevice对象和Font对象的集合。该对象的中的资源可以本地资源,也可以远程机器上的。
		GraphicsDevice对象可以屏幕、打印机或图像缓存,并且都Graphics2D绘图方法的目标。每个
		GraphicsDevice有许多与之相关的GraphicsConfiguration对象,这些对象指定使用GraphicsDevice所
		需的不同配置。
		*/
	public ScreenManager(){
		//初始化显示设置环境对象
		GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
		//然后从该环境显示确定的显卡
		device = environment.getDefaultScreenDevice();
	}
	
	/**
		功能:获取当前的所有的显示模型
		*/
	public DisplayMode[] getCompatibleDsiplayModes(){
		return device.getDisplayModes();
	}
	
	public DisplayMode findFirstCompatibleMode(DisplayMode[] modes){
		DisplayMode[] goodModes = device.getDisplayModes();
		for(int i = 0; i < modes.length; i++){
			for(int j = 0; j < goodModes.length; j++){
				if(displayModesMatch(modes[i],goodModes[j])){
					return modes[i];
				}
			}
		}
		return null; //否则找不到时返回null
	}
	
	/**
		功能:比较两个模型是否相同
		*/
	public boolean displayModesMatch(DisplayMode mode1, DisplayMode mode2){
		if(mode1.getWidth() != mode2.getWidth() ||
		   mode1.getHeight() != mode2.getHeight()){
			return false;
		}
		
		if(mode1.getBitDepth() != DisplayMode.BIT_DEPTH_MULTI &&
		   mode2.getBitDepth() != DisplayMode.BIT_DEPTH_MULTI &&
		   mode1.getBitDepth() != mode2.getBitDepth()){
			return false;
		}
		
		if(mode1.getRefreshRate() != 
		   DisplayMode.REFRESH_RATE_UNKNOWN &&
		   mode2.getRefreshRate() !=
		   DisplayMode.REFRESH_RATE_UNKNOWN &&
		   mode1.getRefreshRate() != mode2.getRefreshRate()){
			return false;
		}
		
		//否则返回true
		return true;
	}
	
	/**
		功能:输入全屏幕模型,然后修改显示模型,如果指定的显示模型是null或者不兼容,那么
			  使用当前的显示模型。显示时使用BufferStratgey的双缓存技术实现。
		*/
	public void setFullScreen(DisplayMode displayMode){
		JFrame frame = new JFrame();
		frame.setUndecorated(true);//不显示标题栏
		frame.setIgnoreRepaint(true); //能呼叫repain方法
		frame.setResizable(false); //不允许缩放
		//呼叫显卡设置全屏幕
		device.setFullScreenWindow(frame);
		//如果有确定的显示模型
		if(displayMode != null && device.isDisplayChangeSupported()){
			//那么设置该显示模型
			try{
				device.setDisplayMode(displayMode);
			}catch(IllegalArgumentException ex){
				ex.printStackTrace();
			}
		}
		
		//使用双缓存技术显示全屏幕--这是关键代码。该方法为被呼叫的组件创建一个新的缓冲
		//策略--双缓冲策略。该方法中根据提供的缓冲区数来创建可用的最佳策略。它将始终根据
		//该缓冲区数创建BufferStrategy--首先浓度page-flipping策略,然后是使用加速缓冲区
		//blitting策略,最后使用不加速的blitting策略。Bit BLIT (which stands for bit-block [image] 
		//transfer but is pronounced bit blit) is a computer graphics operation in which 
		//several bitmaps are combined into one using a raster operator.
		//必须首先在这里设置device显示模型为双缓存方式,然后参见getGraphics()方法中的
		//BufferStrategy strategy = window.getBufferStrategy()语句在屏幕绘制使用双缓存
		//技术显示
		frame.createBufferStrategy(2);
	}
	
	/**
		功能:取得图形上下文来进行显示,ScreenManager使用双缓存实现,所以应用程序必须
			  呼叫update()方法来显示任何绘制的图形。然后应用程序必须释放图形对象。
		*/
	public Graphics2D getGraphics(){
		Window window = device.getFullScreenWindow(); //从显卡取得全屏幕的窗体对象
		//如果该对象不为null
		if(window != null){
			//取得在setFullScreen方法设置的双缓存设置策略,然后初始化BufferStrategy对象
			BufferStrategy strategy = window.getBufferStrategy(); 
			//最后把图形绘制在Graphics2D对象中,参见AnimationTestTwo类中的animationLoop()
			//方法中的Graphics2D g = screen.getGraphics();语句。
			return (Graphics2D)strategy.getDrawGraphics();
		}else{
			return null; //否则返回null
		}
	}
	
	
	/**
		功能:更新显示--使用双缓存策略来显示。
		实现双缓存最关键的对象不device成员变量!!
		*/
	public void update(){
		Window window = device.getFullScreenWindow(); //从显卡取得窗体对象
		//如果该窗体不为null
		if(window != null){
			//那么使用双缓存策略对象处理
			BufferStrategy strategy = window.getBufferStrategy();
			//如果内容没有丢失
			if(!strategy.contentsLost()){
				//那么在屏幕上显出来--此处代码是在屏幕中显示的核心关键代码!
				//屏蔽该语句之后,不会在屏幕中显示出来画面来--black一片。
				strategy.show();
			}
		}
		//否则在系统同步显示出现--下面这句话是fix Linux中的事件阶段问题
		Toolkit.getDefaultToolkit().sync();
	}
	
	/**
		功能:返回当前全屏幕的当前窗体,如果没有全屏幕模型,那么返回null
		*/
	public Window getFullScreenWindow(){
		return device.getFullScreenWindow(); //返回全屏幕对象窗体
	}
	
	/**
		功能:返回当前全屏幕的宽度值。如果没有不全屏幕,那么回返0
		*/
	public int getWidth(){
		Window window = device.getFullScreenWindow();//从显卡取得窗体对象
		//如果该窗体对象不为null
		if(window != null){
			return window.getWidth(); //那么返回该窗体对象的宽度
		}else{
			return 0; //否则返回0
		}
	}
	
	/**
		功能:返回当前全屏幕的高度值。如果没有不全屏幕,那么回返0
		*/
	public int getHeight(){
		Window window = device.getFullScreenWindow();//从显卡取得窗体对象
		//如果该窗体对象不为null
		if(window != null){
			return window.getHeight(); //那么返回该窗体对象的高度
		}else{
			return 0; //否则返回0
		}
	}
	
	
	public void restoreScreen(){
		Window window = device.getFullScreenWindow();//从显卡取得窗体对象
		//如果该窗体对象不为null
		if(window != null){
			window.dispose(); //那么翻译该资源
		}
		//然后取消全屏幕显示
		device.setFullScreenWindow(null);
	}
	
	public BufferedImage createCompatibleImage(int w, int h, int transparency){
		Window window = device.getFullScreenWindow();//从显卡取得窗体对象
		//如果该窗体不为null
		if(window != null){
			//那么取得图形的配置对象
			GraphicsConfiguration configuration = window.getGraphicsConfiguration();
			//然后返回兼容的图形
			return configuration.createCompatibleImage(w,h,transparency);
		}
		return null; //否则返回null
	}
}

使用BufferStrategy类(源码追踪)

该类是一个抽象类,它用来表示特定的Canvas或者Window上组织复杂的内存机制。硬件与软件限制了决定是否能够使用特定的缓存策略,以及它如何实现。从创建Canvas和Window对象所使用GraphicsConfiguration的性能可以发现这些限制的存在。

注意:术语buffer与surface的相同:视频设置内存或系统内存的连续内存区域。在实际开发中,双缓存、分页和等待显示器重新刷新都是使用该类来实现。总之一句话,该类帮助我们完成这些物理上的动作。使用显示器刷新的缺点是,如果显示器的刷新频率是75HZ,也就是每秒75个窗体画面,那么我们不可能运行每秒200个画面的游戏了。类Canvas和类Window对象都有BufferStrategy可以使用,并且使用createBufferStrategy()方法创建该策略对象。

image-20210330112450296.png

我们源代码追踪一下:

frame类继承Window类,而Window类继承自Container容器类,并且实现Accessible接口

image-20210330112642353.png 在createBufferStrategy方法中吃了super类的createBufferStrategy方法

image-20210330112804558.png 我们再进行源代码追踪,在Component类的3837行

image-20210330113031860.png 该方法完整的代码我们扣出来展示如下:

    void createBufferStrategy(int numBuffers) {
        BufferCapabilities bufferCaps;
        if (numBuffers > 1) {
            // Try to create a page-flipping strategy
            bufferCaps = new BufferCapabilities(new ImageCapabilities(true),
                                                new ImageCapabilities(true),
                                                BufferCapabilities.FlipContents.UNDEFINED);
            try {
                createBufferStrategy(numBuffers, bufferCaps);
                return; // Success
            } catch (AWTException e) {
                // Failed
            }
        }
        // Try a blitting (but still accelerated) strategy
        bufferCaps = new BufferCapabilities(new ImageCapabilities(true),
                                            new ImageCapabilities(true),
                                            null);
        try {
            createBufferStrategy(numBuffers, bufferCaps);
            return; // Success
        } catch (AWTException e) {
            // Failed
        }
        // Try an unaccelerated blitting strategy
        bufferCaps = new BufferCapabilities(new ImageCapabilities(false),
                                            new ImageCapabilities(false),
                                            null);
        try {
            createBufferStrategy(numBuffers, bufferCaps);
            return; // Success
        } catch (AWTException e) {
            // Code should never reach here (an unaccelerated blitting
            // strategy should always work)
            throw new InternalError("Could not create a buffer strategy", e);
        }
    }

我们看到Java开源组织书写了一大堆的封装类,根据策略模式实现了底层的双缓存机制,并且提供给我们第三方开发人员快捷的使用--我们只是简单的使用Frame.createBufferStrategy方法来调用BufferStrategy策略对象就完成了我们想的实现双缓存效果,从而加快了开发效率。

创建屏幕管理器

下面我们改进SimpleScreenManager类,新增一些功能:

  1. 使用BufferStrategy来实现双缓存技术和分页刷新
  2. 使用getGraphics()方法获取显示的图片上下文
  3. 使用update()来更新显示
  4. 使用getCompatibleDisplayModes()来获取可兼容的显示模型
  5. 使用getCurrentDisplayMode()来获取当前的显示模型
  6. 使用findFirstCompatibleMode()方法来获取模型列表中的第一个显示模型

如果不需要主动呈现,那么没有必须给JFrame使用全屏幕显示,这时需要我们关闭它 frame.ignoreRepaint(true); 但是,它不会关闭repaint事件,所以可以使用paint事件。

下面我们列出AnimationTestTwo类,以帮助我们理解

AnimationTestTwo类

该类与ScreenManger类一起配合使用

import static java.lang.System.*;
import java.awt.*;
import javax.swing.*;

/**
	功能:书写一个使用双缓存技术实现的动画效果
	作者:技术大黍
	*/
public class AnimationTestTwo{
	
	public AnimationTestTwo(){
		run();
	}
	
	public static void main(String[] args){
		new AnimationTestTwo();
	}
	
	private static final DisplayMode POSSIBLE_MODES[] = {
		new DisplayMode(1280,800,32,0),
		new DisplayMode(1280,800,24,0),
		new DisplayMode(1280,800,16,0),
		new DisplayMode(1024,768,32,0),
		new DisplayMode(1024,768,24,0),
		new DisplayMode(1024,768,16,0),
		new DisplayMode(800,600,32,0),
		new DisplayMode(800,600,24,0),
		new DisplayMode(800,600,16,0)
	};
	
	private static final long DEMO_TIME = 10000;
	private ScreenManager screen;
	private Image bgImage;
	private Animation animation;
	
	public void loadImages(){
		//装载图片
		bgImage = loadImage("images/background.jpg");
		Image player1 = loadImage("images/player1.png");
		Image player2 = loadImage("images/player2.png");
		Image player3 = loadImage("images/player3.png");
		//创建动画
		animation = new Animation();
		animation.addFrame(player1,250);
		animation.addFrame(player2,150);
		animation.addFrame(player1,150);
		animation.addFrame(player2,150);
		animation.addFrame(player3,200);
		animation.addFrame(player2,150);
		/*
		animation = new Animation();
		Image player1 = loadImage("images/01.png");
		Image player2 = loadImage("images/02.png");
		Image player3 = loadImage("images/03.png");
		Image player4 = loadImage("images/04.png");
		Image player5 = loadImage("images/05.png");
		Image player6 = loadImage("images/06.png");
		
		animation.addFrame(player1,150);
		animation.addFrame(player2,150);
		animation.addFrame(player3,150);
		animation.addFrame(player4,150);
		animation.addFrame(player5,150);
		animation.addFrame(player6,150);*/
	}
	
	private Image loadImage(String fileName){
		return new ImageIcon(fileName).getImage();
	}
	
	private void run(){
		screen = new ScreenManager();
		try{
			DisplayMode displayMode = screen.findFirstCompatibleMode(POSSIBLE_MODES);
			//呼叫screen对象的setFullScreen方法是实现双缓存技术显示的关键代码!
			screen.setFullScreen(displayMode);
			loadImages();
			animationLoop();
		}finally{
			screen.restoreScreen();
		}
	}
	
	/*
		实现双缓存动画的核心方法。
		*/
	private void animationLoop(){
		long startTime = currentTimeMillis();
		long currentTime = startTime;
		while(currentTime - startTime < DEMO_TIME){
			long elapsedTime = currentTimeMillis() - currentTime;
			currentTime += elapsedTime;
			//更新动画--确定需要显示的图形
			animation.update(elapsedTime);
			//绘制和刷新屏幕--绘制图形在缓存对象中--关键是使用Graphics2D类来实现双缓存显示!
			Graphics2D g = screen.getGraphics();
			//在屏幕中绘制背景和动画--在屏幕中绘制出现(离屏绘制)
			draw(g);
			g.dispose();
			//然后在缓存中绘制图形--实现双缓存的关键代码--绘制到屏幕中
			screen.update();
			//停顿一下
			try{
				Thread.sleep(20);
			}catch(InterruptedException ex){
				ex.printStackTrace();
			}
		}
	}
	
	/**
		注意:g.drawImage(animation.getImage(),0,0,null)方法是在固定的坐标绘制双缓存的图形
			  所以只有动画效果,但是没有小怪的移动动作。
		*/
	public void draw(Graphics g){
		g.drawImage(bgImage, 0,0, null);//绘制背景
		g.drawImage(animation.getImage(),620,350,null);//绘制图片
	}
}

屏幕管理效果

image-20210330113914953.png

妖怪(sprite)

现在使用双缓存技术画面运行流畅,但是没有其它的动画出现,所以我们需要需要创建一个妖怪在屏幕中运动。因为妖怪也是一个图片,只不过它是独立在屏幕中的,所以该妖怪也是一个动画效果,并且它可以一边动画一边移动。

现在,因为动画问题已经解决,那么我们需要解决妖怪移动的问题—也就是一个怪由两个事情组成:位置和速率(velocity)。速率是速度和方向的组成,这样我们把速度分成水平和垂直两个方向,我们是每秒多少像素来计算移动速度。可能我们会问:“为什么不通过更新多个frame中的怪物的位置来实现动画,而非得使用速率?”如果,这样做,那么这个怪物在不同的机器上移动的速度就会不一样!性能好的机器上的怪物运行比较快,而性能慢的机器上的怪物运行比较慢。而怪物的动画我们使用主动呈现的技术来实现。

注意,我们把妖怪的位置值使用浮点来计算,而不是整数,这是因为如果使用整数,那么每隔10毫秒更新时,有一毫秒的时间图片不会移动。该类非常简单,只有getter和setter方法,以及update方法更新动画与移动位置。

Sprite类

import static java.lang.System.*;
import java.awt.Image;

/**
	功能:书写一个妖怪类用来演示游戏中人物角色的动画实现方式
	作者:技术大黍
	备注:
		  妖怪由两个部分组成:动画和移动效果
	*/
public class Sprite{
	//使用动画管理器来管理妖怪的动画效果--用Animation来封装妖怪的图形的样子。
	private Animation animation;
	//设置妖怪的显示位置--实现妖怪的移动需要两种元素:当前妖怪的位置和它的移动速率。
	private float x;
	private float y;
	//计算妖怪的速率
	private float dx;
	private float dy;
	
	public Sprite(Animation animation){
		this.animation = animation;
	}
	
	/**
		功能:更新该妖怪的动画和位置,更新时根据它的速率来计算。
			  该方法是妖怪移动的核心方法
		*/
	public void update(long elapsedTime){
		//根据流失的时间值重新计算妖怪图片的x和y的值
		x += dx * elapsedTime;
		y += dy * elapsedTime;
		//根据流失的时间值来控制动画效果
		animation.update(elapsedTime);
	}
	
	public float getX(){
		return x;
	}
	
	public float getY(){
		return y;
	}
	
	public void setX(float x){
		this.x = x;
	}
	
	public void setY(float y){
		this.y = y;
	}
	
	/**
		获取该图片的宽度
		*/
	public float getWidth(){
		return animation.getImage().getWidth(null);
	}
	
	/**
		获取该图片的高度
		*/
	public float getHeight(){
		return animation.getImage().getHeight(null);
	}
	
	public float getVelocityX(){
		return dx;
	}
	
	public float getVelocityY(){
		return dy;
	}
	
	public void setVelocityX(float dx){
		this.dx = dx;
	}
	
	public void setVelocityY(float dy){
		this.dy = dy;
	}
	
	public Image getImage(){
		return animation.getImage();
	}
}

SprinteTest类

import static java.lang.System.*;
import java.awt.*;
import javax.swing.ImageIcon;
/**
	功能:书写一个测试类,用来测试妖怪的动画效果
	作者:技术大黍
	*/

public class SpriteTest{
	private ScreenManager screen;
	private Image bgImage;
	private Sprite sprite;
	private static final long DEMO_TIME = 20000;
	private Animation animation;
	
	private static final DisplayMode POSSIBLE_MODES[] = {
		new DisplayMode(1280,800,32,0),
		new DisplayMode(1280,800,24,0),
		new DisplayMode(1280,800,16,0),
		new DisplayMode(1024,768,32,0),
		new DisplayMode(1024,768,24,0),
		new DisplayMode(1024,768,16,0),
		new DisplayMode(800,600,32,0),
		new DisplayMode(800,600,24,0),
		new DisplayMode(800,600,16,0)
	};
	
	public SpriteTest(){
		run();
	}
	
	/**
		这里妖怪移动的核心实现方法
		*/
	public void loadImages(){
		//装载图片
		bgImage = loadImage("images/background.jpg");
		Image player1 = loadImage("images/player1.png");
		Image player2 = loadImage("images/player2.png");
		Image player3 = loadImage("images/player3.png");
		//1.创建动画--装载背景画面
		animation = new Animation();
		animation.addFrame(player1,250);//其中250表示该图片的显示结束时间
		animation.addFrame(player2,150);
		animation.addFrame(player1,150);
		animation.addFrame(player2,150);
		animation.addFrame(player3,200);
		animation.addFrame(player2,150);
		//2.装载妖怪图片
		sprite = new Sprite(animation);
		//3.开始移动妖怪
		sprite.setVelocityX(0.3f);
		sprite.setVelocityY(0.3f);
	}
	
	
	private Image loadImage(String fileName){
		return new ImageIcon(fileName).getImage();
	}
	
	private void run(){
		screen = new ScreenManager();
		try{
			DisplayMode displayMode = screen.findFirstCompatibleMode(POSSIBLE_MODES);
			screen.setFullScreen(displayMode);
			loadImages();
			animationLoop();
		}finally{
			screen.restoreScreen();
		}
	}
	
	/*
		实现动画最关键的如何获取当前时间与肇始启始时间的差值(elapsed time),然后把这个赋给需要被实现
		动画的对象去计算移动的位置。
		*/
	private void animationLoop(){
		long startTime = currentTimeMillis();
		long currentTime = startTime;
		while(currentTime - startTime < DEMO_TIME){
			//计算当前时间与启始时间之间差值
			long elpasedTime = currentTimeMillis() - currentTime;
			//根据流失的时间值更新当前时间
			currentTime += elpasedTime;
			//更新怪物--根据流失时间来移动妖怪
			update(elpasedTime);//1.第一个步骤计算小怪的当前坐标值
			//在屏幕中绘制妖怪
			Graphics2D g = screen.getGraphics();
			draw(g); //2. 根据当前小怪的坐标值来实现小怪移动(动态绘制在内存中)
			g.dispose();
			//然后刷新屏幕画面
			screen.update();//3. 使用双缓存计算来显示图形(最后一次性显示在屏幕中)
			try{
				Thread.sleep(20);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
	}
	
	/*
		实现动画与移动的核心方法,计算当前小怪的坐标值
		*/
	private void update(long elapsedTime){
		if(sprite.getX() < 0){//如果妖怪的x坐标小于0
			sprite.setVelocityX(Math.abs(sprite.getVelocityX()));//那么让速率
		}else if(sprite.getX() + sprite.getWidth() >= screen.getWidth()){
			sprite.setVelocityX(-Math.abs(sprite.getVelocityX()));
		}
		
		if(sprite.getY() < 0){
			sprite.setVelocityY(Math.abs(sprite.getVelocityY()));
		}else if(sprite.getY() + sprite.getHeight() >= screen.getHeight()){
			sprite.setVelocityY(-Math.abs(sprite.getVelocityY()));
		}
		//更新妖怪--更新妖怪图片的坐标值
		sprite.update(elapsedTime);
	}
	
	/**
		注意:g.drawImage(sprite.getImage(),Math.round(sprite.getX()),Math.round(sprite.getY()),null);
			  方法,它是让小怪移动的关键方法。这里与AnimationTestTwo类的publi void draw()方法的主要区别。
		*/
	public void draw(Graphics g){
		g.drawImage(bgImage,0,0,null);
		g.drawImage(sprite.getImage(),
			Math.round(sprite.getX()),
			Math.round(sprite.getY()),
			null);
	}
	
	public static void main(String[] args){
		new SpriteTest();
	}
}

运行效果

在VS Code的运行效果如下:

image-20210330114447402.png

简单特效—图片变换

我们可以使用sprite类来创建不同的小妖,只需要以下几段代码变可以实现:

for(int i = 0; i < sprites.length; i++){
	sprites[i].update(elapsedTime);
	g.drawImage(sprites[i].getImage(),
		Math.round(sprite[i].getX()),
		Math.round(sprite[i].getY()),
		null);
}

注意:每个sprite只更新自己的动画效果,所以sprites不可能共享相同的Animation对象;而动画对象可以多次被更新。如果我们需要相同的动画对象,那么可以使用clone方法来复制后得到。

比较cool的图片特效是图片的旋转和缩放,这种效果我们叫做图片的转换(image transform)。图片转换可以让我们旋转、上下翻转、缩放图片,甚至飞行图片。在Java中有AffineTransform类可以描述图片的转换,该类提供了rotate(), scale()和translate()方法来实现图片的置换。Graphics2D对象是图片实际转换的地方,该类使用drawImage方法带一个AffineTransform对象可以实现图片的转换。比如

    AffineTransform transform = new AffineTransform();
    transform.scale(2,2);
    transform.translate(100,100);
    g.drawImage(image, transform, null);

下面我们演示图片的转换效果。

image-20210330115358518.png

总结

我们到书写可以重用的类:ScreenManager, Animation和Sprite类。

这些类不可能是最好的类—实现动画的基类,对于我们设计的游戏来说,应该根据自己的观点来设计工具类,不一定按照以上三类的限制来书写代码。

Java的标准API把复杂的底层渲染呈现机制给深度封装了,我们第三方的Java程序员只需要读懂封装的过程和代码就行了,然后就是愉快使用它们来帮助我们快速开发游戏。

如果大家有兴趣,那么希望大家能够使用这些代码和游戏理论,自己灵活运用,编写自己的游戏代码。并请大家继续参看下一篇“Java游戏编程不完全详解-3”。

最后

记得给大黍❤️关注+点赞+收藏+评论+转发❤️

作者:老九学堂—技术大黍

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值